newtechdevng commited on
Commit
6dda80b
·
verified ·
1 Parent(s): 4bb1cf7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -166
app.py CHANGED
@@ -1,190 +1,141 @@
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 = "newtechdevng/construction_detection_fine_tune"
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
- })
 
1
+ from fastapi import FastAPI, File, UploadFile, Form
 
 
 
 
 
2
  from fastapi.middleware.cors import CORSMiddleware
 
3
  from huggingface_hub import hf_hub_download
4
  from ultralytics import YOLO
5
+ import cv2
6
  import numpy as np
7
+ import base64
8
+ import time
9
+ import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ app = FastAPI(title="Construction Detection API")
 
 
 
 
 
12
 
13
  app.add_middleware(
14
  CORSMiddleware,
15
+ allow_origins=["*"],
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
  )
19
 
20
+ # Load YOLO model
21
+ HF_REPO_ID = "newtechdevng/construction_detection_fine_tune"
22
+ MODEL_FILE = "best_v2_finetune.pt"
23
+ model_path = hf_hub_download(repo_id=HF_REPO_ID, filename=MODEL_FILE)
24
+ model = YOLO(model_path)
25
+
26
+ # ArUco setup
27
+ ARUCO_DICT = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
28
+ ARUCO_PARAMS = cv2.aruco.DetectorParameters()
29
+ ARUCO_DETECTOR = cv2.aruco.ArucoDetector(ARUCO_DICT, ARUCO_PARAMS)
30
+
31
+ CLASS_COLORS = {
32
+ "beam": (255, 100, 0),
33
+ "column": ( 0, 255, 255),
34
+ "door": (255, 0, 255),
35
+ "floor": ( 0, 255, 0),
36
+ "stairs": (255, 255, 0),
37
+ "wall": ( 0, 100, 255),
38
+ "window": (100, 0, 255),
39
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ def detect_aruco_scale(img, marker_size_cm=10.0):
42
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
43
+ corners, ids, _ = ARUCO_DETECTOR.detectMarkers(gray)
44
+ if ids is None:
45
+ return None, None
46
+ # Use first detected marker
47
+ marker_corners = corners[0][0]
48
+ w_px = np.linalg.norm(marker_corners[0] - marker_corners[1])
49
+ h_px = np.linalg.norm(marker_corners[1] - marker_corners[2])
50
+ pixels_per_cm = (w_px + h_px) / 2 / marker_size_cm
51
+ return pixels_per_cm, corners
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  @app.get("/")
54
+ def root():
55
  return {
56
+ "model": MODEL_FILE,
57
+ "classes": list(CLASS_COLORS.keys()),
58
+ "calibration": "Auto via ArUco marker on hard hat (10cm × 10cm)",
59
  "endpoints": {
60
+ "POST /detect": "Send image → get detections in cm (if hard hat in frame)",
61
+ "GET /health": "Health check"
 
62
  }
63
  }
64
 
65
  @app.get("/health")
66
+ def health():
67
+ return {"status": "ok", "model": MODEL_FILE}
68
+
69
+ @app.post("/detect")
70
+ async def detect(
71
+ file: UploadFile = File(...),
72
+ marker_size_cm: float = Form(10.0),
73
+ confidence: float = Form(0.4)
 
 
 
 
74
  ):
75
+ start = time.time()
76
+
77
+ contents = await file.read()
78
+ nparr = np.frombuffer(contents, np.uint8)
79
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
80
+
81
+ # Try ArUco auto-calibration
82
+ pixels_per_cm, aruco_corners = detect_aruco_scale(img, marker_size_cm)
83
+ calibrated = pixels_per_cm is not None
84
+
85
+ # Draw ArUco marker highlight
86
+ if calibrated:
87
+ cv2.aruco.drawDetectedMarkers(img, aruco_corners)
88
+
89
+ # Run YOLO
90
+ results = model(img, conf=confidence)[0]
91
+ detections = []
92
+
93
+ for box in results.boxes:
94
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
95
+ cls = results.names[int(box.cls[0])]
96
+ conf = round(float(box.conf[0]), 3)
97
+ w_px = x2 - x1
98
+ h_px = y2 - y1
99
+ color = CLASS_COLORS.get(cls, (0, 255, 0))
100
+
101
+ w_cm = round(w_px / pixels_per_cm, 1) if calibrated else None
102
+ h_cm = round(h_px / pixels_per_cm, 1) if calibrated else None
103
+
104
+ # Draw bounding box
105
+ cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
106
+
107
+ # Label with cm if calibrated
108
+ label = f"{cls} {conf:.2f}"
109
+ if calibrated:
110
+ label += f" | {w_cm}x{h_cm}cm"
111
+ cv2.putText(img, label, (x1, y1 - 8),
112
+ cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 2)
113
+
114
+ detections.append({
115
+ "class": cls,
116
+ "confidence": conf,
117
+ "bbox": [x1, y1, x2, y2],
118
+ "width_px": w_px,
119
+ "height_px": h_px,
120
+ "width_cm": w_cm,
121
+ "height_cm": h_cm,
122
+ })
123
+
124
+ # Encode result image
125
+ _, buf = cv2.imencode(".jpg", img)
126
+ img_b64 = base64.b64encode(buf).decode()
127
 
128
  return {
129
+ "success": True,
130
+ "calibrated": calibrated,
131
+ "pixels_per_cm": round(pixels_per_cm, 2) if calibrated else None,
132
+ "marker_size_cm": marker_size_cm,
133
+ "inference_time_s": round(time.time() - start, 3),
134
+ "total": len(detections),
135
+ "detections": detections,
136
+ "image_base64": img_b64,
137
  }
138
 
139
+ if __name__ == "__main__":
140
+ import uvicorn
141
+ uvicorn.run(app, host="0.0.0.0", port=7860)