sairamv1 commited on
Commit
14e144e
Β·
verified Β·
1 Parent(s): f4212d3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -80
app.py CHANGED
@@ -1,134 +1,158 @@
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
- from huggingface_hub import hf_hub_download # NEW
10
 
 
 
 
11
  os.environ["KERAS_BACKEND"] = "jax"
12
- import keras
13
 
14
- # ─── Config ───────────────────────────────────────────────────────────────────
15
- API_KEY = os.environ["ROBOFLOW_API_KEY"]
16
- WORKFLOW_URL = "https://serverless.roboflow.com/YOUR_WORKSPACE/YOUR_WORKFLOW" # adjust
17
  IMG_SIZE = 256
18
- THRESHOLD = 0.05 # ideally derived from healthy images [file:1]
19
 
20
- # ─── Load model once at startup ───────────────────────────────────────────────
21
  MODEL_REPO = "sairamv1/TeethCariesDetection_CNNAutoencoder"
22
- MODEL_FILENAME = "autoencoder.keras" # or whatever file you uploaded
23
-
24
- local_model_path = hf_hub_download(
25
- repo_id=MODEL_REPO,
26
- filename=MODEL_FILENAME,
27
- )
28
-
29
- MODEL = keras.saving.load_model(local_model_path)
30
-
31
- app = FastAPI()
32
-
33
-
34
- def first(seq):
35
- """Return first element or None."""
 
 
 
 
 
 
 
 
 
36
  return seq[0] if seq else None
37
 
 
 
 
 
 
 
 
 
 
38
 
39
  @app.post("/analyze")
40
  async def analyze(file: UploadFile = File(...)):
41
- # ── Read & decode image ───────────────────────────────────────────────────
 
 
 
42
  contents = await file.read()
43
  nparr = np.frombuffer(contents, np.uint8)
44
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
45
  if img is None:
46
- raise HTTPException(status_code=400, detail="Invalid image file")
47
-
48
  img_h, img_w = img.shape[:2]
49
 
50
- # ── 1. Call Roboflow ──────────────────────────────────────────────────────
51
  image_base64 = base64.b64encode(contents).decode("utf-8")
52
  try:
53
  response = requests.post(
54
  WORKFLOW_URL,
55
- json={
56
- "api_key": API_KEY,
57
- "inputs": {"image": {"type": "base64", "value": image_base64}},
58
- },
59
- timeout=60,
60
  )
61
  response.raise_for_status()
62
- except requests.exceptions.Timeout:
63
- raise HTTPException(status_code=504, detail="Roboflow request timed out")
64
- except requests.exceptions.RequestException as e:
65
- raise HTTPException(status_code=502, detail=f"Roboflow error: {e}")
66
-
67
- result = response.json()
68
- item0 = first(result)
69
- if not item0:
70
- return {"tooth_count": 0, "results": []}
71
-
72
- outputs = first(item0.get("outputs", []))
73
- pred_blob = outputs.get("predictions", {}) if outputs else {}
74
- predictions_list: List[Dict[str, Any]] = (
75
- pred_blob.get("predictions", []) if isinstance(pred_blob, dict) else (pred_blob or [])
76
- )
77
 
78
  if not predictions_list:
79
- return {"tooth_count": 0, "results": []}
80
 
81
- # ── 2. Prepare batch ──────────────────────────────────────────────────────
82
  batch_images = []
83
  metadata = []
84
-
85
- for i, det in enumerate(predictions_list):
86
  xc, yc, w, h = det["x"], det["y"], det["width"], det["height"]
87
- x1 = max(0, int(round(xc - w / 2)))
88
- y1 = max(0, int(round(yc - h / 2)))
89
- x2 = min(img_w, int(round(xc + w / 2)))
90
- y2 = min(img_h, int(round(yc + h / 2)))
91
-
92
  crop = img[y1:y2, x1:x2]
93
- if crop.size == 0:
94
- continue
95
 
96
- # Grayscale + CLAHE
97
  gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
98
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
99
  gray = clahe.apply(gray)
100
 
101
- # Resize with padding (preserve aspect ratio)
102
- ch, cw = gray.shape[:2]
103
  scale = IMG_SIZE / max(ch, cw)
