joao-dutra commited on
Commit
6801165
·
verified ·
1 Parent(s): fbad683
Files changed (1) hide show
  1. app.py +117 -66
app.py CHANGED
@@ -108,8 +108,6 @@
108
  # @app.get("/")
109
  # def read_root():
110
  # return {"status": "ok"}
111
-
112
- # app.py
113
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
114
  from fastapi.middleware.cors import CORSMiddleware
115
  from fastapi.responses import JSONResponse
@@ -119,17 +117,18 @@ import base64
119
  import traceback
120
  from typing import Dict, List
121
 
122
- app = FastAPI(title="Detector de Corrosão Branca — multi-objetos (notebook-based)")
123
 
 
124
  app.add_middleware(
125
  CORSMiddleware,
126
- allow_origins=["*"], # restrinja em produção
127
  allow_credentials=True,
128
  allow_methods=["*"],
129
  allow_headers=["*"],
130
  )
131
 
132
- # ----------------- utils -----------------
133
  def to_data_uri_rgb(img_rgb: np.ndarray) -> str | None:
134
  if img_rgb is None:
135
  return None
@@ -141,7 +140,7 @@ def to_data_uri_rgb(img_rgb: np.ndarray) -> str | None:
141
  return f"data:image/png;base64,{b64}"
142
 
143
  def feather_mask(mask_u8: np.ndarray, feather_px: float) -> np.ndarray:
144
- """Retorna máscara float32 [0..1] com feather (Gaussiano) opcional."""
145
  if feather_px and feather_px > 0:
146
  alpha = cv2.GaussianBlur(mask_u8, (0, 0), feather_px).astype(np.float32) / 255.0
147
  else:
@@ -149,27 +148,33 @@ def feather_mask(mask_u8: np.ndarray, feather_px: float) -> np.ndarray:
149
  return np.clip(alpha, 0.0, 1.0)
150
 
151
  def compose_on_black(bgr: np.ndarray, alpha01: np.ndarray) -> np.ndarray:
152
- """Aplica alpha (H×W float [0..1]) sobre imagem BGR e retorna RGB uint8 com fundo preto."""
153
  comp = (bgr.astype(np.float32) * alpha01[..., None]).astype(np.uint8)
154
  return cv2.cvtColor(comp, cv2.COLOR_BGR2RGB)
155
 
156
- def corrosion_mask_from_isolated(bgr: np.ndarray, obj_mask: np.ndarray) -> np.ndarray:
157
- """Detecta 'corrosão branca' (S baixo, V alto) dentro da máscara do objeto."""
158
- hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
159
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
160
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
161
  m = cv2.inRange(hsv, lower_white, upper_white)
162
- return cv2.bitwise_and(m, m, mask=obj_mask)
163
 
164
  # ----------------- core -----------------
165
  def process_image_bytes_multi(
166
  img_bytes: bytes,
167
- margem: int = 5,
168
- min_area_rel: float = 1/30000,
 
169
  kernel_sz: int = 3,
170
- dilatacao_px: float = 0.5,
171
  feather_px: float = 1.0,
172
  sort: str = "x",
 
 
 
 
 
173
  ) -> Dict:
174
  # decodifica
175
  nparr = np.frombuffer(img_bytes, np.uint8)
@@ -177,60 +182,106 @@ def process_image_bytes_multi(
177
  if img is None:
178
  raise ValueError("Não foi possível decodificar a imagem.")
179
  H, W = img.shape[:2]
 
180
 
181
- # --- pré-processamento conforme notebook ---
182
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
183
  gray = cv2.GaussianBlur(gray, (5, 5), 0)
184
 
185
- # Otsu (inv) + Adaptativa (inv) -> OR
186
  _, thr_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
187
  thr_adap = cv2.adaptiveThreshold(
188
  gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 51, 2
189
  )
190
  thr = cv2.bitwise_or(thr_otsu, thr_adap)
191
 
192
- # morfologia (fechamento)
193
- k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (max(1, kernel_sz)|1, max(1, kernel_sz)|1))
194
  thr = cv2.morphologyEx(thr, cv2.MORPH_CLOSE, k, iterations=1)
