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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -39
app.py CHANGED
@@ -1,20 +1,31 @@
 
 
 
 
 
 
 
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
  from shapely.geometry import Polygon
8
  from PIL import Image
9
- from io import BytesIO
10
- from concurrent.futures import ThreadPoolExecutor
11
- from google.oauth2.credentials import Credentials
12
  from googleapiclient.discovery import build
13
  from googleapiclient.http import MediaIoBaseUpload
14
- import gspread
15
- import time
16
 
17
- APP_VERSION = "2.1"
 
 
 
 
 
 
 
18
 
19
  # =========================
20
  # Roboflow init
@@ -27,22 +38,23 @@ 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,
36
- refresh_token=st.secrets["GOOGLE_DRIVE_REFRESH_TOKEN"],
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
  # =========================
@@ -77,7 +89,13 @@ 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"]
@@ -85,6 +103,7 @@ def find_or_create_folder(folder_name, parent=None):
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
 
@@ -97,6 +116,11 @@ def get_image_bytes(image):
97
 
98
 
99
  def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
 
 
 
 
 
100
  try:
101
  safe_name = uploaded_file.name.replace(" ", "_")
102
  image = Image.open(uploaded_file).convert("RGB")
@@ -112,6 +136,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
112
  with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as temp_file:
113
  image.save(temp_file.name)
114
  prediction = safe_predict(temp_file.name)
 
115
  if not prediction:
116
  return {
117
  "Imagem": safe_name,
@@ -120,10 +145,13 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
120
  "SemSegmentacao": True,
121
  "Exibir": image,
122
  "Original": get_image_bytes(image),
 
 
123
  }
 
124
  prediction_data = prediction.json()
125
 
126
- if not prediction_data["predictions"]:
127
  return {
128
  "Imagem": safe_name,
129
  "Área Segmentada (px²)": None,
@@ -131,20 +159,23 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
131
  "SemSegmentacao": True,
132
  "Exibir": image,
133
  "Original": get_image_bytes(image),
 
 
134
  }
135
 
136
  points = prediction_data["predictions"][0]["points"]
137
- area_px2 = calculate_polygon_area(points)
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)
@@ -153,6 +184,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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)
@@ -178,23 +210,34 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=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())))
@@ -237,20 +280,17 @@ def render_feedback_block(result, prefix_key=""):
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(
@@ -281,6 +321,7 @@ results = []
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")
@@ -292,9 +333,7 @@ if upload_option == "Single image":
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)
@@ -340,9 +379,7 @@ elif upload_option == "Image folder":
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:
@@ -351,6 +388,7 @@ elif upload_option == "Image folder":
351
  continue
352
 
353
  results.append(result)
 
354
  st.markdown("---")
355
  st.markdown(f"### Result {idx} · {result['Imagem']}")
356
 
@@ -368,7 +406,6 @@ elif upload_option == "Image folder":
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
 
@@ -423,4 +460,4 @@ elif upload_option == "Image folder":
423
  file_name="segmented_images.zip",
424
  mime="application/zip",
425
  use_container_width=True,
426
- )
 
1
+ import json
2
+ import time
3
+ import zipfile
4
+ import tempfile
5
+ from io import BytesIO
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
  import streamlit as st
9
  import roboflow
10
  import pandas as pd
11
  import matplotlib.pyplot as plt
12
+
 
13
  from shapely.geometry import Polygon
14
  from PIL import Image
15
+
16
+ import gspread
17
+ from google.oauth2.service_account import Credentials
18
  from googleapiclient.discovery import build
19
  from googleapiclient.http import MediaIoBaseUpload
 
 
20
 
21
+
22
+ # =========================
23
+ # Page config
24
+ # =========================
25
+ st.set_page_config(page_title="Scratch Assay Segmentation", layout="wide")
26
+
27
+ APP_VERSION = "2.4"
28
+
29
 
30
  # =========================
31
  # Roboflow init
 
