intisarhasnain commited on
Commit
4fae684
·
1 Parent(s): 037fe03

align app.py with original run.py: BGR->RGB fix, lower wall threshold

Browse files
Files changed (1) hide show
  1. app.py +194 -119
app.py CHANGED
@@ -1,171 +1,246 @@
 
 
1
  import numpy as np
 
2
  import cv2
3
- import sys
4
- import os
5
  import base64
6
- import io
7
- from fastapi import FastAPI, File, UploadFile
8
- from fastapi.responses import HTMLResponse, JSONResponse
9
- from PIL import Image
10
  import uvicorn
 
 
 
 
11
 
12
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "mmdetection"))
13
- from mmdet.apis import init_detector, inference_detector
14
 
15
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
16
- CONFIG_FILE = os.path.join(BASE_DIR, "configs", "faster_rcnn.py")
17
- WEIGHTS_FILE = os.path.join(BASE_DIR, "weights", "faster_rcnn_latest.pth")
18
- DEVICE = "cpu"
19
- CLASS_NAMES = ["wall", "room"]
20
- CLASS_COLORS = {"wall": (60, 60, 220), "room": (50, 200, 80)}
21
- SCORE_THRESH = 0.4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- print("Loading model…")
24
- model = init_detector(CONFIG_FILE, WEIGHTS_FILE, device=DEVICE)
25
- print("Model ready.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
 
 
 
27
  app = FastAPI()
28
 
29
- HTML = """
30
- <!DOCTYPE html>
31
  <html>
32
  <head>
33
  <title>Floor Plan Detection</title>
34
  <style>
35
- body { font-family: monospace; max-width: 960px; margin: 40px auto; padding: 0 20px; background: #0f0f0f; color: #e0e0e0; }
36
- h1 { color: #7eb8f7; }
37
- .row { display: flex; gap: 24px; flex-wrap: wrap; }
38
- .col { flex: 1; min-width: 300px; }
39
- img { max-width: 100%; border: 1px solid #333; border-radius: 6px; }
40
- input[type=file] { display: none; }
41
- label.upload { display: inline-block; padding: 10px 20px; background: #1e3a5f; color: #7eb8f7;
42
- border: 1px solid #7eb8f7; border-radius: 4px; cursor: pointer; }
43
- label.upload:hover { background: #2a4f7f; }
44
- button { padding: 10px 28px; background: #7eb8f7; color: #0f0f0f; border: none;
45
- border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 1rem; }
46
- button:hover { background: #5a9ee0; }
47
- #preview, #result { width: 100%; min-height: 200px; background: #1a1a1a;
48
- border: 1px solid #333; border-radius: 6px; display: flex;
49
- align-items: center; justify-content: center; color: #555; }
50
- #summary { margin-top: 12px; background: #1a1a1a; padding: 12px; border-radius: 6px;
51
- border: 1px solid #333; white-space: pre-wrap; min-height: 60px; font-size: 0.9rem; }
52
- .legend span { display: inline-block; width: 12px; height: 12px; margin-right: 4px; border-radius: 2px; }
53
- .loading { color: #7eb8f7; }
 
 
 
 
54
  </style>
55
  </head>
56
  <body>
57
  <h1>🏠 Floor Plan Detection</h1>
58
- <p>Upload a floor plan image to detect <strong>walls</strong> and <strong>rooms</strong>
59
- using Faster R-CNN fine-tuned on CubiCasa5k.</p>
60
-
61
- <div style="margin-bottom:16px; display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
62
- <label class="upload" for="fileInput">📂 Choose Image</label>
63
- <input type="file" id="fileInput" accept="image/*">
64
- <button onclick="runDetection()"> Run Detection</button>
65
- <span id="filename" style="color:#555">No file chosen</span>
66
  </div>
67
 
68
  <div class="row">
69
  <div class="col">
70
- <p style="color:#888; margin:0 0 6px">Input</p>
71
- <div id="preview">No image loaded</div>
72
  </div>
73
  <div class="col">
74
- <p style="color:#888; margin:0 0 6px">Detections</p>
75
- <div id="result">Run detection to see results</div>
76
  </div>
77
  </div>
78
 
79
  <div id="summary">Upload an image and click Run Detection.</div>
80
 
81
- <p class="legend" style="margin-top:16px">
82
- <strong>Legend:</strong>
83
- <span style="background:#3c3cdc"></span>Wall &nbsp;
84
- <span style="background:#32c850"></span>Room
85
- </p>
86
 
87
  <script>
88
- let selectedFile = null;
89
-
90
- document.getElementById('fileInput').addEventListener('change', function(e) {
91
- selectedFile = e.target.files[0];
92
- if (!selectedFile) return;
93
- document.getElementById('filename').textContent = selectedFile.name;
94
- const reader = new FileReader();
95
- reader.onload = ev => {
96
- document.getElementById('preview').innerHTML = `<img src="${ev.target.result}">`;
97
- };
98
- reader.readAsDataURL(selectedFile);
99
  });
100
 
101
- async function runDetection() {
102
- if (!selectedFile) { alert('Please choose an image first.'); return; }
103
- document.getElementById('result').innerHTML = '<span class="loading">Running detection… (may take 30–60s on CPU)</span>';
104
  document.getElementById('summary').textContent = 'Processing…';
105
-
106
- const form = new FormData();
107
- form.append('file', selectedFile);
108
-
109
  try {
110
- const resp = await fetch('/detect', { method: 'POST', body: form });
111
- const data = await resp.json();
112
- document.getElementById('result').innerHTML = `<img src="data:image/jpeg;base64,${data.image}">`;
113
- document.getElementById('summary').textContent = data.summary;
114
- } catch(err) {
115
- document.getElementById('result').innerHTML = 'Error — see console.';
116
- document.getElementById('summary').textContent = String(err);
 
 
 
 
 
 
 
117
  }
118
  }
119
  </script>
120
  </body>
121
- </html>
122
- """
123
 
124
  @app.get("/", response_class=HTMLResponse)
125
  def index():
126
  return HTML
127
 
128
  @app.post("/detect")
129
- async def detect(file: UploadFile = File(...)):
130
- contents = await file.read()
131
- arr = np.frombuffer(contents, np.uint8)
132
- bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
133
- if bgr is None:
134
- return JSONResponse({"error": "Could not decode image"}, status_code=400)
135
-
136
- result = inference_detector(model, bgr)
137
- annotated = bgr.copy()
138
- lines = []
139
- counts = {"wall": 0, "room": 0}
140
-
141
- pred = result.pred_instances
142
- bboxes = pred.bboxes.cpu().numpy()
143
- scores = pred.scores.cpu().numpy()
144
- labels = pred.labels.cpu().numpy()
145
-
146
- for bbox, score, label in zip(bboxes, scores, labels):
147
- if score < SCORE_THRESH or label >= len(CLASS_NAMES):
148
- continue
149
- name = CLASS_NAMES[label]
150
- color = CLASS_COLORS[name]
151
- x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
152
- overlay = annotated.copy()
153
- cv2.rectangle(overlay, (x1, y1), (x2, y2), color, -1)
154
- cv2.addWeighted(overlay, 0.15, annotated, 0.85, 0, annotated)
155
- cv2.rectangle(annotated, (x1, y1), (x2, y2), color, 2)
156
- lbl = f"{name} {score:.2f}"
157
- (tw, th), _ = cv2.getTextSize(lbl, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
158
- cv2.rectangle(annotated, (x1, y1-th-6), (x1+tw+4, y1), color, -1)
159
- cv2.putText(annotated, lbl, (x1+2, y1-4), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1)
160
- counts[name] += 1
161
- lines.append(f" • {name.capitalize()} [{x1},{y1} → {x2},{y2}] conf={score:.3f}")
162
 
163
- _, buf = cv2.imencode(".jpg", annotated, [cv2.IMWRITE_JPEG_QUALITY, 90])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  b64 = base64.b64encode(buf).decode()
165
- summary = f"Detected: {counts['wall']} wall(s) | {counts['room']} room(s) (threshold >= {SCORE_THRESH})\n\n"
166
- summary += "\n".join(lines) if lines else "No detections above threshold."
167
 
168
- return {"image": b64, "summary": summary}
 
 
 
169
 
170
  if __name__ == "__main__":
171
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ import os
2
+ import json
3
  import numpy as np
4
+ from typing import Dict, Any
5
  import cv2
6
+ import torch
7
+ import logging
8
  import base64
 
 
 
 
9
  import uvicorn
10
+ from fastapi import FastAPI, File, UploadFile, HTTPException
11
+ from fastapi.responses import HTMLResponse, JSONResponse
12
+ from mmdet.apis import init_detector, inference_detector
13
+ import sys
14
 
15
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), "mmdetection"))
 
