omgy commited on
Commit
af9b9f1
Β·
verified Β·
1 Parent(s): 72ef9dc

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +231 -0
app.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ import numpy as np
5
+ import cv2
6
+ import requests
7
+ import pickle
8
+ from tensorflow.keras.models import load_model, Model
9
+
10
+ app = FastAPI(
11
+ title="Embryo Quality Classifier & Ranker API",
12
+ description="Classify and rank multiple embryos by viability score",
13
+ version="2.0.0"
14
+ )
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ # ── Load models on startup ───────────────────────────────────────────────────
24
+ print("Loading models...")
25
+ full_model = load_model("efficientnet_embryo_model.h5")
26
+ efficientnet_feature_extractor = Model(
27
+ inputs=full_model.input,
28
+ outputs=full_model.layers[-3].output,
29
+ )
30
+ fusion_model = load_model("dual_branch_embryo_model.keras")
31
+ with open("morph_scaler.pkl", "rb") as f:
32
+ scaler = pickle.load(f)
33
+ print("All models loaded!")
34
+
35
+
36
+ # ── Helper functions ─────────────────────────────────────────────────────────
37
+ def download_image(url: str):
38
+ try:
39
+ resp = requests.get(url, timeout=10)
40
+ resp.raise_for_status()
41
+ arr = np.frombuffer(resp.content, np.uint8)
42
+ return cv2.imdecode(arr, cv2.IMREAD_COLOR)
43
+ except Exception as e:
44
+ print(f"Download error: {e}")
45
+ return None
46
+
47
+
48
+ def extract_efficientnet_features(img: np.ndarray) -> np.ndarray:
49
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
50
+ img_resized = cv2.resize(img_rgb, (224, 224)) / 255.0
51
+ features = efficientnet_feature_extractor.predict(
52
+ np.expand_dims(img_resized, axis=0), verbose=0
53
+ )
54
+ return features.flatten()
55
+
56
+
57
+ def extract_morphological_features(img: np.ndarray) -> np.ndarray:
58
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
59
+ blur = cv2.GaussianBlur(gray, (5, 5), 0)
60
+ _, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
61
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
62
+
63
+ centroids = []
64
+ for cnt in contours:
65
+ M = cv2.moments(cnt)
66
+ if M["m00"] != 0:
67
+ centroids.append((int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"])))
68
+
69
+ symmetry_score = 0.0
70
+ if len(centroids) > 1:
71
+ distances = [
72
+ np.linalg.norm(np.array(centroids[i]) - np.array(centroids[j]))
73
+ for i in range(len(centroids))
74
+ for j in range(i + 1, len(centroids))
75
+ ]
76
+ symmetry_score = float(np.mean(distances))
77
+
78
+ embryo_area = int(np.sum(thresh == 255))
79
+ fragmented_area = sum(cv2.contourArea(c) for c in contours if cv2.contourArea(c) < 500)
80
+ fragmentation_ratio = fragmented_area / embryo_area if embryo_area > 0 else 0.0
81
+
82
+ return np.array([symmetry_score, fragmentation_ratio])
83
+
84
+
85
+ def analyze_single_image(img: np.ndarray) -> dict:
86
+ """Run full pipeline on one image, return raw scores."""
87
+ deep_features = extract_efficientnet_features(img)
88
+ morph_raw = extract_morphological_features(img)
89
+ morph_scaled = scaler.transform([morph_raw])[0]
90
+
91
+ combined = np.expand_dims(np.concatenate([deep_features, morph_scaled]), axis=0)
92
+ prediction = fusion_model.predict(combined, verbose=0)[0] # shape: (2,)
93
+
94
+ class_id = int(np.argmax(prediction))
95
+ good_prob = float(prediction[1]) # probability of being Good quality
96
+ poor_prob = float(prediction[0]) # probability of being Poor quality
97
+
98
+ return {
99
+ "class_id": class_id,
100
+ "label": "Good Quality Embryo" if class_id == 1 else "Poor Quality Embryo",
101
+ "confidence": round(float(np.max(prediction)), 4),
102
+ "viability_score_percent": round(good_prob * 100, 2), # always "good" probability as score
103
+ "good_probability": round(good_prob, 4),
104
+ "poor_probability": round(poor_prob, 4),
105
+ "symmetry_score": round(float(morph_raw[0]), 4),
106
+ "fragmentation_ratio": round(float(morph_raw[1]), 6),
107
+ }
108
+
109
+
110
+ # ── Schemas ───────────────────────────────────────────────────────────────────
111
+ class SingleRequest(BaseModel):
112
+ image_url: str
113
+
114
+ class RankRequest(BaseModel):
115
+ embryos: list[dict] # each: {"id": "E1", "image_url": "https://..."}
116
+
117
+ class EmbryoResult(BaseModel):
118
+ rank: int
119
+ id: str
120
+ label: str
121
+ viability_score_percent: float
122
+ confidence: float
123
+ good_probability: float
124
+ poor_probability: float
125
+ symmetry_score: float
126
+ fragmentation_ratio: float
127
+ recommendation: str
128
+
129
+ class RankResponse(BaseModel):
130
+ total_analyzed: int
131
+ best_embryo_id: str
132
+ ranked_embryos: list[EmbryoResult]
133
+
134
+
135
+ # ── Endpoints ──────────────────���──────────────────────────────────────────────
136
+ @app.get("/")
137
+ def root():
138
+ return {
139
+ "message": "Embryo Quality Classifier & Ranker",
140
+ "endpoints": {
141
+ "POST /predict": "Analyze a single embryo image",
142
+ "POST /rank": "Rank multiple embryos by viability score",
143
+ },
144
+ "docs": "/docs"
145
+ }
146
+
147
+ @app.get("/health")
148
+ def health():
149
+ return {"status": "ok"}
150
+
151
+
152
+ @app.post("/predict")
153
+ def predict_single(request: SingleRequest):
154
+ """Analyze one embryo image from a URL."""
155
+ img = download_image(request.image_url)
156
+ if img is None:
157
+ raise HTTPException(status_code=400, detail="Could not download image.")
158
+ return analyze_single_image(img)
159
+
160
+
161
+ @app.post("/rank", response_model=RankResponse)
162
+ def rank_embryos(request: RankRequest):
163
+ """
164
+ Rank multiple embryos from a list of image URLs.
165
+
166
+ Request body example:
167
+ {
168
+ "embryos": [
169
+ {"id": "E1", "image_url": "https://..."},
170
+ {"id": "E2", "image_url": "https://..."},
171
+ {"id": "E3", "image_url": "https://..."}
172
+ ]
173
+ }
174
+
175
+ Returns all embryos ranked #1 (best viability) to #N (worst).
176
+ Each embryo gets a viability_score_percent, label, and transfer recommendation.
177
+ """
178
+ if len(request.embryos) < 1:
179
+ raise HTTPException(status_code=400, detail="Provide at least 1 embryo.")
180
+ if len(request.embryos) > 20:
181
+ raise HTTPException(status_code=400, detail="Maximum 20 embryos per request.")
182
+
183
+ results = []
184
+ for i, embryo in enumerate(request.embryos):
185
+ embryo_id = embryo.get("id") or f"Embryo_{i+1}"
186
+ image_url = embryo.get("image_url")
187
+
188
+ if not image_url:
189
+ raise HTTPException(status_code=400, detail=f"Missing image_url for '{embryo_id}'.")
190
+
191
+ img = download_image(image_url)
192
+ if img is None:
193
+ raise HTTPException(status_code=400, detail=f"Could not download image for '{embryo_id}'.")
194
+
195
+ analysis = analyze_single_image(img)
196
+ results.append({"id": embryo_id, **analysis})
197
+
198
+ # Sort best β†’ worst by viability score
199
+ results.sort(key=lambda x: x["viability_score_percent"], reverse=True)
200
+
201
+ ranked = []
202
+ for rank_pos, r in enumerate(results, start=1):
203
+ score = r["viability_score_percent"]
204
+
205
+ if score >= 80:
206
+ rec = "Highly recommended for transfer"
207
+ elif score >= 60:
208
+ rec = "Suitable for transfer"
209
+ elif score >= 40:
210
+ rec = "Marginal quality β€” use only if no better option available"
211
+ else:
212
+ rec = "Poor quality β€” not recommended for transfer"
213
+
214
+ ranked.append(EmbryoResult(
215
+ rank=rank_pos,
216
+ id=r["id"],
217
+ label=r["label"],
218
+ viability_score_percent=r["viability_score_percent"],
219
+ confidence=r["confidence"],
220
+ good_probability=r["good_probability"],
221
+ poor_probability=r["poor_probability"],
222
+ symmetry_score=r["symmetry_score"],
223
+ fragmentation_ratio=r["fragmentation_ratio"],
224
+ recommendation=rec,
225
+ ))
226
+
227
+ return RankResponse(
228
+ total_analyzed=len(ranked),
229
+ best_embryo_id=ranked[0].id,
230
+ ranked_embryos=ranked,
231
+ )