Mobile mapping generic
The general-purpose mobile-mapping delivery: 360° panoramas with a pose CSV, and optionally point clouds alongside them. If you are delivering street-level imagery and nobody has told you otherwise, this is the template.
Panoramas and point clouds are indexed independently, so a delivery may contain either or both — but not neither.
Folder layout#
<dataset bucket>/
├── panorama/ one of three accepted names — see below
│ ├── poses.csv exactly one CSV, any name
│ └── jpg/
│ ├── PANO_0001.jpg
│ └── PANO_0002.jpg
└── pointcloud/ optional; see Point cloud generic for details
└── scan_001.laz
The panorama base folder is looked for under these names, in this order — the first base folder containing any object wins, and candidates below it are then dead:
panorama/ingest/Panorama1/Panorama1/
Note the asymmetry that catches people out: once a base folder is chosen, an empty images folder inside it skips this half of the delivery entirely — it does not fall through to the next candidate.
The pointcloud folder is looked for under these names, in this order — the first containing at least one matching file wins:
pointcloud/Pointcloud/ingest/Pointcloud/
Each half is optional on its own, but a delivery must contain at least one of them: a bucket with no panorama or pointcloud content is rejected rather than processed into nothing.
Images live in the chosen base's jpg/ subfolder. No panorama folder at
all is not an error; the step finishes and only the point clouds are
processed. A panorama folder with no CSV is an error.
Every delivered photo is opened and checked, not just listed:
- Every delivered photo must start with a JPEG marker and end with an end-of-image marker. This catches a file that is not a JPEG whatever its extension says, and the truncated upload — which a size check cannot see, because a truncated file just looks smaller and nothing knows how big it should have been.
The point cloud half follows Point cloud generic exactly, with one difference: here a missing point cloud folder is skipped rather than fatal. Its files must also hold together:
- The LAS public header of each point cloud file must parse (it is read from the first 64 KiB). A corrupt or truncated header fails the run, naming the file.
- Point-data record formats 0 and 2 carry no GPS time, so the merged point cloud gets no temporal index and cannot be filtered by capture time. (a warning, not an error)
- The point records must actually be there: a header can parse while the data behind it is truncated (a half-finished upload or copy). Checked for uncompressed LAS, where the declared point count and record length give an exact expected size; LAZ stores points in variable-size chunks, so the same arithmetic does not apply.
The pose CSV#
CSVs in subfolders are ignored. If you deliver two, only one is used — so deliver one. The file must be UTF-8.
| Separator | Comma |
| Preamble | None — the header row comes first |
| Columns | Positional — the header row is required but its names are ignored |
| Extra columns | Ignored |
| Decimals | Both . and , accepted |
| Photo names | Matched exactly as written, then with .jpg appended, then with .jpeg appended — first hit wins; case-sensitive |
| Which file | The alphabetically first .csv directly inside the folder itself |
| Column | Type | Meaning |
|---|---|---|
0 | text | Photo filename — resolved against the real jpg/ listing; see filename matching |
1 | number | Easting, in the dataset's CRS |
2 | number | Northing |
3 | number | Height |
4 | number | Heading (pan), degrees |
5 | number | Pitch (tilt), degrees |
6 | number | Roll, degrees |
7 | timestamp | Capture time |
Because columns are positional, the header row exists only so the parser can skip it. Name the columns however you like — but you must have a header row, or your first photo is silently dropped. Columns after position 7 are ignored, so extra fields are harmless.
Timestamps#
timestamp accepts any of: epoch seconds (whole or fractional); RFC 3339 (2026-05-07T09:18:00Z); YYYY-MM-DD HH:MM:SS[.fff] (read as UTC); YYYY-MM-DDTHH:MM:SS[.fff] (read as UTC); YYYY/MM/DD HH:MM:SS[.fff] (read as UTC); DD-MM-YYYY HH:MM:SS[.fff] (read as Dutch local time, Europe/Amsterdam); YYYY-MM-DD (UTC midnight).
Formats are tried in that order. Every date-string shape takes optional
fractional seconds. The unzoned shapes are read as UTC, except the
day-first DD-MM-YYYY shape, which is read as Dutch local time
(Europe/Amsterdam, so CET or CEST depending on the date).
Example#
filepath,x,y,z,heading,pitch,roll,timestamp
PANO_2025-12-13_15-09-06_704440.jpg,182518.396,477934.743,37.326,0.0,0.0,0.0,1765631346.704
PANO_2025-12-13_15-09-08_112900.jpg,182519.204,477936.881,37.341,1.4,0.1,-0.2,1765631348.113
Filename matching#
Both .jpg and .jpeg photos are accepted. The name in column 0 is resolved
against the real listing of the jpg/ folder: a name that already ends in
.jpg or .jpeg is used as-is, and a bare name is tried with .jpg
appended, then .jpeg.
Matching is case-sensitive. Only files directly inside jpg/ are
considered — subfolders are not scanned.
Tolerance#
- Up to 10 % of a recording's CSV rows may reference missing photos. Beyond that the run fails, naming the recording.
- Photos in the delivery that the CSV does not reference are not indexed (noted, not an error).
The missing-photo allowance exists so one corrupt frame doesn't sink a delivery of 50 000.
Ingest parameters#
| Key | Required | Type | Meaning |
|---|---|---|---|
anonymisation | yes | string — recognised: already_anonymised, auto_anonymise | Compared exactly: already_anonymised marks the photos as already blurred, auto_anonymise queues them for blurring, any other value does neither. |
height_above_ground | yes | number | Camera height above the road surface, in metres. |
set_name | no | string | The set the indexed photos are grouped under. Default: 1. |
tile_pointclouds | no | flag — anything except missing, null, false or "no" enables it | Grid-tile the point clouds; the tiles, not your source files, are then indexed. |
Both required keys really are required — a dataset created with no parameters at all fails at this step.
Coordinate reference system#
Nothing is reprojected. Positions are stamped with the dataset's 3D SRID, point cloud boundaries with its 2D SRID. See Coordinates are not reprojected.
Deliver heading, pitch and roll as your capture system produces them. See Rotation angles.
What you get back#
- Indexed panorama records with positions and orientations.
- A tilepack (
.tpc) per photo. Omnibase tenants view the JPEG; DZP is not generated at ingest. - Blurred derivatives, if
anonymisationisauto_anonymise. - Indexed point clouds plus a merged COPC, if point clouds were delivered.
- Grid-tiled point clouds under
pointcloud_tiles/, iftile_pointcloudsis enabled — in which case the tiles, not your source files, are what gets indexed.
Common failures#
| Symptom | Cause |
|---|---|
| "the delivery has no panorama or pointcloud content" | Nothing was uploaded, or everything was uploaded under folder names this template does not look in |
| "no CSV file found under …" | The base folder exists but has no CSV directly inside it |
| Panoramas silently skipped | The base folder exists but has no jpg/ subfolder — this does not fall through to the next candidate folder |
| First photo missing | Your CSV has no header row, so row 1 was consumed as one |
| "missing required key height_above_ground" | The dataset was created with no ingest parameters |
| More than 10 % of photos missing | Filename case mismatch, or images in a subfolder of jpg/ rather than directly inside it |