ProRail PVR
Train-mounted inspection deliveries. A supplier uploads one or more root delivery folders, each holding many individual passes. Every pass becomes its own set.
This is the older ProRail template, for imagery captured from a train. The 2025 tenders for station and street captures are separate: stationsopnamen and straatopnamen.
Folder layout#
<dataset bucket>/
├── PVR2026 P1 DL1/ root delivery folder, any name
│ ├── Train_260226_071432_PVR/ one pass — name must start with "Train_"
│ │ ├── Planar1/
│ │ │ ├── 20260226_run.csv exactly one CSV, any name
│ │ │ └── jpg/
│ │ │ ├── 0000001.jpg
│ │ │ └── 0000002.jpg
│ │ └── Pointcloud/
│ │ ├── tile_001.laz
│ │ └── tile_002.laz
│ ├── Train_260226_084501_PVR/
│ │ └── …
│ └── Rapportage/ ignored
└── PVR2026 P1 DL2/
└── …
Passes are found by scanning for folders whose name starts with Train_, up
to three levels deep. Everything else at that level — report folders, stray
files — is ignored, so you do not need to clean the delivery up first.
The set name is the pass folder's own name, e.g. Train_260226_071432_PVR.
Inside each pass:
Planar1/*.csv | Exactly one. The filename is not fixed — it carries a date and varies by supplier, so it is found by glob |
Planar1/jpg/*.jpg | The photos. .jpg only |
Pointcloud/*.laz | .laz only — .las is not picked up |
A pass with no CSV, or with an empty jpg/ folder, is skipped with a note
rather than failing the run.
If no Train_* folder is found anywhere, the run finishes having done nothing.
The position CSV#
| Separator | Comma |
| Header row | Required — the first row is consumed as one |
| Columns | Positional. Header names are ignored entirely |
| Values | Read as text, then converted — so a stray quote or space is tolerated |
Because columns are positional, only the index matters. Note the gaps: the file carries paired value/quality columns, and only the value half is read.
| Position | Meaning |
|---|---|
| 0 | Photo filename, without the .jpg extension |
| 1 | X — easting, in the dataset's CRS |
| 3 | Y — northing |
| 5 | Z — height |
| 7 | Omega, degrees |
| 9 | Phi, degrees |
| 11 | Kappa, degrees |
| 13 | Focal length, millimetres |
| 14 | Pixel size, micrometres |
| 16 | Capture time, HHMMSSffffff |
| 17 | Capture date, YYYYMMDD |
Positions 2, 4, 6, 8, 10, 12 and 15 are not read. Columns past 17 are ignored.
Focal length and pixel size are stored without unit conversion — deliver them in mm and µm respectively, which is what the standard export already does.
The date and time are concatenated and parsed as YYYYMMDDHHMMSSffffff, so
position 16 must carry sub-second digits.
Rotation angles#
The two suppliers export in different conventions, so the supplier parameter
is required and is not guessed:
supplier | Applied |
|---|---|
IGL | Angles used as delivered, with +90° added to kappa |
Volker | All three angles negated |
Any other value is a hard error. Getting this wrong produces photos that point in plausible but wrong directions rather than an obvious failure, so confirm it with the supplier rather than inferring it.
Photo dimensions#
Not delivered and not configured. The pixel dimensions are read once per pass from a single photo's header — every photo in one pass comes from the same camera run and shares a resolution, but that resolution differs between suppliers and runs, so it cannot be assumed.
Up to 20 photos are tried before giving up, because some supplier exports contain corrupt images whose file size looks normal. If all 20 fail, that pass is skipped with a note.
Tolerance#
- A CSV row whose photo is missing → skipped, with a note. No percentage limit.
- A pass with no CSV, no photos, or unreadable photo dimensions → skipped, and the rest of the delivery continues.
Nothing here fails the run outright. A delivery that is half-broken indexes half-complete, so compare the indexed count against what you sent.
Ingest parameters#
| Key | Required | Values | Default |
|---|---|---|---|
supplier | yes | IGL or Volker | — |
set_name | no | string — names the merged point cloud only | 1 |
set_name does not name the photos or the per-pass point clouds; those
always take their pass folder's name.
Coordinate reference system#
Nothing is reprojected. Positions are stamped with the dataset's SRID and point cloud boundaries come from the LAS headers unchanged. See Coordinates are not reprojected.
Each photo gets a 20 m square footprint centred on its capture position. That is fixed and not configurable.
What you get back#
- Indexed photos per pass, with camera geometry and footprints.
- Indexed point clouds per pass, keeping the delivered folder structure.
- A single merged COPC at
copc/pointcloud.copc.lazacross every pass.
Photos are never downloaded during indexing — only listed, plus one header read per pass — so a delivery of hundreds of gigabytes indexes without moving the bulk of it.
Common failures#
| Symptom | Cause |
|---|---|
| "No Train_* folders found" | The pass folders are nested more than three levels deep, or named differently |
| A pass missing entirely | No CSV in its Planar1/, an empty jpg/, or every sampled photo corrupt |
| Photos point the wrong way | The wrong supplier value — the two conventions differ |
| "Unknown supplier" | supplier is set to something other than IGL or Volker |
| Capture times wrong | Position 16 has no sub-second digits, or the date and time columns are swapped |
| First photo missing | The CSV has no header row, so row 1 was consumed as one |