30% off your first 3 months

Get the deal

Map a room with a 2D LiDAR

by DuckyHelperNov 12, 20264 min read
Robotics

TL;DR:

  • A 2D LiDAR gives a list of (angle, distance) pairs. Rotate them by the robot's heading and add its position to get world coordinates.

  • An occupancy grid divides the floor into cells. The cell where a beam ends is likely occupied, and the cells along the beam are likely free.

  • If the robot's own position estimate drifts, the map smears. Drive slowly and keep turns gentle.

A robot with a distance sensor knows what is in front of it. A robot with a map knows what is in the room. The step between them is bookkeeping: every distance reading has to be placed in a shared picture of the room, in the right spot, using where the robot was when it took the reading.

What you will build

You will drive a small wheeled robot slowly around a room with a spinning 2D LiDAR on top, and build an occupancy grid: a grid of small squares, each marked free, occupied or unknown. If you have never looked at what the sensor actually outputs, read what a LiDAR actually measures first.

Parts and tools

  • A wheeled robot base with wheel encoders (see the encoder build in this series)

  • A hobby 2D LiDAR designed for small robots, powered the way its manual says

  • A small computer such as a single-board computer, or a laptop that rides on the robot, to run the mapping code

  • A room with walls and a few boxes, and a tape measure to check the map afterwards

LiDAR safety: Use only a LiDAR whose documentation states it is eye safe, and never open the unit or look into the emitter on purpose. Do not modify it. Keep its spinning head clear of fingers and cables.

Wiring and assembly

Mount the LiDAR high enough to see over the robot's own parts, with a clear 360 degree view, and level. Measure where its center is relative to the middle of the wheel axle. That offset goes into your transform, and a wrong offset shifts the whole map.

Reading a scan

Each reading is an angle and a distance, measured from the sensor. Ignore readings that are zero or beyond the sensor's stated range: they mean the beam found nothing, not that there is a wall at distance zero. Glass, mirrors and very dark surfaces can also return nothing or something wrong, so expect gaps.

Worked example: one beam into one cell

Choose a grid of 5 cm cells covering a 4 m by 4 m area, which is 4 ÷ 0.05 = 80 cells each way. The world origin is the bottom-left corner. The robot stands at (1.0 m, 1.0 m) facing 90 degrees (along +y). One beam leaves at +30 degrees relative to the robot and returns a distance of 2.0 m.

  1. World angle of the beam: 90° + 30° = 120°.

  2. Endpoint x: 1.0 + 2.0 × cos 120° = 1.0 − 1.0 = 0.0 m.

  3. Endpoint y: 1.0 + 2.0 × sin 120° = 1.0 + 1.732 = 2.732 m.

  4. Cell index: x: 0.0 ÷ 0.05 = 0, y: 2.732 ÷ 0.05 = 54.6, so cell (0, 54).

Check it with Pythagoras: the displacement from the robot is (−1.0, 1.732), and 1.0² + 1.732² = 1.0 + 3.0 = 4.0, whose square root is 2.0 m, the measured distance. The cell (0, 54) is marked occupied, and every cell the beam passed through between the robot and that point is marked free.

The code, step by step

  1. Convert each beam: Add the robot's heading to the beam angle, then use cosine and sine to get the endpoint relative to the robot, then add the robot's position (and the LiDAR's offset from the axle).

  2. Walk along the beam: Step from the robot's cell to the endpoint cell using a line algorithm (Bresenham's is the common one) and nudge each cell on the way toward free.

  3. Mark the end: Nudge the end cell toward occupied. Keep a number per cell (a log-odds value is the usual choice) rather than a yes or no, so one noisy beam cannot flip a cell.

  4. Repeat for every beam and every scan: The numbers accumulate. Walls become consistent dark lines, free space becomes clear.

import math
CELL = 0.05

def beam_to_cell(rx, ry, heading, beam_angle, dist):
    a = heading + beam_angle
    x = rx + dist * math.cos(a)
    y = ry + dist * math.sin(a)
    return int(x / CELL), int(y / CELL)

print(beam_to_cell(1.0, 1.0, math.radians(90), math.radians(30), 2.0))
# (0, 54)

The endpoint sits almost exactly on the map edge (x is zero up to floating point rounding), and int() truncates toward zero, so it lands in cell 0. A real program should also check that the cell is inside the grid before writing to it. Real maps are fuzzy at the cell scale, and edges are where they are fuzziest.

Test, measure and fix

View the grid as an image. Walk the tape measure along one wall and compare: if a 3.00 m wall shows as 3.00 m ± a cell or two, your scale and offsets are right. If walls appear doubled or smeared after a turn, the heading estimate is off. Wheel encoders drift, especially when wheels slip in turns, so slow turns and a firm floor help a lot.

Make it better

  • Add a gyroscope to improve the heading between scans.

  • Match each new scan against the existing map to correct the robot's position. This is what full SLAM systems do, and it is how they avoid the smearing you see here.

  • Save the grid and use it for path planning around the boxes.

Stay safe

Run the first tests with the robot on blocks, wheels off the floor. Drive slowly in a cleared room and keep a hand near the power switch, since a robot that is watching the map may not be watching your foot.

Frequently asked questions

Why do my walls look thick?

Each scan marks the wall cell a little differently because of sensor noise and position error. Averaging over many scans narrows it, and better position estimates narrow it more.

Do I need ROS for this?

No. The example above is plain geometry and a grid. ROS packages can do the mapping for you later, once you understand what they are doing.

Get started

DuckyHelper can
do more

It sees your screen and works in any app you use. Try it for 10 minutes, no card.

Get it for Mac, Windows or Linux

Share

Questions and jobs

Do anything

Ask it a question or hand it a job, in any app.

Doing jobs for you is in early access on Mac. Asking works on Mac, Windows and Linux.