SaswatML123 commited on
Commit
10b056e
Β·
verified Β·
1 Parent(s): 4d43d48

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +15 -0
  2. README.md +22 -4
  3. main.py +94 -0
  4. model_loader.py +152 -0
  5. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy app code
10
+ COPY . .
11
+
12
+ # HuggingFace Spaces runs on port 7860
13
+ EXPOSE 7860
14
+
15
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,10 +1,28 @@
1
  ---
2
- title: MedScan API
3
- emoji: πŸ“‰
4
  colorFrom: red
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MediScan AI
3
+ emoji: πŸ₯
4
  colorFrom: red
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # MediScan AI β€” Medical Diagnostic API
11
+
12
+ FastAPI backend serving skin cancer and pneumonia predictions.
13
+
14
+ ## Endpoints
15
+
16
+ | Method | URL | Description |
17
+ |--------|-----|-------------|
18
+ | GET | `/` | Status |
19
+ | GET | `/health` | Health check |
20
+ | GET | `/docs` | Swagger UI |
21
+ | POST | `/predict/pneumonia` | Chest X-ray analysis |
22
+ | POST | `/predict/skin` | Skin lesion classification |
23
+
24
+ ## Models
25
+ - **Pneumonia**: Custom CNN (`SaswatML123/PneuModel`)
26
+ - **Skin Cancer**: EfficientNetV2M + EfficientNetV2S + ConvNeXt ensemble (`SaswatML123/Skin_cancer_detection`)
27
+
28
+ > For research use only. Not a medical device.
main.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MediScan AI β€” HuggingFace Space Backend
3
+ Port 7860 (required by HuggingFace Spaces)
4
+ """
5
+
6
+ import uvicorn
7
+ from fastapi import FastAPI, File, UploadFile, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from contextlib import asynccontextmanager
10
+ from PIL import Image
11
+ import io
12
+
13
+ from model_loader import (
14
+ load_pneumo_model, load_skin_models,
15
+ predict_pneumonia, predict_skin
16
+ )
17
+
18
+
19
+ @asynccontextmanager
20
+ async def lifespan(app: FastAPI):
21
+ print("=" * 50)
22
+ print(" MediScan AI Space β€” Loading models...")
23
+ print("=" * 50)
24
+ load_pneumo_model()
25
+ load_skin_models()
26
+ print("=" * 50)
27
+ print(" All models ready!")
28
+ print("=" * 50)
29
+ yield
30
+
31
+
32
+ app = FastAPI(
33
+ title="MediScan AI",
34
+ version="1.0.0",
35
+ lifespan=lifespan,
36
+ )
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=["*"],
41
+ allow_methods=["*"],
42
+ allow_headers=["*"],
43
+ )
44
+
45
+
46
+ @app.get("/")
47
+ def root():
48
+ return {
49
+ "status": "ok",
50
+ "endpoints": {
51
+ "pneumonia": "POST /predict/pneumonia",
52
+ "skin": "POST /predict/skin",
53
+ "docs": "/docs",
54
+ }
55
+ }
56
+
57
+
58
+ @app.get("/health")
59
+ def health():
60
+ return {"status": "healthy"}
61
+
62
+
63
+ @app.post("/predict/pneumonia")
64
+ async def pneumonia_endpoint(file: UploadFile = File(...)):
65
+ if not file.content_type.startswith("image/"):
66
+ raise HTTPException(400, "Must be an image file.")
67
+ data = await file.read()
68
+ try:
69
+ image = Image.open(io.BytesIO(data))
70
+ except Exception:
71
+ raise HTTPException(400, "Could not read image.")
72
+ try:
73
+ return predict_pneumonia(image)
74
+ except Exception as e:
75
+ raise HTTPException(500, f"Inference error: {e}")
76
+
77
+
78
+ @app.post("/predict/skin")
79
+ async def skin_endpoint(file: UploadFile = File(...)):
80
+ if not file.content_type.startswith("image/"):
81
+ raise HTTPException(400, "Must be an image file.")
82
+ data = await file.read()
83
+ try:
84
+ image = Image.open(io.BytesIO(data))
85
+ except Exception:
86
+ raise HTTPException(400, "Could not read image.")
87
+ try:
88
+ return predict_skin(image)
89
+ except Exception as e:
90
+ raise HTTPException(500, f"Inference error: {e}")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ uvicorn.run(app, host="0.0.0.0", port=7860)
model_loader.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ model_loader.py
3
+ Downloads models from HuggingFace repos, caches them, runs inference.
4
+ """
5
+
6
+ import os
7
+ import numpy as np
8
+ from PIL import Image
9
+ from huggingface_hub import hf_hub_download
10
+
11
+ CACHE_DIR = "/app/model_cache"
12
+ os.makedirs(CACHE_DIR, exist_ok=True)
13
+
14
+
15
+ # ══════════════════════════════════════════════════════════════════════════════
16
+ # PNEUMONIA β€” Keras .h5
17
+ # ══════════════════════════════════════════════════════════════════════════════
18
+
19
+ PNEUMO_REPO = "SaswatML123/PneuModel"
20
+ PNEUMO_FILE = "pneumodel.h5"
21
+ PNEUMO_SIZE = (224, 224)
22
+ _pneumo_model = None
23
+
24
+
25
+ def _download(repo, filename):
26
+ local = os.path.join(CACHE_DIR, filename)
27
+ if os.path.exists(local):
28
+ print(f"[Cache] {filename}")
29
+ return local
30
+ print(f"[HuggingFace] Downloading {filename}...")
31
+ return hf_hub_download(repo_id=repo, filename=filename, local_dir=CACHE_DIR)
32
+
33
+
34
+ def load_pneumo_model():
35
+ global _pneumo_model
36
+ if _pneumo_model is not None:
37
+ return
38
+ import tensorflow as tf
39
+ path = _download(PNEUMO_REPO, PNEUMO_FILE)
40
+ print("[Pneumonia] Loading...")
41
+ _pneumo_model = tf.keras.models.load_model(path)
42
+ print("[Pneumonia] βœ“ Ready")
43
+
44
+
45
+ def predict_pneumonia(image: Image.Image) -> dict:
46
+ load_pneumo_model()
47
+ img = image.convert("RGB").resize(PNEUMO_SIZE)
48
+ arr = np.array(img, dtype=np.float32) / 255.0
49
+ arr = np.expand_dims(arr, axis=0)
50
+ preds = _pneumo_model.predict(arr, verbose=0)
51
+
52
+ if preds.shape[-1] == 1:
53
+ pneumonia_prob = float(preds[0][0])
54
+ normal_prob = 1.0 - pneumonia_prob
55
+ else:
56
+ normal_prob = float(preds[0][0])
57
+ pneumonia_prob = float(preds[0][1])
58
+
59
+ if pneumonia_prob >= 0.5:
60
+ label, confidence = "PNEUMONIA", pneumonia_prob
61
+ else:
62
+ label, confidence = "NORMAL", normal_prob
63
+
64
+ return {
65
+ "label": label,
66
+ "confidence": round(confidence, 4),
67
+ "probabilities": {
68
+ "NORMAL": round(normal_prob, 4),
69
+ "PNEUMONIA": round(pneumonia_prob, 4),
70
+ },
71
+ }
72
+
73
+
74
+ # ══════════════════════════════════════════════════════════════════════════════
75
+ # SKIN CANCER β€” PyTorch ensemble
76
+ # ══════════════════════════════════════════════════════════════════════════════
77
+
78
+ SKIN_REPO = "SaswatML123/Skin_cancer_detection"
79
+ SKIN_FILES = {
80
+ "efficientnetv2m": "model1_efficientnetv2m.pth",
81
+ "efficientnetv2s": "model2_efficientnetv2s.pth",
82
+ "convnext": "model3_convnext.pth",
83
+ }
84
+ SKIN_CLASSES = [
85
+ "Melanocytic nevi",
86
+ "Melanoma",
87
+ "Benign keratosis",
88
+ "Basal cell carcinoma",
89
+ "Actinic keratosis",
90
+ "Vascular lesions",
91
+ "Dermatofibroma",
92
+ ]
93
+ NUM_SKIN_CLASSES = len(SKIN_CLASSES)
94
+ _skin_models = []
95
+ SKIN_TRANSFORM = None
96
+
97
+
98
+ def load_skin_models():
99
+ global _skin_models, SKIN_TRANSFORM
100
+ if _skin_models:
101
+ return
102
+ import torch
103
+ import timm
104
+ from torchvision import transforms
105
+
106
+ SKIN_TRANSFORM = transforms.Compose([
107
+ transforms.Resize((224, 224)),
108
+ transforms.ToTensor(),
109
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
110
+ ])
111
+
112
+ arch_map = {
113
+ "efficientnetv2m": "tf_efficientnetv2_m",
114
+ "efficientnetv2s": "tf_efficientnetv2_s",
115
+ "convnext": "convnext_base",
116
+ }
117
+ device = torch.device("cpu") # Space free tier is CPU only
118
+
119
+ for arch, filename in SKIN_FILES.items():
120
+ path = _download(SKIN_REPO, filename)
121
+ model = timm.create_model(arch_map[arch], pretrained=False, num_classes=NUM_SKIN_CLASSES)
122
+ state = torch.load(path, map_location=device)
123
+ if isinstance(state, dict):
124
+ for key in ("model_state_dict", "state_dict", "model"):
125
+ if key in state:
126
+ state = state[key]
127
+ break
128
+ model.load_state_dict(state, strict=False)
129
+ model.eval()
130
+ _skin_models.append(model)
131
+ print(f"[Skin] βœ“ {arch}")
132
+
133
+ print(f"[Skin] Ensemble ready β€” {len(_skin_models)} models")
134
+
135
+
136
+ def predict_skin(image: Image.Image) -> dict:
137
+ import torch
138
+ load_skin_models()
139
+ img_t = SKIN_TRANSFORM(image.convert("RGB")).unsqueeze(0)
140
+ all_probs = []
141
+ with torch.no_grad():
142
+ for model in _skin_models:
143
+ probs = torch.softmax(model(img_t), dim=1).squeeze().numpy()
144
+ all_probs.append(probs)
145
+ avg = np.mean(all_probs, axis=0)
146
+ top = int(np.argmax(avg))
147
+ return {
148
+ "label": SKIN_CLASSES[top],
149
+ "confidence": round(float(avg[top]), 4),
150
+ "probabilities": {c: round(float(p), 4) for c, p in zip(SKIN_CLASSES, avg)},
151
+ "model_count": len(_skin_models),
152
+ }
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ torch
5
+ torchvision
6
+ timm
7
+ tensorflow
8
+ Pillow
9
+ numpy
10
+ huggingface_hub