Particle filters explained
How a particle filter works out where a robot is: scatter guesses, move them with the robot, weigh them against a sensor reading and resample. Watch the cloud collapse onto a robot, fail with too few particles and recover after a kidnap, in Python you can change and run.
A particle filter works out where a robot is by keeping hundreds of guesses at once and throwing away the ones that do not fit what the robot sees. Used to find a robot on a map, it is called Monte Carlo localisation. It is how many robot vacuum cleaners and warehouse robots know where they are, and it is the standard localiser in ROS. On this page a small robot on a 2 metre square mat works out where it is from one depth sensor and its odometry (an optical flow sensor and a gyro), and each demo below is a real program you can change and run.
In the overhead view of each demo, the blue dots are the guesses (the particles) and the red dot is the estimate. The chart underneath shows two lines. The error is how far the red dot is from the robot, in cm; the simulator knows where the robot is, and uses that only to draw this line. The spread is how far the guesses are, on average, from their middle: how unsure the filter is. A good filter takes both lines down and keeps the spread about as big as the error.
The idea in one loop
scatter N guesses all over the map
each tick:
move every guess the way the robot moved, plus a little noise
weigh every guess: would it see what the robot sees?
resample: draw N new guesses from the old ones,
heavy ones more often
the estimate is the middle of the cloud
- Move (predict). The robot's odometry says it went 2 cm forward, so every guess moves 2 cm forward. Each one also gets a small random nudge, because odometry is never exact, and so that no two guesses are ever the same.
- Weigh (correct). Every guess works out what the depth sensor would read if the robot were there: here, the distance to the wall it would be facing. A guess that would read 80 cm when the robot reads 79 gets a weight near 1. A guess that would read 140 gets a weight near 0.
- Resample. Pick
Nnew guesses at random from the old ones, in proportion to their weights. Heavy guesses get copied several times, light ones vanish. The cloud gathers where the evidence is.
"Monte Carlo" means using random samples to work something out, after the casino. The filter never writes down a formula for where the robot might be: the cloud of samples is the answer, whatever shape it takes.
The sensor model
The one piece of this filter that belongs to a particular robot is the sensor model: given a guess, what should the sensor read? This robot faces along a heading h (0 is the wall ahead at y = 200, 270 is the left wall at x = 0), and the mat has a wall on every side, so the prediction is the distance along that heading to the first wall:
def expected(x, y, h):
dx = math.sin(math.radians(h))
dy = math.cos(math.radians(h))
d = 400
if dx > 0.01: d = min(d, (200 - x) / dx)
if dx < -0.01: d = min(d, -x / dx)
if dy > 0.01: d = min(d, (200 - y) / dy)
if dy < -0.01: d = min(d, -y / dy)
return d
The weight is a bell curve on the difference between that and the real reading, exp(-err² / 2σ²). SIGMA (σ) says how far out a reading can be: this robot's depth sensor wobbles by about 3 cm, so the demos use 5.
Each guess here is only a position. The heading comes from the robot's gyro through odometry(), which was 2 to 3 degrees out in these runs. Tracking the heading as well means a third number in every guess, and more guesses. A room with furniture needs a map and a ray cast instead of four walls, but the filter around it does not change.
The cloud finds the robot
The robot starts at x = 150, y = 120, facing the wall ahead. It is not told where it is: 300 guesses are scattered over the whole mat. It stands still for 3 seconds, turns left to face the left wall, drives about 63 cm towards it, turns left again and drives about 50 cm towards the bottom wall.
The program
from bugbot import *
import math, random
connect()
random.seed(1)
# change these numbers and press Run
N = 300 # how many particles
MOVE_NOISE = 1.0 # cm of noise added to each move
SIGMA = 5.0 # cm the sensor can be out by
START = (150, 120) # the truth: only for the chart
# the drive: (seconds, forward speed, turn speed)
ROUTE = [(3, 0, 0), (2.4, 0, -30), (6, 60, 0),
(2.4, 0, -30), (5, 60, 0)]
def expected(x, y, h):
# what the depth sensor would read at (x, y)
# facing h degrees: the distance to the wall
dx = math.sin(math.radians(h))
dy = math.cos(math.radians(h))
d = 400
if dx > 0.01: d = min(d, (200 - x) / dx)
if dx < -0.01: d = min(d, -x / dx)
if dy > 0.01: d = min(d, (200 - y) / dy)
if dy < -0.01: d = min(d, -y / dy)
return d
def weight(x, y, h, seen):
# near 1 if the guess would see what the robot
# saw, near 0 if not (never exactly 0)
err = expected(x, y, h) - seen
return math.exp(-err ** 2 / (2 * SIGMA ** 2)) + 1e-9
def scatter():
# guesses anywhere on the 200 cm mat
return [(random.uniform(0, 200),
random.uniform(0, 200)) for i in range(N)]
def follow(route):
global cloud, ox, oy
for secs, fwd, turn in route:
drive(fwd, 0, turn)
for tick in range(int(secs / 0.2)):
wait(0.2)
x, y, h = odometry()
mx, my = x - ox, y - oy
ox, oy = x, y
# 2. move every guess as the robot moved
cloud = [(a + mx + random.gauss(0, MOVE_NOISE),
b + my + random.gauss(0, MOVE_NOISE))
for a, b in cloud]
if turn == 0: # no readings while turning
# 3. weigh every guess
seen = distance()
w = [weight(a, b, h, seen) for a, b in cloud]
# 4. resample: heavy guesses picked more
cloud = random.choices(cloud, w, k=N)
show()
def show():
# the estimate is the middle of the cloud
ex = sum(a for a, b in cloud) / N
ey = sum(b for a, b in cloud) / N
spread = math.sqrt(sum((a - ex) ** 2 + (b - ey) ** 2
for a, b in cloud) / N)
draw("particles", cloud, "blue")
draw("estimate", [(ex, ey)], "red", size=5)
px, py = position() # the truth, for the chart
tx, ty = START[0] + px, START[1] + py
plot("error cm", math.hypot(ex - tx, ey - ty))
plot("spread cm", spread)
cloud = scatter() # 1. scatter
ox, oy, h = odometry()
follow(ROUTE)
stop()
Watch the cloud in three stages.
- A band. The robot reads about 80 cm to the wall ahead. That rules out every guess at the wrong distance from that wall, but says nothing about how far left or right the robot is, so within a second the cloud is a band across the mat, with about 2 cm of spread up and down and 50 cm from side to side. The red dot sits in the middle of the band, level with the robot and about 35 cm to its left. The middle of a band is not a good answer, and the spread line says so.
- A blob. After the turn the sensor reads the distance to the left wall, which pins down x. At 5.4 s the band collapses onto the robot, and from then on the error stays between 0.5 and 3.8 cm.
- Tracking. While the robot drives towards the left wall, the spread creeps up from 4 to 7 cm, because a reading straight ahead says nothing about sideways drift. The second turn pulls it back to 5 cm. It ends 2.5 cm from the robot.
This is why a robot localising with one sensor has to move. One reading leaves a whole stripe of the mat possible; a second reading from a different direction cuts the stripe down to a spot.
Readings at a slant
The program only weighs the guesses when the robot is not turning. The depth sensor looks along a narrow cone a few degrees wide and reports the nearest thing in it. Facing a wall square on, that is the same as the single line expected() draws. Pointing at a wall at a slant, the near edge of the cone hits first: halfway through the first turn the robot read 148 cm where the single line gives 167. A filter fed readings its model cannot explain moves the cloud to the wrong place with full confidence. The fix here is to skip those readings. The better fix is a sensor model that matches the sensor.
Too few particles
The same program with N = 10.
The program
from bugbot import *
import math, random
connect()
random.seed(1)
# change these numbers and press Run
N = 10 # how many particles
MOVE_NOISE = 1.0 # cm of noise added to each move
SIGMA = 5.0 # cm the sensor can be out by
START = (150, 120) # the truth: only for the chart
# the drive: (seconds, forward speed, turn speed)
ROUTE = [(3, 0, 0), (2.4, 0, -30), (6, 60, 0),
(2.4, 0, -30), (5, 60, 0)]
def expected(x, y, h):
# what the depth sensor would read at (x, y)
# facing h degrees: the distance to the wall
dx = math.sin(math.radians(h))
dy = math.cos(math.radians(h))
d = 400
if dx > 0.01: d = min(d, (200 - x) / dx)
if dx < -0.01: d = min(d, -x / dx)
if dy > 0.01: d = min(d, (200 - y) / dy)
if dy < -0.01: d = min(d, -y / dy)
return d
def weight(x, y, h, seen):
# near 1 if the guess would see what the robot
# saw, near 0 if not (never exactly 0)
err = expected(x, y, h) - seen
return math.exp(-err ** 2 / (2 * SIGMA ** 2)) + 1e-9
def scatter():
# guesses anywhere on the 200 cm mat
return [(random.uniform(0, 200),
random.uniform(0, 200)) for i in range(N)]
def follow(route):
global cloud, ox, oy
for secs, fwd, turn in route:
drive(fwd, 0, turn)
for tick in range(int(secs / 0.2)):
wait(0.2)
x, y, h = odometry()
mx, my = x - ox, y - oy
ox, oy = x, y
# 2. move every guess as the robot moved
cloud = [(a + mx + random.gauss(0, MOVE_NOISE),
b + my + random.gauss(0, MOVE_NOISE))
for a, b in cloud]
if turn == 0: # no readings while turning
# 3. weigh every guess
seen = distance()
w = [weight(a, b, h, seen) for a, b in cloud]
# 4. resample: heavy guesses picked more
cloud = random.choices(cloud, w, k=N)
show()
def show():
# the estimate is the middle of the cloud
ex = sum(a for a, b in cloud) / N
ey = sum(b for a, b in cloud) / N
spread = math.sqrt(sum((a - ex) ** 2 + (b - ey) ** 2
for a, b in cloud) / N)
draw("particles", cloud, "blue")
draw("estimate", [(ex, ey)], "red", size=5)
px, py = position() # the truth, for the chart
tx, ty = START[0] + px, START[1] + py
plot("error cm", math.hypot(ex - tx, ey - ty))
plot("spread cm", spread)
cloud = scatter() # 1. scatter
ox, oy, h = odometry()
follow(ROUTE)
stop()
Ten guesses spread over a 2 metre mat are about 60 cm apart, and none of them is near the robot. The first reading gives nearly all the weight to the one guess whose distance from the wall fits best, 24 cm too near it and 61 cm to the left, and the whole cloud becomes copies of it. The move noise lets the copies wander back towards the right distance while the robot stands still, but nothing brings them across to the right x. After the turn no guess explains the reading at all, every weight is the same tiny number, and resampling has nothing to choose between.
A filter can only keep guesses it already has. Resampling copies good guesses; it never invents a new one in the right place.
How many is enough? The same drive, run 20 times with different random seeds, ended within 10 cm of the robot this often:
| Particles | Runs within 10 cm | Middle result |
|---|---|---|
| 10 | 8 of 20 | 46 cm out |
| 20 | 14 of 20 | 4.6 cm |
| 50 | 16 of 20 | 3.7 cm |
| 100 | 19 of 20 | 3.8 cm |
| 300 | 20 of 20 | 2.6 cm |
| 1000 | 20 of 20 | 2.5 cm |
Below about 100 it is a lottery: try N = 20, which happens to find the robot in this run and ends 3 cm out. Above 300 it gets no more accurate, only slower. The rule of thumb is to start with enough guesses that a few of them land within the sensor's error of wherever the robot might be. For a 2 metre mat and a sensor good to 5 cm that is a few hundred; for a whole building it is many thousands, which is why real systems change the number as they go (see AMCL below). Keep N at 1000 or less on this page: the simulator keeps up to 100,000 drawn points a run, so a bigger cloud stops being drawn part way through.
Too sure of itself
Back to 300 particles, and now SIGMA = 0.5: the filter is told the sensor is good to half a centimetre, when it wobbles by about 3.
The program
from bugbot import *
import math, random
connect()
random.seed(1)
# change these numbers and press Run
N = 300 # how many particles
MOVE_NOISE = 1.0 # cm of noise added to each move
SIGMA = 0.5 # cm the sensor can be out by
START = (150, 120) # the truth: only for the chart
# the drive: (seconds, forward speed, turn speed)
ROUTE = [(3, 0, 0), (2.4, 0, -30), (6, 60, 0),
(2.4, 0, -30), (5, 60, 0)]
def expected(x, y, h):
# what the depth sensor would read at (x, y)
# facing h degrees: the distance to the wall
dx = math.sin(math.radians(h))
dy = math.cos(math.radians(h))
d = 400
if dx > 0.01: d = min(d, (200 - x) / dx)
if dx < -0.01: d = min(d, -x / dx)
if dy > 0.01: d = min(d, (200 - y) / dy)
if dy < -0.01: d = min(d, -y / dy)
return d
def weight(x, y, h, seen):
# near 1 if the guess would see what the robot
# saw, near 0 if not (never exactly 0)
err = expected(x, y, h) - seen
return math.exp(-err ** 2 / (2 * SIGMA ** 2)) + 1e-9
def scatter():
# guesses anywhere on the 200 cm mat
return [(random.uniform(0, 200),
random.uniform(0, 200)) for i in range(N)]
def follow(route):
global cloud, ox, oy
for secs, fwd, turn in route:
drive(fwd, 0, turn)
for tick in range(int(secs / 0.2)):
wait(0.2)
x, y, h = odometry()
mx, my = x - ox, y - oy
ox, oy = x, y
# 2. move every guess as the robot moved
cloud = [(a + mx + random.gauss(0, MOVE_NOISE),
b + my + random.gauss(0, MOVE_NOISE))
for a, b in cloud]
if turn == 0: # no readings while turning
# 3. weigh every guess
seen = distance()
w = [weight(a, b, h, seen) for a, b in cloud]
# 4. resample: heavy guesses picked more
cloud = random.choices(cloud, w, k=N)
show()
def show():
# the estimate is the middle of the cloud
ex = sum(a for a, b in cloud) / N
ey = sum(b for a, b in cloud) / N
spread = math.sqrt(sum((a - ex) ** 2 + (b - ey) ** 2
for a, b in cloud) / N)
draw("particles", cloud, "blue")
draw("estimate", [(ex, ey)], "red", size=5)
px, py = position() # the truth, for the chart
tx, ty = START[0] + px, START[1] + py
plot("error cm", math.hypot(ex - tx, ey - ty))
plot("spread cm", spread)
cloud = scatter() # 1. scatter
ox, oy, h = odometry()
follow(ROUTE)
stop()
With SIGMA = 0.5, a guess 1 cm out gets a weight 7 times smaller than one that matches, and a guess 3 cm out gets 65 million times less. So the first reading, noise and all, gives nearly all the weight to a handful of guesses that happen to match it, and their x comes with them, right or wrong. The band from the first demo never forms: by 1.2 s the cloud is one spot with a spread of 2 cm, 23 cm to the left of the robot. Every later reading is so far from what the spot predicts that it cannot pull it across.
The chart shows the danger. The spread line says the filter is sure to about 1 cm; the error line says it is 18 cm out. In 20 runs with different seeds, 15 ended more than 10 cm from the robot, and in every one of the 20 the spread was less than half the error.
Too little move noise does the same thing more slowly. Try MOVE_NOISE = 0: copies of a guess now stay identical, resampling keeps fewer and fewer different ones, and by 11 s the whole cloud is a single point with a spread of exactly 0. In this run that point is 2 cm from the robot, and it drifts to 4 cm by the end because the cloud has no way left to correct itself. Over 20 seeds, half the runs ended more than 11 cm out, all with a spread below 1 cm.
Try SIGMA = 20 for the other direction: the filter still finds the robot (0.7 cm out at the end), but every reading counts for less, so the cloud stays about 9 cm across instead of 5. When in doubt, set SIGMA a little larger than the sensor's real error, and keep some move noise.
The kidnapped robot
The hard test for a localiser is to pick the robot up and put it down somewhere else without telling it. The filter's cloud is tight, confident and in the wrong place. Here the robot finds itself as before, then drives 57 cm across the mat with the filter switched off. To the filter that is the same as being picked up and carried: it never hears about the move.
The fix is two lines. If no guess can explain the reading (the best weight is below 0.001, so every guess is more than about 19 cm out), the filter is lost, and it scatters the cloud over the whole mat and starts again.
The program
from bugbot import *
import math, random
connect()
random.seed(1)
# change this and press Run
RESCUE = True # start again when lost
N = 300 # how many particles
MOVE_NOISE = 1.0 # cm of noise added to each move
SIGMA = 5.0 # cm the sensor can be out by
START = (150, 120) # the truth: only for the chart
# the drive: (seconds, forward speed, turn speed)
FIND = [(3, 0, 0), (2.4, 0, -30), (6, 60, 0)]
AGAIN = [(3, 0, 0), (2.4, 0, -30), (5, 60, 0)]
def expected(x, y, h):
# what the depth sensor would read at (x, y)
# facing h degrees: the distance to the wall
dx = math.sin(math.radians(h))
dy = math.cos(math.radians(h))
d = 400
if dx > 0.01: d = min(d, (200 - x) / dx)
if dx < -0.01: d = min(d, -x / dx)
if dy > 0.01: d = min(d, (200 - y) / dy)
if dy < -0.01: d = min(d, -y / dy)
return d
def weight(x, y, h, seen):
# near 1 if the guess would see what the robot
# saw, near 0 if not (never exactly 0)
err = expected(x, y, h) - seen
return math.exp(-err ** 2 / (2 * SIGMA ** 2)) + 1e-9
def scatter():
# guesses anywhere on the 200 cm mat
return [(random.uniform(0, 200),
random.uniform(0, 200)) for i in range(N)]
def follow(route):
global cloud, ox, oy
for secs, fwd, turn in route:
drive(fwd, 0, turn)
for tick in range(int(secs / 0.2)):
wait(0.2)
x, y, h = odometry()
mx, my = x - ox, y - oy
ox, oy = x, y
# 2. move every guess as the robot moved
cloud = [(a + mx + random.gauss(0, MOVE_NOISE),
b + my + random.gauss(0, MOVE_NOISE))
for a, b in cloud]
if turn == 0: # no readings while turning
# 3. weigh every guess
seen = distance()
w = [weight(a, b, h, seen) for a, b in cloud]
if RESCUE and max(w) < 0.001:
# no guess explains the reading,
# so the filter is lost: start again
cloud = scatter()
w = [weight(a, b, h, seen)
for a, b in cloud]
# 4. resample: heavy guesses picked more
cloud = random.choices(cloud, w, k=N)
show()
def show():
# the estimate is the middle of the cloud
ex = sum(a for a, b in cloud) / N
ey = sum(b for a, b in cloud) / N
spread = math.sqrt(sum((a - ex) ** 2 + (b - ey) ** 2
for a, b in cloud) / N)
draw("particles", cloud, "blue")
draw("estimate", [(ex, ey)], "red", size=5)
px, py = position() # the truth, for the chart
tx, ty = START[0] + px, START[1] + py
plot("error cm", math.hypot(ex - tx, ey - ty))
plot("spread cm", spread)
cloud = scatter() # 1. scatter
ox, oy, h = odometry()
follow(FIND)
# the kidnap: carry the robot away without
# telling the filter
drive(-40, 40, 0)
wait(6)
stop()
wait(0.5)
ox, oy, h = odometry() # the move is never seen
follow(AGAIN)
stop()
While the robot is moved, the blue cloud stays where it was. At 17.9 s the first reading after the kidnap is about 45 cm longer than any guess expects, so the filter starts again: the cloud becomes a band (the right x this time, since the robot faces the left wall) and the error jumps to between 75 and 96 cm, which is the filter admitting it does not know. After the next turn, at 23.3 s, it has found the robot again, and it stays within 6 cm of it to the end.
Set RESCUE = False and run it again. Now the filter never admits it is lost. The cloud creeps a little towards the robot, but it ends 33 cm away with a spread of 5 cm, sure of an answer that is wrong. Over 20 seeds, 19 of the runs without the rescue ended more than 10 cm out; all 20 with it ended within 10 cm.
Starting again from scratch is the bluntest version. Real filters sprinkle a few random guesses over the map every tick, and more of them the worse the recent readings have fitted. That is the recovery half of AMCL.
Particle filter vs Kalman filter
Both filters do the same two steps, predict and correct. They differ in how they hold the belief. The Kalman filter keeps one best guess and a measure of how unsure it is, a single bell curve. The particle filter keeps a cloud.
| Kalman filter | Particle filter | |
|---|---|---|
| The belief | one bell curve: a mean and a covariance | a cloud of guesses, any shape |
| Two possible places at once | no | yes |
| Starting with no idea where it is | poorly | yes: scatter over the map |
| Recovering from a kidnap | not on its own | yes, with random guesses added |
| Work per step | a few small matrix sums | one prediction per particle |
| Best when | you roughly know where you are, and the noise is bell-shaped | the map is odd, the start is unknown, or the robot may be moved |
A Kalman filter is exact when the robot and sensors are close to linear and the noise is bell-shaped, and it costs almost nothing, which is why it runs in phones, drones and GPS receivers. It fails when the belief is not one hump: a robot that could be in either of two identical corridors, or one that has no idea where it started. A particle filter handles those, at the cost of doing the sensor model hundreds of times a tick. Many real robots use both: a particle filter to find out where they are on the map, and a Kalman filter to fuse the fast sensors in between.
Questions
How does a particle filter work?
It keeps many guesses (particles) about the state, such as where a robot is. Each tick it moves every guess by what the robot did plus a little random noise, weighs each guess by how well the reading it would give matches the real reading, and then resamples: it draws a new set of guesses from the old ones, copying heavy ones and dropping light ones. The cloud gathers around the answer, and its middle is the estimate.
What is Monte Carlo localisation?
Monte Carlo localisation (MCL) is a particle filter used to find a robot on a known map. Each particle is a possible pose, the motion model moves the particles by the odometry, and the sensor model compares the robot's range readings with what each particle would see on the map. It can start with no idea where the robot is, which is called global localisation.
What is resampling and why is it needed?
Resampling replaces the weighted particles with a fresh set drawn in proportion to the weights, so likely guesses are copied and unlikely ones disappear. Without it, after a few readings nearly all the weight sits on a handful of particles and the rest are wasted work. Many filters resample only when the effective sample size, 1 / sum(w²) for normalised weights, falls below half the particle count, which keeps more variety in the cloud.
How many particles do you need?
Enough that, at the start, a few of them land within the sensor's error of wherever the robot might be. On the 2 metre mat on this page, 300 particles found the robot in 20 runs out of 20 and 10 particles in 8 out of 20. A building-sized map needs thousands at the start and far fewer once the robot is found, which is why adaptive filters change the number as they go.
What is the difference between a Kalman filter and a particle filter?
A Kalman filter holds the belief as one bell curve, a best guess with an uncertainty, and updates it with a few matrix sums. A particle filter holds it as a cloud of samples, which can take any shape, including several separate places at once. The Kalman filter is cheaper and exact for linear systems with bell-shaped noise; the particle filter copes with unknown starting positions, odd maps and kidnapping, at the cost of evaluating every particle every tick.
What is adaptive Monte Carlo localisation (AMCL)?
AMCL is Monte Carlo localisation that adapts as it runs. It uses many particles while the robot is unsure and few once the cloud is tight (KLD sampling), and it adds random particles when the readings stop fitting the cloud, so it can recover when the robot is lost or moved. It is the standard localiser in ROS and Nav2, working on a 2D laser scan and an occupancy grid map.
How do you write a particle filter in Python?
Keep a list of (x, y) guesses. Each tick, add the robot's measured movement and some random.gauss noise to every guess; compute a weight for each with math.exp(-err ** 2 / (2 * SIGMA ** 2)), where err is the difference between the reading that guess predicts and the real one; then resample with random.choices(cloud, weights, k=N). The mean of the list is the estimate. Every demo on this page is a complete program of under 100 lines that does exactly this.
What is the kidnapped robot problem?
It is the test of picking a robot up and putting it down somewhere else without telling its localiser. The filter's cloud is then confident and in the wrong place, and nothing in plain resampling can move it. A filter recovers by noticing that no particle explains the readings and adding fresh random particles across the map.
Why does my particle filter converge to the wrong place?
Usually one of four things. Too few particles, so none started near the truth. A SIGMA smaller than the sensor's real error, so one noisy reading picks a winner. Too little motion noise, so the cloud collapses to copies of one guess. Or a sensor model that does not match the sensor, such as a range reading taken at a slant. The warning sign is a spread much smaller than the error you can measure against a known position.
Why add random noise to the particles?
Because resampling copies particles exactly. Without noise, after a few ticks every particle is a copy of the same few guesses and the filter can no longer correct itself. The noise also stands for the real error in the odometry: the robot does not move exactly as its sensors say, so the guesses should not either.
Learn it step by step
These lessons build the same ideas one at a time, each with tasks the simulator marks.
- U7.1 Where am I? Localisation, University
- U7.2 A cloud of guesses Localisation, University
- U7.3 Moving the cloud Localisation, University
- U7.4 Weighing the guesses Localisation, University
- U7.5 Resampling Localisation, University
- U7.6 Monte Carlo localisation Localisation, University
- U7.7 Project: the kidnapped robot Localisation, University
- U8.5 Ray casting the map Mapping, University