stevafernandes commited on
Commit
d1ede70
·
verified ·
1 Parent(s): ead795a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +87 -156
app.py CHANGED
@@ -1,48 +1,30 @@
1
- """Gradio app for cell fluorescence quantification.
2
-
3
- Upload one or more fluorescence images (blue = DAPI / nuclei,
4
- red = cytoplasmic marker). The app automatically detects up to N cells
5
- per image, draws the two concentric outlines (outer = cell, inner =
6
- nucleus), and reports per-cell measurements:
7
- - Total cell area = nucleus + surrounding cytoplasm pixels
8
- - Nucleus area
9
- - Cytoplasm area = Cell area - Nucleus area
10
- - Total Cell IntDen = sum of red-channel intensity in the cell
11
- - Nucleus IntDen = sum of red-channel intensity in the nucleus
12
- - Cytoplasm IntDen = Cell IntDen - Nucleus IntDen
13
- - Mean Cytoplasm Fluorescence = Cytoplasm IntDen / Cytoplasm area
14
  """
15
  from __future__ import annotations
16
 
17
- import io
18
  import os
19
- import tempfile
20
 
 
21
  import gradio as gr
22
  import numpy as np
23
- import pandas as pd
24
  from PIL import Image
25
 
26
- from quantification import analyze_image, cells_to_records, draw_overlay
27
 
28
  DEFAULT_N_CELLS = 5
29
  DEFAULT_DILATION_RADIUS = 12
 
 
30
 
31
- COLUMN_ORDER = [
32
- "Image",
33
- "Cell",
34
- "Total cell area",
35
- "Nucleus area",
36
- "Cytoplasm area",
37
- "Total Cell IntDen",
38
- "Nucleus IntDen",
39
- "Cytoplasm IntDen",
40
- "Mean Cytoplasm Fluorescence",
41
- ]
42
 
43
 
44
  def _ensure_rgb(arr: np.ndarray) -> np.ndarray:
45
- """Make sure we have a uint8 RGB array."""
46
  if arr.ndim == 2:
47
  arr = np.stack([arr, arr, arr], axis=-1)
48
  if arr.shape[2] == 4:
@@ -52,111 +34,71 @@ def _ensure_rgb(arr: np.ndarray) -> np.ndarray:
52
  return arr
53
 
54
 
55
- def process_files(
56
- files: list[str] | None,
57
- n_cells: int,
58
- dilation_radius: int,
59
- ):
60
- """Run the pipeline on each uploaded image and return UI outputs."""
61
- if not files:
62
- return [], pd.DataFrame(columns=COLUMN_ORDER), None, "Upload one or more images to begin."
63
-
64
- gallery_items: list[tuple[np.ndarray, str]] = []
65
- all_rows: list[dict] = []
66
-
67
- for file_path in files:
68
- try:
69
- image_pil = Image.open(file_path).convert("RGB")
70
- except Exception as exc: # noqa: BLE001
71
- gallery_items.append((
72
- np.zeros((200, 200, 3), dtype=np.uint8),
73
- f"{os.path.basename(file_path)} (failed: {exc})",
74
- ))
75
- continue
76
-
77
- image_rgb = _ensure_rgb(np.array(image_pil))
78
- image_name = os.path.basename(file_path)
79
-
80
- cells = analyze_image(
81
- image_rgb,
82
- n_cells=int(n_cells),
83
- dilation_radius=int(dilation_radius),
84
- )
85
- annotated = draw_overlay(image_rgb, cells)
86
- gallery_items.append((annotated, f"{image_name} — {len(cells)} cell(s) detected"))
87
-
88
- for row in cells_to_records(cells):
89
- row_with_image = {"Image": image_name, **row}
90
- all_rows.append(row_with_image)
91
-
92
- if not all_rows:
93
- df = pd.DataFrame(columns=COLUMN_ORDER)
94
- else:
95
- df = pd.DataFrame(all_rows)
96
- df = df[COLUMN_ORDER]
97
-
98
- # Build a downloadable Excel/CSV file
99
- csv_path = None
100
- if not df.empty:
101
- tmp_dir = tempfile.mkdtemp(prefix="cellquant_")
102
- csv_path = os.path.join(tmp_dir, "cell_quantification.csv")
103
- df.to_csv(csv_path, index=False)
104
-
105
- n_images = len(files)
106
- n_cells_detected = len(all_rows)
107
- if n_cells_detected == 0:
108
- status = (
109
- f"Processed {n_images} image(s) but no cells were detected. "
110
- "Check that the image has a clear blue nucleus channel."
111
  )
112
- else:
113
- avg = df["Mean Cytoplasm Fluorescence"].mean()
114
- status = (
115
- f"Processed {n_images} image(s) — detected {n_cells_detected} cell(s) total. "
116
- f"Average Mean Cytoplasm Fluorescence across all cells: {avg:.2f}."
117
  )
 
118
 
119
- return gallery_items, df, csv_path, status
120
 
 
 
 
 
121
 
