joao-dutra commited on
Commit
f3a53d2
·
verified ·
1 Parent(s): 8210e48

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -28
app.py CHANGED
@@ -7,6 +7,12 @@ import numpy as np
7
  import cv2
8
  import base64
9
  import mimetypes
 
 
 
 
 
 
10
  from fastapi import FastAPI, File, UploadFile, HTTPException
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import JSONResponse
@@ -16,6 +22,11 @@ from botocore.exceptions import NoCredentialsError
16
  # 1. CONFIGURAÇÃO E CLIENTES (CARREGADOS NA INICIALIZAÇÃO)
17
  # ==============================================================================
18
 
 
 
 
 
 
19
  # Carrega as configurações das variáveis de ambiente (Secrets do Hugging Face)
20
  AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
21
  AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
@@ -24,7 +35,7 @@ AWS_S3_REGION = os.getenv("AWS_S3_REGION")
24
  SUPABASE_URL = os.getenv("SUPABASE_URL")
25
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
26
 
27
- # Validação para garantir que todos os secrets foram configurados
28
  if not all([AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET_NAME, AWS_S3_REGION, SUPABASE_URL, SUPABASE_KEY]):
29
  raise RuntimeError("Erro: Nem todos os secrets necessários foram configurados no Hugging Face Space.")
30
 
@@ -44,7 +55,7 @@ except Exception as e:
44
  # Inicializa a aplicação FastAPI
45
  app = FastAPI(title="Detector de Corrosão Branca com Salvamento S3/Supabase")
46
 
47
- # Em produção, restrinja ao domínio do seu frontend
48
  app.add_middleware(
49
  CORSMiddleware,
50
  allow_origins=["*"],
@@ -57,25 +68,23 @@ app.add_middleware(
57
  # 2. FUNÇÕES HELPER (LÓGICA REUTILIZÁVEL)
58
  # ==============================================================================
59
 
 
60
  def process_image_bytes(img_bytes: bytes):
61
- """Lógica de análise de imagem com OpenCV. Retorna um dict com métricas e a imagem de corrosão em numpy (RGB)."""
 
62
  nparr = np.frombuffer(img_bytes, np.uint8)
63
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
64
  if img is None:
65
  raise ValueError("Não foi possível decodificar a imagem.")
66
 
67
  hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
68
-
69
- # Segmentação do objeto principal (fundo escuro)
70
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
71
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
72
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
73
  mask_obj = cv2.bitwise_not(mask_bg)
74
-
75
  kernel = np.ones((5, 5), np.uint8)
76
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
77
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
78
-
79
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
80
  if not contours:
81
  raise ValueError("Nenhum objeto principal detectado na imagem.")
@@ -83,11 +92,8 @@ def process_image_bytes(img_bytes: bytes):
83
  largest = max(contours, key=cv2.contourArea)
84
  mask_clean = np.zeros_like(mask_obj)
85
  cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
86
-
87
  isolated = cv2.bitwise_and(img, img, mask=mask_clean)
88
  hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
89
-
90
- # Corrosão branca (faixa de brancos)
91
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
92
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
93
  mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
@@ -103,8 +109,7 @@ def process_image_bytes(img_bytes: bytes):
103
  def to_data_uri(img_arr_rgb):
104
  bgr = cv2.cvtColor(img_arr_rgb, cv2.COLOR_RGB2BGR)
105
  ok, buf = cv2.imencode(".png", bgr)
106
- if not ok:
107
- return None
108
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
109
  return f"data:image/png;base64,{b64}"
110
 
@@ -115,14 +120,17 @@ def process_image_bytes(img_bytes: bytes):
115
  "isolated_image": to_data_uri(isolated_rgb),
116
  "corrosion_image": to_data_uri(corrosion_vis_rgb),
117
  }
118
-
119
  return analysis_results, corrosion_vis_rgb
120
 
121
-
 
 
122
  def analyze_and_persist(img_bytes: bytes, logical_name: str):
123
  """
124
  Processa a imagem em memória (process_image_bytes), faz upload no S3 (original & resultado)
125
- e insere na tabela 'amostras'. Retorna o dict da análise + chaves S3 + database_id.
 
126
  """
127
  # 1) Processa
128
  analysis_results, corrosion_image_np = process_image_bytes(img_bytes)
@@ -152,7 +160,7 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
152
  ContentType='image/png'
153
  )
154
 
155
- # 4) Insert Supabase (na mesma tabela 'amostras')
156
  dados_para_inserir = {
157
  "imagem_original": s3_key_original,
158
  "imagem_resultado": s3_key_resultado,
@@ -170,15 +178,16 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
170
  }
171
  return out
172
 
