nmariotto commited on
Commit
2fb0f89
·
verified ·
1 Parent(s): 9c5c92e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -247
app.py CHANGED
@@ -14,20 +14,11 @@ from googleapiclient.http import MediaIoBaseUpload
14
  import gspread
15
  import time
16
 
17
- # ----------------------------
18
- # Page config (layout only)
19
- # ----------------------------
20
- st.set_page_config(
21
- page_title="Scratch Assay Segmentation",
22
- page_icon="🧪",
23
- layout="wide"
24
- )
25
-
26
  APP_VERSION = "2.1"
27
 
28
- # ----------------------------
29
- # Roboflow init (unchanged)
30
- # ----------------------------
31
  API_KEY = st.secrets["roboflow_api_key"]
32
  rf = roboflow.Roboflow(api_key=API_KEY)
33
  project = rf.workspace(st.secrets["roboflow_workspace"]).project(st.secrets["roboflow_project"])
@@ -36,9 +27,9 @@ model.confidence = 80
36
  model.overlap = 25
37
  dpi_value = 300
38
 
39
- # ----------------------------
40
- # Google Drive / Sheets (unchanged)
41
- # ----------------------------
42
  scope = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets"]
43
  credentials = Credentials(
44
  token=None,
@@ -46,26 +37,25 @@ credentials = Credentials(
46
  token_uri="https://oauth2.googleapis.com/token",
47
  client_id=st.secrets["GOOGLE_DRIVE_CLIENT_ID"],
48
  client_secret=st.secrets["GOOGLE_DRIVE_CLIENT_SECRET"],
49
- scopes=scope
50
  )
51
  drive_service = build("drive", "v3", credentials=credentials)
52
  sheets_client = gspread.authorize(credentials)
53
  sheet = sheets_client.open_by_url(st.secrets["feedback_sheet_url"]).sheet1
54
 
55
-
56
- # ----------------------------
57
- # Helpers (unchanged)
58
- # ----------------------------
59
  def calculate_polygon_area(points):
60
- polygon = Polygon([(p['x'], p['y']) for p in points])
61
  return polygon.area
62
 
63
 
64
  def safe_predict(image_path):
65
- for attempt in range(3):
66
  try:
67
  return model.predict(image_path)
68
- except:
69
  time.sleep(1)
70
  return None
71
 
@@ -75,11 +65,11 @@ def resize_image(image):
75
 
76
 
77
  def upload_to_drive(image_bytes, filename, folder_id):
78
- media = MediaIoBaseUpload(image_bytes, mimetype='image/png')
79
  drive_service.files().create(
80
  body={"name": filename, "parents": [folder_id]},
81
  media_body=media,
82
- fields='id'
83
  ).execute()
84
 
85
 
@@ -87,16 +77,16 @@ def find_or_create_folder(folder_name, parent=None):
87
  query = f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
88
  if parent:
89
  query += f" and '{parent}' in parents"
90
- results = drive_service.files().list(q=query, spaces='drive', fields='files(id, name)').execute()
91
- folders = results.get('files', [])
92
  if folders:
93
- return folders[0]['id']
94
 
95
- file_metadata = {'name': folder_name, 'mimeType': 'application/vnd.google-apps.folder'}
96
  if parent:
97
- file_metadata['parents'] = [parent]
98
- file = drive_service.files().create(body=file_metadata, fields='id').execute()
99
- return file.get('id')
100
 
101
 
102
  def get_image_bytes(image):
@@ -111,7 +101,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
111
  safe_name = uploaded_file.name.replace(" ", "_")
112
  image = Image.open(uploaded_file).convert("RGB")
113
 
114
- width_px, height_px = image.size
115
 
116
  effective_pixel_size_um = None
117
  if pixel_size_um is not None and pixel_size_um > 0:
@@ -148,27 +138,28 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
148
 
149
  area_um2 = None
150
  if effective_pixel_size_um is not None:
151
- area_um2 = area_px2 * (effective_pixel_size_um ** 2)
152
 
153
- x = [p['x'] for p in points] + [points[0]['x']]
154
- y = [p['y'] for p in points] + [points[0]['y']]
155
 
156
  original_buffer = get_image_bytes(image)
157
 
158
  segmented_buffer = BytesIO()
159
  fig, ax = plt.subplots(figsize=(6, 6), dpi=dpi_value)
160
  ax.imshow(image)
161
- ax.plot(x, y, color='red', linewidth=2)
162
- plt.savefig(segmented_buffer, format="png", bbox_inches='tight')
 
163
  plt.close()
164
 
165
  polygon_buffer = BytesIO()
166
  fig2, ax2 = plt.subplots(figsize=(6, 6), dpi=dpi_value)
167
- ax2.plot(x, y, 'r-', linewidth=2)
168
- ax2.scatter(x, y, color='red', s=5)
169
- ax2.set_title("Contorno do Polígono")
170
- ax2.grid()
171
- plt.savefig(polygon_buffer, format="png", bbox_inches='tight')
172
  plt.close()
173
 
174
  return {
@@ -182,108 +173,40 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
182
  "SemSegmentacao": False,
183
  }
184
 
185
- except:
186
  return None
187
 
188
 
189
  def save_feedback(result, avaliacao, observacao):
190
  image_name = result["Imagem"]
191
 
192
- row = [image_name, avaliacao, observacao]
193
- sheet.append_row(row)
194
 
 
195
  if avaliacao in ["Acceptable", "Bad", "No segmentation"]:
196
- sufixo = (
197
- "aceitavel" if avaliacao == "Acceptable"
198
- else "ruim" if avaliacao == "Bad"
199
- else "sem_segmentacao"
200
- )
201
  parent_folder = find_or_create_folder("Feedback Segmentacoes")
202
  subfolder = find_or_create_folder(image_name.replace(".png", ""), parent_folder)
203
 
204
  resized_original = resize_image(result["Exibir"])
205
- buffer = BytesIO()
206
- resized_original.save(buffer, format="PNG")
207
- buffer.seek(0)
208
- upload_to_drive(buffer, f"original_{sufixo}.png", subfolder)
209
 
210
- if avaliacao != "No segmentation" and "Segmentada" in result and "Poligono" in result:
211
  resized_segmented = resize_image(Image.open(BytesIO(result["Segmentada"].getvalue())))
212
  resized_polygon = resize_image(Image.open(BytesIO(result["Poligono"].getvalue())))
213
 
214
  for img_obj, nome in zip([resized_segmented, resized_polygon], ["segmentada", "poligono"]):
215
- buffer = BytesIO()
216
- img_obj.save(buffer, format="PNG")
217
- buffer.seek(0)
218
- upload_to_drive(buffer, f"{nome}_{sufixo}.png", subfolder)
219
-
220
-
221
- # ----------------------------
222
- # UI (layout refactor only)
223
- # ----------------------------
224
- st.title("Scratch Assay Segmentation Tool")
225
- st.caption(f"Version {APP_VERSION} · Deep learning–based wound closure segmentation")
226
-
227
- st.markdown("---")
228
-
229
- # Upload + settings in columns (layout only)
230
- left, right = st.columns([1.1, 1.0])
231
-
232
- with left:
233
- st.subheader("Input")
234
- upload_option = st.radio(
235
- "Upload mode",
236
- ["Single image", "Image folder"],
237
- horizontal=True
238
- )
239
-
240
- if upload_option == "Single image":
241
- uploaded_file = st.file_uploader(
242
- "Select an image",
243
- type=["png", "jpg", "jpeg", "tiff"]
244
- )
245
- uploaded_files = None
246
- else:
247
- uploaded_files = st.file_uploader(
248
- "Upload multiple images",
249
- type=["png", "jpg", "jpeg", "tiff"],
250
- accept_multiple_files=True
251
- )
252
- uploaded_file = None
253
-
254
- with right:
255
- st.subheader("Settings")
256
- with st.expander("Advanced settings", expanded=False):
257
- model.confidence = st.slider("Model confidence (%)", 20, 100, 80)
258
-
259
- st.markdown("**Physical calibration (optional)**")
260
- st.caption(
261
- "Provide the physical scale to convert pixel area to µm². "
262
- "If left empty, results will be reported only in pixels²."
263
- )
264
- c1, c2 = st.columns(2)
265
- fov_um = c1.number_input(
266
- "Field of view width (µm)",
267
- min_value=0.0,
268
- value=0.0,
269
- step=1.0
270
- )
271
- pixel_size_um = c2.number_input(
272
- "Pixel size (µm / pixel)",
273
- min_value=0.0,
274
- value=0.0,
275
- step=0.01
276
- )
277
 
278
- st.markdown("---")
279
-
280
- results = []
281
-
282
- def render_metrics_block(result):
283
- if result["SemSegmentacao"]:
284
- st.warning("No segmentation detected for this image.")
285
- return
286
 
 
287
  area_px2 = result["Área Segmentada (px²)"]
288
  area_um2 = result["Área Segmentada (µm²)"]
289
 
@@ -294,158 +217,210 @@ def render_metrics_block(result):
294
  st.markdown(f"- {area_um2:.2f} µm²")
295
 
296
 
297
- def render_images_three_cols(result):
298
- colA, colB, colC = st.columns(3)
299
-
300
- with colA:
301
- st.image(result["Exibir"], caption="Original", use_container_width=True)
302
-
303
- with colB:
304
- if not result["SemSegmentacao"] and "Segmentada" in result and result["Segmentada"] is not None:
305
- st.image(result["Segmentada"], caption="Segmentation", use_container_width=True)
306
- else:
307
- st.empty()
308
-
309
- with colC:
310
- if not result["SemSegmentacao"] and "Poligono" in result and result["Poligono"] is not None:
311
- st.image(result["Poligono"], caption="Polygon", use_container_width=True)
312
- else:
313
- st.empty()
314
-
315
-
316
- def render_feedback_block(result, key_prefix):
317
- st.markdown("**Segmentation quality feedback**")
318
  st.caption("User evaluation used for future model refinement.")
 
319
  avaliacao = st.radio(
320
- "Assessment",
321
  ["Great", "Acceptable", "Bad", "No segmentation"],
322
  horizontal=True,
323
- key=f"{key_prefix}_radio_{result['Imagem']}"
324
  )
325
  observacao = st.text_area(
326
- "Observations (optional)",
327
- key=f"{key_prefix}_obs_{result['Imagem']}"
328
  )
329
- if st.button("Save feedback", key=f"{key_prefix}_btn_{result['Imagem']}"):
330
  save_feedback(result, avaliacao, observacao)
331
  st.success("Feedback saved successfully.")
332
 
333
 
334
- # ----------------------------
335
- # Single image
336
- # ----------------------------
337
- if upload_option == "Single image" and uploaded_file:
338
- st.subheader("Result")
339
 
340
- result = process_image(uploaded_file, fov_um=fov_um, pixel_size_um=pixel_size_um)
341
- if result:
342
- results.append(result)
343
 
344
- st.markdown(f"### {result['Imagem']}")
345
- render_images_three_cols(result)
346
 
347
- st.markdown("---")
348
- render_metrics_block(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- if not result["SemSegmentacao"]:
351
- st.markdown("---")
352
- st.subheader("Export")
353
- st.download_button(
354
- label="Download segmented overlay (PNG)",
355
- data=result["Segmentada"],
356
- file_name="segmented_image.png",
357
- mime="image/png",
358
- )
359
 
 
 
 
 
 
 
360
  st.markdown("---")
361
- st.subheader("Feedback")
362
- render_feedback_block(result, key_prefix="single")
363
 
 
 
 
364
 
365
- # ----------------------------
366
- # Folder
367
- # ----------------------------
368
- if upload_option == "Image folder" and uploaded_files:
369
- st.subheader("Batch processing")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
 
371
- def process_wrapper(f):
372
- return process_image(f, fov_um=fov_um, pixel_size_um=pixel_size_um)
373
 
374
- with ThreadPoolExecutor(max_workers=4) as executor:
375
- processed = list(executor.map(process_wrapper, uploaded_files))
 
 
 
 
 
 
 
376
 
377
- falhas = [f.name for f, r in zip(uploaded_files, processed) if r and r.get("SemSegmentacao")]
378
- if falhas:
379
- st.warning(
380
- f"{len(falhas)} image(s) with no segmentation detected:\n\n- " + "\n- ".join(falhas)
381
- )
382
 
383
- zip_images_buffer = BytesIO()
384
- with zipfile.ZipFile(zip_images_buffer, "w") as zip_file:
385
- for result in processed:
386
- if not result:
387
- continue
388
 
389
- results.append(result)
 
390
 
391
- st.markdown(f"### {result['Imagem']}")
392
- render_images_three_cols(result)
 
 
 
393
 
394
- st.markdown("---")
395
- render_metrics_block(result)
 
 
 
396
 
397
- if not result["SemSegmentacao"]:
398
- zip_file.writestr(f"segmentada_{result['Imagem']}.png", result["Segmentada"].getvalue())
399
- zip_file.writestr(f"poligono_{result['Imagem']}.png", result["Poligono"].getvalue())
400
 
401
- st.markdown("---")
402
- st.subheader("Feedback")
403
- render_feedback_block(result, key_prefix="folder")
 
 
 
 
 
 
 
 
404
 
405
- st.markdown("---")
406
 
407
- zip_images_buffer.seek(0)
408
-
409
- if results:
410
- st.subheader("Quantitative results")
411
-
412
- df = pd.DataFrame([
413
- {
414
- "Image": r["Imagem"],
415
- "Segmented Area (px²)": (
416
- r["Área Segmentada (px²)"]
417
- if (not r["SemSegmentacao"] and r["Área Segmentada (px²)"] is not None)
418
- else "No Segmentation"
419
- ),
420
- "Segmented Area (µm²)": (
421
- f"{r['Área Segmentada (µm²)']:.2f}"
422
- if (not r["SemSegmentacao"] and r["Área Segmentada (µm²)"] is not None)
423
- else ""
424
- ),
425
- }
426
- for r in results
427
- ])
428
 
429
- st.dataframe(df, use_container_width=True)
430
 
431
- excel_buffer = BytesIO()
432
- df.to_excel(excel_buffer, index=False)
433
- excel_buffer.seek(0)
434
 
435
- st.markdown("---")
436
- st.subheader("Export results")
437
- btn1, btn2 = st.columns(2)
438
- with btn1:
439
- st.download_button(
440
- "Download table (Excel)",
441
- data=excel_buffer,
442
- file_name="segmentation_results.xlsx",
443
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
444
- )
445
- with btn2:
446
- st.download_button(
447
- "Download segmented images (ZIP)",
448
- data=zip_images_buffer,
449
- file_name="segmented_images.zip",
450
- mime="application/zip",
 
 
 
 
 
 
451
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  import gspread
15
  import time
16
 
 
 
 
 
 
 
 
 
 
17
  APP_VERSION = "2.1"
18
 
19
+ # =========================
20
+ # Roboflow init
21
+ # =========================
22
  API_KEY = st.secrets["roboflow_api_key"]
23
  rf = roboflow.Roboflow(api_key=API_KEY)
24
  project = rf.workspace(st.secrets["roboflow_workspace"]).project(st.secrets["roboflow_project"])
 
27
  model.overlap = 25
28
  dpi_value = 300
29
 
30
+ # =========================
31
+ # Google Drive + Sheets (OAuth2)
32
+ # =========================
33
  scope = ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets"]
34
  credentials = Credentials(
35
  token=None,
 
37
  token_uri="https://oauth2.googleapis.com/token",
38
  client_id=st.secrets["GOOGLE_DRIVE_CLIENT_ID"],
39
  client_secret=st.secrets["GOOGLE_DRIVE_CLIENT_SECRET"],
40
+ scopes=scope,
41
  )
42
  drive_service = build("drive", "v3", credentials=credentials)
43
  sheets_client = gspread.authorize(credentials)
44
  sheet = sheets_client.open_by_url(st.secrets["feedback_sheet_url"]).sheet1
45
 
46
+ # =========================
47
+ # Helpers
48
+ # =========================
 
49
  def calculate_polygon_area(points):
50
+ polygon = Polygon([(p["x"], p["y"]) for p in points])
51
  return polygon.area
52
 
53
 
54
  def safe_predict(image_path):
55
+ for _ in range(3):
56
  try:
57
  return model.predict(image_path)
58
+ except Exception:
59
  time.sleep(1)
60
  return None
61
 
 
65
 
66
 
67
  def upload_to_drive(image_bytes, filename, folder_id):
68
+ media = MediaIoBaseUpload(image_bytes, mimetype="image/png")
69
  drive_service.files().create(
70
  body={"name": filename, "parents": [folder_id]},
71
  media_body=media,
72
+ fields="id",
73
  ).execute()
74
 
75
 
 
77
  query = f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
78
  if parent:
79
  query += f" and '{parent}' in parents"
80
+ results = drive_service.files().list(q=query, spaces="drive", fields="files(id, name)").execute()
81
+ folders = results.get("files", [])
82
  if folders:
83
+ return folders[0]["id"]
84
 
85
+ file_metadata = {"name": folder_name, "mimeType": "application/vnd.google-apps.folder"}
86
  if parent:
87
+ file_metadata["parents"] = [parent]
88
+ file = drive_service.files().create(body=file_metadata, fields="id").execute()
89
+ return file.get("id")
90
 
91
 
92
  def get_image_bytes(image):
 
101
  safe_name = uploaded_file.name.replace(" ", "_")
102
  image = Image.open(uploaded_file).convert("RGB")
103
 
104
+ width_px, _ = image.size
105
 
106
  effective_pixel_size_um = None
107
  if pixel_size_um is not None and pixel_size_um > 0:
 
138
 
139
  area_um2 = None
140
  if effective_pixel_size_um is not None:
141
+ area_um2 = area_px2 * (effective_pixel_size_um**2)
142
 
143
+ x = [p["x"] for p in points] + [points[0]["x"]]
144
+ y = [p["y"] for p in points] + [points[0]["y"]]
145
 
146
  original_buffer = get_image_bytes(image)
147
 
148
  segmented_buffer = BytesIO()
149
  fig, ax = plt.subplots(figsize=(6, 6), dpi=dpi_value)
150
  ax.imshow(image)
151
+ ax.plot(x, y, color="red", linewidth=2)
152
+ ax.axis("off")
153
+ plt.savefig(segmented_buffer, format="png", bbox_inches="tight", pad_inches=0)
154
  plt.close()
155
 
156
  polygon_buffer = BytesIO()
157
  fig2, ax2 = plt.subplots(figsize=(6, 6), dpi=dpi_value)
158
+ ax2.plot(x, y, "r-", linewidth=2)
159
+ ax2.scatter(x, y, color="red", s=5)
160
+ ax2.set_title("Polygon contour")
161
+ ax2.grid(True)
162
+ plt.savefig(polygon_buffer, format="png", bbox_inches="tight")
163
  plt.close()
164
 
165
  return {
 
173
  "SemSegmentacao": False,
174
  }
175
 
176
+ except Exception:
177
  return None
178
 
179
 
180
  def save_feedback(result, avaliacao, observacao):
181
  image_name = result["Imagem"]
182
 
183
+ # 1) Sheet
184
+ sheet.append_row([image_name, avaliacao, observacao])
185
 
186
+ # 2) Drive curation
187
  if avaliacao in ["Acceptable", "Bad", "No segmentation"]:
188
+ sufixo = "aceitavel" if avaliacao == "Acceptable" else "ruim" if avaliacao == "Bad" else "sem_segmentacao"
 
 
 
 
189
  parent_folder = find_or_create_folder("Feedback Segmentacoes")
190
  subfolder = find_or_create_folder(image_name.replace(".png", ""), parent_folder)
191
 
192
  resized_original = resize_image(result["Exibir"])
193
+ buf = BytesIO()
194
+ resized_original.save(buf, format="PNG")
195
+ buf.seek(0)
196
+ upload_to_drive(buf, f"original_{sufixo}.png", subfolder)
197
 
198
+ if avaliacao != "No segmentation" and result.get("Segmentada") and result.get("Poligono"):
199
  resized_segmented = resize_image(Image.open(BytesIO(result["Segmentada"].getvalue())))
200
  resized_polygon = resize_image(Image.open(BytesIO(result["Poligono"].getvalue())))
201
 
202
  for img_obj, nome in zip([resized_segmented, resized_polygon], ["segmentada", "poligono"]):
203
+ buf = BytesIO()
204
+ img_obj.save(buf, format="PNG")
205
+ buf.seek(0)
206
+ upload_to_drive(buf, f"{nome}_{sufixo}.png", subfolder)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
 
 
 
 
 
 
 
 
208
 
209
+ def render_metrics(result):
210
  area_px2 = result["Área Segmentada (px²)"]
211
  area_um2 = result["Área Segmentada (µm²)"]
212
 
 
217
  st.markdown(f"- {area_um2:.2f} µm²")
218
 
219
 
220
+ def render_feedback_block(result, prefix_key=""):
221
+ st.markdown("#### Segmentation quality feedback")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  st.caption("User evaluation used for future model refinement.")
223
+
224
  avaliacao = st.radio(
225
+ "Segmentation quality assessment:",
226
  ["Great", "Acceptable", "Bad", "No segmentation"],
227
  horizontal=True,
228
+ key=f"{prefix_key}radio_{result['Imagem']}",
229
  )
230
  observacao = st.text_area(
231
+ "Observations (optional):",
232
+ key=f"{prefix_key}obs_{result['Imagem']}",
233
  )
234
+ if st.button("Save feedback", key=f"{prefix_key}btn_{result['Imagem']}"):
235
  save_feedback(result, avaliacao, observacao)
236
  st.success("Feedback saved successfully.")
237
 
238
 
239
+ # =========================
240
+ # Layout / UI
241
+ # =========================
242
+ st.set_page_config(page_title="Scratch Assay Segmentation", layout="wide")
 
243
 
244
+ st.title("Scratch Assay Segmentation Tool")
245
+ st.caption(f"Version {APP_VERSION} · Deep learning–based wound closure segmentation")
 
246
 
247
+ st.markdown("---")
 
248
 
249
+ # Upload block
250
+ st.markdown("### Input")
251
+ upload_option = st.radio("Choose upload type:", ["Single image", "Image folder"], horizontal=True)
252
+
253
+ # Advanced settings (collapsed by default)
254
+ with st.expander("⚙️ Advanced Settings", expanded=False):
255
+ model.confidence = st.slider("Model confidence (%)", 20, 100, 80)
256
+ st.markdown(
257
+ "### Physical calibration (optional)\n"
258
+ "Provide the physical scale for conversion from pixel area to physical units (µm²). "
259
+ "If left empty, results will be reported only in pixels²."
260
+ )
261
+ c1, c2 = st.columns(2)
262
+ fov_um = c1.number_input(
263
+ "Field of view width (µm)",
264
+ min_value=0.0,
265
+ value=0.0,
266
+ step=1.0,
267
+ help="Physical width of the image field, in micrometers.",
268
+ )
269
+ pixel_size_um = c2.number_input(
270
+ "Pixel size (µm / pixel)",
271
+ min_value=0.0,
272
+ value=0.0,
273
+ step=0.01,
274
+ help="If provided, this overrides the FOV-based calibration.",
275
+ )
276
 
277
+ results = []
 
 
 
 
 
 
 
 
278
 
279
+ # =========================
280
+ # Single image
281
+ # =========================
282
+ if upload_option == "Single image":
283
+ uploaded_file = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg", "tiff"])
284
+ if uploaded_file:
285
  st.markdown("---")
286
+ st.markdown("### Result")
 
287
 
288
+ result = process_image(uploaded_file, fov_um=fov_um, pixel_size_um=pixel_size_um)
289
+ if result:
290
+ results.append(result)
291
 
292
+ st.markdown(f"#### {result['Imagem']}")
293
+
294
+ if result["SemSegmentacao"]:
295
+ col = st.columns(1)[0]
296
+ with col:
297
+ st.image(result["Exibir"], caption="Original", use_container_width=True)
298
+ st.warning("No segmentation was detected for this image.")
299
+ else:
300
+ col1, col2, col3 = st.columns(3)
301
+ with col1:
302
+ st.image(result["Exibir"], caption="Original", use_container_width=True)
303
+ with col2:
304
+ st.image(result["Segmentada"], caption="Segmentation", use_container_width=True)
305
+ with col3:
306
+ st.image(result["Poligono"], caption="Polygon", use_container_width=True)
307
+
308
+ render_metrics(result)
309
+
310
+ st.markdown("### Export")
311
+ st.download_button(
312
+ "Download segmented overlay (PNG)",
313
+ data=result["Segmentada"],
314
+ file_name=f"segmented_{result['Imagem']}.png",
315
+ mime="image/png",
316
+ )
317
 
318
+ st.markdown("---")
319
+ render_feedback_block(result, prefix_key="single_")
320
 
321
+ # =========================
322
+ # Folder
323
+ # =========================
324
+ elif upload_option == "Image folder":
325
+ uploaded_files = st.file_uploader(
326
+ "Upload multiple images",
327
+ type=["png", "jpg", "jpeg", "tiff"],
328
+ accept_multiple_files=True,
329
+ )
330
 
331
+ if uploaded_files:
332
+ st.markdown("---")
333
+ st.markdown("### Processing")
 
 
334
 
335
+ def process_wrapper(f):
336
+ return process_image(f, fov_um=fov_um, pixel_size_um=pixel_size_um)
 
 
 
337
 
338
+ with ThreadPoolExecutor(max_workers=4) as executor:
339
+ processed = list(executor.map(process_wrapper, uploaded_files))
340
 
341
+ falhas = [f.name for f, r in zip(uploaded_files, processed) if r and r.get("SemSegmentacao")]
342
+ if falhas:
343
+ st.warning(
344
+ f"{len(falhas)} image(s) with no segmentation detected:\n\n- " + "\n- ".join(falhas)
345
+ )
346
 
347
+ zip_images_buffer = BytesIO()
348
+ with zipfile.ZipFile(zip_images_buffer, "w") as zip_file:
349
+ for idx, result in enumerate(processed, start=1):
350
+ if not result:
351
+ continue
352
 
353
+ results.append(result)
354
+ st.markdown("---")
355
+ st.markdown(f"### Result {idx} · {result['Imagem']}")
356
 
357
+ if result["SemSegmentacao"]:
358
+ st.image(result["Exibir"], caption="Original", use_container_width=True)
359
+ st.warning("No segmentation was detected for this image.")
360
+ else:
361
+ col1, col2, col3 = st.columns(3)
362
+ with col1:
363
+ st.image(result["Exibir"], caption="Original", use_container_width=True)
364
+ with col2:
365
+ st.image(result["Segmentada"], caption="Segmentation", use_container_width=True)
366
+ with col3:
367
+ st.image(result["Poligono"], caption="Polygon", use_container_width=True)
368
 
369
+ render_metrics(result)
370
 
371
+ # Build ZIP
372
+ zip_file.writestr(f"segmentada_{result['Imagem']}.png", result["Segmentada"].getvalue())
373
+ zip_file.writestr(f"poligono_{result['Imagem']}.png", result["Poligono"].getvalue())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
+ render_feedback_block(result, prefix_key="folder_")
376
 
377
+ zip_images_buffer.seek(0)
 
 
378
 
379
+ # Summary table + exports
380
+ if results:
381
+ st.markdown("---")
382
+ st.markdown("### Quantitative results")
383
+
384
+ df = pd.DataFrame(
385
+ [
386
+ {
387
+ "Image": r["Imagem"],
388
+ "Segmented Area (px²)": (
389
+ r["Área Segmentada (px²)"]
390
+ if (not r["SemSegmentacao"] and r["Área Segmentada (px²)"] is not None)
391
+ else "No Segmentation"
392
+ ),
393
+ "Segmented Area (µm²)": (
394
+ f"{r['Área Segmentada (µm²)']:.2f}"
395
+ if (not r["SemSegmentacao"] and r["Área Segmentada (µm²)"] is not None)
396
+ else ""
397
+ ),
398
+ }
399
+ for r in results
400
+ ]
401
  )
402
+
403
+ st.dataframe(df, use_container_width=True)
404
+
405
+ excel_buffer = BytesIO()
406
+ df.to_excel(excel_buffer, index=False)
407
+ excel_buffer.seek(0)
408
+
409
+ st.markdown("### Export results")
410
+ c1, c2 = st.columns(2)
411
+ with c1:
412
+ st.download_button(
413
+ "Download table (Excel)",
414
+ data=excel_buffer,
415
+ file_name="segmentation_results.xlsx",
416
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
417
+ use_container_width=True,
418
+ )
419
+ with c2:
420
+ st.download_button(
421
+ "Download segmented images (ZIP)",
422
+ data=zip_images_buffer,
423
+ file_name="segmented_images.zip",
424
+ mime="application/zip",
425
+ use_container_width=True,
426
+ )