rishab1090 commited on
Commit
63a8caf
·
verified ·
1 Parent(s): 130e656

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -139
app.py CHANGED
@@ -1,58 +1,38 @@
1
  # app.py
2
- # app.py
3
- import io
4
- import os
5
- import time
6
- import base64
7
  from pathlib import Path
8
  from typing import Optional
9
-
10
- import uvicorn
11
  from fastapi import FastAPI, File, UploadFile, Query, HTTPException
12
  from fastapi.responses import JSONResponse
13
- import numpy as np
14
- import cv2
15
  from ultralytics import YOLO
16
- import requests
17
 
18
- # ------------------------
19
- # Config
20
- # ------------------------
21
- # Put your model file next to this script and set MODEL_PATH to the filename
22
  MODEL_PATH = os.getenv("MODEL_PATH", "best_rockfall_model.pt")
23
  CONF_DEFAULT = float(os.getenv("CONF_DEFAULT", 0.25))
24
  IMG_SIZE_DEFAULT = int(os.getenv("IMG_SIZE_DEFAULT", 640))
25
 
26
- if not Path(MODEL_PATH).exists():
27
- print(f"Warning: MODEL_PATH '{MODEL_PATH}' not found. Please place your .pt file next to app.py or set MODEL_PATH env var.")
28
-
29
- # ------------------------
30
- # App + Model loader
31
- # ------------------------
32
- app = FastAPI(title="Rockfall Detection API (fixed single-class behavior)")
33
 
34
- MODEL = {"model": None, "path": None}
35
-
36
- def load_model(path: str = MODEL_PATH):
37
- # Use YOLO.from_pretrained if using a named model; here we load a local .pt
38
- model = YOLO(path)
39
- MODEL["model"] = model
40
- MODEL["path"] = path
41
- print("Model loaded from:", path)
42
- return model
43
 
44
  @app.on_event("startup")
45
  def startup():
46
  try:
47
  load_model(MODEL_PATH)
48
  except Exception as e:
49
- # don't crash server; let user fix model and call reload endpoint if implemented
50
  MODEL["model"] = None
51
- print("Failed to load model on startup:", e)
52
 
53
- # ------------------------
54
- # Helpers
55
- # ------------------------
56
  def read_imagefile(upload_file: UploadFile) -> np.ndarray:
57
  data = upload_file.file.read()
58
  upload_file.file.close()
@@ -62,80 +42,104 @@ def read_imagefile(upload_file: UploadFile) -> np.ndarray:
62
  raise ValueError("Cannot decode image")
63
  return img
64
 
65
- def fetch_image_from_url(url: str, timeout: int = 10) -> np.ndarray:
66
- r = requests.get(url, timeout=timeout)
67
- if r.status_code != 200:
68
- raise ValueError(f"Failed to fetch image: status {r.status_code}")
69
- arr = np.frombuffer(r.content, np.uint8)
70
- img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
71
- if img is None:
72
- raise ValueError("Invalid image content")
73
- return img
74
 
75
- def run_inference(img_bgr: np.ndarray, conf: float, imgsz: int):
76
  if MODEL["model"] is None:
77
- raise RuntimeError("Model is not loaded. Ensure MODEL_PATH is correct and model file exists.")
78
- # Ultralytics accepts numpy image (BGR). Use predict() to get full results object.
79
- start = time.time()
 
80
  results = MODEL["model"].predict(source=img_bgr, conf=conf, imgsz=imgsz, verbose=False)
81
- elapsed_ms = (time.time() - start) * 1000.0
82
- res = results[0] # single image result
83
- return res, elapsed_ms
 
 
 
84
 
85
- def boxes_from_result(res) -> list:
86
- """
87
- Convert ultralytics result to a unified list of detections.
88
- Force single-class mapping: class_id=0, class_name='rockfall' for every detection.
89
- """
90
  detections = []
