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:
- a small Python tool,
tools/mask-to-plan/in the repository, turns the mask into a rooms-only Plan JSON: room rectangles plus awalls_from_roomsstatement, nothing else; archlang compile --from-jsoncompiles that JSON, andwalls from roomsderives every wall from the rooms (an exterior ring on their outline, a partition on each shared edge);archlang validateandarchlang takeoffthen 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
| Recovered | Not recovered |
|---|---|
| Each room as one or more rectangles, snapped to the plan grid | Doors, windows and openings |
| Room labels, from the class names you give | Furniture and fixtures |
Wall positions, derived from the rooms by walls from rooms | Wall thicknesses (you pass them) |
| Floor areas: within 1 % on the worked example, when the mask meets the limits below | Curved, 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:
{
"background": [0],
"wall": [1],
"rooms": { "2": "Living", "3": "Kitchen", "4": "Hall", "5": "Bedroom", "6": "Bath" }
}Convert, then compile, check and count:
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.jsonjson_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:
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:
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/--partitionmoves 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.mjssays 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.
--gridmust 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,
bedroomandbedroom_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.