joao-dutra commited on
Commit
beb0975
·
verified ·
1 Parent(s): dbf2455

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +146 -124
app.py CHANGED
@@ -1,37 +1,34 @@
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
  import os
10
  import uuid
11
  import boto3
12
- from supabase import create_client, Client
13
-
14
- # Variaveis de ambiente
 
 
 
 
 
 
15
 
16
- # --- AWS S3 ---
17
- AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
18
- AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
19
- AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
20
- AWS_S3_REGION = os.environ.get("AWS_S3_REGION")
21
 
22
- # --- Supabase ---
23
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
24
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
 
 
 
 
25
 
26
- # Supabase
27
- try:
28
- supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
29
- print("Cliente Supabase inicializado com sucesso.")
30
- except Exception as e:
31
- print(f"ERRO ao inicializar Supabase: {e}")
32
- raise
33
 
34
- # S3 (boto3)
35
  try:
36
  s3_client = boto3.client(
37
  's3',
@@ -39,14 +36,15 @@ try:
39
  aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
40
  region_name=AWS_S3_REGION
41
  )
42
- print("Cliente S3 (boto3) inicializado com sucesso.")
 
43
  except Exception as e:
44
- print(f"ERRO ao inicializar S3: {e}")
45
- raise
46
 
47
- app = FastAPI(title="Detector de Corrosão Branca")
 
48
 
49
- # PARA PROTOTIPO: permitir todas origens. Em produção restrinja ao domínio do frontend.
50
  app.add_middleware(
51
  CORSMiddleware,
52
  allow_origins=["*"],
@@ -55,155 +53,179 @@ app.add_middleware(
55
  allow_headers=["*"],
56
  )
57
 
 
 
 
 
58
  def process_image_bytes(img_bytes: bytes):
59
- # bytes em numpy + OpenCV
60
  nparr = np.frombuffer(img_bytes, np.uint8)
61
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
62
  if img is None:
63
  raise ValueError("Não foi possível decodificar a imagem.")
64
 
65
- # 1) Converter para HSV
66
  hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
67
-
68
- # 2) máscara do fundo preto (V baixo)
69
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
70
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
71
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
72
-
73
- # 3) objeto = invertendo máscara do fundo
74
  mask_obj = cv2.bitwise_not(mask_bg)
75
-
76
- # 4) limpar máscara (morfologia)
77
  kernel = np.ones((5, 5), np.uint8)
78
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
79
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
80
-
81
- # 5) maior contorno (supõe um parafuso)
82
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
83
  if not contours:
84
- return {"error": "Nenhum objeto detectado"}
85
 
86
  largest = max(contours, key=cv2.contourArea)
87
  mask_clean = np.zeros_like(mask_obj)
88
  cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
89
-
90
- # 6) isolar objeto
91
  isolated = cv2.bitwise_and(img, img, mask=mask_clean)
92
-
93
- # 7) detectar corrosão BRANCA (S baixa, V alta)
94
  hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
95
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
96
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
97
  mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
98
  mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
99
 
100
- # 8) métricas
101
  total_pixels = int(np.count_nonzero(mask_clean))
102
  corrosion_pixels = int(np.count_nonzero(mask_white))
103
  percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
104
 
105
- # 9) preparar imagens para frontend (PNG base64)
106
- # isolado em RGB para visualização
107
  isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
108
- corrosion_vis = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
109
 
110
- def to_data_uri(img_arr):
111
- # img_arr: RGB uint8
112
- bgr = cv2.cvtColor(img_arr, cv2.COLOR_RGB2BGR)
113
  ok, buf = cv2.imencode(".png", bgr)
114
- if not ok:
115
- return None
116
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
117
  return f"data:image/png;base64,{b64}"
118
 
119
- isolated_b64 = to_data_uri(isolated_rgb)
120
- corrosion_b64 = to_data_uri(corrosion_vis)
121
-
122
- return {
123
  "percent": round(percent, 4),
124
  "total_pixels": total_pixels,
125
  "corrosion_pixels": corrosion_pixels,
126
- "isolated_image": isolated_b64,
127
- "corrosion_image": corrosion_b64,
128
  }
 
 
 
 
 
 
 
 
 
 
129
 
130
- def image_link_gen(amostra_id: int) -> dict | None:
 
131
  """
132
- Busca uma amostra pelo ID no Supabase e gera URLs pré-assinadas para suas imagens.
133
- Retorna um dicionário com os links ou None se a amostra não for encontrada.
134
  """
135
- print(f"\nBuscando amostra com ID: {amostra_id} no Supabase...")
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- # 1. Busca os dados da amostra no banco
138
- response = supabase.from_("amostra").select("imagem_original, imagem_resultado").eq("id", amostra_id).single().execute()
 
 
 
 
 
 
 
 
139
 
140
- amostra = response.data
141
- if not amostra:
142
- print(f"Amostra com ID {amostra_id} não encontrada.")
143
- return None
 
144
 
145
- print("Amostra encontrada. Gerando links temporários...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- # Pega as chaves S3 do resultado
148
- s3_key_original = amostra.get("imagem_original")
149
- s3_key_resultado = amostra.get("imagem_resultado")
150
 
151
- links = {}
 
 
 
 
 
 
 
 
 
 
152
 
153
- # 2. Gera URL pré-assinada para a imagem original
154
- if s3_key_original:
155
- url_original = s3_client.generate_presigned_url(
156
- 'get_object',
157
- Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_original},
158
- ExpiresIn=3600 # Link válido por 1 hora (3600 segundos)
159
- )
160
- links['original'] = url_original
161
-
162
- # 3. Gera URL pré-assinada para a imagem de resultado (se existir)
163
- if s3_key_resultado:
164
- url_resultado = s3_client.generate_presigned_url(
165
- 'get_object',
166
- Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_resultado},
167
- ExpiresIn=3600
168
- )
169
- links['resultado'] = url_resultado
170
-
171
- return links
172
 
