Skip to main content

argus-survey

Point-cloud deliverables for land surveyors — with every datum, transformation and command on the record.

Give it LiDAR, drone photogrammetry (LAS/LAZ/COPC) or terrestrial scans (E57) and it produces what a surveyor hands over: a QA report, ground classification, DTM/DSM/CHM, contours, stockpile volumes, cut/fill against a design, long sections, change between epochs and a vertical accuracy statement against check points (ASPRS Edition 2). Each job ends in a printable report.html and a job.json that records the inputs' SHA256s, the CRS, the exact PROJ operation and grid applied, and every PDAL pipeline and GDAL command in the order it ran.

It is built to be judged on correct datums, honest numbers and reproducible outputs:

  • No silent datums. Heights without a vertical datum are refused for height work unless you say what they are (--assume-vertical), and then the report carries a red banner. A transformation that would need a missing geoid grid is refused — never quietly downgraded to a "ballpark" one (around Perth that is ~30 m).
  • Numbers come with their uncertainty. Volumes state the cell size, void %, method and the base's fit; accuracy states n, exclusions, the standard and edition; change states its level of detection.
  • Tested against answers known in advance: a cone stockpile of πr²h/3 on a tilted plane, a 1 m apex growth between epochs, ellipsoidal ⇄ AHD round trips through AUSGeoid2020 to the millimetre.

First audience: Western Australian civil surveyors (MGA2020 zones 49–51, PCG2020 and the other WA LSG2020 grids, AHD via AUSGeoid2020). Nothing is hard-coded to WA; other regions work the same way.

The viewer: hillshade, contours and coverage on the map; the classified cloud streamed from COPC

Five-minute start

You need Docker (with compose) and git. Everything else — PDAL 2.10, GDAL 3.13, PROJ 9.9 and the datum grids — is inside the image.

git clone https://github.com/smk762/argus-survey && cd argus-survey
cp .env.example .env            # set UID/GID to `id -u` / `id -g`
make image                      # builds, then runs `argus-survey doctor --strict` inside
make demo                       # a synthetic site with known answers → data/jobs/demo
make up                         # API + viewer on http://127.0.0.1:8107/viewer

data/jobs/demo/report.html is the deliverable; open it in a browser and print it to PDF.

Walkthrough on real, public LiDAR

make sample   # ~160k points of USGS 3DEP (public domain) → data/in/usgs-sample.laz

docker compose run --rm survey info /data/in/usgs-sample.laz
#   CRS: WGS 84 / Pseudo-Mercator (EPSG:3857) · vertical: none

docker compose run --rm survey run /data/in/usgs-sample.laz --job usgs \
    --assume-vertical EPSG:5703 --t-srs EPSG:6340+5703 --res 0.5 --interval 1

What that shows:

  1. The file declares no vertical datum, so --assume-vertical EPSG:5703 (NAVD88) is required for surfaces and contours — and is recorded and flagged as an assumption, never presented as fact.
  2. Web Mercator "metres" are not ground metres (×1.2 at that latitude), so working in it is refused; --t-srs takes it to NAD83(2011) / UTM 11N + NAVD88 through one explicit PROJ operation. That operation is only accurate to ~2 m (WGS 84 → NAD83), and the job warns that it is not survey grade.
  3. QA reports the density gaps; the DTM's 7 % voids are measured inside the coverage, not hidden by interpolation.

Then, for the same job, with your own polygon, line and check points in data/in:

docker compose run --rm survey volume  --job usgs --polygon /data/in/pile.geojson --base plane --surface dsm
docker compose run --rm survey profile --job usgs --line /data/in/xs1.geojson --step 0.5
docker compose run --rm survey accuracy --job usgs --checks /data/in/checks.csv --crs EPSG:6340+5703 \
    --checkpoint-rmse 0.02 --class-cm 10
docker compose run --rm survey report  --job usgs