195
 
196
- # dilatação extra (se solicitado)
197
  if dilatacao_px and dilatacao_px > 0:
198
- ksz = int(2 * dilatacao_px + 1)
199
- ksz = max(1, ksz) | 1 # ímpar
200
- k_dil = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ksz, ksz))
201
  thr = cv2.dilate(thr, k_dil, iterations=1)
202
 
203
- # contornos externos
204
  cnts, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
205
 
206
- min_area = max(200, int((H * W) * min_area_rel))
207
- cand = []
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  for c in cnts:
209
  area = cv2.contourArea(c)
210
- if area >= min_area:
211
- x, y, w, h = cv2.boundingRect(c)
212
- cand.append({"contour": c, "area": area, "bbox": (x, y, w, h)})
213
-
214
- if not cand:
215
- return {"error": "Nenhum objeto detectado acima do limiar", "items": [], "total_objects": 0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- # ordenação
218
  if sort == "area":
219
- cand.sort(key=lambda d: d["area"], reverse=True)
220
  else:
221
- cand.sort(key=lambda d: d["bbox"][0]) # por X
222
 
223
- # overview com retângulos verdes
224
  overview = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2RGB)
225
-
226
  items: List[Dict] = []
227
  total_pix = 0
228
  total_cor = 0
229
 
230
- for idx, c in enumerate(cand, 1):
231
  x, y, w, h = c["bbox"]
232
 
233
- # ROI com margem, clamped
234
  x0 = max(x - margem, 0)
235
  y0 = max(y - margem, 0)
236
  x1 = min(x + w + margem, W)
