PyOpenCRG
Unofficial implementation of the OpenCRG file format. Not affiliated with, endorsed by or supported by ASAM e.V. OpenCRG® is a registered trademark of ASAM e.V.
Belgian block sample road of the ASAM OpenCRG project, drawn with road.view3d(color="grid", cmap="jet", z_scale=2.5) (relief exaggerated 2.5 times).
PyOpenCRG (import pyopencrg) reads, analyses, edits, shows and writes OpenCRG
road surface files (.crg) in pure Python. You do not need to compile anything, and it runs on Windows, Linux
and macOS.
- All OpenCRG formats: binary
KRBI/KDBIand textLRFI/LDFI, with full header support (comments, options, modifiers, map projection, WGS84). - Fast: everything is vectorised with numpy. A file of 10 million values is read in 0.1 s (binary) or about 4 s (text).
- Accurate: data stays in double precision (float64). The algorithms follow the OpenCRG reference tools.
- Simple: one object,
CrgRoad, with plain-named methods. Every editing method returns a new road. - Complete: everything in the OpenCRG MATLAB tools, plus tools to clean laser scans, convert STL meshes and export to OBJ / STL / CSV / images / Simpack.
French guide: docs/guide_fr.md. Equivalence with the MATLAB tools: docs/matlab_equivalence.md.
Installation
pip install "pyopencrg[all]" # recommended: everything (scipy, matplotlib, PyVista, Pillow)
pip install pyopencrg # light: only numpy (read, check, evaluate, edit, write)
The light installation is useful in a simulation or server environment (e.g. conda, where mixing pip and conda packages can conflict). The other features need an extra package; if it is missing, the error message gives the command to install it:
| Extra | Package | Needed for |
|---|---|---|
scipy |
scipy | smoothing, filters, re-sampling of curved roads, scan repair, point cloud import |
plot |
matplotlib | figures (road.show(), road.plot...) and image export |
viewer |
PyVista | interactive 3D viewer road.view3d() |
image |
Pillow | engrave_image() |
In 10 lines
import pyopencrg as crg
road = crg.read("my_road.crg") # any OpenCRG file
print(road) # summary: length, width, grid, slope...
print(road.check()) # consistency checks
z = road.uv_to_z(120.0, 0.5) # height 120 m along the road, 0.5 m left of the reference line
x, y = road.uv_to_xy(120.0, 0.5) # where this point is in the world
part = road.crop(u=(100, 200)) # keep 100 m of road (a NEW road, `road` is unchanged)
part.save("part.crg", fmt="LDFI") # save as a text file
road.show() # overview figures
road.view3d() # interactive 3D view (PyVista)
Files are checked when they are read: a truncated, unreadable or badly defined file raises a
CrgCheckError explaining the problem, and minor problems are shown once as a warning. Use
crg.read(path, check=False) to open a damaged file anyway (to repair it), or check="strict" to
refuse files with warnings too.
Large files: only what you ask for is loaded.
preview = crg.read("scan.crg", step=4) # one point out of 4 in both directions (16 x less memory)
detail = crg.read("scan.crg", u=(500, 520)) # 20 m at full resolution
light = crg.read("scan.crg", dtype="float32") # half the memory for the elevation grid
with crg.open("scan.crg") as f: # open once, like a Python file...
for u0 in range(0, int(f.length), 100):
part = f.read(u=(u0, u0 + 100)) # ...then read parts in a few milliseconds
A part gives exactly the same x, y, z as the whole road. With apply_mods=True (in f.read() or
crg.read(path, u=...)), the modifiers are computed on the whole file, so the part is placed
exactly where it is in the modified whole road.
Tyre contact and simulation
The surface normal and the slopes are exact derivatives of the interpolation (grid, slope, banking and curvature of the road included). For a simulation loop, an evaluator prepares everything once:
nx, ny, nz = road.xy_to_normal(x, y) # unit normal, (0, 0, 1) on a flat road
dz_du, dz_dv = road.uv_to_gradient(u, v) # slopes along and across the road
ev = road.evaluator() # prepared once
for step in range(n_steps):
z, nx, ny, nz = ev.xy_to_z(x, y, with_normal=True) # height and normal of 640 contact points: ~0.2 ms
The evaluator remembers the last position of each point and starts the next search from there.
Give u_hint (one value, e.g. the u of the wheel) to road.xy_to_uv() / road.xy_to_z() when the
road passes several times close to the same place.
Files larger than the memory: f.evaluator() has the same methods and keeps only a window of
the road in memory. The window is read again (a few milliseconds) when a point comes closer than
margin to one of its ends; the results are the ones of the whole road.
with crg.open("track.crg") as f:
ev = f.evaluator(window=40.0, margin=10.0, apply_mods=True) # 40 m in memory
for step in range(n_steps):
z, nx, ny, nz = ev.xy_to_z(x, y, with_normal=True)
Speed of every method: python benchmarks/bench_evaluation.py.
Gallery
Height map of the whole road, and a zoom where every 1 cm grid point is visible
(road.plot.heightmap(cmap="jet"); zooming in a figure re-samples the image automatically):
Reference line, valid data limits, cross sections and long sections (road.plot.show_cuts_and_limits()):
Pictures rendered from the sample belgian_block.crg of the ASAM OpenCRG project
(data provided by DaimlerChrysler, 2005; Apache License 2.0). The sample file is not distributed with PyOpenCRG;
the pictures are made with docs/make_images.py.
Coordinates in one picture
OpenCRG describes a road along a reference line (usually the road centre):
u= distance along the reference line (m),v= lateral distance to it (m, positive to the left);x, y, z= position in the world.
The data is a grid road.z of shape (nu, nv): road.z[i, j] is the height at u = road.u[i],
v = road.v[j], relative to the reference line. The absolute height of the road is
z_absolute = road.z + road.zr (reference line elevation) + road.banking * v
road.uv_to_z(u, v) always returns the absolute height.
What you can do
| Task | Code |
|---|---|
| Read / write | crg.read(path), crg.open(path), road.save(path, fmt="KRBI"), crg.convert(a, b, "LDFI") |
| Information | print(road), road.check(), road.stats() |
| Elevation, position | uv_to_z, uv_to_xy, xy_to_uv, xy_to_z, u_to_phi, u_to_curvature |
| Normal, slopes | uv_to_normal, xy_to_normal, uv_to_gradient, xy_to_z(..., with_normal=True) |
| Fast repeated calls | ev = road.evaluator(), then ev.xy_to_z(...), ev.uv_to_z(...)... |
| Simulation on a huge file | with crg.open(path) as f: ev = f.evaluator(window=40.0) |
| Profiles | road.long_section(v=0.9), road.cross_section(u=50) |
| Cut, join, flip | crop(u=..., v=...), trim(1, 1), crg.concatenate([r1, r2]), flip() |
| Re-sample | resample(u_inc=0.05, v_inc=0.05) |
| Slope / banking | extract_slope(), extract_banking(), separate_slope_banking(), set_banking() |
| Modifiers | road.mods, apply_mods(), move_to(x, y, z, phi), transform(...), scale(...) |
| Filter | filter("gauss", 5), clip_z(-0.05, 0.05) |
| Repair scans | remove_artifacts(0.3, 15), fill_holes("linear"), fill_nan(), find_peaks() |
| Synthetic roads | crg.generate.synthetic_road(...), from_function, road_from_points, trapezoid_bumps |
| Import | crg.importers.from_stl(path), from_point_cloud(x, y, z) |
| Add features | add_profile(...), add_random(...), overlay_uv(other), engrave_image("logo.png") |
| Export | to_obj, to_stl, to_csv, to_trm (Simpack), save_heightmap, to_html_map |
| Geodesy | xy_to_wgs84, xyz_to_llh, llh_to_xyz, crg.wgs84.distance(...) |
| Plots | road.show(), road.plot.heightmap() (zoom shows every grid point), road.plot.cross_sections(), ... |
| 3D viewer | road.view3d(color="z" / "grid" / "normal", z_scale=5) (millions of points, GPU) |
Every function has a detailed docstring: type help(road.crop) in Python.
Command line
pyopencrg info road.crg
pyopencrg check road.crg
pyopencrg show road.crg
pyopencrg view road.crg # 3D viewer (PyVista)
pyopencrg convert road.crg road_text.crg --fmt LDFI
pyopencrg export road.crg road.obj --step 2 # .obj .stl .csv .png .html .trm
Examples
See the examples folder: reading and plotting, editing, synthetic roads, cleaning a laser scan, STL to CRG conversion, an obstacle test track.
Modifiers: a word of warning
The $ROAD_CRG_MODS block of a file tells a simulator how to modify the road when it loads it
(scale it, fill NaN values, move it). crg.read() returns the road as stored in the file; call
road.apply_mods() (or crg.read(path, apply_mods=True)) to get what a simulator sees. For a part
of a file, use crg.read(path, u=..., apply_mods=True) or f.read(u=..., apply_mods=True): the
modifiers are then computed on the whole file (part.apply_mods() would move the part relative to
its own start). If a file
has no modifier block, OpenCRG applies default modifiers: NaN values are replaced by the last
valid value of the cross section and the road start is moved to x = y = z = 0 with heading 0.
Development
pip install -e ".[test,image]"
pytest
Citing
If you use pyopencrg in a report, a publication or another software, please cite it (see CITATION.cff):
Hardy, T. PyOpenCRG: OpenCRG road surface files in pure Python, version 0.2.2, 2026.
License
Apache License 2.0, see LICENSE and NOTICE. Parts of the algorithms are derived from the ASAM OpenCRG MATLAB tools (Apache 2.0).
Metadata
Release files for PyOpenCRG 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyopencrg-0.2.2.tar.gz | 788.1 kB | With provenance | Details for pyopencrg-0.2.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyopencrg-0.2.2-py3-none-any.whl | Python 3 | none | any | Details for pyopencrg-0.2.2-py3-none-any.whl |
Total release size: 905.7 kB
Release files / pyopencrg-0.2.2.tar.gz
| Download URL | pyopencrg-0.2.2.tar.gz |
|---|---|
| Size | 788.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
73358c1a2b6897893aaf63a7677df91b1d8df5b4c6909b53d013d1e2dbdaad2b
|
|
BLAKE2b-256 checksum How to use checksums |
f196203c68064ee36711bc7927dfe4bd187c4529cbcea33366bd5f2c6e02083b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logRelease files / pyopencrg-0.2.2-py3-none-any.whl
| Download URL | pyopencrg-0.2.2-py3-none-any.whl |
|---|---|
| Size | 117.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a9cffeaa172423a7505002d506d98fdfe1adadb5883e809b9031af8881de8e3
|
|
BLAKE2b-256 checksum How to use checksums |
5df1de1b3b92641fe153111e36a2306ae4507f006460c2b95439b4b7b043064b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logSecurity for PyOpenCRG 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for pyopencrg-0.2.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.