Container paths: inputs are read from /data/in (mounted read-only from ./data/in), jobs are written to /data/jobs (./data/jobs). Point SURVEY_IN_HOST/SURVEY_JOBS_HOST in .env elsewhere if you like.

WA datums in one table

What you have Ingest with
MGA2020 zone 50 + AHD, declared in the file nothing extra — read and recorded
Drone output in GDA2020 lat/long + ellipsoidal heights (EPSG:7843) --t-srs EPSG:7850+5711 → AUSGeoid2020 applied, recorded
Drone output labelled MGA2020 z50 but heights are ellipsoidal --assume-vertical ellipsoidal --t-srs EPSG:7850+5711
Heights known to be AHD, file says nothing --assume-vertical EPSG:5711 (flagged in red)
Working in a local grid (Perth metro) --t-srs EPSG:8031+5711 (PCG2020 + AHD)
Legacy GDA94 / MGA94 + AHD --t-srs EPSG:7850+5711; pin the ICSM grid with --operation "GDA94 to GDA2020 (2)" when the data derive from state control

MGA2020 zones 49/50/51 are EPSG:7849/7850/7851. WA LSG2020 grids: ALB2020 8013, BIO2020 8014, BRO2020 8015, BCG2020 8016, CARN2020 8017, COL2020 8020, ESP2020 8021, EXM2020 8022, GCG2020 8023, GOLD2020 8024, JCG2020 8025, KALB2020 8026, KAR2020 8027, KUN2020 8028, LCG2020 8029, MRCG2020 8030, PCG2020 8031, PHG2020 8032 (from projinfo --list-crs --area "Western Australia"). Every transformation's candidates are listed in the report, so the choice can be checked.

Commands

