kaveh commited on
Commit
482c7f2
·
1 Parent(s): 379a002

fix autoboundary

Browse files
Files changed (2) hide show
  1. S2FApp/ui/heatmaps.py +13 -14
  2. S2FApp/utils/segmentation.py +20 -7
S2FApp/ui/heatmaps.py CHANGED
@@ -98,22 +98,21 @@ def make_annotated_heatmap_multi_regions(heatmap_rgb, masks, labels, cell_mask=N
98
 
99
 
100
  def add_cell_contour_to_fig(fig_pl, cell_mask, row=1, col=2):
101
- """Add red contour overlay to Plotly heatmap subplot."""
102
  if cell_mask is None or not np.any(cell_mask > 0):
103
  return
104
  contours, _ = cv2.findContours(cell_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
105
  if not contours:
106
  return
107
- # Use largest contour
108
- cnt = max(contours, key=cv2.contourArea)
109
- pts = cnt.squeeze()
110
- if pts.ndim == 1:
111
- pts = pts.reshape(1, 2)
112
- x, y = pts[:, 0].tolist(), pts[:, 1].tolist()
113
- if x[0] != x[-1] or y[0] != y[-1]:
114
- x.append(x[0])
115
- y.append(y[0])
116
- fig_pl.add_trace(
117
- go.Scatter(x=x, y=y, mode="lines", line=dict(color="red", width=4), showlegend=False),
118
- row=row, col=col
119
- )
 
98
 
99
 
100
  def add_cell_contour_to_fig(fig_pl, cell_mask, row=1, col=2):
101
+ """Add red contour overlay to Plotly heatmap subplot. Draws all contours (handles multiple disconnected regions)."""
102
  if cell_mask is None or not np.any(cell_mask > 0):
103
  return
104
  contours, _ = cv2.findContours(cell_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
105
  if not contours:
106
  return
107
+ for cnt in contours:
108
+ pts = cnt.squeeze()
109
+ if pts.ndim == 1:
110
+ pts = pts.reshape(1, 2)
111
+ x, y = pts[:, 0].tolist(), pts[:, 1].tolist()
112
+ if x[0] != x[-1] or y[0] != y[-1]:
113
+ x.append(x[0])
114
+ y.append(y[0])
115
+ fig_pl.add_trace(
116
+ go.Scatter(x=x, y=y, mode="lines", line=dict(color="red", width=4), showlegend=False),
117
+ row=row, col=col
118
+ )
 
S2FApp/utils/segmentation.py CHANGED
@@ -7,10 +7,13 @@ from skimage.measure import label, regionprops
7
 
8
 
9
  def estimate_cell_mask(heatmap, sigma=2, min_size=200, exclude_full_image=True,
10
- threshold_relax=0.85, dilate_radius=4):
11
  """
12
  Estimate cell region from force map using Otsu thresholding and morphological cleanup.
13
 
 
 
 
14
  Args:
15
  heatmap: 2D float array [0, 1] - predicted force map
16
  sigma: Gaussian smoothing sigma to reduce noise. Default 2.
@@ -21,6 +24,9 @@ def estimate_cell_mask(heatmap, sigma=2, min_size=200, exclude_full_image=True,
21
  Default 0.85.
22
  dilate_radius: Radius to dilate mask outward to include surrounding pixels.
23
  Default 4.
 
 
 
24
 
25
  Returns:
26
  mask: Binary uint8 array, 1 = estimated cell, 0 = background
@@ -39,9 +45,10 @@ def estimate_cell_mask(heatmap, sigma=2, min_size=200, exclude_full_image=True,
39
  # Morphological cleanup
40
  mask = closing(mask, disk(5)).astype(np.uint8)
41
  mask = opening(mask, disk(3)).astype(np.uint8)
42
- mask = remove_small_objects(mask.astype(bool), max_size=min_size - 1).astype(np.uint8)
43
 
44
- # Select component: second largest if largest is whole image
 
45
  labeled = label(mask)
46
  props = list(regionprops(labeled))
47
 
@@ -51,12 +58,18 @@ def estimate_cell_mask(heatmap, sigma=2, min_size=200, exclude_full_image=True,
51
  props_sorted = sorted(props, key=lambda x: x.area, reverse=True)
52
  total_px = heatmap.shape[0] * heatmap.shape[1]
53
 
 
54
  if exclude_full_image and len(props_sorted) >= 2 and props_sorted[0].area > 0.7 * total_px:
55
- region = props_sorted[1]
56
- else:
57
- region = props_sorted[0]
 
 
 
58
 
59
- mask = (labeled == region.label).astype(np.uint8)
 
 
60
 
61
  # Dilate to include surrounding pixels
62
  if dilate_radius > 0:
 
7
 
8
 
9
  def estimate_cell_mask(heatmap, sigma=2, min_size=200, exclude_full_image=True,
10
+ threshold_relax=0.85, dilate_radius=4, min_area_ratio=0.2):
11
  """
12
  Estimate cell region from force map using Otsu thresholding and morphological cleanup.
13
 
14
+ Supports multiple disconnected regions (e.g., two cells): components whose area is
15
+ at least min_area_ratio of the largest are merged into the final mask.
16
+
17
  Args:
18
  heatmap: 2D float array [0, 1] - predicted force map
19
  sigma: Gaussian smoothing sigma to reduce noise. Default 2.
 
24
  Default 0.85.
25
  dilate_radius: Radius to dilate mask outward to include surrounding pixels.
26
  Default 4.
27
+ min_area_ratio: Include components with area >= this fraction of the largest
28
+ component (0–1). E.g. 0.2 = include regions at least 20% the size of the
29
+ largest. Handles multiple disconnected force regions. Default 0.2.
30
 
31
  Returns:
32
  mask: Binary uint8 array, 1 = estimated cell, 0 = background
 
45
  # Morphological cleanup
46
  mask = closing(mask, disk(5)).astype(np.uint8)
47
  mask = opening(mask, disk(3)).astype(np.uint8)
48
+ mask = remove_small_objects(mask.astype(bool), min_size=min_size).astype(np.uint8)
49
 
50
+ # Select component(s): optionally exclude full-image background, then merge
51
+ # all significant components (handles multiple disconnected force regions)
52
  labeled = label(mask)
53
  props = list(regionprops(labeled))
54
 
 
58
  props_sorted = sorted(props, key=lambda x: x.area, reverse=True)
59
  total_px = heatmap.shape[0] * heatmap.shape[1]
60
 
61
+ # Skip largest if it covers most of image (likely background)
62
  if exclude_full_image and len(props_sorted) >= 2 and props_sorted[0].area > 0.7 * total_px:
63
+ props_sorted = props_sorted[1:]
64
+
65
+ # Reference area for "significant" components
66
+ ref_area = props_sorted[0].area
67
+ # Include all components with area >= min_area_ratio * ref_area
68
+ labels_to_keep = [p.label for p in props_sorted if p.area >= min_area_ratio * ref_area]
69
 
70
+ mask = np.zeros_like(labeled, dtype=np.uint8)
71
+ for lab in labels_to_keep:
72
+ mask[labeled == lab] = 1
73
 
74
  # Dilate to include surrounding pixels
75
  if dilate_radius > 0: