Occupancy Grid Mapping: How Off-Road Ground Robots Decide Where They Can Drive
Have you ever doubted your loving partner? In 1945, a Canadian spy named Max Vatan just received an intel that changed everything. His wife Marianne was suspected of being a German spy. His orders were now to find out if it was true, within 72 hours. If she was guilty, he then had to kill her. This is NOT a true story, but the story of the movie Allied, with Brad Pitt and Marion Cotillard.
So how do you find out if your wife is a german spy? Max had easy options and complex ones. He could have her followed. He could interrogate her friends, search the house, pull her files... but that was complex. So instead, he made a simpler choice; let her overhear a phone conversation sharing false intelligence, and see if the enemy acted on it. Unfortunately for him, they did.
This technique is called the canary trap. It's used to identify leaks by feeding slightly different versions of the same false information to different suspects. Whoever's version shows up in the enemy's hands is the traitor. There is something I love about it: it's extremely simple. While many techniques are based on heavy profiling, and probabilities... this one is just relying one one piece of information...
That simplicity, I think Occupancy Grid Mapping in robotics shares it. You could build a world with a complex 3D representation, bounding boxes all over the place, HD Maps, and all the complexity... or you could build a 2D grid, and define if you can drive there or not. This is particularly useful in environments where objects aren't easily learned (military, agriculture, ...), and thus, we need new ways.
In this article, we're going to explore the topic of Occupancy Grid via 3 core points:
- What is an Occupancy Map?
- How to Fill an Occupancy Grid Map?
- How to use Occupancy Maps for tasks like Planning & Traversability
Let's begin:
What is an Occupancy Map?
You see all these articles out there? They show you the same basic 2D grid drawn in 1995. Let's try something different, let's see...
Off-Road Occupancy: The ORAD-3D Dataset (Mobile Robot Perception)
I am a big fan of off-road autonomous driving; anything that doesn't have clear lane lines and traffic signs, but mud, trees, bushes, lakes, and so on... I've worked with companies in the defense space on that topic, and I find it fascinating. One of the most used of all algorithms is Occupancy. So I did something for you, the reader:
I found a dataset called ORAD-3D, which contains labels for a task called "Occupancy". This is RARE in datasets, especially autonomous driving which don't have that, so I am thinking... wouldn't you like to understand Occupancy from a practical point of view, with a REAL example from ground robots, rather than reading another explanation of Probabilistic Robotics from Sebastian Thrun?
Yes! Let's do this.
So I downloaded the dataset, and I found that, for every frame, there's a NumPy file, that, once decompressed, shows a [24919 x 4] array:
| 0 | 9 | 10 | 0 |
| 0 | 9 | 11 | 0 |
| 0 | 9 | 12 | 0 |
| ... | ... | ... | ... |
| 70 | 13 | 10 | 0 |
| 70 | 14 | 9 | 0 |
| 70 | 14 | 10 | 0 |
How do you make sense of it? Let me first challenge your intuition; what do you think these 24,919 rows and columns are for?
When you look at the ORAD-3D dataset paper, it provides an interesting comment; the occupancy grid map was built using the KISS-ICP algorithm, and when you dig into the GitHub issues, and try to really understand how it was built, you get that:
- The rows represent the number of voxels captured by the LiDAR
- The first 3 columns represent XYZ indices
- Column 4 represents the semantic value (class/category).
Have I lost you already? Look, if we read the first row, it looks like this:
| 0 | 9 | 10 | 0 |
At cell (0, 9, 10), we have an object of category 0.
If we then dig further, we'll see that:
- Label 0 means non-drivable, we have 21,333 voxels (85.6%) with this category
- label 1 means drivable, we have 3,233 voxels (13.0%) with this category
- label 4 and 5 represent non-drivable areas too, but different objects (trees? rocks?mud?)
In reality, we can use semantic information there too. This is the first way to explain the occupancy grid; a 3D grid gives us XYZ information, and then the semantic value shows the class. In the visualization above, I showed the grid in 2D.
But XYZ are cell indices, not real-world distances. To turn that into meaningful information, we must use grid parameters:
- Grid resolution: 0.5 m/cell (not super high-resolution maps, but that works)
- Grid Dimensions: 100 x 100 x 16 cells (x,y,z)
- XYZ Coverage: X +/-25 m —— Y 0-50 m forward —— Z -3 to +5 m
Using THIS, you can turn a cell into a real 3D information. On top of this, the orientation of each voxel is typically aligned with the axes of the coordinate system used. For example, in a 2D map, voxels are oriented along the x and y axes.
So here is my function to visualize:
def build_occupancy(occ_voxels):
"""Collapse 3D occupancy voxels to a 2D BEV image (100x100 RGB).
Iterates over voxels and colors each (x, y) cell: green=drivable, grey=non-drivable."""
occ_bev = np.zeros((100, 100, 3), np.uint8) # black = unannotated
for row in occ_voxels:
x, y, label = int(row[0]), int(row[1]), int(row[3]) # skip Z
if label == 1:
occ_bev[y, x] = [30, 200, 80] # green = drivable
elif occ_bev[y, x, 1] < 200:
occ_bev[y, x] = [140, 140, 140] # grey = non-drivable
return occ_bevIn the code above, we build a 100x100 map, that is filled with black pixels. Then, we fill in the occupancy values based on the elements; at cell x= 0, y = 9, we put a grey cell. Do this long enough, and we get this view of the robot's environment:
That's occupancy: Grid is Drivable, Grey is Non Drivable, Black is Unknown.
But wait. We had 24,919 voxels. Our grid is 100 x 100 x 16, which is 160,000 possible cells. What appened to the 130,000 other cells of our grid? These are the black pixels. Only about 16% of our 100x100x16 grid is filled. That's what's called a Sparse Occupancy Map.
Here is how it looks like in a video, where we also show the other labels (4 and 5):
The Types of Occupancy Maps
I really like this map, but it's just ONE example of map. In fact, we saw that:
- It's a 3D Map (has XYZ cells), but shown in 2D. This means we have 2D vs 3D Occupancy Maps.
- It's a Semantic Map (has many labels), shown as a Binary Map (drivable or not). This means we have Binary vs Non Binary Maps.
- We have a Sparse Map, because the majority of cells are unknown. It means we have Sparse vs Dense Occupancy Maps
Can you start seeing the types of maps? Of course, you can imagine there are more categories, as there are in robotic mapping too, Let's try a very simple summary map:
Wow! It's the hottest illustration of my entire blog you got here! Now, let's try and understand how to create that type of map:
How to Build an Occupancy Map
In this section, I am going to keep it simple: we are going to see how to build a 2D, Non-Binary Map. Filling a 3D Map is much harder, it's no longer a canary trap but a giant duck, and it's often done using Occupancy Networks. I show how in my Tesla Occupancy Networks article, and it's the very advanced way.
Let's focus on filling a 2D map with, not 1 or 0, but probabilities. Here is an example of such a map I found online, it does what we want, but also adds the idea of dynamic objects:
How do you arrive to this? The simplest way is by using LiDAR scans. You are projecting a point cloud over your 2D grid representation, and if you see enough points falling in one cell, it means that cell is occupied. The logical thinking would be: "Do I have more than 3 points in that cell? Yes? Then it's occupied."
This is the intuition I want us to work on. Of course, this doesn't work. What happens to the floor points? Is it occupied, just because beams hit the floor? What happens to leafs? Or what if we have ghosts? False positives? The simple technique is a good intuition which sometimes is too slow, for shure, and it needs to be improved, for shure 😎.
So let's see how:
Understanding Bayesian Occupancy Grid Mapping algorithms
If you walk past a bus station and see one person standing there, would you say it's occupied? It depends, is the person standing? or sitting? Does it look like he's going to cross the street? Hard to tell. Yet, if the person behaves like it's waiting, you can definitely mark the station as occupied. And if there's 30 people? You don't even need to look at them, station are busy!
An occupancy grid thinks exactly the same way. One beam hitting a cell starts building evidence. But it's not enough to be certain. The more beams that confirm it across consecutive scans, the higher the probability climbs. One hit gives you suspicion. Ten hits give you confidence.
You want a system that has memory, and that updates itself over time...
Here is how it works...
There are really 3 ideas you should know:
- A cell's belief is stored as log-odds, not 0/1, and not occupancy probability values. The formula is shown below. The difference is log-odds allow us to go beyond [0...1] range, and thus build more confidence. As intuition:
- p=0.3 → l=−0.847 (leaning free)
- p=0.5 → l=0 (unknown)
- p=0.7 → l=0.847 (leaning occupied)
- p=0.9 → l=2.197 (strongly occupied)
- At stage 0, all cells have l = 0. Every cell starts at p=0.5 (complete uncertainty). Converting to log-odds gives us 0...
- When a new LiDAR measurement arrives, we apply this update formula:
It looks scary, but it's really an unfolded formula.
In fact, let's see it via an example:
- Say we have this cell on the left image, we have no idea if it's free or occupied. So we set p = 0.5, and l becomes 0 (unknown belief).
- The next frame, we see one point falling in the cell. This increases our probability a little, let's say 0.7. Our belief moves from 0 to 0.847.
- We could consider 0.847 high enough to be occupied, so we mark it as occupied [here, the colors are terrible, occupied should be red].
This is the perfect way to be "smooth" with our data, to set confidence, steps, and so on... You can also notice a symmetry between hit and miss.
Hit or Miss: Did I invent the probability values?
You may be wondering... Did I just make up the p = 0.7 number in case there is a point hitting a cell? Not really. The 0.7 is the hit probability of the inverse sensor model, and the short version of where it comes from has not changed: you set it, you do not measure it. It encodes a single decision, how much you trust one hit, based on the sensor noise.
Most 3D occupancy mapping runs on OctoMap, so its defaults are the de facto convention. They pair a hit probability of roughly 0.65 to 0.7 with a miss probability of about 0.4, and they clamp the cell's probability between 0.12 and 0.97 so it can never reach a hard 0 or 1. Each beam endpoint pushes its cell up by the hit amount, each cell the beam passes through on the way gets pushed down by the miss amount, and both accumulate in log-odds until they reach the clamps
Now, you could try the exercise with prior occupied cells, or prior free cells.
Example: Using Occupancy Grid Code in Action (Probabilistic Robotics)
Alright, I'd like to show you, in this more advanced example, how to implement these formulas. Here is me running a rosbag containing an occupancy grid map:
How is it built? I have found a Matlab code online that does exactly what we just discussed. It can feel a bit complex, so the reason should be (1) look at the images, (2) look at the yellow arrows, and (3) read the code. Here is the algorithm:
Can you see the line where the occupancy formula is applied? Can you see how we set the free and occupied space? The whole function also considers whether a beam hits, or just "traverse" a cell. The formula is towards the bottom, an arrow points to it, and hopefully, the illustration helps a little.
Alright, so this was advanced, but it's a good introduction! Now let's see the final idea of this article...
How to use Occupancy Maps for Robotics Tasks (Path Planning, Traversability, ...)
There are 2 core ideas I'd like to explain here, especially since we're in the topic of "Off Road". Occupancy is fantastic in 2 use cases:
- Self-Driving Car companies using it, especially with End-To-End Learning
- All ground based mobile robot perception algorithms that use Occupancy to know where to drive
In off-road, we often have issues, such as "Can I drive on this bush?" or "What do I do if I have no GPS?". These can be solved by one core idea:
Traversability Estimation
But first of, a word for context:
Last December, I got the opportunity to give a DL seminar to a company named IAI (Israel Aerospace Industries, one of the big 4 there), and they showed me their "off road" autonomous bulldozers.
This is where I learned about the concept for the first time, when an engineer shared it to me. I then researched more, and recently, EarthSense, the autonomous agriculture robots company I told you about, shared they were also using it. In fact, all robots that drive "off-road" are using it. It's a PILLAR.
Off-roads means most of the time no GPS, no traffic sign, light, sometimes no GPU, datasets, or anything you'd normally use in a self-driving car. Even the objects are rocks, cliffs, mountains, bushes or at the very best... barbed wires? You can't use YOLO there, it makes no sense.
These robots almost all use traversability estimation. Imagine you own a robot driving in an agriculture field... Your robot will drive on grass, mud, terrain, but will sometimes face leafs, corns, or as EarthSense taught me the word... "fronds". (palm leafs basically) Can you guess what happens if you use a LiDAR, or an occupancy map? Of course, all the fronds will be occupied space. Your robot will stop at every leaf.
What we want is not "leaf" or "grass", we want traversable or not.
And to know whether you can traverse or not, robotics companies use traversability estimation algorithms. An example of one running:
Notice how, as the grass gets higher, we have red values?
How is it built? It's a formula, based on a few factors, such as:
- Elevation of the points (altitude, ...)
- Slope of the surface (is it steep? flat?)
- Roughness of the terrain (how many points, how close they are)
- Semantics (grass is fine, trees isn't)
- Occupancy Value (can be used, it's actually optional - but a good input)
So you can see, we have a lot of factors helping us determine whether a surface, even though NOT occupied, can be traversed or not.
A* and Path Planning
The second thing you can do with Occupancy Maps is Planning. After you did any kind of robotic mapping, whether SLAM based or here, occupancy based, you have a map. And in a map, you do things like Path Planning. So how do we do that? Basically, we set a goal, and apply algorithms like A*, which find the shortest path through the free space while avoiding obstacles.
And this is how it looks like when you combine both:
Ok we've seen a lot, let's do a summary!
Summary & Next Steps
Here is a bullet point summary of the article content provided:
- Occupancy Grid Mapping is a simple yet powerful technique for robotic navigation that uses a grid (2D or 3D) to represent the environment.
- The ORAD-3D dataset provides a real-world example of a 3D occupancy map with semantic labels indicating drivable and non-drivable areas.
- There are multiple types of occupancy maps; 2D vs 3D, binary vs non-binary, and sparse vs dense.
- Building an occupancy map involves filling grid cells with probabilities rather than binary values, accounting for sensor uncertainty.
- Bayesian Occupancy Grid Mapping uses log-odds to update per cell occupancy probability values over time, improving confidence with repeated sensor readings. Occupancy grids can be dynamically updated as the robot explores the environment.
- The inverse sensor model assigns hit and miss probabilities to sensor measurements, influencing occupancy updates.
- Occupancy maps support robotics tasks like traversability estimation and path planning. Traversability estimation considers factors like elevation, slope, roughness, semantics, and occupancy to determine if terrain is navigable. Algorithms like A* and RRT use occupancy grids to calculate the shortest, collision-free path from a robot's position to a target.
- Occupancy grids provide a foundation for autonomous navigation in complex and off-road robotics. Engineers learning End-To-End / AV 2.0 should seriously consider it, and those working on UGV, Robotics, Defense should absolutely learn it.
Next Steps
Here are a few next steps for you...
- First, I would recommend reading my Robot Mapping article. Occupancy Maps are ONE of the many types of maps we have in robotics. Seeing them in a global context would be helpful.
- Second, you could also read the Tesla Occupancy Networks article. It provides the Deep Learning version of this one, but applied to Tesla and how THEY do it in 3D.
- Finally, would you like to build this exact ground robot/off-road AV project? That'd be a great next step. We're implementing Occupancy, Traversability, A*, and Off-Road Algorithms in The Edgeneer's Land; this is my community membership where each month a company teaches you how THEY build self-driving cars.
On March 2026, EarthSense, an agriculture robotics company, taught up about Off-Road, and the episode came with a workshop on Off-Road. You can access it in the annual edition of the membership.