Automated Floor Plan Parsing & Vectorization: Engineering Production-Grade Indoor Maps

Indoor mapping and wayfinding systems live or die on the quality of their underlying geometry, and almost every facility hands you that geometry in a form a routing engine cannot consume directly: a scanned PDF, an exploded DWG, an SVG dumped from a BIM authoring tool, or a decade-old TIFF overlay. Turning that heterogeneous pile of drawings into deterministic, topologically valid spatial data is non-trivial precisely because it must be done without a human redrawing each room. At a single-building scale you can paper over gaps by hand; across a portfolio of hundreds of floor levels that refresh on renovation cycles, manual digitization becomes the bottleneck that stalls emergency routing, asset tracking, and space analytics. This guide covers the end-to-end engineering of automated floor plan parsing and vectorization — the ingestion, geometric extraction, semantic enrichment, and routing-graph construction stages that convert raw architectural exports into GeoJSON that downstream wayfinding SDKs can trust.

Layered Pipeline Architecture

A production vectorization system decomposes into five deterministic stages, each with a single responsibility and a typed contract to its neighbour. Decoupling them lets you reprocess a single floor level without re-running an entire portfolio, isolate failures to one stage, and scale the expensive geometry work independently of cheap I/O.

Automated vectorization pipeline: five typed stages A left-to-right data flow. Stage 1 Ingestion and Decode (DWG, SVG, PDF, TIFF) passes a raster mask to Stage 2 Normalize and Calibrate (affine deskew into a metric frame), which passes a calibrated mask to Stage 3 Geometric Extraction (Hough/LSD line detection and contour tracing), which passes a MultiLineString to Stage 4 Semantic Enrichment (room, door, and stair classification), which passes classified polygons to Stage 5 Routing Graph and Delivery, which emits a GeoJSON FeatureCollection. Automated vectorization pipeline — five typed stages raster mask calibrated mask MultiLineString classified polygons 1 2 3 4 5 Ingestion& Decode Normalize& Calibrate GeometricExtraction SemanticEnrichment Routing Graph& Delivery DWG · SVGPDF · TIFF affine deskewmetric frame Hough / LSDcontour trace room · doorstair classes weighted graphtopology audit GeoJSON FeatureCollection
  1. Ingestion & decoding. Format-specific decoders isolate floor levels, resolve block references, and convert proprietary entities into standardized primitives. Native CAD and SVG inputs are handled by the SVG/DWG Parsing Workflows described later in this collection, which preserve drawing hierarchies, extract text annotations, and keep units consistent across multi-building portfolios.
  2. Normalization & calibration. Every drawing is mapped onto a local Cartesian grid with a defined origin, then projected into a consistent Indoor Coordinate Reference System so that adjacent buildings share one frame. Raster inputs are deskewed, contrast-normalized, and morphologically cleaned before any tracing begins.
  3. Geometric extraction. Walls, openings, and boundaries are recovered as vector primitives. Raster sources go through edge detection, contour tracing, and line-segment approximation; vector-native sources read coordinate arrays directly. Reliable opening segmentation depends on the Wall & Door Detection Algorithms covered separately.
  4. Semantic enrichment. Bare polygons are classified into rooms, corridors, doors, stairs, elevators, and service zones, then tagged with operational metadata through Attribute Mapping from Blueprints. Vertical relationships are resolved against level mapping and Z-axis logic so stairs and elevators connect the correct floor levels.
  5. Routing graph & delivery. Classified geometry is converted into a weighted routing graph and serialized as a GeoJSON FeatureCollection. At scale this stage runs behind Async Batch Processing Pipelines so that publishing never blocks live wayfinding traffic.

The three engineering hazards that thread through every stage are format heterogeneity (mixed deliverables in one ingest), geometric ambiguity (dashed property lines vs. structural walls, exploded blocks), and topological consistency (closed polygons, doors that connect exactly two zones). The sections below address each in turn.

Core Data Model & Spatial Schema

Every stage exchanges geometry through one envelope: a GeoJSON FeatureCollection whose properties carry the indoor semantics that routing depends on. Keeping a single schema across the pipeline means a topology validator, a routing-graph builder, and a client SDK all read the same fields. The contract below matches the envelope established across this site and feeds directly into JSON Schema Design for Indoor Maps on the delivery side.

