crabbly commited on
Commit
1c164f3
·
verified ·
1 Parent(s): 5a6d3b0

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +45 -65
main.py CHANGED
@@ -17,7 +17,6 @@ from skimage import color
17
  # OOM PREVENTION
18
  torch.set_num_threads(1)
19
 
20
- # Import your helpers (assuming cv_helpers.py is in the same folder)
21
  from cv_helpers import blend_mask_overlays, stem_tip_tangent_deg
22
 
23
  # --- CONFIGURATION ---
@@ -26,7 +25,7 @@ MAX_IMAGE_SIZE = 2048
26
  CHECKER_WIDTH_CM = 6.3
27
 
28
  # ==============================================================================
29
- # --- COLOR CALIBRATION LOGIC (Embedded) ---
30
  # ==============================================================================
31
  def to_linear_srgb(u8_bgr):
32
  rgb = cv2.cvtColor(u8_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
@@ -68,31 +67,29 @@ def compute_deltaE_00(lin_src, lin_ref):
68
  return color.deltaE_ciede2000(color.rgb2lab(srgb_src.reshape(1, -1, 3)), color.rgb2lab(srgb_ref.reshape(1, -1, 3))).flatten()
69
 
70
  def apply_color_pipeline(target_bgr, ref24, tgt24):
71
- gains = np.median(ref24[18:24], axis=0) / np.maximum(np.median(tgt24[18:24], axis=0), 1e-6)
72
- lin = to_linear_srgb(target_bgr)
73
- lin_wb = lin * gains.reshape(1, 1, 3)
74
- tgt24_wb = tgt24 * gains
75
-
76
- M = np.linalg.lstsq(tgt24_wb[:18], ref24[:18], rcond=None)[0].T
77
- corrected = (lin_wb.reshape(-1, 3) @ M.T).reshape(lin_wb.shape)
78
- tgt24_ccm = tgt24_wb @ M.T
79
-
80
- w = np.array([0.2126, 0.7152, 0.0722], np.float32)
81
- Ls, Lt = (tgt24_ccm[19:23] @ w), (ref24[19:23] @ w)
82
- sort_idx = np.argsort(Ls)
83
- Ls, Lt = np.concatenate([[0.01], Ls[sort_idx], [0.98]]), np.concatenate([[0.01], Lt[sort_idx],[0.98]])
84
-
85
- L = np.clip(np.tensordot(corrected, w, axes=([2],[0])), 0, 1)
86
- Lt_mapped = np.interp(L, Ls, Lt)
87
-
88
- knee, strength = 0.90, 0.6
89
- below = Lt_mapped < knee
90
- Lt_final = np.empty_like(Lt_mapped)
91
- Lt_final[below] = Lt_mapped[below]
92
- Lt_final[~below] = knee + (1.0 - knee) * (1.0 - np.exp(-strength * ((Lt_mapped[~below] - knee) / (1.0 - knee))))
93
-
94
- scale = (Lt_final + 1e-6) / (L + 1e-6)
95
- return to_srgb_u8(np.clip(corrected * scale[..., None], 0, 1))
96
 
97
  # ==============================================================================
98
  # --- CORE API & PROCESSOR ---
@@ -215,7 +212,6 @@ class WatermelonProcessor:
215
  if image is None: return ProcessResult(success=False, message="Could not decode image.")
216
  h, w = image.shape[:2]
217
 
218
- # --- 1. CALIBRATION & SCALING ---
219
  dE_initial, dE_final, cm_per_px, checker_corners = None, None, None, None
220
 
221
  try:
@@ -235,12 +231,9 @@ class WatermelonProcessor:
235
  except Exception as e:
236
  print(f"Calibration skipped for {source_name}: {e}")
237
 
238
- # --- 2. YOLO INFERENCE (3 CLASSES FIXED) ---
239
  results = self.model(image, conf=0.25, retina_masks=True, verbose=False)
240
  rind_mask = np.zeros((h, w), dtype=np.uint8)
241
-
242
- flesh_l_contours =[]
243
- flesh_r_contours = []
244
 
245
  if results[0].masks is None:
246
  return ProcessResult(success=False, message="No masks detected.")
@@ -251,29 +244,20 @@ class WatermelonProcessor:
251
  if c_id == 0:
252
  cv2.drawContours(rind_mask, [contour], -1, 255, -1)
253
  elif c_id == 1:
254
- flesh_l_contours.append(contour)
255
  elif c_id == 2:
256
- flesh_r_contours.append(contour)
257
-
258
- # Failsafe: If YOLO predicted multiple class 1s and 0 class 2s (or vice versa), split them up by X-coordinate
259
- if len(flesh_l_contours) >= 2 and len(flesh_r_contours) == 0:
260
- flesh_l_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
261
- flesh_r_contours.append(flesh_l_contours.pop())
262
- elif len(flesh_r_contours) >= 2 and len(flesh_l_contours) == 0:
263
- flesh_r_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
264
- flesh_l_contours.append(flesh_r_contours.pop(0))
265
-
266
- flesh_l = np.zeros((h, w), dtype=np.uint8)
267
- flesh_r = np.zeros((h, w), dtype=np.uint8)
268
-
269
- for cnt in flesh_l_contours:
270
- cv2.drawContours(flesh_l, [cnt], -1, 255, -1)
271
- for cnt in flesh_r_contours:
272
- cv2.drawContours(flesh_r, [cnt], -1, 255, -1)
273
 
274
- flesh_combined = cv2.bitwise_or(flesh_l, flesh_r)
 
 
 
 
 
 
 
275
 
276
- # --- 3. FIT & EXTRACTION ---
277
  perimeter_data = self.get_stable_perimeter_data(rind_mask, flesh_combined)
278
  if perimeter_data is None: return ProcessResult(success=False, message="No stable perimeter.")
279
 
@@ -294,7 +278,6 @@ class WatermelonProcessor:
294
  r_fit = self.watermelon_model(t_fit, *popt) * scale
295
  fit_pts = np.array([[r * np.cos(t) + cx, cy - r * np.sin(t)] for t, r in zip(t_fit, r_fit)])
296
 
297
- # Re-scale back to original size for true measurements
298
  orig_scale = 1.0 / scale_ratio
299
  if cm_per_px is None: cm_per_px = 1.0
300
 
@@ -306,28 +289,26 @@ class WatermelonProcessor:
306
  height_val = float(height_px * cm_per_px * orig_scale)
307
  perimeter_val = float(perimeter_px * cm_per_px * orig_scale)
308
 
309
- # --- 4. DRAWING ---
310
- midline = self.get_dual_mask_midline(flesh_l, flesh_r, rind_cnt, fit_pts, cx, cy)
311
 
312
- # Color coding: Green=Rind, Blue=Left Flesh, Red=Right Flesh
313
  output = image.copy().astype(np.float32)
314
  alpha = 0.42
315
  output[..., 0] = np.where(rind_mask > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
316
  output[..., 1] = np.where(rind_mask > 0, output[..., 1] * (1 - alpha) + 170.0 * alpha, output[..., 1])
317
  output[..., 2] = np.where(rind_mask > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
318
 
319
- output[..., 0] = np.where(flesh_l > 0, output[..., 0] * (1 - alpha) + 255.0 * alpha, output[..., 0])
320
- output[..., 1] = np.where(flesh_l > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
321
- output[..., 2] = np.where(flesh_l > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
322
 
323
- output[..., 0] = np.where(flesh_r > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
324
- output[..., 1] = np.where(flesh_r > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
325
- output[..., 2] = np.where(flesh_r > 0, output[..., 2] * (1 - alpha) + 255.0 * alpha, output[..., 2])
326
 
327
  output = np.clip(output, 0, 255).astype(np.uint8)
328
 
329
  if checker_corners is not None:
330
- cv2.polylines(output,[np.int32(checker_corners)], True, (0, 165, 255), 4)
331
 
332
  if len(midline) > 1: cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
333
  cv2.polylines(output,[fit_pts.astype(np.int32)], True, (0, 255, 0), 3)
@@ -343,13 +324,12 @@ class WatermelonProcessor:
343
  cv2.line(output, p1, p2, (255, 0, 255), 2)
344
 
345
  _, buffer = cv2.imencode('.jpg', output, [cv2.IMWRITE_JPEG_QUALITY, 85])
346
- img_base64 = base64.b64encode(buffer).decode('utf-8')
347
 
348
  return ProcessResult(
349
  success=True, message="Success", r2_score=float(r2),
350
  width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
351
  delta_e_initial=dE_initial, delta_e_final=dE_final,
352
- image_base64=img_base64, filename=source_name
353
  )
354
 
355
 
 
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 ---
 
25
  CHECKER_WIDTH_CM = 6.3
26
 
27
  # ==============================================================================
28
+ # --- COLOR CALIBRATION LOGIC (Polynomial Color Correction) ---
29
  # ==============================================================================
30
  def to_linear_srgb(u8_bgr):
31
  rgb = cv2.cvtColor(u8_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
 
67
  return color.deltaE_ciede2000(color.rgb2lab(srgb_src.reshape(1, -1, 3)), color.rgb2lab(srgb_ref.reshape(1, -1, 3))).flatten()
68
 
69
  def apply_color_pipeline(target_bgr, ref24, tgt24):
70
+ """
71
+ Polynomial Color Correction Matrix (PCCM).
72
+ Fits a 10-term polynomial to robustly map Target colors -> Reference colors.
73
+ """
74
+ def extract_features(rgb_array):
75
+ R, G, B = rgb_array[..., 0], rgb_array[..., 1], rgb_array[..., 2]
76
+ return np.stack([
77
+ R, G, B,
78
+ R*G, R*B, G*B,
79
+ R**2, G**2, B**2,
80
+ np.ones_like(R)
81
+ ], axis=-1)
82
+
83
+ # 1. Fit the polynomial weights to the 24 patches
84
+ X_tgt = extract_features(tgt24) # Shape: (24, 10)
85
+ W, _, _, _ = np.linalg.lstsq(X_tgt, ref24, rcond=None) # Shape: (10, 3)
86
+
87
+ # 2. Apply the weights to the entire target image
88
+ lin_img = to_linear_srgb(target_bgr)
89
+ img_features = extract_features(lin_img) # Shape: (H, W, 10)
90
+ corrected_lin = img_features @ W # Shape: (H, W, 3)
91
+
92
+ return to_srgb_u8(np.clip(corrected_lin, 0, 1))
 
 
93
 
94
  # ==============================================================================
95
  # --- CORE API & PROCESSOR ---
 
212
  if image is None: return ProcessResult(success=False, message="Could not decode image.")
213
  h, w = image.shape[:2]
214
 
 
215
  dE_initial, dE_final, cm_per_px, checker_corners = None, None, None, None
216
 
217
  try:
 
231
  except Exception as e:
232
  print(f"Calibration skipped for {source_name}: {e}")
233
 
 
234
  results = self.model(image, conf=0.25, retina_masks=True, verbose=False)
235
  rind_mask = np.zeros((h, w), dtype=np.uint8)
236
+ flesh_contours = []
 
 
237
 
238
  if results[0].masks is None:
239
  return ProcessResult(success=False, message="No masks detected.")
 
244
  if c_id == 0:
245
  cv2.drawContours(rind_mask, [contour], -1, 255, -1)
246
  elif c_id == 1:
247
+ flesh_contours.append(contour)
248
  elif c_id == 2:
249
+ flesh_contours.append(contour)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ flesh_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
252
+ flesh_l_m, flesh_r_m = np.zeros((h, w), dtype=np.uint8), np.zeros((h, w), dtype=np.uint8)
253
+
254
+ if len(flesh_contours) >= 2:
255
+ cv2.drawContours(flesh_l_m, [flesh_contours[0]], -1, 255, -1)
256
+ cv2.drawContours(flesh_r_m,[flesh_contours[1]], -1, 255, -1)
257
+ elif len(flesh_contours) == 1:
258
+ cv2.drawContours(flesh_l_m,[flesh_contours[0]], -1, 255, -1)
259
 
260
+ flesh_combined = cv2.bitwise_or(flesh_l_m, flesh_r_m)
261
  perimeter_data = self.get_stable_perimeter_data(rind_mask, flesh_combined)
262
  if perimeter_data is None: return ProcessResult(success=False, message="No stable perimeter.")
263
 
 
278
  r_fit = self.watermelon_model(t_fit, *popt) * scale
279
  fit_pts = np.array([[r * np.cos(t) + cx, cy - r * np.sin(t)] for t, r in zip(t_fit, r_fit)])
280
 
 
281
  orig_scale = 1.0 / scale_ratio
282
  if cm_per_px is None: cm_per_px = 1.0
283
 
 
289
  height_val = float(height_px * cm_per_px * orig_scale)
290
  perimeter_val = float(perimeter_px * cm_per_px * orig_scale)
291
 
292
+ midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
 
293
 
 
294
  output = image.copy().astype(np.float32)
295
  alpha = 0.42
296
  output[..., 0] = np.where(rind_mask > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
297
  output[..., 1] = np.where(rind_mask > 0, output[..., 1] * (1 - alpha) + 170.0 * alpha, output[..., 1])
298
  output[..., 2] = np.where(rind_mask > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
299
 
300
+ output[..., 0] = np.where(flesh_l_m > 0, output[..., 0] * (1 - alpha) + 255.0 * alpha, output[..., 0])
301
+ output[..., 1] = np.where(flesh_l_m > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
302
+ output[..., 2] = np.where(flesh_l_m > 0, output[..., 2] * (1 - alpha) + 0.0 * alpha, output[..., 2])
303
 
304
+ output[..., 0] = np.where(flesh_r_m > 0, output[..., 0] * (1 - alpha) + 0.0 * alpha, output[..., 0])
305
+ output[..., 1] = np.where(flesh_r_m > 0, output[..., 1] * (1 - alpha) + 0.0 * alpha, output[..., 1])
306
+ output[..., 2] = np.where(flesh_r_m > 0, output[..., 2] * (1 - alpha) + 255.0 * alpha, output[..., 2])
307
 
308
  output = np.clip(output, 0, 255).astype(np.uint8)
309
 
310
  if checker_corners is not None:
311
+ cv2.polylines(output, [np.int32(checker_corners)], True, (0, 165, 255), 4)
312
 
313
  if len(midline) > 1: cv2.polylines(output, [midline.astype(np.int32)], False, (0, 255, 255), 3)
314
  cv2.polylines(output,[fit_pts.astype(np.int32)], True, (0, 255, 0), 3)
 
324
  cv2.line(output, p1, p2, (255, 0, 255), 2)
325
 
326
  _, buffer = cv2.imencode('.jpg', output, [cv2.IMWRITE_JPEG_QUALITY, 85])
 
327
 
328
  return ProcessResult(
329
  success=True, message="Success", r2_score=float(r2),
330
  width_val=width_val, height_val=height_val, perimeter_val=perimeter_val,
331
  delta_e_initial=dE_initial, delta_e_final=dE_final,
332
+ image_base64=base64.b64encode(buffer).decode('utf-8'), filename=source_name
333
  )
334
 
335