Floor plan recognition and takeoff
The applied problem: turn a drawing into quantities. How many doors, how many linear metres of partition wall, what floor area per room. In construction this is takeoff — the measurement step that feeds an estimate — and it is done by hand at enormous cost, which is why it attracts automation.
This note is the pipeline the rest of this folder assembles into.
Check whether you need vision at all
The first question, and the one that separates engineers from model-reachers: what is actually in the file?
| Source | Contains | Right tool |
|---|---|---|
| DWG or DXF | real geometry, layers | parse it |
| IFC or Revit | a semantic model | query it |
| Vector PDF | drawing primitives | extract paths |
| Raster PDF or photo | pixels only | computer vision |
A vector PDF holds actual line segments with coordinates. Rendering it to pixels and running segmentation to recover lines you could have read exactly is a real and common mistake. Detect the type at ingest and route accordingly — CV is the fallback for when the structure has been thrown away, not the default.
Even in a vector PDF the semantics are missing: you get lines, not walls. So the pipeline is often hybrid — exact geometry from the file, classification of what each element means from a model.
The pipeline
ingest ─> classify source ─> vector path ──────┐
└─> render/normalise │
↓ │
rectify │
↓ │
detect symbols │
segment regions │
↓ │
OCR labels, dims │
↓ │
vectorise, snap ──┤
↓
scale ─> measure
─> validate ─> emitStage by stage, with the note that owns each:
- Normalise and rectify — Geometry: homography, rectification, vanishing lines. Photographs need a homography; scans need a deskew.
- Detect symbols — doors, windows, fixtures, north arrow, scale bar. Detection, not segmentation: they are countable and a box suffices. Object detection.
- Segment regions — walls, rooms, circulation. Pixels, because you need areas and lengths. Segmentation architectures.
- OCR — room labels, dimension strings, the scale bar, the title block. OCR inside a vision pipeline.
- Vectorise and snap — Contours, polygons and vector geometry.
- Scale, measure, validate — below.
Walls are the hard part
Rooms are easy: they are large closed regions with good contrast. Walls are where the accuracy goes, for reasons worth being able to state:
- They are thin. Two to four pixels at typical scan resolution, which is where pooling erases them. This drives input resolution and tiling.
- They are interrupted. Doors and windows break a wall’s run, but the wall continues logically. A pure mask gives you segments; a takeoff needs the centreline through the openings.
- Thickness carries meaning. Structural versus partition is often encoded as line weight or hatching, and the distinction changes the estimate.
- They are shared. One wall between two rooms is one wall, not two. Naive per-room perimeter summation double-counts every internal wall — the single most common quantity bug in this domain.
The centreline is usually what you want to measure, not the mask:
from skimage.morphology import skeletonize
centre = skeletonize(wall_mask > 0) # 1px medial axis
graph = build_graph(centre) # nodes at junctions, edges = runs
graph = bridge_openings(graph, doors) # rejoin across door gaps
length_m = sum(e.length for e in graph.edges) * m_per_pxModelling walls as a graph rather than a mask is what makes the downstream questions answerable: which rooms does this wall separate, is this loop closed, is this room actually enclosed. It also gives you a natural place to put the shared-wall rule — an edge belongs to one wall regardless of how many rooms touch it.
Scale is a single point of failure
Every quantity is a pixel measurement times one scale factor. Get it wrong by 10x and every number is wrong by 10x, uniformly and plausibly.
Sources, in order of reliability: an OCR’d scale bar measured in pixels; a stated ratio in the title block combined with page size and DPI; an OCR’d dimension annotation compared against its own pixel length; a known object width as a last resort.
Derive it two ways and compare. If the scale bar and a dimension annotation disagree beyond a few percent, that is not a number to average — it is a signal to abstain. Emitting a whole takeoff built on a bad scale is far worse than returning nothing, because it looks right.
Validation that actually catches errors
def validate(plan):
problems = []
if abs(sum(r.area for r in plan.rooms) - plan.footprint) > 0.15 * plan.footprint:
problems.append("room areas do not sum to footprint")
if any(a.intersection(b).area > 0.02 * a.area
for a, b in combinations(plan.rooms, 2)):
problems.append("rooms overlap")
if any(not r.is_enclosed for r in plan.rooms):
problems.append("unenclosed region")
if any(d.width_m > 2.5 or d.width_m < 0.6 for d in plan.doors):
problems.append("implausible door width")
return problemsEach check is trivial and each catches a class of failure that no model metric reports. The area-sum check in particular catches missed regions, which is the error most likely to reach a customer, because a missing room does not look like a bug — it looks like a plan with fewer rooms.
Where it breaks in production
- Double-counted shared walls. The classic. Model walls as a graph.
- Scale from a mis-OCR’d bar. Cross-check, then abstain on disagreement.
- Drawing conventions vary by practice. Hatching, symbol sets and line weights differ between firms and countries. A model trained on one office’s output does not transfer; this is what Synthetic data and image normalization exists to address.
- Multi-sheet sets. A drawing set is many pages with a key plan, revisions and overlaps. Deduplicate revisions and pick the current one, or you total two versions of the same floor.
- Units. Millimetres in Europe, feet and inches in the US, and a drawing that states one while annotating in the other. Carry units in the type, never as a bare float.
- Legend-dependent symbols. The same glyph means different things in different sets. Read the legend if there is one.
Related
Interview angle 4
- “Design a system that measures wall lengths from uploaded floor plans.” - classify the source first and parse vector formats directly; CV is the fallback for rasters. Then rectify, segment walls, skeletonise to centrelines, model as a graph so shared walls count once, fix scale from two independent sources, and validate that areas sum before emitting anything.
- “Why is your wall total roughly double the true figure?” - shared walls counted once per adjoining room. Summing per-room perimeters double-counts every internal wall. Model walls as graph edges owned once.
- “The customer’s numbers are all 10x too big.” - the scale factor, almost certainly a mis-read scale bar or a millimetre/metre confusion. Derive scale two independent ways and abstain when they disagree rather than averaging.
- “Why are walls harder than rooms?” - thin enough to be erased by pooling, interrupted by openings so the mask is fragmented, semantically shared between rooms, and their thickness encodes structural versus partition. Rooms are big high-contrast blobs by comparison.