Automated LiDAR Quality Control Inspector (las-inspector) 🔍
A lightweight Python CLI tool designed to ingest standard ASPRS LAS/LAZ point cloud files, run automated quality control checks, and generate interactive, client-ready HTML reports complete with spatial density heatmaps and elevation profiles.
📄 View a live example report — or open
example-report.htmllocally.
ℹ️ I started this as an experiment to understand low-level ASPRS binary formats and memory-efficient NumPy arrays. It ended up robust and useful enough to become a general-purpose QC CLI — a portfolio piece showing how AI multiplies a solo engineer's output, contrasting a basic prototype (
main.py) with a custom binary-parsing tool (las_inspector.py).
📊 Dataset Credit & Sourcing
The sample dataset (las2018.laz) used for development was sourced from the USGS National Map Lidar Explorer.
- Format:
.laz(Compressed LiDAR) - Scale Precision: Millimeter (0.001m precision)
To run the pipeline with your own data:
- Download a tile in
.lasor.lazformat from the USGS portal. - Save it to the working directory.
- Pass the file path as an argument to
las_inspector.py.
🤔 Why Use This?
While professional civil engineering projects typically rely on feature-rich CAD suites (like Autodesk Civil 3D) or GIS software (like ArcGIS Pro) to inspect LiDAR files, this lightweight Python CLI workflow offers key practical advantages for specific use cases:
- Zero Licensing Cost: It uses free, open-source libraries (
numpy,matplotlib,lazrs). Anyone can inspect point clouds without purchasing expensive proprietary CAD/GIS software licenses. - Low Overhead: Heavy CAD programs can take minutes to load massive datasets and often freeze on standard laptops. This script starts instantly, parses headers with custom binary decoding, and runs QC checks with memory-efficient NumPy arrays — no GUI, no license server, no project setup.
- Easy Automation & CI/CD Pipelines: Because it is a simple terminal command-line tool, it can easily be scheduled (e.g., cron jobs), integrated into automated file pipelines, or run as an automated check (pre-commit hooks or GitHub Actions) on newly received survey tiles.
- Transparency & Customizability: All QC thresholds (like density limits and classification ratios) are explicitly written in code. This provides a clear, reproducible calculation logic rather than a "black-box" proprietary algorithm.
📋 Sample Scenario: Quality Control (QC) Delivery Auditing
The Context
A civil engineering firm is working on a municipal subdivision project. Every week, a drone surveying subcontractor delivers raw LiDAR point cloud tiles (.laz format) covering different sections of the project site.
The Problem
- The firm must verify that the point cloud files meet the contractually required specifications (e.g., density of at least 2.0 pts/m², sufficient ground classification for digital terrain modeling, and no corrupt headers).
- Manually loading each tile into heavy GIS software to inspect metadata and run statistics takes hours of manual click-work every week.
- If a contractor submits a tile with incorrect classification or low density, the error might not be discovered until weeks later during detailed modeling, causing costly project delays.
The Solution with this Script
- Data Collection: The subcontractor uploads the weekly survey tiles to a shared directory.
- Execution: An automated script triggers
python las_inspector.py [filename].laz --densityfor each incoming file. - Result: The script automatically parses the binary structure, validates the file against density and classification heuristics, and outputs a clean text summary along with a standalone
qc_report.htmlfeaturing embedded density and elevation plots. - Value: The project engineer receives an automated notification with the QC verdict (PASSED/FAILED) and a single-file interactive report. Subcontractor deliveries can be audited and accepted or rejected instantly, ensuring only high-quality data enters the design pipeline.
🛠️ How it Works
- Low-Level Header Decoding: Reads the public header block of the LAS/LAZ file to extract scale factors, offsets, coordinate bounds, point records count, and coordinate reference system (CRS) WKT data.
- Binary Point Record Parsing: Bypasses heavy wrapper libraries by decoding point records directly using Python's
structmodule and NumPy buffers to unpack X, Y, Z coordinates, intensities, return numbers, and classification bytes. - Dynamic LAZ Decompression: Transparently detects compressed
.lazfiles and reads the LASzip VLR (Variable Length Record), using high-speed Rust-basedlazrsbindings to decompress point data on the fly. - Heuristic QC Warning Engine: Scans point flags and statistics to trigger warnings/errors for low point density, lack of ground-classified points, withheld/synthetic flags, or potential flight-line overlap zones.
- Asset-Embedded Report Compilation: Renders an interactive, responsive HTML5 report styled with a dark theme. It base64-encodes the generated density and elevation plots directly into the HTML file, creating a single, fully portable deliverable.
🧠 AI Co-Pilot Case Study: Baseline vs. Enhanced
This repository serves as a showcase of using AI as a force multiplier for software engineering, contrasting a basic prototyping script with a custom binary-parsing CLI tool.
| Feature | Baseline Script (main.py) |
Enhanced CLI Tool (las_inspector.py) |
|---|---|---|
| Dependencies | Requires laspy high-level wrapper |
Native binary decoding (uses Python struct & numpy buffers) |
| Parsing Logic | Wrapped abstraction | Low-level binary reading from file offsets (IEEE 754 doubles, packed bytes) |
| Visualizations | None | 2D Density Heatmap & Elevation profiles |
| Outputs | Console log + basic HTML snippet | Standalone, semantic HTML5 report styled with dark mode and modern typography |
| Warnings | Hardcoded checks | Configurable warning heuristics (low density, missing ground data) |
📚 Technical Learnings & Code Evolution
Developing the enhanced CLI tool involved deep diving into the ASPRS LAS specification details:
-
Bitwise Unpacking: LiDAR point formats compress spatial attributes using bitfields. The parser extracts packed properties using bitwise masks.
- For LAS 1.2 (Formats 0-5), the classification and flags share a single byte at offset 15:
flag_synthetic = bool(byte & 0x20) flag_keypoint = bool(byte & 0x40) flag_withheld = bool(byte & 0x80) classification = byte & 0x1F # 5-bit code
- For LAS 1.4 (Formats 6-10), attributes are expanded. Return Number and Number of Returns are packed together in byte 14, and Classification Flags are separated from the Classification byte:
# Byte 14: Return number (bits 0-3) & Number of returns (bits 4-7) return_number = byte_14 & 0x0F number_of_returns = (byte_14 >> 4) & 0x0F
- For LAS 1.2 (Formats 0-5), the classification and flags share a single byte at offset 15:
-
Byte Alignment Debugging: During development, we resolved a critical coordinate misalignment where LAS 1.4 files (PDRF 6-10) were returning false-positive classification warnings. By analyzing byte sizes, we corrected the offsets (e.g. referencing byte 16 for the expanded 8-bit Classification code, and reading the 8-byte GPS Time double at offset 22 instead of 20).
-
Memory Optimization Thoughts: While the current version loads the binary file into a byte buffer for speed, dealing with multi-gigabyte point clouds requires mapping the file via
mmapor streaming point packets in chunks.
⏱️ Benchmarks
Honest numbers first: laspy (native C++/Rust parsing) beats this tool's pure-Python parser at raw point parsing. The win here is workflow overhead — no GUI, no license, instant startup, scriptable — not CPU throughput. Reproduce with python scripts/benchmark.py <file> --repeat 3:
Workload (las2018.laz, 2.2M pts, LAS 1.4) |
Mean | Best of 3 |
|---|---|---|
las_inspector.read_las + compute_stats |
4.25 s | 4.13 s |
laspy.read + NumPy stats |
0.24 s | 0.23 s |
Takeaway: if you need maximum parse throughput, use laspy (or PDAL). If you need a one-command QC verdict plus a portable client report with zero setup, this tool earns its keep. A header-only fast path (skip point records when only bounds/density estimates are needed) is planned future work.
⚙️ Configuration & Parameters
To customize the CLI run behavior and thresholds, adjust the command-line options or modify the warning heuristics within las_inspector.py:
CLI Flags & Arguments
input(positional): Path to the raw.lasor.lazfile.--output,-o: Name of the output HTML report (defaults toqc_report.html).--density: Generates a dual-plot PNG showing the 2D Point Density heatmap and terrain-colored elevation scatter.--max-points: Limits the number of points processed (useful for rapid testing on massive files).- Overlap detection: runs automatically on every file — possible flight-line overlap zones are reported as info-level warnings.
Modifying Heuristic Thresholds
Open las_inspector.py and modify the following conditional checks in compute_stats() to fit your project standards:
- Minimum Ground Classification:
if 2 not in classifications or classifications[2] < len(pts) * 0.01: # Warns if Ground points (Class 2) are less than 1% of total points
- Point Density Warning:
if hdr.point_density < 10: # Triggers warning if density drops below 10 points per square unit
- Withheld Point Tolerance:
withheld_pct = (withheld / len(pts)) * 100 if withheld_pct > 5: # Triggers warning if more than 5% of points are flagged as withheld
🚀 Quick Start
Prerequisites
- Python 3.12 or newer
- uv (recommended) or
pip/pipx
Install & Run
- Clone this repository.
- Try it immediately on the bundled sample tile (
data/sample.laz, ~100k points, 1.2 MB):
# Using uv:
uv sync
source .venv/bin/activate
las-inspector data/sample.laz --density --output qc_report.html
# Or install as a standalone tool with pipx (no clone needed):
pipx install git+https://github.com/gokserpirik/las-inspector.git
las-inspector your-tile.laz --density # point it at your own .las/.laz file
# Or using standard pip:
pip install -r requirements.txt
python las_inspector.py data/sample.laz --density --output qc_report.html
To build the sample yourself from a large tile: python scripts/make_sample.py las2018.laz --output data/sample.laz --stride 22.
Output Files & Visualizations
The script generates three key deliverables (names derive from your input/output paths):
qc_report.html: A standalone, semantic HTML5 report with responsive dark-mode styling, Google Fonts typography, interactive cards, and a detailed summary of point count, CRS, and returns. If--densityis enabled, the visualization is embedded directly into this file.<input>_qc_summary.txt(e.g.sample_qc_summary.txt): A clean, lightweight text file summary saved to the working directory for quick inspection or piping to other tools.<output>_density.png(only with--density): A dual-panel figure mapping point density per bin next to a terrain-colored elevation scatter plot.
🧪 Running Tests
uv sync
.venv/bin/python -m pytest tests/ -v
40 tests covering header math, bitfield unpacking, QC stats, binary LAS parsing, reports, and the CLI.
📄 License
MIT — see LICENSE.
Metadata
Release files for las-inspector 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| las_inspector-0.1.0.tar.gz | 22.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| las_inspector-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.6 kB
Release files / las_inspector-0.1.0.tar.gz
| Download URL | las_inspector-0.1.0.tar.gz |
|---|---|
| Size | 22.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a37c462cef7324041ca134534781be0d0823c99a702bceb500864645a313c9a8
|
|
BLAKE2b-256 checksum How to use checksums |
32874d0c39f8ec50b341213109fe3b8dd915e6e519b2c2a8acecb4b3c03dcf25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / las_inspector-0.1.0-py3-none-any.whl
| Download URL | las_inspector-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fda0e7ff1f8b314097884e0b6e21639c766c28c8d56c84153d4133bf649a10d2
|
|
BLAKE2b-256 checksum How to use checksums |
971902269a66946de122baa492ffa5c2f8b9db76a312e9c289b76e3b3fb5dd25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|