Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,125 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
app = FastAPI()
|
| 4 |
|
| 5 |
-
|
| 6 |
-
def
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import base64
|
| 3 |
+
from typing import List, Dict, Any
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import cv2
|
| 7 |
+
import requests
|
| 8 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 9 |
+
|
| 10 |
+
os.environ["KERAS_BACKEND"] = "jax"
|
| 11 |
+
import keras
|
| 12 |
+
|
| 13 |
+
# βββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
API_KEY = os.environ["ROBOFLOW_API_KEY"]
|
| 15 |
+
WORKFLOW_URL = "https://serverless.roboflow.com/YOUR_WORKSPACE/YOUR_WORKFLOW" # adjust
|
| 16 |
+
IMG_SIZE = 256
|
| 17 |
+
THRESHOLD = 0.05 # ideally derived from healthy images [file:1]
|
| 18 |
+
|
| 19 |
+
# βββ Load model once at startup βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
+
MODEL = keras.saving.load_model("hf://sairamv1/TeethCariesDetection_CNNAutoencoder")
|
| 21 |
|
| 22 |
app = FastAPI()
|
| 23 |
|
| 24 |
+
|
| 25 |
+
def first(seq):
|
| 26 |
+
"""Return first element or None."""
|
| 27 |
+
return seq[0] if seq else None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@app.post("/analyze")
|
| 31 |
+
async def analyze(file: UploadFile = File(...)):
|
| 32 |
+
# ββ Read & decode image βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
contents = await file.read()
|
| 34 |
+
nparr = np.frombuffer(contents, np.uint8)
|
| 35 |
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 36 |
+
if img is None:
|
| 37 |
+
raise HTTPException(status_code=400, detail="Invalid image file")
|
| 38 |
+
|
| 39 |
+
img_h, img_w = img.shape[:2]
|
| 40 |
+
|
| 41 |
+
# ββ 1. Call Roboflow ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
+
image_base64 = base64.b64encode(contents).decode("utf-8")
|
| 43 |
+
try:
|
| 44 |
+
response = requests.post(
|
| 45 |
+
WORKFLOW_URL,
|
| 46 |
+
json={
|
| 47 |
+
"api_key": API_KEY,
|
| 48 |
+
"inputs": {"image": {"type": "base64", "value": image_base64}},
|
| 49 |
+
},
|
| 50 |
+
timeout=60,
|
| 51 |
+
)
|
| 52 |
+
response.raise_for_status()
|
| 53 |
+
except requests.exceptions.Timeout:
|
| 54 |
+
raise HTTPException(status_code=504, detail="Roboflow request timed out")
|
| 55 |
+
except requests.exceptions.RequestException as e:
|
| 56 |
+
raise HTTPException(status_code=502, detail=f"Roboflow error: {e}")
|
| 57 |
+
|
| 58 |
+
result = response.json()
|
| 59 |
+
item0 = first(result)
|
| 60 |
+
if not item0:
|
| 61 |
+
return {"tooth_count": 0, "results": []}
|
| 62 |
+
|
| 63 |
+
outputs = first(item0.get("outputs", []))
|
| 64 |
+
pred_blob = outputs.get("predictions", {}) if outputs else {}
|
| 65 |
+
predictions_list: List[Dict[str, Any]] = (
|
| 66 |
+
pred_blob.get("predictions", []) if isinstance(pred_blob, dict) else (pred_blob or [])
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
if not predictions_list:
|
| 70 |
+
return {"tooth_count": 0, "results": []}
|
| 71 |
+
|
| 72 |
+
# ββ 2. Prepare batch ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 73 |
+
batch_images = []
|
| 74 |
+
metadata = []
|
| 75 |
+
|
| 76 |
+
for i, det in enumerate(predictions_list):
|
| 77 |
+
xc, yc, w, h = det["x"], det["y"], det["width"], det["height"]
|
| 78 |
+
x1 = max(0, int(round(xc - w / 2)))
|
| 79 |
+
y1 = max(0, int(round(yc - h / 2)))
|
| 80 |
+
x2 = min(img_w, int(round(xc + w / 2)))
|
| 81 |
+
y2 = min(img_h, int(round(yc + h / 2)))
|
| 82 |
+
|
| 83 |
+
crop = img[y1:y2, x1:x2]
|
| 84 |
+
if crop.size == 0:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
# Grayscale + CLAHE
|
| 88 |
+
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
|
| 89 |
+
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
| 90 |
+
gray = clahe.apply(gray)
|
| 91 |
+
|
| 92 |
+
# Resize with padding (preserve aspect ratio)
|
| 93 |
+
ch, cw = gray.shape[:2]
|
| 94 |
+
scale = IMG_SIZE / max(ch, cw)
|
| 95 |
+
new_w, new_h = max(1, int(round(cw * scale))), max(1, int(round(ch * scale)))
|
| 96 |
+
resized = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| 97 |
+
|
| 98 |
+
canvas = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.uint8)
|
| 99 |
+
y0, x0 = (IMG_SIZE - new_h) // 2, (IMG_SIZE - new_w) // 2
|
| 100 |
+
canvas[y0 : y0 + new_h, x0 : x0 + new_w] = resized
|
| 101 |
+
|
| 102 |
+
final_img = (canvas.astype(np.float32) / 255.0)[..., np.newaxis]
|
| 103 |
+
batch_images.append(final_img)
|
| 104 |
+
metadata.append({"id": i, "box": [x1, y1, x2, y2]})
|
| 105 |
+
|
| 106 |
+
if not batch_images:
|
| 107 |
+
return {"tooth_count": 0, "results": []}
|
| 108 |
+
|
| 109 |
+
# ββ 3. Batch inference ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 110 |
+
batch_array = np.array(batch_images) # (N, 256, 256, 1)
|
| 111 |
+
reconstructions = MODEL.predict(batch_array)
|
| 112 |
+
|
| 113 |
+
# ββ 4. Anomaly scores ββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½
|
| 114 |
+
errors = np.mean(np.abs(batch_array - reconstructions), axis=(1, 2, 3))
|
| 115 |
+
|
| 116 |
+
final_results = []
|
| 117 |
+
for idx, score in enumerate(errors):
|
| 118 |
+
final_results.append({
|
| 119 |
+
"tooth_index": metadata[idx]["id"],
|
| 120 |
+
"score": float(score),
|
| 121 |
+
"is_anomaly": bool(score > THRESHOLD),
|
| 122 |
+
"bounding_box": metadata[idx]["box"],
|
| 123 |
+
})
|
| 124 |
+
|
| 125 |
+
return {"tooth_count": len(final_results), "results": final_results}
|