dzakybd commited on
Commit
c8eaf20
·
verified ·
1 Parent(s): 05cb263

Add the trained segmentation model

Browse files
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), or a classical OpenCV room baseline. 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
 
 
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>How much can a 2-pixel error distort a floor-plan estimate?</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">Mean signed room-area error, per plan: simple (5 rooms) / medium (9) / dense (28).</p>
68
  <div class="scroll"><table id="perturb"></table></div>
69
  </section>
70
  <section class="panel">
71
- <h2>OpenCV baseline on the same plans</h2>
72
- <p class="muted" style="font-size:13px">Tuned on three CubiCasa5K train plans, then frozen.</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" };
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 (baseline)"],
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
- return values.length ? `${fmt(Math.min(...values), digits)} to ${fmt(Math.max(...values), digits)}` : "—";
 
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>Reference rooms</th><th>Found</th><th>Matched</th><th>Count F1</th><th>Total area</th></tr>" +
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.pred_rooms}</td><td>${p.matched}</td><td>${p.count_f1.toFixed(2)}</td><td>${fmt(p.total_area_err_pct)}%</td></tr>`; }).join("");
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>