173
- def segmentate(file_bytes: bytes):
 
174
  """
175
  Roda o YOLO, salva predição e crops, e retorna:
176
  - save_dir (Path): diretório raiz onde o Ultralytics salvou esta execução
177
  - crop_paths (list[Path]): lista com todos os arquivos de imagem dentro de /crops
178
  """
179
- modelo = YOLO('back-ciser/best.pt')
180
 
181
- # Passa a imagem como array (sem salvar a original só para inferir)
182
  img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
183
  np_img = np.array(img)
184
  np_img_bgr = cv2.cvtColor(np_img, cv2.COLOR_RGB2BGR)
@@ -191,7 +200,7 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
191
  save=True,
192
  save_crop=True,
193
  project=LOCAL_RESULT,
194
- name="predict", # deixa previsível
195
  exist_ok=True,
196
  verbose=False
197
  )
@@ -203,7 +212,7 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
203
  if not crops_dir.exists():
204
  return save_dir, []
205
 
206
- # Varre recursivamente (crops/<classe>/*.jpg|png|...)
207
  crop_paths = []
208
  for ext in ("*.jpg", "*.png", "*.jpeg", "*.bmp", "*.tif", "*.tiff"):
209
  crop_paths.extend(crops_dir.rglob(ext))
@@ -211,6 +220,9 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
211
  crop_paths = sorted(crop_paths, key=lambda p: str(p).lower())
212
  return save_dir, crop_paths
213
 
 
 
 
214
  # ==============================================================================
215
  # 3. ENDPOINTS DA API
216
  # ==============================================================================
@@ -219,11 +231,13 @@ def analyze_and_persist(img_bytes: bytes, logical_name: str):
219
  def read_root():
220
  return {"status": "ok", "message": "API de Análise de Corrosão no ar!"}
221
 
 
 
222
  @app.post("/analyze")
223
  async def analyze(file: UploadFile = File(...)):
224
  """
225
  Endpoint principal: Analisa a imagem, salva original e resultado no S3,
226
- e persiste os dados no Supabase.
227
  """
228
  content = await file.read()
229
  s3_key_original = None
@@ -246,6 +260,7 @@ async def analyze(file: UploadFile = File(...)):
246
  ContentType=content_type
247
  )
248
 
 
249
  # 3. Faz o upload da IMAGEM DE RESULTADO (em memória) para o S3
250
  s3_key_resultado = f"imagens_resultados/{uuid.uuid4()}.png"
251
 
@@ -265,10 +280,6 @@ async def analyze(file: UploadFile = File(...)):
265
  # 4. Salva os metadados e as chaves S3 no Supabase
266
  print("Inserindo registro no Supabase...")
267
  dados_para_inserir = {
268
- #"nome_amostra": file.filename,
269
- #"percentual_corrosao": analysis_results["percent"],
270
- #"pixels_totais_obj": analysis_results["total_pixels"],
271
- #"pixels_corrosao": analysis_results["corrosion_pixels"],
272
  "imagem_original": s3_key_original,
273
  "imagem_resultado": s3_key_resultado,
274
  "resultado": analysis_results["percent"]
@@ -282,7 +293,7 @@ async def analyze(file: UploadFile = File(...)):
282
  return JSONResponse(content=analysis_results)
283
 
284
  except Exception as e:
285
- # Lógica de Rollback: Se algo falhar, tenta limpar os arquivos do S3
286
  print(f"ERRO no processo de análise: {e}")
287
  if s3_key_original:
288
  print(f"Removendo objeto órfão do S3: {s3_key_original}")
@@ -290,10 +301,59 @@ async def analyze(file: UploadFile = File(...)):
290
  if s3_key_resultado:
291
  print(f"Removendo objeto órfão do S3: {s3_key_resultado}")
292
  s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_resultado)
293
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno: {e}")
295
 
296
 
 
297
  @app.get("/samples/{sample_id}")
298
  async def get_sample_images(sample_id: int):
