hmgill commited on
Commit
b9ba433
Β·
verified Β·
1 Parent(s): 0d7f1ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -59
app.py CHANGED
@@ -54,7 +54,7 @@ load_models()
54
  # --- Helpers ---
55
 
56
  def load_excel_data(logs_text):
57
- """Finds and loads the Excel report."""
58
  placeholder = pd.DataFrame({"Status": ["No Data Available"]})
59
  candidates = glob.glob("/tmp/*.xlsx") + glob.glob("*.xlsx")
60
 
@@ -65,9 +65,29 @@ def load_excel_data(logs_text):
65
 
66
  try:
67
  xls = pd.ExcelFile(report_file, engine='openpyxl')
68
- morph = pd.read_excel(xls, "Morphology") if "Morphology" in xls.sheet_names else placeholder
69
- spatial = pd.read_excel(xls, "Spatial") if "Spatial" in xls.sheet_names else placeholder
70
- relational = pd.read_excel(xls, "Relational") if "Relational" in xls.sheet_names else placeholder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return report_file, morph, spatial, relational
72
  except Exception as e:
73
  print(f"⚠️ Error reading Excel: {e}")
@@ -82,13 +102,42 @@ def get_available_layers():
82
  layers.append(name)
83
  return sorted(layers)
84
 
85
- def generate_overlay(image_path_str, selected_layers):
86
- """Regenerates the AnnotatedImage based on selected layers."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  if not image_path_str:
88
  return None
89
 
90
- base_image = Image.open(image_path_str).convert("RGB")
91
- annotations = []
 
 
 
 
 
 
 
 
 
 
92
 
93
  for layer_name in selected_layers:
94
  file_path = f"/tmp/data_{layer_name}.npz"
@@ -105,15 +154,34 @@ def generate_overlay(image_path_str, selected_layers):
105
 
106
  # Resize if mask dimensions differ from image
107
  h_mask, w_mask = combined_mask.shape
108
- if base_image.size != (w_mask, h_mask):
 
109
  base_image = base_image.resize((w_mask, h_mask), Image.Resampling.LANCZOS)
110
 
111
  combined_mask = combined_mask.astype(bool)
112
- annotations.append((combined_mask, layer_name.replace("_", " ").title()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  except Exception as e:
114
  print(f"Error loading layer {layer_name}: {e}")
115
 
116
- return (base_image, annotations)
 
 
117
 
118
 
119
  # --- Core Logic ---
@@ -124,10 +192,11 @@ async def run_analysis(image_path_str, user_prompt, session_id_state):
124
  Updates the global ACTIVE_RUNNER and returns a session ID.
125
  """
126
  waiting_df = pd.DataFrame({"Status": ["Waiting..."]})
 
127
 
128
  if not image_path_str:
129
  # Return empty state
130
- yield [], None, None, [], None, waiting_df, waiting_df, waiting_df
131
  return
132
 
133
  # Cleanup previous run files
@@ -179,7 +248,7 @@ async def run_analysis(image_path_str, user_prompt, session_id_state):
179
  {"role": "assistant", "content": full_log}
180
  ]
181
 
182
- yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df
183
  try:
184
  async for event in ACTIVE_RUNNER.run_async(user_id="demo_user", session_id=session.id, new_message=content):
185
  author = event.author
@@ -187,7 +256,7 @@ async def run_analysis(image_path_str, user_prompt, session_id_state):
187
  if event.get_function_calls():
188
  for fc in event.get_function_calls():
189
  logs.append(f"πŸ”§ **{author}**: Calling `{fc.name}`")
190
- yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df
191
 
192
  if event.content and event.content.parts:
193
  for part in event.content.parts:
@@ -203,16 +272,16 @@ async def run_analysis(image_path_str, user_prompt, session_id_state):
203
  else:
204
  logs.append(f"βœ… **{author}**: {part.text}")
205
 
206
- yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df
207
 
208
  except Exception as e:
209
  logs.append(f"❌ Error: {e}")
210
- yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df
211
  return
212
 
213
  # Finalize
214
  logs.append("\n🏁 Analysis Complete. Loading results...")
215
- yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df
216
 
217
  await asyncio.sleep(0.5)
218
 
@@ -227,7 +296,8 @@ async def run_analysis(image_path_str, user_prompt, session_id_state):
227
  {"role": "user", "content": user_prompt},
228
  {"role": "assistant", "content": full_log_text}
229
  ]
230
- yield final_history, session_id, initial_overlay, gr.CheckboxGroup(choices=layers, value=layers), report_file, df_m, df_s, df_r
 
231
 
232
 
233
  async def run_qa_turn(user_message, history, session_id):
@@ -283,12 +353,13 @@ with gr.Blocks(title="Cellemetry Agent") as demo:
283
  # State to hold the session ID
284
  session_id_state = gr.State(None)
285
  current_image_path = gr.State()
 
286
 
287
  gr.Markdown("## πŸ”¬ Cellemetry: Agentic Microscopy Analysis")
288
 
289
  with gr.Row():
290
 
291
- # --- LEFT COLUMN ---
292
  with gr.Column(scale=1):
293
  gr.Markdown("### 1. Configuration")
294
  img_input = gr.Image(type="filepath", label="Microscopy Image", height=300)
@@ -301,65 +372,97 @@ with gr.Blocks(title="Cellemetry Agent") as demo:
301
 
302
  run_btn = gr.Button("πŸ§ͺ Run Analysis", variant="primary", size="lg")
303
 
304
- gr.Markdown("### 3. Interactive Results")
305
- # 2. REMOVED 'type="tuples"' from Chatbot
306
  chatbot = gr.Chatbot(
307
- label="Agent Conversation",
308
  height=400,
309
  elem_id="chatbot"
310
- # type="messages" is now the default
311
  )
312
  qa_input = gr.Textbox(
313
  label="Ask a follow-up question",
314
  placeholder="e.g. 'What was the average cell area?'"
315
  )
316
 
317
- # --- RIGHT COLUMN ---
318
  with gr.Column(scale=2):
319
-
320
- # UPPER PANEL
321
- gr.Markdown("### 2. Interactive Segmentation")
322
- with gr.Row():
323
- with gr.Column(scale=3):
324
- overlay_output = gr.AnnotatedImage(
325
- label="Segmentation Result",
326
- height=500,
327
- color_map={"Green Cell": "#00ff00", "Blue Nucleus": "#0000ff"}
328
- )
329
- with gr.Column(scale=1):
330
- layer_checkboxes = gr.CheckboxGroup(
331
- label="Visible Layers",
332
- choices=[],
333
- value=[],
334
- interactive=True
335
- )
336
-
337
- # BOTTOM PANEL
338
- gr.Markdown("### 4. Quantitative Results")
339
  with gr.Tabs():
340
- with gr.Tab("Morphology"):
341
- tbl_morph = gr.Dataframe(interactive=False)
342
- with gr.Tab("Spatial"):
343
- tbl_spatial = gr.Dataframe(interactive=False)
344
- with gr.Tab("Relational"):
345
- tbl_rel = gr.Dataframe(interactive=False)
346
-
347
- download_btn = gr.File(label="Download Excel Report")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
  # --- Event Wiring ---
350
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  run_btn.click(
352
  fn=run_analysis,
353
  inputs=[img_input, prompt_input, session_id_state],
354
  outputs=[
355
  chatbot, # Output to Chatbot
356
  session_id_state, # Save Session ID
357
- overlay_output, # Annotated Image
358
  layer_checkboxes, # Checkbox Options
359
  download_btn, # Excel File
360
  tbl_morph, # Tables...
361
  tbl_spatial,
362
- tbl_rel
 
 
 
 
363
  ]
364
  )
365
 
@@ -372,11 +475,13 @@ with gr.Blocks(title="Cellemetry Agent") as demo:
372
 
373
  img_input.change(lambda x: x, inputs=img_input, outputs=current_image_path)
374
 
375
- layer_checkboxes.change(
376
- fn=generate_overlay,
377
- inputs=[current_image_path, layer_checkboxes],
378
- outputs=[overlay_output]
379
- )
 
 
380
 
