comvis / main.py
Jennigwen's picture
deploy BE ke huggingface
5c22e5c
Raw
History Blame Contribute Delete
5.11 kB
"""
main.py β€” DermaAI FastAPI backend.
Endpoints:
GET /api/health β€” health check + model status
POST /api/analyze β€” upload image β†’ 120D features β†’ ONNX β†’ JSON result
"""
import io
import logging
import time
from contextlib import asynccontextmanager
import cv2
import numpy as np
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image, UnidentifiedImageError
from features import extract_features
from inference import load_model, predict
logger = logging.getLogger(__name__)
# ── Lifespan: pre-load model on startup ───────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
load_model()
print("βœ… Random Forest ONNX model loaded successfully.")
except FileNotFoundError as e:
print(f"⚠️ {e}")
yield
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(
title="DermaAI API",
description="Skin disease detection using Random Forest ONNX + 120D CV features",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://comvisproject.vercel.app/"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Helpers ───────────────────────────────────────────────────────────────────
ALLOWED_IMAGE_FORMATS = {"JPEG", "PNG", "BMP", "WEBP"}
MAX_SIZE_MB = 10
MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024
UPLOAD_CHUNK_SIZE = 1024 * 1024
class InvalidImageError(ValueError):
"""Raised when uploaded bytes are not a supported image."""
def decode_image(data: bytes) -> np.ndarray:
"""Decode uploaded bytes β†’ BGR ndarray."""
try:
with Image.open(io.BytesIO(data)) as pil:
if pil.format not in ALLOWED_IMAGE_FORMATS:
raise InvalidImageError("Unsupported image format. Use JPEG, PNG, BMP, or WEBP.")
rgb = pil.convert("RGB")
except InvalidImageError:
raise
except (Image.DecompressionBombError, OSError, UnidentifiedImageError) as exc:
raise InvalidImageError("Invalid image data. Upload a valid image file.") from exc
bgr = cv2.cvtColor(np.array(rgb), cv2.COLOR_RGB2BGR)
return bgr
async def read_limited_upload(file: UploadFile) -> bytes:
"""Read an upload while enforcing the configured byte limit."""
data = bytearray()
while chunk := await file.read(UPLOAD_CHUNK_SIZE):
if len(data) + len(chunk) > MAX_SIZE_BYTES:
raise HTTPException(status_code=400, detail=f"File too large (max {MAX_SIZE_MB} MB).")
data.extend(chunk)
return bytes(data)
# ── Routes ────────────────────────────────────────────────────────────────────
@app.get("/api/health")
def health():
"""Health check β€” confirms API and model status."""
try:
load_model()
model_ok = True
except Exception:
model_ok = False
return {
"status": "ok",
"model_loaded": model_ok,
"model": "final_model_Random_Forest.onnx",
"features": "120D (GLCM 24 + LBP 26 + Gabor 24 + ColourHist 32 + ColourMoments 9 + ABCD 5)",
}
@app.post("/api/analyze")
async def analyze(file: UploadFile = File(...)):
"""
Upload a skin image and receive classification results.
Pipeline:
1. Decode image
2. Preprocess: hair removal β†’ Gaussian blur β†’ CLAHE β†’ resize 256Γ—256
3. Otsu segmentation mask
4. 120D feature extraction
5. ONNX Random Forest inference
6. Return label, probabilities, plain-language explanation
"""
raw = await read_limited_upload(file)
try:
t0 = time.perf_counter()
# Decode
img_bgr = decode_image(raw)
# Feature extraction (preprocess + mask + 120D)
features = extract_features(img_bgr)
# ONNX inference
result = predict(features)
elapsed_ms = round((time.perf_counter() - t0) * 1000)
return {
**result,
"filename": file.filename,
"elapsed_ms": elapsed_ms,
"feature_dims": 120,
}
except FileNotFoundError as e:
raise HTTPException(status_code=503, detail=str(e))
except InvalidImageError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception("Feature extraction or inference failed")
raise HTTPException(
status_code=500,
detail="Feature extraction or inference failed."
) from e