Spaces:
Running
Running
Add the trained segmentation model
Browse files- README.md +1 -1
- data/results.json +0 -0
- data/synth-dense/segmented.png +0 -0
- data/synth-medium/segmented.png +0 -0
- data/synth-simple/segmented.png +0 -0
- index.html +13 -12
README.md
CHANGED
|
@@ -11,7 +11,7 @@ short_description: "How small floor-plan errors distort area and room counts"
|
|
| 11 |
|
| 12 |
# quantity-lab showcase
|
| 13 |
|
| 14 |
-
How small geometry errors turn into wrong takeoff quantities. Pick a synthetic floor plan and a mode: the annotated rooms (oracle), a controlled error (rooms 2 or 5 px too small or too big, a missing divider, a spurious line),
|
| 15 |
|
| 16 |
Code (MIT): [github.com/dzakybd/quantity-lab](https://github.com/dzakybd/quantity-lab). Write-up: [dzaky.ai.id](https://dzaky.ai.id/en/blog/quantity-lab-takeoff-errors).
|
| 17 |
|
|
|
|
| 11 |
|
| 12 |
# quantity-lab showcase
|
| 13 |
|
| 14 |
+
How small geometry errors turn into wrong takeoff quantities. Pick a synthetic floor plan and a mode: the annotated rooms (oracle), a controlled error (rooms 2 or 5 px too small or too big, a missing divider, a spurious line), a classical OpenCV room baseline, or the pretrained CubiCasa5K segmentation model (its output on synthetic plans, computed ahead of time). The tables give measured results on ten held-out CubiCasa5K test plans, as numbers only. Everything was computed ahead of time; the page runs no model.
|
| 15 |
|
| 16 |
Code (MIT): [github.com/dzakybd/quantity-lab](https://github.com/dzakybd/quantity-lab). Write-up: [dzaky.ai.id](https://dzaky.ai.id/en/blog/quantity-lab-takeoff-errors).
|
| 17 |
|
data/results.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/synth-dense/segmented.png
ADDED
|
data/synth-medium/segmented.png
ADDED
|
data/synth-simple/segmented.png
ADDED
|
index.html
CHANGED
|
@@ -42,8 +42,8 @@ footer { margin-top: 28px; font-size: 13px; color: var(--muted); }
|
|
| 42 |
</head>
|
| 43 |
<body>
|
| 44 |
<main>
|
| 45 |
-
<h1>
|
| 46 |
-
<p class="muted">quantity-lab measures room area, perimeter and count on floor plans three ways: the annotated rooms (oracle), the same rooms with a controlled error, and a classical OpenCV baseline that labels the free space between walls. Units are image pixels; no physical scale is claimed.</p>
|
| 47 |
<pre>uv run python -m quantity_lab --input plan.pdf --annotations model.svg --mode perturbed --perturbation erode --pixels 2 --output out/plan</pre>
|
| 48 |
<p class="muted" style="margin-top:8px">Code (MIT): <a href="https://github.com/dzakybd/quantity-lab">github.com/dzakybd/quantity-lab</a>. Write-up: <a href="https://dzaky.ai.id/en/blog/quantity-lab-takeoff-errors">dzaky.ai.id</a>. Results were computed ahead of time; this page runs no model.</p>
|
| 49 |
|
|
@@ -64,19 +64,19 @@ footer { margin-top: 28px; font-size: 13px; color: var(--muted); }
|
|
| 64 |
|
| 65 |
<section class="panel" style="margin-top:16px">
|
| 66 |
<h2>Controlled errors on ten held-out CubiCasa5K plans (ranges; numbers only)</h2>
|
| 67 |
-
<p class="muted" style="font-size:13px">
|
| 68 |
<div class="scroll"><table id="perturb"></table></div>
|
| 69 |
</section>
|
| 70 |
<section class="panel">
|
| 71 |
-
<h2>
|
| 72 |
-
<p class="muted" style="font-size:13px">
|
| 73 |
<div class="scroll"><table id="pred"></table></div>
|
| 74 |
</section>
|
| 75 |
|
| 76 |
<footer>CubiCasa5K: Kalervo et al., SCIA 2019, <a href="https://zenodo.org/records/2613548">zenodo.org/records/2613548</a> (CC BY-NC-SA 4.0). No CubiCasa5K image is shown or included here; every picture is a synthetic plan made for this project.</footer>
|
| 77 |
</main>
|
| 78 |
<script>
|
| 79 |
-
const VIEWS = { "oracle": "Oracle", "perturbed_erode_2": "Pulled in 2 px", "perturbed_erode_5": "Pulled in 5 px", "perturbed_dilate_5": "Pushed out 5 px", "perturbed_merge_20": "Missing divider", "perturbed_split_3": "Spurious line", "predicted": "OpenCV baseline" };
|
| 80 |
const ERRS = { "erode_2": "Pulled in 2 px", "erode_5": "Pulled in 5 px", "dilate_2": "Pushed out 2 px", "dilate_5": "Pushed out 5 px", "shift_5": "Shifted 5 px", "scale_2": "Scale +2%", "merge_20": "Missing divider", "split_3": "Spurious line" };
|
| 81 |
let data, cur = { p: 0, v: "oracle" };
|
| 82 |
const fmt = (v, d = 1) => v === null || v === undefined ? "—" : (v > 0 ? "+" : "") + Number(v).toFixed(d);
|
|
@@ -94,21 +94,22 @@ function render() {
|
|
| 94 |
document.getElementById("scoreTitle").textContent = `${p.title} · ${VIEWS[cur.v]}`;
|
| 95 |
document.getElementById("stats").innerHTML = [
|
| 96 |
[s.count_f1.toFixed(2), "room count F1"], [fmt(s.area_signed_err_pct_mean) + "%", "room area error (matched)"],
|
| 97 |
-
[fmt(s.total_area_err_pct) + "%", "total area error"], [cur.v === "oracle" ? p.rooms : (s.pred_rooms ?? "—"), cur.v === "oracle" ? "reference rooms" : "rooms found
|
| 98 |
].map(([b, t]) => `<div class="stat"><b>${b}</b><span>${t}</span></div>`).join("");
|
| 99 |
}
|
| 100 |
-
function range(rows, key, digits = 1) {
|
| 101 |
const values = rows.map(r => parseFloat(r[key])).filter(Number.isFinite);
|
| 102 |
-
|
|
|
|
| 103 |
}
|
| 104 |
function tables() {
|
| 105 |
const by = {};
|
| 106 |
data.perturbations.forEach(r => { (by[`${r.perturbation}_${r.pixels}`] ||= []).push(r); });
|
| 107 |
document.getElementById("perturb").innerHTML = "<tr><th>Error</th><th>Room area</th><th>Total area</th><th>Count F1</th></tr>" +
|
| 108 |
-
Object.entries(ERRS).filter(([k]) => by[k]).map(([k, t]) => `<tr><td>${t}</td><td>${range(by[k], "area_signed_err_pct_mean")}%</td><td>${range(by[k], "total_area_err_pct")}%</td><td>${range(by[k], "count_f1", 2)}</td></tr>`).join("");
|
| 109 |
const names = { "high_quality_488": "Simple", "high_quality_architectural_9453": "Medium (open kitchen)", "high_quality_architectural_6276": "Dense (two floors)" };
|
| 110 |
-
document.getElementById("pred").innerHTML = "<tr><th>Plan</th><th>
|
| 111 |
-
Object.entries(data.held_out).map(([k, v]) => { const p = v.predicted; return `<tr><td>${names[k] || k}</td><td>${p.gt_rooms}</td><td>${p.
|
| 112 |
}
|
| 113 |
fetch("data/results.json").then(r => r.json()).then(j => { data = j; render(); tables(); });
|
| 114 |
</script>
|
|
|
|
| 42 |
</head>
|
| 43 |
<body>
|
| 44 |
<main>
|
| 45 |
+
<h1>Right total, wrong rooms: what overlap scores miss in floor-plan takeoffs</h1>
|
| 46 |
+
<p class="muted">quantity-lab measures room area, perimeter and count on floor plans three ways: the annotated rooms (oracle), the same rooms with a controlled error, and two real room finders: a classical OpenCV baseline that labels the free space between walls, and the pretrained CubiCasa5K segmentation model. Units are image pixels; no physical scale is claimed.</p>
|
| 47 |
<pre>uv run python -m quantity_lab --input plan.pdf --annotations model.svg --mode perturbed --perturbation erode --pixels 2 --output out/plan</pre>
|
| 48 |
<p class="muted" style="margin-top:8px">Code (MIT): <a href="https://github.com/dzakybd/quantity-lab">github.com/dzakybd/quantity-lab</a>. Write-up: <a href="https://dzaky.ai.id/en/blog/quantity-lab-takeoff-errors">dzaky.ai.id</a>. Results were computed ahead of time; this page runs no model.</p>
|
| 49 |
|
|
|
|
| 64 |
|
| 65 |
<section class="panel" style="margin-top:16px">
|
| 66 |
<h2>Controlled errors on ten held-out CubiCasa5K plans (ranges; numbers only)</h2>
|
| 67 |
+
<p class="muted" style="font-size:13px">Ranges over the ten plans: mean signed room-area error, total-area error and room F1.</p>
|
| 68 |
<div class="scroll"><table id="perturb"></table></div>
|
| 69 |
</section>
|
| 70 |
<section class="panel">
|
| 71 |
+
<h2>Two room finders on the same plans</h2>
|
| 72 |
+
<p class="muted" style="font-size:13px">OpenCV baseline tuned on three train plans; the trained model's pixel-to-room step tuned on ten validation plans. Both frozen before these plans were scored. High pixel overlap (IoU) does not mean the rooms are right.</p>
|
| 73 |
<div class="scroll"><table id="pred"></table></div>
|
| 74 |
</section>
|
| 75 |
|
| 76 |
<footer>CubiCasa5K: Kalervo et al., SCIA 2019, <a href="https://zenodo.org/records/2613548">zenodo.org/records/2613548</a> (CC BY-NC-SA 4.0). No CubiCasa5K image is shown or included here; every picture is a synthetic plan made for this project.</footer>
|
| 77 |
</main>
|
| 78 |
<script>
|
| 79 |
+
const VIEWS = { "oracle": "Oracle", "perturbed_erode_2": "Pulled in 2 px", "perturbed_erode_5": "Pulled in 5 px", "perturbed_dilate_5": "Pushed out 5 px", "perturbed_merge_20": "Missing divider", "perturbed_split_3": "Spurious line", "predicted": "OpenCV baseline", "segmented": "Trained model" };
|
| 80 |
const ERRS = { "erode_2": "Pulled in 2 px", "erode_5": "Pulled in 5 px", "dilate_2": "Pushed out 2 px", "dilate_5": "Pushed out 5 px", "shift_5": "Shifted 5 px", "scale_2": "Scale +2%", "merge_20": "Missing divider", "split_3": "Spurious line" };
|
| 81 |
let data, cur = { p: 0, v: "oracle" };
|
| 82 |
const fmt = (v, d = 1) => v === null || v === undefined ? "—" : (v > 0 ? "+" : "") + Number(v).toFixed(d);
|
|
|
|
| 94 |
document.getElementById("scoreTitle").textContent = `${p.title} · ${VIEWS[cur.v]}`;
|
| 95 |
document.getElementById("stats").innerHTML = [
|
| 96 |
[s.count_f1.toFixed(2), "room count F1"], [fmt(s.area_signed_err_pct_mean) + "%", "room area error (matched)"],
|
| 97 |
+
[fmt(s.total_area_err_pct) + "%", "total area error"], [cur.v === "oracle" ? p.rooms : (s.pred_rooms ?? "—"), cur.v === "oracle" ? "reference rooms" : "rooms found"],
|
| 98 |
].map(([b, t]) => `<div class="stat"><b>${b}</b><span>${t}</span></div>`).join("");
|
| 99 |
}
|
| 100 |
+
function range(rows, key, digits = 1, signed = true) {
|
| 101 |
const values = rows.map(r => parseFloat(r[key])).filter(Number.isFinite);
|
| 102 |
+
const f = v => signed ? fmt(v, digits) : v.toFixed(digits);
|
| 103 |
+
return values.length ? `${f(Math.min(...values))} to ${f(Math.max(...values))}` : "—";
|
| 104 |
}
|
| 105 |
function tables() {
|
| 106 |
const by = {};
|
| 107 |
data.perturbations.forEach(r => { (by[`${r.perturbation}_${r.pixels}`] ||= []).push(r); });
|
| 108 |
document.getElementById("perturb").innerHTML = "<tr><th>Error</th><th>Room area</th><th>Total area</th><th>Count F1</th></tr>" +
|
| 109 |
+
Object.entries(ERRS).filter(([k]) => by[k]).map(([k, t]) => `<tr><td>${t}</td><td>${range(by[k], "area_signed_err_pct_mean")}%</td><td>${range(by[k], "total_area_err_pct")}%</td><td>${range(by[k], "count_f1", 2, false)}</td></tr>`).join("");
|
| 110 |
const names = { "high_quality_488": "Simple", "high_quality_architectural_9453": "Medium (open kitchen)", "high_quality_architectural_6276": "Dense (two floors)" };
|
| 111 |
+
document.getElementById("pred").innerHTML = "<tr><th>Plan</th><th>Rooms</th><th>OpenCV F1</th><th>OpenCV IoU</th><th>Model found</th><th>Model matched</th><th>Model F1</th><th>Model IoU</th><th>Model total area</th></tr>" +
|
| 112 |
+
Object.entries(data.held_out).map(([k, v]) => { const p = v.predicted, m = v.segmented; return `<tr><td>${names[k] || k}</td><td>${p.gt_rooms}</td><td>${p.count_f1.toFixed(2)}</td><td>${p.mask_iou.toFixed(2)}</td><td>${m.pred_rooms}</td><td>${m.matched}</td><td>${m.count_f1.toFixed(2)}</td><td>${m.mask_iou.toFixed(2)}</td><td>${fmt(m.total_area_err_pct)}%</td></tr>`; }).join("");
|
| 113 |
}
|
| 114 |
fetch("data/results.json").then(r => r.json()).then(j => { data = j; render(); tables(); });
|
| 115 |
</script>
|