newtechdevng commited on
Commit
c97b6b6
Β·
verified Β·
1 Parent(s): b5bc540

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +190 -0
app.py CHANGED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Construction Detection API β€” Hugging Face Space
3
+ Loads model from HF Hub, serves REST API for mobile app
4
+ """
5
+
6
+ from fastapi import FastAPI, File, UploadFile, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import JSONResponse
9
+ from huggingface_hub import hf_hub_download
10
+ from ultralytics import YOLO
11
+ import numpy as np
12
+ import cv2, base64, time, os
13
+
14
+ # ── CONFIG ──────────────────────────────────────────────────────────────────
15
+ HF_REPO_ID = "dipangshuborah/construction-detection-yolov8"
16
+ MODEL_FILE = "best_v2_finetune.pt"
17
+ CONF = 0.25
18
+ IOU = 0.45
19
+
20
+ COLORS = {
21
+ "beam": [255, 0, 0 ],
22
+ "column": [0, 255, 255],
23
+ "door": [255, 0, 255],
24
+ "floor": [0, 165, 255],
25
+ "stairs": [0, 255, 0 ],
26
+ "wall": [255, 255, 0 ],
27
+ "window": [0, 0, 255],
28
+ }
29
+
30
+ # ── APP ─────────────────────────────────────────────────────────────────────
31
+ app = FastAPI(
32
+ title = "Construction Detection API",
33
+ description = "Detects construction elements and measures dimensions",
34
+ version = "1.0.0",
35
+ )
36
+
37
+ app.add_middleware(
38
+ CORSMiddleware,
39
+ allow_origins = ["*"],
40
+ allow_methods = ["*"],
41
+ allow_headers = ["*"],
42
+ )
43
+
44
+ # ── GLOBAL STATE ─────────────────────────────────────────────────────────────
45
+ model = None
46
+ pixels_per_cm = None
47
+
48
+ # ── STARTUP ──────────────────────────────────────────────────────────────────
49
+ @app.on_event("startup")
50
+ async def load_model():
51
+ global model
52
+ print(f"Downloading {MODEL_FILE} from {HF_REPO_ID}...")
53
+ path = hf_hub_download(repo_id=HF_REPO_ID, filename=MODEL_FILE)
54
+ model = YOLO(path)
55
+ print("βœ… Model loaded!")
56
+
57
+ # ── HELPERS ──────────────────────────────────────────────────────────────────
58
+ def bytes_to_image(data: bytes) -> np.ndarray:
59
+ arr = np.frombuffer(data, np.uint8)
60
+ return cv2.imdecode(arr, cv2.IMREAD_COLOR)
61
+
62
+ def image_to_base64(img: np.ndarray) -> str:
63
+ _, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 85])
64
+ return base64.b64encode(buf).decode("utf-8")
65
+
66
+ def px_to_cm(pixels: float) -> float | None:
67
+ if pixels_per_cm is None:
68
+ return None
69
+ return round(pixels / pixels_per_cm, 1)
70
+
71
+ def draw_boxes(img: np.ndarray, detections: list) -> np.ndarray:
72
+ for det in detections:
73
+ x1, y1, x2, y2 = det["bbox"]
74
+ cls = det["class"]
75
+ conf = det["confidence"]
76
+ color = COLORS.get(cls, [255, 255, 255])
77
+
78
+ cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
79
+
80
+ if det.get("width_cm"):
81
+ label = f"{cls} {conf:.2f} W:{det['width_cm']}cm H:{det['height_cm']}cm"
82
+ else:
83
+ label = f"{cls} {conf:.2f} W:{det['width_px']}px H:{det['height_px']}px"
84
+
85
+ (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
86
+ cv2.rectangle(img, (x1, y1 - th - 8), (x1 + tw + 4, y1), color, -1)
87
+ cv2.putText(img, label, (x1 + 2, y1 - 5),
88
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
89
+ return img
90
+
91
+ def run_detection(img: np.ndarray) -> list:
92
+ results = model.predict(img, conf=CONF, iou=IOU, task="detect", verbose=False)
93
+ detections = []
94
+ for result in results:
95
+ for box in result.boxes:
96
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
97
+ cls = model.names[int(box.cls)]
98
+ conf = round(float(box.conf), 3)
99
+ w_px = x2 - x1
100
+ h_px = y2 - y1
101
+ detections.append({
102
+ "class": cls,
103
+ "confidence": conf,
104
+ "bbox": [x1, y1, x2, y2],
105
+ "width_px": w_px,
106
+ "height_px": h_px,
107
+ "width_cm": px_to_cm(w_px),
108
+ "height_cm": px_to_cm(h_px),
109
+ "color": COLORS.get(cls, [255, 255, 255]),
110
+ })
111
+ return detections
112
+
113
+ # ── ROUTES ───────────────────────────────────────────────────────────────────
114
+ @app.get("/")
115
+ async def root():
116
+ return {
117
+ "status": "running",
118
+ "model": MODEL_FILE,
119
+ "classes": list(COLORS.keys()),
120
+ "endpoints": {
121
+ "POST /detect": "Upload image β†’ detections + dimensions",
122
+ "POST /calibrate": "Set reference object for real-world units",
123
+ "GET /health": "Health check",
124
+ }
125
+ }
126
+
127
+ @app.get("/health")
128
+ async def health():
129
+ return {"status": "ok", "model_loaded": model is not None}
130
+
131
+ @app.post("/calibrate")
132
+ async def calibrate(
133
+ file: UploadFile = File(...),
134
+ bbox_x1: int = 0,
135
+ bbox_y1: int = 0,
136
+ bbox_x2: int = 210,
137
+ bbox_y2: int = 297,
138
+ real_width: float = 21.0,
139
+ real_height: float = 29.7,
140
+ ):
141
+ """
142
+ Calibrate using a reference object (e.g. A4 paper = 21cm x 29.7cm).
143
+ Provide bounding box of the reference object in pixels.
144
+ """
145
+ global pixels_per_cm
146
+ data = await file.read()
147
+ img = bytes_to_image(data)
148
+ if img is None:
149
+ raise HTTPException(400, "Invalid image")
150
+
151
+ ref_px_w = bbox_x2 - bbox_x1
152
+ ref_px_h = bbox_y2 - bbox_y1
153
+ px_per_w = ref_px_w / real_width
154
+ px_per_h = ref_px_h / real_height
155
+ pixels_per_cm = round((px_per_w + px_per_h) / 2, 4)
156
+
157
+ return {
158
+ "message": "βœ… Calibration successful",
159
+ "pixels_per_cm": pixels_per_cm,
160
+ }
161
+
162
+ @app.post("/detect")
163
+ async def detect(file: UploadFile = File(...)):
164
+ """
165
+ Upload a construction site image.
166
+ Returns all detected objects with bounding boxes and dimensions.
167
+ """
168
+ if model is None:
169
+ raise HTTPException(503, "Model not loaded")
170
+
171
+ data = await file.read()
172
+ img = bytes_to_image(data)
173
+ if img is None:
174
+ raise HTTPException(400, "Invalid image")
175
+
176
+ start = time.time()
177
+ detections = run_detection(img)
178
+ elapsed = round(time.time() - start, 3)
179
+
180
+ annotated = draw_boxes(img.copy(), detections)
181
+ img_b64 = image_to_base64(annotated)
182
+
183
+ return JSONResponse({
184
+ "success": True,
185
+ "total": len(detections),
186
+ "inference_time_s": elapsed,
187
+ "calibrated": pixels_per_cm is not None,
188
+ "image_base64": img_b64,
189
+ "detections": detections,
190
+ })