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

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +62 -24
main.py CHANGED
@@ -17,7 +17,7 @@ from skimage import color
17
  # OOM PREVENTION
18
  torch.set_num_threads(1)
19
 
20
- # Import your helpers
21
  from cv_helpers import blend_mask_overlays, stem_tip_tangent_deg
22
 
23
  # --- CONFIGURATION ---
@@ -26,7 +26,7 @@ MAX_IMAGE_SIZE = 2048
26
  CHECKER_WIDTH_CM = 6.3
27
 
28
  # ==============================================================================
29
- # --- COLOR CALIBRATION LOGIC ---
30
  # ==============================================================================
31
  def to_linear_srgb(u8_bgr):
32
  rgb = cv2.cvtColor(u8_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
@@ -177,6 +177,12 @@ class WatermelonProcessor:
177
  m_inv = cv2.getRotationMatrix2D((cx, cy), -rot_angle, 1.0)
178
  l_rot, r_rot = cv2.warpAffine(f_left, m_rot, (w, h)), cv2.warpAffine(f_right, m_rot, (w, h))
179
 
 
 
 
 
 
 
180
  gap_points =[]
181
  y_l, y_r = np.where(l_rot > 0)[0], np.where(r_rot > 0)[0]
182
  if len(y_l) > 0 and len(y_r) > 0:
@@ -187,8 +193,8 @@ class WatermelonProcessor:
187
 
188
  gap_points = np.array(gap_points)
189
  if len(gap_points) > 10:
190
- y_min, y_max = np.min(gap_points[:, 0]), np.max(gap_points[:, 0])
191
- y_span, y_mean = max(y_max - y_min, 1), (y_max + y_min) / 2.0
192
  def parabola(y_n, a, b, c): return a*(y_n**2) + b*y_n + c
193
  try:
194
  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]))
@@ -229,9 +235,12 @@ class WatermelonProcessor:
229
  except Exception as e:
230
  print(f"Calibration skipped for {source_name}: {e}")
231
 
232
- # --- 2. YOLO INFERENCE ---
233
  results = self.model(image, conf=0.25, retina_masks=True, verbose=False)
234
- rind_mask, flesh_contours = np.zeros((h, w), dtype=np.uint8), []
 
 
 
235
 
236
  if results[0].masks is None:
237
  return ProcessResult(success=False, message="No masks detected.")
@@ -239,19 +248,32 @@ class WatermelonProcessor:
239
  for mask_data, cls in zip(results[0].masks.xy, results[0].boxes.cls):
240
  contour = np.array(mask_data, dtype=np.int32)
241
  c_id = int(cls)
242
- if c_id == 0: cv2.drawContours(rind_mask, [contour], -1, 255, -1)
243
- elif c_id == 1: flesh_contours.append(contour)
244
-
245
- flesh_contours.sort(key=lambda cnt: cv2.moments(cnt)['m10'] / (cv2.moments(cnt)['m00'] + 1e-5))
246
- flesh_l_m, flesh_r_m = np.zeros((h, w), dtype=np.uint8), np.zeros((h, w), dtype=np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- if len(flesh_contours) >= 2:
249
- cv2.drawContours(flesh_l_m, [flesh_contours[0]], -1, 255, -1)
250
- cv2.drawContours(flesh_r_m,[flesh_contours[1]], -1, 255, -1)
251
- elif len(flesh_contours) == 1:
252
- cv2.drawContours(flesh_l_m,[flesh_contours[0]], -1, 255, -1)
253
 
254
- flesh_combined = cv2.bitwise_or(flesh_l_m, flesh_r_m)
 
 
255
  perimeter_data = self.get_stable_perimeter_data(rind_mask, flesh_combined)
256
  if perimeter_data is None: return ProcessResult(success=False, message="No stable perimeter.")
257
 
@@ -272,7 +294,7 @@ class WatermelonProcessor:
272
  r_fit = self.watermelon_model(t_fit, *popt) * scale
273
  fit_pts = np.array([[r * np.cos(t) + cx, cy - r * np.sin(t)] for t, r in zip(t_fit, r_fit)])
274
 
275
- # --- 3. DIMENSIONAL MATH (IN CM) ---
276
  orig_scale = 1.0 / scale_ratio
277
  if cm_per_px is None: cm_per_px = 1.0
278
 
@@ -280,20 +302,35 @@ class WatermelonProcessor:
280
  height_px = float(np.max(fit_pts[:, 1]) - np.min(fit_pts[:, 1]))
281
  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]))
