crabbly commited on
Commit
c463ed6
·
verified ·
1 Parent(s): 2f18545

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +75 -3
main.py CHANGED
@@ -113,11 +113,20 @@ class ProcessResult:
113
  width_val: Optional[float] = None
114
  height_val: Optional[float] = None
115
  perimeter_val: Optional[float] = None
 
 
 
 
 
 
 
 
116
  delta_e_initial: Optional[float] = None
117
  delta_e_final: Optional[float] = None
118
  image_base64: Optional[str] = None
119
  filename: Optional[str] = None
120
  measurement_unit: Optional[str] = None
 
121
  scale_source: Optional[str] = None
122
  color_checker_found: bool = False
123
  rind_source: Optional[str] = None
@@ -248,6 +257,52 @@ class WatermelonProcessor:
248
  warnings.append("Whole-watermelon mask did not overlap flesh and fallback was unavailable.")
249
  return best_mask, "low_overlap_whole_mask", float(best_ratio), warnings
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  @staticmethod
252
  def get_stable_perimeter_data(rind_mask, flesh_combined):
253
  cnts, _ = cv2.findContours(rind_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
@@ -466,27 +521,40 @@ class WatermelonProcessor:
466
  width_px = float(np.max(fit_pts[:, 0]) - np.min(fit_pts[:, 0]))
467
  height_px = float(np.max(fit_pts[:, 1]) - np.min(fit_pts[:, 1]))
468
  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]))
 
 
 
 
 
 
 
 
 
469
 
470
  if cm_per_px is not None:
471
  measurement_unit = "cm"
 
472
  scale_source = "color_checker"
 
473
  width_val = float(width_px * cm_per_px)
474
  height_val = float(height_px * cm_per_px)
475
  perimeter_val = float(perimeter_px * cm_per_px)
476
  else:
477
  measurement_unit = "px"
 
478
  scale_source = "original_pixels"
479
  orig_scale = 1.0 / scale_ratio
 
480
  width_val = float(width_px * orig_scale)
481
  height_val = float(height_px * orig_scale)
482
  perimeter_val = float(perimeter_px * orig_scale)
 
 
483
  mark("fit")
484
 
485
  # --- 4. DRAWING ---
486
  img_base64 = None
487
  if include_image:
488
- midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
489
-
490
  # Color coding: Green=chosen rind, Blue=Left Flesh, Red=Right Flesh.
491
  output = image.copy().astype(np.float32)
492
  alpha = 0.42
@@ -524,9 +592,13 @@ class WatermelonProcessor:
524
  return ProcessResult(
525
  success=True, message="Success", r2_score=float(r2),
526
  width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
 
 
 
 
527
  delta_e_initial=dE_initial, delta_e_final=dE_final,
528
  image_base64=img_base64, filename=source_name,
529
- measurement_unit=measurement_unit, scale_source=scale_source,
530
  color_checker_found=checker_corners is not None,
531
  rind_source=rind_source, rind_overlap_ratio=rind_overlap_ratio,
532
  warnings=warnings or None, timings_ms=timings
 
113
  width_val: Optional[float] = None
114
  height_val: Optional[float] = None
115
  perimeter_val: Optional[float] = None
116
+ total_area: Optional[float] = None
117
+ flesh_area: Optional[float] = None
118
+ flesh_area_ratio: Optional[float] = None
119
+ elongation_factor: Optional[float] = None
120
+ circularity: Optional[float] = None
121
+ asymmetry_score: Optional[float] = None
122
+ flesh_asymmetry_score: Optional[float] = None
123
+ midline_curvature: Optional[float] = None
124
  delta_e_initial: Optional[float] = None
125
  delta_e_final: Optional[float] = None
126
  image_base64: Optional[str] = None
127
  filename: Optional[str] = None
128
  measurement_unit: Optional[str] = None
129
+ area_unit: Optional[str] = None
130
  scale_source: Optional[str] = None
131
  color_checker_found: bool = False
132
  rind_source: Optional[str] = None
 
257
  warnings.append("Whole-watermelon mask did not overlap flesh and fallback was unavailable.")
258
  return best_mask, "low_overlap_whole_mask", float(best_ratio), warnings
259
 
260
+ @staticmethod
261
+ def contour_area_px(points):
262
+ cnt = points.reshape(-1, 1, 2).astype(np.float32)
263
+ return float(abs(cv2.contourArea(cnt)))
264
+
265
+ @staticmethod
266
+ def elongation_from_points(points):
267
+ cnt = points.reshape(-1, 1, 2).astype(np.float32)
268
+ if len(cnt) >= 5:
269
+ _, (axis_a, axis_b), _ = cv2.fitEllipse(cnt)
270
+ minor = max(min(axis_a, axis_b), 1e-6)
271
+ return float(max(axis_a, axis_b) / minor)
272
+
273
+ _, _, bw, bh = cv2.boundingRect(cnt.astype(np.int32))
274
+ return float(max(bw, bh) / max(min(bw, bh), 1))
275
+
276
+ @staticmethod
277
+ def split_asymmetry(region_mask, midline, thickness=5):
278
+ if midline is None or len(midline) < 2 or cv2.countNonZero(region_mask) == 0:
279
+ return None
280
+
281
+ split_mask = region_mask.copy()
282
+ cv2.polylines(split_mask, [midline.astype(np.int32)], False, 0, thickness)
283
+ n_labels, _, stats, _ = cv2.connectedComponentsWithStats((split_mask > 0).astype(np.uint8), connectivity=8)
284
+ if n_labels <= 2:
285
+ return None
286
+
287
+ areas = sorted([int(stats[i, cv2.CC_STAT_AREA]) for i in range(1, n_labels)], reverse=True)
288
+ if len(areas) < 2 or areas[0] + areas[1] == 0:
289
+ return None
290
+
291
+ return float(abs(areas[0] - areas[1]) / (areas[0] + areas[1]))
292
+
293
+ @staticmethod
294
+ def midline_curvature_score(midline):
295
+ if midline is None or len(midline) < 3:
296
+ return None
297
+
298
+ diffs = np.diff(midline.astype(np.float32), axis=0)
299
+ path_len = float(np.sum(np.linalg.norm(diffs, axis=1)))
300
+ chord_len = float(np.linalg.norm(midline[-1] - midline[0]))
301
+ if chord_len <= 1e-6:
302
+ return None
303
+
304
+ return float(max(0.0, (path_len / chord_len) - 1.0))
305
+
306
  @staticmethod
307
  def get_stable_perimeter_data(rind_mask, flesh_combined):
308
  cnts, _ = cv2.findContours(rind_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
 
521
  width_px = float(np.max(fit_pts[:, 0]) - np.min(fit_pts[:, 0]))
522
  height_px = float(np.max(fit_pts[:, 1]) - np.min(fit_pts[:, 1]))
523
  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]))