38
  model.overlap = 25
39
  dpi_value = 300
40
 
41
+
42
  # =========================
43
+ # Google Drive + Sheets (Service Account) ✅ FIX
44
  # =========================
45
+ SCOPES = [
46
+ "https://www.googleapis.com/auth/drive",
47
+ "https://www.googleapis.com/auth/spreadsheets",
48
+ ]
49
+
50
+ sa_info = json.loads(st.secrets["gcp_service_account"])
51
+ credentials = Credentials.from_service_account_info(sa_info, scopes=SCOPES)
52
+
 
53
  drive_service = build("drive", "v3", credentials=credentials)
54
  sheets_client = gspread.authorize(credentials)
55
  sheet = sheets_client.open_by_url(st.secrets["feedback_sheet_url"]).sheet1
56
 
57
+
58
  # =========================
59
  # Helpers
60
  # =========================
 
89
  query = f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"
90
  if parent:
91
  query += f" and '{parent}' in parents"
92
+
93
+ results = drive_service.files().list(
94
+ q=query,
95
+ spaces="drive",
96
+ fields="files(id, name)"
97
+ ).execute()
98
+
99
  folders = results.get("files", [])
100
  if folders:
101
  return folders[0]["id"]
 
103
  file_metadata = {"name": folder_name, "mimeType": "application/vnd.google-apps.folder"}
104
  if parent:
105
  file_metadata["parents"] = [parent]
106
+
107
  file = drive_service.files().create(body=file_metadata, fields="id").execute()
108
  return file.get("id")
109
 
 
116
 
117
 
118
  def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
119
+ """
120
+ - Runs inference through Roboflow
121
+ - Computes area in px² and, if calibrated, in µm²
122
+ - Returns buffers for Original / Segmented overlay / Polygon
123
+ """
124
  try:
125
  safe_name = uploaded_file.name.replace(" ", "_")
126
  image = Image.open(uploaded_file).convert("RGB")
 
136
  with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as temp_file:
137
  image.save(temp_file.name)
138
  prediction = safe_predict(temp_file.name)
139
+
140
  if not prediction:
141
  return {
142
  "Imagem": safe_name,
 
145
  "SemSegmentacao": True,
146
  "Exibir": image,
147
  "Original": get_image_bytes(image),
148
+ "Segmentada": None,
149
+ "Poligono": None,
150
  }
151
+
152
  prediction_data = prediction.json()
153
 
154
+ if not prediction_data.get("predictions"):
155
  return {
156
  "Imagem": safe_name,
157
  "Área Segmentada (px²)": None,
 
159
  "SemSegmentacao": True,
160
  "Exibir": image,
161
  "Original": get_image_bytes(image),
162
+ "Segmentada": None,
163
+ "Poligono": None,
164
  }
165
 
166
  points = prediction_data["predictions"][0]["points"]
 
167
 
168
+ area_px2 = calculate_polygon_area(points)
169
  area_um2 = None
170
  if effective_pixel_size_um is not None:
171
+ area_um2 = area_px2 * (effective_pixel_size_um ** 2)
172
 
173
  x = [p["x"] for p in points] + [points[0]["x"]]
174
  y = [p["y"] for p in points] + [points[0]["y"]]
175
 
176
  original_buffer = get_image_bytes(image)
177
 
178
+ # Segmented overlay
179
  segmented_buffer = BytesIO()
180
  fig, ax = plt.subplots(figsize=(6, 6), dpi=dpi_value)
181
  ax.imshow(image)
 
184
  plt.savefig(segmented_buffer, format="png", bbox_inches="tight", pad_inches=0)
185
  plt.close()
186
 
187
+ # Polygon only
188
  polygon_buffer = BytesIO()
189
  fig2, ax2 = plt.subplots(figsize=(6, 6), dpi=dpi_value)
190
  ax2.plot(x, y, "r-", linewidth=2)
 
210
 
