| """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). |
| """ |
| |
| coords = cv2.findNonZero(mask) |
| |
| |
| if coords is None: |
| return image, mask |
| |
| |
| x, y, w, h_rect = cv2.boundingRect(coords) |
| |
| H, W = image.shape[:2] |
| |
| |
| |
| 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) |
| |
| |
| filename = os.path.basename(filepath) |
| save_visual_inspection(image_bgr, mask, filename) |
|
|
| |
| 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"] |