Skip to content

This page is generated from docs/import.md — edit it there.

Import a plan from a segmentation mask (experimental) ​

ArchLang reads text, never images: the compiler is pure and has no image decoder, and it will not gain one. If you already have a floor plan as a label mask (a PNG in which each pixel's value is a class: wall, living room, bedroom and so on, the usual output of a segmentation model), there is a path into ArchLang that keeps the image work outside the compiler:

  1. a small Python tool, tools/mask-to-plan/ in the repository, turns the mask into a rooms-only Plan JSON: room rectangles plus a walls_from_rooms statement, nothing else;
  2. archlang compile --from-json compiles that JSON, and walls from rooms derives every wall from the rooms (an exterior ring on their outline, a partition on each shared edge);
  3. archlang validate and archlang takeoff then work on the result like on any plan.

This is experimental: the tool is a recipe in the repository, not part of the npm package, and its output is a starting point that you edit, not a finished plan.

A PDF floor plan has to be rasterised and segmented first, by you or your model; nothing here reads a PDF or a photo.

What the tool recovers, and what it does not ​

RecoveredNot recovered
Each room as one or more rectangles, snapped to the plan gridDoors, windows and openings
Room labels, from the class names you giveFurniture and fixtures
Wall positions, derived from the rooms by walls from roomsWall thicknesses (you pass them)
Floor areas: within 1 % on the worked example, when the mask meets the limits belowCurved, angled or freely polygonal rooms

walls from rooms derives walls only between rectangles, so a room that is not one rectangle (an L-shaped living room) becomes several rectangles joined by open between clauses: the partition between them is voided, and the space still reads as one room. describe counts each part's net area separately and subtracts the voided partition's strip, so the parts' sum is a little under the mask's area (under 1 % in the worked example).

How it works ​

Every 4-connected region of a room class becomes one room. The labels are snapped to the grid (each grid cell takes its most frequent label). Each room then grows across the walls around it, by half the wall thickness, first along rows and then along columns. Two neighbouring rooms meet on the wall's centre line, and an outer room reaches the exterior wall's centre line: that is where walls from rooms puts the walls it derives. Only a run of wall cells at most one wall thick is crossed, so a room never grows along a wall. Last, each grown region is cut into rectangles.

Worked example ​

The example in tools/mask-to-plan/example/ is a 1000 × 800 px mask at 10 mm per pixel (make_example.py draws it from a known layout): an L-shaped living room, a kitchen, a hall, two bedrooms and a bath, with 200 mm exterior walls and 100 mm partitions. The classes file names the values:

json
{
  "background": [0],
  "wall": [1],
  "rooms": { "2": "Living", "3": "Kitchen", "4": "Hall", "5": "Bedroom", "6": "Bath" }
}

Convert, then compile, check and count:

bash
cd tools/mask-to-plan
pip install -r requirements.txt
python -I mask_to_plan.py example/mask.png example/classes.json --mm-per-px 10 --name "Mask example" \
  --grid 50 --exterior 200 --partition 100 -o example/plan.json --areas example/areas.json
archlang compile --from-json example/plan.json -o plan.svg
node json_to_arch.mjs example/plan.json > example/plan.arch
archlang validate example/plan.arch
archlang takeoff example/plan.arch --format md
node check_areas.mjs example/plan.json example/areas.json

json_to_arch.mjs writes the .arch source with the library's planJsonToArch (the CLI compiles Plan JSON but does not print it as source). For the example it gives:

arch
plan "Mask example" {
  units mm
  walls from rooms exterior 200 partition 100 { open between living_a and living_b }
  room id=living_a at (0,0) size 6000x4000 label "Living"
  room id=living_b at (0,4000) size 3000x3000
  room id=kitchen at (6000,0) size 3000x4000 label "Kitchen"
  room id=hall at (3000,4000) size 1500x3000 label "Hall"
  room id=bedroom at (4500,4000) size 2500x3000 label "Bedroom"
  room id=bedroom_2 at (7000,5500) size 2000x1500 label "Bedroom"
  room id=bath at (7000,4000) size 2000x1500 label "Bath"
}

check_areas.mjs compares each region's floor area in the mask (its room pixels times the pixel area) with the net area describe reports for the rooms it became:

text
Living       mask   30.93 m2  net   30.64 m2  0.95 %  (living_a, living_b)
Kitchen      mask   10.97 m2  net   10.97 m2  0.02 %  (kitchen)
Hall         mask    3.99 m2  net    3.99 m2  0.00 %  (hall)
Bedroom      mask    6.84 m2  net    6.84 m2  0.00 %  (bedroom)
Bedroom      mask    2.50 m2  net    2.50 m2  0.10 %  (bedroom_2)
Bath         mask    2.59 m2  net    2.59 m2  0.00 %  (bath)
worst 0.95 % (tolerance 2.0 %)

validate passes with warnings, as it should for a plan with no doors or windows yet: most rooms are W_ROOM_DISCONNECTED, there is no entrance (W_NO_ENTRANCE), the rooms have no windows and the bath and kitchen no fixtures. Add the doors and windows as you would in any plan (door between living_a and hall …, window on kitchen.top …), and run validate again.

Limits ​

  • Axis-aligned only. A wall at an angle snaps into a staircase of small rectangles whose shared edges are shorter than a partition, so the result does not compile (E_PARTITION_TOO_SHORT, once per pair). Rotate the mask so the walls run along the pixel grid first.
  • Every room fully walled, and the thicknesses you pass match the mask. A room touching the image edge has no wall outside it, and a wall thicker or thinner than --exterior / --partition moves every edge it bounds: the floor areas then drift by several per cent. The tool measures the mask's own wall runs and warns in both cases; check_areas.mjs says by how much.
  • Adjacent rooms of one class need a wall between them. Two bedrooms that touch with no wall pixels between them are one region, so they become one room.
  • The grid decides the accuracy. --grid must be a whole number of pixels, and half of each wall thickness a whole number of grid steps (50 mm with 200 and 100 mm walls). A coarser grid moves room edges by up to half a step.
  • One storey per mask, no courtyards (a hole in the building's outline), and rooms must cover the floor: a gap between rooms that is not wall becomes a hole the walls cannot close.
  • Regions under --min-area-m2 (0.5 m² by default) are dropped as noise.
  • The labels are your class names. A class used for two separate regions (two bedrooms) gives two rooms, bedroom and bedroom_2, with the same label.

The tool is MIT-licensed like the rest of the repository and depends on numpy, SciPy and Pillow, never on the compiler's package. Run it with python -I so nothing beside it is imported by accident.