nmariotto commited on
Commit
1391243
·
verified ·
1 Parent(s): b561795

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -39
app.py CHANGED
@@ -1,9 +1,7 @@
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
@@ -11,21 +9,36 @@ 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.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"])
25
- model = project.version(st.secrets["roboflow_version"]).model
26
- model.confidence = 80
27
- model.overlap = 25
28
- dpi_value = 300
 
 
 
29
 
30
  # =========================
31
  # Google Drive + Sheets (OAuth2)
@@ -43,6 +56,7 @@ 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
  # =========================
@@ -51,10 +65,16 @@ def calculate_polygon_area(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
@@ -96,10 +116,11 @@ def get_image_bytes(image):
96
  return buf
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")
 
103
 
104
  width_px, _ = image.size
105
 
@@ -109,21 +130,22 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
109
  elif fov_um is not None and fov_um > 0:
110
  effective_pixel_size_um = fov_um / float(width_px)
111
 
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,
118
- "Área Segmentada (px²)": None,
119
- "Área Segmentada (µm²)": 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,
@@ -133,12 +155,31 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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"]]
@@ -146,7 +187,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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")
@@ -154,7 +195,7 @@ def process_image(uploaded_file, fov_um=None, pixel_size_um=None):
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")
@@ -239,8 +280,6 @@ def render_feedback_block(result, prefix_key=""):
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
 
@@ -248,11 +287,19 @@ 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²). "
@@ -298,7 +345,13 @@ if upload_option == "Single image":
298
  st.markdown("---")
299
  st.markdown("### Result")
300
 
301
- result = process_image(uploaded_file, fov_um=fov_um, pixel_size_um=pixel_size_um)
 
 
 
 
 
 
302
  if result:
303
  results.append(result)
304
 
@@ -331,6 +384,7 @@ if upload_option == "Single image":
331
  st.markdown("---")
332
  render_feedback_block(result, prefix_key="single_")
333
 
 
334
  # =========================
335
  # Folder
336
  # =========================
@@ -346,9 +400,16 @@ elif upload_option == "Image folder":
346
  st.markdown("### Processing")
347
 
348
  def process_wrapper(f):
349
- return process_image(f, fov_um=fov_um, pixel_size_um=pixel_size_um)
 
 
 
 
 
 
350
 
351
- with ThreadPoolExecutor(max_workers=4) as executor:
 
352
  processed = list(executor.map(process_wrapper, uploaded_files))
353
 
354
  falhas = [f.name for f, r in zip(uploaded_files, processed) if r and r.get("SemSegmentacao")]
 
1
  import streamlit as st
 
2
  import pandas as pd
3
  import matplotlib.pyplot as plt
4
  import zipfile
 
5
  from shapely.geometry import Polygon
6
  from PIL import Image
7
  from io import BytesIO
 
9
  from google.oauth2.credentials import Credentials
10
  from googleapiclient.discovery import build
11
  from googleapiclient.http import MediaIoBaseUpload
12
+ from huggingface_hub import hf_hub_download
13
+ from ultralytics import YOLO
14
  import gspread
15
+ import numpy as np
16
  import time
17
 
18
+ st.set_page_config(page_title="Scratch Assay Segmentation", layout="wide")
19
+
20
  APP_VERSION = "2.4"
21
+ DEFAULT_IMGSZ = 640
22
+
23
+ MODEL_OPTIONS = {
24
+ "2.4": "24.pt",
25
+ "3.7": "37.pt",
26
+ }
27
+
28
 
29
  # =========================
30
+ # Local model init (Hugging Face private repo)
31
  # =========================
32
+ @st.cache_resource
33
+ def load_model(model_filename):
34
+ local_model_path = hf_hub_download(
35
+ repo_id=st.secrets["HF_MODEL_REPO"],
36
+ filename=model_filename,
37
+ repo_type="model",
38
+ token=st.secrets["HF_TOKEN"],
39
+ )
40
+ return YOLO(local_model_path)
41
+
42
 
43
  # =========================
44
  # Google Drive + Sheets (OAuth2)
 
56
  sheets_client = gspread.authorize(credentials)
57
  sheet = sheets_client.open_by_url(st.secrets["feedback_sheet_url"]).sheet1
58
 
59
+
60
  # =========================
61
  # Helpers
62
  # =========================
 
65
  return polygon.area
66
 
67
 
68
+ def safe_predict(model, image_array, conf_threshold):
69
  for _ in range(3):
70
  try:
71
+ results = model.predict(
72
+ source=image_array,
73
+ imgsz=DEFAULT_IMGSZ,
74
+ conf=conf_threshold,
75
+ verbose=False,
76
+ )
77
+ return results
78
  except Exception:
79
  time.sleep(1)
80
  return None
 
116
  return buf
117
 
118
 
119
+ def process_image(uploaded_file, model, model_confidence, fov_um=None, pixel_size_um=None):
120
  try:
121
  safe_name = uploaded_file.name.replace(" ", "_")
122
  image = Image.open(uploaded_file).convert("RGB")
123
+ image_np = np.array(image)
124
 
125
  width_px, _ = image.size
