joao-dutra commited on
Commit
fbad683
·
verified ·
1 Parent(s): 6fdde1e
Files changed (1) hide show
  1. app.py +145 -79
app.py CHANGED
@@ -109,7 +109,6 @@
109
  # def read_root():
110
  # return {"status": "ok"}
111
 
112
-
113
  # app.py
114
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
115
  from fastapi.middleware.cors import CORSMiddleware
@@ -120,17 +119,20 @@ import base64
120
  import traceback
121
  from typing import Dict, List
122
 
123
- app = FastAPI(title="Detector de Corrosão Branca")
124
 
125
  app.add_middleware(
126
  CORSMiddleware,
127
- allow_origins=["*"], # restrinja em produção
128
  allow_credentials=True,
129
  allow_methods=["*"],
130
  allow_headers=["*"],
131
  )
132
 
 
133
  def to_data_uri_rgb(img_rgb: np.ndarray) -> str | None:
 
 
134
  bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
135
  ok, buf = cv2.imencode(".png", bgr)
136
  if not ok:
@@ -138,122 +140,185 @@ def to_data_uri_rgb(img_rgb: np.ndarray) -> str | None:
138
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
139
  return f"data:image/png;base64,{b64}"
140
 
141
- def process_one_object(img_bgr: np.ndarray, obj_mask: np.ndarray) -> Dict:
142
- isolated = cv2.bitwise_and(img_bgr, img_bgr, mask=obj_mask)
143
- hsv = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
144
-
145
- # "branco" (S baixo, V alto) — ajuste se necessário
 
 
 
 
 
 
 
 
 
 
 
146
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
147
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
148
- mask_white = cv2.inRange(hsv, lower_white, upper_white)
149
- mask_white = cv2.bitwise_and(mask_white, mask_white, mask=obj_mask)
150
-
151
- total_pixels = int(np.count_nonzero(obj_mask))
152
- corrosion_pixels = int(np.count_nonzero(mask_white))
153
- percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
154
-
155
- iso_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
156
- corro_vis = cv2.bitwise_and(iso_rgb, iso_rgb, mask=mask_white)
157
-
158
- return {
159
- "total_pixels": total_pixels,
160
- "corrosion_pixels": corrosion_pixels,
161
- "percent": round(percent, 4),
162
- "isolated_image": to_data_uri_rgb(iso_rgb),
163
- "corrosion_image": to_data_uri_rgb(corro_vis),
164
- }
165
-
166
- def process_image_bytes_multi(img_bytes: bytes, min_area: int = 1500, sort: str = "x") -> Dict:
167
  nparr = np.frombuffer(img_bytes, np.uint8)
168
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
169
  if img is None:
170
  raise ValueError("Não foi possível decodificar a imagem.")
171
-
172
- # CLAHE no canal L (corrigido para cv2)
173
- lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
174
- l, a, b = cv2.split(lab)
175
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
176
- l2 = clahe.apply(l)
177
- lab2 = cv2.merge([l2, a, b])
178
- img = cv2.cvtColor(lab2, cv2.COLOR_LAB2BGR)
179
-
180
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
181
- lower_bg = np.array([0, 0, 0], dtype=np.uint8)
182
- upper_bg = np.array([180, 255, 50], dtype=np.uint8)
183
- mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
184
- mask_obj = cv2.bitwise_not(mask_bg)
185
-
186
- kernel = np.ones((5, 5), np.uint8)
187
- mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
188
- mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
189
-
190
- contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
191
- if not contours:
192
- return {"error": "Nenhum objeto detectado", "items": [], "total_objects": 0}
193
-
194
- # filtra objetos pequenos
 
 
 
 
195
  cand = []
196
- for c in contours:
197
  area = cv2.contourArea(c)
198
  if area >= min_area:
199
  x, y, w, h = cv2.boundingRect(c)
200
  cand.append({"contour": c, "area": area, "bbox": (x, y, w, h)})