16
 
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
21
+ CONFIG_FILE = os.path.join(BASE_DIR, "configs", "faster_rcnn.py")
22
+ CHECKPOINT_FILE = os.path.join(BASE_DIR, "weights", "faster_rcnn_latest.pth")
23
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
24
+ SCORE_THRESH = 0.3 # lower than default to catch more walls
25
+
26
+ CLASS_COLORS = {
27
+ 0: (220, 60, 60), # wall — red (RGB)
28
+ 1: (50, 200, 80), # room — green (RGB)
29
+ }
30
+ CLASS_NAMES = {0: "wall", 1: "room"}
31
+
32
+ # ── Device ───────────────────────────────────────────────────────────────────
33
+ def determine_device():
34
+ if torch.cuda.is_available():
35
+ try:
36
+ torch.cuda.init()
37
+ return "cuda:0"
38
+ except Exception as e:
39
+ logger.warning(f"CUDA failed: {e}. Using CPU.")
40
+ return "cpu"
41
+
42
+ # ── Model load ───────────────────────────────────────────────────────────────
43
+ device = determine_device()
44
+ logger.info(f"Loading Faster R-CNN on {device}…")
45
+ model = init_detector(CONFIG_FILE, CHECKPOINT_FILE, device=device)
46
+ logger.info("Model ready.")
47
+
48
+ # ── Result processing (mirrors original run.py exactly) ──────────────────────
49
+ def process_inference_result(result) -> Dict[str, Any]:
50
+ bboxes = result.pred_instances.bboxes.cpu().numpy()
51
+ labels = result.pred_instances.labels.cpu().numpy()
52
+ scores = result.pred_instances.scores.cpu().numpy()
53
+
54
+ walls, rooms = [], []
55
+ for i, (bbox, label, score) in enumerate(zip(bboxes, labels, scores)):
56
+ if score < SCORE_THRESH:
57
+ continue
58
+ x1, y1, x2, y2 = bbox
59
+ item = {
60
+ "id": f"{'wall' if label == 0 else 'room'}_{i+1}",
61
+ "position": {
62
+ "start": {"x": float(x1), "y": float(y1)},
63
+ "end": {"x": float(x2), "y": float(y2)}
64
+ },
65
+ "confidence": float(score)
66
+ }
67
+ if label == 0:
68
+ walls.append(item)
69
+ else:
70
+ rooms.append(item)
71
+
72
+ all_scores = scores[scores >= SCORE_THRESH]
73
+ return {
74
+ "type": "floor_plan",
75
+ "confidence": float(np.mean(all_scores)) if len(all_scores) else 0.0,
76
+ "detectionResults": {"walls": walls, "rooms": rooms}
77
+ }
78
+
79
+ # ── Visualisation ─────────────────────────────────────────────────────────────
80
+ def draw_detections(img_rgb: np.ndarray, result) -> np.ndarray:
81
+ annotated = img_rgb.copy()
82
+ bboxes = result.pred_instances.bboxes.cpu().numpy()
83
+ labels = result.pred_instances.labels.cpu().numpy()
84
+ scores = result.pred_instances.scores.cpu().numpy()
85
 
