Menemukan Garis Batas: Deteksi Tepi dan Segmentasi Warna
Sobel, Canny, dan HSV dengan Python
ALIMIN, M.Kom
Dasar
01Tepi adalah lompatan angkaPerubahan kecerahan yang mendadak
Di mana tepi daun itu? Kita menunjuknya dengan mata. Komputer menunjuknya dengan selisih angka.
Satu baris piksel dari tepi daun (nilai kecerahan 0 sampai 255):
| 61 | 60 | 60 | 59 | 60 | 72 | 102 | 128 | 138 | 139 |
Selisih antar piksel bertetangga: -1, +0, -1, +1, +12, +30, +26, +10, +1. Lompatan terbesar (+30) terjadi di antara 72 dan 102. Di situlah tepi berada.
02Mengapa deteksi tepi dibutuhkan?Dari kontur objek sampai kendaraan otonom
Tepi membuang detail yang tidak perlu dan menyisakan bentuk. Bentuk itulah yang dipakai komputer untuk mengenali objek.
- Industri: mengukur dimensi dan mencari retak pada produk.
- Medis: memisahkan batas organ atau tumor pada citra rontgen.
- Kendaraan: mengenali garis marka jalan.
- Dokumen: menemukan batas halaman dan baris teks sebelum dibaca OCR.
03Gradien: ukuran kecuramanBesar dan arah perubahan kecerahan
Gradien mengukur seberapa curam perubahan kecerahan di satu piksel. Kita hitung dua arah, lalu digabung:
Gy = perubahan ke arah tegak
G = akar(Gx² + Gy²)
Semakin besar G, semakin kuat tepinya. Di kode Python, ini ditulis ketika menghitung Sobel: np.sqrt(sobelx**2 + sobely**2).
Operator Tepi
04Sobel: dua kernel 3 x 3Satu mendeteksi tepi tegak, satu tepi mendatar
Sobel menggeser kernel kecil di atas citra. Pada tiap posisi, setiap piksel dikalikan bobot kernel, lalu semuanya dijumlahkan.
| -1 | 0 | 1 |
| -2 | 0 | 2 |
| -1 | 0 | 1 |
| -1 | -2 | -1 |
| 0 | 0 | 0 |
| 1 | 2 | 1 |
Bobot 2 di baris atau kolom tengah memberi perhatian lebih pada piksel yang paling dekat dengan pusat.
05Menghitung satu piksel dengan SobelContoh angka dari tepi daun
Ambil potongan 3 x 3 dari tepi daun:
| 70 | 93 | 122 |
| 86 | 116 | 135 |
| 108 | 132 | 139 |
Gx = (122 - 70) + 2(135 - 86) + (139 - 108) = 181
Gy = (108 + 2 x 132 + 139) - (70 + 2 x 93 + 122) = 133
G = akar(181² + 133²) = 225. Nilai setinggi ini tampil sebagai garis terang pada citra hasil.
06Canny: empat tahapTepi tipis dan bersih
Canny bukan sekadar satu kernel, tetapi rangkaian empat tahap:
- Haluskan dengan Gaussian blur agar noise berkurang.
- Hitung gradien (mirip Sobel).
- Tipiskan tepi: hanya piksel dengan gradien paling besar di sepanjang arah tepi yang dipertahankan.
- Hysteresis: dua ambang. Di atas ambang tinggi pasti tepi. Di bawah ambang rendah dibuang. Di antaranya dipertahankan hanya jika tersambung ke tepi kuat.
Pada kode praktik: cv2.Canny(gray, th_val, th_val * 2), ambang atas dibuat dua kali ambang bawah.
07Sobel melawan CannyPerbandingan pada citra daun yang sama
| Aspek | Sobel | Canny |
|---|---|---|
| Tebal garis | Tebal | Tipis (1 piksel) |
| Noise | Mudah ikut terdeteksi | Diredam oleh blur dan hysteresis |
| Parameter | Hampir tidak ada | Dua ambang |
| Kecepatan | Sangat cepat | Sedikit lebih berat |
Segmentasi Warna HSV
08Mengapa HSV, bukan RGB?Memisahkan warna dari terang gelap
Pada RGB, hijau yang terang dan hijau yang gelap punya angka R, G, B yang sangat berbeda. Pada HSV, warna dan terang dipisah:
| Kanal | Arti | Rentang di OpenCV |
|---|---|---|
| H (Hue) | Jenis warna | 0 sampai 179 |
| S (Saturation) | Kepekatan warna | 0 sampai 255 |
| V (Value) | Terang gelap | 0 sampai 255 |
Titik acuan Hue di OpenCV: merah 0, kuning 30, hijau 60, cyan 90, biru 120, magenta 150.
09Segmentasi warna hijauMask lalu bitwise_and
Rentang yang dipakai pada kode: bawah [25, 40, 40], atas [85, 255, 255]. Piksel di dalam rentang menjadi putih pada mask, lainnya hitam.
Nilai S dan V minimum (40) membuang piksel abu-abu atau terlalu gelap yang Hue-nya tidak bermakna.
10Memilih metode yang tepatRangkuman Pertemuan 6 dan 7
| Metode | Dasar kerja | Cocok untuk |
|---|---|---|
| Manual threshold | Satu T pilihan sendiri | Pencahayaan seragam |
| Otsu | T otomatis dari histogram | Objek dan latar jelas berbeda |
| Adaptive Gaussian | T tiap area | Dokumen berbayangan |
| Sobel | Gradien | Kekuatan tepi dengan cepat |
| Canny | Gradien, tipis, hysteresis | Garis batas bersih |
| HSV inRange | Rentang warna | Objek dengan warna khas |
Praktik & Penutup
11Source code praktik: segmentasi_citra.pyKode Python lengkap, blok semua lalu salin
Instal: pip install opencv-python numpy pillow lalu jalankan python segmentasi_citra.py. Pilih metode Sobel, Canny, atau Segmentasi Hijau (HSV) pada menu dropdown. Kode ini sama dengan Pertemuan 6 dan dipakai untuk bereksperimen sendiri, bukan tugas.
import tkinter as tk
from tkinter import filedialog, ttk
import cv2
import numpy as np
from PIL import Image, ImageTk
class SegmentationApp:
def __init__(self, root):
self.root = root
self.root.title("Pertemuan 6 & 7: Segmentasi Citra & Deteksi Tepi")
self.root.geometry("1000x650")
self.root.configure(bg="#f8fafc")
self.cv_img = None
self.setup_ui()
def setup_ui(self):
hdr = tk.Frame(self.root, bg="#b45309", pady=10)
hdr.pack(fill=tk.X)
tk.Label(hdr, text="P06-07: SEGMENTASI AMBANG (THRESHOLDING), DETEKSI TEPI & WARNA",
font=("Segoe UI", 12, "bold"), fg="white", bg="#b45309").pack()
ctrl = tk.Frame(self.root, bg="#f8fafc", pady=10)
ctrl.pack(fill=tk.X, padx=15)
tk.Button(ctrl, text="Load Citra", command=self.load_image, bg="#d97706", fg="white", padx=10).pack(side=tk.LEFT)
# DIPERBAIKI: nilai awal harus sama persis dengan salah satu isi daftar methods
self.method_var = tk.StringVar(value="Canny Edge Detector")
methods = ["Manual Threshold", "Otsu Thresholding", "Adaptive Gaussian", "Sobel Edge Detector", "Canny Edge Detector", "Segmentasi Hijau (HSV)"]
combo = ttk.Combobox(ctrl, textvariable=self.method_var, values=methods, state="readonly", width=24)
combo.pack(side=tk.LEFT, padx=10)
combo.bind("<<ComboboxSelected>>", self.apply_segmentation)
tk.Label(ctrl, text="Ambang/Thresh Slider:", bg="#f8fafc").pack(side=tk.LEFT, padx=(10, 2))
self.slider = tk.Scale(ctrl, from_=0, to=255, orient=tk.HORIZONTAL, command=self.apply_segmentation)
self.slider.set(128)
self.slider.pack(side=tk.LEFT, padx=5)
# Dual Panel
body = tk.Frame(self.root, bg="#f8fafc")
body.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)
f1 = tk.LabelFrame(body, text="Citra Asli", bg="#ffffff")
f1.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
self.lbl_orig = tk.Label(f1, text="Pilih citra", bg="#f1f5f9")
self.lbl_orig.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
f2 = tk.LabelFrame(body, text="Hasil Segmentasi / Kontur", bg="#ffffff")
f2.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)
self.lbl_res = tk.Label(f2, text="Belum diproses", bg="#f1f5f9")
self.lbl_res.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
def load_image(self):
p = filedialog.askopenfilename(filetypes=[("Image Files", "*.png *.jpg *.jpeg *.bmp")])
if not p:
return
self.cv_img = cv2.imread(p)
# DIPERBAIKI: batalkan jika file gagal dibaca (imread mengembalikan None)
if self.cv_img is None:
return
self.render(self.cv_img, self.lbl_orig)
self.apply_segmentation()
def apply_segmentation(self, _=None):
if self.cv_img is None:
return
method = self.method_var.get()
th_val = self.slider.get()
gray = cv2.cvtColor(self.cv_img, cv2.COLOR_BGR2GRAY)
if method == "Manual Threshold":
_, res = cv2.threshold(gray, th_val, 255, cv2.THRESH_BINARY)
res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR)
elif method == "Otsu Thresholding":
_, res = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR)
elif method == "Adaptive Gaussian":
res = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR)
elif method == "Sobel Edge Detector":
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
res = np.uint8(np.clip(np.sqrt(sobelx**2 + sobely**2), 0, 255))
res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR)
elif method == "Canny Edge Detector":
res = cv2.Canny(gray, th_val, th_val * 2)
res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR)
elif method == "Segmentasi Hijau (HSV)":
hsv = cv2.cvtColor(self.cv_img, cv2.COLOR_BGR2HSV)
lower_green = np.array([25, 40, 40])
upper_green = np.array([85, 255, 255])
mask = cv2.inRange(hsv, lower_green, upper_green)
res = cv2.bitwise_and(self.cv_img, self.cv_img, mask=mask)
else:
res = self.cv_img
self.render(res, self.lbl_res)
def render(self, cv_img, lbl):
rgb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
h, w, _ = rgb.shape
scale = min(440 / w, 450 / h)
disp_w = max(1, int(w * scale))
disp_h = max(1, int(h * scale))
resized = cv2.resize(rgb, (disp_w, disp_h))
img_tk = ImageTk.PhotoImage(image=Image.fromarray(resized))
lbl.config(image=img_tk, text="")
lbl.image = img_tk
if __name__ == "__main__":
r = tk.Tk()
app = SegmentationApp(r)
r.mainloop()
12Jembatan ke tugas UTSPertemuan 6 dan 7 tanpa praktikum
Dua pertemuan ini tidak ada tugas praktikum. Fokus Anda adalah tugas UTS:
- Tiap kelompok mencari 3 paper atau jurnal dengan topik berbeda.
- Review ketiganya menjadi satu paper baru dengan kolom Research Problem, Low/Mid/High Level, dan Objective.
- Buat slide, lalu unggah presentasi ke YouTube.
13Satu kalimat untuk dibawa pulangDan satu ajakan
Tepi adalah tempat angka berubah mendadak, dan warna adalah rentang yang bisa kita tentukan sendiri.
Ajakan: buka kode praktik, jalankan dengan satu foto milik Anda, lalu bandingkan Sobel dan Canny. Catat satu perbedaan yang Anda lihat.