| """ |
| Segmentasi ROI (Region of Interest) permukaan daging. |
| Pendekatan heuristik berbasis Strict HSV Thresholding & Margin Annihilation. |
| """ |
| import cv2 |
| import numpy as np |
| import config |
|
|
| def segment_meat_roi(image_bgr, min_area_ratio=0.015): |
| """ |
| Menghasilkan mask biner (uint8, nilai 0/255) area permukaan daging (foreground) murni. |
| """ |
| h, w = image_bgr.shape[:2] |
| total_area = float(h * w) |
|
|
| |
| blurred = cv2.GaussianBlur(image_bgr, (7, 7), 0) |
| hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) |
|
|
| |
| |
| |
| mask1 = cv2.inRange(hsv, np.array([0, 30, 25]), np.array([30, 255, 255])) |
| mask2 = cv2.inRange(hsv, np.array([160, 30, 25]), np.array([180, 255, 255])) |
| color_mask = cv2.bitwise_or(mask1, mask2) |
|
|
| |
| margin_y, margin_x = int(h * 0.12), int(w * 0.12) |
| color_mask[0:margin_y, :] = 0 |
| color_mask[h-margin_y:h, :] = 0 |
| color_mask[:, 0:margin_x] = 0 |
| color_mask[:, w-margin_x:w] = 0 |
|
|
| |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11)) |
| morph_mask = cv2.morphologyEx(color_mask, cv2.MORPH_CLOSE, kernel, iterations=2) |
| morph_mask = cv2.morphologyEx(morph_mask, cv2.MORPH_OPEN, kernel, iterations=1) |
|
|
| |
| cnts = cv2.findContours(morph_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
| contours = cnts[0] if len(cnts) == 2 else cnts[1] |
|
|
| final_mask = np.zeros((h, w), dtype=np.uint8) |
| fallback_used = False |
|
|
| if not contours: |
| final_mask.fill(255) |
| fallback_used = True |
| else: |
| center_x, center_y = w // 2, h // 2 |
| best_contour = None |
| best_score = -1 |
|
|
| for c in contours: |
| area = cv2.contourArea(c) |
| |
| if area > (total_area * min_area_ratio): |
| M = cv2.moments(c) |
| if M["m00"] != 0: |
| cx = int(M["m10"] / M["m00"]) |
| cy = int(M["m01"] / M["m00"]) |
| else: |
| cx, cy = center_x, center_y |
|
|
| dist = np.sqrt((cx - center_x)**2 + (cy - center_y)**2) |
| score = area / (dist + 1) |
| |
| if score > best_score: |
| best_score = score |
| best_contour = c |
|
|
| if best_contour is not None: |
| |
| cv2.drawContours(final_mask, [best_contour], -1, 255, thickness=cv2.FILLED) |
| else: |
| final_mask.fill(255) |
| fallback_used = True |
|
|
| meta = {"foreground_ratio": float(np.count_nonzero(final_mask)) / total_area, "fallback_used": fallback_used} |
| return final_mask, meta |