381
  if __name__ == "__main__":
382
  # 3. ADDED 'theme' to launch()
 
54
  # --- Helpers ---
55
 
56
  def load_excel_data(logs_text):
57
+ """Finds and loads the Excel report, transposing for better display."""
58
  placeholder = pd.DataFrame({"Status": ["No Data Available"]})
59
  candidates = glob.glob("/tmp/*.xlsx") + glob.glob("*.xlsx")
60
 
 
65
 
66
  try:
67
  xls = pd.ExcelFile(report_file, engine='openpyxl')
68
+
69
+ # Read and transpose tables for better horizontal display
70
+ if "Morphology" in xls.sheet_names:
71
+ morph = pd.read_excel(xls, "Morphology").T
72
+ morph.columns = morph.iloc[0] # Set first row as column headers
73
+ morph = morph.drop(morph.index[0]) # Remove the header row
74
+ else:
75
+ morph = placeholder
76
+
77
+ if "Spatial" in xls.sheet_names:
78
+ spatial = pd.read_excel(xls, "Spatial").T
79
+ spatial.columns = spatial.iloc[0]
80
+ spatial = spatial.drop(spatial.index[0])
81
+ else:
82
+ spatial = placeholder
83
+
84
+ if "Relational" in xls.sheet_names:
85
+ relational = pd.read_excel(xls, "Relational").T
86
+ relational.columns = relational.iloc[0]
87
+ relational = relational.drop(relational.index[0])
88
+ else:
89
+ relational = placeholder
90
+
91
  return report_file, morph, spatial, relational
92
  except Exception as e:
93
  print(f"⚠️ Error reading Excel: {e}")
 
102
  layers.append(name)
103
  return sorted(layers)
104
 
105
+ def update_opacity_sliders(layers):
106
+ """Returns updated slider configurations based on available layers."""
107
+ updates = []
108
+ for i in range(4): # We have 4 sliders
109
+ if i < len(layers):
110
+ layer_name = layers[i].replace("_", " ").title()
111
+ updates.append(gr.update(visible=True, label=f"{layer_name} Opacity", value=0.6))
112
+ else:
113
+ updates.append(gr.update(visible=False))
114
+ return updates
115
+
116
+ def collect_layer_opacities(layers, op1, op2, op3, op4):
117
+ """Collects opacity values into a dictionary."""
118
+ opacities = {}
119
+ opacity_values = [op1, op2, op3, op4]
120
+ for i, layer in enumerate(layers[:4]): # Only use first 4 layers
121
+ opacities[layer] = opacity_values[i]
122
+ return opacities
123
+
124
+ def generate_overlay(image_path_str, selected_layers, layer_opacities=None):
125
+ """Regenerates the overlay image with adjustable opacity for each layer."""
126
  if not image_path_str:
127
  return None
128
 
129
+ base_image = Image.open(image_path_str).convert("RGBA")
130
+
131
+ # Default colors for different layers (can expand as needed)
132
+ color_map = {
133
+ "green_cell": (0, 255, 0),
134
+ "blue_nucleus": (0, 0, 255),
135
+ "cell": (0, 255, 0),
136
+ "nucleus": (0, 0, 255),
137
+ }
138
+
139
+ # Create overlay layer
140
+ overlay = Image.new('RGBA', base_image.size, (0, 0, 0, 0))
141
 
142
  for layer_name in selected_layers:
143
  file_path = f"/tmp/data_{layer_name}.npz"
 
154
 
155
  # Resize if mask dimensions differ from image
156
  h_mask, w_mask = combined_mask.shape
157
+ if overlay.size != (w_mask, h_mask):
158
+ overlay = overlay.resize((w_mask, h_mask), Image.Resampling.LANCZOS)
159
  base_image = base_image.resize((w_mask, h_mask), Image.Resampling.LANCZOS)
160
 
161
  combined_mask = combined_mask.astype(bool)
