Skip to content

Repository files navigation

WebPointCloud

A web-based 3D point cloud viewer and analysis tool.
Load, visualize, and analyze LAS/LAZ point cloud files directly in your browser — no plugins, no desktop app required.

WebPointCloud Screenshot

Demo

Drag & Drop Loading

Drag & Drop

Post-Processing Filters (EDL / SSAO)

Post-Processing Filters

View Controls & Color Modes

View Controls

ICP Registration

ICP Registration

3D Gaussian Splatting Viewer

3DGS Viewer


Features

Viewer

  • GPU-Accelerated 3D Rendering — Powered by Three.js with custom WebGL shaders
  • COPC Octree Streaming — Large maps stream view-dependent LOD from a COPC octree instead of loading whole (see COPC Streaming)
  • Color Modes — Intensity, Height (turbo colormap), RGB, Classification (ASPRS palette + legend)
  • Post-Processing — Eye-Dome Lighting (EDL), Screen-Space Ambient Occlusion (SSAO)
  • Measurement Tools — Distance measurement, polygon selection, point info
  • Map Comparison — Overlay two point clouds with transform controls (offset + rotation)
  • ICP Registration — Iterative Closest Point alignment with initial pose support
  • 3D Gaussian Splatting — View 3DGS .ply and .splat files with real-time depth-sorted splatting (auto-detected)
  • Large Coordinate Support — UTM / survey coordinates handled with automatic float64 centering to prevent precision loss
  • Clipping Planes — X/Y/Z axis clipping
  • Camera Bookmarks — Save and restore camera positions
  • Live Streaming — View a ROS 2 PointCloud2 topic in real time, with the robot pose and trajectory (see Live Streaming)

Analysis

  • Statistics — Point count, bounding box, density, height distribution histogram
  • SOR Filter — Statistical Outlier Removal (k-nearest neighbors)
  • Cross Section — Extract slices along any axis
  • Volume Estimation — 2.5D grid-based volume computation
  • C2C Distance — Cloud-to-Cloud distance between two point clouds

General

  • Drag & Drop — Drop point cloud files directly into the viewer
  • File Management — Browse, rename, delete saved point clouds
  • Screenshot — Export the current view as PNG
  • Dark / Light Theme
  • Responsive UI — Works on desktop and tablets

Quick Start

Requires Python 3.10 – 3.13.

git clone https://github.com/warhammer50K/WebPointCloud.git
cd WebPointCloud

python3 app.py

The first run creates .venv, installs requirements.txt into it, and re-execs there; later runs skip straight to the server. ./run.sh does the same thing from the shell side, and activating the venv yourself still works:

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python app.py

The virtual environment is not just a nicety — Ubuntu 24.04, Debian 12, and recent Fedora ship a PEP 668 marked interpreter, so installing into the system Python fails outright with error: externally-managed-environment. That is why app.py builds one instead of asking pip to install anywhere. Set WPC_NO_BOOTSTRAP=1 to turn the bootstrap off and manage the environment entirely yourself.

Open http://localhost:6001 in your browser and drag & drop a point cloud file. A sample is included at sample/building_scan.las (100k points) — drag it in to try the viewer out.

For clouds you keep around, copy them into the maps directory and pick them from Load Map instead. New installs use ~/webpointcloud/maps; if you already have a ~/maps, that keeps being used. The startup banner prints the path it actually chose:

  Maps dir : /home/you/webpointcloud/maps

Both layouts show up in the list, so either works:

~/webpointcloud/maps/
├── scan.las              ← a file on its own
└── warehouse_run/        ← or a folder holding one or more clouds
    ├── map.las
    └── map.copc.laz      (the streaming conversion, built on first load)

Already have a collection somewhere else? Point the app at it — nothing is copied or moved:

WPC_MAPS_DIR=~/maps python3 app.py

Supported Formats

Format Extension Notes
LAS .las ASPRS LAS 1.2 - 1.4
LAZ .laz Compressed LAS (via lazrs)
COPC .copc.laz Cloud Optimized Point Cloud — streamed with octree LOD
PLY .ply ASCII and binary (little/big endian)
XYZ .xyz .txt .csv Whitespace or comma delimited
PCD .pcd Point Cloud Library format (ASCII and binary)
PTS .pts Leica / common scanner ASCII format
3DGS PLY .ply 3D Gaussian Splatting (auto-detected)
Splat .splat Compact 3DGS binary format

Large Coordinate / UTM Handling

Point clouds from surveying or GIS workflows often use UTM or other projected coordinate systems with large absolute values (e.g., X=712345, Y=7034567). Storing these directly in float32 causes visible precision loss (jitter, staircase artifacts).