126
 
 
130
  elif fov_um is not None and fov_um > 0:
131
  effective_pixel_size_um = fov_um / float(width_px)
132
 
133
+ conf_threshold = model_confidence / 100.0
134
+ results = safe_predict(model, image_np, conf_threshold)
135
+
136
+ if not results or len(results) == 0:
137
+ return {
138
+ "Imagem": safe_name,
139
+ "Área Segmentada (px²)": None,
140
+ "Área Segmentada (µm²)": None,
141
+ "SemSegmentacao": True,
142
+ "Exibir": image,
143
+ "Original": get_image_bytes(image),
144
+ }
145
+
146
+ result = results[0]
147
+
148
+ if result.masks is None or len(result.masks.xyn) == 0:
149
  return {
150
  "Imagem": safe_name,
151
  "Área Segmentada (px²)": None,
 
155
  "Original": get_image_bytes(image),
156
  }
157
 
158
+ best_idx = 0
159
+ if result.boxes is not None and result.boxes.conf is not None and len(result.boxes.conf) > 0:
160
+ best_idx = int(result.boxes.conf.argmax().item())
161
+
162
+ contour_norm = result.masks.xyn[best_idx]
163
+ if contour_norm is None or len(contour_norm) < 3:
164
+ return {
165
+ "Imagem": safe_name,
166
+ "Área Segmentada (px²)": None,
167
+ "Área Segmentada (µm²)": None,
168
+ "SemSegmentacao": True,
169
+ "Exibir": image,
170
+ "Original": get_image_bytes(image),
171
+ }
172
+
173
+ points = [
174
+ {"x": float(x * width_px), "y": float(y * image.height)}
175
+ for x, y in contour_norm
176
+ ]
177
+
178
  area_px2 = calculate_polygon_area(points)
179
 
180
  area_um2 = None
181
  if effective_pixel_size_um is not None:
182
+ area_um2 = area_px2 * (effective_pixel_size_um ** 2)
183
 
184
  x = [p["x"] for p in points] + [points[0]["x"]]
185
  y = [p["y"] for p in points] + [points[0]["y"]]
 
187
  original_buffer = get_image_bytes(image)
188
 
189
  segmented_buffer = BytesIO()
190
+ fig, ax = plt.subplots(figsize=(6, 6), dpi=300)
191
  ax.imshow(image)
192
  ax.plot(x, y, color="red", linewidth=2)
193
  ax.axis("off")
 
195
  plt.close()
196
 
197
  polygon_buffer = BytesIO()
198
+ fig2, ax2 = plt.subplots(figsize=(6, 6), dpi=300)
199
  ax2.plot(x, y, "r-", linewidth=2)
200
  ax2.scatter(x, y, color="red", s=5)
201
  ax2.set_title("Polygon contour")
 
280
  # =========================
281
  # Layout / UI
282
  # =========================
 
 
283
  st.title("Scratch Assay Segmentation Tool")
284
  st.caption(f"Version {APP_VERSION} · Deep learning–based wound closure segmentation")
285
 
 
287
 
288
  # Upload block
289
  st.markdown("### Input")
290
+ col_input_1, col_input_2 = st.columns([2, 1])
291
+
292
+ with col_input_1:
293
+ upload_option = st.radio("Choose upload type:", ["Single image", "Image folder"], horizontal=True)
294
+
295
+ with col_input_2:
296
+ selected_model_label = st.selectbox("Model checkpoint", list(MODEL_OPTIONS.keys()), index=0)
297
+
298
+ model = load_model(MODEL_OPTIONS[selected_model_label])
299
 
300
  # Advanced settings (collapsed by default)
301
  with st.expander("⚙️ Advanced Settings", expanded=False):
302
+ model_confidence = st.slider("Model confidence (%)", 20, 100, 80)
303
  st.markdown(
304
  "### Physical calibration (optional)\n"
305
  "Provide the physical scale for conversion from pixel area to physical units (µm²). "
 
345
  st.markdown("---")
346
  st.markdown("### Result")
347
 
348
+ result = process_image(
349
+ uploaded_file,
350
+ model=model,
351
+ model_confidence=model_confidence,
352
+ fov_um=fov_um,
353
+ pixel_size_um=pixel_size_um,
354
+ )
355
  if result:
356
  results.append(result)
357
 
 
384
  st.markdown("---")
385
  render_feedback_block(result, prefix_key="single_")
386
 
387
+
388
  # =========================
389
  # Folder
390
  # =========================
 
400
  st.markdown("### Processing")
401
 
402
  def process_wrapper(f):
403
+ return process_image(
404
+ f,
405
+ model=model,
406
+ model_confidence=model_confidence,
407
+ fov_um=fov_um,
408
+ pixel_size_um=pixel_size_um,
409
+ )
410
 
411
+ # Local inference is more stable with a single worker.
412
+ with ThreadPoolExecutor(max_workers=1) as executor:
413
  processed = list(executor.map(process_wrapper, uploaded_files))
414
 
415
  falhas = [f.name for f, r in zip(uploaded_files, processed) if r and r.get("SemSegmentacao")]