LiDAR Dataset#
The LiDAR dataset format stores full LiDAR sweeps as range images: from ACRES Core, from the game, or from a recording of the Polaris. The package acres_data in Data/ writes the format, reads it and makes the data sets. A model that predicts scans, or that learns from scans, reads the data through this package.
Before you start
- The data is not in Git. The default folder is
~/data/acres/lidar. The variableACRES_DATAsets a different folder. - The tutorial Collect Data makes a simulated data set step by step.
Format#
The stored form is the full cloud of the sensor as a range image.
The RoboSense Helios of the Polaris has 32 rings and 1800 columns for each sweep, at 10 Hz.
One cell is one beam: the ring r and the column c. The format does not store a top view.
<data root>/<set>/index.json the sequences of the set
<data root>/<set>/<sequence>/meta.json source, identity, calibration, split tag, layers, counts
<data root>/<set>/<sequence>/scans.npy one row for each scan: time, pose, quality, motion, counts
<data root>/<set>/<sequence>/range-00000.npy uint16, shape (scans, 32, 1800)
<data root>/<set>/<sequence>/intensity-00000.npy uint8, shape (scans, 32, 1800)
<data root>/<set>/<sequence>/body_mask.npy bool, shape (32, 1800)
<data root>/<set>/<sequence>/labels/ layers and tables that only a simulator has
| Item | Value |
|---|---|
| Schema | acres-lidar-dataset-1 |
| Cell order | Ring (0 to 31, the ring order of the driver), then column (0 to 1799). |
| Column 0 | Straight ahead. The columns go clockwise, 0.2 degrees for each column. |
range |
uint16. Range in meters = value x 0.0025. The value 0 is a cell without a return. |
| Largest stored range | 163.8375 m. The sensor gives 161.3 m at most. |
| Range error | 1.25 mm at most. The Helios packet has the same step of 2.5 mm: the range of a real scan has no loss. |
intensity |
uint8, the reflectivity byte of the sensor (0 to 255). 0 in a cell without a return. Absent when the source has no intensity. |
| Shard | 256 scans in one .npy file. The last shard of a sequence can be shorter. |
| Sequence | One continuous drive, or one section of a drive, with one split tag. |
| Split tag | train, val, test, guard, excluded or none. |
The range is the distance from the lens center along the beam, as the driver of the sensor and ACRES Core define it. The lens center is 0.035 m from the spin axis. A point from the stored range and the beam table is 1.5 mm (root mean square) from the point of the source cloud. The largest such distance in the drive of 2 October is 0.114 m. It comes from the beam table and not from the range.
Each shard is a plain array file. The reader maps it into memory, and a read of one scan is one part of one file. The files have no compression: a random read costs no decompression.
The sensor layers are range and intensity. They are the same for a real and for a simulated sequence.
The labels of a simulator are in the folder labels, apart from the sensor layers. The reader gives them only on request.
| Label | Form | Source |
|---|---|---|
material |
uint8 for each cell, in shards | ACRES Core, the game. The codes are in meta.json. |
class |
uint8 for each cell, in shards | The game. |
objects |
float32 (scans, 8, 10): present, kind, x, y, yaw, length, width, height, vx, vy | ACRES Core: the true objects on the path. |
commands |
float32 (scans, 4): curvature, speed, gear, shield flag | ACRES Core: the command of the default driver. |
Scan Table#
scans.npy has one row for each scan.
| Field | Type | Unit | Description |
|---|---|---|---|
t |
float64 | s | The time of the scan on the clock of the pose. A real scan: Unix time. A simulated scan: time from the start. |
stamp_ns |
int64 | ns | The time stamp of the source message. |
east, north, up |
float64 | m | The position of base_footprint in the frame of the ACRE tile. |
qw, qx, qy, qz |
float32 | The attitude: the axes of base_footprint (x forward, y left, z up) to the axes of the map. |
|
speed, lateral_speed |
float32 | m/s | The velocity in the body axes. |
yaw_rate, pitch_rate, roll_rate |
float32 | rad/s | The angular rates in the body axes. |
pose_quality |
uint8 | 0 none, 1 degraded, 2 ins, 3 rtk, 4 truth. |
|
flags |
uint8 | Bit 0: a gap. The time from the scan before is not one period. | |
dropped_columns |
uint16 | The columns in which no ring has a return. | |
returns, body_returns |
uint32 | The cells with a return, and those that the body rule marks. | |
source_index |
uint32 | The number of the scan in its source. |
A value that is not known is NaN.
The motion between two scans is known only when the quality of the two poses is ins or better.
A sequence with a degraded pose keeps its position to some meters, and the reader marks its motion as not valid.
The time of a real scan is the time of its last column. The rule for column c is in the calibration.
The returns of the vehicle body stay in the data. The body rule is that of the scan filter of the vehicle.
A return is from the body when its cell is in body_mask.npy, or when its point is in the body box of meta.json.
Calibration#
Each meta.json has the calibration of its sensor.
| Key | Description |
|---|---|
elevations_deg, azimuth_offsets_deg |
One value for each ring. A beam has the azimuth of its column plus the offset of its ring. |
clockwise, lens_center_m |
The column order and the distance of the lens center from the spin axis. |
sweep_s, last_column_at_stamp |
The time of one sweep. Column c fires (1799 - c) / 1800 x sweep_s before the time of the scan. |
ring_firing_s |
The firing time of each ring in its column. |
mount_xyz_m, mount_rpy_deg |
The sensor frame in base_footprint. |
min_range_m, max_range_m |
The limits of the sensor. |
A sweep_s of 0 shows a scan from one instant: all beams have one pose. A set made with --sweep has a sweep_s of 0.1: ACRES Core traced each column from the pose at its firing time.
Size#
| Layers | Bytes for Each Scan | Bytes for Each Hour at 10 Hz |
|---|---|---|
range |
115,200 | 4.15 GB |
range and intensity (a real scan) |
172,800 | 6.22 GB |
range and material (ACRES Core with no options) |
172,800 | 6.22 GB |
range, intensity and material (ACRES Core with the options) |
230,400 | 8.29 GB |
range, intensity, class and material (the game) |
288,000 | 10.37 GB |
The scan table and the object table add 0.4 kB for each scan.
Data Sets#
A data set is one folder below the data root. The tools make these kinds of data sets:
| Kind | Tool | Content |
|---|---|---|
| ACRES Core missions | python -m acres_data.generate_core |
The scans of single-field missions with objects on the path. Each sequence is one mission: a field, a soil condition, an object scenario and a seed. |
| ACRES Core twin of a route | python -m acres_data.replay_core |
An ACRES Core scan at the pose of each recorded scan of a drive |
| Game sensor episode | python -m acres_data.convert_game |
The GPU LiDAR scans of one sensor episode of the game |
| Polaris recording | python -m acres_data.convert_polaris, python -m acres_data.convert_extract |
The scans of a recorded bag or of a bag extract |
Each sequence of ACRES Core states the map hash, the simulation version and the hash of the sensor file. The split tag of a sequence (train, val, test) is that of its field in configs/splits_v1.json.
Reader#
LidarSet, Sequence#
LidarSet reads the index of a set. Sequence reads one sequence through memory maps.
The two classes need numpy only.
| Name | Description |
|---|---|
LidarSet.open(name, splits=None) |
A set below the data root. splits keeps the sequences with these tags. |
Sequence.range_m(index) |
The range in meters, float32. NaN is a cell without a return. index is one number, a slice or a list. |
Sequence.intensity(index) |
The intensity bytes, or None. |
Sequence.label(name, index) |
A label of a simulator. |
Sequence.scans |
The scan table. |
Sequence.calibration.points(range_m, frame) |
The points in the frame "sensor" or "base". |
Sequence.body_mask |
The cells that see the vehicle body. |
from acres_data import LidarSet
from acres_data.transforms import TopView, body_cells
ds = LidarSet.open("core-v3", splits=("train",))
seq = ds["S001"]
r = seq.range_m(100) # (32, 1800)
xyz = seq.calibration.points(r, "base") # (32, 1800, 3)
body = body_cells(seq.calibration, r, seq.body_mask)
grid = TopView()(seq.calibration, r, drop=body)
TopView#
The top view is a transform of the reader. It gives a grid in base_footprint: x forward, y left.
| Name | Default | Description |
|---|---|---|
x_min, y_min |
-25.6 m | The corner of the grid. |
cell_m |
0.2 m | The side of a cell. |
cells_x, cells_y |
256 | The number of cells. The grid is 51.2 m square, with the vehicle in the center. |
z_min, z_max |
-1 m, 3 m | The height band above base_footprint. |
The result has two channels. Channel 0 is 1 where a cell has a return. Channel 1 is the largest height in the cell.
ScanWindows#
acres_data.torch_dataset.ScanWindows is the PyTorch data set. It is the only module that needs torch.
One sample has K past scans, the next scans, and the motion of the vehicle between them.
A sample has no action. The commands are labels.
| Key | Shape | Description |
|---|---|---|
past |
(K, 32, 1800) or (K, 2, 256, 256) | The range in meters with 0 for no return, or the top view. |
future |
(F, ...) | The next scans in the same form. |
motion |
(K + F, 3) | x forward, y left and yaw of each scan in the frame of the last past scan. |
motion_valid |
(K + F) | False where a pose is degraded or absent. |
simulated, set, sequence, anchor |
The origin of the sample. |
A window does not cross a gap or the end of a sequence.
The argument match applies the sensor-matching step to simulated sequences only.
The reader with 8 worker processes gives about 7,000 range images, or about 1,500 top views, for each second. Run python -m acres_data.speed to measure your computer.
Sensor Differences#
The simulated sensor is the block lidar of Acres/Content/Simulation/sensors_polaris.json. The table compares it with the RoboSense Helios of the Polaris.
| Item | Real Sensor | ACRES Core |
|---|---|---|
| Rings and columns | 32 and 1800, clockwise, column 0 ahead | The same |
| Beam angles | Read from the sensor at its start | A table measured from recorded scans, within 0.007 degrees |
| Range | 0.2 m to 161.3 m, step 2.5 mm | 0.2 m to 161.3 m, noise 2.2 mm |
| Column time | 55.56 µs for each column: a sweep of 0.1 s | An option: each column traced from the pose at its firing time. Off: all beams at one instant. |
| Returns for each beam | One, the strongest | One |
| Intensity | A byte, 0 to 255 | An option of Batch.lidar: the same byte |
| Vehicle body | In the scan | From a table measured on real scans |
| Crops and ground cover | In the scan | An option: plant cells in the crop patches of the map, and a grass layer |
The game and ACRES Core share the crop canopy model (AcresLidarCanopy.cpp) and the crop state of a day (AcresLidarDayState.cpp). The game option -LidarCropState=<path> and the Python call Batch.set_lidar_crop_state load a daily crop state for the LiDAR. The page LiDAR gives the model and its validation.
Sensor-Matching Step#
The step acres_data.match.SensorMatch makes a simulated scan statistically like a real scan. It is a transform of the reader for simulated scans. It does not change stored data. It has a seed: the same scan gives the same result.
| Part | Function | Fitted Values |
|---|---|---|
| Sweep | Draws the scan of one instant as a sweep of 0.1 s. Each column gets the direction and the range for the pose at its firing time. | None |
| Keep | A return stays with a probability for its ring and its range bin (32 rings, 21 bins). | 672 values |
| Cell | A return stays with a probability for its ring and its group of 4 columns: the real pattern of missing cells. | 14,400 values |
| Noise | A normal error of the range on ground returns, one value for each ring. | 32 values, 0 to 70 mm |
| Chop | Removes the returns beyond one range. A mode of its own. | 150 m |
| File | Use |
|---|---|
Data/acres_data/configs/sensor_match_v1.json |
Scans of one instant with no crops and no intensity |
Data/acres_data/configs/sensor_match_v2.json |
Scans with crops, intensity and the sweep. The sweep part is off. |
Data/acres_data/configs/sensor_match_game_v0.json |
A first keep table for the GPU LiDAR of the game |
The step removes returns and adds none. Its keep table has no class, thus it also removes near crop returns. For a model that must see the crop near the vehicle, use the scans with crops and no step, or the noise part only.
Make a Data Set#
Run the commands in the repository root with the Python of your build. Each tool has the option --workers. The result does not depend on the number of workers.
-
Set the paths:
-
Make a simulated data set with crops, intensity and the sweep:
python -m acres_data.generate_core --set core-v3 --crops --sweep --seed 1 --fields-tile 20,6,10 --fields-region 20,7,12The run-time library drives each mission with the skills
scoutandreturn_home.--listshows the missions. -
Convert a sensor episode of the game:
-
Fit the sensor-matching step on a twin of a route:
-
Measure the reader:
python -m acres_data.speed --out <file>.
One ACRES Core scan of 57,600 beams takes about 28 ms on one thread.
Limits#
- A simulated crop is mature and standing in each planted field, with the marks of the vehicle. It has no plant rows.
- The crop state of the site on a recorded day is not known unless a crop-state file gives it.
- The body rule does not remove the motion in the sweep before it tests the body box.
- A data set from the game needs Unreal and a GPU, and only one game can run at a time.