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

Bove Teste

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