Probability for Robotics · Theory · M10
M10-T1 — Occupancy Grids and the Binary Bayes Filter
A map as a probability per cell, updated by adding log odds.
An occupancy grid divides the world into cells and keeps, for each, the probability that it is occupied. It is the simplest useful map, the one most navigation stacks plan on, and it is just the binary Bayes filter of Module 3 run on every cell independently. This lesson derives the log-odds update from that filter, builds the inverse sensor model, traces rays through the grid, and shows what a map looks like after one, three and ten scans.
Learning objectives
After this lesson, you should be able to:
- Write the occupancy of a cell as a binary Bayes filter and convert it to log odds, where an update is an addition.
- Define the inverse sensor model and explain why the prior must be subtracted.
- Update a grid from a range beam: free cells along the ray, an occupied cell at its end.
- Explain clamping and how it lets the map follow a changing world.
The intuition
A range scanner fires a beam and gets a range. The beam travelled through empty space up to that range, so every cell along it is probably free; at the end, it hit something, so that cell is probably occupied. Beyond that nothing is known. Do this for hundreds of beams from many poses and the walls build up as cells that are repeatedly seen occupied, and free space as cells repeatedly seen empty.
Each reading is uncertain (the sensor can be wrong, a beam can graze a corner), so one reading only nudges the probability of a cell: from 0.5 to 0.7 for “occupied”, to 0.3 for “free”. Several agreeing readings push it further. The tool that does this exactly is Bayes' rule, and the form that makes it easy is log odds.
The left panel follows a single cell that is a closed door for 15 readings and then opens. The sensor is right 80% of the time. The log-odds sum climbs steadily while the door is closed, to a probability of 1.000 at reading 15. When the door opens, the evidence has to be un-learned reading by reading. With the limit of ±10 on the log odds (practically no limit), the probability is still 0.97 at reading 20, and falls below 0.5 only around reading 25; with a clamp at ±2 the cell recovers within five readings. Clamping is a deliberate loss of certainty to keep the map able to change.
The right panel shows a map being built. One scan from one pose sees only the walls in line of sight; 79 of the wall cells are found and 80 missed, and 881 of the 1 584 cells have never been seen. After three poses nearly all are found (7 missed). After ten, the walls are complete and no wall cell is missing.
A grid map is a collection of independent binary Bayes filters, one per cell. Independence is an approximation (a wall is a joint object) that makes the problem cheap and works well.
Definitions
- Cell
- A binary variable: occupied (1) or free (0). The map is .
- Occupancy
- The probability that the cell is occupied given all readings and poses (the poses are known here).
- Log odds
- : zero at probability ½; additive evidence.
- Inverse sensor model
- What a single reading says about a cell, from the sensor’s point of view: 0.7 for the cell at the end of the beam, 0.3 for the cells before it, and no information (0.5) for the rest.
- Clamping
- Limiting the log odds to so that a cell can change its mind.
Derivation: the log-odds update from Bayes’ rule
For one cell (index dropped), apply Bayes' rule to the latest reading , given the earlier ones.
Bayes’ rule with the earlier readings in the background (the Bayes filter of Module 6, for a static state).
Given the cell’s state, the readings are conditionally independent (the Markov assumption).
Bayes again, to express the likelihood through the inverse sensor model , which is what is easy to specify.
Divide the equation for by that for : the normaliser and cancel. This is the odds form of Module 3, lesson 4.
Take logarithms. Multiplication of odds becomes addition of log odds, and the prior odds are subtracted because the inverse sensor model has already included the prior once.
Why subtract the prior
With the term vanishes. With a different prior, for example (most cells are free), the inverse sensor model quoted as a probability already contains the prior; without the subtraction, each reading would add the prior's odds again and the cell would drift towards 0 or 1 even with uninformative readings.
Rays through the grid
A beam from a cell to the cell where it ended passes through the cells found by a line-drawing algorithm (Bresenham). The cells before the end get the “free” reading (0.3, log odds ≈ −0.85) and the last cell the “occupied” reading (0.7, ≈ +0.85). A reading at maximum range is a miss: all its cells are free and none is occupied.
Assumptions and their cost
- Independent cells. The posterior of the whole map is taken as the product of the posteriors of the cells. Walls are correlated, so the grid cannot say “this is a wall, so its neighbours probably are too”, but this keeps the map update linear in the number of cells touched.
- Known poses. The derivation conditions on the poses. With a drifting pose estimate the walls smear (next lessons).
- Static world. The update is for a cell with a fixed state, so old evidence is never forgotten except through clamping.
Occupancy mapping is the binary Bayes filter per cell, in log odds so that fusing a reading is one addition. The clamp is not a technicality: it is the map’s memory length.
A worked example
Two programs with the library's .
- One cell. The sensor says “occupied” with probability 0.8 while the door is closed (readings 1 to 15) and 0.2 afterwards. Each reading is turned into 0.7 or 0.3. The unclamped cell (±10) goes to 0.927 after 5 readings, 0.994 after 10 and 1.000 after 15. After the door opens it is still 0.967 at reading 20 and 0.300 at reading 25, as its large evidence is paid back. The cell clamped at ±2 stays at 0.881 from reading 5 to 15, and is at 0.119 by reading 20.
- A room. A room of 10 × 8 m with a divider, a grid of 44 × 36 cells of 25 cm (1 584 cells) with 160 wall cells. A range scan has 180 beams with 5 cm noise. From one pose, 79 occupied cells lie at a wall, 10 occupied cells are more than one cell from any wall (grazing beams and noise), 80 wall cells have no occupied cell next to them, and 881 cells have never been seen. With three poses: 173 found, 1 false, 7 missed, 242 unseen. With ten: 203, 0, 0, 182.
- The unseen cells. The 182 cells never touched by a ray after ten poses are outside the room (behind the walls), where no beam goes. Cells inside the room are all known.
import math
from robotprob import Rng, OccupancyGrid, logodds_update, logit, inv_logit
# 1. one cell: the binary Bayes filter in log odds. The cell is a closed door for 15 readings, then the door opens.
print('reading sensor says p(occupied), clamp +-10 p(occupied), clamp +-2')
l_a = l_b = 0.0
rng = Rng(4)
for t in range(1, 41):
door_closed = t <= 15
says_occ = rng.random() < (0.8 if door_closed else 0.2) # the sensor is right 80% of the time
p = 0.7 if says_occ else 0.3 # the inverse sensor model: what one reading says about the cell
l_a = logodds_update(l_a, p, 0.0, -10.0, 10.0)
l_b = logodds_update(l_b, p, 0.0, -2.0, 2.0)
if t in (1, 5, 10, 15, 20, 25, 30, 40):
print(str(t).rjust(7), str('occ' if says_occ else 'free').rjust(11), str(round(inv_logit(l_a), 3)).rjust(21), str(round(inv_logit(l_b), 3)).rjust(24))
# 2. a grid of a 10 m x 8 m room with a divider, built from a robot at known poses with 36-beam scans
SEGS = [((0, 0), (10, 0)), ((10, 0), (10, 8)), ((10, 8), (0, 8)), ((0, 8), (0, 0)), ((5, 0), (5, 4))]
ZMAX = 6.0
def cast(x, y, th):
dx, dy, best = math.cos(th), math.sin(th), ZMAX
for (x1, y1), (x2, y2) in SEGS:
ex, ey = x2 - x1, y2 - y1
den = dx * ey - dy * ex
if abs(den) < 1e-12: continue
t = ((x1 - x) * ey - (y1 - y) * ex) / den
u = ((x1 - x) * dy - (y1 - y) * dx) / den
if t > 0.0 and 0.0 <= u <= 1.0 and t < best: best = t
return best
RES = 0.25
def new_grid(): return OccupancyGrid(44, 36, RES, origin=(-0.5, -0.5))
truth_occ = set() # the cells that really contain a wall
for (x1, y1), (x2, y2) in SEGS:
n = int(max(abs(x2 - x1), abs(y2 - y1)) / 0.05)
g0 = new_grid()
for k in range(n + 1):
truth_occ.add(g0.cell(x1 + (x2 - x1) * k / n, y1 + (y2 - y1) * k / n))
def build(poses, seed=5, beams=180):
rng = Rng(seed)
g = new_grid()
for (px, py) in poses:
for k in range(beams):
a = 2 * math.pi * k / beams
r = cast(px, py, a) + rng.gauss(0.0, 0.05)
g.update_ray(px, py, a, max(0.1, r), ZMAX, 0.7, 0.3)
return g
def near_wall(i, j):
return any((i + a, j + b) in truth_occ for a in (-1, 0, 1) for b in (-1, 0, 1))
def score(g):
P = g.probabilities()
found = false_wall = missed = unknown = 0
for j in range(g.h):
for i in range(g.w):
p = P[j][i]
if abs(p - 0.5) < 1e-9: unknown += 1
if p > 0.5 and near_wall(i, j): found += 1
elif p > 0.5: false_wall += 1
for (i, j) in truth_occ: # wall cells with no occupied cell within one cell of them
if not any(P[j + b][i + a] > 0.5 for a in (-1, 0, 1) for b in (-1, 0, 1) if g.inside(i + a, j + b)): missed += 1
return found, false_wall, missed, unknown
print('wall cells in the true map:', len(truth_occ), ' cells in the grid:', 44 * 36)
print('poses occupied cells at a wall occupied cells far from walls wall cells missed cells never seen')
for poses in ([(2.0, 2.0)], [(2.0, 2.0), (3.0, 6.0), (7.5, 2.0)], [(2.0, 2.0), (3.0, 6.0), (7.5, 2.0), (8.0, 6.5), (4.0, 4.0), (6.5, 5.5), (1.5, 4.5), (9.0, 1.0), (5.5, 7.0), (2.5, 7.0)]):
f, fw, mi, un = score(build(poses))
print(str(len(poses)).rjust(5), str(f).rjust(20), str(fw).rjust(30), str(mi).rjust(18), str(un).rjust(17))reading sensor says p(occupied), clamp +-10 p(occupied), clamp +-2
1 free 0.3 0.3
5 occ 0.927 0.881
10 occ 0.994 0.881
15 occ 1.0 0.881
20 free 0.967 0.119
25 free 0.3 0.119
30 occ 0.033 0.24
40 free 0.0 0.119
wall cells in the true map: 160 cells in the grid: 1584
poses occupied cells at a wall occupied cells far from walls wall cells missed cells never seen
1 79 10 80 881
3 173 1 7 242
10 203 0 0 182logodds_update is the binary Bayes filter of one cell; OccupancyGrid.update_ray traces a beam with Bresenham’s algorithm and applies free and occupied updates. The probabilities of the grid are inv_logit of its log odds.In robotics
The map that planners use
The 2-D occupancy grid is the standard input to global and local planners: free cells are traversable, occupied cells are obstacles, unknown cells are treated as obstacles or as risk. The Navigation course builds on this representation in its mapping and planning lessons and the SLAM course produces such maps in its occupancy-grid lessons. The Robot Learning and Perception tracks use the same cell-wise Bayes idea for depth and semantic maps.
Choosing resolution
A finer grid represents thin obstacles but costs memory (quadratic in 2-D, cubic in 3-D) and needs more readings per cell. A rule of thumb is a resolution a few times finer than the sensor noise and the smallest obstacle that matters. 3-D maps use octrees (OctoMap) to store only the occupied and known parts.
Choosing the model parameters
- pocc and pfree (here 0.7 and 0.3): how much one reading moves a cell. Larger values converge faster and are less able to average out noise. Values come from the sensor's hit and false-alarm rates.
- Clamps: ±2 (probabilities 0.12 to 0.88) is typical for a world that changes; larger values for a static, precisely measured world.
- Free-space handling for max-range readings and for glass or absorbing surfaces, which produce no hit.
Dynamic and changing worlds
People and doors leave traces: cells marked occupied for a moving person remain so until readings say otherwise. Clamping shortens that memory; ray tracing through the cell with later readings clears it. The map is not a snapshot but a running estimate.
Common misconceptions
“The probability of a cell is the fraction of beams that hit it.”
It is the Bayesian combination of all readings through the inverse sensor model, not a hit ratio. A cell crossed by 10 beams and hit by 1 is almost certainly free; a hit ratio of 10% would not say so.
“An occupancy of 0.5 means the cell is half full.”
It means unknown (or contradictory evidence): the cell is either full or empty and we do not know which. The probability is about our knowledge, not about the amount of matter.
“Once a wall is seen, it is permanent.”
Not with clamping: ±2 means a few readings in the other direction flip it. Without clamping, a changed wall takes as many readings to unlearn as it took to learn.
“Independent cells mean the map is wrong where objects are connected.”
It means the map does not encode the correlations. Occupancy grids are used successfully because most decisions (is this cell traversable?) depend on single cells and nearby ones.
Key takeaways
- An occupancy grid keeps a probability per cell, updated independently by the binary Bayes filter.
- In log odds the update is an addition: ℓ ← ℓ + logit(p(m|z)) − ℓ₀. The prior is subtracted because the inverse sensor model includes it.
- A range beam makes the cells along it more likely free and its end cell more likely occupied; a max-range reading makes cells free only.
- Clamping the log odds (±2) limits the strength of evidence so that the map can change; an unclamped cell took about ten readings to unlearn after the door opened.
- A grid fills in with the number of scans: 79 of the 160 wall cells found after one scan, 173 (with 7 missed) after three, all after ten in the example.
References and further reading
Each source is marked free or paid. Free to read does not mean free to reuse: the course resources page lists every source with its licence.
- S. Thrun, W. Burgard and D. Fox, Probabilistic Robotics, MIT Press, 2005, chapter 9 (occupancy grid mapping), sections 9.1 to 9.2. Paid book.
- C. Stachniss, “Occupancy Grid Maps” (video lecture) . Free to watch.
- A. Elfes, “Using Occupancy Grids for Mobile Robot Perception and Navigation”, Computer 22 (6), 1989. Paid journal article.
- T. D. Barfoot, State Estimation for Robotics, 2nd edition, 2024 . Free to read as the author’s PDF; the book is copyrighted.
Knowledge check
Three questions. Write an answer, then reveal the explanation. Your answers stay in your browser.
Question 1
A cell starts at probability 0.5. Three readings in a row give an inverse sensor model of 0.7. What is the probability afterwards, using log odds? What if the next reading says 0.3?
Explanation
The log odds of 0.7 is log(0.7/0.3) = 0.847. After three readings: 2.542, probability 1/(1 + e^(−2.542)) = 0.927. A reading of 0.3 has log odds −0.847, which brings the total to 1.695, probability 0.845. In probabilities this is the same as the Bayes update in odds: 0.7/0.3 cubed is 12.7 (odds), then divided by 7/3 gives 5.44 (odds) = 0.845.
Question 2
Why does an update that forgets to subtract the prior drift when the prior is 0.2 and the reading carries no information (the inverse sensor model equals the prior, 0.2)?
Explanation
With p(m|z) = 0.2 the reading says nothing beyond the prior: the correct update adds log(0.2/0.8) − ℓ₀ = 0, so the cell stays at 0.2. If the prior is not subtracted, each such reading adds log(0.25) = −1.386, and after a few readings the cell approaches probability 0 although nothing was learned. The inverse sensor model already includes the prior once, and each reading must add only its extra information.
Question 3
A cell clamped at ±2 has been at its upper limit for a long time. The door opens and every reading now has probability 0.3. How many readings does it take to fall below 0.5, and how many for an unclamped cell that is at log odds 8?
Explanation
Each “free” reading adds −0.847. From +2 the cell is below 0 after 3 readings (2 − 2.54 = −0.54): 3 readings. An unclamped cell at +8 needs 10 readings (8 − 8.47 = −0.47), more than three times as many, and it will have needed as many to reach 8 in the first place. A clamp trades a bit of confidence for a map that tracks changes quickly.
What’s next
We have been giving the grid the probability of a cell from a single reading, by decree: 0.7 or 0.3. The next lesson looks at how that inverse sensor model is built, for laser beams, sonar cones and depth cameras, and at what goes wrong when it is wrong.
Related content
Nothing below is required to finish this lesson.
- LabM11-L1: Occupancy Grid and Pose Graph SLAM
The map of the lab has a doubled wall where the loop should close. Build the grid in log odds, find why it smears, and correct the trajectory with a pose graph before mapping again.
Other courses on this site treat neighbouring topics from their own angle:
Get notified when the next lesson and new labs are released.
One email per new lesson or lab.