122
- def build_demo() -> gr.Blocks:
123
- description = """
124
- # Cell Fluorescence Quantification
125
 
126
- Upload fluorescence microscopy images (RGB) where:
127
- - **Blue** channel labels nuclei (e.g. DAPI)
128
- - **Red** channel labels the cytoplasmic marker to measure
129
 
130
- For each image, the app automatically detects representative cells, draws
131
- two concentric outlines (outer = whole cell, inner = nucleus), and reports
132
- per-cell measurements following this protocol:
 
 
133
 
134
- ```
135
- Cytoplasm Area = Cell Area - Nucleus Area
136
- Cytoplasm IntDen = Cell IntDen - Nucleus IntDen
137
- Mean Cytoplasm = Cytoplasm IntDen / Cytoplasm Area
138
- ```
139
 
140
- Intensities are measured in the **red** channel.
141
- """
142
 
143
- with gr.Blocks(title="Cell Fluorescence Quantification") as demo:
 
 
 
 
 
 
 
 
 
144
  gr.Markdown(description)
145
 
146
  with gr.Row():
147
  with gr.Column(scale=1):
148
- files_in = gr.File(
149
- label="Upload image(s) — JPG / PNG / TIFF",
150
- file_count="multiple",
151
- file_types=["image"],
152
  type="filepath",
 
153
  )
154
  n_cells_slider = gr.Slider(
155
  minimum=1,
156
- maximum=15,
157
  value=DEFAULT_N_CELLS,
158
  step=1,
159
- label="Cells per image",
160
  )
161
  dilation_slider = gr.Slider(
162
  minimum=4,
@@ -164,65 +106,54 @@ Intensities are measured in the **red** channel.
164
  value=DEFAULT_DILATION_RADIUS,
165
  step=1,
166
  label="Cytoplasm ring thickness (pixels)",
167
- info=(
168
- "Each nucleus is dilated outward by this many pixels "
169
- "to define the cell boundary."
170
- ),
171
  )
172
- run_btn = gr.Button("Analyze", variant="primary")
173
- status_box = gr.Markdown("")
174
 
175
  with gr.Column(scale=2):
176
  gallery = gr.Gallery(
177
- label="Annotated images",
178
  columns=2,
179
- height=520,
180
  show_label=True,
 
181
  )
182
 
183
- gr.Markdown("### Per-cell measurements")
184
- table = gr.Dataframe(
185
- headers=COLUMN_ORDER,
186
- datatype=["str", "str", "number", "number", "number",
187
- "number", "number", "number", "number"],
188
- wrap=True,
189
- interactive=False,
190
- )
191
- csv_out = gr.File(label="Download results (CSV)")
192
-
193
  run_btn.click(
194
- fn=process_files,
195
- inputs=[files_in, n_cells_slider, dilation_slider],
196
- outputs=[gallery, table, csv_out, status_box],
197
  )
198
 
199
- # Also re-run automatically when sliders change (only if files already uploaded)
200
- for control in (n_cells_slider, dilation_slider):
201
- control.release(
202
- fn=process_files,
203
- inputs=[files_in, n_cells_slider, dilation_slider],
204
- outputs=[gallery, table, csv_out, status_box],
205
- )
206
-
207
- # Example images (lazy: only loaded if present in repo)
208
- example_dir = os.path.join(os.path.dirname(__file__), "examples")
209
- if os.path.isdir(example_dir):
210
  example_files = sorted(
211
- os.path.join(example_dir, f)
212
- for f in os.listdir(example_dir)
213
  if f.lower().endswith((".jpg", ".jpeg", ".png", ".tif", ".tiff"))
214
  )
215
- if example_files:
216
- gr.Examples(
217
- examples=[[[p], DEFAULT_N_CELLS, DEFAULT_DILATION_RADIUS]
218
- for p in example_files],
219
- inputs=[files_in, n_cells_slider, dilation_slider],
220
- label="Example images",
221
- )
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  return demo
224
 
225
 
226
  if __name__ == "__main__":
227
  demo = build_demo()
228
- demo.launch()
 
1
+ """Gradio app: detect cells in a fluorescence image and return red-channel
2
+ grayscale images with cell + nucleus outlines drawn in yellow.
3
+
4
+ One output image is produced per detected cell, matching the documentation
5
+ style: grayscale background + two concentric yellow outlines, nothing else.
 
 
 
 
 
 
 
 
6
  """
7
  from __future__ import annotations
8
 
 
9
  import os
 
10
 
11
+ import cv2
12
  import gradio as gr
13
  import numpy as np
 
14
  from PIL import Image
15
 
16
+ from quantification import analyze_image
17
 
18
  DEFAULT_N_CELLS = 5
19
  DEFAULT_DILATION_RADIUS = 12
20
+ OUTLINE_COLOR_BGR_AS_RGB = (255, 255, 0) # yellow in RGB
21
+ OUTLINE_THICKNESS = 2
22
 
23
+ EXAMPLES_DIR = os.path.join(os.path.dirname(__file__), "examples")
24
+ DEFAULT_EXAMPLE = os.path.join(EXAMPLES_DIR, "Picture1.jpg")
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  def _ensure_rgb(arr: np.ndarray) -> np.ndarray:
 
