How to Define an Indoor CRS for Multi-Building Campuses
This page covers the one technique that keeps a campus routable end to end — collapsing every building’s private drawing frame onto a single surveyed plane — and it sits under Indoor Coordinate Reference Systems within the broader Indoor Mapping Architecture & Standards reference. When that plane is shared, a corridor in Building A and a skybridge into Building B agree on what “one metre north” means; when it is not, the routing graph silently fragments at every building boundary.
Concept Definition
A campus-scale indoor CRS is a single facility-local Cartesian frame, metric, orthogonal, and anchored to one surveyed origin, into which every building’s geometry is transformed before it reaches a spatial database or routing graph. The origin is fixed to a permanent reference — a surveyed geodetic control marker, a structural column-grid intersection, or a high-precision GNSS point — and never moves once published.
Each building arrives in its own local frame, carrying its own drawing unit, (0,0) anchor, and User Coordinate System rotation. Unifying them is a per-building 2D similarity (Helmert) transform — translation, rotation, and a single uniform scale — solved from surveyed control points, plus a shared vertical datum so floor levels stack consistently. The transform is valid only when its residual error is small; an affine transform is the fallback when a building’s drawing carries non-uniform scale (different X and Y units), at the cost of one extra degree of freedom. Three invariants must hold across the whole campus:
- Consistent horizontal datum — all CAD/BIM exports project to a common local tangent plane or state-plane system before the per-building offset is applied.
- Deterministic vertical datum — floor elevations resolve to one vertical reference (NAVD88, EGM2008, or a surveyed site benchmark) with
Z = 0at the ground-floor finished floor level. - Scale preservation — legacy export artifacts (1:100 vs 1:50 mismatches) are absorbed by explicitly solving for uniform scale, never by eyeballing.
Minimal Working Example
The core technique is solving the per-building similarity transform from at least three non-collinear control points and reporting the residual RMS in metres. This self-contained routine returns the 3×3 affine matrix and the RMS, the single number that gates whether the building may be published.
import logging
from typing import Tuple
import numpy as np
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def solve_campus_helmert(
source: np.ndarray, target: np.ndarray
) -> Tuple[np.ndarray, float]:
"""Solve a 2D similarity (Helmert) transform mapping a building's local
frame onto the surveyed campus plane; return (3x3 affine, RMS in metres)."""
if source.shape[0] < 3:
raise ValueError("Need >=3 non-collinear control points for scale+rotation.")
src_c = source - source.mean(axis=0)
tgt_c = target - target.mean(axis=0)
a = np.sum(src_c[:, 0] * tgt_c[:, 0] + src_c[:, 1] * tgt_c[:, 1])
b = np.sum(src_c[:, 0] * tgt_c[:, 1] - src_c[:, 1] * tgt_c[:, 0])
denom = np.sum(src_c ** 2)
if denom == 0:
raise ValueError("Degenerate control points: source points coincide.")
scale = np.hypot(a, b) / denom
theta = np.arctan2(b, a)
rot = scale * np.array([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]])
trans = target.mean(axis=0) - rot @ source.mean(axis=0)
fitted = (rot @ source.T).T + trans
rms = float(np.sqrt(np.mean(np.sum((fitted - target) ** 2, axis=1))))
logging.info("scale=%.6f theta=%.4f rad RMS=%.4f m", scale, theta, rms)
return np.vstack([np.column_stack([rot, trans]), [0, 0, 1]]), rms
Call it once per building with that building’s surveyed control points (source = local drawing coordinates, target = campus-plane coordinates), then refuse to publish any building whose RMS exceeds the threshold below.
Parameter & Threshold Reference
| Parameter | Type | Default / Threshold | Notes |
|---|---|---|---|
source, target |
np.ndarray shape (N, 2) |
— | Paired control points in metres; N ≥ 3, non-collinear. |
| Control points per building | int |
3 minimum, 5+ recommended |
Extra points let RMS detect a bad pick; collinear sets are rejected. |
max_rms_threshold |
float |
0.15 m |
Publish gate. Above this, the transform is unfit for routing. |
| Horizontal datum | EPSG / string | local tangent plane or state plane | Common across all buildings before per-building offset. |
| Vertical datum | string | NAVD88 / EGM2008 / site benchmark |
One reference campus-wide; mixing them stacks floors wrong. |
Ground-floor Z |
float |
0.0 m at FFL |
Logical level indices (B1, L1, L2) map to physical Z-offsets. |
| Portal coordinate tolerance | float |
0.0 m (exact parity) |
Skybridge/tunnel endpoints must be byte-identical in both graphs. |
| Z-plane bbox overlap | float |
0.3 m horizontal max |
Larger overlap between adjacent floor levels signals datum misalignment. |
| Transform model | enum | helmert | affine |
Use affine only when X and Y scales genuinely differ. |
Common Errors & Fixes
RuntimeError: RMS 0.83m exceeds threshold 0.15m — almost always a scale artifact: one building’s drawing was exported at 1:50 while the campus grid assumes 1:100, so its geometry is uniformly stretched. Because the Helmert solver returns scale explicitly, inspect it — a value near 2.0 or 0.5 confirms a unit/scale mismatch rather than a survey error. Re-export the offending DWG at the correct scale (or pre-multiply its coordinates) and re-solve; do not raise the threshold to force a pass.
Stairwell routing failures / “elevator shaft clipping” — the horizontal transform is clean but the vertical datum is not, so two floor levels occupy overlapping Z ranges. Flag the geometry whose Z falls outside its building envelope before ingestion:
import logging
def flag_z_outliers(features: list[dict], z_min: float, z_max: float) -> list[str]:
"""Return ids of features whose elevation escapes the building envelope."""
bad = [f["id"] for f in features
if not (z_min <= f["properties"]["z"] <= z_max)]
if bad:
logging.warning("Z-datum misaligned: %d feature(s) outside envelope", len(bad))
return bad
If this returns more than ~5% of a building’s features, the vertical datum is offset from the horizontal CRS — cross-reference laser-scanned point clouds against the CAD baseline to find the systematic shift, and resolve floor indices the way Converting CAD Elevations to Indoor Z-Levels describes.
Graph disconnection at a skybridge or tunnel — both buildings solve cleanly in isolation, yet the routing graph splits into two components. The cause is a portal node whose coordinates differ by a few centimetres between the two building datasets. Cross-building transitions need dedicated portal nodes whose coordinates are exactly shared, expressed in the campus GeoJSON envelope so both graphs snap to one vertex:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "portal-skybridge-A3-B2",
"geometry": { "type": "Point", "coordinates": [1042.50, 2080.75] },
"properties": { "kind": "skybridge", "z": 7.20, "level_id": "L2",
"connects": ["bldg-A", "bldg-B"] }
}
]
}
Integration Point
This transform is the contract every downstream stage assumes. Geometry only reaches it after vector ingestion — the SVG/DWG parsing workflows deliver a unit-aligned FeatureCollection, and the campus CRS then anchors it to the surveyed datum. Vertical resolution is handed to Level Mapping & Z-Axis Logic, which turns clustered Z-planes into routing-ready level IDs. Once geometry is on the shared plane, Z-axis routing weights and the exact-parity portal nodes above feed the wayfinding engine, while Fallback Routing Architectures rely on consistent grid alignment so dead-reckoning corridor vectors stay valid when RTLS or BLE positioning drops out. Publish the result through the envelope defined in JSON Schema Design for Indoor Maps so the campus plane and its POI placements stay interoperable across SDKs.
Frequently Asked Questions
How many control points does one building need?
Three is the arithmetic minimum for a similarity transform to be over-determined enough to produce a residual at all, and five to eight is what you want in practice. The reason is not precision but detectability: with exactly two points the transform fits perfectly by construction and the residual is zero whether or not one of the points was mistyped. With five or more, a single bad point stands out as an outlier against its peers, which is what makes the per-point residual view useful. Spread them across the building rather than clustering them near one entrance — control points concentrated in one corner constrain rotation poorly, so the far end of the building can be metres out while the aggregate RMS still looks healthy.
What do I do when a building fails the RMS gate?
Look at the per-point residuals before touching the transform. A single point an order of magnitude worse than the rest is a transcription error and should be removed and re-surveyed. Residuals that are uniformly poor point at a scale problem — usually a drawing in feet declared as metres, or a 1:50 export treated as 1:100 — which is fixed at the unit-scaling stage, not by the solve. Residuals that grow steadily with distance from the control cluster mean the drawing itself is distorted, which happens with plans that were traced from a scan. What you should not do is switch to an affine transform to make the number go down: the extra degrees of freedom absorb the error and publish the distortion instead of reporting it.
Can two buildings on one campus use different vertical datums?
They can be surveyed against different benchmarks, but they must be reconciled to one before publication, and the reconciliation belongs in the ingest rather than in the router. Vertical datums differ by tens of centimetres to a couple of metres between local benchmarks, which is small enough to look plausible and large enough to put a skybridge half a storey out of alignment. Resolve every building’s floor elevations onto one site datum with Z = 0 at the ground-floor finished floor level of a nominated reference building, and record the offset applied to each. Level indices then stack consistently across the campus, which is what level mapping and Z-axis logic assumes when it turns elevations into storeys.
Related
- Converting CAD Elevations to Indoor Z-Levels
- Best Practices for Indoor POI Taxonomy
- Fallback Routing Architectures
This page sits under the Indoor Coordinate Reference Systems collection, part of the Indoor Mapping Architecture & Standards reference.