Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -119,6 +119,8 @@ class ProcessResult:
|
|
| 119 |
width_val: Optional[float] = None
|
| 120 |
height_val: Optional[float] = None
|
| 121 |
perimeter_val: Optional[float] = None
|
|
|
|
|
|
|
| 122 |
total_area: Optional[float] = None
|
| 123 |
flesh_area: Optional[float] = None
|
| 124 |
flesh_area_ratio: Optional[float] = None
|
|
@@ -163,6 +165,44 @@ class WatermelonProcessor:
|
|
| 163 |
divot_bot = d_bot * np.exp(w_bot * (-np.sin(t) - 1))
|
| 164 |
return (ellipse * asymmetry) - divot_top - divot_bot + c_skew * np.sin(t) + c_bend * np.cos(t) * (np.sin(t) ** 2)
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
@staticmethod
|
| 167 |
def contour_centroid(contour):
|
| 168 |
M = cv2.moments(contour)
|
|
@@ -529,9 +569,19 @@ class WatermelonProcessor:
|
|
| 529 |
# If smoothing is off, use the raw OpenCV contour for the perimeter
|
| 530 |
fit_pts = rind_cnt.reshape(-1, 2).astype(np.float32)
|
| 531 |
|
| 532 |
-
width_px = float(np.max(fit_pts[:, 0]) - np.min(fit_pts[:, 0]))
|
| 533 |
-
height_px = float(np.max(fit_pts[:, 1]) - np.min(fit_pts[:, 1]))
|
| 534 |
perimeter_px = float(np.sum(np.linalg.norm(np.diff(fit_pts, axis=0), axis=1)) + np.linalg.norm(fit_pts[-1] - fit_pts[0]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
total_area_px = self.contour_area_px(fit_pts)
|
| 536 |
flesh_area_px = float(cv2.countNonZero(flesh_combined))
|
| 537 |
flesh_area_ratio = float(flesh_area_px / total_area_px) if total_area_px > 0 else None
|
|
@@ -542,23 +592,29 @@ class WatermelonProcessor:
|
|
| 542 |
flesh_asymmetry_score = self.split_asymmetry(flesh_combined, midline, thickness=3)
|
| 543 |
midline_curvature = self.midline_curvature_score(midline)
|
| 544 |
|
|
|
|
|
|
|
| 545 |
if cm_per_px is not None:
|
| 546 |
-
measurement_unit = "cm"
|
| 547 |
-
area_unit = "cm2"
|
| 548 |
-
scale_source = "color_checker"
|
| 549 |
area_scale = cm_per_px ** 2
|
| 550 |
width_val = float(width_px * cm_per_px)
|
| 551 |
height_val = float(height_px * cm_per_px)
|
| 552 |
perimeter_val = float(perimeter_px * cm_per_px)
|
|
|
|
|
|
|
| 553 |
else:
|
| 554 |
-
measurement_unit = "px"
|
| 555 |
-
area_unit = "px2"
|
| 556 |
-
scale_source = "original_pixels"
|
| 557 |
orig_scale = 1.0 / scale_ratio
|
| 558 |
area_scale = orig_scale ** 2
|
| 559 |
width_val = float(width_px * orig_scale)
|
| 560 |
height_val = float(height_px * orig_scale)
|
| 561 |
perimeter_val = float(perimeter_px * orig_scale)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
total_area = float(total_area_px * area_scale)
|
| 563 |
flesh_area = float(flesh_area_px * area_scale)
|
| 564 |
mark("fit")
|
|
@@ -586,6 +642,10 @@ class WatermelonProcessor:
|
|
| 586 |
if checker_corners is not None:
|
| 587 |
cv2.polylines(output, [np.int32(checker_corners)], True, (0, 165, 255), 4)
|
| 588 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
if len(midline) > 1:
|
| 590 |
cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
|
| 591 |
pt_top = (int(midline[0][0]), int(midline[0][1]))
|
|
@@ -604,6 +664,7 @@ class WatermelonProcessor:
|
|
| 604 |
return ProcessResult(
|
| 605 |
success=True, message="Success", r2_score=float(r2) if r2 is not None else None,
|
| 606 |
width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
|
|
|
|
| 607 |
total_area=total_area, flesh_area=flesh_area, flesh_area_ratio=flesh_area_ratio,
|
| 608 |
elongation_factor=elongation_factor, circularity=circularity,
|
| 609 |
asymmetry_score=asymmetry_score, flesh_asymmetry_score=flesh_asymmetry_score,
|
|
|
|
| 119 |
width_val: Optional[float] = None
|
| 120 |
height_val: Optional[float] = None
|
| 121 |
perimeter_val: Optional[float] = None
|
| 122 |
+
rind_thickness_val: Optional[float] = None
|
| 123 |
+
rind_thickness_ratio: Optional[float] = None
|
| 124 |
total_area: Optional[float] = None
|
| 125 |
flesh_area: Optional[float] = None
|
| 126 |
flesh_area_ratio: Optional[float] = None
|
|
|
|
| 165 |
divot_bot = d_bot * np.exp(w_bot * (-np.sin(t) - 1))
|
| 166 |
return (ellipse * asymmetry) - divot_top - divot_bot + c_skew * np.sin(t) + c_bend * np.cos(t) * (np.sin(t) ** 2)
|
| 167 |
|
| 168 |
+
@staticmethod
|
| 169 |
+
def calculate_axis_metrics(cx, cy, phi, rind_mask, flesh_mask):
|
| 170 |
+
"""Casts rays along the major/minor axes to find height, width, and rind thickness."""
|
| 171 |
+
h, w = rind_mask.shape
|
| 172 |
+
|
| 173 |
+
def ray_cast(theta):
|
| 174 |
+
max_r = int(np.hypot(h, w))
|
| 175 |
+
r_steps = np.arange(0, max_r, 0.5)
|
| 176 |
+
xs = np.clip(np.round(cx + r_steps * np.cos(theta)), 0, w - 1).astype(int)
|
| 177 |
+
ys = np.clip(np.round(cy - r_steps * np.sin(theta)), 0, h - 1).astype(int)
|
| 178 |
+
|
| 179 |
+
rind_vals = rind_mask[ys, xs]
|
| 180 |
+
inside_rind = np.where(rind_vals > 0)[0]
|
| 181 |
+
r_rind = r_steps[inside_rind[-1]] if len(inside_rind) > 0 else 0
|
| 182 |
+
|
| 183 |
+
flesh_vals = flesh_mask[ys, xs]
|
| 184 |
+
inside_flesh = np.where(flesh_vals > 0)[0]
|
| 185 |
+
r_flesh = r_steps[inside_flesh[-1]] if len(inside_flesh) > 0 else 0
|
| 186 |
+
|
| 187 |
+
pt_rind = (int(np.round(cx + r_rind * np.cos(theta))), int(np.round(cy - r_rind * np.sin(theta))))
|
| 188 |
+
return r_rind, r_flesh, pt_rind
|
| 189 |
+
|
| 190 |
+
# phi is the rotation of the fruit. Top/Bot are perpendicular to Left/Right
|
| 191 |
+
r_top, _, pt_top = ray_cast(phi + np.pi/2)
|
| 192 |
+
r_bot, _, pt_bot = ray_cast(phi - np.pi/2)
|
| 193 |
+
r_right, f_right, pt_right = ray_cast(phi)
|
| 194 |
+
r_left, f_left, pt_left = ray_cast(phi + np.pi)
|
| 195 |
+
|
| 196 |
+
height_px = float(r_top + r_bot)
|
| 197 |
+
width_px = float(r_left + r_right)
|
| 198 |
+
|
| 199 |
+
rind_thick_px = None
|
| 200 |
+
if f_left > 0 and f_right > 0:
|
| 201 |
+
# Average the rind thickness of the left and right sides
|
| 202 |
+
rind_thick_px = float((max(0, r_left - f_left) + max(0, r_right - f_right)) / 2.0)
|
| 203 |
+
|
| 204 |
+
return height_px, width_px, rind_thick_px, (pt_top, pt_bot), (pt_left, pt_right)
|
| 205 |
+
|
| 206 |
@staticmethod
|
| 207 |
def contour_centroid(contour):
|
| 208 |
M = cv2.moments(contour)
|
|
|
|
| 569 |
# If smoothing is off, use the raw OpenCV contour for the perimeter
|
| 570 |
fit_pts = rind_cnt.reshape(-1, 2).astype(np.float32)
|
| 571 |
|
|
|
|
|
|
|
| 572 |
perimeter_px = float(np.sum(np.linalg.norm(np.diff(fit_pts, axis=0), axis=1)) + np.linalg.norm(fit_pts[-1] - fit_pts[0]))
|
| 573 |
+
|
| 574 |
+
# Calculate rotation angle for axes
|
| 575 |
+
if apply_smoothing:
|
| 576 |
+
phi = popt[7]
|
| 577 |
+
else:
|
| 578 |
+
_, _, angle = cv2.fitEllipse(rind_cnt)
|
| 579 |
+
phi = np.deg2rad(180 - angle) if angle > 90 else np.deg2rad(-angle)
|
| 580 |
+
|
| 581 |
+
height_px, width_px, rind_thick_px, h_line, w_line = self.calculate_axis_metrics(
|
| 582 |
+
cx, cy, phi, target_rind_mask, flesh_combined
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
total_area_px = self.contour_area_px(fit_pts)
|
| 586 |
flesh_area_px = float(cv2.countNonZero(flesh_combined))
|
| 587 |
flesh_area_ratio = float(flesh_area_px / total_area_px) if total_area_px > 0 else None
|
|
|
|
| 592 |
flesh_asymmetry_score = self.split_asymmetry(flesh_combined, midline, thickness=3)
|
| 593 |
midline_curvature = self.midline_curvature_score(midline)
|
| 594 |
|
| 595 |
+
rind_thickness_val, rind_thickness_ratio = None, None
|
| 596 |
+
|
| 597 |
if cm_per_px is not None:
|
| 598 |
+
measurement_unit, area_unit, scale_source = "cm", "cm2", "color_checker"
|
|
|
|
|
|
|
| 599 |
area_scale = cm_per_px ** 2
|
| 600 |
width_val = float(width_px * cm_per_px)
|
| 601 |
height_val = float(height_px * cm_per_px)
|
| 602 |
perimeter_val = float(perimeter_px * cm_per_px)
|
| 603 |
+
if rind_thick_px is not None:
|
| 604 |
+
rind_thickness_val = float(rind_thick_px * cm_per_px)
|
| 605 |
else:
|
| 606 |
+
measurement_unit, area_unit, scale_source = "px", "px2", "original_pixels"
|
|
|
|
|
|
|
| 607 |
orig_scale = 1.0 / scale_ratio
|
| 608 |
area_scale = orig_scale ** 2
|
| 609 |
width_val = float(width_px * orig_scale)
|
| 610 |
height_val = float(height_px * orig_scale)
|
| 611 |
perimeter_val = float(perimeter_px * orig_scale)
|
| 612 |
+
if rind_thick_px is not None:
|
| 613 |
+
rind_thickness_val = float(rind_thick_px * orig_scale)
|
| 614 |
+
|
| 615 |
+
if rind_thick_px is not None and width_px > 0:
|
| 616 |
+
rind_thickness_ratio = float((rind_thick_px * 2.0) / width_px)
|
| 617 |
+
|
| 618 |
total_area = float(total_area_px * area_scale)
|
| 619 |
flesh_area = float(flesh_area_px * area_scale)
|
| 620 |
mark("fit")
|
|
|
|
| 642 |
if checker_corners is not None:
|
| 643 |
cv2.polylines(output, [np.int32(checker_corners)], True, (0, 165, 255), 4)
|
| 644 |
|
| 645 |
+
# Draw axis lines underneath the other features
|
| 646 |
+
cv2.line(output, h_line[0], h_line[1], (255, 100, 255), 2) # Height (Purple)
|
| 647 |
+
cv2.line(output, w_line[0], w_line[1], (255, 255, 100), 2) # Width (Cyan)
|
| 648 |
+
|
| 649 |
if len(midline) > 1:
|
| 650 |
cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
|
| 651 |
pt_top = (int(midline[0][0]), int(midline[0][1]))
|
|
|
|
| 664 |
return ProcessResult(
|
| 665 |
success=True, message="Success", r2_score=float(r2) if r2 is not None else None,
|
| 666 |
width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
|
| 667 |
+
rind_thickness_val=rind_thickness_val, rind_thickness_ratio=rind_thickness_ratio,
|
| 668 |
total_area=total_area, flesh_area=flesh_area, flesh_area_ratio=flesh_area_ratio,
|
| 669 |
elongation_factor=elongation_factor, circularity=circularity,
|
| 670 |
asymmetry_score=asymmetry_score, flesh_asymmetry_score=flesh_asymmetry_score,
|