201
 
202
  if not cand:
203
- return {"error": "Somente ruído abaixo do min_area", "items": [], "total_objects": 0}
204
 
205
- cand.sort(key=(lambda d: d["area"]), reverse=(sort == "area"))
206
- if sort == "x":
207
- cand.sort(key=lambda d: d["bbox"][0])
 
 
208
 
209
- # overview com caixas verdes
210
  overview = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2RGB)
 
211
  items: List[Dict] = []
212
- tot_pix = 0
213
- tot_cor = 0
214
 
215
- h, w = mask_obj.shape[:2]
216
- for i, c in enumerate(cand, 1):
217
- obj_mask = np.zeros((h, w), dtype=np.uint8)
218
- cv2.drawContours(obj_mask, [c["contour"]], -1, 255, cv2.FILLED)
219
 
220
- r = process_one_object(img, obj_mask)
221
- tot_pix += r["total_pixels"]
222
- tot_cor += r["corrosion_pixels"]
 
 
223
 
224
- x, y, ww, hh = c["bbox"]
225
- cv2.rectangle(overview, (x, y), (x + ww, y + hh), (0, 255, 0), 2)
226
- cv2.putText(overview, f"#{i} {r['percent']:.1f}%",
227
- (x, max(0, y - 6)), cv2.FONT_HERSHEY_SIMPLEX, 0.6,
228
- (255, 50, 50), 2, cv2.LINE_AA)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  items.append({
231
- "id": i,
232
- "bbox": {"x": x, "y": y, "w": ww, "h": hh},
233
  "area_pixels": int(c["area"]),
234
- **r,
 
 
 
 
235
  })
236
 
237
- overall = (tot_cor / max(1, tot_pix)) * 100.0
 
 
 
 
 
238
 
239
  return {
240
  "total_objects": len(items),
241
  "items": items,
242
- "total_pixels": int(tot_pix),
243
- "total_corrosion_pixels": int(tot_cor),
244
  "overall_percent": round(overall, 4),
245
  "overview_image": to_data_uri_rgb(overview),
246
  }
247
 
 
248
  @app.post("/analyze")
249
  async def analyze(
250
  file: UploadFile = File(...),
251
- min_area: int = Query(1500, ge=1),
 
 
 
 
252
  sort: str = Query("x", pattern="^(x|area)$"),
253
  ):
254
  try:
255
  content = await file.read()
256
- result = process_image_bytes_multi(content, min_area=min_area, sort=sort)
 
 
 
 
 
 
 
 
257
  return JSONResponse(result)
258
  except HTTPException:
259
  raise
@@ -264,3 +329,4 @@ async def analyze(
264
  @app.get("/")
265
  def read_root():
266
  return {"status": "ok"}
 
 
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
 
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
136
  bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
137
  ok, buf = cv2.imencode(".png", bgr)
138
  if not ok:
 
140
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
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:
148
+ alpha = (mask_u8.astype(np.float32) / 255.0)
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)
176
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
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)
237
+ y1 = min(y + h + margem, H)
238
 
239
+ roi_bgr = img[y0:y1, x0:x1].copy()
240
+
241
+ # máscara do contorno deslocado para ROI
242
+ mask_roi = np.zeros((y1 - y0, x1 - x0), dtype=np.uint8)
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,
275
+ "bbox": {"x": int(x), "y": int(y), "w": int(w), "h": int(h)},
276
  "area_pixels": int(c["area"]),
277
+ "total_pixels": total_pixels,
278
+ "corrosion_pixels": corrosion_pixels,
279
+ "percent": round(percent, 4),
280
+ "isolated_image": to_data_uri_rgb(iso_rgb),
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
 
291
  return {
292
  "total_objects": len(items),
293
  "items": items,
294
+ "total_pixels": int(total_pix),
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:
324
  raise
 
329
  @app.get("/")
330
  def read_root():
331
  return {"status": "ok"}
332
+