Documentation menu

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#

LocationPlanar1/jpg/ inside the pass, directly — subfolders are not read
Extension.jpg, lowercase
NamingThe CSV's photo name plus .jpg
DimensionsNot 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 (HHMMSS plus 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.

SeparatorComma
PreambleNone — the header row comes first
ColumnsPositional — the header row is required but its names are ignored
Extra columnsDeliver only the columns listed; others are reserved for fields this format may gain
Decimals. only
Photo namesMatched with .jpg appended — first hit wins; case-sensitive
Which fileThe one .csv directly inside Planar1/ — any name, but exactly one: a second fails the delivery
ColumnTypeMeaning
0textPhoto name without the .jpg extension
1numberEasting, in the dataset's CRS
3numberNorthing
5numberHeight
7numberOmega, degrees — see rotation
9numberPhi, degrees
11numberKappa, degrees
13numberFocal length, millimetres — stored as delivered
14numberPixel size, micrometres — stored as delivered
16textLocal capture time (Lokale_tijd), HHMMSS plus 1–6 fraction digits — see capture time
17textCapture 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:

supplierConvention
IGLIGL's export, as delivered
VolkerVolker'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
RecursionYes — subfolders under the chosen prefix are scanned
Minimum size10 bytes — smaller files are skipped with a warning rather than failing the run
  • Every .laz file's public header must parse. A corrupt header fails the delivery here instead of the point-cloud conversion later.
  • Every .laz file 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 .laz files in its Pointcloud/ 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 u16 field, or ±DBL_MAX on an f32, 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#

KeyRequiredTypeMeaning
supplieryesone of IGL, VolkerWho 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#

SymptomCause
No Train_* folders foundThe passes are nested more than three levels deep, or named differently
No CSV in a passThat pass's Planar1/ folder has no CSV, or it sits one folder deeper
A photo is not a JPEGThe supplier export contains a file of normal size whose bytes are all zero
A LAZ file is truncatedThe upload of that file was interrupted — upload it again
Photos point the wrong wayThe 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 nameThe same pass was delivered twice, in two delivery folders