File size: 4,921 Bytes
78aefa8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | """Preprocessing citra: resize, denoising, segmentasi ROI, QA visual, dan Bounding Box Crop."""
import os
import cv2
import numpy as np
import config
from src import segmentation
def resize_keep_aspect(image_bgr, max_side):
h, w = image_bgr.shape[:2]
scale = max_side / float(max(h, w))
if scale >= 1.0:
return image_bgr
new_w, new_h = int(round(w * scale)), int(round(h * scale))
return cv2.resize(image_bgr, (new_w, new_h), interpolation=cv2.INTER_AREA)
def denoise(image_bgr):
if config.DENOISE_H is None:
return image_bgr
return cv2.fastNlMeansDenoisingColored(
image_bgr, None,
h=config.DENOISE_H, hColor=config.DENOISE_H_COLOR,
templateWindowSize=config.DENOISE_TEMPLATE_WINDOW,
searchWindowSize=config.DENOISE_SEARCH_WINDOW,
)
def to_hsv(image_bgr):
return cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
def crop_to_bounding_box(image, mask, pad_ratio=0.01):
"""
Pemotongan matriks secara presisi absolut (tight fit) berbasis piksel aktif.
Mengeliminasi redundansi dimensi latar belakang untuk menjaga kemurnian
ekstraksi fitur spasial (SURF) dan momen statistik warna (HSV).
"""
# 1. Ekstraksi koordinat absolut dari piksel foreground murni
coords = cv2.findNonZero(mask)
# Fallback jika mask secara anomali kosong
if coords is None:
return image, mask
# 2. Kalkulasi bounding box yang presisi mengelilingi piksel aktif
x, y, w, h_rect = cv2.boundingRect(coords)
H, W = image.shape[:2]
# 3. Margin sangat ketat (1% dari dimensi objek)
# untuk menyisakan sedikit ruang agar detektor tepi SURF tidak terpotong
pad_x, pad_y = int(w * pad_ratio), int(h_rect * pad_ratio)
x1, y1 = max(0, x - pad_x), max(0, y - pad_y)
x2, y2 = min(W, x + w + pad_x), min(H, y + h_rect + pad_y)
return image[y1:y2, x1:x2], mask[y1:y2, x1:x2]
def save_visual_inspection(original_bgr, mask, filename):
"""
Pembuatan artefak visual QA berdampingan dengan resolusi asli.
Latar belakang diubah menjadi putih murni untuk lampiran manuskrip.
"""
os.makedirs(config.SEGMENTED_VIEW_DIR, exist_ok=True)
segmented_bgr = cv2.bitwise_and(original_bgr, original_bgr, mask=mask)
white_bg = np.full(original_bgr.shape, 255, dtype=np.uint8)
background = cv2.bitwise_and(white_bg, white_bg, mask=cv2.bitwise_not(mask))
segmented_clean = cv2.add(segmented_bgr, background)
cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = cnts[0] if len(cnts) == 2 else cnts[1]
annotated_bgr = original_bgr.copy()
cv2.drawContours(annotated_bgr, contours, -1, (0, 0, 255), 3)
combined_view = np.hstack((annotated_bgr, segmented_clean))
out_path = os.path.join(config.SEGMENTED_VIEW_DIR, f"QA_{os.path.splitext(filename)[0]}.png")
cv2.imwrite(out_path, combined_view)
def preprocess_single_image(filepath):
"""
Alur komputasi: Normalisasi -> Denoising -> Masking -> QA Visual -> Cropping.
"""
image_bgr = cv2.imread(filepath, cv2.IMREAD_COLOR)
if image_bgr is None:
return None
image_bgr = resize_keep_aspect(image_bgr, config.RESIZE_MAX_SIDE)
image_bgr = denoise(image_bgr)
mask, seg_meta = segmentation.segment_meat_roi(image_bgr)
# Render gambar QA sebelum citra direduksi oleh Bounding Box
filename = os.path.basename(filepath)
save_visual_inspection(image_bgr, mask, filename)
# Eksekusi pemotongan latar belakang (Bounding Box)
if not seg_meta["fallback_used"]:
image_bgr, mask = crop_to_bounding_box(image_bgr, mask)
hsv = to_hsv(image_bgr)
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
return {
"gray": gray,
"hsv": hsv,
"mask": mask,
"seg_meta": seg_meta,
"shape": image_bgr.shape[:2],
}
def save_mask(mask, filename, masks_dir=None):
masks_dir = config.MASKS_DIR if masks_dir is None else masks_dir
os.makedirs(masks_dir, exist_ok=True)
stem = os.path.splitext(filename)[0]
out_path = os.path.join(masks_dir, stem + "_mask.npz")
np.savez_compressed(out_path, mask=mask)
return out_path
def save_processed(gray, hsv, mask, filename, processed_dir=None):
processed_dir = config.PROCESSED_DIR if processed_dir is None else processed_dir
os.makedirs(processed_dir, exist_ok=True)
stem = os.path.splitext(filename)[0]
out_path = os.path.join(processed_dir, stem + ".npz")
np.savez_compressed(out_path, gray=gray, hsv=hsv, mask=mask)
return out_path
def load_processed(filename, processed_dir=None):
processed_dir = config.PROCESSED_DIR if processed_dir is None else processed_dir
stem = os.path.splitext(filename)[0]
in_path = os.path.join(processed_dir, stem + ".npz")
data = np.load(in_path)
return data["gray"], data["hsv"], data["mask"] |