299
  """
 
7
  import cv2
8
  import base64
9
  import mimetypes
10
+ # --- NOVAS importações do 'teste yolo.txt' ---
11
+ import io
12
+ from pathlib import Path
13
+ from PIL import Image
14
+ from ultralytics import YOLO
15
+ # --- Fim das novas importações ---
16
  from fastapi import FastAPI, File, UploadFile, HTTPException
17
  from fastapi.middleware.cors import CORSMiddleware
18
  from fastapi.responses import JSONResponse
 
22
  # 1. CONFIGURAÇÃO E CLIENTES (CARREGADOS NA INICIALIZAÇÃO)
23
  # ==============================================================================
24
 
25
+ # --- NOVAS constantes do 'teste yolo.txt' ---
26
+ LOCAL_RESULT = 'back-ciser/result'
27
+ YOLO_MODEL_PATH = 'back-ciser/best.pt' # Caminho para o seu modelo treinado
28
+ # --- Fim das novas constantes ---
29
+
30
  # Carrega as configurações das variáveis de ambiente (Secrets do Hugging Face)
31
  AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
32
  AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
 
35
  SUPABASE_URL = os.getenv("SUPABASE_URL")
36
  SUPABASE_KEY = os.getenv("SUPABASE_KEY")
37
 
38
+ # Validação (do back atual.txt)
39
  if not all([AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET_NAME, AWS_S3_REGION, SUPABASE_URL, SUPABASE_KEY]):
40
  raise RuntimeError("Erro: Nem todos os secrets necessários foram configurados no Hugging Face Space.")
41
 
 
55
  # Inicializa a aplicação FastAPI
56
  app = FastAPI(title="Detector de Corrosão Branca com Salvamento S3/Supabase")
57
 
58
+ # Middleware (do back atual.txt)
59
  app.add_middleware(
60
  CORSMiddleware,
61
  allow_origins=["*"],
 
68
  # 2. FUNÇÕES HELPER (LÓGICA REUTILIZÁVEL)
69
  # ==============================================================================
70
 
71
+ # --- Função ORIGINAL do 'back atual.txt' (usada por ambas as lógicas) ---
72
  def process_image_bytes(img_bytes: bytes):
73
+ """Lógica de análise de imagem com OpenCV.
74
+ Retorna um dict com métricas e a imagem de corrosão em numpy."""
75
  nparr = np.frombuffer(img_bytes, np.uint8)
76
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
77
  if img is None:
78
  raise ValueError("Não foi possível decodificar a imagem.")
79
 
80
  hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
 
 
81
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
82
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
83
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
84
  mask_obj = cv2.bitwise_not(mask_bg)
 
85
  kernel = np.ones((5, 5), np.uint8)
86
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
87
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
 
88
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
89
  if not contours:
90
  raise ValueError("Nenhum objeto principal detectado na imagem.")
 
92
  largest = max(contours, key=cv2.contourArea)
93
  mask_clean = np.zeros_like(mask_obj)
94
  cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
 
95
  isolated = cv2.bitwise_and(img, img, mask=mask_clean)
96
  hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
 
 
97
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
98
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
99
  mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
 
109
  def to_data_uri(img_arr_rgb):
110
  bgr = cv2.cvtColor(img_arr_rgb, cv2.COLOR_RGB2BGR)
111
  ok, buf = cv2.imencode(".png", bgr)
112
+ if not ok: return None
 
113
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
114
  return f"data:image/png;base64,{b64}"
115
 
 
120
  "isolated_image": to_data_uri(isolated_rgb),
121
  "corrosion_image": to_data_uri(corrosion_vis_rgb),
122
  }
123
+
124
  return analysis_results, corrosion_vis_rgb
125
 
126
+
127
+ # --- NOVAS funções helper do 'teste yolo.txt' (para o fluxo de segmentação) ---
128
+
129
  def analyze_and_persist(img_bytes: bytes, logical_name: str):
130
  """
131
  Processa a imagem em memória (process_image_bytes), faz upload no S3 (original & resultado)
132
+ e insere na tabela 'amostras'.
133
+ Retorna o dict da análise + chaves S3 + database_id.
134
  """
135
  # 1) Processa
136
  analysis_results, corrosion_image_np = process_image_bytes(img_bytes)
 
160
  ContentType='image/png'
161
  )
162
 
163
+ # 4) Insert Supabase (na mesma tabela 'amostra')
164
  dados_para_inserir = {
165
  "imagem_original": s3_key_original,
166
  "imagem_resultado": s3_key_resultado,
 
178
  }
179
  return out
180
 
181
+
182
+ def segmentate(file_bytes: bytes):
183
  """
184
  Roda o YOLO, salva predição e crops, e retorna:
185
  - save_dir (Path): diretório raiz onde o Ultralytics salvou esta execução
186
  - crop_paths (list[Path]): lista com todos os arquivos de imagem dentro de /crops