@@ -243,32 +294,27 @@ def process_image_bytes_multi(
243
  c_shift = c["contour"] - [x0, y0]
244
  cv2.drawContours(mask_roi, [c_shift], -1, 255, thickness=-1)
245
 
246
- # feather -> alpha 0..1
247
  alpha01 = feather_mask(mask_roi, feather_px)
248
-
249
- # composição no fundo preto (isolado)
250
  iso_rgb = compose_on_black(roi_bgr, alpha01)
251
 
252
- # para métricas de corrosão, crie máscara do objeto no espaço original
253
  obj_mask_full = np.zeros((H, W), dtype=np.uint8)
254
  cv2.drawContours(obj_mask_full, [c["contour"]], -1, 255, thickness=-1)
255
 
256
- # métricas de corrosão (no original, limitado à máscara do objeto)
257
- mask_white_full = corrosion_mask_from_isolated(img, obj_mask_full)
258
-
259
  total_pixels = int(np.count_nonzero(obj_mask_full))
260
- corrosion_pixels = int(np.count_nonzero(mask_white_full))
261
  percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
262
 
263
  total_pix += total_pixels
264
  total_cor += corrosion_pixels
265
 
266
- # visual da corrosão limitado à ROI (em cima do isolado)
267
- mask_white_roi = mask_white_full[y0:y1, x0:x1]
268
- corro_vis_rgb = (iso_rgb.copy()).astype(np.uint8)
269
- # aplica como máscara no RGB já composto
270
  for ch in range(3):
271
- corro_vis_rgb[..., ch] = cv2.bitwise_and(corro_vis_rgb[..., ch], mask_white_roi)
272
 
273
  items.append({
274
  "id": idx,
@@ -281,10 +327,11 @@ def process_image_bytes_multi(
281
  "corrosion_image": to_data_uri_rgb(corro_vis_rgb),
282
  })
283
 
284
- # desenha bbox + label na overview
285
  cv2.rectangle(overview, (x, y), (x + w, y + h), (0, 255, 0), 2)
286
- cv2.putText(overview, f"#{idx} {percent:.1f}%", (x, max(0, y - 6)),
287
- cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 50, 50), 2, cv2.LINE_AA)
 
288
 
289
  overall = (total_cor / max(1, total_pix)) * 100.0
290
 
@@ -295,29 +342,32 @@ def process_image_bytes_multi(
295
  "total_corrosion_pixels": int(total_cor),
296
  "overall_percent": round(overall, 4),
297
  "overview_image": to_data_uri_rgb(overview),
 
298
  }
299
 
300
  # ----------------- API -----------------
301
  @app.post("/analyze")
302
  async def analyze(
303
  file: UploadFile = File(...),
304
- margem: int = Query(5, ge=0, description="pixels extras no recorte"),
305
- min_area_rel: float = Query(1/30000, gt=0, description="fração da área total para filtrar ruído"),
306
- kernel_sz: int = Query(3, ge=1, description="kernel morfológico (ímpar)"),
307
- dilatacao_px: float = Query(0.5, ge=0, description="força da dilatação adicional"),
308
- feather_px: float = Query(1.0, ge=0, description="raio do desfoque para tirar a 'áurea'"),
 
309
  sort: str = Query("x", pattern="^(x|area)$"),
 
 
 
 
 
310
  ):
311
  try:
312
  content = await file.read()
313
  result = process_image_bytes_multi(
314
- content,
315
- margem=margem,
316
- min_area_rel=min_area_rel,
317
- kernel_sz=kernel_sz,
318
- dilatacao_px=dilatacao_px,
319
- feather_px=feather_px,
320
- sort=sort,
321
  )
322
  return JSONResponse(result)
323
  except HTTPException:
@@ -330,3 +380,4 @@ async def analyze(
330
  def read_root():
331
  return {"status": "ok"}
332
 
 
 
108
  # @app.get("/")
109
  # def read_root():
110
  # return {"status": "ok"}
 
 
111
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
112
  from fastapi.middleware.cors import CORSMiddleware
113
  from fastapi.responses import JSONResponse
 
117
  import traceback
118
  from typing import Dict, List
119
 
120
+ app = FastAPI(title="Detector de Corrosão Branca — multi-objetos")
121
 
122
+ # Em produção, restrinja allow_origins ao seu domínio
123
  app.add_middleware(
124
  CORSMiddleware,
125
+ allow_origins=["*"],
126
  allow_credentials=True,
127
  allow_methods=["*"],
128
  allow_headers=["*"],
129
  )
130
 
131
+ # ----------------- utilidades -----------------
132
  def to_data_uri_rgb(img_rgb: np.ndarray) -> str | None:
133
  if img_rgb is None:
134
  return None
 
140
  return f"data:image/png;base64,{b64}"
141
 
142
  def feather_mask(mask_u8: np.ndarray, feather_px: float) -> np.ndarray:
143
+ """Mask float [0..1] com feather opcional."""
144
  if feather_px and feather_px > 0:
145
  alpha = cv2.GaussianBlur(mask_u8, (0, 0), feather_px).astype(np.float32) / 255.0
146
  else:
 
148
  return np.clip(alpha, 0.0, 1.0)
149
 
150
  def compose_on_black(bgr: np.ndarray, alpha01: np.ndarray) -> np.ndarray:
151
+ """Aplica alpha na ROI BGR e retorna RGB uint8 em fundo preto."""
152
  comp = (bgr.astype(np.float32) * alpha01[..., None]).astype(np.uint8)
153
  return cv2.cvtColor(comp, cv2.COLOR_BGR2RGB)
154
 
155
+ def corrosion_mask_from_obj(bgr_full: np.ndarray, obj_mask_full: np.ndarray) -> np.ndarray:
156
+ """'Corrosão branca' = S baixo, V alto dentro do objeto."""
157
+ hsv = cv2.cvtColor(bgr_full, cv2.COLOR_BGR2HSV)
158
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
159
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
160
  m = cv2.inRange(hsv, lower_white, upper_white)
161
+ return cv2.bitwise_and(m, m, mask=obj_mask_full)
162
 
163
  # ----------------- core -----------------
164
  def process_image_bytes_multi(
165
  img_bytes: bytes,
166
+ margem: int = 6,
167
+ min_area_rel: float = 1/20000,
168
+ max_area_rel: float = 0.25,
169
  kernel_sz: int = 3,
170
+ dilatacao_px: float = 1.0,
171
  feather_px: float = 1.0,
172
  sort: str = "x",
173
+ ar_min: float = 0.6,
174
+ ar_max: float = 1.6,
175
+ exclude_border: int = 8,
176
+ min_solidity: float = 0.75,
177
+ min_circ: float = 0.35,
178
  ) -> Dict:
179
  # decodifica
180
  nparr = np.frombuffer(img_bytes, np.uint8)
 
182
  if img is None:
183
  raise ValueError("Não foi possível decodificar a imagem.")
184
  H, W = img.shape[:2]
185
+ img_area = H * W
186
 
187
+ # --- segmentação estilo notebook ---
188
  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
189
  gray = cv2.GaussianBlur(gray, (5, 5), 0)
190
 
 
191
  _, thr_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
192
  thr_adap = cv2.adaptiveThreshold(
193
  gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 51, 2
194
  )
195
  thr = cv2.bitwise_or(thr_otsu, thr_adap)
196
 
197
+ ksz = max(1, kernel_sz) | 1
198
+ k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ksz, ksz))
199
  thr = cv2.morphologyEx(thr, cv2.MORPH_CLOSE, k, iterations=1)
