Spaces:
Sleeping
Sleeping
| # 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 | |
| # ============================================================================== | |
| def read_root(): | |
| return {"status": "ok", "message": "API de Análise de Corrosão no ar!"} | |
| 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}") | |
| 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}") |