Jawaban UTS Pengolahan Citra Digital Kelompok I

Soal 1: selisih Laplacian of Gaussian (sigma 0,1 dan 0,2).
Soal 2: memecah teks menjadi karakter individual dengan regionprops.
Soal 3: menghitung jumlah koin per ukuran dengan regionprops.
This commit is contained in:
MelvynFaith
2026-10-22 23:43:56 +07:00
commit 003b2c544c
39 changed files with 1191 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
"""Membuat gambar uji koin_100.png dari koin.png.
Soal mengandaikan gambar berisi 100 koin berbagai ukuran yang tidak saling
bersentuhan di atas latar belakang seragam. koin.png hanya berisi satu koin,
jadi koin tersebut diperkecil ke beberapa ukuran lalu ditempel 100 kali pada
posisi acak di atas latar seragam. Setiap koin diputar acak dan seluruh gambar
diberi sedikit noise agar koin sejenis tidak identik piksel per piksel, seperti
pada foto asli. Jumlah koin per ukuran diketahui, sehingga hasil
pengolahan_citra.py dapat diperiksa.
Cara menjalankan:
python buat_gambar_koin.py
"""
import os
import cv2
import numpy as np
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SOURCE_PATH = os.path.join(BASE_DIR, "koin.png")
OUTPUT_PATH = os.path.join(BASE_DIR, "koin_100.png")
CANVAS_SIZE = 3000
BACKGROUND_BGR = (40, 40, 40) # latar abu-abu gelap yang seragam
MIN_GAP = 10 # jarak minimum antar-tepi koin (piksel), agar tidak bersentuhan
# (diameter koin dalam piksel, jumlah koin); total 100 koin
COIN_SIZES = [(140, 30), (170, 27), (205, 25), (245, 18)]
NOISE_STD = 3.0 # simpangan baku noise Gaussian (tingkat keabuan)
SEED = 0
def load_coin() -> np.ndarray:
coin = cv2.imread(SOURCE_PATH, cv2.IMREAD_UNCHANGED)
if coin is None or coin.shape[2] != 4:
raise FileNotFoundError(f"{SOURCE_PATH} harus berupa PNG dengan kanal alpha.")
# Potong ke area koin saja (alpha > 0)
rows, cols = np.nonzero(coin[:, :, 3])
return coin[rows.min():rows.max() + 1, cols.min():cols.max() + 1]
def main() -> None:
rng = np.random.default_rng(SEED)
coin = load_coin()
canvas = np.full((CANVAS_SIZE, CANVAS_SIZE, 3), BACKGROUND_BGR, np.float64)
diameters = [d for d, n in COIN_SIZES for _ in range(n)]
rng.shuffle(diameters)
# Tempatkan koin besar lebih dulu agar mudah mendapat tempat
placed = [] # (baris pusat, kolom pusat, jari-jari)
for d in sorted(diameters, reverse=True):
r = d / 2
for _ in range(100000):
y, x = rng.uniform(r + MIN_GAP, CANVAS_SIZE - r - MIN_GAP, size=2)
if all(np.hypot(y - py, x - px) >= r + pr + MIN_GAP for py, px, pr in placed):
placed.append((y, x, r))
break
else:
raise RuntimeError("Tidak cukup ruang untuk menempatkan semua koin.")
for y, x, r in placed:
d = round(2 * r)
small = cv2.resize(coin, (d, d), interpolation=cv2.INTER_AREA)
# Putar koin dengan sudut acak; area di luar koin tetap transparan
rotation = cv2.getRotationMatrix2D(((d - 1) / 2, (d - 1) / 2), rng.uniform(0, 360), 1.0)
small = cv2.warpAffine(small, rotation, (d, d), flags=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT, borderValue=(0, 0, 0, 0))
small = small.astype(np.float64)
top, left = round(y - r), round(x - r)
region = canvas[top:top + d, left:left + d]
# Alpha blending: tepi koin menyatu halus dengan latar
alpha = small[:, :, 3:4] / 255
region[:] = alpha * small[:, :, :3] + (1 - alpha) * region
canvas += rng.normal(0, NOISE_STD, canvas.shape)
cv2.imwrite(OUTPUT_PATH, np.clip(np.round(canvas), 0, 255).astype(np.uint8))
print(f"Gambar uji disimpan di: {OUTPUT_PATH}")
for d, n in COIN_SIZES:
print(f"Diameter {d} piksel: {n} koin")
if __name__ == "__main__":
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