187
  """
188
+ modelo = YOLO(YOLO_MODEL_PATH) # Usando a constante definida no início
189
 
190
+ # Passa a imagem como array
191
  img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
192
  np_img = np.array(img)
193
  np_img_bgr = cv2.cvtColor(np_img, cv2.COLOR_RGB2BGR)
 
200
  save=True,
201
  save_crop=True,
202
  project=LOCAL_RESULT,
203
+ name="predict",
204
  exist_ok=True,
205
  verbose=False
206
  )
 
212
  if not crops_dir.exists():
213
  return save_dir, []
214
 
215
+ # Varre recursivamente
216
  crop_paths = []
217
  for ext in ("*.jpg", "*.png", "*.jpeg", "*.bmp", "*.tif", "*.tiff"):
218
  crop_paths.extend(crops_dir.rglob(ext))
 
220
  crop_paths = sorted(crop_paths, key=lambda p: str(p).lower())
221
  return save_dir, crop_paths
222
 
223
+ # --- Fim das novas funções helper ---
224
+
225
+
226
  # ==============================================================================
227
  # 3. ENDPOINTS DA API
228
  # ==============================================================================
 
231
  def read_root():
232
  return {"status": "ok", "message": "API de Análise de Corrosão no ar!"}
233
 
234
+
235
+ # --- Endpoint ORIGINAL do 'back atual.txt' (Sem alterações) ---
236
  @app.post("/analyze")
237
  async def analyze(file: UploadFile = File(...)):
238
  """
239
  Endpoint principal: Analisa a imagem, salva original e resultado no S3,
240
+ e persiste os dados no Supabase. (LÓGICA ANTIGA)
241
  """
242
  content = await file.read()
243
  s3_key_original = None
 
260
  ContentType=content_type
261
  )
262
 
263
+
264
  # 3. Faz o upload da IMAGEM DE RESULTADO (em memória) para o S3
265
  s3_key_resultado = f"imagens_resultados/{uuid.uuid4()}.png"
266
 
 
280
  # 4. Salva os metadados e as chaves S3 no Supabase
281
  print("Inserindo registro no Supabase...")
282
  dados_para_inserir = {
 
 
 
 
283
  "imagem_original": s3_key_original,
284
  "imagem_resultado": s3_key_resultado,
285
  "resultado": analysis_results["percent"]
 
293
  return JSONResponse(content=analysis_results)
294
 
295
  except Exception as e:
296
+ # Lógica de Rollback:
297
  print(f"ERRO no processo de análise: {e}")
298
  if s3_key_original:
299
  print(f"Removendo objeto órfão do S3: {s3_key_original}")
 
301
  if s3_key_resultado:
302
  print(f"Removendo objeto órfão do S3: {s3_key_resultado}")
303
  s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_resultado)
304
+
305
+ raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno: {e}")
306
+
307
+
308
+ # --- NOVO Endpoint (lógica do 'teste yolo.txt') ---
309
+ @app.post("/segment_and_analyze")
310
+ async def segment_and_analyze(file: UploadFile = File(...)):
311
+ """
312
+ Endpoint com YOLO: Segmenta a imagem, analisa a original E cada crop,
313
+ e salva tudo no S3/Supabase.
314
+ (Lógica dos sources 36-39)
315
+ """
316
+ content = await file.read()
317
+
318
+ # Para rollback em caso de erro
319
+ created_s3_keys = []
320
+
321
+ try:
322
+ # 0) Segmenta e obtém caminhos dos crops
323
+ save_dir, crop_paths = segmentate(content)
324
+
325
+ # 1) Analisa a imagem ORIGINAL
326
+ original_res = analyze_and_persist(content, logical_name=file.filename or "upload_sem_nome")
327
+ created_s3_keys.extend([original_res["s3_original"], original_res["s3_resultado"]])
328
+
329
+ # 2) Analisa cada CROP
330
+ crop_analyses = []
331
+ for crop_path in crop_paths:
332
+ crop_bytes = crop_path.read_bytes()
333
+ logical_name = str(crop_path.relative_to(save_dir)).replace("\\", "/") # nome amigável
334
+ crop_res = analyze_and_persist(crop_bytes, logical_name=logical_name)
335
+ created_s3_keys.extend([crop_res["s3_original"], crop_res["s3_resultado"]])
336
+ crop_res["crop_path"] = logical_name
337
+ crop_analyses.append(crop_res)
338
+
339
+ # 3) Resposta consolidada
340
+ return JSONResponse(content={
341
+ "original": original_res,
342
+ "crops_count": len(crop_analyses),
343
+ "crops": crop_analyses
344
+ })
345
+
346
+ except Exception as e:
347
+ # Rollback simples: tenta apagar do S3 o que já foi criado
348
+ for key in created_s3_keys:
349
+ try:
350
+ s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=key)
351
+ except Exception:
352
+ pass
353
  raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno: {e}")
354
 
355
 
356
+ # --- Endpoint ORIGINAL do 'back atual.txt' (Sem alterações) ---
357
  @app.get("/samples/{sample_id}")
358
  async def get_sample_images(sample_id: int):
359
  """