joao-dutra commited on
Commit
aaac6d6
·
verified ·
1 Parent(s): b70cb7b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -50
app.py CHANGED
@@ -1,15 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # app.py
2
- from fastapi import FastAPI, File, UploadFile, HTTPException
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from fastapi.responses import JSONResponse
5
  import numpy as np
6
  import cv2
7
  import base64
8
- import io
9
 
10
- app = FastAPI(title="Detector de Corrosão Branca")
11
 
12
- # PARA PROTOTIPO: permitir todas origens. Em produção restrinja ao domínio do frontend.
13
  app.add_middleware(
14
  CORSMiddleware,
15
  allow_origins=["*"],
@@ -18,83 +123,153 @@ app.add_middleware(
18
  allow_headers=["*"],
19
  )
20
 
21
- def process_image_bytes(img_bytes: bytes):
22
- # bytes em numpy + OpenCV
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  nparr = np.frombuffer(img_bytes, np.uint8)
24
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
25
  if img is None:
26
  raise ValueError("Não foi possível decodificar a imagem.")
27
 
28
- # 1) Converter para HSV
29
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
30
 
31
- # 2) máscara do fundo preto (V baixo)
 
32
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
33
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
34
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
35
-
36
- # 3) objeto = invertendo máscara do fundo
37
  mask_obj = cv2.bitwise_not(mask_bg)
38
 
39
- # 4) limpar máscara (morfologia)
40
  kernel = np.ones((5, 5), np.uint8)
41
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
42
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
43
 
44
- # 5) maior contorno (supõe um parafuso)
45
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
46
  if not contours:
47
- return {"error": "Nenhum objeto detectado"}
48
 
49
- largest = max(contours, key=cv2.contourArea)
50
- mask_clean = np.zeros_like(mask_obj)
51
- cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
 
 
 
 
52
 
53
- # 6) isolar objeto
54
- isolated = cv2.bitwise_and(img, img, mask=mask_clean)
55
 
56
- # 7) detectar corrosão BRANCA (S baixa, V alta)
57
- hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
58
- lower_white = np.array([0, 0, 180], dtype=np.uint8)
59
- upper_white = np.array([180, 60, 255], dtype=np.uint8)
60
- mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
61
- mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
62
 
63
- # 8) métricas
64
- total_pixels = int(np.count_nonzero(mask_clean))
65
- corrosion_pixels = int(np.count_nonzero(mask_white))
66
- percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
67
 
68
- # 9) preparar imagens para frontend (PNG base64)
69
- # isolado em RGB para visualização
70
- isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
71
- corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- def to_data_uri(img_arr):
74
- # img_arr: RGB uint8
75
- bgr = cv2.cvtColor(img_arr, cv2.COLOR_RGB2BGR)
76
- ok, buf = cv2.imencode(".png", bgr)
77
- if not ok:
78
- return None
79
- b64 = base64.b64encode(buf.tobytes()).decode("ascii")
80
- return f"data:image/png;base64,{b64}"
81
 
82
- isolated_b64 = to_data_uri(isolated_rgb)
83
- corrosion_b64 = to_data_uri(corrosion_vis)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  return {
86
- "percent": round(percent, 4),
87
- "total_pixels": total_pixels,
88
- "corrosion_pixels": corrosion_pixels,
89
- "isolated_image": isolated_b64,
90
- "corrosion_image": corrosion_b64,
 
91
  }
92
 
93
  @app.post("/analyze")
94
- async def analyze(file: UploadFile = File(...)):
 
 
 
 
 
95
  content = await file.read()
96
  try:
97
- result = process_image_bytes(content)
98
  except ValueError as e:
99
  raise HTTPException(status_code=400, detail=str(e))
100
  return JSONResponse(result)
@@ -102,3 +277,4 @@ async def analyze(file: UploadFile = File(...)):
102
  @app.get("/")
103
  def read_root():
104
  return {"status": "ok"}
 
 
1
+ # # app.py
2
+ # from fastapi import FastAPI, File, UploadFile, HTTPException
3
+ # from fastapi.middleware.cors import CORSMiddleware
4
+ # from fastapi.responses import JSONResponse
5
+ # import numpy as np
6
+ # import cv2
7
+ # import base64
8
+ # import io
9
+
10
+ # app = FastAPI(title="Detector de Corrosão Branca")
11
+
12
+ # # PARA PROTOTIPO: permitir todas origens. Em produção restrinja ao domínio do frontend.
13
+ # app.add_middleware(
14
+ # CORSMiddleware,
15
+ # allow_origins=["*"],
16
+ # allow_credentials=True,
17
+ # allow_methods=["*"],
18
+ # allow_headers=["*"],
19
+ # )
20
+
21
+ # def process_image_bytes(img_bytes: bytes):
22
+ # # lê bytes em numpy + OpenCV
23
+ # nparr = np.frombuffer(img_bytes, np.uint8)
24
+ # img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
25
+ # if img is None:
26
+ # raise ValueError("Não foi possível decodificar a imagem.")
27
+
28
+ # # 1) Converter para HSV
29
+ # hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
30
+
31
+ # # 2) máscara do fundo preto (V baixo)
32
+ # lower_bg = np.array([0, 0, 0], dtype=np.uint8)
33
+ # upper_bg = np.array([180, 255, 50], dtype=np.uint8)
34
+ # mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
35
+
36
+ # # 3) objeto = invertendo máscara do fundo
37
+ # mask_obj = cv2.bitwise_not(mask_bg)
38
+
39
+ # # 4) limpar máscara (morfologia)
40
+ # kernel = np.ones((5, 5), np.uint8)
41
+ # mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
42
+ # mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
43
+
44
+ # # 5) maior contorno (supõe um parafuso)
45
+ # contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
46
+ # if not contours:
47
+ # return {"error": "Nenhum objeto detectado"}
48
+
49
+ # largest = max(contours, key=cv2.contourArea)
50
+ # mask_clean = np.zeros_like(mask_obj)
51
+ # cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
52
+
53
+ # # 6) isolar objeto
54
+ # isolated = cv2.bitwise_and(img, img, mask=mask_clean)
55
+
56
+ # # 7) detectar corrosão BRANCA (S baixa, V alta)
57
+ # hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
58
+ # lower_white = np.array([0, 0, 180], dtype=np.uint8)
59
+ # upper_white = np.array([180, 60, 255], dtype=np.uint8)
60
+ # mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
61
+ # mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
62
+
63
+ # # 8) métricas
64
+ # total_pixels = int(np.count_nonzero(mask_clean))
65
+ # corrosion_pixels = int(np.count_nonzero(mask_white))
66
+ # percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
67
+
68
+ # # 9) preparar imagens para frontend (PNG base64)
69
+ # # isolado em RGB para visualização
70
+ # isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
71
+ # corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
72
+
73
+ # def to_data_uri(img_arr):
74
+ # # img_arr: RGB uint8
75
+ # bgr = cv2.cvtColor(img_arr, cv2.COLOR_RGB2BGR)
76
+ # ok, buf = cv2.imencode(".png", bgr)
77
+ # if not ok:
78
+ # return None
79
+ # b64 = base64.b64encode(buf.tobytes()).decode("ascii")
80
+ # return f"data:image/png;base64,{b64}"
81
+
82
+ # isolated_b64 = to_data_uri(isolated_rgb)
83
+ # corrosion_b64 = to_data_uri(corrosion_vis)
84
+
85
+ # return {
86
+ # "percent": round(percent, 4),
87
+ # "total_pixels": total_pixels,
88
+ # "corrosion_pixels": corrosion_pixels,
89
+ # "isolated_image": isolated_b64,
90
+ # "corrosion_image": corrosion_b64,
91
+ # }
92
+
93
+ # @app.post("/analyze")
94
+ # async def analyze(file: UploadFile = File(...)):
95
+ # content = await file.read()
96
+ # try:
97
+ # result = process_image_bytes(content)
98
+ # except ValueError as e:
99
+ # raise HTTPException(status_code=400, detail=str(e))
100
+ # return JSONResponse(result)
101
+
102
+ # @app.get("/")
103
+ # def read_root():
104
+ # return {"status": "ok"}
105
+
106
  # app.py
107
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query
108
  from fastapi.middleware.cors import CORSMiddleware
109
  from fastapi.responses import JSONResponse
110
  import numpy as np
111
  import cv2
112
  import base64
113
+ from typing import List, Dict
114
 
115
+ app = FastAPI(title="Detector de Corrosão Branca (multi-objetos)")
116
 
117
+ # CORS (ajuste allow_origins em produção)
118
  app.add_middleware(
119
  CORSMiddleware,
120
  allow_origins=["*"],
 
123
  allow_headers=["*"],
124
  )
125
 
126
+ def to_data_uri_from_rgb(img_rgb: np.ndarray) -> str | None:
127
+ """Recebe imagem RGB uint8 e retorna data URI PNG."""
128
+ if img_rgb is None:
129
+ return None
130
+ bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
131
+ ok, buf = cv2.imencode(".png", bgr)
132
+ if not ok:
133
+ return None
134
+ b64 = base64.b64encode(buf.tobytes()).decode("ascii")
135
+ return f"data:image/png;base64,{b64}"
136
+
137
+ def process_one_object(img_bgr: np.ndarray, obj_mask: np.ndarray) -> Dict:
138
+ """
139
+ Calcula métricas e imagens para um único objeto (parafuso).
140
+ - img_bgr: imagem original BGR
141
+ - obj_mask: máscara binária 0/255 do objeto (mesmo tamanho da imagem)
142
+ """
143
+ # isolar objeto em BGR e converter para RGB p/ visualização
144
+ isolated = cv2.bitwise_and(img_bgr, img_bgr, mask=obj_mask)
145
+ isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
146
+
147
+ # corrosão branca: S baixo, V alto (em HSV)
148
+ hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
149
+ lower_white = np.array([0, 0, 180], dtype=np.uint8)
150
+ upper_white = np.array([180, 60, 255], dtype=np.uint8)
151
+ mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
152
+ mask_white = cv2.bitwise_and(mask_white, mask_white, mask=obj_mask)
153
+
154
+ total_pixels = int(np.count_nonzero(obj_mask))
155
+ corrosion_pixels = int(np.count_nonzero(mask_white))
156
+ percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
157
+
158
+ # visual da corrosão em cima do isolado
159
+ corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
160
+
161
+ return {
162
+ "total_pixels": total_pixels,
163
+ "corrosion_pixels": corrosion_pixels,
164
+ "percent": round(percent, 4),
165
+ "isolated_image": to_data_uri_from_rgb(isolated_rgb),
166
+ "corrosion_image": to_data_uri_from_rgb(corrosion_vis),
167
+ }
168
+
169
+ def process_image_bytes_multi(img_bytes: bytes, min_area: int, max_items: int, sort: str):
170
+ # decodifica
171
  nparr = np.frombuffer(img_bytes, np.uint8)
172
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
173
  if img is None:
174
  raise ValueError("Não foi possível decodificar a imagem.")
175
 
176
+ h, w = img.shape[:2]
 
177
 
178
+ # HSV + fundo preto
179
+ hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
180
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
181
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
182
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
 
 
183
  mask_obj = cv2.bitwise_not(mask_bg)
184
 
185
+ # limpeza morfológica
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
+ # contornos externos
191
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
192
  if not contours:
193
+ return {"error": "Nenhum objeto detectado", "items": []}
194
 
195
+ # filtra por área
196
+ candidates = []
197
+ for c in contours:
198
+ area = cv2.contourArea(c)
199
+ if area >= max(1, min_area):
200
+ x, y, ww, hh = cv2.boundingRect(c)
201
+ candidates.append({"contour": c, "area": area, "bbox": (x, y, ww, hh)})
202
 
203
+ if not candidates:
204
+ return {"error": "Somente ruído encontrado abaixo do min_area", "items": []}
205
 
206
+ # ordenação
207
+ if sort == "area":
208
+ candidates.sort(key=lambda d: d["area"], reverse=True)
209
+ else: # "x" (esquerda -> direita)
210
+ candidates.sort(key=lambda d: d["bbox"][0])
 
211
 
212
+ candidates = candidates[:max_items]
 
 
 
213
 
214
+ # imagem de overview (RGB) p/ desenhar anotações
215
+ overview = cv2.cvtColor(img.copy(), cv2.COLOR_BGR2RGB)
216
+
217
+ items: List[Dict] = []
218
+ total_pixels_sum = 0
219
+ corrosion_pixels_sum = 0
220
+
221
+ for idx, obj in enumerate(candidates, 1):
222
+ c = obj["contour"]
223
+ x, y, ww, hh = obj["bbox"]
224
+
225
+ # máscara do objeto atual
226
+ obj_mask = np.zeros((h, w), dtype=np.uint8)
227
+ cv2.drawContours(obj_mask, [c], -1, 255, cv2.FILLED)
228
+
229
+ # métricas e imagens do objeto
230
+ r = process_one_object(img, obj_mask)
231
 
232
+ # acumula totais
233
+ total_pixels_sum += r["total_pixels"]
234
+ corrosion_pixels_sum += r["corrosion_pixels"]
 
 
 
 
 
235
 
236
+ # desenha no overview
237
+ cv2.rectangle(overview, (x, y), (x + ww, y + hh), (0, 255, 0), 2)
238
+ label = f"#{idx} {r['percent']:.2f}%"
239
+ cv2.putText(overview, label, (x, max(0, y - 6)),
240
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 50, 50), 2, cv2.LINE_AA)
241
+
242
+ items.append({
243
+ "id": idx,
244
+ "bbox": {"x": x, "y": y, "w": ww, "h": hh},
245
+ "area_pixels": int(obj["area"]),
246
+ **r, # total_pixels, corrosion_pixels, percent, images...
247
+ })
248
+
249
+ overall_percent = (corrosion_pixels_sum / max(1, total_pixels_sum)) * 100.0
250
+
251
+ # adiciona overview
252
+ overview_data_uri = to_data_uri_from_rgb(overview)
253
 
254
  return {
255
+ "total_objects": len(items),
256
+ "items": items,
257
+ "total_pixels": int(total_pixels_sum),
258
+ "total_corrosion_pixels": int(corrosion_pixels_sum),
259
+ "overall_percent": round(overall_percent, 4),
260
+ "overview_image": overview_data_uri,
261
  }
262
 
263
  @app.post("/analyze")
264
+ async def analyze(
265
+ file: UploadFile = File(...),
266
+ min_area: int = Query(1500, ge=1, description="Área mínima do objeto (px)"),
267
+ max_items: int = Query(20, ge=1, le=200, description="Limite de objetos"),
268
+ sort: str = Query("x", pattern="^(x|area)$", description="Ordenação: x|area"),
269
+ ):
270
  content = await file.read()
271
  try:
272
+ result = process_image_bytes_multi(content, min_area=min_area, max_items=max_items, sort=sort)
273
  except ValueError as e:
274
  raise HTTPException(status_code=400, detail=str(e))
275
  return JSONResponse(result)
 
277
  @app.get("/")
278
  def read_root():
279
  return {"status": "ok"}
280
+