@@ -0,0 +1,5 @@
Ukuran,Jumlah koin,Diameter rata-rata (piksel)
1,30,137.17
2,27,167.29
3,25,202.27
4,18,242.21
1 Ukuran Jumlah koin Diameter rata-rata (piksel)
2 1 30 137.17
3 2 27 167.29
4 3 25 202.27
5 4 18 242.21
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

+239
View File
@@ -0,0 +1,239 @@
"""Menghitung jumlah koin untuk setiap kelompok ukuran.
Pseudocode:
1. Baca gambar koin dan ubah menjadi grayscale.
2. Haluskan sedikit gambar, lalu lakukan threshold Otsu untuk memisahkan
koin dari latar belakang yang seragam.
3. Tentukan polaritas objek dari piksel tepi gambar; pastikan koin berwarna
putih pada mask.
4. Isi lubang di dalam setiap koin agar relief koin tidak memecah satu koin
menjadi beberapa komponen.
5. Beri label pada setiap komponen yang terpisah, lalu buang komponen yang
menyentuh tepi gambar (koin terpotong, ukurannya tidak utuh).
6. Gunakan regionprops untuk mengambil luas, luas convex hull, dan pusat
setiap komponen; abaikan komponen yang lebih kecil dari batas minimum.
Diameter koin = diameter lingkaran yang luasnya sama dengan convex hull.
7. Kelompokkan diameter yang cukup berdekatan sebagai ukuran koin yang sama.
8. Hitung dan tampilkan jumlah koin pada setiap kelompok, lalu simpan tabel
hasil dan gambar anotasi.
Cara menjalankan:
python pengolahan_citra.py [jalur_gambar_koin]
Jika argumen tidak diberikan, program membaca koin_100.png di folder skrip
(dibuat oleh buat_gambar_koin.py).
"""
import argparse
import csv
import os
from dataclasses import dataclass
import cv2
import numpy as np
from scipy.ndimage import binary_fill_holes
from skimage.measure import label, regionprops
from skimage.segmentation import clear_border
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_IMAGE_PATH = os.path.join(BASE_DIR, "koin_100.png")
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
# Sesuaikan dengan resolusi foto. Komponen lebih kecil dari nilai ini
# dianggap noise, bukan koin.
MIN_COIN_AREA = 100
# Dua diameter dikelompokkan sebagai ukuran yang sama jika selisihnya
# tidak lebih dari persentase ini terhadap diameter rata-rata kelompok.
# Nilai harus lebih besar dari variasi diameter koin sejenis, tetapi lebih
# kecil dari selisih antar-ukuran koin. Dengan 8%, ukuran yang berbeda ~7%
# ikut tergabung; 3% tetap memisahkannya.
SIZE_TOLERANCE_PERCENT = 3.0
EXPECTED_COIN_COUNT = 100
# Warna anotasi per kelompok ukuran (BGR), kontras terhadap koin dan latar
GROUP_COLORS = [
(0, 0, 255), # merah
(255, 128, 0), # biru
(0, 200, 0), # hijau
(255, 0, 255), # magenta
(0, 255, 255), # kuning
(255, 255, 0), # cyan
]
@dataclass(frozen=True)
class CoinMeasurement:
label: int
area: float
diameter_px: float
centroid: tuple[float, float]
def read_image(path: str) -> np.ndarray:
image = cv2.imread(path)
if image is None:
raise FileNotFoundError(
f"Gambar tidak dapat dibaca: {path}. "
"Sediakan gambar koin dan jalankan ulang program."
)
return image
def segment_coins(image_bgr: np.ndarray) -> np.ndarray:
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, mask = cv2.threshold(
blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
# Latar belakang diasumsikan terlihat pada mayoritas piksel tepi.
border = np.concatenate(
(mask[0, :], mask[-1, :], mask[:, 0], mask[:, -1])
)
if np.mean(border == 255) > 0.5:
mask = cv2.bitwise_not(mask)
# Relief/pantulan pada koin bisa menjadi lubang setelah threshold dan
# memecah satu koin menjadi beberapa komponen; isi lubang tersebut.
mask = binary_fill_holes(mask > 0).astype(np.uint8) * 255
return mask
def measure_coins(mask: np.ndarray) -> list[CoinMeasurement]:
# Koin yang menyentuh tepi gambar terpotong sehingga diameternya tidak
# utuh dan bisa masuk kelompok ukuran yang salah; buang komponen tersebut.
labeled_mask = clear_border(label(mask > 0, connectivity=2))
measurements = []
for region in regionprops(labeled_mask):
if region.area < MIN_COIN_AREA:
continue
measurements.append(
CoinMeasurement(
label=region.label,
area=float(region.area),
# Bagian koin yang gelap (bayangan di tepi) bisa terpotong saat
# threshold. Convex hull menutup potongan itu, sehingga diameter
# dihitung dari luas convex hull, bukan luas region.
diameter_px=float(np.sqrt(4 * region.area_convex / np.pi)),
centroid=(float(region.centroid[0]), float(region.centroid[1])),
)
)
return measurements
def group_by_size(
measurements: list[CoinMeasurement],
) -> list[list[CoinMeasurement]]:
groups: list[list[CoinMeasurement]] = []
for coin in sorted(measurements, key=lambda item: item.diameter_px):
matching_groups = []
for group in groups:
mean_diameter = float(np.mean([item.diameter_px for item in group]))
difference_percent = (
abs(coin.diameter_px - mean_diameter) / mean_diameter * 100
)
if difference_percent <= SIZE_TOLERANCE_PERCENT:
matching_groups.append((difference_percent, group))
if matching_groups:
_, closest_group = min(matching_groups, key=lambda item: item[0])
closest_group.append(coin)
else:
groups.append([coin])
return sorted(
groups,
key=lambda group: float(np.mean([coin.diameter_px for coin in group])),
)
def save_results(
image_bgr: np.ndarray,
mask: np.ndarray,
groups: list[list[CoinMeasurement]],
) -> None:
os.makedirs(OUTPUT_DIR, exist_ok=True)
annotated = image_bgr.copy()
csv_path = os.path.join(OUTPUT_DIR, "jumlah_koin_per_ukuran.csv")
with open(csv_path, "w", newline="", encoding="utf-8") as result_file:
writer = csv.writer(result_file)
writer.writerow(["Ukuran", "Jumlah koin", "Diameter rata-rata (piksel)"])
for size_number, group in enumerate(groups, start=1):
mean_diameter = float(np.mean([coin.diameter_px for coin in group]))
writer.writerow([size_number, len(group), f"{mean_diameter:.2f}"])
color = GROUP_COLORS[(size_number - 1) % len(GROUP_COLORS)]
for coin in group:
row, column = coin.centroid
center = (round(column), round(row))
radius = max(1, round(coin.diameter_px / 2))
# Tebal garis dan ukuran teks mengikuti ukuran koin agar
# tetap terbaca pada gambar beresolusi tinggi
thickness = max(2, round(coin.diameter_px / 40))
font_scale = coin.diameter_px / 60
text = str(size_number)
(text_w, text_h), _ = cv2.getTextSize(
text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness
)
text_origin = (center[0] - text_w // 2, center[1] + text_h // 2)
cv2.circle(annotated, center, radius, color, thickness)
cv2.putText(
annotated,
text,
text_origin,
cv2.FONT_HERSHEY_SIMPLEX,
font_scale,
color,
thickness,
cv2.LINE_AA,
)
cv2.imwrite(os.path.join(OUTPUT_DIR, "mask_koin.png"), mask)
cv2.imwrite(os.path.join(OUTPUT_DIR, "anotasi_koin.jpg"), annotated)
print(f"Tabel hasil disimpan di: {csv_path}")
print(f"Mask segmentasi dan anotasi disimpan di: {OUTPUT_DIR}")
def main() -> None:
parser = argparse.ArgumentParser(
description="Menghitung jumlah koin untuk setiap kelompok ukuran."
)
parser.add_argument(
"image",
nargs="?",
default=DEFAULT_IMAGE_PATH,
help="jalur gambar koin (default: koin.jpg di folder skrip)",
)
args = parser.parse_args()
image = read_image(args.image)
mask = segment_coins(image)
measurements = measure_coins(mask)
groups = group_by_size(measurements)
print(f"Jumlah koin terdeteksi: {len(measurements)}")
if len(measurements) != EXPECTED_COIN_COUNT:
print(
f"Perhatian: jumlah terdeteksi berbeda dari asumsi "
f"{EXPECTED_COIN_COUNT} koin. Periksa mask dan MIN_COIN_AREA."
)
print("\nHasil pengelompokan ukuran:")
print("Ukuran | Jumlah koin | Diameter rata-rata (piksel)")
for size_number, group in enumerate(groups, start=1):
mean_diameter = float(np.mean([coin.diameter_px for coin in group]))
print(f"{size_number:>6} | {len(group):>11} | {mean_diameter:>28.2f}")
save_results(image, mask, groups)
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
numpy
opencv-python
scikit-image
scipy
+240
View File
@@ -0,0 +1,240 @@
# Soal 3 — Menghitung Jumlah Koin untuk Setiap Ukuran dengan `regionprops`
Program: `pengolahan_citra.py` · Input: `koin_100.png` (dibuat oleh `buat_gambar_koin.py` dari `koin.png`) · Output: folder `output/`
Cara menjalankan:
```
pip install -r requirements.txt
python buat_gambar_koin.py # membuat koin_100.png
python pengolahan_citra.py # atau: python pengolahan_citra.py <gambar_koin>
```
Referensi teori: Gonzalez & Woods, *Digital Image Processing*, edisi ke-4 (Pearson, 2018).
## 1. Tujuan dan Asumsi
Sebuah gambar berisi 100 koin dengan berbagai ukuran di atas latar belakang seragam, dan koin-koin tidak saling bersentuhan. Program harus menentukan jumlah koin untuk setiap ukuran.
Asumsi yang dipakai:
- Koin tidak bersentuhan, sesuai soal. Akibatnya satu komponen terhubung = satu koin.
- Latar belakang seragam dan kontras terhadap koin.
- Jumlah dan nilai ukuran koin **tidak** diketahui sebelumnya. Program mengelompokkan ukuran secara otomatis dari data diameter.
## 2. Gambar Uji
Soal tidak menyertakan gambar. `koin.png` hanya berisi satu koin, jadi `buat_gambar_koin.py` menyusun gambar uji `koin_100.png` (3000 × 3000 piksel):
- Koin diperkecil ke 4 ukuran dan ditempel 100 kali di posisi acak.
- Jarak antar-tepi koin minimal 10 piksel, jadi tidak ada yang bersentuhan.
- Latar abu-abu gelap seragam (nilai 40).
- Setiap koin diputar acak dan seluruh gambar diberi noise Gaussian (σ = 3), supaya koin sejenis tidak identik piksel per piksel, seperti pada foto asli.
- Seed acak tetap, jadi gambar yang sama bisa dibuat ulang.
| Diameter tempel | Jumlah koin |
|---|---|
| 140 piksel | 30 |
| 170 piksel | 27 |
| 205 piksel | 25 |
| 245 piksel | 18 |
| **Total** | **100** |
Karena jumlah per ukuran diketahui, hasil program dapat diperiksa.
![Gambar uji koin_100.png](koin_100.png)
## 3. Kode Semu
```
INPUT : citra RGB berisi koin
OUTPUT: daftar (ukuran, jumlah koin, diameter rata-rata)
1. gray ← konversi citra ke grayscale
2. gray ← Gaussian blur 5×5 (gray)
3. T ← threshold Otsu dari histogram gray
4. mask ← (gray > T)
5. JIKA mayoritas piksel tepi gambar bernilai 1 MAKA mask ← NOT mask
(agar koin = 1 dan latar = 0)
6. mask ← isi lubang (mask)
7. label ← labeling komponen terhubung 8-ketetanggaan (mask)
8. label ← buang komponen yang menyentuh tepi gambar (label)
9. daftar_koin ← kosong
10. UNTUK SETIAP region DALAM regionprops(label):
JIKA region.area < MIN_AREA: lanjutkan (noise)
d ← √(4 · region.area_convex / π) (diameter koin)
tambahkan (region.label, d, region.centroid) ke daftar_koin
11. kelompok ← kosong
12. UNTUK SETIAP koin DALAM daftar_koin, terurut dari diameter terkecil:
cari kelompok k dengan |d − rata2(k)| / rata2(k) ≤ TOLERANSI
JIKA ada: masukkan koin ke kelompok yang paling dekat
JIKA tidak: buat kelompok baru berisi koin tersebut
13. urutkan kelompok berdasarkan diameter rata-rata
14. UNTUK SETIAP kelompok: tampilkan (nomor ukuran, jumlah anggota, diameter rata-rata)
15. simpan tabel CSV, mask, dan gambar anotasi
```
## 4. Penjelasan Setiap Langkah
### 4.1 Grayscale dan penghalusan (Bab 2, Bab 3, Bab 7)
Citra warna BGR diubah ke satu kanal intensitas (`cv2.cvtColor`) dengan kombinasi linear:
```
Y = 0,299 R + 0,587 G + 0,114 B
```
Lalu citra dihaluskan dengan kernel Gaussian 5 × 5 (`cv2.GaussianBlur`). Ini filter penghalus spasial linear dari Bab 3:
```
G(s, t) = K · e^(−(s² + t²) / 2σ²)
```
Dengan argumen σ = 0, OpenCV menghitung σ dari ukuran kernel, yaitu σ = 0,3·((5 − 1)·0,5 − 1) + 0,8 = 1,1. Penghalusan meredam noise sehingga threshold tidak menghasilkan bintik-bintik di latar.
### 4.2 Threshold Otsu
Citra biner diperoleh dengan:
```
mask(x, y) = 1 jika f(x, y) > T
mask(x, y) = 0 jika f(x, y) ≤ T
```
Nilai T dipilih otomatis dengan metode Otsu, yaitu nilai k yang memaksimalkan varians antar-kelas:
```
σ²_B(k) = [m_G · P₁(k) − m(k)]² / [P₁(k) · (1 − P₁(k))]
```
dengan P₁(k) = proporsi piksel ≤ k, m(k) = rata-rata kumulatif sampai k, dan m_G = rata-rata global. Latar yang seragam membuat histogram bimodal (latar dan koin), sehingga Otsu cocok. Pada `koin_100.png`, T = 126.
Catatan: metode Otsu dibahas di Bab 10 (Segmentasi), setelah materi UTS. Konsep fungsi thresholding sudah dibahas di Bab 3.
### 4.3 Polaritas
Koin bisa lebih terang atau lebih gelap dari latar. Program memeriksa piksel di keempat tepi gambar: jika lebih dari separuhnya bernilai 1, berarti latar ikut terdeteksi sebagai objek, dan mask dibalik. Pada `koin_100.png`, 0% piksel tepi bernilai 1, jadi mask tidak dibalik.
### 4.4 Pengisian lubang (Bab 9)
Relief koin (bintang, simbol $) dan pantulan cahaya bisa membuat sebagian piksel di dalam koin berada di bawah T. Akibatnya koin berlubang, atau bahkan terpecah menjadi beberapa komponen. Lubang adalah daerah latar yang dikelilingi objek. Lubang diisi dengan algoritma morfologi:
```
X_k = (X_{k−1} ⊕ B) ∩ Iᶜ, k = 1, 2, 3, ...
```
dengan I = mask, Iᶜ = komplemennya, dan B = elemen penstruktur. Iterasi berhenti saat X_k = X_{k−1}, lalu hasilnya X_k ∪ I. `binary_fill_holes` menjalankan versi otomatisnya: setiap daerah latar yang tidak terhubung ke tepi gambar dianggap lubang dan diisi. Pada `koin_100.png`, 31.941 piksel lubang terisi.
### 4.5 Labeling komponen terhubung (Bab 2 dan Bab 9)
`label(mask, connectivity=2)` memberi nomor unik pada setiap komponen terhubung dengan 8-ketetanggaan: piksel p terhubung dengan 8 tetangganya, yaitu 4 horizontal/vertikal dan 4 diagonal. Secara morfologi, satu komponen yang memuat titik awal X₀ diekstraksi dengan:
```
X_k = (X_{k−1} ⊕ B) ∩ I
```
Karena koin tidak bersentuhan, setiap koin menjadi tepat satu komponen. Pada `koin_100.png` terbentuk 450 komponen: 100 koin dan 350 fragmen kecil, yaitu potongan tepi koin yang terpisah oleh bagian bayangan.
### 4.6 Membuang koin di tepi gambar (Bab 9)
Koin yang terpotong tepi gambar luasnya tidak utuh, sehingga diameternya terukur lebih kecil dan bisa masuk ke kelompok ukuran yang salah. `clear_border` membuang setiap komponen yang menyentuh tepi gambar. Di buku, operasi ini disebut *border clearing* dan dilakukan dengan rekonstruksi morfologi. Pada `koin_100.png` tidak ada koin di tepi. Pada uji D (bagian 6), langkah ini membuang satu koin yang terpotong.
### 4.7 Cara `regionprops` digunakan
`regionprops(label)` menghasilkan satu objek pengukuran per komponen. Properti yang dipakai:
| Properti | Arti | Kegunaan di program |
|---|---|---|
| `area` | Jumlah piksel region | Membuang noise: region dengan area < 100 piksel diabaikan |
| `area_convex` | Jumlah piksel convex hull region | Menghitung diameter koin |
| `centroid` | Titik pusat region (rata-rata koordinat piksel) | Posisi lingkaran dan nomor ukuran pada gambar anotasi |
| `label` | Nomor region | Identitas koin |
**Filter area.** Pada `koin_100.png`, fragmen terbesar berukuran 37 piksel dan koin terkecil 13.484 piksel. Batas 100 piksel memisahkan keduanya dengan aman, jadi 350 fragmen terbuang dan tepat 100 koin tersisa.
**Diameter dari convex hull.** Bayangan di tepi koin cukup gelap sehingga berada di bawah T. Mask koin menjadi lingkaran yang terpotong berbentuk sabit di satu sisi. Potongan ini terbuka ke latar, jadi tidak terisi oleh pengisian lubang. Convex hull H dari region adalah himpunan konveks terkecil yang memuat region tersebut (algoritma morfologi di Bab 9). Convex hull menutup potongan sabit, sehingga luasnya mendekati luas lingkaran utuh. Diameter dihitung sebagai diameter lingkaran yang luasnya sama dengan luas convex hull:
```
A_H = π (d/2)² → d = √(4 · A_H / π)
```
Perbandingan pada `koin_100.png`:
| Diameter tempel | d dari `area` | d dari `area_convex` |
|---|---|---|
| 140 | 131,1 (93,6%) | 137,2 (98,0%) |
| 170 | 159,4 (93,7%) | 167,3 (98,4%) |
| 205 | 192,4 (93,8%) | 202,3 (98,7%) |
| 245 | 230,2 (93,9%) | 242,2 (98,9%) |
Catatan: deskriptor region seperti area dan centroid dibahas lebih lanjut di Bab 11. Program memakainya lewat `regionprops`, sesuai petunjuk soal.
### 4.8 Pengelompokan ukuran
Diameter diurutkan dari terkecil. Setiap koin dibandingkan dengan diameter rata-rata setiap kelompok yang sudah ada. Koin masuk ke kelompok terdekat jika selisih relatifnya ≤ toleransi:
```
|d − d̄_k| / d̄_k × 100% ≤ 3%
```
Jika tidak ada kelompok yang memenuhi, koin membentuk kelompok baru. Jumlah kelompok tidak ditentukan di awal, tetapi muncul dari data.
**Pemilihan toleransi.** Toleransi harus lebih besar dari variasi diameter koin sejenis, tetapi lebih kecil dari selisih antar-ukuran:
- Variasi diameter koin sejenis pada `koin_100.png` paling besar **0,21%** dari rata-rata kelompoknya.
- Selisih antar-ukuran yang berdekatan **19,7–22,0%**.
- Pada uji C (bagian 6), ukuran berbeda sekitar 7%. Toleransi 8% menggabungkan ukuran-ukuran itu, sedangkan 3% memisahkannya dengan benar.
## 5. Hasil
Output program pada `koin_100.png`:
```
Jumlah koin terdeteksi: 100
Hasil pengelompokan ukuran:
Ukuran | Jumlah koin | Diameter rata-rata (piksel)
1 | 30 | 137.17
2 | 27 | 167.29
3 | 25 | 202.27
4 | 18 | 242.21
```
| Ukuran | Diameter tempel | Jumlah sebenarnya | Jumlah dari program |
|---|---|---|---|
| 1 | 140 | 30 | 30 |
| 2 | 170 | 27 | 27 |
| 3 | 205 | 25 | 25 |
| 4 | 245 | 18 | 18 |
Semua jumlah tepat. File output di folder `output/`:
- `jumlah_koin_per_ukuran.csv`: tabel hasil.
- `mask_koin.png`: hasil segmentasi setelah pengisian lubang.
- `anotasi_koin.jpg`: setiap koin dilingkari dan diberi nomor ukuran, dengan warna berbeda per ukuran.
| Mask segmentasi | Anotasi |
|---|---|
| ![mask](output/mask_koin.png) | ![anotasi](output/anotasi_koin.jpg) |
## 6. Pengujian Tambahan
Untuk menguji ketahanan program, dibuat 4 gambar sintetis tambahan, masing-masing berisi koin berbentuk lingkaran yang tidak bersentuhan dengan noise Gaussian. Setiap gambar diuji dengan versi awal program (tanpa pengisian lubang, tanpa `clear_border`, toleransi 8%) dan versi akhir.
| Uji | Kondisi | Jumlah sebenarnya | Versi awal | Versi akhir |
|---|---|---|---|---|
| A | Koin polos, 3 ukuran | 40 / 35 / 25 | 40 / 35 / 25 | 40 / 35 / 25 |
| B | Koin dengan relief gelap di dalam | 40 / 35 / 25 | 200 koin, 6 ukuran | 40 / 35 / 25 |
| C | 4 ukuran, selisih sekitar 7% | 30 / 25 / 25 / 20 | 55 / 45 | 30 / 25 / 25 / 20 |
| D | 3 ukuran, ditambah 1 koin terpotong tepi | 40 / 35 / 24 + 1 terpotong | 40 / 36 / 24 | 40 / 35 / 24 |
- **Uji B** menunjukkan pentingnya pengisian lubang. Relief membelah setiap koin menjadi cincin luar dan bagian dalam, sehingga satu koin terhitung dua.
- **Uji C** menunjukkan pentingnya toleransi yang tepat.
- **Uji D** menunjukkan pentingnya `clear_border`. Pada versi akhir, program juga mencetak peringatan karena jumlah koin utuh (99) berbeda dari 100.
## 7. Batasan
- **Koin bersentuhan.** Koin yang bersentuhan menjadi satu komponen dan terhitung satu koin besar. Ini sesuai asumsi soal. Untuk kasus bersentuhan diperlukan teknik pemisahan seperti watershed.
- **Parameter bergantung pada gambar.** `MIN_COIN_AREA` (100 piksel) bergantung pada resolusi. Toleransi 3% bergantung pada seberapa mirip ukuran-ukuran koin dan seberapa besar variasi pengukuran. Pada foto asli, kedua nilai ini perlu dicek ulang.
- **Ukuran dalam piksel.** Diameter dinyatakan dalam piksel. Untuk ukuran dalam milimeter diperlukan benda acuan berukuran diketahui di dalam gambar.
- **Perspektif.** Jika foto diambil miring, koin tampak elips dan koin yang lebih jauh tampak lebih kecil. Foto sebaiknya diambil tegak lurus dari atas.