crabbly commited on
Commit
d09b2d7
·
verified ·
1 Parent(s): 70bd897

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +69 -8
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,