200
 
 
201
  if dilatacao_px and dilatacao_px > 0:
202
+ dsz = int(2 * dilatacao_px + 1)
203
+ dsz = max(1, dsz) | 1
204
+ k_dil = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dsz, dsz))
205
  thr = cv2.dilate(thr, k_dil, iterations=1)
206
 
 
207
  cnts, _ = cv2.findContours(thr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
208
 
209
+ # --- filtros geométricos ---
210
+ min_area = max(200, int(img_area * min_area_rel))
211
+ max_area = int(img_area * max_area_rel)
212
+
213
+ filters_stats = {
214
+ "total_cnts": len(cnts),
215
+ "too_small": 0,
216
+ "too_big": 0,
217
+ "aspect_ratio": 0,
218
+ "border_touch": 0,
219
+ "low_solidity": 0,
220
+ "low_circularity": 0,
221
+ }
222
+
223
+ candidates = []
224
  for c in cnts:
225
  area = cv2.contourArea(c)
226
+ if area < min_area:
227
+ filters_stats["too_small"] += 1
228
+ continue
229
+ if area > max_area:
230
+ filters_stats["too_big"] += 1
231
+ continue
232
+
233
+ x, y, w, h = cv2.boundingRect(c)
234
+
235
+ if exclude_border > 0 and (
236
+ x <= exclude_border or y <= exclude_border or
237
+ x + w >= W - exclude_border or y + h >= H - exclude_border
238
+ ):
239
+ filters_stats["border_touch"] += 1
240
+ continue
241
+
242
+ ar = (w / h) if h > 0 else 0
243
+ if ar < ar_min or ar > ar_max:
244
+ filters_stats["aspect_ratio"] += 1
245
+ continue
246
+
247
+ hull = cv2.convexHull(c)
248
+ hull_area = cv2.contourArea(hull) or 1.0
249
+ solidity = area / hull_area
250
+ if solidity < min_solidity:
251
+ filters_stats["low_solidity"] += 1
252
+ continue
253
+
254
+ perim = cv2.arcLength(c, True) or 1.0
255
+ circularity = (4.0 * np.pi * area) / (perim * perim)
256
+ if circularity < min_circ:
257
+ filters_stats["low_circularity"] += 1
258
+ continue
259
+
260
+ candidates.append({"contour": c, "area": area, "bbox": (x, y, w, h)})
261
+
262
+ if not candidates:
263
+ return {
264
+ "error": "Nenhum objeto após filtros",
265
+ "items": [],
266
+ "total_objects": 0,
267
+ "filters_stats": filters_stats,
268
+ }
269
 
 
270
  if sort == "area":
271
+ candidates.sort(key=lambda d: d["area"], reverse=True)
272
  else:
273
+ candidates.sort(key=lambda d: d["bbox"][0]) # por X (esq→dir)
274
 
275
+ # --- overview + métricas ---
276
  overview = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2RGB)
 
