| """ |
| 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__) |
|
|
|
|
| |
|
|
| @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 = 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=["*"], |
| ) |
|
|
|
|
| |
|
|
| 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) |
|
|
|
|
| |
|
|
| @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() |
|
|
| |
| img_bgr = decode_image(raw) |
|
|
| |
| features = extract_features(img_bgr) |
|
|
| |
| 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 |
|
|