104
- new_w, new_h = max(1, int(round(cw * scale))), max(1, int(round(ch * scale)))
105
  resized = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_AREA)
106
-
107
  canvas = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.uint8)
108
- y0, x0 = (IMG_SIZE - new_h) // 2, (IMG_SIZE - new_w) // 2
109
- canvas[y0 : y0 + new_h, x0 : x0 + new_w] = resized
110
-
 
111
  final_img = (canvas.astype(np.float32) / 255.0)[..., np.newaxis]
112
  batch_images.append(final_img)
113
- metadata.append({"id": i, "box": [x1, y1, x2, y2]})
114
 
115
  if not batch_images:
116
- return {"tooth_count": 0, "results": []}
117
-
118
- # ── 3. Batch inference ────────────────────────────────────────────────────
119
- batch_array = np.array(batch_images) # (N, 256, 256, 1)
120
- reconstructions = MODEL.predict(batch_array)
121
 
122
- # ── 4. Anomaly scores ─────────────────────────────────────────────────────
123
- errors = np.mean(np.abs(batch_array - reconstructions), axis=(1, 2, 3))
 
 
124
 
125
- final_results = []
126
  for idx, score in enumerate(errors):
127
- final_results.append({
128
  "tooth_index": metadata[idx]["id"],
129
  "score": float(score),
130
- "is_anomaly": bool(score > THRESHOLD),
131
- "bounding_box": metadata[idx]["box"],
 
132
  })
133
 
134
- return {"tooth_count": len(final_results), "results": final_results}
 
 
 
 
 
 
1
  import os
2
  import base64
3
+ import logging
 
4
  import numpy as np
5
  import cv2
6
  import requests
7
+ from typing import List, Dict, Any
8
+
9
+ import keras
10
  from fastapi import FastAPI, File, UploadFile, HTTPException
11
+ from huggingface_hub import hf_hub_download
12
 
13
+ # ─── Logging & Backend ────────────────────────────────────────────────────────
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
  os.environ["KERAS_BACKEND"] = "jax"
17
+ os.environ["JAX_PLATFORM_NAME"] = "cpu" # Fast startup, no GPU needed
18
 
19
+ # ─── Config (use Space Secrets for API_KEY) ───────────────────────────────────
20
+ API_KEY = os.getenv("ROBOFLOW_API_KEY") # Set in Space Settings β†’ Secrets
21
+ WORKFLOW_URL = "https://serverless.roboflow.com/my-workspace-iy9ll/find-teeth" # From your SDK
22
  IMG_SIZE = 256
23
+ THRESHOLD = 0.05 # mean+2*std from healthy images [file:1]
24
 
 
25
  MODEL_REPO = "sairamv1/TeethCariesDetection_CNNAutoencoder"
26
+ MODEL_FILENAME = "model.keras" # Change if your file is named differently
27
+
28
+ # ─── Load Model at Startup ────────────────────────────────────────────────────
29
+ MODEL = None
30
+ try:
31
+ logger.info("πŸš€ Downloading model...")
32
+ local_model_path = hf_hub_download(
33
+ repo_id=MODEL_REPO,
34
+ filename=MODEL_FILENAME,
35
+ cache_dir="/tmp"
36
+ )
37
+ logger.info(f"πŸ“₯ Model at: {local_model_path}")
38
+
39
+ logger.info("πŸ”„ Loading Keras model...")
40
+ MODEL = keras.saving.load_model(local_model_path)
41
+ logger.info("βœ… Model loaded successfully!")
42
+ except Exception as e:
43
+ logger.error(f"❌ Model error: {e}")
44
+ MODEL = None
45
+
46
+ app = FastAPI(title="Teeth Caries Detection API")
47
+
48
+ def first(seq: Any) -> Any:
49
  return seq[0] if seq else None
50
 
51
+ @app.get("/")
52
+ async def root():
53
+ return {
54
+ "status": "alive βœ…",
55
+ "model_loaded": MODEL is not None,
56
+ "endpoints": ["/analyze"],
57
+ "tooth_size": f"{IMG_SIZE}x{IMG_SIZE}",
58
+ "threshold": THRESHOLD
59
+ }
60
 
61
  @app.post("/analyze")
62
  async def analyze(file: UploadFile = File(...)):