Field Type Notes
feature_id str Stable identifier, survives re-vectorization of the same floor level.
space_class enum One of room, corridor, door, stair, elevator, service.
level int Floor level index; negative values for basements. Drives vertical connectivity.
is_routable bool false flags geometry held back for human review.
room_type str Domain taxonomy value, e.g. office, restroom, lobby.
accessibility_rating str step_free, assisted, or restricted — consumed by accessible routing.
topology_hash str Content hash of the validated geometry; lets the delivery layer detect real changes.
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": { "type": "Polygon", "coordinates": [[[0,0],[6,0],[6,4],[0,4],[0,0]]] },
      "properties": {
        "feature_id": "L2-room-0184",
        "space_class": "room",
        "level": 2,
        "is_routable": true,
        "room_type": "office",
        "accessibility_rating": "step_free",
        "topology_hash": "9f3c2a7e"
      }
    },
    {
      "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [6, 2] },
      "properties": {
        "feature_id": "L2-door-0411",
        "space_class": "door",
        "level": 2,
        "is_routable": true,
        "width": 0.9,
        "accessibility_rating": "step_free",
        "topology_hash": "1bd0c4f5"
      }
    }
  ]
}

Coordinates are stored in the floor level’s local metric frame (metres from a survey-anchored origin), not raw pixels — the calibration step below is what earns that guarantee.

Data Ingestion & Format Standardization

The ingestion layer is the deterministic entry point for every spatial asset. Raw drawings rarely conform to a routable standard: they carry non-georeferenced coordinates, arbitrary scaling factors, and embedded metadata that must be stripped before geometric processing begins. Format-specific decoders isolate floor levels, resolve block references, and convert proprietary entities into standardized primitives while preserving drawing hierarchies, text annotations, and unit consistency.

Coordinate alignment is non-negotiable for downstream routing accuracy. Each floor plan is mapped to a local Cartesian grid with a defined origin — typically a survey control point or building corner. Scaling calibration extracts known reference dimensions (standard door widths, gridline spacing, title-block scales) and applies an affine transform to normalize the pixel-to-metre ratio. Raster inputs need extra preprocessing: deskewing, contrast normalization, and morphological noise reduction to guarantee line continuity before vector tracing starts.

# Production-grade coordinate normalization & affine scaling.
import logging
import cv2
import numpy as np

logger = logging.getLogger("vectorize.ingest")

def calibrate_and_transform(
    image: np.ndarray,
    ref_points: list[tuple[float, float]],
    target_metres: list[tuple[float, float]],
) -> np.ndarray:
    """Compute an affine matrix from matched reference points and warp the raster
    into the floor level's local metric frame.

    ref_points pair pixel coordinates with their known real-world position in
    target_metres. At least three non-collinear pairs are required.
    """
    if len(ref_points) < 3 or len(ref_points) != len(target_metres):
        raise ValueError("Need >= 3 matched (pixel, metre) reference pairs for affine calibration")

    src = np.asarray(ref_points, dtype=np.float32)
    dst = np.asarray(target_metres, dtype=np.float32)
    try:
        matrix, inliers = cv2.estimateAffine2D(src, dst)
    except cv2.error as exc:  # malformed point arrays / degenerate configuration
        logger.exception("OpenCV affine estimation failed: %s", exc)
        raise

    if matrix is None:
        raise ValueError("Affine calibration produced no solution (collinear reference points?)")

    logger.info("Calibrated floor level with %d/%d inlier reference points",
                int(inliers.sum()) if inliers is not None else 0, len(src))
    return cv2.warpAffine(image, matrix, (image.shape[1], image.shape[0]))

Geometric Feature Extraction

Once normalized, the pipeline recovers structural primitives. Raster vectorization combines edge detection, contour tracing, and line-segment approximation: the probabilistic Hough transform or a Line Segment Detector (LSD) extracts continuous wall boundaries, while morphological thinning reduces thick architectural strokes to single-pixel centrelines. Vector-native inputs skip rasterization entirely and read coordinate arrays straight from DXF or SVG primitives.

Extraction has to survive occlusions, overlapping drawing content, and dashed-line conventions — distinguishing a property line from a structural wall, or a swing arc from a door leaf. Correctly segmenting openings from continuous barriers (the job of the Wall & Door Detection Algorithms collection) is what prevents false routing blocks. Production systems take a two-pass approach: extract continuous polylines first, then intersect them to find junctions and micro-gaps.

# Line-segment extraction & polyline chaining with gap snapping.
import logging
import numpy as np
from shapely.geometry import LineString, MultiLineString
from shapely.ops import linemerge

logger = logging.getLogger("vectorize.extract")

