crabbly commited on
Commit
4106a5d
·
verified ·
1 Parent(s): c3f87d9

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +65 -45
main.py CHANGED
@@ -17,15 +17,13 @@ from skimage import color
17
  # OOM PREVENTION
18
  torch.set_num_threads(1)
19
 
20
- from cv_helpers import blend_mask_overlays, stem_tip_tangent_deg
21
-
22
  # --- CONFIGURATION ---
23
  MODEL_PATH = "best.pt"
24
  MAX_IMAGE_SIZE = 2048
25
  CHECKER_WIDTH_CM = 6.3
26
 
27
  # ==============================================================================
28
- # --- DIRECT DELTA-E COLOR CALIBRATION ---
29
  # ==============================================================================
30
  def to_linear_srgb(u8_bgr):
31
  rgb = cv2.cvtColor(u8_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
@@ -43,12 +41,11 @@ def detect_checker_corners(img_bgr):
43
  cc = det.getListColorChecker()[0]
44
  return np.array(cc.getBox() if hasattr(cc, "getBox") else cc.getCorners(), dtype=np.float32)
45
 
46
- # Fallback: Sometimes OpenCV fails on high-res noise. Scale by 50% and try again.
47
  img_small = cv2.resize(img_bgr, (0,0), fx=0.5, fy=0.5)
48
  if det.process(img_small, cv2.mcc.MCC24):
49
  cc = det.getListColorChecker()[0]
50
  pts = np.array(cc.getBox() if hasattr(cc, "getBox") else cc.getCorners(), dtype=np.float32)
51
- return pts * 2.0 # Scale corners back up
52
 
53
  raise RuntimeError("ColorChecker not found")
54
 
@@ -75,31 +72,22 @@ def compute_deltaE_00(lin_src, lin_ref):
75
  return color.deltaE_ciede2000(color.rgb2lab(srgb_src.reshape(1, -1, 3)), color.rgb2lab(srgb_ref.reshape(1, -1, 3))).flatten()
76
 
77
  def apply_color_pipeline(target_bgr, ref24, tgt24):
78
- """Safely scales White Balance, then uses Powell's method to minimize Delta E."""
79
  tgt_lin = to_linear_srgb(target_bgr)
80
-
81
- # 1. White Balance (Von Kries scaling using 6 neutrals)
82
  gains = np.median(ref24[18:24], axis=0) / np.maximum(np.median(tgt24[18:24], axis=0), 1e-6)
83
  tgt_lin_wb = tgt_lin * gains.reshape(1, 1, 3)
84
  tgt24_wb = tgt24 * gains
85
 
86
- # 2. Objective Function: Explicitly optimize the matrix for the lowest Delta E score
87
  def objective(W_flat):
88
  W = W_flat.reshape(3, 3)
89
  pred_lin = np.clip(tgt24_wb @ W, 0, 1)
90
  return np.mean(compute_deltaE_00(pred_lin, ref24))
91
 
92
- # Get a fast starting point using standard Ridge Regression
93
  X, Y = tgt24_wb, ref24
94
  W_init = np.linalg.inv(X.T @ X + 0.05 * np.eye(3)) @ X.T @ Y
95
-
96
- # Optimize matrix to minimize CIEDE2000 natively
97
  res = minimize(objective, W_init.flatten(), method='Powell')
98
  W_opt = res.x.reshape(3, 3)
99
 
100
- # 3. Apply to full image securely
101
  corrected_lin = (tgt_lin_wb.reshape(-1, 3) @ W_opt).reshape(tgt_lin_wb.shape)
102
-
103
  return to_srgb_u8(np.clip(corrected_lin, 0, 1))
104
 
105
  # ==============================================================================
@@ -122,7 +110,6 @@ class ProcessResult:
122
  class WatermelonProcessor:
123
  def __init__(self, model_path: str):
124
  self.model = YOLO(model_path)
125
-
126
  self.ref24 = None
127
  if os.path.exists("reference.png"):
128
  try:
@@ -185,8 +172,7 @@ class WatermelonProcessor:
185
  m_inv = cv2.getRotationMatrix2D((cx, cy), -rot_angle, 1.0)
186
  l_rot, r_rot = cv2.warpAffine(f_left, m_rot, (w, h)), cv2.warpAffine(f_right, m_rot, (w, h))
187
 
188
- l_idx = np.where(l_rot > 0)[1]
189
- r_idx = np.where(r_rot > 0)[1]
190
  if len(l_idx) > 0 and len(r_idx) > 0:
191
  if np.mean(l_idx) > np.mean(r_idx):
192
  l_rot, r_rot = r_rot, l_rot
@@ -204,13 +190,14 @@ class WatermelonProcessor:
204
  y_min_g, y_max_g = np.min(gap_points[:, 0]), np.max(gap_points[:, 0])
205
  y_span, y_mean = max(y_max_g - y_min_g, 1), (y_max_g + y_min_g) / 2.0
206
  def parabola(y_n, a, b, c): return a*(y_n**2) + b*y_n + c
 
207
  try:
208
- popt_mid, _ = curve_fit(parabola, (gap_points[:,0]-y_mean)/y_span, gap_points[:,1], bounds=([-w*0.08, -np.inf, -np.inf],[w*0.08, np.inf, np.inf]))
209
  except: popt_mid = [0.0, 0.0, cx]
210
 
211
  ys_extrap = np.linspace(0, h, 500)
212
  xs_extrap = parabola((ys_extrap - y_mean)/y_span, *popt_mid)
213
- pts_rot = np.vstack([xs_extrap, ys_extrap, np.ones_like(xs_extrap)])
214
  else:
215
  ys_extrap = np.linspace(0, h, 500)
216
  pts_rot = np.vstack([np.full_like(ys_extrap, cx), ys_extrap, np.ones_like(ys_extrap)])
@@ -223,8 +210,8 @@ class WatermelonProcessor:
223
  if image is None: return ProcessResult(success=False, message="Could not decode image.")
224
  h, w = image.shape[:2]
225
 
 
226
  dE_initial, dE_final, cm_per_px, checker_corners = None, None, None, None
227
-
228
  try:
229
  checker_corners = detect_checker_corners(image)
230
  top_width = np.linalg.norm(checker_corners[1] - checker_corners[2])
@@ -232,18 +219,23 @@ class WatermelonProcessor:
232
  cm_per_px = CHECKER_WIDTH_CM / ((top_width + bot_width) / 2.0)
233
 
234
  if self.ref24 is not None:
235
- tgt24 = sample_24_patches(warp_checker(image, checker_corners))
 
236
  dE_initial = float(np.mean(compute_deltaE_00(tgt24, self.ref24)))
237
 
238
  image = apply_color_pipeline(image, self.ref24, tgt24)
239
 
240
- tgt24_corr = sample_24_patches(warp_checker(image, checker_corners))
 
241
  dE_final = float(np.mean(compute_deltaE_00(tgt24_corr, self.ref24)))
242
  except Exception as e:
243
  print(f"Calibration skipped for {source_name}: {e}")
244
 
 
245
  results = self.model(image, conf=0.25, retina_masks=True, verbose=False)
246
- rind_mask, flesh_contours = np.zeros((h, w), dtype=np.uint8), []
 
 
247
 
248
  if results[0].masks is None:
249
  return ProcessResult(success=False, message="No masks detected.")
@@ -251,19 +243,28 @@ class WatermelonProcessor:
251
  for mask_data, cls in zip(results[0].masks.xy, results[0].boxes.cls):
252
  contour = np.array(mask_data, dtype=np.int32)
253
  c_id = int(cls)
254
- if c_id == 0: cv2.drawContours(rind_mask, [contour], -1, 255, -1)
255
- elif c_id == 1: flesh_contours.append(contour)
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- flesh_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
258
  flesh_l_m, flesh_r_m = np.zeros((h, w), dtype=np.uint8), np.zeros((h, w), dtype=np.uint8)
259
-
260
- if len(flesh_contours) >= 2:
261
- cv2.drawContours(flesh_l_m,[flesh_contours[0]], -1, 255, -1)
262
- cv2.drawContours(flesh_r_m,[flesh_contours[1]], -1, 255, -1)
263
- elif len(flesh_contours) == 1:
264
- cv2.drawContours(flesh_l_m,[flesh_contours[0]], -1, 255, -1)
265
 
266
  flesh_combined = cv2.bitwise_or(flesh_l_m, flesh_r_m)
 
 
267
  perimeter_data = self.get_stable_perimeter_data(rind_mask, flesh_combined)
268
  if perimeter_data is None: return ProcessResult(success=False, message="No stable perimeter.")
269
 
@@ -295,24 +296,43 @@ class WatermelonProcessor:
295
  height_val = float(height_px * cm_per_px * orig_scale)
296
  perimeter_val = float(perimeter_px * cm_per_px * orig_scale)
297
 
 
298
  midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
299
- output = blend_mask_overlays(image, rind_mask, flesh_combined)
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  if checker_corners is not None:
302
  cv2.polylines(output, [np.int32(checker_corners)], True, (0, 165, 255), 4)
303
 
304
- if len(midline) > 1: cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
305
- cv2.polylines(output,[fit_pts.astype(np.int32)], True, (0, 255, 0), 3)
306
-
307
- stem = stem_tip_tangent_deg(rind_cnt, (cx, cy))
308
- if stem is not None:
309
- tx, ty, tdeg = stem
310
- rad = np.deg2rad(tdeg)
311
- L = min(w, h) * 0.08
312
- p1 = (int(round(tx)), int(round(ty)))
313
- p2 = (int(round(tx + L * np.cos(rad))), int(round(ty + L * np.sin(rad))))
314
- cv2.circle(output, p1, 6, (255, 0, 255), -1)
315
- cv2.line(output, p1, p2, (255, 0, 255), 2)
 
316
 
317
  _, buffer = cv2.imencode('.jpg', output,[cv2.IMWRITE_JPEG_QUALITY, 85])
318
  img_base64 = base64.b64encode(buffer).decode('utf-8')
 
17
  # OOM PREVENTION
18
  torch.set_num_threads(1)
19
 
 
 
20
  # --- CONFIGURATION ---
21
  MODEL_PATH = "best.pt"
22
  MAX_IMAGE_SIZE = 2048
23
  CHECKER_WIDTH_CM = 6.3
24
 
25
  # ==============================================================================
26
+ # --- COLOR CALIBRATION LOGIC ---
27
  # ==============================================================================
28
  def to_linear_srgb(u8_bgr):
29
  rgb = cv2.cvtColor(u8_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
 
41
  cc = det.getListColorChecker()[0]
42
  return np.array(cc.getBox() if hasattr(cc, "getBox") else cc.getCorners(), dtype=np.float32)
43
 
 
44
  img_small = cv2.resize(img_bgr, (0,0), fx=0.5, fy=0.5)
45
  if det.process(img_small, cv2.mcc.MCC24):
46
  cc = det.getListColorChecker()[0]
47
  pts = np.array(cc.getBox() if hasattr(cc, "getBox") else cc.getCorners(), dtype=np.float32)
48
+ return pts * 2.0
49
 
50
  raise RuntimeError("ColorChecker not found")
51
 
 
72
  return color.deltaE_ciede2000(color.rgb2lab(srgb_src.reshape(1, -1, 3)), color.rgb2lab(srgb_ref.reshape(1, -1, 3))).flatten()
73
 
74
  def apply_color_pipeline(target_bgr, ref24, tgt24):
 
75
  tgt_lin = to_linear_srgb(target_bgr)
 
 
76
  gains = np.median(ref24[18:24], axis=0) / np.maximum(np.median(tgt24[18:24], axis=0), 1e-6)
77
  tgt_lin_wb = tgt_lin * gains.reshape(1, 1, 3)
78
  tgt24_wb = tgt24 * gains
79
 
 
80
  def objective(W_flat):
81
  W = W_flat.reshape(3, 3)
82
  pred_lin = np.clip(tgt24_wb @ W, 0, 1)
83
  return np.mean(compute_deltaE_00(pred_lin, ref24))
84
 
 
85
  X, Y = tgt24_wb, ref24
86
  W_init = np.linalg.inv(X.T @ X + 0.05 * np.eye(3)) @ X.T @ Y
 
 
87
  res = minimize(objective, W_init.flatten(), method='Powell')
88
  W_opt = res.x.reshape(3, 3)
89
 
 
90
  corrected_lin = (tgt_lin_wb.reshape(-1, 3) @ W_opt).reshape(tgt_lin_wb.shape)
 
91
  return to_srgb_u8(np.clip(corrected_lin, 0, 1))
92
 
93
  # ==============================================================================
 
110
  class WatermelonProcessor:
111
  def __init__(self, model_path: str):
112
  self.model = YOLO(model_path)
 
113
  self.ref24 = None
114
  if os.path.exists("reference.png"):
115
  try:
 
172
  m_inv = cv2.getRotationMatrix2D((cx, cy), -rot_angle, 1.0)
173
  l_rot, r_rot = cv2.warpAffine(f_left, m_rot, (w, h)), cv2.warpAffine(f_right, m_rot, (w, h))
174
 
175
+ l_idx, r_idx = np.where(l_rot > 0)[1], np.where(r_rot > 0)[1]
 
176
  if len(l_idx) > 0 and len(r_idx) > 0:
177
  if np.mean(l_idx) > np.mean(r_idx):
178
  l_rot, r_rot = r_rot, l_rot
 
190
  y_min_g, y_max_g = np.min(gap_points[:, 0]), np.max(gap_points[:, 0])
191
  y_span, y_mean = max(y_max_g - y_min_g, 1), (y_max_g + y_min_g) / 2.0
192
  def parabola(y_n, a, b, c): return a*(y_n**2) + b*y_n + c
193
+ max_bend = w * 0.08
194
  try:
195
+ popt_mid, _ = curve_fit(parabola, (gap_points[:,0]-y_mean)/y_span, gap_points[:,1], bounds=([-max_bend, -np.inf, -np.inf],[max_bend, np.inf, np.inf]))
196
  except: popt_mid = [0.0, 0.0, cx]
197
 
198
  ys_extrap = np.linspace(0, h, 500)
199
  xs_extrap = parabola((ys_extrap - y_mean)/y_span, *popt_mid)
200
+ pts_rot = np.vstack([xs_extrap, ys_extrap, np.ones_like(ys_extrap)])
201
  else:
202
  ys_extrap = np.linspace(0, h, 500)
203
  pts_rot = np.vstack([np.full_like(ys_extrap, cx), ys_extrap, np.ones_like(ys_extrap)])
 
210
  if image is None: return ProcessResult(success=False, message="Could not decode image.")
211
  h, w = image.shape[:2]
212
 
213
+ # --- 1. CALIBRATION & SCALING ---
214
  dE_initial, dE_final, cm_per_px, checker_corners = None, None, None, None
 
215
  try:
216
  checker_corners = detect_checker_corners(image)
217
  top_width = np.linalg.norm(checker_corners[1] - checker_corners[2])
 
219
  cm_per_px = CHECKER_WIDTH_CM / ((top_width + bot_width) / 2.0)
220
 
221
  if self.ref24 is not None:
222
+ tgt_warped = warp_checker(image, checker_corners)
223
+ tgt24 = sample_24_patches(tgt_warped)
224
  dE_initial = float(np.mean(compute_deltaE_00(tgt24, self.ref24)))
225
 
226
  image = apply_color_pipeline(image, self.ref24, tgt24)
227
 
228
+ tgt_warped_corr = warp_checker(image, checker_corners)
229
+ tgt24_corr = sample_24_patches(tgt_warped_corr)
230
  dE_final = float(np.mean(compute_deltaE_00(tgt24_corr, self.ref24)))
231
  except Exception as e:
232
  print(f"Calibration skipped for {source_name}: {e}")
233
 
234
+ # --- 2. YOLO INFERENCE (STRICTLY PARSING ALL 3 CLASSES) ---
235
  results = self.model(image, conf=0.25, retina_masks=True, verbose=False)
236
+ rind_mask = np.zeros((h, w), dtype=np.uint8)
237
+ flesh_l_contours =[]
238
+ flesh_r_contours = []
239
 
240
  if results[0].masks is None:
241
  return ProcessResult(success=False, message="No masks detected.")
 
243
  for mask_data, cls in zip(results[0].masks.xy, results[0].boxes.cls):
244
  contour = np.array(mask_data, dtype=np.int32)
245
  c_id = int(cls)
246
+ if c_id == 0:
247
+ cv2.drawContours(rind_mask, [contour], -1, 255, -1)
248
+ elif c_id == 1:
249
+ flesh_l_contours.append(contour)
250
+ elif c_id == 2:
251
+ flesh_r_contours.append(contour)
252
+
253
+ # Failsafe: if YOLO missed one side but predicted multiple of the other
254
+ if len(flesh_l_contours) >= 2 and len(flesh_r_contours) == 0:
255
+ flesh_l_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
256
+ flesh_r_contours.append(flesh_l_contours.pop())
257
+ elif len(flesh_r_contours) >= 2 and len(flesh_l_contours) == 0:
258
+ flesh_r_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
259
+ flesh_l_contours.append(flesh_r_contours.pop(0))
260
 
 
261
  flesh_l_m, flesh_r_m = np.zeros((h, w), dtype=np.uint8), np.zeros((h, w), dtype=np.uint8)
262
+ for cnt in flesh_l_contours: cv2.drawContours(flesh_l_m, [cnt], -1, 255, -1)
263
+ for cnt in flesh_r_contours: cv2.drawContours(flesh_r_m, [cnt], -1, 255, -1)
 
 
 
 
264
 
265
  flesh_combined = cv2.bitwise_or(flesh_l_m, flesh_r_m)
266
+
267
+ # --- 3. FIT & EXTRACTION ---
268
  perimeter_data = self.get_stable_perimeter_data(rind_mask, flesh_combined)
269
  if perimeter_data is None: return ProcessResult(success=False, message="No stable perimeter.")
270
 
 
296
  height_val = float(height_px * cm_per_px * orig_scale)
297
  perimeter_val = float(perimeter_px * cm_per_px * orig_scale)
298
 
299
+ # --- 4. DRAWING ---
300
  midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
 
301
 
302
+ # Color coding: Green=Rind, Blue=Left Flesh, Red=Right Flesh
303
+ output = image.copy().astype(np.float32)
304
+ alpha = 0.42
305
+ output[..., 0] = np.where(rind_mask > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
306
+ output[..., 1] = np.where(rind_mask > 0, output[..., 1] * (1 - alpha) + 170.0 * alpha, output[..., 1])
307
+ output[..., 2] = np.where(rind_mask > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
308
+
309
+ output[..., 0] = np.where(flesh_l_m > 0, output[..., 0] * (1 - alpha) + 255.0 * alpha, output[..., 0])
310
+ output[..., 1] = np.where(flesh_l_m > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
311
+ output[..., 2] = np.where(flesh_l_m > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
312
+
313
+ output[..., 0] = np.where(flesh_r_m > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
314
+ output[..., 1] = np.where(flesh_r_m > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
315
+ output[..., 2] = np.where(flesh_r_m > 0, output[..., 2] * (1 - alpha) + 255.0 * alpha, output[..., 2])
316
+
317
+ output = np.clip(output, 0, 255).astype(np.uint8)
318
+
319
+ # Draw Color Checker Box
320
  if checker_corners is not None:
321
  cv2.polylines(output, [np.int32(checker_corners)], True, (0, 165, 255), 4)
322
 
323
+ # Draw Midline and Midline Intersections
324
+ if len(midline) > 1:
325
+ cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
326
+ # Add intersection dots (White inner, Black outer)
327
+ pt_top = (int(midline[0][0]), int(midline[0][1]))
328
+ pt_bot = (int(midline[-1][0]), int(midline[-1][1]))
329
+ cv2.circle(output, pt_top, 10, (0, 0, 0), 2)
330
+ cv2.circle(output, pt_top, 8, (255, 255, 255), -1)
331
+ cv2.circle(output, pt_bot, 10, (0, 0, 0), 2)
332
+ cv2.circle(output, pt_bot, 8, (255, 255, 255), -1)
333
+
334
+ # Draw Predicted Perimeter Boundary
335
+ cv2.polylines(output, [fit_pts.astype(np.int32)], True, (0, 255, 0), 3)
336
 
337
  _, buffer = cv2.imencode('.jpg', output,[cv2.IMWRITE_JPEG_QUALITY, 85])
338
  img_base64 = base64.b64encode(buffer).decode('utf-8')