Spaces:
Sleeping
Sleeping
Update main.py
Browse files
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
|