91
- if hasattr(res, "boxes") and len(res.boxes) > 0:
92
- xyxy = res.boxes.xyxy.cpu().numpy() # x1,y1,x2,y2
93
- confs = res.boxes.conf.cpu().numpy()
94
- # cls = res.boxes.cls.cpu().numpy().astype(int) # original classes (ignored)
95
- for b, score in zip(xyxy, confs):
96
- x1, y1, x2, y2 = map(int, b)
97
- detections.append({
98
- "class_id": 0, # force to single-class 0
99
- "class_name": "rockfall", # canonical class name
100
- "confidence": float(score),
101
- "bbox": [x1, y1, x2, y2]
102
- })
103
- return detections
104
-
105
- def annotate_image_with_boxes(img_bgr: np.ndarray, detections: list) -> np.ndarray:
106
- """
107
- Draw boxes (green) and labels on a copy of the input image (BGR).
108
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  out = img_bgr.copy()
110
- for det in detections:
111
- x1, y1, x2, y2 = det["bbox"]
112
- conf = det["confidence"]
113
- label = f"{det['class_name']} {conf:.2f}"
114
- # draw rectangle
115
- cv2.rectangle(out, (x1, y1), (x2, y2), (0, 255, 0), thickness=2)
116
- # label background
117
  (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1)
118
- cv2.rectangle(out, (x1, y1 - th - 6), (x1 + tw + 6, y1), (0, 255, 0), -1)
119
- cv2.putText(out, label, (x1 + 3, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1, cv2.LINE_AA)
120
  return out
121
 
122
- def image_to_base64(img_bgr: np.ndarray) -> str:
123
- _, buf = cv2.imencode(".jpg", img_bgr, [int(cv2.IMWRITE_JPEG_QUALITY), 90])
124
- b64 = base64.b64encode(buf.tobytes()).decode("utf-8")
125
- return f"data:image/jpeg;base64,{b64}"
126
-
127
- # ------------------------
128
- # Endpoints
129
- # ------------------------
130
  @app.get("/health")
131
  def health():
132
- return {"status": "ok", "model_loaded": MODEL["model"] is not None, "model_path": MODEL["path"]}
133
 
134
  @app.post("/predict/image")
135
  async def predict_image(file: UploadFile = File(...), conf: Optional[float] = Query(CONF_DEFAULT), imgsz: Optional[int] = Query(IMG_SIZE_DEFAULT)):
136
  """