28
  if arr.ndim == 2:
29
  arr = np.stack([arr, arr, arr], axis=-1)
30
  if arr.shape[2] == 4:
 
34
  return arr
35
 
36
 
37
+ def _draw_cell_outline(
38
+ gray_rgb: np.ndarray,
39
+ cell_mask: np.ndarray,
40
+ nucleus_mask: np.ndarray,
41
+ ) -> np.ndarray:
42
+ """Draw the outer (cell) and inner (nucleus) outlines on a copy of `gray_rgb`."""
43
+ out = gray_rgb.copy()
44
+ for mask in (cell_mask, nucleus_mask):
45
+ contours, _ = cv2.findContours(
46
+ mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  )
48
+ cv2.drawContours(
49
+ out, contours, -1, OUTLINE_COLOR_BGR_AS_RGB, OUTLINE_THICKNESS
 
 
 
50
  )
51
+ return out
52
 
 
53
 
54
+ def process_image(image_path: str | None, n_cells: int, dilation_radius: int):
55
+ """Return a list of one annotated image per detected cell."""
56
+ if image_path is None:
57
+ return []
58
 
59
+ image_pil = Image.open(image_path).convert("RGB")
60
+ image_rgb = _ensure_rgb(np.array(image_pil))
 
61
 
62
+ # Background for outputs: the red channel rendered as a grayscale RGB.
63
+ red = image_rgb[..., 0]
64
+ gray_rgb = np.stack([red, red, red], axis=-1)
65
 
66
+ cells = analyze_image(
67
+ image_rgb,
68
+ n_cells=int(n_cells),
69
+ dilation_radius=int(dilation_radius),
70
+ )
71
 
72
+ return [
73
+ _draw_cell_outline(gray_rgb, c.cell_mask, c.nucleus_mask) for c in cells
74
+ ]
 
 
75
 
 
 
76
 
77
+ def build_demo() -> gr.Blocks:
78
+ description = (
79
+ "Upload a fluorescence image (RGB: blue = nuclei, red = cytoplasm). "
80
+ "The app detects representative cells and returns the red channel as "
81
+ "grayscale with the cell + nucleus boundaries drawn in yellow — one "
82
+ "output image per cell."
83
+ )
84
+
85
+ with gr.Blocks(title="Cell Boundary Detection") as demo:
86
+ gr.Markdown("# Cell Boundary Detection")
87
  gr.Markdown(description)
88
 
89
  with gr.Row():
90
  with gr.Column(scale=1):
91
+ image_in = gr.Image(
92
+ label="Input image",
 
 
93
  type="filepath",
94
+ value=DEFAULT_EXAMPLE if os.path.exists(DEFAULT_EXAMPLE) else None,
95
  )
96
  n_cells_slider = gr.Slider(
97
  minimum=1,
98
+ maximum=10,
99
  value=DEFAULT_N_CELLS,
100
  step=1,
101
+ label="Number of cells",
102
  )
103
  dilation_slider = gr.Slider(
104
  minimum=4,
 
106
  value=DEFAULT_DILATION_RADIUS,
107
  step=1,
108
  label="Cytoplasm ring thickness (pixels)",
 
 
 
 
109
  )
110
+ run_btn = gr.Button("Detect cells", variant="primary")
 
111
 
112
  with gr.Column(scale=2):
113
  gallery = gr.Gallery(
114
+ label="Detected cells (one per image)",
115
  columns=2,
116
+ height=620,
117
  show_label=True,
118
+ object_fit="contain",
119
  )
120
 
 
 
 
 
 
 
 
 
 
 
121
  run_btn.click(
122
+ fn=process_image,
123
+ inputs=[image_in, n_cells_slider, dilation_slider],
124
+ outputs=[gallery],
125
  )
126
 
127
+ # Example images (other defaults from prior dataset).
128
+ example_files = []
129
+ if os.path.isdir(EXAMPLES_DIR):
 
 
 
 
 
 
 
 
130
  example_files = sorted(
131
+ os.path.join(EXAMPLES_DIR, f)
132
+ for f in os.listdir(EXAMPLES_DIR)
133
  if f.lower().endswith((".jpg", ".jpeg", ".png", ".tif", ".tiff"))
134
  )
135
+ if example_files:
136
+ gr.Examples(
137
+ examples=[[p, DEFAULT_N_CELLS, DEFAULT_DILATION_RADIUS]
138
+ for p in example_files],
139
+ inputs=[image_in, n_cells_slider, dilation_slider],
140
+ outputs=[gallery],
141
+ fn=process_image,
142
+ cache_examples=False,
143
+ label="Example images",
144
+ )
145
+
146
+ # Preload outputs for the default image on app start.
147
+ if os.path.exists(DEFAULT_EXAMPLE):
148
+ demo.load(
149
+ fn=process_image,
150
+ inputs=[image_in, n_cells_slider, dilation_slider],
151
+ outputs=[gallery],
152
+ )
153
 
154
  return demo
155
 
156
 
157
  if __name__ == "__main__":
158
  demo = build_demo()
159
+ demo.launch()