WebPointCloud handles this automatically:

  1. Read in float64 — raw coordinates are loaded at full double precision
  2. Compute center offset — bounding-box midpoint is extracted as a float64 offset (ox, oy, oz)
  3. Center & convert — coordinates are centered (x - ox) in float64, then cast to float32 for GPU rendering
  4. Reconstruct on display — point info, legend, and analysis results add the offset back to show original coordinates

When two clouds are loaded for comparison, the offset difference is applied automatically so they align correctly in the scene even if they come from different UTM zones or reference frames.

No configuration is required — just load the file and coordinates are preserved to sub-millimeter precision regardless of their absolute magnitude.

COPC Streaming (Large Point Clouds)

Small files are parsed and rendered whole. Large LAS/LAZ files (≥ 2M points by default) are automatically converted to COPC — a LAZ file with an internal octree — in the background on first load. Progress is shown in the loading overlay, and the result is written next to the source (map.copc.laz), so the conversion cost is paid only once; every later load streams the COPC directly.

COPC maps are never loaded whole. The viewer streams octree nodes on demand from the current camera view — coarse levels for the whole scene, finer levels only where the camera is looking — under a resident point budget (25M points by default). Chunks that fall out of view or budget are evicted and re-fetched later. This is why a multi-GB, hundred-million-point map opens in seconds and stays smooth while navigating, where parsing the raw LAS up front would take minutes and exhaust GPU memory.

Conversion backends

Conversion tries three backends in order, so no extra setup is needed for the common case:

  1. untwine — used automatically if untwine is on PATH. Out-of-core: it spills to a temp directory next to the output (needs about the source size free on disk) and keeps RAM bounded, so it is the only backend that handles clouds larger than RAM. Build it from hobuinc/untwine against an installed PDAL (cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build) and put build/bin/untwine on PATH.
  2. PDAL (pdal translate -w writers.copc) — used if PDAL >= 2.4 is on PATH. Holds the whole cloud in memory.
  3. copclib — the bundled multiprocess octree builder in tools/las_to_copc.py. Installed by requirements.txt, so this path always works out of the box, but it also holds the whole cloud in memory (~3x the file size) and refuses files that would not fit in RAM.

If PDAL is killed by the OOM killer, conversion stops rather than falling through to copclib. If no backend can convert the file, the file falls back to the legacy whole-cloud load path.

.copc.laz files can also be produced offline with tools/las_to_copc.py <input.las> (multiprocess writer; see the WPC_COPC_* variables below).

Live Streaming (ROS 2)

Besides files, the viewer can render a live point cloud pushed to it while it is running — a lidar scan, a SLAM map being built, anything published as sensor_msgs/PointCloud2.

# on the robot (or anywhere with the topic visible)
source /opt/ros/humble/setup.bash
python3 tools/ros2_bridge.py --topic /velodyne_points

Then open the viewer, expand Live Stream in the sidebar, and press Connect. The panel lists the active streams with their rate and point count.

How it fits together

ros2_bridge.py ──HTTP POST──▶ /api/live/publish ──polled──▶ browser
 (needs rclpy)                  (Flask, no ROS)              (Three.js)

The bridge is the only piece that needs ROS. It decodes PointCloud2, thins the cloud to a rendering budget, and POSTs plain float32 frames; the server holds the newest frame per stream and hands it to whichever browsers ask. So the machine running app.py never needs a ROS install, and the bridge can run on the robot while the viewer runs on a laptop — point --url at it.

Bridge options

Flag Default What it is for
--topic (required) The PointCloud2 topic to subscribe to
--url http://localhost:6001 Where app.py is listening
--layer cur cur/raw/map replace the cloud each frame; kfrm accumulates frames into one growing map
--rate 10 Max frames published per second (0 = every frame)
--max-points 400000 Thin each frame to this many points
--voxel 0 Voxel size in metres to thin by first — much better than plain thinning for dense map clouds
--qos best-effort Must match the publisher. See below
--transient-local off For latched topics that publish a map once
--odom / --pose — nav_msgs/Odometry or geometry_msgs/PoseStamped topic to drive the pose marker and trajectory
--intensity-max 0 (auto) Intensity value that maps to full brightness
--compress 1 zlib level for the wire payload (0 = off)
--token — Required when the server sets WPC_LIVE_TOKEN

