Documentation menu

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/*.csvExactly one. The filename is not fixed — it carries a date and varies by supplier, so it is found by glob
Planar1/jpg/*.jpgThe 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#

SeparatorComma
Header rowRequired — the first row is consumed as one
ColumnsPositional. Header names are ignored entirely
ValuesRead 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.

PositionMeaning
0Photo filename, without the .jpg extension
1X — easting, in the dataset's CRS
3Y — northing
5Z — height
7Omega, degrees
9Phi, degrees
11Kappa, degrees
13Focal length, millimetres
14Pixel size, micrometres
16Capture time, HHMMSSffffff
17Capture 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:

supplierApplied
IGLAngles used as delivered, with +90° added to kappa
VolkerAll 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#

KeyRequiredValuesDefault
supplieryesIGL or Volker
set_namenostring — names the merged point cloud only1

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.laz across 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#

SymptomCause
"No Train_* folders found"The pass folders are nested more than three levels deep, or named differently
A pass missing entirelyNo CSV in its Planar1/, an empty jpg/, or every sampled photo corrupt
Photos point the wrong wayThe wrong supplier value — the two conventions differ
"Unknown supplier"supplier is set to something other than IGL or Volker
Capture times wrongPosition 16 has no sub-second digits, or the date and time columns are swapped
First photo missingThe CSV has no header row, so row 1 was consumed as one