nmariotto commited on
Commit
f5fb339
·
verified ·
1 Parent(s): 2acbec7

Update app.py

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