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:
@@ -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()
|
||||
Reference in New Issue
Block a user