Menemukan Garis Batas: Deteksi Tepi dan Segmentasi Warna

Menemukan Garis Batas: Deteksi Tepi dan Segmentasi Warna
Pengolahan Citra Digital · Pertemuan 7

Menemukan Garis Batas: Deteksi Tepi dan Segmentasi Warna

Sobel, Canny, dan HSV dengan Python

ALIMIN, M.Kom

Citra asli
Citra asli
Hasil Canny
Hasil Canny

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.

Perumpamaan: tepi adalah tebing di peta kontur. Dataran rata berarti selisih kecil. Tebing curam berarti selisih besar.
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:

Gx = perubahan ke arah mendatar
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.

Gx (tepi tegak)
-1 0 1
-2 0 2
-1 0 1
Gy (tepi mendatar)
-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:

  1. Haluskan dengan Gaussian blur agar noise berkurang.
  2. Hitung gradien (mirip Sobel).
  3. Tipiskan tepi: hanya piksel dengan gradien paling besar di sepanjang arah tepi yang dipertahankan.
  4. 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.

Perumpamaan: Sobel menandai semua tebing, termasuk gundukan kecil. Canny menandai hanya tebing yang benar-benar tersambung membentuk garis batas.
07Sobel melawan CannyPerbandingan pada citra daun yang sama
Citra asli
Citra asli
Sobel: tepi tebal, noise latar ikut muncul
Sobel: tepi tebal, noise latar ikut muncul
Canny: garis tipis dan bersih
Canny: garis tipis dan bersih
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.

Perumpamaan: mencari baju hijau di ruangan. Di RGB kita harus mengingat setiap tingkat terang. Di HSV cukup bilang: "cari yang Hue-nya hijau".
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.

Citra asli
Citra asli
Mask inRange
Mask inRange
Hasil bitwise_and
Hasil bitwise_and

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.
Metode yang baru kita pelajari (thresholding, Sobel, Canny, HSV) bisa menjadi pintu masuk untuk menentukan topik jurnal.
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.

Klik judul kartu untuk membuka atau menutup materi.