162
+
163
+ # Get color for this layer
164
+ color = color_map.get(layer_name.lower(), (255, 255, 0)) # Default to yellow
165
+
166
+ # Get opacity (default 0.5)
167
+ opacity = 0.5
168
+ if layer_opacities and layer_name in layer_opacities:
169
+ opacity = layer_opacities[layer_name]
170
+
171
+ # Create colored mask with opacity
172
+ mask_overlay = np.zeros((*combined_mask.shape, 4), dtype=np.uint8)
173
+ mask_overlay[combined_mask] = (*color, int(255 * opacity))
174
+
175
+ # Composite onto overlay
176
+ mask_image = Image.fromarray(mask_overlay, 'RGBA')
177
+ overlay = Image.alpha_composite(overlay, mask_image)
178
+
179
  except Exception as e:
180
  print(f"Error loading layer {layer_name}: {e}")
181
 
182
+ # Composite overlay onto base image
183
+ result = Image.alpha_composite(base_image, overlay)
184
+ return result.convert("RGB")
185
 
186
 
187
  # --- Core Logic ---
 
192
  Updates the global ACTIVE_RUNNER and returns a session ID.
193
  """
194
  waiting_df = pd.DataFrame({"Status": ["Waiting..."]})
195
+ empty_slider_updates = [gr.update()] * 4 # Placeholder for 4 sliders
196
 
197
  if not image_path_str:
198
  # Return empty state
199
+ yield [], None, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
200
  return
201
 
202
  # Cleanup previous run files
 
248
  {"role": "assistant", "content": full_log}
249
  ]
250
 
251
+ yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
252
  try:
253
  async for event in ACTIVE_RUNNER.run_async(user_id="demo_user", session_id=session.id, new_message=content):
254
  author = event.author
 
256
  if event.get_function_calls():
257
  for fc in event.get_function_calls():
258
  logs.append(f"πŸ”§ **{author}**: Calling `{fc.name}`")
259
+ yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
260
 
261
  if event.content and event.content.parts:
262
  for part in event.content.parts:
 
272
  else:
273
  logs.append(f"βœ… **{author}**: {part.text}")
274
 
275
+ yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
276
 
277
  except Exception as e:
278
  logs.append(f"❌ Error: {e}")
279
+ yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
280
  return
281
 
282
  # Finalize
283
  logs.append("\n🏁 Analysis Complete. Loading results...")
284
+ yield yield_status(logs), session_id, None, [], None, waiting_df, waiting_df, waiting_df, *empty_slider_updates
285
 
286
  await asyncio.sleep(0.5)
287
 
 
296
  {"role": "user", "content": user_prompt},
297
  {"role": "assistant", "content": full_log_text}
298
  ]
299
+ slider_updates = update_opacity_sliders(layers)
300
+ yield final_history, session_id, initial_overlay, gr.CheckboxGroup(choices=layers, value=layers), report_file, df_m, df_s, df_r, *slider_updates
301
 
302
 
303
  async def run_qa_turn(user_message, history, session_id):
 
353
  # State to hold the session ID
354
  session_id_state = gr.State(None)
355
  current_image_path = gr.State()
356
+ layer_opacity_state = gr.State({}) # Store opacity values per layer
357
 
358
  gr.Markdown("## πŸ”¬ Cellemetry: Agentic Microscopy Analysis")
359
 
360
  with gr.Row():
361
 
362
+ # --- LEFT COLUMN (1/3) ---
363
  with gr.Column(scale=1):
364
  gr.Markdown("### 1. Configuration")
365
  img_input = gr.Image(type="filepath", label="Microscopy Image", height=300)
 
372
 
373
  run_btn = gr.Button("πŸ§ͺ Run Analysis", variant="primary", size="lg")
374
 
375
+ gr.Markdown("### 2. Agent Conversation")
 
376
  chatbot = gr.Chatbot(
377
+ label="Live Analysis",
378
  height=400,
379
  elem_id="chatbot"
 
380
  )
381
  qa_input = gr.Textbox(
382
  label="Ask a follow-up question",
383
  placeholder="e.g. 'What was the average cell area?'"
384
  )
385
 
386
+ # --- RIGHT COLUMN (2/3) WITH TABS ---
387
  with gr.Column(scale=2):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  with gr.Tabs():
389
+ # TAB 1: Overlays
390
+ with gr.Tab("πŸ” Segmentation"):
391
+ with gr.Row():
392
+ with gr.Column(scale=3):
393
+ overlay_output = gr.Image(
394
+ label="Segmentation Result",
395
+ height=600,
396
+ type="pil"
397
+ )
398
+ with gr.Column(scale=1):
399
+ gr.Markdown("**Layer Controls**")
400
+ layer_checkboxes = gr.CheckboxGroup(
401
+ label="Visible Layers",
402
+ choices=[],
403
+ value=[],
404
+ interactive=True
405
+ )
406
+ gr.Markdown("**Opacity Controls**")
407
+ # Pre-create sliders for common layer types
408
+ opacity_slider_1 = gr.Slider(
409
+ minimum=0, maximum=1, value=0.6, step=0.1,
410
+ label="Layer 1 Opacity", visible=False
411
+ )
412
+ opacity_slider_2 = gr.Slider(
413
+ minimum=0, maximum=1, value=0.6, step=0.1,
414
+ label="Layer 2 Opacity", visible=False
415
+ )
416
+ opacity_slider_3 = gr.Slider(
417
+ minimum=0, maximum=1, value=0.6, step=0.1,
418
+ label="Layer 3 Opacity", visible=False
419
+ )
420
+ opacity_slider_4 = gr.Slider(
421
+ minimum=0, maximum=1, value=0.6, step=0.1,
422
+ label="Layer 4 Opacity", visible=False
423
+ )
424
+
425
+ # TAB 2: Data Tables
426
+ with gr.Tab("πŸ“Š Quantitative Results"):
427
+ download_btn = gr.File(label="Download Excel Report")
428
+ with gr.Tabs():
429
+ with gr.Tab("Morphology"):
430
+ tbl_morph = gr.Dataframe(interactive=False, wrap=True)
431
+ with gr.Tab("Spatial"):
432
+ tbl_spatial = gr.Dataframe(interactive=False, wrap=True)
433
+ with gr.Tab("Relational"):
434
+ tbl_rel = gr.Dataframe(interactive=False, wrap=True)
435
 
436
  # --- Event Wiring ---
437
 
438
+ # Wrapper function to regenerate overlay with opacity
439
+ def regenerate_overlay_with_opacity(img_path, selected_layers, op1, op2, op3, op4):
440
+ if not img_path or not selected_layers:
441
+ return None
442
+ # Map slider values to selected layers
443
+ opacities = {}
444
+ opacity_values = [op1, op2, op3, op4]
445
+ all_layers = get_available_layers()
446
+ for i, layer in enumerate(all_layers[:4]):
447
+ opacities[layer] = opacity_values[i]
448
+ return generate_overlay(img_path, selected_layers, opacities)
449
+
450
  run_btn.click(
451
  fn=run_analysis,
452
  inputs=[img_input, prompt_input, session_id_state],
453
  outputs=[
454
  chatbot, # Output to Chatbot
455
  session_id_state, # Save Session ID
456
+ overlay_output, # Image
457
  layer_checkboxes, # Checkbox Options
458
  download_btn, # Excel File
459
  tbl_morph, # Tables...
460
  tbl_spatial,
461
+ tbl_rel,
462
+ opacity_slider_1, # Opacity sliders
463
+ opacity_slider_2,
464
+ opacity_slider_3,
465
+ opacity_slider_4
466
  ]
467
  )
468
 
 
475
 
476
  img_input.change(lambda x: x, inputs=img_input, outputs=current_image_path)
477
 
478
+ # Update overlay when checkboxes or sliders change
479
+ for component in [layer_checkboxes, opacity_slider_1, opacity_slider_2, opacity_slider_3, opacity_slider_4]:
480
+ component.change(
481
+ fn=regenerate_overlay_with_opacity,
482
+ inputs=[current_image_path, layer_checkboxes, opacity_slider_1, opacity_slider_2, opacity_slider_3, opacity_slider_4],
483
+ outputs=[overlay_output]
484
+ )
485
 
486
  if __name__ == "__main__":
487
  # 3. ADDED 'theme' to launch()