joao-dutra commited on
Commit
6fdde1e
·
verified ·
1 Parent(s): 4778694
Files changed (1) hide show
  1. app.py +217 -61
app.py CHANGED
@@ -1,110 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # 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
  if img is None:
32
  raise ValueError("Não foi possível decodificar a imagem.")
33
 
34
- # 1) Converter para HSV
35
- hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
 
 
 
 
 
36
 
37
- # 2) máscara do fundo preto (V baixo)
38
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
39
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
40
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
41
-
42
- # 3) objeto = invertendo máscara do fundo
43
  mask_obj = cv2.bitwise_not(mask_bg)
44
 
45
- # 4) limpar máscara (morfologia)
46
  kernel = np.ones((5, 5), np.uint8)
47
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
48
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
49
 
50
- # 5) maior contorno (supõe um parafuso)
51
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
52
  if not contours:
53
- return {"error": "Nenhum objeto detectado"}
54
 
55
- largest = max(contours, key=cv2.contourArea)
56
- mask_clean = np.zeros_like(mask_obj)
57
- cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
 
 
 
 
58
 
59
- # 6) isolar objeto
60
- isolated = cv2.bitwise_and(img, img, mask=mask_clean)
61
 
62
- # 7) detectar corrosão BRANCA (S baixa, V alta)
63
- hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
64
- lower_white = np.array([0, 0, 180], dtype=np.uint8)
65
- upper_white = np.array([180, 60, 255], dtype=np.uint8)
66
- mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
67
- mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
68
 
69
- # 8) métricas
70
- total_pixels = int(np.count_nonzero(mask_clean))
71
- corrosion_pixels = int(np.count_nonzero(mask_white))
72
- percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
 
73
 
74
- # 9) preparar imagens para frontend (PNG base64)
75
- # isolado em RGB para visualização
76
- isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
77
- corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
78
 
79
- def to_data_uri(img_arr):
80
- # img_arr: RGB uint8
81
- bgr = cv2.cvtColor(img_arr, cv2.COLOR_RGB2BGR)
82
- ok, buf = cv2.imencode(".png", bgr)
83
- if not ok:
84
- return None
85
- b64 = base64.b64encode(buf.tobytes()).decode("ascii")
86
- return f"data:image/png;base64,{b64}"
87
 
88
- isolated_b64 = to_data_uri(isolated_rgb)
89
- corrosion_b64 = to_data_uri(corrosion_vis)
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
  return {
92
- "percent": round(percent, 4),
93
- "total_pixels": total_pixels,
94
- "corrosion_pixels": corrosion_pixels,
95
- "isolated_image": isolated_b64,
96
- "corrosion_image": corrosion_b64,
 
97
  }
98
 
99
  @app.post("/analyze")
100
- async def analyze(file: UploadFile = File(...)):
101
- content = await file.read()
 
 
 
102
  try:
103
- result = process_image_bytes(content)
104
- except ValueError as e:
105
- raise HTTPException(status_code=400, detail=str(e))
106
- return JSONResponse(result)
 
 
 
 
107
 
108
  @app.get("/")
109
  def read_root():
110
- 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
+ # 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
+ # if img is None:
32
+ # raise ValueError("Não foi possível decodificar a imagem.")
33
+
34
+ # # 1) Converter para HSV
35
+ # hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
36
+
37
+ # # 2) máscara do fundo preto (V baixo)
38
+ # lower_bg = np.array([0, 0, 0], dtype=np.uint8)
39
+ # upper_bg = np.array([180, 255, 50], dtype=np.uint8)
40
+ # mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
41
+
42
+ # # 3) objeto = invertendo máscara do fundo
43
+ # mask_obj = cv2.bitwise_not(mask_bg)
44
+
45
+ # # 4) limpar máscara (morfologia)
46
+ # kernel = np.ones((5, 5), np.uint8)
47
+ # mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
48
+ # mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
49
+
50
+ # # 5) maior contorno (supõe um parafuso)
51
+ # contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
52
+ # if not contours:
53
+ # return {"error": "Nenhum objeto detectado"}
54
+
55
+ # largest = max(contours, key=cv2.contourArea)
56
+ # mask_clean = np.zeros_like(mask_obj)
57
+ # cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
58
+
59
+ # # 6) isolar objeto
60
+ # isolated = cv2.bitwise_and(img, img, mask=mask_clean)
61
+
62
+ # # 7) detectar corrosão BRANCA (S baixa, V alta)
63
+ # hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
64
+ # lower_white = np.array([0, 0, 180], dtype=np.uint8)
65
+ # upper_white = np.array([180, 60, 255], dtype=np.uint8)
66
+ # mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
67
+ # mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
68
+
69
+ # # 8) métricas
70
+ # total_pixels = int(np.count_nonzero(mask_clean))
71
+ # corrosion_pixels = int(np.count_nonzero(mask_white))
72
+ # percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
73
+
74
+ # # 9) preparar imagens para frontend (PNG base64)
75
+ # # isolado em RGB para visualização
76
+ # isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
77
+ # corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
78
+
79
+ # def to_data_uri(img_arr):
80
+ # # img_arr: RGB uint8
81
+ # bgr = cv2.cvtColor(img_arr, cv2.COLOR_RGB2BGR)
82
+ # ok, buf = cv2.imencode(".png", bgr)
83
+ # if not ok:
84
+ # return None
85
+ # b64 = base64.b64encode(buf.tobytes()).decode("ascii")
86
+ # return f"data:image/png;base64,{b64}"
87
+
88
+ # isolated_b64 = to_data_uri(isolated_rgb)
89
+ # corrosion_b64 = to_data_uri(corrosion_vis)
90
+
91
+ # return {
92
+ # "percent": round(percent, 4),
93
+ # "total_pixels": total_pixels,
94
+ # "corrosion_pixels": corrosion_pixels,
95
+ # "isolated_image": isolated_b64,
96
+ # "corrosion_image": corrosion_b64,
97
+ # }
98
+
99
+ # @app.post("/analyze")
100
+ # async def analyze(file: UploadFile = File(...)):
101
+ # content = await file.read()
102
+ # try:
103
+ # result = process_image_bytes(content)
104
+ # except ValueError as e:
105
+ # raise HTTPException(status_code=400, detail=str(e))
106
+ # return JSONResponse(result)
107
+
108
+ # @app.get("/")
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
116
  from fastapi.responses import JSONResponse
117
  import numpy as np
118
  import cv2
119
  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:
137
+ return 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
260
+ except Exception as e:
261
+ traceback.print_exc()
262
+ raise HTTPException(status_code=400, detail=f"processing_error: {e}")
263
 
264
  @app.get("/")
265
  def read_root():
266
+ return {"status": "ok"}