63
+ if MODEL is None:
64
+ raise HTTPException(status_code=500, detail="Model failed to load at startup")
65
+
66
+ # 1. Decode image
67
  contents = await file.read()
68
  nparr = np.frombuffer(contents, np.uint8)
69
  img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
70
  if img is None:
71
+ raise HTTPException(status_code=400, detail="Invalid image")
72
+
73
  img_h, img_w = img.shape[:2]
74
 
75
+ # 2. Roboflow workflow
76
  image_base64 = base64.b64encode(contents).decode("utf-8")
77
  try:
78
  response = requests.post(
79
  WORKFLOW_URL,
80
+ params={"api_key": API_KEY},
81
+ json={"inputs": {"image": {"type": "base64", "value": image_base64}}},
82
+ timeout=60
 
 
83
  )
84
  response.raise_for_status()
85
+ except Exception as e:
86
+ raise HTTPException(status_code=502, detail=f"Roboflow error: {str(e)}")
87
+
88
+ # 3. Parse predictions (flexible for different response formats)
89
+ result_json = response.json()
90
+ predictions_list = []
91
+ try:
92
+ if isinstance(result_json, list):
93
+ outputs = first(result_json).get("outputs", {})
94
+ else:
95
+ outputs = result_json.get("outputs", [{}])[0]
96
+ pred_blob = outputs.get("predictions", {})
97
+ predictions_list = pred_blob.get("predictions", []) if isinstance(pred_blob, dict) else pred_blob
98
+ except:
99
+ predictions_list = []
100
 
101
  if not predictions_list:
102
+ return {"status": "success", "tooth_count": 0, "results": [], "message": "No teeth detected"}
103
 
104
+ # 4. Crop + preprocess teeth
105
  batch_images = []
106
  metadata = []
107
+ for i, det in enumerate(predictions_list[:5]): # Max 5 teeth
 
108
  xc, yc, w, h = det["x"], det["y"], det["width"], det["height"]
109
+ x1, y1 = max(0, int(xc - w/2)), max(0, int(yc - h/2))
110
+ x2, y2 = min(img_w, int(xc + w/2)), min(img_h, int(yc + h/2))
111
+
 
 
112
  crop = img[y1:y2, x1:x2]
113
+ if crop.size == 0: continue
 
114
 
115
+ # Grayscale + CLAHE enhancement
116
  gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
117
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
118
  gray = clahe.apply(gray)
119
 
120
+ # Resize with padding (preserves aspect ratio)
121
+ ch, cw = gray.shape
122
  scale = IMG_SIZE / max(ch, cw)
123
+ new_w, new_h = max(1, int(cw * scale)), max(1, int(ch * scale))
124
  resized = cv2.resize(gray, (new_w, new_h), interpolation=cv2.INTER_AREA)
125
+
126
  canvas = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.uint8)
127
+ y0, x0 = (IMG_SIZE - new_h)//2, (IMG_SIZE - new_w)//2
128
+ canvas[y0:y0+new_h, x0:x0+new_w] = resized
129
+
130
+ # Normalize to [0,1] + channel dim
131
  final_img = (canvas.astype(np.float32) / 255.0)[..., np.newaxis]
132
  batch_images.append(final_img)
133
+ metadata.append({"id": i, "box": [x1,y1,x2,y2]})
134
 
135
  if not batch_images:
136
+ return {"status": "success", "tooth_count": 0, "results": []}
 
 
 
 
137
 
138
+ # 5. Autoencoder inference + anomaly scoring
139
+ batch_array = np.array(batch_images)
140
+ reconstructions = MODEL.predict(batch_array, verbose=0)
141
+ errors = np.mean(np.abs(batch_array - reconstructions), axis=(1,2,3))
142
 
143
+ results = []
144
  for idx, score in enumerate(errors):
145
+ results.append({
146
  "tooth_index": metadata[idx]["id"],
147
  "score": float(score),
148
+ "is_anomaly": score > THRESHOLD,
149
+ "confidence": float(1.0 - score), # Lower error = higher confidence healthy
150
+ "bounding_box": metadata[idx]["box"]
151
  })
152
 
153
+ return {
154
+ "status": "success",
155
+ "tooth_count": len(results),
156
+ "results": results,
157
+ "threshold_used": THRESHOLD
158
+ }