If nothing shows up, check QoS first. A RELIABLE subscriber receives nothing at all from a BEST_EFFORT publisher — no error, no warning — and most sensor drivers publish best-effort. That is why the default here is best-effort; a latched map topic may need --qos reliable --transient-local instead. ros2 topic info -v <topic> shows what the publisher offers.

Publishing from something other than ROS

The relay knows nothing about ROS, so anything that can POST can feed it:

POST /api/live/publish?stream=<name>&layer=cur&points=<n>&compressed=0
body: n × 7 little-endian float32 — x, y, z, intensity, r, g, b

Intensity and RGB are 0..1 (the shaders read them as normalized values), and coordinates are float32 metres in whatever frame you choose. With compressed=1 the body is a 4-byte little-endian original length followed by a zlib stream of the same data. Poses go to /api/live/pose as {"matrix": [16 column-major floats]}.

Notes and limits

  • Frames are not queued for cur/raw/map: only the newest is held, so a browser that cannot keep up skips frames rather than falling behind. The kfrm layer does queue (bounded by WPC_LIVE_QUEUE_MB), because a dropped frame there is a permanent hole in the accumulated map.
  • Live layers coexist with a loaded file, but the analysis tools operate on the file cloud, not on streamed points.
  • The transport is HTTP polling, not WebSocket — sized for a LAN, where a poll costs a couple of milliseconds.
  • A publisher that goes quiet for WPC_LIVE_STALE_SEC (15 s) is dropped, so a stale cloud does not sit on screen pretending to be live.

Classification Coloring

LAS/LAZ point classification (ground, vegetation, building, water, …) is carried through the whole pipeline — direct loads, COPC conversion, and streaming — and can be used as a color mode:

  1. Load a classified LAS/LAZ/COPC file
  2. Pick Color > Classification in the sidebar

Points are colored with the standard ASPRS palette (2 Ground = brown, 3-5 Vegetation = greens, 6 Building = red, 9 Water = blue, …), the legend shows a swatch per class, and the Point Info tool reports the class number (C:) of the hovered point. Classification survives polygon delete / undo / downsampling.

Notes:

  • Files whose points are all class 0 (typical for SLAM-generated maps) render uniformly gray in this mode — the data simply carries no classification.
  • map.copc.laz files converted before classification support (2026-07) were written without the classification field. Delete the .copc.laz and reload the map to re-convert it with classes preserved.

Configuration

Environment Variable Default Description
WEB_PORT 6001 HTTP server port
WPC_DATA_DIR ~/webpointcloud Everything the app owns: maps, logs, caches, session secret
WPC_MAPS_DIR ~/maps if it exists, else $WPC_DATA_DIR/maps Where point clouds are read from — set it to use a collection you already have
FLASK_DEBUG 0 Enable Flask debug mode (forces the bind address to 127.0.0.1 — see Security)
WPC_ALLOW_REMOTE_DEBUG 0 Set to 1 to let debug mode bind 0.0.0.0. Exposes the Werkzeug debugger — remote code execution for anyone who can reach the port.
FLASK_SECRET_KEY auto-generated Flask session secret key
WPC_PUBLIC 0 Set to 1 to disable the IP whitelist (see Security)
WPC_MAX_UPLOAD_MB 5120 Maximum upload size in MB
WPC_COPC_MIN_POINTS 2000000 Point count above which LAS/LAZ auto-converts to COPC streaming
WPC_COPC_BUDGET 25000000 Resident point budget for COPC streaming (~28 B/point on GPU)
WPC_COPC_WRITE_PROCS 8 COPC converter: parallel write processes
WPC_COPC_MIN_NODE 2000 COPC converter: collapse octree leaves below this point count
WPC_COPC_THREADS CPU count COPC converter: threads for untwine / PDAL writers.copc
WPC_COPC_PARALLEL_MIN 2000000 COPC converter: point count below which the build stays single-process
WPC_COPC_SPLIT_DEPTH 3 COPC converter: octree depth at which subtrees fan out to worker processes
WPC_LIVE_TOKEN (unset) Shared secret for live publishing; required when WPC_PUBLIC=1
WPC_LIVE_MAX_FRAME_MB 32 Maximum size of one live frame
WPC_LIVE_QUEUE_MB 64 Backlog held for an accumulating (kfrm) live stream
WPC_LIVE_MAX_STREAMS 8 Maximum concurrent live publishers
WPC_LIVE_STALE_SEC 15 Silence after which a live publisher is considered gone

Upgrading from an earlier version