137
- Upload one image file for prediction.
138
- Returns JSON with detections and annotated image (base64).
 
 
139
  """
140
  try:
141
  img = read_imagefile(file)
@@ -143,56 +147,26 @@ async def predict_image(file: UploadFile = File(...), conf: Optional[float] = Qu
143
  raise HTTPException(status_code=400, detail=f"Invalid image: {e}")
144
 
145
  try:
146
- res, t_ms = run_inference(img, conf=float(conf), imgsz=int(imgsz))
147
  except Exception as e:
148
  raise HTTPException(status_code=500, detail=f"Inference error: {e}")
149
 
150
- detections = boxes_from_result(res)
151
- annotated = annotate_image_with_boxes(img, detections)
152
- return {
153
- "filename": file.filename,
154
- "num_detections": len(detections),
155
- "predictions": detections,
156
- "annotated_image_base64": image_to_base64(annotated),
157
- "inference_time_ms": t_ms
158
- }
159
-
160
- @app.post("/predict/url")
161
- async def predict_url(image_url: str, conf: Optional[float] = Query(CONF_DEFAULT), imgsz: Optional[int] = Query(IMG_SIZE_DEFAULT)):
162
- """
163
- Predict on an image fetched from a URL.
164
- """
165
- try:
166
- img = fetch_image_from_url(image_url)
167
- except Exception as e:
168
- raise HTTPException(status_code=400, detail=str(e))
169
-
170
- try:
171
- res, t_ms = run_inference(img, conf=float(conf), imgsz=int(imgsz))
172
- except Exception as e:
173
- raise HTTPException(status_code=500, detail=f"Inference error: {e}")
174
 
175
- detections = boxes_from_result(res)
176
- annotated = annotate_image_with_boxes(img, detections)
177
  return {
178
- "image_url": image_url,
179
- "num_detections": len(detections),
180
- "predictions": detections,
181
- "annotated_image_base64": image_to_base64(annotated),
182
- "inference_time_ms": t_ms
 
 
 
 
183
  }
184
 
185
- # Optional: admin reload endpoint
186
- @app.get("/model/reload")
187
- def reload_model():
188
- try:
189
- load_model(MODEL_PATH)
190
- return {"status": "reloaded", "model_path": MODEL["path"]}
191
- except Exception as e:
192
- raise HTTPException(status_code=500, detail=str(e))
193
-
194
- # ------------------------
195
- # Run (dev)
196
- # ------------------------
197
  if __name__ == "__main__":
198
- uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
 
1
  # app.py
2
+ import io, os, time, base64
 
 
 
 
3
  from pathlib import Path
4
  from typing import Optional
5
+ import cv2
6
+ import numpy as np
7
  from fastapi import FastAPI, File, UploadFile, Query, HTTPException
8
  from fastapi.responses import JSONResponse
 
 
9
  from ultralytics import YOLO
10
+ import uvicorn
11
 
12
+ # ---------- config ----------
 
 
 
13
  MODEL_PATH = os.getenv("MODEL_PATH", "best_rockfall_model.pt")
14
  CONF_DEFAULT = float(os.getenv("CONF_DEFAULT", 0.25))
15
  IMG_SIZE_DEFAULT = int(os.getenv("IMG_SIZE_DEFAULT", 640))
16
 
17
+ app = FastAPI(title="Rockfall API - mask + bbox + probability")
 
 
 
 
 
 
18
 
19
+ # load model
20
+ MODEL = {"model": None}
21
+ def load_model(path=MODEL_PATH):
22
+ if not Path(path).exists():
23
+ print(f"Warning: model file not found at {path}")
24
+ MODEL["model"] = YOLO(path)
25
+ print("Model loaded:", path)
 
 
26
 
27
  @app.on_event("startup")
28
  def startup():
29
  try:
30
  load_model(MODEL_PATH)
31
  except Exception as e:
 
32
  MODEL["model"] = None
33
+ print("Model load failed:", e)
34
 
35
+ # ---------- helpers ----------
 
 
36
  def read_imagefile(upload_file: UploadFile) -> np.ndarray:
37
  data = upload_file.file.read()
38
  upload_file.file.close()
 
42
  raise ValueError("Cannot decode image")
43
  return img
44
 
45
+ def image_to_datauri_png(img_bgr: np.ndarray) -> str:
46
+ # encode as PNG
47
+ _, buf = cv2.imencode(".png", img_bgr)
48
+ b64 = base64.b64encode(buf).decode("utf-8")
49
+ return f"data:image/png;base64,{b64}"
 
 
 
 
50
 
51
+ def run_predict_and_build_mask(img_bgr: np.ndarray, conf: float, imgsz: int):
52
  if MODEL["model"] is None:
53
+ raise RuntimeError("Model not loaded")
54
+
55
+ # Run the model (ultralytics). Provide numpy BGR image; returns list
56
+ t0 = time.time()
57
  results = MODEL["model"].predict(source=img_bgr, conf=conf, imgsz=imgsz, verbose=False)
58
+ elapsed_ms = (time.time() - t0) * 1000.0
59
+ res = results[0]
60
+
61
+ h, w = img_bgr.shape[:2]
62
+ # Start with empty mask
63
+ mask = np.zeros((h, w), dtype=np.uint8)
64
 
 
 
 
 
 
65
  detections = []
66
+ max_conf = 0.0
67
+ first_bbox = None
68
+
69
+ # If model provides segmentation masks (res.masks), prefer them
70
+ if hasattr(res, "masks") and res.masks is not None and len(res.masks.xy) > 0:
71
+ # res.masks.xy is list of polygons; res.masks.data contains bitmasks depending on version
72
+ # Try to produce a binary mask by rasterizing the polygons
73
+ for poly in res.masks.xy:
74
+ # poly is list of (x,y) floats; draw fill polygon
75
+ pts = np.array(poly, dtype=np.int32).reshape(-1, 2)
76
+ cv2.fillPoly(mask, [pts], 255)
77
+ # get confidences / boxes if available
78
+ if hasattr(res, "boxes") and len(res.boxes) > 0:
79
+ xyxy = res.boxes.xyxy.cpu().numpy()
80
+ confs = res.boxes.conf.cpu().numpy()
81
+ max_conf = float(confs.max()) if len(confs)>0 else 0.0
82
+ first_bbox = list(map(int, xyxy[0]))
83
+ else:
84
+ # No segmentation output -> build mask using bounding boxes
85
+ if hasattr(res, "boxes") and len(res.boxes) > 0:
86
+ xyxy = res.boxes.xyxy.cpu().numpy() # x1,y1,x2,y2
87
+ confs = res.boxes.conf.cpu().numpy()
88
+ max_conf = float(confs.max()) if len(confs)>0 else 0.0
89
+ for i, (b, c) in enumerate(zip(xyxy, confs)):
90
+ x1, y1, x2, y2 = map(int, b)
91
+ # clip
92
+ x1, y1 = max(0,x1), max(0,y1)
93
+ x2, y2 = min(w-1,x2), min(h-1,y2)
94
+ if x2<=x1 or y2<=y1:
95
+ continue
96
+ # fill the rectangle in mask (255 for rockfall)
97
+ cv2.rectangle(mask, (x1,y1), (x2,y2), 255, thickness=-1)
98
+ detections.append({"bbox":[x1,y1,x2,y2], "confidence": float(c)})
99
+ # pick first bbox (if exists)
100
+ if detections:
101
+ first_bbox = detections[0]["bbox"]
102
+ else:
103
+ # no boxes -> empty mask, prob 0
104
+ max_conf = 0.0
105
+
106
+ # compute mask stats
107
+ mask_area = int((mask>0).sum())
108
+ mask_fraction = float(mask_area) / (w*h)
109
+
110
+ return {
111
+ "mask": mask,
112
+ "mask_area": mask_area,
113
+ "mask_fraction": mask_fraction,
114
+ "probability": float(max_conf),
115
+ "bbox": first_bbox,
116
+ "detections": detections,
117
+ "inference_ms": elapsed_ms
118
+ }
119
+
120
+ def annotate_image(img_bgr: np.ndarray, bbox: Optional[list], probability: float):
121
  out = img_bgr.copy()
122
+ if bbox:
123
+ x1,y1,x2,y2 = bbox
124
+ cv2.rectangle(out, (x1,y1), (x2,y2), (0,255,0), 2)
125
+ label = f"rockfall {probability:.2f}"
 
 
 
126
  (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1)
127
+ cv2.rectangle(out, (x1, y1 - th - 6), (x1 + tw + 6, y1), (0,255,0), -1)
128
+ cv2.putText(out, label, (x1+3, y1-4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,0), 1, cv2.LINE_AA)
129
  return out
130
 
131
+ # ---------- endpoints ----------
 
 
 
 
 
 
 
132
  @app.get("/health")
133
  def health():
134
+ return {"status":"ok", "model_loaded": MODEL["model"] is not None}
135
 
136
  @app.post("/predict/image")
137
  async def predict_image(file: UploadFile = File(...), conf: Optional[float] = Query(CONF_DEFAULT), imgsz: Optional[int] = Query(IMG_SIZE_DEFAULT)):
138
  """