524
+ total_area_px = self.contour_area_px(fit_pts)
525
+ flesh_area_px = float(cv2.countNonZero(flesh_combined))
526
+ flesh_area_ratio = float(flesh_area_px / total_area_px) if total_area_px > 0 else None
527
+ elongation_factor = self.elongation_from_points(fit_pts)
528
+ circularity = float((4.0 * np.pi * total_area_px) / (perimeter_px ** 2)) if perimeter_px > 0 and total_area_px > 0 else None
529
+ midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
530
+ asymmetry_score = self.split_asymmetry(target_rind_mask, midline)
531
+ flesh_asymmetry_score = self.split_asymmetry(flesh_combined, midline, thickness=3)
532
+ midline_curvature = self.midline_curvature_score(midline)
533
 
534
  if cm_per_px is not None:
535
  measurement_unit = "cm"
536
+ area_unit = "cm2"
537
  scale_source = "color_checker"
538
+ area_scale = cm_per_px ** 2
539
  width_val = float(width_px * cm_per_px)
540
  height_val = float(height_px * cm_per_px)
541
  perimeter_val = float(perimeter_px * cm_per_px)
542
  else:
543
  measurement_unit = "px"
544
+ area_unit = "px2"
545
  scale_source = "original_pixels"
546
  orig_scale = 1.0 / scale_ratio
547
+ area_scale = orig_scale ** 2
548
  width_val = float(width_px * orig_scale)
549
  height_val = float(height_px * orig_scale)
550
  perimeter_val = float(perimeter_px * orig_scale)
551
+ total_area = float(total_area_px * area_scale)
552
+ flesh_area = float(flesh_area_px * area_scale)
553
  mark("fit")
554
 
555
  # --- 4. DRAWING ---
556
  img_base64 = None
557
  if include_image:
 
 
558
  # Color coding: Green=chosen rind, Blue=Left Flesh, Red=Right Flesh.
559
  output = image.copy().astype(np.float32)
560
  alpha = 0.42
 
592
  return ProcessResult(
593
  success=True, message="Success", r2_score=float(r2),
594
  width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
595
+ total_area=total_area, flesh_area=flesh_area, flesh_area_ratio=flesh_area_ratio,
596
+ elongation_factor=elongation_factor, circularity=circularity,
597
+ asymmetry_score=asymmetry_score, flesh_asymmetry_score=flesh_asymmetry_score,
598
+ midline_curvature=midline_curvature,
599
  delta_e_initial=dE_initial, delta_e_final=dE_final,
600
  image_base64=img_base64, filename=source_name,
601
+ measurement_unit=measurement_unit, area_unit=area_unit, scale_source=scale_source,
602
  color_checker_found=checker_corners is not None,
603
  rind_source=rind_source, rind_overlap_ratio=rind_overlap_ratio,
604
  warnings=warnings or None, timings_ms=timings