FixTheRobot Explore Robotics / Systems Engineering / Engineering Guides / Module 12

Engineering Guides · Module 12

Frame definitions and indoor/outdoor map alignment

An indoor map is in its own frame. To use GNSS and the map together, fit one rigid transform between them from points you know in both.

  • Intermediate
  • Module 12: Outdoor-to-Indoor Localization & GNSS-Denied Navigation
  • Written for ROS 2 Jazzy where ROS 2 is used
  • Sources checked 2026-11-06

Assumptions: Numbers in the examples are illustrative and the code is executed by the site’s tests. Facts taken from a cited source are marked; other statements are general engineering knowledge and must be confirmed on the datasheet of the part you use.

1. Engineering problem

GNSS reports latitude and longitude on the WGS 84 ellipsoid; an indoor map is an arbitrary Cartesian frame. Without a measured alignment the robot jumps by the offset when it hands over between them. The alignment is a rotation and a translation that can be found from a few control points.

2. When is this needed?

  • Connecting an indoor map to GNSS coordinates.
  • When the handover jump is constant at one place.
  • Adding a new building.

3. Selection and design decisions

The NavSatFix definition (Jazzy, read 2026-11-05) gives latitude and longitude in degrees on the WGS 84 ellipsoid, altitude in metres, and position covariance in East-North-Up order; the header frame is the antenna’s frame, not a global one. REP 105 gives the frames map (non-drifting, may jump) and odom (continuous). A local tangent-plane (ENU) frame from latitude and longitude is the usual bridge; for areas of a few hundred metres an equirectangular approximation is adequate, for larger areas use a proper geodetic library. The alignment of the indoor map to the ENU frame is a 2-D rotation plus translation fitted from control points that are known in both frames.

Overview
FrameDefinitionSource
WGS 84 lat/lonGlobal geodetic coordinatesGNSS receiver
ENU (local tangent plane)East, north, up in metres from a chosen originComputed from lat/lon
mapFixed world frame used by the robot (non-drifting)Chosen to be the ENU frame or the indoor map frame
indoor map frameFrame of the saved indoor mapThe mapping session
odomContinuous, drifting local frameWheel, IMU, LiDAR odometry

4. Advantages and disadvantages

  • Make the indoor map the global map frame and register GNSS into it, or make ENU the map and register the indoor map into it: choose one and keep it everywhere.
  • More control points reduce the error from noisy points and cost survey time.
  • Surveyed points beat points picked from satellite imagery.

5. Requirements and constraints

  • At least two non-identical control points (three or more to see the residual).
  • Control points known to better than the accuracy you need.
  • The same transform is used by every node (published as a static transform).

6. Architecture

lat/lon ─▶ ENU (origin lat0, lon0) ─▶ fitted rigid transform (yaw, translation) ─▶ indoor map frame

7. Implementation

  1. Choose the ENU origin and the global frame convention.
  2. Pick control points at the entrance and inside that you can measure in both frames.
  3. Convert the GNSS points to ENU and fit the yaw and translation (below).
  4. Check the residuals and add points if they exceed your tolerance.
  5. Publish the transform as a static transform and verify it by walking to a known point.
import math
R = 6371000.0
def enu(lat, lon, lat0, lon0):                          # equirectangular approximation, adequate over a few hundred metres, not survey grade
    x = math.radians(lon - lon0) * R * math.cos(math.radians(lat0))
    y = math.radians(lat - lat0) * R
    return x, y
def fit(A, B):                                          # 2-D rigid transform with B ~ R(theta) A + t from matched control points
    n = len(A)
    ca = (sum(p[0] for p in A) / n, sum(p[1] for p in A) / n); cb = (sum(p[0] for p in B) / n, sum(p[1] for p in B) / n)
    sxx = sum((a[0] - ca[0]) * (b[0] - cb[0]) + (a[1] - ca[1]) * (b[1] - cb[1]) for a, b in zip(A, B))
    sxy = sum((a[0] - ca[0]) * (b[1] - cb[1]) - (a[1] - ca[1]) * (b[0] - cb[0]) for a, b in zip(A, B))
    th = math.atan2(sxy, sxx)
    c, s = math.cos(th), math.sin(th)
    t = (cb[0] - (c * ca[0] - s * ca[1]), cb[1] - (s * ca[0] + c * ca[1]))
    return th, t
lat0, lon0 = 48.0000, 11.0000
ctrl_gnss = [(48.00010, 11.00000), (48.00010, 11.00020), (48.00000, 11.00020)]
A = [enu(la, lo, lat0, lon0) for la, lo in ctrl_gnss]
th_true = math.radians(30); t_true = (5.0, -2.0)
B = [(math.cos(th_true) * x - math.sin(th_true) * y + t_true[0] + dx, math.sin(th_true) * x + math.cos(th_true) * y + t_true[1] + dy) for (x, y), (dx, dy) in zip(A, [(0.02, -0.01), (-0.03, 0.02), (0.01, 0.03)])]
th, t = fit(A, B)
res = [math.dist(B[i], (math.cos(th) * A[i][0] - math.sin(th) * A[i][1] + t[0], math.sin(th) * A[i][0] + math.cos(th) * A[i][1] + t[1])) for i in range(3)]
print("ENU of the control points (m):", [(round(x, 2), round(y, 2)) for x, y in A])
print("fitted yaw %.2f deg, translation (%.2f, %.2f) m" % (math.degrees(th), t[0], t[1]))
print("residuals (m):", [round(r, 3) for r in res])

Printed (checked by the site’s tests):

ENU of the control points (m): [(0.0, 11.12), (14.88, 11.12), (14.88, 0.0)]
fitted yaw 30.16 deg, translation (5.03, -2.00) m
residuals (m): [0.016, 0.014, 0.006]

Reading it: three control points about 15 m apart convert to ENU coordinates (0, 11.12), (14.88, 11.12) and (14.88, 0); a fit against the same points measured in the indoor frame (with a few centimetres of noise) recovers a yaw of 30.16° (true 30°) and a translation of (5.03, −2.00) m, with residuals under 2 cm. The equirectangular conversion is an approximation, and the numbers are illustrative.

8. Real examples

  • Hospital entrance: three painted markers inside and outside the main door measured with RTK GNSS and in the map frame.

9. Failure scenarios

  • Using only one control point, so the rotation is unknown.
  • Control points picked from imagery with metre-level error.
  • Different parts of the system using different transforms.

10. Troubleshooting

  1. If the jump at the door is a constant vector, the translation is off.
  2. If the error grows with distance from the control points, the rotation is off.
  3. Check the lat/lon origin and the axis order (east, north).

11. Validation

  • The fit example prints the lines shown (executed by the site tests).
  • A robot driven to a surveyed point reports it within the tolerance in both frames.

12. Engineering checklist

13. Further references

  • REP 105, Coordinate Frames for Mobile Platforms: rep-0105.rst, read 2026-11-05.
  • ROS 2 sensor_msgs/NavSatFix and NavSatStatus (Jazzy): NavSatFix.msg, read 2026-11-05.

Related on FixTheRobot