nmariotto commited on
Commit
9c5c92e
·
verified ·
1 Parent(s): 08db159

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +248 -223
app.py CHANGED
@@ -14,11 +14,20 @@ from googleapiclient.http import MediaIoBaseUpload
14
  import gspread
15
  import time
16
 
17
- APP_VERSION = "2.4"
 
 
 
 
 
 
 
 
 
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,9 +36,9 @@ model.confidence = 80
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,25 +46,26 @@ credentials = Credentials(
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,11 +75,11 @@ def resize_image(image):
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,16 +87,16 @@ def find_or_create_folder(folder_name, parent=None):
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,7 +111,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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,28 +148,27 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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,40 +182,108 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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,210 +294,158 @@ def render_metrics(result):
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
- )
 
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
  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
  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
 
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
  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
  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
 
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
  "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
  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
  )