callumtilbury commited on
Commit
8b5437e
·
verified ·
1 Parent(s): 8db452d

Hybrid sizing: detect@0.3 + measure@0.5 core — fixes 10% size bias

Browse files
Files changed (1) hide show
  1. inference.py +25 -10
inference.py CHANGED
@@ -261,26 +261,41 @@ def analyze_image(
261
  cell_prob = 1.0 / (1.0 + np.exp(-out[2])) # sigmoid
262
  dist_transform = np.maximum(out[3], 0)
263
 
264
- # Instance segmentation
265
  masks = segment_instances(cell_prob, dist_transform, prob_threshold, min_size_px)
266
 
267
- # Measure bubbles from instance masks
 
 
 
 
268
  bubbles = []
269
  num_rejected = 0
270
 
271
  for region in regionprops(masks):
272
- circ = (4 * np.pi * region.area) / (region.perimeter ** 2 + 1e-8)
273
- major = getattr(region, "axis_major_length", 0) or getattr(region, "major_axis_length", 0)
274
- minor = getattr(region, "axis_minor_length", 0) or getattr(region, "minor_axis_length", 0)
275
- ar = major / (minor + 1e-8)
 
 
 
 
 
276
 
277
- diam_px = getattr(region, "equivalent_diameter_area", 0) or getattr(region, "equivalent_diameter", 0)
278
  diam_um = diam_px * scale_um_per_pixel
279
  radius_um = diam_um / 2
280
  vol = (4 / 3) * np.pi * radius_um ** 3
281
 
282
- # Distance-transform radius (peak within this instance)
283
- inst_mask = (masks == region.label)
 
 
 
 
 
 
284
  dt_vals = dist_transform[inst_mask]
285
  radius_dt = float(dt_vals.max()) if len(dt_vals) > 0 else 0.0
286
 
@@ -296,7 +311,7 @@ def analyze_image(
296
  bubble_id=region.label,
297
  diameter_um=diam_um,
298
  volume_um3=vol,
299
- area_px=region.area,
300
  centroid_x=region.centroid[1],
301
  centroid_y=region.centroid[0],
302
  radius_dt_px=radius_dt,
 
261
  cell_prob = 1.0 / (1.0 + np.exp(-out[2])) # sigmoid
262
  dist_transform = np.maximum(out[3], 0)
263
 
264
+ # Instance segmentation (detect at prob_threshold for good recall)
265
  masks = segment_instances(cell_prob, dist_transform, prob_threshold, min_size_px)
266
 
267
+ # Measure bubbles from instance masks.
268
+ # KEY: size each bubble using only the high-confidence core (>0.5) to
269
+ # avoid threshold-dependent bloating. Detection at 0.3 finds bubbles;
270
+ # the 0.5 interior gives accurate area → diameter.
271
+ SIZE_THRESHOLD = 0.5
272
  bubbles = []
273
  num_rejected = 0
274
 
275
  for region in regionprops(masks):
276
+ inst_mask = (masks == region.label)
277
+
278
+ # Sizing: use the high-confidence interior if available
279
+ core = inst_mask & (cell_prob > SIZE_THRESHOLD)
280
+ core_area = int(core.sum())
281
+ if core_area >= 5:
282
+ sizing_area = core_area
283
+ else:
284
+ sizing_area = region.area # fallback for faint detections
285
 
286
+ diam_px = np.sqrt(sizing_area / np.pi) * 2
287
  diam_um = diam_px * scale_um_per_pixel
288
  radius_um = diam_um / 2
289
  vol = (4 / 3) * np.pi * radius_um ** 3
290
 
291
+ # Shape QC from the full detection mask (not the core)
292
+ circ = (4 * np.pi * region.area) / (region.perimeter ** 2 + 1e-8)
293
+ major = getattr(region, "axis_major_length", 0) or getattr(region, "major_axis_length", 0)
294
+ minor = getattr(region, "axis_minor_length", 0) or getattr(region, "minor_axis_length", 0)
295
+ ar = major / (minor + 1e-8)
296
+
297
+ # Distance-transform peak (for reference — note: student DT is normalised,
298
+ # not in absolute pixels, so this is relative only)
299
  dt_vals = dist_transform[inst_mask]
300
  radius_dt = float(dt_vals.max()) if len(dt_vals) > 0 else 0.0
301
 
 
311
  bubble_id=region.label,
312
  diameter_um=diam_um,
313
  volume_um3=vol,
314
+ area_px=sizing_area,
315
  centroid_x=region.centroid[1],
316
  centroid_y=region.centroid[0],
317
  radius_dt_px=radius_dt,