def extract_and_chain_lines(
    binary_mask: np.ndarray,
    min_length: float = 0.5,
    snap_tol: float = 0.15,
) -> MultiLineString:
    """Extract wall segments via the probabilistic Hough transform, merge collinear
    runs, and drop sub-threshold fragments left by anti-aliasing.
    """
    import cv2

    try:
        lines = cv2.HoughLinesP(
            binary_mask, rho=1, theta=np.pi / 180,
            threshold=50, minLineLength=20, maxLineGap=10,
        )
    except cv2.error as exc:  # non-binary or empty mask
        logger.exception("Hough transform failed on mask shape %s: %s", binary_mask.shape, exc)
        raise

    if lines is None:
        logger.warning("No line segments detected; returning empty geometry")
        return MultiLineString()

    segments = [LineString([(x1, y1), (x2, y2)]) for ln in lines for x1, y1, x2, y2 in ln]
    merged = linemerge(segments)
    # linemerge collapses to a LineString when the input forms one chain;
    # normalize to an iterable of LineStrings either way.
    merged_lines = list(merged.geoms) if hasattr(merged, "geoms") else [merged]

    kept = [ln for ln in merged_lines if ln.length >= min_length]
    logger.info("Extracted %d wall segments (snap_tol=%.3f m)", len(kept), snap_tol)
    return MultiLineString(kept)

Semantic Enrichment & Routing Graph Construction

Geometric primitives alone cannot drive wayfinding. The pipeline classifies extracted shapes into spatial categories — rooms, corridors, doors, stairs, elevators, service zones — using spatial reasoning over polygon topology, aspect ratios, and adjacency. Systematic Attribute Mapping from Blueprints then attaches the metadata that routing and analytics consume (room_type, occupancy_limit, accessibility_rating), aligned to the controlled vocabularies defined in the POI taxonomy work.

Graph construction turns classified polygons into a navigable routing graph. Nodes are decision points (doorways, intersections, elevator lobbies); edges are traversable paths weighted by distance, accessibility constraints, or live congestion. An R-tree spatial index over feature bounds keeps adjacency queries near-linear in floor area rather than quadratic.

# Routing-graph construction from room polygons & door geometry.
import logging
import geopandas as gpd
import networkx as nx
from rtree import index

logger = logging.getLogger("vectorize.graph")

def build_routing_graph(
    rooms_gdf: gpd.GeoDataFrame,
    doors_gdf: gpd.GeoDataFrame,
) -> nx.Graph:
    """Build an undirected routing graph: rooms become nodes, doors that touch two
    rooms become weighted edges. Door width seeds the accessibility weight.
    """
    if "geometry" not in rooms_gdf or "geometry" not in doors_gdf:
        raise KeyError("Both GeoDataFrames must carry a 'geometry' column")

    graph = nx.Graph()
    idx = index.Index()
    for room_id, row in rooms_gdf.iterrows():
        centroid = row.geometry.centroid
        graph.add_node(room_id, coords=(centroid.x, centroid.y),
                       space_class=row.get("room_type", "unknown"))
        idx.insert(int(room_id), row.geometry.bounds)

    edges = 0
    for _, door in doors_gdf.iterrows():
        try:
            touching = [r for r in idx.intersection(door.geometry.bounds)
                        if rooms_gdf.loc[r, "geometry"].intersects(door.geometry)]
        except KeyError as exc:  # index references a dropped room id
            logger.warning("Door %s referenced missing room: %s", door.get("feature_id"), exc)
            continue
        for a, b in zip(touching, touching[1:]):
            graph.add_edge(a, b, weight=float(door.get("width", 1.0)))
            edges += 1

    logger.info("Routing graph built: %d nodes, %d edges", graph.number_of_nodes(), edges)
    return graph

Validation & Failure Modes

Indoor routing engines fail catastrophically on non-planar graphs, unclosed polygons, or dangling edges — and the failures surface downstream, in a client SDK, long after the bad data was published. The pipeline therefore enforces topological rules before anything is serialized. The most useful mental model is a checklist of what breaks when each stage is skipped or misconfigured:

  • Skip calibration → geometry lands in pixel space; door widths read as hundreds of “metres” and every weight in the routing graph is meaningless.
  • Skip gap snapping → sub-pixel breaks in walls leave corridors that look closed to a human but route through the wall, because the polygon never actually closed.
  • Skip opening segmentation → doors are absorbed into wall runs; rooms become sealed islands with zero edges and the graph fragments into disconnected components.
  • Skip Z-axis resolution → stairs and elevators connect the wrong floor levels, producing routes that teleport between buildings.
  • Skip the topology audit → self-intersecting polygons reach the SDK and the wayfinding engine throws a TopologyException mid-route.

