Inside Kansumi: how a floor plan becomes a scene graph
A technical tour of the JSON scene graph behind every Kansumi render, and what its confidence scores actually measure.
On this page
Most AI interior tools treat your floor plan as a picture. Kansumi treats it as data. The moment extraction finishes, your plan stops being pixels and becomes a versioned JSON document we call the scene graph, and every wall you drag, every door you move, and every render you generate reads and writes that document, never the image.
This article is a tour of how that works, and why it makes the renders trustworthy.

Why images make poor sources of truth
An image of a floor plan knows nothing. It cannot tell you a wall's length, whether a door swings inward, or if the sofa you asked for even fits. Tools that edit the image have to re-guess all of that on every change, which is how you end up with renders where the room silently gained a meter.
If the geometry isn't stored anywhere, every edit is a new act of imagination.
So we store it. All of it.
The scene graph, in millimeters
Every project owns a scene.v1.json document. All coordinates and lengths are integers of real-world millimeters: no pixel units anywhere past extraction. Here's a trimmed excerpt of a real scene:
{
"schema_version": "v1",
"units": "mm",
"scale": {
"confirmed": true,
"reference": { "type": "wall_length", "element_id": "wall_a", "value_mm": 4200 }
},
"walls": [
{
"id": "wall_a",
"start": { "x": 0, "y": 0 },
"end": { "x": 4200, "y": 0 },
"thickness_mm": 120,
"confidence": 0.94,
"user_confirmed": true
}
],
"openings": [
{
"id": "door_1",
"kind": "door",
"host_wall_id": "wall_a",
"offset_mm_from_start": 2050,
"width_mm": 900,
"swing": "left",
"swing_direction": "in",
"confidence": 0.81,
"user_confirmed": false
}
]
}Two details in that snippet carry most of the weight.
Openings bind to walls parametrically
A door is not stored at an (x, y) position. It's stored as "on wall_a, 2050 mm from its start, 900 mm wide." When you drag a wall, split it, or straighten it in the editor, the door re-anchors deterministically. It can't drift into the middle of a room.

Scale is a first-class citizen
Extraction estimates a provisional pixel-to-millimeter ratio, but the scene records whether a human has confirmed it, and against which reference: a known wall length, an OCR'd dimension label, or a standard door width. Until scale.confirmed is true, the editor shows it, honestly, as an estimate.
What confidence scores actually measure
Every extracted element carries a confidence between 0 and 1. It isn't decoration: the editor sorts its review queue by it, and the pipeline refuses to render from geometry that's both low-confidence and unreviewed.
| Element | Confidence source | Below threshold, we… |
|---|---|---|
| Walls | Vision-model consensus across passes | Highlight for review in the editor |
| Openings | Detection score × host-wall confidence | Ask you to confirm swing + width |
| Rooms | Polygon closure + OCR label agreement | Leave the room type unset |
| Dimensions | OCR quality on the printed label | Ignore the label for scaling |
Editing without breaking geometry
Because the scene graph is the record, editor operations are small, pure functions over it. Splitting a wall, for example, re-anchors every opening by arithmetic: not by re-detection:
function splitWall(scene: SceneV1, wallId: string, at_mm: number): SceneV1 {
const wall = scene.walls.find((w) => w.id === wallId);
if (!wall) throw new Error(`unknown wall: ${wallId}`);
const [left, right] = divideSegment(wall, at_mm);
const openings = scene.openings.map((o) => {
if (o.host_wall_id !== wallId) return o;
// An opening before the cut stays on the left piece with its offset
// untouched; one after the cut moves to the right piece, offset shifted.
return o.offset_mm_from_start < at_mm
? { ...o, host_wall_id: left.id }
: { ...o, host_wall_id: right.id, offset_mm_from_start: o.offset_mm_from_start - at_mm };
});
return { ...scene, walls: replaceWall(scene.walls, wallId, [left, right]), openings };
}Undo and redo fall out for free: each edit yields a new scene document, so history is just a stack of them.
Renders are receipts, not guesses
When you generate concepts, the renderer doesn't look at your uploaded image at all. It builds a control image from the scene graph (walls, openings, and furniture footprints projected from the room's canonical camera) and conditions the diffusion model on that. The render matches your dimensions because it was never shown anything else.
That's also why the chat editor can say "the 1.6 m sofa doesn't fit on the 1.4 m wall" instead of quietly rendering an impossible room: fitting is integer math on millimeters, checked before a single pixel is generated.
Takeaways
- Geometry lives in one place: a versioned JSON scene graph, in millimeters, never in pixels.
- Relationships are parametric: openings belong to walls, cameras belong to rooms, so edits compose safely.
- Uncertainty is explicit: confidence scores and
user_confirmedflags gate what the pipeline will render. - Renders derive from the graph: which is why they agree with your tape measure.
If you haven't tried it yet, upload a floor plan and watch the scene graph get built in front of you.