139
+ Returns:
140
+ - mask_base64: binary mask PNG as data URI (rockfall=255)
141
+ - annotated_image_base64: annotated image PNG as data URI (bbox + label)
142
+ - mask_area, mask_fraction, probability, bbox (first)
143
  """
144
  try:
145
  img = read_imagefile(file)
 
147
  raise HTTPException(status_code=400, detail=f"Invalid image: {e}")
148
 
149
  try:
150
+ out = run_predict_and_build_mask(img, float(conf), int(imgsz))
151
  except Exception as e:
152
  raise HTTPException(status_code=500, detail=f"Inference error: {e}")
153
 
154
+ mask_png_uri = image_to_datauri_png(out["mask"])
155
+ annotated = annotate_image(img, out["bbox"], out["probability"])
156
+ annotated_png_uri = image_to_datauri_png(annotated)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
 
 
158
  return {
159
+ "filename": file.filename,
160
+ "num_detections": len(out["detections"]),
161
+ "probability": out["probability"],
162
+ "bbox": out["bbox"], # null or [x1,y1,x2,y2]
163
+ "mask_area_pixels": out["mask_area"],
164
+ "mask_area_fraction": out["mask_fraction"],
165
+ "mask_base64": mask_png_uri,
166
+ "annotated_image_base64": annotated_png_uri,
167
+ "inference_time_ms": out["inference_ms"]
168
  }
169
 
170
+ # run
 
 
 
 
 
 
 
 
 
 
 
171
  if __name__ == "__main__":
172
+ uvicorn.run("app:app", host="127.0.0.1", port=8000, reload=True)