277
  items: List[Dict] = []
278
  total_pix = 0
279
  total_cor = 0
280
 
281
+ for idx, c in enumerate(candidates, 1):
282
  x, y, w, h = c["bbox"]
283
 
284
+ # ROI com margem (clamped)
285
  x0 = max(x - margem, 0)
286
  y0 = max(y - margem, 0)
287
  x1 = min(x + w + margem, W)
 
294
  c_shift = c["contour"] - [x0, y0]
295
  cv2.drawContours(mask_roi, [c_shift], -1, 255, thickness=-1)
296
 
297
+ # feather + composição isolada
298
  alpha01 = feather_mask(mask_roi, feather_px)
 
 
299
  iso_rgb = compose_on_black(roi_bgr, alpha01)
300
 
301
+ # métricas de corrosão no espaço original
302
  obj_mask_full = np.zeros((H, W), dtype=np.uint8)
303
  cv2.drawContours(obj_mask_full, [c["contour"]], -1, 255, thickness=-1)
304
 
305
+ white_full = corrosion_mask_from_obj(img, obj_mask_full)
 
 
306
  total_pixels = int(np.count_nonzero(obj_mask_full))
307
+ corrosion_pixels = int(np.count_nonzero(white_full))
308
  percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
309
 
310
  total_pix += total_pixels
311
  total_cor += corrosion_pixels
312
 
313
+ # visual de corrosão na ROI
314
+ white_roi = white_full[y0:y1, x0:x1]
315
+ corro_vis_rgb = iso_rgb.copy()
 
316
  for ch in range(3):
317
+ corro_vis_rgb[..., ch] = cv2.bitwise_and(corro_vis_rgb[..., ch], white_roi)
318
 
319
  items.append({
320
  "id": idx,
 
327
  "corrosion_image": to_data_uri_rgb(corro_vis_rgb),
328
  })
329
 
330
+ # desenha bbox + rótulo na overview
331
  cv2.rectangle(overview, (x, y), (x + w, y + h), (0, 255, 0), 2)
332
+ cv2.putText(overview, f"#{idx} {percent:.1f}%",
333
+ (x, max(0, y - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6,
334
+ (255, 50, 50), 2, cv2.LINE_AA)
335
 
336
  overall = (total_cor / max(1, total_pix)) * 100.0
337
 
 
342
  "total_corrosion_pixels": int(total_cor),
343
  "overall_percent": round(overall, 4),
344
  "overview_image": to_data_uri_rgb(overview),
345
+ "filters_stats": filters_stats,
346
  }
347
 
348
  # ----------------- API -----------------
349
  @app.post("/analyze")
350
  async def analyze(
351
  file: UploadFile = File(...),
352
+ margem: int = Query(6, ge=0),
353
+ min_area_rel: float = Query(1/20000, gt=0),
354
+ max_area_rel: float = Query(0.25, gt=0, le=1.0),
355
+ kernel_sz: int = Query(3, ge=1),
356
+ dilatacao_px: float = Query(1.0, ge=0),
357
+ feather_px: float = Query(1.0, ge=0),
358
  sort: str = Query("x", pattern="^(x|area)$"),
359
+ ar_min: float = Query(0.6, gt=0),
360
+ ar_max: float = Query(1.6, gt=0),
361
+ exclude_border: int = Query(8, ge=0),
362
+ min_solidity: float = Query(0.75, ge=0, le=1.0),
363
+ min_circ: float = Query(0.35, ge=0, le=1.0),
364
  ):
365
  try:
366
  content = await file.read()
367
  result = process_image_bytes_multi(
368
+ content, margem, min_area_rel, max_area_rel, kernel_sz,
369
+ dilatacao_px, feather_px, sort, ar_min, ar_max,
370
+ exclude_border, min_solidity, min_circ
 
 
 
 
371
  )
372
  return JSONResponse(result)
373
  except HTTPException:
 
380
  def read_root():
381
  return {"status": "ok"}
382
 
383
+