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}")