211
 
212
  def save_feedback(result, avaliacao, observacao):
213
+ """
214
+ - Appends feedback to Google Sheet
215
+ - Uploads images to Drive for curation (Acceptable/Bad/No segmentation)
216
+ """
217
  image_name = result["Imagem"]
218
 
219
+ # 1) Sheet (same structure as you had)
220
  sheet.append_row([image_name, avaliacao, observacao])
221
 
222
+ # 2) Drive curation (same logic as before)
223
  if avaliacao in ["Acceptable", "Bad", "No segmentation"]:
224
+ sufixo = (
225
+ "aceitavel" if avaliacao == "Acceptable"
226
+ else "ruim" if avaliacao == "Bad"
227
+ else "sem_segmentacao"
228
+ )
229
+
230
  parent_folder = find_or_create_folder("Feedback Segmentacoes")
231
  subfolder = find_or_create_folder(image_name.replace(".png", ""), parent_folder)
232
 
233
+ # Original
234
  resized_original = resize_image(result["Exibir"])
235
  buf = BytesIO()
236
  resized_original.save(buf, format="PNG")
237
  buf.seek(0)
238
  upload_to_drive(buf, f"original_{sufixo}.png", subfolder)
239
 
240
+ # Segmented + Polygon only if segmentation exists
241
  if avaliacao != "No segmentation" and result.get("Segmentada") and result.get("Poligono"):
242
  resized_segmented = resize_image(Image.open(BytesIO(result["Segmentada"].getvalue())))
243
  resized_polygon = resize_image(Image.open(BytesIO(result["Poligono"].getvalue())))
 
280
 
281
 
282
  # =========================
283
+ # UI
284
  # =========================
 
 
285
  st.title("Scratch Assay Segmentation Tool")
286
  st.caption(f"Version {APP_VERSION} · Deep learning–based wound closure segmentation")
 
287
  st.markdown("---")
288
 
289
+ # Input block
290
  st.markdown("### Input")
291
  upload_option = st.radio("Choose upload type:", ["Single image", "Image folder"], horizontal=True)
292
 
293
+ # Advanced settings (collapsed)
294
  with st.expander("⚙️ Advanced Settings", expanded=False):
295
  model.confidence = st.slider("Model confidence (%)", 20, 100, 80)
296
  st.markdown(
 
321
  # =========================
322
  if upload_option == "Single image":
323
  uploaded_file = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg", "tiff"])
324
+
325
  if uploaded_file:
326
  st.markdown("---")
327
  st.markdown("### Result")
 
333
  st.markdown(f"#### {result['Imagem']}")
334
 
335
  if result["SemSegmentacao"]:
336
+ st.image(result["Exibir"], caption="Original", use_container_width=True)
 
 
337
  st.warning("No segmentation was detected for this image.")
338
  else:
339
  col1, col2, col3 = st.columns(3)
 
379
 
380
  falhas = [f.name for f, r in zip(uploaded_files, processed) if r and r.get("SemSegmentacao")]
381
  if falhas:
382
+ st.warning(f"{len(falhas)} image(s) with no segmentation detected:\n\n- " + "\n- ".join(falhas))
 
 
383
 
384
  zip_images_buffer = BytesIO()
385
  with zipfile.ZipFile(zip_images_buffer, "w") as zip_file:
 
388
  continue
389
 
390
  results.append(result)
391
+
392
  st.markdown("---")
393
  st.markdown(f"### Result {idx} · {result['Imagem']}")
394
 
 
406
 
407
  render_metrics(result)
408
 
 
409
  zip_file.writestr(f"segmentada_{result['Imagem']}.png", result["Segmentada"].getvalue())
410
  zip_file.writestr(f"poligono_{result['Imagem']}.png", result["Poligono"].getvalue())
411
 
 
460
  file_name="segmented_images.zip",
461
  mime="application/zip",
462
  use_container_width=True,
463
+ )