86
+ for bbox, label, score in zip(bboxes, labels, scores):
87
+ if score < SCORE_THRESH or label not in CLASS_NAMES:
88
+ continue
89
+ color = CLASS_COLORS[label]
90
+ name = CLASS_NAMES[label]
91
+ x1, y1, x2, y2 = int(bbox[0]), int(bbox[1]), int(bbox[2]), int(bbox[3])
92
+
93
+ # Semi-transparent fill
94
+ overlay = annotated.copy()
95
+ cv2.rectangle(overlay, (x1, y1), (x2, y2), color, -1)
96
+ cv2.addWeighted(overlay, 0.15, annotated, 0.85, 0, annotated)
97
+ # Border
98
+ cv2.rectangle(annotated, (x1, y1), (x2, y2), color, 2)
99
+ # Label
100
+ lbl = f"{name} {score:.2f}"
101
+ (tw, th), _ = cv2.getTextSize(lbl, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
102
+ cv2.rectangle(annotated, (x1, y1-th-6), (x1+tw+4, y1), color, -1)
103
+ cv2.putText(annotated, lbl, (x1+2, y1-4),
104
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
105
 
106
+ return annotated
107
+
108
+ # ── FastAPI ───────────────────────────────────────────────────────────────────
109
  app = FastAPI()
110
 
111
+ HTML = """<!DOCTYPE html>
 
112
  <html>
113
  <head>
114
  <title>Floor Plan Detection</title>
115
  <style>
116
+ *{box-sizing:border-box;margin:0;padding:0}
117
+ body{font-family:monospace;background:#0f0f0f;color:#e0e0e0;padding:32px 24px}
118
+ h1{color:#7eb8f7;margin-bottom:8px}
119
+ p.sub{color:#888;margin-bottom:24px;font-size:.9rem}
120
+ .controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:24px}
121
+ input[type=file]{display:none}
122
+ label.btn{padding:9px 18px;background:#1e3a5f;color:#7eb8f7;border:1px solid #7eb8f7;border-radius:4px;cursor:pointer}
123
+ label.btn:hover{background:#2a4f7f}
124
+ button{padding:9px 24px;background:#7eb8f7;color:#0f0f0f;border:none;border-radius:4px;cursor:pointer;font-weight:bold;font-size:.95rem}
125
+ button:hover{background:#5a9ee0}
126
+ #fname{color:#555;font-size:.85rem}
127
+ .row{display:flex;gap:20px;flex-wrap:wrap;margin-bottom:16px}
128
+ .col{flex:1;min-width:280px}
129
+ .col p{color:#888;font-size:.8rem;margin-bottom:6px}
130
+ .imgbox{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:6px;min-height:220px;
131
+ display:flex;align-items:center;justify-content:center;color:#444;overflow:hidden}
132
+ .imgbox img{max-width:100%;display:block}
133
+ #summary{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:6px;padding:14px;
134
+ white-space:pre-wrap;font-size:.85rem;min-height:60px;color:#ccc}
135
+ .legend{margin-top:12px;font-size:.85rem;color:#888}
136
+ .dot{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:4px;vertical-align:middle}
137
+ .loading{color:#7eb8f7;animation:pulse 1.2s infinite}
138
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
139
  </style>
140
  </head>
141
  <body>
142
  <h1>🏠 Floor Plan Detection</h1>
143
+ <p class="sub">Faster R-CNN · ResNet-101 · FPN · fine-tuned on CubiCasa5k</p>
144
+
145
+ <div class="controls">
146
+ <label class="btn" for="fi">📂 Choose Image</label>
147
+ <input type="file" id="fi" accept="image/jpeg,image/png">
148
+ <button onclick="detect()">▶ Run Detection</button>
149
+ <span id="fname">No file chosen</span>
 
150
  </div>
151
 
152
  <div class="row">
153
  <div class="col">
154
+ <p>Input</p>
155
+ <div class="imgbox" id="preview">No image loaded</div>
156
  </div>
157
  <div class="col">
158
+ <p>Detections</p>
159
+ <div class="imgbox" id="result">Run detection to see results</div>
160
  </div>
161
  </div>
162
 
163
  <div id="summary">Upload an image and click Run Detection.</div>
164
 
165
+ <div class="legend">
166
+ <span class="dot" style="background:#dc3c3c"></span>Wall &nbsp;
167
+ <span class="dot" style="background:#32c850"></span>Room
168
+ </div>
 
169
 
170
  <script>
171
+ let file = null;
172
+ document.getElementById('fi').addEventListener('change', e => {
173
+ file = e.target.files[0];
174
+ if (!file) return;
175
+ document.getElementById('fname').textContent = file.name;
176
+ const r = new FileReader();
177
+ r.onload = ev => document.getElementById('preview').innerHTML = `<img src="${ev.target.result}">`;
178
+ r.readAsDataURL(file);
 
 
 
179
  });
180
 
181
+ async function detect() {
182
+ if (!file) { alert('Choose an image first.'); return; }
183
+ document.getElementById('result').innerHTML = '<span class="loading">Running… (30–60s on CPU)</span>';
184
  document.getElementById('summary').textContent = 'Processing…';
185
+ const fd = new FormData();
186
+ fd.append('image', file);
 
 
187
  try {
188
+ const r = await fetch('/detect', {method:'POST', body:fd});
189
+ const d = await r.json();
190
+ if (d.error) { document.getElementById('result').innerHTML = 'Error'; document.getElementById('summary').textContent = d.error; return; }
191
+ document.getElementById('result').innerHTML = `<img src="data:image/jpeg;base64,${d.image}">`;
192
+ const w = d.json.detectionResults.walls.length;
193
+ const rm = d.json.detectionResults.rooms.length;
194
+ let txt = `Detected: ${w} wall(s) | ${rm} room(s) (conf threshold: 0.30)\n`;
195
+ txt += `Overall confidence: ${(d.json.confidence*100).toFixed(1)}%\n\n`;
196
+ d.json.detectionResults.walls.forEach(x => txt += ` • Wall ${x.id} conf=${x.confidence.toFixed(3)}\n`);
197
+ d.json.detectionResults.rooms.forEach(x => txt += ` • Room ${x.id} conf=${x.confidence.toFixed(3)}\n`);
198
+ document.getElementById('summary').textContent = txt;
199
+ } catch(e) {
200
+ document.getElementById('result').innerHTML = 'Error';
201
+ document.getElementById('summary').textContent = String(e);
202
  }
203
  }
204
  </script>
205
  </body>
206
+ </html>"""
 
207
 
208
  @app.get("/", response_class=HTMLResponse)
209
  def index():
210
  return HTML
211
 
212
  @app.post("/detect")
213
+ async def detect(image: UploadFile = File(...)):
214
+ if image.content_type not in ["image/jpeg", "image/png"]:
215
+ raise HTTPException(status_code=400, detail="Only JPEG and PNG supported.")
216
+
217
+ contents = await image.read()
218
+ if len(contents) > MAX_FILE_SIZE:
219
+ raise HTTPException(status_code=400, detail="File exceeds 10 MB limit.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
+ nparr = np.frombuffer(contents, np.uint8)
222
+ img_bgr = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
223
+ if img_bgr is None:
224
+ raise HTTPException(status_code=400, detail="Could not decode image.")
225
+
226
+ # Original run.py converts BGR→RGB before inference
227
+ img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
228
+
229
+ result = inference_detector(model, img_rgb)
230
+
231
+ # JSON output — matches original run.py exactly
232
+ processed = process_inference_result(result)
233
+
234
+ # Visual output — draw on RGB image, encode as JPEG
235
+ annotated_rgb = draw_detections(img_rgb, result)
236
+ annotated_bgr = cv2.cvtColor(annotated_rgb, cv2.COLOR_RGB2BGR)
237
+ _, buf = cv2.imencode(".jpg", annotated_bgr, [cv2.IMWRITE_JPEG_QUALITY, 90])
238
  b64 = base64.b64encode(buf).decode()
 
 
239
 
240
+ logger.info(f"Inference done: {len(processed['detectionResults']['walls'])} walls, "
241
+ f"{len(processed['detectionResults']['rooms'])} rooms")
242
+
243
+ return {"image": b64, "json": processed}
244
 
245
  if __name__ == "__main__":
246
  uvicorn.run(app, host="0.0.0.0", port=7860)