Extracting Room Boundaries from SVG Floor Plans for Indoor Wayfinding Automation

Converting raw SVG floor plans into closed, navigable room polygons requires deterministic DOM traversal, coordinate normalization, and topological reconstruction. Facilities technicians, GIS developers, and indoor navigation teams routinely encounter fragmented path data, nested transform matrices, and micro-gaps that break automated wayfinding graph generation. This guide isolates the exact implementation steps required to extract, validate, and bind room boundaries from production-grade SVG assets, with emphasis on deterministic parsing, metric alignment, and topology validation.

DOM Flattening & Transform Resolution

SVG exports from CAD/BIM tools rarely present clean, flat geometry. Elements are heavily nested inside <g> tags, inherit cascading styles, and rely on relative coordinate systems. Before boundary extraction, the DOM must be flattened and normalized to a consistent metric space. This normalization step is foundational to any robust Automated Floor Plan Parsing & Vectorization architecture, as downstream topology validators assume absolute, unit-aligned coordinates.

Resolving Nested Transform Matrices

Nested <g> elements apply cumulative transform="matrix(a b c d e f)" operations to child paths. Failing to resolve these matrices results in misaligned walls, phantom room overlaps, and broken adjacency graphs.

Diagnostic Step: Inspect the SVG for transform attributes on parent groups. If transform-origin, rotate(), or scale() appears alongside translate(), standard CSS parsing will fail. SVG transforms are applied right-to-left in the DOM tree, meaning child matrices must be post-multiplied by parent matrices.

import numpy as np
from lxml import etree
from typing import Iterator, Tuple
import re

SVG_TRANSFORM_RE = re.compile(r"matrix\(\s*([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s+([-\d.eE+]+)\s*\)")

def parse_svg_matrix(transform_str: str) -> np.ndarray:
    """Parse SVG matrix(a b c d e f) into a 3x3 homogeneous transformation matrix."""
    match = SVG_TRANSFORM_RE.search(transform_str)
    if not match:
        return np.identity(3)
    a, b, c, d, e, f = map(float, match.groups())
    # SVG uses column-major notation: [a c e; b d f; 0 0 1]
    return np.array([
        [a, c, e],
        [b, d, f],
        [0, 0, 1]
    ])

def flatten_transforms(element: etree.Element, parent_matrix: np.ndarray = np.identity(3)) -> Iterator[Tuple[etree.Element, np.ndarray]]:
    """Recursively resolve nested SVG transform matrices and yield leaf geometry."""
    current_matrix = np.identity(3)
    transform_attr = element.get("transform")
    if transform_attr:
        current_matrix = parse_svg_matrix(transform_attr)
    
    # SVG applies transforms right-to-left: parent * child
    cumulative = np.dot(parent_matrix, current_matrix)
    
    tag = etree.QName(element.tag).localname
    if tag in {"path", "polygon", "rect", "line", "polyline"}:
        yield element, cumulative
    else:
        for child in element:
            yield from flatten_transforms(child, cumulative)

Debugging Note: Reversing the multiplication order (child * parent instead of parent * child) will invert rotation centers and shift walls by hundreds of pixels. Always verify matrix propagation against known anchor points in the source CAD file.

Coordinate Normalization & Metric Alignment

SVG defaults to a top-left origin (0,0) with Y increasing downward. GIS and indoor mapping engines typically expect a bottom-left origin with Y increasing upward, and coordinates expressed in real-world units (meters or feet).

Y-Axis Inversion & Unit Calibration

Coordinate space alignment requires three deterministic operations:

  1. Apply the resolved transform matrix to raw path coordinates.
  2. Invert the Y-axis relative to the SVG viewBox height.
  3. Scale pixels to metric units using a known calibration factor or architectural scale.
def transform_and_normalize_path(d_attr: str, matrix: np.ndarray, viewbox_height: float, px_per_meter: float) -> np.ndarray:
    """Parse SVG path 'd' attribute, apply matrix, flip Y, and convert to meters."""
    # Simplified parser for M, L, Z commands (production requires full SVG 1.1 spec compliance)
    coords = []
    current_cmd = None
    token_stream = re.findall(r"[MLZmlz]|[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", d_attr)
    
    for token in token_stream:
        if token.upper() in "ML":
            current_cmd = token.upper()
        elif token.upper() == "Z":
            continue
        else:
            coords.append(float(token))
    
    if len(coords) % 2 != 0:
        raise ValueError("Malformed path: odd number of coordinate values.")
    
    points = np.array(coords).reshape(-1, 2)
    # Apply homogeneous transformation
    ones = np.ones((points.shape[0], 1))
    homogeneous = np.hstack([points, ones])
    transformed = (matrix @ homogeneous.T).T[:, :2]
    
    # Y-axis inversion relative to viewBox
    transformed[:, 1] = viewbox_height - transformed[:, 1]
    # Convert to meters
    return transformed / px_per_meter

For full compliance with SVG coordinate transformations, reference the W3C SVG 1.1 Coordinate Systems specification, which details how nested viewBox attributes and preserveAspectRatio flags alter the final projection.

Path Parsing & Geometric Linearization

SVG paths frequently contain Bézier curves (C, S, Q, T) and elliptical arcs (A). Indoor wayfinding engines require linearized, closed polygons. Curves must be sampled at a tolerance threshold that preserves architectural intent without introducing excessive vertex density.