The audit runs in three stages: geometric integrity (shapely.is_valid / make_valid), connectivity (every routing-graph component is reachable), and semantic consistency (flag rooms with no door, corridors with no endpoint). Failures are either auto-repaired with a buffer-simplify pass or routed to a human-in-the-loop queue with exact coordinate annotations.

# Topology validation & repair before serialization.
import logging
import geopandas as gpd
from shapely.validation import make_valid

logger = logging.getLogger("vectorize.validate")

def validate_and_repair(polygons: gpd.GeoDataFrame) -> tuple[gpd.GeoDataFrame, list[int]]:
    """Return repaired geometry plus the ids that needed repair, for the review queue."""
    try:
        valid_mask = polygons.geometry.is_valid
    except AttributeError as exc:  # missing/empty geometry column
        logger.exception("GeoDataFrame has no usable geometry column: %s", exc)
        raise

    invalid_ids = polygons[~valid_mask].index.tolist()
    repaired = polygons.copy()
    repaired.loc[~valid_mask, "geometry"] = (
        repaired.loc[~valid_mask, "geometry"].apply(make_valid)
    )
    logger.info("Validated %d polygons, repaired %d", len(polygons), len(invalid_ids))
    return repaired, invalid_ids

Operational Considerations

Enterprise deployments process thousands of floor levels across distributed portfolios, so the pipeline runs asynchronously: ingestion, parsing, validation, and publishing are decoupled stages behind Async Batch Processing Pipelines, letting facilities teams queue updates without blocking live wayfinding services. Message brokers (RabbitMQ, Redis Streams) paired with worker orchestration (Celery, Dask) give horizontal scaling across containerized workers.

Facilities are dynamic — renovations, temporary closures, and emergency rerouting demand live updates. Rather than re-vectorizing an entire floor level, incremental topology patches let IoT sensors, access-control logs, and maintenance tickets trigger targeted graph edits. Each published artifact carries a topology_hash, so the delivery layer can tell a genuine geometry change from a no-op re-export and invalidate caches precisely; that signal is exactly what cache invalidation strategies key on. Promotions to production are gated by CI Gating for Map Updates, which re-runs the topology audit and connectivity checks on every candidate dataset before it can reach a client.

Version-controlled spatial datasets (GeoPackage, or PostGIS with temporal extensions) keep the audit trail while holding routing latency under a second. For standards compliance, published outputs align with the OGC Simple Features specification and lean on GDAL/OGR for coordinate transformation and format interoperability, so downstream systems — commercial SDKs, custom Python routing engines, or CMDB integrations — consume the data without proprietary lock-in.

Frequently Asked Questions

Can a single pipeline ingest both raster scans and vector CAD in one run?

Yes — that is the point of the normalization contract. Raster sources (scanned PDF, TIFF) are deskewed, calibrated, and traced into primitives; vector sources (DWG, DXF, SVG) read coordinate arrays directly. Both branches converge on the same UnifiedGeometry representation before semantic enrichment, so the rest of the pipeline never needs to know the original format.

How do I keep coordinates consistent across buildings in one campus?

Calibrate every floor level into a shared metric frame and project it into a campus-wide Indoor Coordinate Reference System. Anchor each drawing to a survey control point rather than its own arbitrary origin, otherwise adjacent buildings will overlap or drift apart when rendered together.

What causes a routing graph to fragment into disconnected components?

Almost always missed openings. If door geometry is absorbed into a wall run, the affected rooms get zero edges and split off as isolated nodes. Run robust Wall & Door Detection Algorithms and a connectivity audit so every traversable space is reachable before you publish.

When should geometry be auto-repaired versus sent to human review?

Auto-repair deterministic, low-risk defects — self-intersections, micro-gaps inside the navigation tolerance — with make_valid and buffer-simplify passes. Anything that changes navigability (an unclosed room boundary, an ambiguous opening) should be flagged is_routable=false and queued for review with exact coordinates, never silently guessed.

How does vectorization stay in sync with facility changes?

Use incremental topology patches keyed on the topology_hash. A renovation ticket or sensor event triggers a localized re-vectorization of only the affected zone; the new hash signals the delivery layer to invalidate just the caches that changed, which is far cheaper than republishing an entire floor level.

Which output format should downstream wayfinding SDKs consume?

A GeoJSON FeatureCollection with the indoor properties schema shown above, validated against the contract in JSON Schema Design for Indoor Maps. For high-volume tile delivery, serialize to FlatGeobuf or vector tiles from the same validated source so the geometry never diverges.

This page is the overview for the Automated Floor Plan Parsing & Vectorization section, part of the wider Indoor Mapping & Wayfinding Automation reference covering architecture standards and production-ready deployment.