Command Output
doctor tool versions, PDAL/GDAL drivers, datum grids (SHA256-checked), key transformations
info FILE header summary and CRS record (no job)
ingest FILES… --job ID points.copc.laz in the job CRS; inputs pinned; datum settled; grid-to-ground scale factor (LAS/LAZ/COPC, or E57 with --s-srs)
qa qa.json, density.tif, boundary.geojson, gaps.geojson
ground [--method smrf|csf] [--preset stockpile] ground.copc.laz (every point kept, reclassified; stockpile keeps steep piles as ground)
surfaces --res R [--fill-voids N] [--chm-method max|radius] dtm.tif, dsm.tif, chm.tif, void masks, hillshade.tif
contours --interval I [--index N] contours.gpkg, contours.geojson
volume --polygon P --base plane|lowest-perimeter|surface:T volume.json
cutfill --design D.tif cutfill.json
change --against EARLIER [--lod M | --lod-from-accuracy] change.json, dod.tif, dod_lod.tif
accuracy --checks C.csv --crs CRS accuracy.json, accuracy_residuals.csv
profile --line L --step S profile.csv, profile.svg
export --points las|e57 [--polygon P] [--thin S] points.las (LAS 1.4, uncompressed) or points.e57 for ReCap / Civil 3D, plus notes (CRS, class legend)
report report.html
run FILES… ingest → qa → ground → surfaces → contours → report
synth OUT.laz / synth DIR --kit a synthetic scene with known answers; with --kit, every demo input written into DIR (truth in kit-truth.json; --kit can't be combined with --epoch, --photogrammetry or --checks)
schema write the wire-contract JSON Schema UIs generate types from (--check in CI)
serve API + viewer on :8107

Run any of them with --help. All job commands (everything except doctor, info, synth, schema and serve) take --job ID (under SURVEY_JOBS_DIR) or --out DIR.

How the numbers are made

  • Grids are snapped: origin = ⌊min / res⌋ · res, so two epochs at the same resolution share cells exactly and a difference never needs resampling.
  • DTM: inverse-distance weighting of ground points within res·√2 of each cell centre; a cell with none is a void, reported as a percentage of the covered area. DSM: the same over first returns — unbiased on stockpiles. (--dsm-method max exists for roofs and canopy; a binned maximum reads ≈ slope × res/2 high on slopes, ~6 % on a 1:2 pile at 0.25 m, so it is never the default.)
  • Volumes: per-cell (upper − lower) × cell area, positive and negative summed separately, with boundary cells weighted by the fraction inside the polygon (gdal raster zonal-stats --pixels fractional). A plane base is a least-squares fit to surface heights sampled along the polygon boundary, with its RMS reported. More than 5 % voids in a polygon is refused unless you accept it.
  • Change: later − earlier; |Δ| below the level of detection is "no detectable change". The LoD is given (--lod), or 1.96·√(σ₁² + σ₂²) from both epochs' accuracy statements (--lod-from-accuracy); with neither it defaults to 0.10 m, and change.json's lod_rule says default 0.1 m.
  • Accuracy: ASPRS Positional Accuracy Standards, Edition 2, Version 2 (2024), checked against the standard's text: RMSE only (Edition 1's 95 % figure is gone); the class is free-valued ("#-cm", NVA RMSE_V ≤ #) and given with --class-cm; product RMSE_V = √(RMSE_V1² + RMSE_V2²) (§7.12.4), where --checkpoint-rmse is the surveyor's 1σ vertical RMSE; NVA pass/fail, VVA reported as found; min, max, mean, median, std and RMSE (§7.16); residuals beyond 3 × the target RMSE listed as blunders and a mean above 25 % of it flagged as bias (§7.2). Statements use §7.16.1's wording, including its reduced-checkpoint statement below 30 points. A check point is VVA only when the CSV's optional description column starts with veg or vva (case-insensitive); every other point, including one with an empty or missing description, is NVA and is pass/fail tested. Check points are given in a stated CRS and transformed through the same explicit operation machinery. Residuals are surface − check.
  • Drone-scale data: ingest streams; the COPC is built out of core by untwine; filters that need neighbours (outliers, SMRF/CSF, height above ground) run on buffered tiles whose cores partition the site exactly — the tiles must add up to the source's point count before they are merged.

Areas and volumes are in the projection's grid units. MGA's line scale runs from 0.9996 on the central meridian to about 1.0006 at a zone edge at WA latitudes, so grid areas and volumes can differ from ground by up to ~0.1 %; the WA LSG2020 grids (PCG2020 etc.) exist to keep that negligible for engineering work. Ingest records the combined scale factor at the site (point scale k from proj -V, with its range over the extent, × the height factor R/(R+h) from the ellipsoidal height, reached through the same explicit geoid operation), and volume/cut-fill reports give ground-scaled areas and volumes (grid ÷ combined²) alongside the grid values.

API and viewer

argus-survey serve (or make up) starts FastAPI on :8107. It is read-only by default: set SURVEY_ALLOW_PROCESS=1 to enable POST /jobs/{id}/{step}, which streams Server-Sent Events (each command, warning, and the result). Inputs are named relative to SURVEY_DATA_DIR; products are fetched by (job id, kind) with HTTP range support, so the viewer streams the COPC straight from the API.

Route Returns
GET /health {status, service, version, read_only, tools}; read_only is not SURVEY_ALLOW_PROCESS
GET /doctor the toolchain report (as argus-survey doctor)
GET /inputs point clouds, vectors, rasters and CSVs under SURVEY_DATA_DIR, by name
GET /jobs {jobs: [JobDigest]}
GET /jobs/{id} the sidecar (job.json)
GET /jobs/{id}/{kind} a product by kind (copc, dtm, report, …), with HTTP Range
GET /jobs/{id}/web/{kind} map view: a raster → {url, lon/lat corners}; a vector → WGS 84 GeoJSON
GET /jobs/{id}/web/{kind}/image the map view's EPSG:3857 PNG
POST /jobs/{id}/{step} run a step (403 unless processing is allowed; 409 while the job is busy)
GET /viewer (/ redirects) the viewer

{step} is one of ingest, qa, ground, surfaces, contours, volume, cutfill, change, accuracy, profile, report or run, each with a JSON body whose fields mirror the CLI options (IngestBody, QaBody, … RunBody; report takes {}). Two differences from the CLI: volume and cutfill accept a drawn polygon_geojson (a WGS 84 Polygon/MultiPolygon geometry) instead of a polygon file, and profile takes a drawn line (a WGS 84 LineString) or a line_file; drawn geometry is saved in the job dir under a content hash so the recorded argv keeps pointing at it. ingest and run create the job; the other steps need it to exist. The response is an SSE stream of start, step, command, warning, then done (the step's result, product kinds, warnings) or error.

The request bodies, HealthInfo, the SSE payloads and job.json are all in one JSON Schema, schema/survey-wire.schema.json (regenerate with argus-survey schema); generate client types from it rather than from this table.

The viewer (/viewer) shows the hillshade, surfaces, density, change, contours and coverage on a map, the classified cloud in 3D (coloured by elevation, class or intensity), the job's CRS and warnings, and — when processing is allowed — draws a profile line and runs the profile step. It loads MapLibre, three.js and copc.js (exact versions) from jsDelivr; all data comes from the API.

Without Docker

micromamba create -p ./.geo -f env/geo-linux-64.lock   # the same PDAL/GDAL/PROJ the image uses
export PATH="$PWD/.geo/bin:$PATH" PROJ_NETWORK=OFF
projsync --target-dir ./.geo/share/proj --file au_ga_AUSGeoid2020_20180201.tif   # + the grids in env/proj-grids.sha256
make install && uv run argus-survey doctor

Develop

make install      # uv venv with all extras
make lint         # ruff
make test         # pure-Python tests here; tool tests skip without PDAL
make test-docker  # the full suite in the image, tools required (what CI runs)

See CLAUDE.md for the code map and conventions, docs/runbook.md for operations, docs/data-sources.md for public data and docs/atlas-showcase.md for turning a survey DTM into an argus-atlas map. Follow-ups are GitHub issues — Autodesk interoperability (LandXML, DXF, ReCap) first.

Part of the Argus suite. MIT licence.

Metadata

Release files for argus-survey 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for argus-survey 0.1.0
File Size Uploaded Provenance Actions
argus_survey-0.1.0.tar.gz 464.4 kB With provenance Details for argus_survey-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for argus-survey 0.1.0
File Interpreter ABI Platform Actions
argus_survey-0.1.0-py3-none-any.whl Python 3 none any Details for argus_survey-0.1.0-py3-none-any.whl

Total release size: 597.9 kB

Release files / argus_survey-0.1.0.tar.gz

Download URL argus_survey-0.1.0.tar.gz
Size 464.4 kB
Tags Source
SHA-256 checksum
How to use checksums
834d7f85bfabeab3e9f37b5141ff91cd91c75720ffb7f0b197df3e76b07379c8
BLAKE2b-256 checksum
How to use checksums
6f541765f8052553f5da320cfd2ab9d6b1dd90e93671857bed460abc8cdb78b6
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 Nov 13, 2026.

Transparency log

Release files / argus_survey-0.1.0-py3-none-any.whl

Download URL argus_survey-0.1.0-py3-none-any.whl
Size 133.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
df5a1252fed025cd9d7d3b931e8a78a8f168d7e1139dfee7c04da8a5ef9b0be6
BLAKE2b-256 checksum
How to use checksums
76abc95186f18a9b2cbcdb960f8c9adc00acbd5f6038c2fa6d12d75083a17844
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 Nov 13, 2026.

Transparency log

Security for argus-survey 0.1.0

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.

Select from 2 files

Attestation for argus_survey-0.1.0.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 Nov 13, 2026.

Transparency log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page