173
- @app.post("/analyze")
174
- async def analyze(file: UploadFile = File(...)):
175
- content = await file.read()
176
- try:
177
- result = process_image_bytes(content)
178
- except ValueError as e:
179
- raise HTTPException(status_code=400, detail=str(e))
180
- return JSONResponse(result)
181
 
182
- @app.get("/links/{amostra_id}")
183
- async def get_link(amostra_id: int):
 
 
 
 
184
  try:
 
 
185
 
186
- links = image_link_gen(amostra_id)
 
 
 
 
 
 
 
187
 
188
- if links is None:
189
- raise HTTPException(
190
- status_code=404,
191
- detail=f"Amostra com ID {amostra_id} não encontrada."
 
192
  )
193
 
194
- return links
 
 
 
 
 
 
 
195
 
196
  except Exception as e:
197
-
198
- print(f"Erro inesperado ao processar ID {amostra_id}: {e}")
199
-
200
- raise HTTPException(
201
- status_code=500,
202
- detail=f"Erro interno no servidor ao tentar buscar os links. {e}"
203
- )
204
-
205
-
206
- @app.get("/")
207
- def read_root():
208
- return {"status": "ok"}
209
-
 
1
  # app.py
 
 
 
 
 
 
 
2
  import os
3
  import uuid
4
  import boto3
5
+ import supabase
6
+ 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
13
+ from botocore.exceptions import NoCredentialsError
14
 
15
+ # ==============================================================================
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")
22
+ AWS_S3_BUCKET_NAME = os.getenv("AWS_S3_BUCKET_NAME")
23
+ 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
 
31
+ # Inicializa os clientes globais
32
  try:
33
  s3_client = boto3.client(
34
  's3',
 
36
  aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
37
  region_name=AWS_S3_REGION
38
  )
39
+ supabase_client = supabase.create_client(SUPABASE_URL, SUPABASE_KEY)
40
+ print("Clientes S3 e Supabase inicializados com sucesso.")
41
  except Exception as e:
42
+ raise RuntimeError(f"Erro ao inicializar os clientes: {e}")
 
43
 
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=["*"],
 
53
  allow_headers=["*"],
54
  )
55
 
56
+ # ==============================================================================
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."""
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
  lower_bg = np.array([0, 0, 0], dtype=np.uint8)
69
  upper_bg = np.array([180, 255, 50], dtype=np.uint8)
70
  mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
 
 
71
  mask_obj = cv2.bitwise_not(mask_bg)
 
 
72
  kernel = np.ones((5, 5), np.uint8)
73
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
74
  mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
 
 
75
  contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
76
  if not contours:
77
+ raise ValueError("Nenhum objeto principal detectado na imagem.")
78
 
79
  largest = max(contours, key=cv2.contourArea)
80
  mask_clean = np.zeros_like(mask_obj)
81
  cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
 
 
82
  isolated = cv2.bitwise_and(img, img, mask=mask_clean)
 
 
83
  hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
84
  lower_white = np.array([0, 0, 180], dtype=np.uint8)
85
  upper_white = np.array([180, 60, 255], dtype=np.uint8)
86
  mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
87
  mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
88
 
 
89
  total_pixels = int(np.count_nonzero(mask_clean))
90
  corrosion_pixels = int(np.count_nonzero(mask_white))
91
  percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
92
 
 
 
93
  isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
94
+ corrosion_vis_rgb = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
95
 
96
+ def to_data_uri(img_arr_rgb):
97
+ bgr = cv2.cvtColor(img_arr_rgb, cv2.COLOR_RGB2BGR)
 
98
  ok, buf = cv2.imencode(".png", bgr)
99
+ if not ok: return None
 
100
  b64 = base64.b64encode(buf.tobytes()).decode("ascii")
101
  return f"data:image/png;base64,{b64}"
102
 
103
+ analysis_results = {
 
 
 
104
  "percent": round(percent, 4),
105
  "total_pixels": total_pixels,
106
  "corrosion_pixels": corrosion_pixels,
107
+ "isolated_image": to_data_uri(isolated_rgb),
108
+ "corrosion_image": to_data_uri(corrosion_vis_rgb),
109
  }
110
+
111
+ return analysis_results, corrosion_vis_rgb
112
+
113
+ # ==============================================================================
114
+ # 3. ENDPOINTS DA API
115
+ # ==============================================================================
116
+
117
+ @app.get("/")
118
+ def read_root():
119
+ return {"status": "ok", "message": "API de Análise de Corrosão no ar!"}
120
 
121
+ @app.post("/analyze")
122
+ async def analyze(file: UploadFile = File(...)):
123
  """
124
+ Endpoint principal: Analisa a imagem, salva original e resultado no S3,
125
+ e persiste os dados no Supabase.
126
  """
127
+ content = await file.read()
128
+ s3_key_original = None
129
+ s3_key_resultado = None
130
+
131
+ try:
132
+ # 1. Processa a imagem para obter métricas e a imagem de resultado
133
+ analysis_results, corrosion_image_np = process_image_bytes(content)
134
+
135
+ # 2. Faz o upload da IMAGEM ORIGINAL para o S3
136
+ content_type = file.content_type or 'application/octet-stream'
137
+ extensao = mimetypes.guess_extension(content_type) or '.jpg'
138
+ s3_key_original = f"imagens_originais/{uuid.uuid4()}{extensao}"
139
 
140
+ print(f"Fazendo upload da imagem original para: {s3_key_original}")
141
+ s3_client.put_object(
142
+ Bucket=AWS_S3_BUCKET_NAME,
143
+ Key=s3_key_original,
144
+ Body=content,
145
+ ContentType=content_type
146
+ )
147
+
148
+ # 3. Faz o upload da IMAGEM DE RESULTADO (em memória) para o S3
149
+ s3_key_resultado = f"imagens_resultados/{uuid.uuid4()}.png"
150
 
151
+ print(f"Fazendo upload da imagem de resultado para: {s3_key_resultado}")
152
+ bgr_result = cv2.cvtColor(corrosion_image_np, cv2.COLOR_RGB2BGR)
153
+ ok, buffer = cv2.imencode('.png', bgr_result)
154
+ if not ok:
155
+ raise ValueError("Falha ao codificar a imagem de resultado para PNG.")
156
 
157
+ s3_client.put_object(
158
+ Bucket=AWS_S3_BUCKET_NAME,
159
+ Key=s3_key_resultado,
160
+ Body=buffer.tobytes(),
161
+ ContentType='image/png'
162
+ )
163
+
164
+ # 4. Salva os metadados e as chaves S3 no Supabase
165
+ print("Inserindo registro no Supabase...")
166
+ dados_para_inserir = {
167
+ "nome_amostra": file.filename,
168
+ "percentual_corrosao": analysis_results["percent"],
169
+ "pixels_totais_obj": analysis_results["total_pixels"],
170
+ "pixels_corrosao": analysis_results["corrosion_pixels"],
171
+ "imagem_original": s3_key_original,
172
+ "imagem_resultado": s3_key_resultado
173
+ }
174
+ response = supabase_client.from_("amostras").insert(dados_para_inserir).execute()
175
 
176
+ # Adiciona o ID do novo registro à resposta
177
+ new_record_id = response.data[0]['id']
178
+ analysis_results['database_id'] = new_record_id
179
 
180
+ return JSONResponse(content=analysis_results)
181
+
182
+ except Exception as e:
183
+ # Lógica de Rollback: Se algo falhar, tenta limpar os arquivos do S3
184
+ print(f"ERRO no processo de análise: {e}")
185
+ if s3_key_original:
186
+ print(f"Removendo objeto órfão do S3: {s3_key_original}")
187
+ s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_original)
188
+ if s3_key_resultado:
189
+ print(f"Removendo objeto órfão do S3: {s3_key_resultado}")
190
+ s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_resultado)
191
 
192
+ raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
 
 
 
 
 
 
 
 
194
 
195
+ @app.get("/samples/{sample_id}")
196
+ async def get_sample_images(sample_id: int):
197
+ """
198
+ Endpoint de recuperação: Busca uma amostra pelo ID e retorna
199
+ links temporários (pré-assinados) para suas imagens no S3.
200
+ """
201
  try:
202
+ print(f"Buscando amostra com ID: {sample_id}...")
203
+ response = supabase_client.from_("amostras").select("imagem_original, imagem_resultado").eq("id", sample_id).single().execute()
204
 
205
+ if not response.data:
206
+ raise HTTPException(status_code=404, detail=f"Amostra com ID {sample_id} não encontrada.")
207
+
208
+ amostra = response.data
209
+ s3_key_original = amostra.get("imagem_original")
210
+ s3_key_resultado = amostra.get("imagem_resultado")
211
+
212
+ links = {}
213
 
214
+ if s3_key_original:
215
+ links['url_original'] = s3_client.generate_presigned_url(
216
+ 'get_object',
217
+ Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_original},
218
+ ExpiresIn=3600 # 1 hora
219
  )
220
 
221
+ if s3_key_resultado:
222
+ links['url_resultado'] = s3_client.generate_presigned_url(
223
+ 'get_object',
224
+ Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_resultado},
225
+ ExpiresIn=3600 # 1 hora
226
+ )
227
+
228
+ return JSONResponse(content=links)
229
 
230
  except Exception as e:
231
+ raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno ao buscar os links: {e}")