Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,12 +8,6 @@ import json
|
|
| 8 |
import numpy as np
|
| 9 |
from huggingface_hub import hf_hub_download
|
| 10 |
|
| 11 |
-
|
| 12 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
-
# Model definition β must match the architecture used during training
|
| 14 |
-
# Output: (B, 8) β 4 corners (x,y) normalised to [0, 1], order TL TR BR BL
|
| 15 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 16 |
-
|
| 17 |
class ParkingSpaceDetector(nn.Module):
|
| 18 |
def __init__(self):
|
| 19 |
super().__init__()
|
|
@@ -21,488 +15,141 @@ class ParkingSpaceDetector(nn.Module):
|
|
| 21 |
self.features = backbone.features
|
| 22 |
self.pool = nn.AdaptiveAvgPool2d(1)
|
| 23 |
self.head = nn.Sequential(
|
| 24 |
-
nn.Dropout(0.3),
|
| 25 |
-
nn.
|
| 26 |
-
nn.
|
| 27 |
-
nn.Dropout(0.2),
|
| 28 |
-
nn.Linear(256, 8),
|
| 29 |
-
nn.Sigmoid(),
|
| 30 |
)
|
| 31 |
-
|
| 32 |
def forward(self, x):
|
| 33 |
return self.head(self.pool(self.features(x)).flatten(1))
|
| 34 |
|
| 35 |
-
|
| 36 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
-
# Model loading
|
| 38 |
-
# Repo: UmeshAdabala/RectArea_Parkospace (model repo type)
|
| 39 |
-
# File: best.pt β plain OrderedDict state_dict
|
| 40 |
-
# (saved with torch.save(model.state_dict(), "best.pt"))
|
| 41 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 42 |
-
|
| 43 |
MODEL = None
|
| 44 |
-
|
| 45 |
-
|
| 46 |
def get_model():
|
| 47 |
global MODEL
|
| 48 |
-
if MODEL
|
| 49 |
-
return MODEL
|
| 50 |
-
|
| 51 |
-
print("Loading model from HuggingFace Hub ...")
|
| 52 |
try:
|
| 53 |
-
path = hf_hub_download(
|
| 54 |
-
repo_id="UmeshAdabala/RectArea_Parkospace",
|
| 55 |
-
filename="best.pt",
|
| 56 |
-
repo_type="model",
|
| 57 |
-
)
|
| 58 |
-
ckpt = torch.load(path, map_location="cpu")
|
| 59 |
-
|
| 60 |
m = ParkingSpaceDetector()
|
| 61 |
-
|
| 62 |
if isinstance(ckpt, dict):
|
| 63 |
-
|
| 64 |
-
if
|
| 65 |
-
state = ckpt["model_state"] # actual format in this repo
|
| 66 |
-
elif "model_state_dict" in ckpt:
|
| 67 |
-
state = ckpt["model_state_dict"]
|
| 68 |
-
elif "state_dict" in ckpt:
|
| 69 |
-
state = ckpt["state_dict"]
|
| 70 |
-
else:
|
| 71 |
-
state = ckpt # bare flat state_dict
|
| 72 |
-
m.load_state_dict(state, strict=True)
|
| 73 |
-
m.eval()
|
| 74 |
-
MODEL = m
|
| 75 |
else:
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
MODEL = ckpt
|
| 79 |
-
|
| 80 |
-
print("Model loaded successfully.")
|
| 81 |
-
|
| 82 |
except Exception as e:
|
| 83 |
-
print(f"Model load error: {e}
|
| 84 |
MODEL = ParkingSpaceDetector().eval()
|
| 85 |
-
|
| 86 |
return MODEL
|
| 87 |
|
| 88 |
-
|
| 89 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 90 |
-
# Image preprocessing
|
| 91 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 92 |
-
|
| 93 |
TRANSFORM = T.Compose([
|
| 94 |
-
T.Resize((224, 224)),
|
| 95 |
-
T.
|
| 96 |
-
T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
|
| 97 |
])
|
| 98 |
|
| 99 |
-
|
| 100 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
-
# Inference helpers
|
| 102 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββββββββββββββββ
|
| 103 |
-
|
| 104 |
-
def run_detection(image: Image.Image):
|
| 105 |
-
"""Run model on image, return pixel-space corners as JSON string."""
|
| 106 |
if image is None:
|
| 107 |
-
return None, "Upload a parking space photo first."
|
| 108 |
-
|
| 109 |
-
img = image.convert("RGB")
|
| 110 |
-
W, H = img.size
|
| 111 |
-
tensor = TRANSFORM(img).unsqueeze(0) # (1, 3, 224, 224)
|
| 112 |
-
|
| 113 |
-
with torch.no_grad():
|
| 114 |
-
raw = get_model()(tensor)[0].tolist() # 8 values in [0, 1]
|
| 115 |
-
|
| 116 |
-
# corners TL TR BR BL β normalised β pixel
|
| 117 |
-
corners_px = [[raw[i] * W, raw[i + 1] * H] for i in range(0, 8, 2)]
|
| 118 |
-
payload = json.dumps({"corners": corners_px, "width": W, "height": H})
|
| 119 |
-
return payload, ""
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def compute_area_md(corners_json: str) -> str:
|
| 123 |
-
"""Estimate dimensions + pricing from pixel-space corners JSON."""
|
| 124 |
-
if not corners_json:
|
| 125 |
-
return ""
|
| 126 |
try:
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
L = 5.0 if asp >= 1 else round(min(max(2.5 / asp, 1), 15), 2)
|
| 136 |
-
B = round(min(max(5.0 / asp, 1), 10), 2) if asp >= 1 else 2.5
|
| 137 |
-
area = round(L * B, 2)
|
| 138 |
price_mo = int(area * 10)
|
| 139 |
-
|
| 140 |
-
return f"""## Detection Results
|
| 141 |
-
|
| 142 |
| Dimension | Value |
|
| 143 |
|-----------|-------|
|
| 144 |
-
| Length
|
| 145 |
-
| Breadth
|
| 146 |
-
| Area
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
| 151 |
-
|
|
| 152 |
-
|
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
*Drag the corner handles to fine-tune the detected region.*"""
|
| 157 |
except Exception as e:
|
| 158 |
-
return f"
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 162 |
-
# Gradio callbacks
|
| 163 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 164 |
-
|
| 165 |
-
def on_detect(image):
|
| 166 |
-
"""Button click: run model, return image + corners JSON + info markdown."""
|
| 167 |
-
if image is None:
|
| 168 |
-
return None, "", "Upload an image to begin."
|
| 169 |
-
|
| 170 |
-
pixel_json, err = run_detection(image)
|
| 171 |
-
if err:
|
| 172 |
-
return image, "", err
|
| 173 |
-
|
| 174 |
-
return image, pixel_json, compute_area_md(pixel_json)
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
def on_corners_change(corners_json):
|
| 178 |
-
"""Corners were updated by JS drag β recompute and return area markdown."""
|
| 179 |
-
return compute_area_md(corners_json)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
-
# Warm up model at startup
|
| 184 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββ
|
| 185 |
|
| 186 |
get_model()
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
#
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
.
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
}
|
| 200 |
-
.gr-button-primary {
|
| 201 |
-
background: #2ED8DF !important;
|
| 202 |
-
color: #0d0d1a !important;
|
| 203 |
-
font-weight: 800 !important;
|
| 204 |
-
border: none !important;
|
| 205 |
-
border-radius: 8px !important;
|
| 206 |
-
letter-spacing: 0.04em !important;
|
| 207 |
-
}
|
| 208 |
-
.gr-button-primary:hover { background: #12EF86 !important; }
|
| 209 |
-
footer { display: none !important; }
|
| 210 |
-
"""
|
| 211 |
-
|
| 212 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 213 |
-
# Canvas HTML + JS
|
| 214 |
-
#
|
| 215 |
-
# Flow:
|
| 216 |
-
# Python β hidden_img updated β JS observer fires β canvas reloads image
|
| 217 |
-
# Python β corners_state updated β JS observer fires β canvas draws handles
|
| 218 |
-
# User drags handle β JS pushes JSON β corners_state.change()
|
| 219 |
-
# β Python recomputes area
|
| 220 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 221 |
-
|
| 222 |
-
CANVAS_BLOCK = """
|
| 223 |
-
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 224 |
-
|
| 225 |
-
<div id="canvas-wrap"
|
| 226 |
-
style="position:relative;width:100%;touch-action:none;user-select:none;">
|
| 227 |
-
<canvas id="parking-canvas"
|
| 228 |
-
style="width:100%;height:auto;display:block;border-radius:10px;
|
| 229 |
-
border:1px solid #1e1e3a;cursor:crosshair;background:#111127;">
|
| 230 |
-
</canvas>
|
| 231 |
-
<p id="canvas-hint"
|
| 232 |
-
style="color:#4b5563;font-size:11px;font-family:'DM Mono',monospace;
|
| 233 |
-
text-align:center;margin:6px 0 0;">
|
| 234 |
-
Detected corners will appear here after clicking Detect.
|
| 235 |
-
</p>
|
| 236 |
-
</div>
|
| 237 |
-
|
| 238 |
-
<script>
|
| 239 |
-
(function () {
|
| 240 |
-
'use strict';
|
| 241 |
-
|
| 242 |
-
const canvas = document.getElementById('parking-canvas');
|
| 243 |
-
const ctx = canvas.getContext('2d');
|
| 244 |
-
const hint = document.getElementById('canvas-hint');
|
| 245 |
-
|
| 246 |
-
let imgObj = null;
|
| 247 |
-
let corners = []; // pixel coords in native image resolution
|
| 248 |
-
let origW = 1;
|
| 249 |
-
let origH = 1;
|
| 250 |
-
let dragging = -1;
|
| 251 |
-
|
| 252 |
-
// Hit radius in CSS pixels β generous for touch
|
| 253 |
-
const HIT_CSS = 24;
|
| 254 |
-
|
| 255 |
-
// ββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
-
|
| 257 |
-
function draw() {
|
| 258 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 259 |
-
if (imgObj) ctx.drawImage(imgObj, 0, 0);
|
| 260 |
-
if (corners.length < 4) return;
|
| 261 |
-
|
| 262 |
-
// Polygon fill + stroke
|
| 263 |
-
ctx.beginPath();
|
| 264 |
-
corners.forEach(([x, y], i) => (i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)));
|
| 265 |
-
ctx.closePath();
|
| 266 |
-
ctx.fillStyle = 'rgba(46,216,223,0.18)';
|
| 267 |
-
ctx.strokeStyle = 'rgba(46,216,223,0.85)';
|
| 268 |
-
ctx.lineWidth = Math.max(2, origW / 220);
|
| 269 |
-
ctx.fill();
|
| 270 |
-
ctx.stroke();
|
| 271 |
-
|
| 272 |
-
// Handles β scaled so they look the same regardless of image resolution
|
| 273 |
-
const scale = origW / canvas.getBoundingClientRect().width;
|
| 274 |
-
const r = HIT_CSS * scale * 0.72;
|
| 275 |
-
corners.forEach(([x, y]) => {
|
| 276 |
-
ctx.beginPath();
|
| 277 |
-
ctx.arc(x, y, r, 0, Math.PI * 2);
|
| 278 |
-
ctx.fillStyle = 'rgba(46,216,223,0.9)';
|
| 279 |
-
ctx.fill();
|
| 280 |
-
ctx.beginPath();
|
| 281 |
-
ctx.arc(x, y, r * 0.42, 0, Math.PI * 2);
|
| 282 |
-
ctx.fillStyle = '#ffffff';
|
| 283 |
-
ctx.fill();
|
| 284 |
-
});
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
// ββ Pointer helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 288 |
-
|
| 289 |
-
function canvasXY(e) {
|
| 290 |
-
const rect = canvas.getBoundingClientRect();
|
| 291 |
-
const scaleX = canvas.width / rect.width;
|
| 292 |
-
const scaleY = canvas.height / rect.height;
|
| 293 |
-
const src = e.touches ? e.touches[0] : e;
|
| 294 |
-
return [
|
| 295 |
-
(src.clientX - rect.left) * scaleX,
|
| 296 |
-
(src.clientY - rect.top) * scaleY,
|
| 297 |
-
];
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
function findHandle(pos) {
|
| 301 |
-
const rect = canvas.getBoundingClientRect();
|
| 302 |
-
const hitPx = HIT_CSS * (canvas.width / rect.width) * 1.3;
|
| 303 |
-
let best = -1, bestD = Infinity;
|
| 304 |
-
corners.forEach(([x, y], i) => {
|
| 305 |
-
const d = Math.hypot(pos[0] - x, pos[1] - y);
|
| 306 |
-
if (d < hitPx && d < bestD) { bestD = d; best = i; }
|
| 307 |
-
});
|
| 308 |
-
return best;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
// ββ Event listeners ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 312 |
-
|
| 313 |
-
canvas.addEventListener('mousedown', down, { passive: false });
|
| 314 |
-
canvas.addEventListener('mousemove', move, { passive: false });
|
| 315 |
-
canvas.addEventListener('mouseup', up);
|
| 316 |
-
canvas.addEventListener('mouseleave', up);
|
| 317 |
-
canvas.addEventListener('touchstart', down, { passive: false });
|
| 318 |
-
canvas.addEventListener('touchmove', move, { passive: false });
|
| 319 |
-
canvas.addEventListener('touchend', up);
|
| 320 |
-
|
| 321 |
-
function down(e) {
|
| 322 |
-
e.preventDefault();
|
| 323 |
-
dragging = findHandle(canvasXY(e));
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
function move(e) {
|
| 327 |
-
if (dragging < 0) return;
|
| 328 |
-
e.preventDefault();
|
| 329 |
-
const [cx, cy] = canvasXY(e);
|
| 330 |
-
corners[dragging] = [
|
| 331 |
-
Math.max(0, Math.min(origW, cx)),
|
| 332 |
-
Math.max(0, Math.min(origH, cy)),
|
| 333 |
-
];
|
| 334 |
-
draw();
|
| 335 |
-
}
|
| 336 |
-
|
| 337 |
-
function up() {
|
| 338 |
-
if (dragging >= 0) pushCorners();
|
| 339 |
-
dragging = -1;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
// ββ Push corners β Gradio hidden Textbox ββββββββββββββββββββββββββββββββββ
|
| 343 |
-
|
| 344 |
-
function pushCorners() {
|
| 345 |
-
const ta = document.querySelector('#corners-state-box textarea');
|
| 346 |
-
if (!ta) return;
|
| 347 |
-
const payload = JSON.stringify({
|
| 348 |
-
corners: corners.map(([x, y]) => [x, y]),
|
| 349 |
-
width: origW,
|
| 350 |
-
height: origH,
|
| 351 |
-
});
|
| 352 |
-
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set;
|
| 353 |
-
setter.call(ta, payload);
|
| 354 |
-
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
// ββ Bridge: observe DOM for Gradio updates βββββββββββββββββββββββββββββββββ
|
| 358 |
-
|
| 359 |
-
let lastImgSrc = '';
|
| 360 |
-
let lastCornersVal = '';
|
| 361 |
-
|
| 362 |
-
const obs = new MutationObserver(() => {
|
| 363 |
-
|
| 364 |
-
// 1. Hidden image updated β reload canvas image
|
| 365 |
-
const imgEl = document.querySelector('#hidden-img-out img');
|
| 366 |
-
if (imgEl && imgEl.src && imgEl.src !== lastImgSrc) {
|
| 367 |
-
lastImgSrc = imgEl.src;
|
| 368 |
-
const img = new Image();
|
| 369 |
-
img.crossOrigin = 'anonymous';
|
| 370 |
-
img.onload = () => {
|
| 371 |
-
imgObj = img;
|
| 372 |
-
origW = img.naturalWidth;
|
| 373 |
-
origH = img.naturalHeight;
|
| 374 |
-
canvas.width = origW;
|
| 375 |
-
canvas.height = origH;
|
| 376 |
-
corners = [];
|
| 377 |
-
draw();
|
| 378 |
-
hint.textContent = 'Image loaded. Click Detect to find corners.';
|
| 379 |
-
};
|
| 380 |
-
img.src = lastImgSrc;
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
// 2. Corners state textbox updated by Python β parse and render
|
| 384 |
-
const ta = document.querySelector('#corners-state-box textarea');
|
| 385 |
-
if (ta && ta.value && ta.value !== lastCornersVal) {
|
| 386 |
-
lastCornersVal = ta.value;
|
| 387 |
-
try {
|
| 388 |
-
const data = JSON.parse(ta.value);
|
| 389 |
-
if (data && Array.isArray(data.corners) && data.corners.length === 4) {
|
| 390 |
-
origW = data.width || origW;
|
| 391 |
-
origH = data.height || origH;
|
| 392 |
-
corners = data.corners.map(([x, y]) => [x, y]);
|
| 393 |
-
draw();
|
| 394 |
-
hint.textContent = 'Drag the handles to fine-tune the detected area.';
|
| 395 |
-
}
|
| 396 |
-
} catch (_) {}
|
| 397 |
-
}
|
| 398 |
-
});
|
| 399 |
-
|
| 400 |
-
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
|
| 401 |
-
|
| 402 |
-
})();
|
| 403 |
-
</script>
|
| 404 |
-
"""
|
| 405 |
-
|
| 406 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 407 |
-
# Gradio layout
|
| 408 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 409 |
-
|
| 410 |
-
with gr.Blocks(title="ParkoSpace β AI Area Detector") as demo:
|
| 411 |
-
|
| 412 |
-
# Inline CSS β works across all Gradio versions
|
| 413 |
-
gr.HTML(f"<style>{CSS}</style>")
|
| 414 |
-
|
| 415 |
gr.HTML("""
|
| 416 |
-
<link href="https://fonts.googleapis.com/css2?family=
|
| 417 |
-
<div style="text-align:center;padding:32px 16px
|
| 418 |
-
<div style="font-size:
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
</div>
|
| 422 |
-
<h1 style="font-family:'DM Mono',monospace;font-size:26px;font-weight:500;
|
| 423 |
-
color:#f0f4f8;margin:0;letter-spacing:-0.02em">ParkoSpace</h1>
|
| 424 |
-
<p style="color:#4b5563;font-size:12px;margin-top:8px;
|
| 425 |
-
font-family:'DM Mono',monospace;letter-spacing:0.03em">
|
| 426 |
-
Upload a parking space photo β AI detects corners β drag to refine β area updates instantly
|
| 427 |
-
</p>
|
| 428 |
</div>
|
| 429 |
""")
|
| 430 |
|
| 431 |
-
with gr.Row(
|
| 432 |
-
|
| 433 |
-
# Left column: upload + detect button
|
| 434 |
with gr.Column(scale=1):
|
| 435 |
-
inp = gr.Image(
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
<p style="color:#4b5563;font-size:11px;font-family:'DM Mono',monospace;
|
| 445 |
-
margin:0;line-height:1.9">
|
| 446 |
-
<span style="color:#2ED8DF">Tips for best results</span><br>
|
| 447 |
-
Stand at one corner and shoot diagonally<br>
|
| 448 |
-
Include all 4 corners in the frame<br>
|
| 449 |
-
Good lighting β avoid strong shadows<br>
|
| 450 |
-
Works with car parks, open spaces, garages
|
| 451 |
</p>
|
| 452 |
-
</div>
|
| 453 |
-
""")
|
| 454 |
-
|
| 455 |
-
# Right column: canvas + results
|
| 456 |
with gr.Column(scale=1):
|
| 457 |
-
gr.
|
| 458 |
-
out_md
|
| 459 |
-
|
| 460 |
-
# ββ Hidden bridge components βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 461 |
|
| 462 |
-
|
| 463 |
-
# and by JS after a drag.
|
| 464 |
-
corners_state = gr.Textbox(
|
| 465 |
-
value="",
|
| 466 |
-
visible=False,
|
| 467 |
-
label="corners_state",
|
| 468 |
-
elem_id="corners-state-box",
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
# Holds the PIL image; Python writes here so JS can grab the img src URL.
|
| 472 |
-
hidden_img = gr.Image(
|
| 473 |
-
type="pil",
|
| 474 |
-
visible=False,
|
| 475 |
-
label="hidden_img",
|
| 476 |
-
elem_id="hidden-img-out",
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
# ββ Event wiring βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 480 |
-
|
| 481 |
-
btn.click(
|
| 482 |
-
fn=on_detect,
|
| 483 |
-
inputs=[inp],
|
| 484 |
-
outputs=[hidden_img, corners_state, out_md],
|
| 485 |
-
)
|
| 486 |
-
|
| 487 |
-
corners_state.change(
|
| 488 |
-
fn=on_corners_change,
|
| 489 |
-
inputs=[corners_state],
|
| 490 |
-
outputs=[out_md],
|
| 491 |
-
)
|
| 492 |
|
| 493 |
gr.HTML("""
|
| 494 |
-
<div style="margin-top:24px;padding:
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
Model: UmeshAdabala/RectArea_Parkospace |
|
| 499 |
-
MobileNetV2 + Keypoint Regression | ~13 MB | MIT License
|
| 500 |
</p>
|
| 501 |
</div>
|
| 502 |
""")
|
| 503 |
|
| 504 |
-
|
| 505 |
if __name__ == "__main__":
|
| 506 |
-
demo.launch()
|
| 507 |
-
else:
|
| 508 |
demo.launch()
|
|
|
|
| 8 |
import numpy as np
|
| 9 |
from huggingface_hub import hf_hub_download
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class ParkingSpaceDetector(nn.Module):
|
| 12 |
def __init__(self):
|
| 13 |
super().__init__()
|
|
|
|
| 15 |
self.features = backbone.features
|
| 16 |
self.pool = nn.AdaptiveAvgPool2d(1)
|
| 17 |
self.head = nn.Sequential(
|
| 18 |
+
nn.Dropout(0.3), nn.Linear(1280, 256),
|
| 19 |
+
nn.ReLU(inplace=True), nn.Dropout(0.2),
|
| 20 |
+
nn.Linear(256, 8), nn.Sigmoid()
|
|
|
|
|
|
|
|
|
|
| 21 |
)
|
|
|
|
| 22 |
def forward(self, x):
|
| 23 |
return self.head(self.pool(self.features(x)).flatten(1))
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
MODEL = None
|
|
|
|
|
|
|
| 26 |
def get_model():
|
| 27 |
global MODEL
|
| 28 |
+
if MODEL: return MODEL
|
|
|
|
|
|
|
|
|
|
| 29 |
try:
|
| 30 |
+
path = hf_hub_download("UmeshAdabala/RectArea_Parkospace", "best.pt", repo_type="model")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
m = ParkingSpaceDetector()
|
| 32 |
+
ckpt = torch.load(path, map_location="cpu")
|
| 33 |
if isinstance(ckpt, dict):
|
| 34 |
+
key = next((k for k in ("model_state_dict","state_dict") if k in ckpt), None)
|
| 35 |
+
m.load_state_dict(ckpt[key] if key else ckpt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
else:
|
| 37 |
+
MODEL = ckpt; MODEL.eval(); return MODEL
|
| 38 |
+
m.eval(); MODEL = m
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
except Exception as e:
|
| 40 |
+
print(f"Model load error: {e}")
|
| 41 |
MODEL = ParkingSpaceDetector().eval()
|
|
|
|
| 42 |
return MODEL
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
TRANSFORM = T.Compose([
|
| 45 |
+
T.Resize((224, 224)), T.ToTensor(),
|
| 46 |
+
T.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]),
|
|
|
|
| 47 |
])
|
| 48 |
|
| 49 |
+
def detect_and_draw(image: Image.Image):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
if image is None:
|
| 51 |
+
return None, "β Upload a parking space photo first."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
try:
|
| 53 |
+
img = image.convert("RGB")
|
| 54 |
+
W, H = img.size
|
| 55 |
+
t = TRANSFORM(img).unsqueeze(0)
|
| 56 |
+
with torch.no_grad():
|
| 57 |
+
c = get_model()(t)[0].tolist()
|
| 58 |
+
|
| 59 |
+
# corners: TL TR BR BL (normalised)
|
| 60 |
+
corners = [(c[i]*W, c[i+1]*H) for i in range(0, 8, 2)]
|
| 61 |
+
xs = [p[0] for p in corners]; ys = [p[1] for p in corners]
|
| 62 |
+
bw = max(xs)-min(xs); bh = max(ys)-min(ys)
|
| 63 |
+
asp = bw / (bh + 1e-6)
|
| 64 |
+
L = 5.0 if asp >= 1 else round(min(max(2.5/asp,1),15),2)
|
| 65 |
+
B = round(min(max(5.0/asp,1),10),2) if asp >= 1 else 2.5
|
| 66 |
+
area = round(L*B, 2)
|
| 67 |
+
|
| 68 |
+
# Draw overlay on image
|
| 69 |
+
out = img.copy().convert("RGBA")
|
| 70 |
+
overlay = Image.new("RGBA", out.size, (0,0,0,0))
|
| 71 |
+
draw = ImageDraw.Draw(overlay)
|
| 72 |
+
|
| 73 |
+
# Shaded region
|
| 74 |
+
poly = [(int(p[0]), int(p[1])) for p in corners]
|
| 75 |
+
draw.polygon(poly, fill=(46,216,223,60), outline=None)
|
| 76 |
+
draw.line(poly + [poly[0]], fill=(46,216,223,255), width=3)
|
| 77 |
+
|
| 78 |
+
# Corner dots
|
| 79 |
+
for px,py in poly:
|
| 80 |
+
draw.ellipse([px-8,py-8,px+8,py+8], fill=(46,216,223,255))
|
| 81 |
+
draw.ellipse([px-4,py-4,px+4,py+4], fill=(255,255,255,255))
|
| 82 |
+
|
| 83 |
+
out = Image.alpha_composite(out, overlay).convert("RGB")
|
| 84 |
|
|
|
|
|
|
|
|
|
|
| 85 |
price_mo = int(area * 10)
|
| 86 |
+
info = f"""## β
Detection Complete
|
|
|
|
|
|
|
| 87 |
| Dimension | Value |
|
| 88 |
|-----------|-------|
|
| 89 |
+
| π Length | **{L} m** |
|
| 90 |
+
| π Breadth | **{B} m** |
|
| 91 |
+
| π¦ Area | **{area} mΒ²** |
|
| 92 |
+
### π° Suggested Pricing
|
| 93 |
+
| Type | Price |
|
| 94 |
+
|------|-------|
|
| 95 |
+
| β± Hourly | **βΉ50** |
|
| 96 |
+
| π
Daily | **βΉ300** |
|
| 97 |
+
| π Monthly | **βΉ{price_mo}** (area Γ βΉ10/mΒ²) |
|
| 98 |
+
---
|
| 99 |
+
*Drag the corner handles on the image to fine-tune the detected region.*"""
|
| 100 |
+
return out, info
|
|
|
|
| 101 |
except Exception as e:
|
| 102 |
+
return image, f"β Error: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
get_model()
|
| 105 |
|
| 106 |
+
with gr.Blocks(
|
| 107 |
+
title="ParkoSpace β AI Area Detector",
|
| 108 |
+
css="""
|
| 109 |
+
body { background: #0d0d1a !important; }
|
| 110 |
+
.gradio-container { background: #0d0d1a !important; max-width: 900px !important; margin: 0 auto; }
|
| 111 |
+
h1 { color: #2ED8DF !important; font-family: 'Space Grotesk', sans-serif; }
|
| 112 |
+
.gr-button-primary { background: #2ED8DF !important; color: #0d0d1a !important; font-weight: 800 !important; border: none !important; }
|
| 113 |
+
.gr-button-primary:hover { background: #12EF86 !important; }
|
| 114 |
+
footer { display: none !important; }
|
| 115 |
+
"""
|
| 116 |
+
) as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
gr.HTML("""
|
| 118 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700;800&display=swap" rel="stylesheet">
|
| 119 |
+
<div style="text-align:center;padding:32px 16px 8px;background:linear-gradient(180deg,#0d0d1a,#0d0d1a)">
|
| 120 |
+
<div style="font-size:40px;margin-bottom:8px">π
Ώ</div>
|
| 121 |
+
<h1 style="font-family:'Space Grotesk',sans-serif;font-size:28px;font-weight:800;color:#2ED8DF;margin:0">ParkoSpace AI Detector</h1>
|
| 122 |
+
<p style="color:#6b7280;font-size:14px;margin-top:6px;font-family:monospace">Upload a parking space photo β AI detects the area β get dimensions + pricing</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</div>
|
| 124 |
""")
|
| 125 |
|
| 126 |
+
with gr.Row():
|
|
|
|
|
|
|
| 127 |
with gr.Column(scale=1):
|
| 128 |
+
inp = gr.Image(type="pil", label="πΈ Upload Parking Space Photo", height=340)
|
| 129 |
+
btn = gr.Button("π Detect Parking Area", variant="primary", size="lg")
|
| 130 |
+
gr.HTML("""<div style="background:#1a1a2e;border:1px solid #2ED8DF22;border-radius:12px;padding:12px;margin-top:8px">
|
| 131 |
+
<p style="color:#6b7280;font-size:11px;font-family:monospace;margin:0;line-height:1.6">
|
| 132 |
+
π <b style="color:#2ED8DF">Tips for best results:</b><br>
|
| 133 |
+
β’ Stand at one corner, shoot diagonally<br>
|
| 134 |
+
β’ Include all 4 corners in the frame<br>
|
| 135 |
+
β’ Good lighting, avoid shadows<br>
|
| 136 |
+
β’ Works with car parks, open spaces, garages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</p>
|
| 138 |
+
</div>""")
|
|
|
|
|
|
|
|
|
|
| 139 |
with gr.Column(scale=1):
|
| 140 |
+
out_img = gr.Image(type="pil", label="π― Detected Area", height=340)
|
| 141 |
+
out_md = gr.Markdown("*Upload a photo and click Detect to see results.*")
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
btn.click(fn=detect_and_draw, inputs=inp, outputs=[out_img, out_md])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
gr.HTML("""
|
| 146 |
+
<div style="margin-top:24px;padding:16px;background:#1a1a2e;border-radius:12px;border:1px solid #ffffff11">
|
| 147 |
+
<p style="color:#4b5563;font-size:11px;font-family:monospace;text-align:center;margin:0">
|
| 148 |
+
Model: <span style="color:#2ED8DF">UmeshAdabala/RectArea_Parkospace</span> Β· MobileNetV2 + Keypoint Regression Β· ~13MB<br>
|
| 149 |
+
This Space powers the <b style="color:#12EF86">ParkoSpace India</b> platform
|
|
|
|
|
|
|
| 150 |
</p>
|
| 151 |
</div>
|
| 152 |
""")
|
| 153 |
|
|
|
|
| 154 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 155 |
demo.launch()
|