Existing installs and forks keep working unchanged — nothing is moved, renamed, or deleted. The defaults used to spread four directories across $HOME (~/maps, ~/webpointcloud, ~/.webpointcloud, ~/.mapper_secret_key), and a fresh install now puts everything under WPC_DATA_DIR instead. The old locations still win where they already exist:

  • Maps. An existing ~/maps stays the default, so your clouds are where they were. Only a machine that has never had one gets $WPC_DATA_DIR/maps. Either way the startup banner prints the path in use.
  • Session secret. An existing ~/.mapper_secret_key is read where it is and left alone; sessions are unaffected. New installs write $WPC_DATA_DIR/secret_key instead. To consolidate, move the file yourself and it will be picked up from the new path.

For forks, the only API change is additive: /api/maps entries gained an is_file flag and now include loose files as single-file entries. name, path and las_files keep their meaning, and path + las_files[0] still resolves to the cloud for both layouts.

Security

WebPointCloud is built as a LAN / workstation tool, not as an internet-facing service. There is no user authentication — anyone allowed through the checks below can browse, upload, rename, and delete point clouds under the maps directory.

The server binds 0.0.0.0:6001 so other machines on your network can reach it, and two before_request checks guard it:

  • IP whitelist — requests are accepted only from loopback and private ranges: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 (CGNAT / Tailscale), ::1, fe80::/10. Anything else gets 403.
  • Rate limit — 60 requests per 60 seconds per IP, then 429. /api/copc/* and /api/live/* are exempt, since octree streaming and live polling both run far above that rate; the IP whitelist still applies to them. /static/ skips both checks.

Two settings deliberately weaken this, and both are off by default:

Setting What it does Use only when
WPC_PUBLIC=1 Removes the IP whitelist entirely — every source IP is accepted The app sits behind a reverse proxy that terminates TLS and enforces authentication
WPC_ALLOW_REMOTE_DEBUG=1 Lets FLASK_DEBUG=1 bind 0.0.0.0 instead of being forced to loopback Never on a shared network — the Werkzeug interactive debugger is remote code execution for anyone who can reach the port

Also worth knowing:

  • python app.py runs Flask's development server. For anything beyond a trusted LAN, put it behind a production WSGI server (gunicorn, uWSGI) and a reverse proxy.
  • FLASK_SECRET_KEY is read from the environment; if unset, a random key is generated and persisted to $WPC_DATA_DIR/secret_key with mode 0600. Set it explicitly for any multi-instance or long-lived deployment.
  • Upload size is capped at WPC_MAX_UPLOAD_MB (default 5 GB), and map filenames are constrained to paths inside the maps directory.
  • Live publishing writes into what every viewer sees. Behind the IP whitelist that is fine, but WPC_PUBLIC=1 removes that gate — so with it set, /api/live/publish refuses everything unless WPC_LIVE_TOKEN is configured and sent as X-Live-Token. Reading a live stream never needs a token.

Development

pip install -r requirements-dev.txt   # requirements.txt + pytest
pytest

The suite covers the maps listing / rename / delete endpoints and path-traversal handling in api.py, rotation math, the rate limiter, 3DGS parsing / binary packing in pointcloud_io.py, the live relay's publish/poll contract in live.py, and PointCloud2 decoding in tools/ros2_bridge.py. No server, sample data, or ROS install is required — the tests run standalone from the repository root.

tests/make_utm_test_data.py is a helper, not a test: run it to generate large-coordinate UTM sample files under sample/ for manually checking float64 centering in the viewer.

Architecture

Browser ──── HTTP ────── Flask (Python) ◀──POST── ros2_bridge.py
  │                        │                       (ROS 2 side, optional)
  ├─ Three.js 3D viewer    ├─ REST API (/api/*)
  ├─ WebGL shaders         ├─ LAS read/write (laspy)
  ├─ Analysis UI           ├─ Analysis (numpy, scipy)
  ├─ Live stream client    ├─ Live relay (/api/live/*)
  └─ Web Workers           └─ File management

Third-Party Libraries

Library License Usage
Three.js MIT 3D rendering
pako MIT + Zlib Zlib decompression
Flask BSD-3 Web framework
laspy BSD-2 LAS file I/O
NumPy BSD-3 Numerical computing
SciPy BSD-3 Spatial algorithms (KDTree, SOR)
copc-lib BSD-3 COPC octree writing (copclib)
untwine (optional) GPL-3 Out-of-core COPC conversion backend when installed (run as a separate CLI)
PDAL (optional) BSD-3 COPC conversion backend when installed

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -m 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License.

About

Web-based 3D point cloud viewer & analysis tool. No install — just open your browser. Supports LAS, LAZ, PLY, PCD, XYZ, PTS.

Topics

Resources

Stars

41 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages