Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -123,6 +123,12 @@ class ProcessResult:
|
|
| 123 |
sm_height: Optional[float] = None
|
| 124 |
raw_perimeter: Optional[float] = None
|
| 125 |
sm_perimeter: Optional[float] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
raw_rind_thick: Optional[float] = None
|
| 127 |
sm_rind_thick: Optional[float] = None
|
| 128 |
raw_rind_ratio: Optional[float] = None
|
|
@@ -343,19 +349,23 @@ class WatermelonProcessor:
|
|
| 343 |
|
| 344 |
@staticmethod
|
| 345 |
def split_asymmetry(region_mask, midline, thickness=5):
|
| 346 |
-
if midline is None or len(midline) < 2 or cv2.countNonZero(region_mask) == 0:
|
| 347 |
-
return None
|
| 348 |
-
|
| 349 |
split_mask = region_mask.copy()
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
n_labels, _, stats, _ = cv2.connectedComponentsWithStats((split_mask > 0).astype(np.uint8), connectivity=8)
|
| 352 |
-
if n_labels <= 2:
|
| 353 |
-
return None
|
| 354 |
|
| 355 |
areas = sorted([int(stats[i, cv2.CC_STAT_AREA]) for i in range(1, n_labels)], reverse=True)
|
| 356 |
-
if len(areas) < 2 or areas[0] + areas[1] == 0:
|
| 357 |
-
return None
|
| 358 |
-
|
| 359 |
return float(abs(areas[0] - areas[1]) / (areas[0] + areas[1]))
|
| 360 |
|
| 361 |
@staticmethod
|
|
@@ -422,13 +432,14 @@ class WatermelonProcessor:
|
|
| 422 |
|
| 423 |
pt_top, pt_bot, height_px = get_intersections(phi + np.pi/2, rind_mask)
|
| 424 |
pt_right, pt_left, width_px = get_intersections(phi, rind_mask)
|
| 425 |
-
_, _,
|
|
|
|
| 426 |
|
| 427 |
rind_thick_px = None
|
| 428 |
if width_px > 0 and flesh_width_px > 0:
|
| 429 |
rind_thick_px = float(max(0.0, (width_px - flesh_width_px) / 2.0))
|
| 430 |
|
| 431 |
-
return height_px, width_px, rind_thick_px, (pt_top, pt_bot), (pt_left, pt_right)
|
| 432 |
|
| 433 |
@staticmethod
|
| 434 |
def get_dual_mask_midline(f_left, f_right, rind_cnt, pred_cnt, cx, cy):
|
|
@@ -561,7 +572,13 @@ class WatermelonProcessor:
|
|
| 561 |
t_r, raw_r_r, (cx, cy), raw_rind_cnt = rind_data
|
| 562 |
pts_r_raw = raw_rind_cnt.reshape(-1, 2).astype(np.float32)
|
| 563 |
|
| 564 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
_, (ma, Ma), angle = cv2.fitEllipse(raw_rind_cnt) if len(raw_rind_cnt) > 5 else (None, (0,0), 0)
|
| 566 |
rot_angle = angle if ma < Ma else angle + 90
|
| 567 |
M_rot = cv2.getRotationMatrix2D((cx, cy), rot_angle, 1.0)
|
|
@@ -603,7 +620,8 @@ class WatermelonProcessor:
|
|
| 603 |
_, _, r_angle = cv2.fitEllipse(raw_rind_cnt)
|
| 604 |
raw_phi = np.deg2rad(180 - r_angle) if r_angle > 90 else np.deg2rad(-r_angle)
|
| 605 |
|
| 606 |
-
|
|
|
|
| 607 |
|
| 608 |
# Midline is found using flesh_combined (with gap) and clipped to the raw rind contour
|
| 609 |
midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, raw_rind_cnt, pts_r_raw, cx, cy)
|
|
@@ -632,7 +650,11 @@ class WatermelonProcessor:
|
|
| 632 |
bounds=([0.5, 0.5, -0.4, 0.0, 0.1, 0.0, 0.1, -1.5, -0.2, -0.2], [2.0, 2.0, 0.4, 0.5, 50.0, 0.5, 50.0, 1.5, 0.2, 0.2]), max_nfev=3000)
|
| 633 |
d_r = np.sum((raw_r_r/scale_r - 1)**2)
|
| 634 |
r2_rind = 1 - (np.sum((raw_r_r/scale_r - self.watermelon_model(t_r, *popt_r))**2) / d_r) if d_r != 0 else None
|
| 635 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
t_fit = np.linspace(-np.pi, np.pi, 500)
|
| 637 |
fit_r = self.watermelon_model(t_fit, *popt_r) * scale_r
|
| 638 |
sm_rind_pts = np.array([[r*np.cos(t)+cx, cy-r*np.sin(t)] for t, r in zip(t_fit, fit_r)], dtype=np.float32)
|
|
@@ -667,11 +689,13 @@ class WatermelonProcessor:
|
|
| 667 |
|
| 668 |
sm_f_a = self.contour_area_px(sm_flesh_pts)
|
| 669 |
sm_flesh_asym = self.split_asymmetry(sm_flesh_mask, midline, thickness=3)
|
|
|
|
| 670 |
else:
|
| 671 |
sm_flesh_mask = np.zeros_like(target_rind_mask)
|
|
|
|
| 672 |
|
| 673 |
# Smooth axes (using the filled smooth masks)
|
| 674 |
-
sm_h, sm_w, sm_rt, sm_h_line, sm_w_line = self.calculate_axis_metrics(cx, cy, sm_phi, sm_rind_mask, sm_flesh_mask)
|
| 675 |
if sm_tot_a > 0 and sm_f_a is not None:
|
| 676 |
sm_f_rat = float(sm_f_a / sm_tot_a)
|
| 677 |
|
|
@@ -699,6 +723,9 @@ class WatermelonProcessor:
|
|
| 699 |
|
| 700 |
raw_width=s(raw_w), sm_width=s(sm_w), raw_height=s(raw_h), sm_height=s(sm_h),
|
| 701 |
raw_perimeter=s(raw_perim), sm_perimeter=s(sm_perim),
|
|
|
|
|
|
|
|
|
|
| 702 |
raw_rind_thick=s(raw_rt), sm_rind_thick=s(sm_rt),
|
| 703 |
raw_rind_ratio=rt_rat(raw_rt, raw_w), sm_rind_ratio=rt_rat(sm_rt, sm_w),
|
| 704 |
raw_total_area=a(raw_tot_a), sm_total_area=a(sm_tot_a),
|
|
@@ -717,7 +744,7 @@ class WatermelonProcessor:
|
|
| 717 |
|
| 718 |
def draw_base(r_m, f_m):
|
| 719 |
out = image.copy().astype(np.float32)
|
| 720 |
-
alpha = 0.
|
| 721 |
|
| 722 |
# --- THE FIX: Only paint the rind where there is NO flesh ---
|
| 723 |
rind_only = (r_m > 0) & (f_m == 0)
|
|
|
|
| 123 |
sm_height: Optional[float] = None
|
| 124 |
raw_perimeter: Optional[float] = None
|
| 125 |
sm_perimeter: Optional[float] = None
|
| 126 |
+
raw_flesh_width: Optional[float] = None
|
| 127 |
+
sm_flesh_width: Optional[float] = None
|
| 128 |
+
raw_flesh_height: Optional[float] = None
|
| 129 |
+
sm_flesh_height: Optional[float] = None
|
| 130 |
+
raw_flesh_perimeter: Optional[float] = None
|
| 131 |
+
sm_flesh_perimeter: Optional[float] = None
|
| 132 |
raw_rind_thick: Optional[float] = None
|
| 133 |
sm_rind_thick: Optional[float] = None
|
| 134 |
raw_rind_ratio: Optional[float] = None
|
|
|
|
| 349 |
|
| 350 |
@staticmethod
|
| 351 |
def split_asymmetry(region_mask, midline, thickness=5):
|
| 352 |
+
if midline is None or len(midline) < 2 or cv2.countNonZero(region_mask) == 0: return None
|
|
|
|
|
|
|
| 353 |
split_mask = region_mask.copy()
|
| 354 |
+
|
| 355 |
+
# FIX: Extrapolate the line 100 pixels in both directions to guarantee it completely bisects smoothed/expanded masks
|
| 356 |
+
m = midline.astype(np.float32)
|
| 357 |
+
p0, p1, pn, pn_1 = m[0], m[1], m[-1], m[-2]
|
| 358 |
+
n0, n1 = np.linalg.norm(p0 - p1), np.linalg.norm(pn - pn_1)
|
| 359 |
+
ext_start = p0 + (p0 - p1) / n0 * 100 if n0 > 1e-5 else p0
|
| 360 |
+
ext_end = pn + (pn - pn_1) / n1 * 100 if n1 > 1e-5 else pn
|
| 361 |
+
ext_midline = np.vstack([ext_start, m, ext_end]).astype(np.int32)
|
| 362 |
+
|
| 363 |
+
cv2.polylines(split_mask, [ext_midline], False, 0, thickness)
|
| 364 |
n_labels, _, stats, _ = cv2.connectedComponentsWithStats((split_mask > 0).astype(np.uint8), connectivity=8)
|
| 365 |
+
if n_labels <= 2: return None
|
|
|
|
| 366 |
|
| 367 |
areas = sorted([int(stats[i, cv2.CC_STAT_AREA]) for i in range(1, n_labels)], reverse=True)
|
| 368 |
+
if len(areas) < 2 or areas[0] + areas[1] == 0: return None
|
|
|
|
|
|
|
| 369 |
return float(abs(areas[0] - areas[1]) / (areas[0] + areas[1]))
|
| 370 |
|
| 371 |
@staticmethod
|
|
|
|
| 432 |
|
| 433 |
pt_top, pt_bot, height_px = get_intersections(phi + np.pi/2, rind_mask)
|
| 434 |
pt_right, pt_left, width_px = get_intersections(phi, rind_mask)
|
| 435 |
+
_, _, f_height_px = get_intersections(phi + np.pi/2, flesh_mask) # NEW
|
| 436 |
+
_, _, f_width_px = get_intersections(phi, flesh_mask)
|
| 437 |
|
| 438 |
rind_thick_px = None
|
| 439 |
if width_px > 0 and flesh_width_px > 0:
|
| 440 |
rind_thick_px = float(max(0.0, (width_px - flesh_width_px) / 2.0))
|
| 441 |
|
| 442 |
+
return height_px, width_px, rind_thick_px, (pt_top, pt_bot), (pt_left, pt_right), f_height_px, f_width_px
|
| 443 |
|
| 444 |
@staticmethod
|
| 445 |
def get_dual_mask_midline(f_left, f_right, rind_cnt, pred_cnt, cx, cy):
|
|
|
|
| 572 |
t_r, raw_r_r, (cx, cy), raw_rind_cnt = rind_data
|
| 573 |
pts_r_raw = raw_rind_cnt.reshape(-1, 2).astype(np.float32)
|
| 574 |
|
| 575 |
+
# Raw Flesh perimeter length
|
| 576 |
+
raw_flesh_perim = None
|
| 577 |
+
if flesh_data:
|
| 578 |
+
_, _, _, raw_flesh_cnt = flesh_data
|
| 579 |
+
raw_flesh_perim = float(cv2.arcLength(raw_flesh_cnt, True))
|
| 580 |
+
|
| 581 |
+
# --- SCAN-LINE FLESH GAP FILLING (Preserves V-shape) ---
|
| 582 |
_, (ma, Ma), angle = cv2.fitEllipse(raw_rind_cnt) if len(raw_rind_cnt) > 5 else (None, (0,0), 0)
|
| 583 |
rot_angle = angle if ma < Ma else angle + 90
|
| 584 |
M_rot = cv2.getRotationMatrix2D((cx, cy), rot_angle, 1.0)
|
|
|
|
| 620 |
_, _, r_angle = cv2.fitEllipse(raw_rind_cnt)
|
| 621 |
raw_phi = np.deg2rad(180 - r_angle) if r_angle > 90 else np.deg2rad(-r_angle)
|
| 622 |
|
| 623 |
+
# Capture raw_f_h and raw_f_w
|
| 624 |
+
raw_h, raw_w, raw_rt, raw_h_line, raw_w_line, raw_f_h, raw_f_w = self.calculate_axis_metrics(cx, cy, raw_phi, target_rind_mask, flesh_closed)
|
| 625 |
|
| 626 |
# Midline is found using flesh_combined (with gap) and clipped to the raw rind contour
|
| 627 |
midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, raw_rind_cnt, pts_r_raw, cx, cy)
|
|
|
|
| 650 |
bounds=([0.5, 0.5, -0.4, 0.0, 0.1, 0.0, 0.1, -1.5, -0.2, -0.2], [2.0, 2.0, 0.4, 0.5, 50.0, 0.5, 50.0, 1.5, 0.2, 0.2]), max_nfev=3000)
|
| 651 |
d_r = np.sum((raw_r_r/scale_r - 1)**2)
|
| 652 |
r2_rind = 1 - (np.sum((raw_r_r/scale_r - self.watermelon_model(t_r, *popt_r))**2) / d_r) if d_r != 0 else None
|
| 653 |
+
|
| 654 |
+
# R² Warning Flag
|
| 655 |
+
if r2_rind is not None and r2_rind < 0.85:
|
| 656 |
+
warnings.append(f"R² below 0.85 ({r2_rind:.2f}). Fruit may be damaged or irregular.")
|
| 657 |
+
|
| 658 |
t_fit = np.linspace(-np.pi, np.pi, 500)
|
| 659 |
fit_r = self.watermelon_model(t_fit, *popt_r) * scale_r
|
| 660 |
sm_rind_pts = np.array([[r*np.cos(t)+cx, cy-r*np.sin(t)] for t, r in zip(t_fit, fit_r)], dtype=np.float32)
|
|
|
|
| 689 |
|
| 690 |
sm_f_a = self.contour_area_px(sm_flesh_pts)
|
| 691 |
sm_flesh_asym = self.split_asymmetry(sm_flesh_mask, midline, thickness=3)
|
| 692 |
+
sm_flesh_perim = float(np.sum(np.linalg.norm(np.diff(sm_flesh_pts, axis=0), axis=1)) + np.linalg.norm(sm_flesh_pts[-1]-sm_flesh_pts[0]))
|
| 693 |
else:
|
| 694 |
sm_flesh_mask = np.zeros_like(target_rind_mask)
|
| 695 |
+
sm_flesh_perim = None
|
| 696 |
|
| 697 |
# Smooth axes (using the filled smooth masks)
|
| 698 |
+
sm_h, sm_w, sm_rt, sm_h_line, sm_w_line, sm_f_h, sm_f_w = self.calculate_axis_metrics(cx, cy, sm_phi, sm_rind_mask, sm_flesh_mask)
|
| 699 |
if sm_tot_a > 0 and sm_f_a is not None:
|
| 700 |
sm_f_rat = float(sm_f_a / sm_tot_a)
|
| 701 |
|
|
|
|
| 723 |
|
| 724 |
raw_width=s(raw_w), sm_width=s(sm_w), raw_height=s(raw_h), sm_height=s(sm_h),
|
| 725 |
raw_perimeter=s(raw_perim), sm_perimeter=s(sm_perim),
|
| 726 |
+
raw_flesh_width=s(raw_f_w), sm_flesh_width=s(sm_f_w),
|
| 727 |
+
raw_flesh_height=s(raw_f_h), sm_flesh_height=s(sm_f_h),
|
| 728 |
+
raw_flesh_perimeter=s(raw_flesh_perim), sm_flesh_perimeter=s(sm_flesh_perim),
|
| 729 |
raw_rind_thick=s(raw_rt), sm_rind_thick=s(sm_rt),
|
| 730 |
raw_rind_ratio=rt_rat(raw_rt, raw_w), sm_rind_ratio=rt_rat(sm_rt, sm_w),
|
| 731 |
raw_total_area=a(raw_tot_a), sm_total_area=a(sm_tot_a),
|
|
|
|
| 744 |
|
| 745 |
def draw_base(r_m, f_m):
|
| 746 |
out = image.copy().astype(np.float32)
|
| 747 |
+
alpha = 0.10 # Transparency
|
| 748 |
|
| 749 |
# --- THE FIX: Only paint the rind where there is NO flesh ---
|
| 750 |
rind_only = (r_m > 0) & (f_m == 0)
|