beefresearch-bima / src /preprocessing.py
abrarwahid's picture
Initial clean version for pipeline
78aefa8
Raw
History Blame Contribute Delete
4.92 kB
"""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"]