joao-dutra commited on
Commit
688d726
·
verified ·
1 Parent(s): ebbf40a
Files changed (1) hide show
  1. app.py +53 -42
app.py CHANGED
@@ -5,11 +5,9 @@ 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,94 +16,107 @@ 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
  lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
26
  l, a, b = cv2.split(lab)
27
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
28
- l_clahe = clahe.apply(l)
29
- lab_clahe = cv2.merge([l_clahe, a, b])
30
- img = cv2.cvtColor(lab_clahe, cv2.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)
 
 
 
108
 
109
  @app.get("/")
110
  def read_root():
111
- return {"status": "ok"}
 
5
  import numpy as np
6
  import cv2
7
  import base64
 
8
 
9
  app = FastAPI(title="Detector de Corrosão Branca")
10
 
 
11
  app.add_middleware(
12
  CORSMiddleware,
13
  allow_origins=["*"],
 
16
  allow_headers=["*"],
17
  )
18
 
19
+ def to_data_uri_rgb(img_rgb: np.ndarray) -> str:
20
+ # img_rgb: HxWx3 uint8
21
+ if img_rgb is None or img_rgb.size == 0:
22
+ return None
23
+ bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
24
+ ok, buf = cv2.imencode(".png", bgr)
25
+ if not ok:
26
+ return None
27
+ import base64
28
+ b64 = base64.b64encode(buf.tobytes()).decode("ascii")
29
+ return f"data:image/png;base64,{b64}"
30
+
31
  def process_image_bytes(img_bytes: bytes):
32
+ # 0) decodificar bytes
33
  nparr = np.frombuffer(img_bytes, np.uint8)
34
+ img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
35
+
36
+ if img is None:
37
+ raise ValueError("Não foi possível decodificar a imagem enviada.")
38
+
39
+ # 0.1) garantir BGR (remover alpha se existir)
40
+ if img.ndim == 2:
41
+ img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
42
+ elif img.shape[2] == 4:
43
+ img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
44
+
45
+ # 1) equalização local leve para realçar contraste (CLAHE no L do LAB)
46
  lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
47
  l, a, b = cv2.split(lab)
48
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
49
+ l = clahe.apply(l)
50
+ img = cv2.cvtColor(cv2.merge([l, a, b]), cv2.COLOR_LAB2BGR)
 
 
 
 
51
 
52
+ # 2) HSV e máscara de fundo preto
53
  hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
 
 
54
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
55
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
56
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
 
 
57
  mask_obj = cv2.bitwise_not(mask_bg)
58
 
59
+ # 3) limpeza morfológica
60
  kernel = np.ones((5, 5), np.uint8)
61
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
62
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
63
 
64
+ # 4) maior componente (parafuso)
65
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
66
  if not contours:
67
+ return {
68
+ "percent": 0.0,
69
+ "total_pixels": 0,
70
+ "corrosion_pixels": 0,
71
+ "isolated_image": None,
72
+ "corrosion_image": None,
73
+ "warning": "Nenhum objeto detectado (fundo pode não ser preto)."
74
+ }
75
 
76
  largest = max(contours, key=cv2.contourArea)
77
  mask_clean = np.zeros_like(mask_obj)
78
  cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
79
 
80
+ # 5) isolar objeto
81
  isolated = cv2.bitwise_and(img, img, mask=mask_clean)
82
 
83
+ # 6) detectar CORROSÃO BRANCA (S baixa e V alta)
84
  hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
85
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
86
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
87
  mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
88
  mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
89
 
90
+ # 7) métricas
91
  total_pixels = int(np.count_nonzero(mask_clean))
92
  corrosion_pixels = int(np.count_nonzero(mask_white))
93
  percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
94
 
95
+ # 8) imagens para o front (RGB + data URI)
 
96
  isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
97
  corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
98
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  return {
100
  "percent": round(percent, 4),
101
  "total_pixels": total_pixels,
102
  "corrosion_pixels": corrosion_pixels,
103
+ "isolated_image": to_data_uri_rgb(isolated_rgb),
104
+ "corrosion_image": to_data_uri_rgb(corrosion_vis),
105
  }
106
 
107
  @app.post("/analyze")
108
  async def analyze(file: UploadFile = File(...)):
 
109
  try:
110
+ content = await file.read()
111
  result = process_image_bytes(content)
112
+ return JSONResponse(result, status_code=200)
113
  except ValueError as e:
114
+ # erro esperado do usuário/entrada -> 400 com JSON
115
+ return JSONResponse({"error": str(e)}, status_code=400)
116
+ except Exception as e:
117
+ # qualquer outra falha -> ainda responder JSON, não HTML
118
+ return JSONResponse({"error": f"Falha interna: {type(e).__name__}: {str(e)}"}, status_code=500)
119
 
120
  @app.get("/")
121
  def read_root():
122
+ return {"status": "ok"}