Download main.py from Jennigwen/comvis: direct link, hf CLI and curl.
- Browser
- Download file 5.11 kB
-
https://huggingface.co/spaces/Jennigwen/comvis/resolve/main/main.py
- Command line
-
hf download hf://spaces/Jennigwen/comvis/main.py
-
curl -L -o main.py https://huggingface.co/spaces/Jennigwen/comvis/resolve/main/main.py
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 βββββββββββββββββββββββββββββββββββββββ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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)", | |
| } | |
| 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 | |