Linearization & Shapely Integration

from shapely.geometry import Polygon, LineString
from shapely.ops import transform
import numpy as np

def linearize_svg_path(d_attr: str, matrix: np.ndarray, viewbox_height: float, px_per_meter: float, tolerance: float = 0.05) -> Polygon:
    """Convert SVG path to a closed Shapely Polygon with metric coordinates."""
    raw_coords = transform_and_normalize_path(d_attr, matrix, viewbox_height, px_per_meter)
    
    # Close the path if missing 'Z'
    if not np.array_equal(raw_coords[0], raw_coords[-1]):
        raw_coords = np.vstack([raw_coords, raw_coords[0]])
    
    # Linearize using Shapely's interpolate/simplify if curves were pre-sampled
    line = LineString(raw_coords)
    polygon = Polygon(line)
    
    if not polygon.is_valid:
        # Fix self-intersections common in hand-drawn CAD exports
        polygon = polygon.buffer(0)
        
    return polygon.simplify(tolerance, preserve_topology=True)

The buffer(0) operation is a standard computational geometry technique for repairing invalid ring orientations and self-intersections. For detailed topology repair workflows, consult the Shapely Geometry Operations documentation.

Micro-Gap Closure & Topological Validation

Production SVGs contain sub-pixel gaps between adjacent wall segments due to anti-aliasing, export artifacts, or manual drafting imprecision. These gaps prevent planar graph construction and cause wayfinding algorithms to treat adjacent rooms as disconnected.

Snapping & Planarization Pipeline

from shapely.geometry import MultiPolygon, box
from shapely.ops import snap, unary_union

def close_room_gaps(polygons: list[Polygon], snap_tolerance: float = 0.1) -> MultiPolygon:
    """Snap nearby vertices and union overlapping boundaries to form closed rooms."""
    # 1. Snap all geometries to a shared grid/tolerance
    snapped = [snap(p, unary_union(polygons), snap_tolerance) for p in polygons]
    
    # 2. Union to merge collinear walls and eliminate micro-gaps
    merged = unary_union(snapped)
    
    # 3. Extract valid polygons (removes sliver artifacts)
    valid_rooms = [p for p in merged.geoms if isinstance(p, Polygon) and p.area > 1.0]
    return MultiPolygon(valid_rooms)

Diagnostic Checklist:

  • Gap Detection: Use shapely.ops.nearest_points() to measure distances between adjacent wall endpoints. Flag gaps > snap_tolerance for manual review.
  • Sliver Removal: Filter polygons by area threshold (p.area < 1.0 m²) to discard drafting noise.
  • Orientation Enforcement: Ensure all room boundaries follow counter-clockwise (CCW) exterior ring orientation per OGC Simple Features standards.

Wayfinding Graph Generation & Pipeline Integration

Once validated room polygons are established, the next step is extracting adjacency relationships for navigation. Doorways and corridors act as graph edges; room centroids act as nodes.

import networkx as nx
from shapely.geometry import Point

def build_wayfinding_graph(rooms: MultiPolygon, door_coords: list[Point], threshold: float = 1.5) -> nx.Graph:
    """Construct a navigable graph from room polygons and detected doorways."""
    G = nx.Graph()
    
    # Add room nodes
    for i, room in enumerate(rooms.geoms):
        centroid = room.centroid
        G.add_node(f"room_{i}", pos=(centroid.x, centroid.y), area=room.area)
        
    # Connect rooms via doorways
    for door in door_coords:
        connected_rooms = []
        for i, room in enumerate(rooms.geoms):
            if room.contains(door) or room.distance(door) < threshold:
                connected_rooms.append(f"room_{i}")
                
        if len(connected_rooms) >= 2:
            G.add_edge(*connected_rooms, weight=door.distance(Point(0,0)), door=True)
            
    return G

This graph structure integrates directly into SVG/DWG Parsing Workflows where vectorized assets are batch-processed, attribute-mapped, and pushed to real-time topology engines.

Production Diagnostics & Performance Hardening

Deploying boundary extraction at scale requires deterministic error handling, memory-efficient DOM traversal, and async batch orchestration.

Key Production Considerations

  1. Memory Management: Large CAD exports can exceed 50MB. Parse incrementally using lxml.etree.iterparse() to avoid loading the entire DOM into RAM.
  2. Deterministic Logging: Log matrix propagation failures, unclosed paths, and topology repair counts. Use structured JSON logs for pipeline observability.
  3. Validation Gates: Implement a post-extraction validator that checks:
  • Total room area vs. building footprint area (±5% tolerance)
  • Graph connectivity (no isolated subgraphs)
  • Doorway alignment with wall segments
  1. Async Batch Processing: Wrap extraction in asyncio pipelines with semaphore-controlled concurrency to prevent I/O bottlenecks during bulk facility onboarding.

For high-throughput XML/SVG parsing, review the lxml Parsing Documentation for incremental parsing patterns and memory-efficient tree iteration.

By enforcing strict matrix resolution, metric normalization, and topological gap closure, facilities and GIS teams can reliably transform fragmented SVG exports into production-ready indoor navigation graphs. The pipeline scales horizontally, supports real-time topology updates, and provides deterministic diagnostics for automated quality assurance.