282
 
283
- # CRITICAL FIX: Wrapped in float() to prevent Numpy JSON serialization errors
284
  width_val = float(width_px * cm_per_px * orig_scale)
285
  height_val = float(height_px * cm_per_px * orig_scale)
286
  perimeter_val = float(perimeter_px * cm_per_px * orig_scale)
287
 
288
  # --- 4. DRAWING ---
289
- midline = self.get_dual_mask_midline(flesh_l_m, flesh_r_m, rind_cnt, fit_pts, cx, cy)
290
- output = blend_mask_overlays(image, rind_mask, flesh_combined)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  if checker_corners is not None:
293
  cv2.polylines(output,[np.int32(checker_corners)], True, (0, 165, 255), 4)
294
 
295
- if len(midline) > 1: cv2.polylines(output,[midline.astype(np.int32)], False, (0, 255, 255), 3)
296
- cv2.polylines(output, [fit_pts.astype(np.int32)], True, (0, 255, 0), 3)
297
 
298
  stem = stem_tip_tangent_deg(rind_cnt, (cx, cy))
299
  if stem is not None:
@@ -315,6 +352,7 @@ class WatermelonProcessor:
315
  image_base64=img_base64, filename=source_name
316
  )
317
 
 
318
  app = FastAPI()
319
 
320
  app.add_middleware(
@@ -324,7 +362,7 @@ app.add_middleware(
324
  processor = WatermelonProcessor(MODEL_PATH)
325
 
326
  @app.get("/")
327
- def read_root(): return {"status": "Watermelon API is awake!"}
328
 
329
  @app.post("/process_single")
330
  async def process_single(file: UploadFile = File(...)):
 
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
  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
 
177
  m_inv = cv2.getRotationMatrix2D((cx, cy), -rot_angle, 1.0)
178
  l_rot, r_rot = cv2.warpAffine(f_left, m_rot, (w, h)), cv2.warpAffine(f_right, m_rot, (w, h))
179
 
180
+ l_idx = np.where(l_rot > 0)[1]
181
+ r_idx = np.where(r_rot > 0)[1]
182
+ if len(l_idx) > 0 and len(r_idx) > 0:
183
+ if np.mean(l_idx) > np.mean(r_idx):
184
+ l_rot, r_rot = r_rot, l_rot
185
+
186
  gap_points =[]
187
  y_l, y_r = np.where(l_rot > 0)[0], np.where(r_rot > 0)[0]
188
  if len(y_l) > 0 and len(y_r) > 0:
 
193
 
194
  gap_points = np.array(gap_points)
195
  if len(gap_points) > 10:
196
+ y_min_g, y_max_g = np.min(gap_points[:, 0]), np.max(gap_points[:, 0])
197
+ y_span, y_mean = max(y_max_g - y_min_g, 1), (y_max_g + y_min_g) / 2.0
198
  def parabola(y_n, a, b, c): return a*(y_n**2) + b*y_n + c
199
  try:
200
  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]))
 
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.")
 
248
  for mask_data, cls in zip(results[0].masks.xy, results[0].boxes.cls):
249
  contour = np.array(mask_data, dtype=np.int32)
250
  c_id = int(cls)
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
  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
 
 
302
  height_px = float(np.max(fit_pts[:, 1]) - np.min(fit_pts[:, 1]))
303
  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]))
304
 
 
305
  width_val = float(width_px * cm_per_px * orig_scale)
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)
334
 
335
  stem = stem_tip_tangent_deg(rind_cnt, (cx, cy))
336
  if stem is not None:
 
352
  image_base64=img_base64, filename=source_name
353
  )
354
 
355
+
356
  app = FastAPI()
357
 
358
  app.add_middleware(
 
362
  processor = WatermelonProcessor(MODEL_PATH)
363
 
364
  @app.get("/")
365
+ def read_root(): return {"status": "Watermelon API is awake and running!"}
366
 
367
  @app.post("/process_single")
368
  async def process_single(file: UploadFile = File(...)):