Spaces:
Sleeping
Sleeping
File size: 12,709 Bytes
c792c63 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 fddf7b4 beb0975 fddf7b4 beb0975 fddf7b4 6801165 fddf7b4 beb0975 c792c63 67a6844 fddf7b4 76396cf 688d726 76396cf c792c63 67a6844 c792c63 67a6844 c792c63 67a6844 c792c63 beb0975 6801165 c792c63 67a6844 c792c63 67a6844 c792c63 beb0975 c792c63 beb0975 76396cf 67a6844 76396cf beb0975 c792c63 beb0975 fddf7b4 67a6844 beb0975 67a6844 beb0975 fddf7b4 beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 8d2818d beb0975 8d2818d beb0975 8d2818d 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee b70cb7b beb0975 5d6f9ee beb0975 8d2818d 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 5d6f9ee beb0975 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 | # app.py
import os
import uuid
import boto3
import supabase
import numpy as np
import cv2
import base64
import mimetypes
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from botocore.exceptions import NoCredentialsError
# ==============================================================================
# 1. CONFIGURAÇÃO E CLIENTES (CARREGADOS NA INICIALIZAÇÃO)
# ==============================================================================
# Carrega as configurações das variáveis de ambiente (Secrets do Hugging Face)
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_S3_BUCKET_NAME = os.getenv("AWS_S3_BUCKET_NAME")
AWS_S3_REGION = os.getenv("AWS_S3_REGION")
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
# Validação para garantir que todos os secrets foram configurados
if not all([AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET_NAME, AWS_S3_REGION, SUPABASE_URL, SUPABASE_KEY]):
raise RuntimeError("Erro: Nem todos os secrets necessários foram configurados no Hugging Face Space.")
# Inicializa os clientes globais
try:
s3_client = boto3.client(
's3',
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
region_name=AWS_S3_REGION
)
supabase_client = supabase.create_client(SUPABASE_URL, SUPABASE_KEY)
print("Clientes S3 e Supabase inicializados com sucesso.")
except Exception as e:
raise RuntimeError(f"Erro ao inicializar os clientes: {e}")
# Inicializa a aplicação FastAPI
app = FastAPI(title="Detector de Corrosão Branca com Salvamento S3/Supabase")
# Em produção, restrinja ao domínio do seu frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==============================================================================
# 2. FUNÇÕES HELPER (LÓGICA REUTILIZÁVEL)
# ==============================================================================
def process_image_bytes(img_bytes: bytes):
"""Lógica de análise de imagem com OpenCV. Retorna um dict com métricas e a imagem de corrosão em numpy (RGB)."""
nparr = np.frombuffer(img_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if img is None:
raise ValueError("Não foi possível decodificar a imagem.")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Segmentação do objeto principal (fundo escuro)
lower_bg = np.array([0, 0, 0], dtype=np.uint8)
upper_bg = np.array([180, 255, 50], dtype=np.uint8)
mask_bg = cv2.inRange(hsv, lower_bg, upper_bg)
mask_obj = cv2.bitwise_not(mask_bg)
kernel = np.ones((5, 5), np.uint8)
mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_OPEN, kernel)
mask_obj = cv2.morphologyEx(mask_obj, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(mask_obj, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
raise ValueError("Nenhum objeto principal detectado na imagem.")
largest = max(contours, key=cv2.contourArea)
mask_clean = np.zeros_like(mask_obj)
cv2.drawContours(mask_clean, [largest], -1, 255, cv2.FILLED)
isolated = cv2.bitwise_and(img, img, mask=mask_clean)
hsv_iso = cv2.cvtColor(isolated, cv2.COLOR_BGR2HSV)
# Corrosão branca (faixa de brancos)
lower_white = np.array([0, 0, 180], dtype=np.uint8)
upper_white = np.array([180, 60, 255], dtype=np.uint8)
mask_white = cv2.inRange(hsv_iso, lower_white, upper_white)
mask_white = cv2.bitwise_and(mask_white, mask_white, mask=mask_clean)
total_pixels = int(np.count_nonzero(mask_clean))
corrosion_pixels = int(np.count_nonzero(mask_white))
percent = (corrosion_pixels / max(1, total_pixels)) * 100.0
isolated_rgb = cv2.cvtColor(isolated, cv2.COLOR_BGR2RGB)
corrosion_vis_rgb = cv2.bitwise_and(isolated_rgb, isolated_rgb, mask=mask_white)
def to_data_uri(img_arr_rgb):
bgr = cv2.cvtColor(img_arr_rgb, cv2.COLOR_RGB2BGR)
ok, buf = cv2.imencode(".png", bgr)
if not ok:
return None
b64 = base64.b64encode(buf.tobytes()).decode("ascii")
return f"data:image/png;base64,{b64}"
analysis_results = {
"percent": round(percent, 4),
"total_pixels": total_pixels,
"corrosion_pixels": corrosion_pixels,
"isolated_image": to_data_uri(isolated_rgb),
"corrosion_image": to_data_uri(corrosion_vis_rgb),
}
return analysis_results, corrosion_vis_rgb
def analyze_and_persist(img_bytes: bytes, logical_name: str):
"""
Processa a imagem em memória (process_image_bytes), faz upload no S3 (original & resultado)
e insere na tabela 'amostras'. Retorna o dict da análise + chaves S3 + database_id.
"""
# 1) Processa
analysis_results, corrosion_image_np = process_image_bytes(img_bytes)
# 2) Upload ORIGINAL
content_type = mimetypes.guess_type(logical_name)[0] or "application/octet-stream"
extensao = mimetypes.guess_extension(content_type) or ".jpg"
s3_key_original = f"imagens_originais/{uuid.uuid4()}{extensao}"
s3_client.put_object(
Bucket=AWS_S3_BUCKET_NAME,
Key=s3_key_original,
Body=img_bytes,
ContentType=content_type
)
# 3) Upload RESULTADO (PNG)
bgr_result = cv2.cvtColor(corrosion_image_np, cv2.COLOR_RGB2BGR)
ok, buffer = cv2.imencode('.png', bgr_result)
if not ok:
raise ValueError("Falha ao codificar a imagem de resultado para PNG.")
s3_key_resultado = f"imagens_resultados/{uuid.uuid4()}.png"
s3_client.put_object(
Bucket=AWS_S3_BUCKET_NAME,
Key=s3_key_resultado,
Body=buffer.tobytes(),
ContentType='image/png'
)
# 4) Insert Supabase (na mesma tabela 'amostras')
dados_para_inserir = {
"imagem_original": s3_key_original,
"imagem_resultado": s3_key_resultado,
"resultado": analysis_results["percent"]
}
response = supabase_client.from_("amostra").insert(dados_para_inserir).execute()
new_record_id = response.data[0]['id']
# Retorno enriquecido
out = {
**analysis_results,
"database_id": new_record_id,
"s3_original": s3_key_original,
"s3_resultado": s3_key_resultado
}
return out
def segmentate(file_bytes: bytes):
"""
Roda o YOLO, salva predição e crops, e retorna:
- save_dir (Path): diretório raiz onde o Ultralytics salvou esta execução
- crop_paths (list[Path]): lista com todos os arquivos de imagem dentro de /crops
"""
modelo = YOLO('back-ciser/best.pt')
# Passa a imagem como array (sem salvar a original só para inferir)
img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
np_img = np.array(img)
np_img_bgr = cv2.cvtColor(np_img, cv2.COLOR_RGB2BGR)
resultados = modelo.predict(
source=np_img,
imgsz=1280,
conf=0.7,
iou=0.3,
save=True,
save_crop=True,
project=LOCAL_RESULT,
name="predict", # deixa previsível
exist_ok=True,
verbose=False
)
# Diretório real onde esta execução foi salva
save_dir = Path(resultados[0].save_dir)
crops_dir = save_dir / "crops"
if not crops_dir.exists():
return save_dir, []
# Varre recursivamente (crops/<classe>/*.jpg|png|...)
crop_paths = []
for ext in ("*.jpg", "*.png", "*.jpeg", "*.bmp", "*.tif", "*.tiff"):
crop_paths.extend(crops_dir.rglob(ext))
crop_paths = sorted(crop_paths, key=lambda p: str(p).lower())
return save_dir, crop_paths
# ==============================================================================
# 3. ENDPOINTS DA API
# ==============================================================================
@app.get("/")
def read_root():
return {"status": "ok", "message": "API de Análise de Corrosão no ar!"}
@app.post("/analyze")
async def analyze(file: UploadFile = File(...)):
"""
Endpoint principal: Analisa a imagem, salva original e resultado no S3,
e persiste os dados no Supabase.
"""
content = await file.read()
s3_key_original = None
s3_key_resultado = None
try:
# 1. Processa a imagem para obter métricas e a imagem de resultado
analysis_results, corrosion_image_np = process_image_bytes(content)
# 2. Faz o upload da IMAGEM ORIGINAL para o S3
content_type = file.content_type or 'application/octet-stream'
extensao = mimetypes.guess_extension(content_type) or '.jpg'
s3_key_original = f"imagens_originais/{uuid.uuid4()}{extensao}"
print(f"Fazendo upload da imagem original para: {s3_key_original}")
s3_client.put_object(
Bucket=AWS_S3_BUCKET_NAME,
Key=s3_key_original,
Body=content,
ContentType=content_type
)
# 3. Faz o upload da IMAGEM DE RESULTADO (em memória) para o S3
s3_key_resultado = f"imagens_resultados/{uuid.uuid4()}.png"
print(f"Fazendo upload da imagem de resultado para: {s3_key_resultado}")
bgr_result = cv2.cvtColor(corrosion_image_np, cv2.COLOR_RGB2BGR)
ok, buffer = cv2.imencode('.png', bgr_result)
if not ok:
raise ValueError("Falha ao codificar a imagem de resultado para PNG.")
s3_client.put_object(
Bucket=AWS_S3_BUCKET_NAME,
Key=s3_key_resultado,
Body=buffer.tobytes(),
ContentType='image/png'
)
# 4. Salva os metadados e as chaves S3 no Supabase
print("Inserindo registro no Supabase...")
dados_para_inserir = {
#"nome_amostra": file.filename,
#"percentual_corrosao": analysis_results["percent"],
#"pixels_totais_obj": analysis_results["total_pixels"],
#"pixels_corrosao": analysis_results["corrosion_pixels"],
"imagem_original": s3_key_original,
"imagem_resultado": s3_key_resultado,
"resultado": analysis_results["percent"]
}
response = supabase_client.from_("amostra").insert(dados_para_inserir).execute()
# Adiciona o ID do novo registro à resposta
new_record_id = response.data[0]['id']
analysis_results['database_id'] = new_record_id
return JSONResponse(content=analysis_results)
except Exception as e:
# Lógica de Rollback: Se algo falhar, tenta limpar os arquivos do S3
print(f"ERRO no processo de análise: {e}")
if s3_key_original:
print(f"Removendo objeto órfão do S3: {s3_key_original}")
s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_original)
if s3_key_resultado:
print(f"Removendo objeto órfão do S3: {s3_key_resultado}")
s3_client.delete_object(Bucket=AWS_S3_BUCKET_NAME, Key=s3_key_resultado)
raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno: {e}")
@app.get("/samples/{sample_id}")
async def get_sample_images(sample_id: int):
"""
Endpoint de recuperação: Busca uma amostra pelo ID e retorna
links temporários (pré-assinados) para suas imagens no S3.
"""
try:
print(f"Buscando amostra com ID: {sample_id}...")
response = supabase_client.from_("amostra").select("imagem_original, imagem_resultado").eq("id", sample_id).single().execute()
if not response.data:
raise HTTPException(status_code=404, detail=f"Amostra com ID {sample_id} não encontrada.")
amostra = response.data
s3_key_original = amostra.get("imagem_original")
s3_key_resultado = amostra.get("imagem_resultado")
links = {}
if s3_key_original:
links['url_original'] = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_original},
ExpiresIn=3600 # 1 hora
)
if s3_key_resultado:
links['url_resultado'] = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': AWS_S3_BUCKET_NAME, 'Key': s3_key_resultado},
ExpiresIn=3600 # 1 hora
)
return JSONResponse(content=links)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno ao buscar os links: {e}") |