ProRail PVR
Train-mounted inspection runs for ProRail. Upload one folder per pass, its name starting with Train_, each holding the Planar1 photos with their position CSV and the pass's LAZ point cloud.
Every pass becomes its own set. Deliveries in the older folder layout use ProRail PVR (older deliveries).
Folder layout#
<dataset bucket>/
├── PVR2026 P1 DL1/ optional 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/
│ │ │ ├── Perspectiefopname_20260226_071440120_P1.jpg
│ │ │ └── Perspectiefopname_20260226_071440320_P1.jpg
│ │ └── Pointcloud/
│ │ ├── PVR_260226_071432_117700_490700_P1.laz
│ │ └── PVR_260226_071432_117700_490800_P1.laz
│ ├── Train_260226_084501_PVR/
│ │ └── …
│ └── Rapportage/ not read
└── Train_260227_063015_PVR/ a pass may also sit at the root
└── …
One folder per photos instance, named Train_*, found anywhere from the bucket root down to 3 levels deep — so they may sit inside delivery folders of your own naming. A matching folder is not searched further, and each needs a name of its own across the whole delivery. Other folders at those levels are ignored.
One folder per pointcloud instance, named Train_*, found anywhere from the bucket root down to 3 levels deep — so they may sit inside delivery folders of your own naming. A matching folder is not searched further, and each needs a name of its own across the whole delivery. Other folders at those levels are ignored.
The set name is the pass folder's own name, e.g. Train_260226_071432_PVR.
Two passes with the same name in one delivery — for instance the same pass in
two delivery folders — are rejected, because they would become one set.
Report folders, notes and anything else not named Train_* may stay in the
delivery; they are not read.
Photos#
| Location | Planar1/jpg/ inside the pass, directly — subfolders are not read |
| Extension | .jpg, lowercase |
| Naming | The CSV's photo name plus .jpg |
| Dimensions | Not delivered: read from the photos themselves. Every photo in one pass must share one resolution |
- Every row of every pass's pose CSV must parse the way processing reads it: at least 18 columns, numbers in the value columns, a capture date (
YYYYMMDD) and a local capture time (HHMMSSplus one to six fraction digits) that exists in Dutch local time — the hour skipped when the clocks go forward does not. - Each row's local time (position 16), read as Dutch local time, must agree with its GPS time (position 15) to within a minute. The export carries both clocks; a row where they disagree by an hour or more has one of them in a different time zone than its column says, and its photo would be placed at the wrong moment. (a warning, not an error)
- Every delivered photo with content must start with a JPEG marker and end with an end-of-image marker. An empty photo (under 1 KiB) is not this check's business: it is skipped with a warning, like a missing one. A photo with content that is not a JPEG fails, because processing cannot convert it.
The position CSV#
Exactly one .csv directly inside each pass's Planar1/ folder. The
filename is free — it usually carries a date.
| Separator | Comma |
| Preamble | None — the header row comes first |
| Columns | Positional — the header row is required but its names are ignored |
| Extra columns | Deliver only the columns listed; others are reserved for fields this format may gain |
| Decimals | . only |
| Photo names | Matched with .jpg appended — first hit wins; case-sensitive |
| Which file | The one .csv directly inside Planar1/ — any name, but exactly one: a second fails the delivery |
| Column | Type | Meaning |
|---|---|---|
0 | text | Photo name without the .jpg extension |
1 | number | Easting, in the dataset's CRS |
3 | number | Northing |
5 | number | Height |
7 | number | Omega, degrees — see rotation |
9 | number | Phi, degrees |
11 | number | Kappa, degrees |
13 | number | Focal length, millimetres — stored as delivered |
14 | number | Pixel size, micrometres — stored as delivered |
16 | text | Local capture time (Lokale_tijd), HHMMSS plus 1–6 fraction digits — see capture time |
17 | text | Capture date (Opnamedatum), YYYYMMDD |
The file pairs each value with a quality column, so the value columns skip one position at a time: positions 2, 4, 6, 8, 10, 12 and 15 are not read. Columns past position 17 are not read either, but do not rely on that — deliver the standard export as it is.
Focal length and pixel size are stored exactly as delivered, so they must be in millimetres and micrometres, which is what the standard export writes.
Rotation angles#
The two suppliers export omega, phi and kappa in different conventions. The
supplier parameter says which one a delivery uses:
supplier | Convention |
|---|---|
IGL | IGL's export, as delivered |
Volker | Volker's export, as delivered |
Getting this wrong places photos plausibly but wrongly rather than failing, so confirm it with the supplier rather than inferring it.
Capture time#
Each row's capture moment is its date (position 17, Opnamedatum,
YYYYMMDD) followed by its local time (position 16, Lokale_tijd, HHMMSS
plus one to six fraction digits), in Dutch local time — CET in winter,
CEST in summer — whichever supplier exports it. For example 20260226 and
071440120 is 26 February 2026, 07:14:40.120 Dutch time.
Keep the GPS time (position 15, GPS_tijd) in the export as it is: each
row's local time is compared with it, and a row where the two disagree by
more than a minute is reported. A time in the hour skipped when the clocks go
forward does not exist and is rejected; in the hour that repeats when they go
back, the first occurrence is used.
Point clouds#
Optional, per pass: the .laz files in the pass's Pointcloud/ folder
(subfolders included). Deliver the tiles as exported; they are kept as they
are, in a set named after their pass.
| Extensions | .laz, matched case-insensitively |
| Recursion | Yes — subfolders under the chosen prefix are scanned |
| Minimum size | 10 bytes — smaller files are skipped with a warning rather than failing the run |
- Every
.lazfile's public header must parse. A corrupt header fails the delivery here instead of the point-cloud conversion later. - Every
.lazfile must be complete. The compressor writes its chunk table last, so an interrupted upload loses it: a file whose recorded table position lies past its end is truncated and cannot be decompressed. - A pass without
.lazfiles in itsPointcloud/folder is indexed as photos only. That is allowed, but every delivery so far has had point clouds in every pass, so it is worth checking that none were left out. (a warning, not an error) - Where a file describes extra per-point fields (the LAS 1.4 Extra Bytes record), every minimum and maximum a field descriptor flags as filled in must be a real statistic: a value the field's data type can hold, not NaN or infinity, and a minimum no larger than the maximum. Unset placeholders such as the largest 64-bit integer on a
u16field, or ±DBL_MAX on anf32, fail the delivery. Write the actual minimum and maximum, or clear the descriptor's min/max flags (options bits 1 and 2) when your software does not compute them. The record must also be a whole number of 192-byte descriptors.
Tolerance#
- Photos in the delivery that the CSV does not reference are not indexed (noted, not an error).
- A recording with no CSV under
Planar1/→ not indexed, with a note.
Missing photos do not reject a delivery. Each one is reported as a warning, and the delivery is processed without it:
- a CSV row whose photo is not in
Planar1/jpg/; - an empty photo (under 1 KiB) — supplier exports have contained files of 0 bytes;
- a pass without its CSV, or without any usable photo: its photos are left out, its point clouds are still processed.
The reviewer then decides at checkup whether what arrived is complete enough. A photo with content that is not a JPEG does reject the delivery, because it cannot be converted — replace it, or remove it and let its row count as missing.
Ingest parameters#
| Key | Required | Type | Meaning |
|---|---|---|---|
supplier | yes | one of IGL, Volker | Who exported the delivery. The two suppliers write rotation angles in different conventions, so this is required and never guessed: a wrong value points photos plausibly but wrongly rather than failing. |
Coordinate reference system#
Nothing is reprojected. Positions are read in the dataset's coordinate reference system, and point-cloud extents come from the LAZ headers as they are. See Coordinates are not reprojected.
Each photo is shown with a 20 m square footprint centred on its position.
What you get back#
- The photos per pass, with camera geometry and footprints.
- The point clouds per pass, keeping the delivered folder structure.
- A single merged point cloud across every pass, for viewing.
Common failures#
| Symptom | Cause |
|---|---|
No Train_* folders found | The passes are nested more than three levels deep, or named differently |
| No CSV in a pass | That pass's Planar1/ folder has no CSV, or it sits one folder deeper |
| A photo is not a JPEG | The supplier export contains a file of normal size whose bytes are all zero |
| A LAZ file is truncated | The upload of that file was interrupted — upload it again |
| Photos point the wrong way | The wrong supplier value — the two conventions differ |
| Local time "away from GPS_tijd" | Lokale_tijd was exported in UTC, or GPS_tijd in another clock — deliver local time in position 16 |
| Two passes with the same name | The same pass was delivered twice, in two delivery folders |