← CloudAnalyzer

User guide

The web viewer opens and analyses point clouds in your browser. Files are read locally and never uploaded. Each section links to a demo that loads sample data and runs the analysis.

Opening data

MouseTouchAction
Left dragOne fingerOrbit
Right dragTwo fingersPan
WheelPinchZoom
ClickTapPick a point
Double-clickDouble tapOrbit around that point

Use Fit (F) and the Top, Front, Side and Iso views to reset the camera. Point size, Round (discs instead of squares), EDL (depth shading) and Point budget (points drawn at once) are in the toolbar. Trajectories and pose graph edges are drawn 2 pixels wide. On a phone, Panels opens the side panels as a sheet.

Next to the point size, Adaptive draws each point as wide as the local point spacing (in scene units), so surfaces close up when you come near; EDL is off in that mode. The colour swatch sets the background. Views remembers camera positions under a name, to come back to them; they are part of share links and sessions.

Coloring and classes

Each cloud in the list has a color mode: RGB, intensity, classification, solid color, distances, normals or hillshade. When a cloud has ASPRS classes, the Classes panel shows or hides each class in every cloud.

Scalar fields

Every per-point value is a scalar field: intensity, Z, computed distances and M3C2 results, splat opacity and size, raster heights, and fields you compute. Pick one in a cloud's color menu, or in the Scalar fields panel, which shows its histogram and statistics (percentiles included) and colors the cloud with the colorbar's ramp and range. Keep range / Remove range cut the points by value into a new cloud.

The Calculator makes a new field from an expression such as abs([C2C distance]) * 1000 or intensity > 200 and Z < 5: fields by name (in brackets when the name has spaces), + - * / % ^, comparisons, and or not, and abs sqrt exp log log10 sin cos tan floor ceil round min max atan2 clamp if. New fields are saved with the cloud in PLY and LAS.

Distances (C2C, C2M)

In Distance, pick the compared cloud and the reference, then press Compute:

The result is shown with a color ramp, an adjustable range, a colorbar and summary statistics (mean, RMS, percentiles).

Try: two LiDAR scans

Change detection (M3C2)

Choose Method: M3C2 in the Distance panel. At each core point, a normal is fitted to the reference within the normal radius. Both clouds are then averaged in a cylinder of the projection radius along that normal, up to the max depth. The result is a new cloud with:

Core points where either cylinder has too few points are grey.

Try: a landslide

Map quality against ground truth

Choose Method: Map quality in the Distance panel, with the map to judge as Compared and a ground-truth map as Reference (for a SLAM result, the same scans built with ground-truth poses). The metrics are:

The map is coloured by its distance to the truth, up to the inlier distance. On KITTI (scans thinned to 0.2 m, the truth built from the ground-truth poses), loop closure and IMU gravity raise F1 at 0.3 m from 0.45 to 0.70 on sequence 07 (completeness 44 → 68 %, AWD 0.33 → 0.25 m) and from 0.16 to 0.42 on 09. KITTI's ground-truth poses are themselves locally noisy, so no map reaches 100 %.

Cut / fill volume

In Volume, pick a before and an after surface. Each can be a cloud, a mesh or a constant height. Both are rasterized on a grid of the chosen cell size, using the mean, lowest or highest point per cell. Fill empty cells interpolates holes.

The result gives the added (fill), removed (cut) and net volume and the areas, with the height difference of each cell shown as a cloud.

Try: a stockpile

Manual alignment

Pick pairs (P): click a point on the moved cloud, then the same spot on the reference, and repeat for at least three pairs spread over the scene (hide the other cloud if it is in the way; Backspace takes the last pick back). Align moves the cloud so the pairs meet in the least-squares sense and lists the residual of each pair. It is a good start for ICP when the clouds are far apart.

Under Move by hand, Move and Rotate show a gizmo on the moved cloud (rotating about its centre); drag it, then Apply. Apply matrix applies a typed row-major 4×4 rigid transform, for example one from CloudCompare. Every alignment can be undone.

Alignment (ICP)

In Registration (ICP), Align moves one cloud onto a reference, point-to-plane or point-to-point. It can pre-align the centroids and trim non-overlapping points. The panel shows the RMS before and after and the 4×4 transform, and Undo alignment reverts it.

Pose graph (SLAM)

To see it at work, open the SLAM loop closure demo: a drive made in the browser, once and a bit round a city block over a hill, with drifting odometry; it finds the loops, ties the keyframes to the IMU's gravity and colors the map by how far each point moved. The real drive demo does the same from a ROS 2 bag of a real drive (part of the NCLT dataset, 10 MB): odometry first, as the bag holds scans and IMU but no poses. The two seasons demo joins the same block driven in June onto it and compares the maps where the drives meet: the changes between April and June.

Fix a SLAM map by closing loops, by hand or automatically. In Pose graph (SLAM), Open folder… takes a folder holding the poses and one scan per pose; Open files… takes the same files picked one by one.

A ROS bag (ROS 1 .bag, or the .mcap file of a ROS 2 bag) opens with Open files… too, and so do scans without poses: LiDAR odometry places the scans first, in the browser. It takes the bag's sensor_msgs/PointCloud2 topic with the most messages, registers each scan onto a local map of the ones before it (points between the Range limits, 1.5–80 m by default), and keeps a scan every Keyframe metres of travel as a node. When the bag has a sensor_msgs/Imu topic, its up direction at each keyframe (from the orientation, else the mean acceleration) then levels the graph as IMU gravity does, with the IMU's mounting estimated. Deskew scans undoes the sensor's motion during each scan (a spinning sensor sees the end of its sweep from somewhere else than its start) from the points' time field (t, time, timestamp…), or, without one, from the order a spinning sensor takes the points; scans sorted by ring or corrected already (KITTI's) are left as they are. It is off by default: on the drives tried (a Segway at 1 m/s, an ATV at 3–5 m/s with a 10 Hz sensor) it made no measurable difference against the ground truth. Compressed bags (lz4, zstd, bz2) open as they are; ROS 2 bags stored in SQLite (.db3) do not open: record them as MCAP, or convert them. A 2.4 GB ROS 1 bag of a Velodyne HDL-32E (1,263 scans, 126 s of driving) opens in about five minutes.

Nodes are white dots, odometry edges blue lines and loop edges orange. Type two node ids in Node A and Node B, or use Pick nodes and click two dots. Add loop registers B's scan onto A's with ICP, starting from their current relative pose, adds the result as a loop edge (weighted by σ loop) and optimises the graph (Levenberg–Marquardt). Robust loops puts a Huber kernel on loop edges, so a wrong loop bends the map less. Undo takes back the last loop, removal or optimisation.

Move A puts a gizmo on Node A: drag its arrows to move it (or turn it, with Rotate); With the nodes after it moves the rest of the path along, to put a broken stretch roughly in place before adding a loop there. Fix A holds a node in place when optimising (it turns red); the first node of a trajectory is fixed from the start. Each drag and each fix can be undone.

Find loops automatically proposes node pairs at least Min travel metres apart along the path, not already joined, at most one every Spacing metres, that the graph puts within Radius metres of each other plus Drift % of the path between them: odometry drifts with distance, so after a long way round a revisit is looked for further away (with 3 %, 60 m after 1.7 km). Each is registered with ICP from the graph's relative pose. When that overlaps too little and the drift allowance is large (5 m or more), it is registered again as if both scans were taken at the same place, from Retry headings headings. A pair is kept when at least Min overlap percent of B's points lie within the Inlier distance of A's and the registration puts B no further from where the graph has it than the drift allows; then the graph is optimised. Turn on Robust loops to soften any wrong loop that slips through, and check the list below. Undo removes all the loops found at once.

Floor constraint removes the tilt and height drift that odometry leaves. Add floor looks for the floor under every keyframe: the plane with the most scan points within Threshold, at most Max tilt from the Up axis and below the sensor (Auto tries +Z for LiDAR, −Y for scans moved into KITTI's camera frame, and +Y). The keyframes that see it are tied to one floor plane (a g2o VERTEX_PLANE with EDGE_SE3_PLANE edges, weighted by σ tilt and σ height), and the graph is optimised. Undo removes the floor again. Use it on sites that are flat overall (a car park, a floor of a building): it asserts that the ground does not bend, and on hills it flattens the path. Planes per stretch of path were tried and do the same, a little at a time, since each still asserts a flat stretch; on hills use IMU gravity instead.

Remove dynamic leaves out the points of things that moved while the scans were taken, as in Removert and dynamic-3d-object-removal: each scan becomes a range image (the nearest return per 3° of azimuth and elevation), and a point is dynamic when at least Votes of the scans within Window keyframes on either side saw through it (their beam in its direction reached more than Margin plus 2 % of the range further) and more of them saw through it than hit it. Dynamic points then gather into objects per scan (0.7 m apart, 15 points or more): stray ones go back, the rest of an object joins. The result is the map without them (_static) and them in red (_dynamic), both ordinary clouds. On SemanticKITTI 07 (its moving classes, KISS-ICP poses, scans thinned to 0.4 m) it scores precision 0.85, recall 0.55, F1 0.67 and keeps 99.9 % of the static points; the 1,101 scans take about 8 s, parts of the drive judged on the worker pool (35 s on one thread). On PandaSet scene 019 (a busy San Francisco street, 80 scans, 8 s; moving vehicles from the labelled cuboids) it scores precision 0.90, recall 0.33 and keeps 99.8 % of the static points: traffic driving along with the car is never seen through in so short a drive, so it stays. A beam that passed more than half a degree above a point does not count as seeing through it: on ground the beams graze, a beam a little higher lands far beyond, and a 32-beam LiDAR's rings leave gaps that wide (on hdl_graph_slam's hdl_400 drive this took the dynamic share from 7.1 to 4.7 %).

Analyse the map hands the corrected map to the analysis tools. Ground & DEM builds the map, keeps its ground points (the Cloth Simulation Filter) and rasterizes them into a terrain model of DEM cell metres, colored between the 2nd and 98th percentile of its heights (save it from the Rasterize panel). Compare parts (M3C2) builds the map of the path before Split at node and of the path from it, and runs M3C2 between them: for two joined sessions (the split then defaults to where the second starts) what changed between them, and for one drive that passes a place twice how well the passes agree. On KITTI 07 split at node 1000, where the drive comes back to its start, the mean M3C2 distance between the passes went from 0.17 m with the odometry alone to 0.02 m after Find loops and IMU gravity. Only the keyframes of each part within 50 m of the other part's path are compared, so a long drive fits in memory, and the colors span the 98th percentile of the changes. KITTI 00 (recorded on 3 October 2011) joined with 07 (30 September, three days earlier) at a street both drove (87 % overlap, then 202 loops between the two) compares at 214,000 core points in 6 s: the buildings agree (mean 0.002 m) and 12 % of the points changed significantly, mostly the cars parked along the streets. Changes then lists the changed objects, largest first: core points that changed significantly by 0.3 m or more, the same way, and within 1 m of each other, with each object's size and mean change; click one to centre the view on it. The M3C2 result gets a change_object field (each object's rank) for the scalar field tools. Between the two KITTI drives the largest objects are rows of parked cars that moved by 0.7 to 0.9 m or went.

IMU gravity is the constraint for hills. Add gravity… takes a KITTI oxts folder (roll and pitch of each frame, turned into the Velodyne frame by calib_imu_to_velo.txt when it is there, and by the scan → pose matrix the scans were opened with) or a text file of frame ux uy uz lines (up in scan coordinates). Each keyframe is held to its measured up direction (σ tilt, raised to the IMU's noise when that is larger; Estimate the IMU's mounting first finds how a real IMU sits against the LiDAR, turned, tilted or upside down, from the drive itself, and uses it when that clearly helps); world up is taken from the first keyframes, where odometry has drifted least. It assumes nothing about the ground, so the path loses its tilt drift, and the height drift that follows, while hills stay hills. After Find loops, on KITTI with KISS-ICP odometry, it took the height RMSE from 0.41 to 0.14 m on sequence 07, from 5.9 to 1.8 m on the hills of 09 (ATE RMSE 6.3 → 2.7 m) and from 9.4 to 1.0 m on 00 (ATE RMSE 11.1 → 6.1 m), in a few seconds each. Gravity ties are not written to g2o.

Join another graph adds a second session of the same place, recorded in its own frame: give a node of this graph (Node here; empty takes Node A) and a node of the other one standing near it (Node there; empty takes its first node), then Join folder… and pick the other session's folder (poses and scans, read with the same loading options). The two scans are registered with ICP from several Headings; the best overlap (at least Min overlap) places the whole second graph, becomes a loop edge between the two, and the joined graph is optimised. Its ids are shifted past this graph's when they clash. Find loops then ties the two sessions together wherever they meet. A join cannot be undone. Join files… takes the other session's files instead, or a ROS bag: odometry places its scans first, and when both bags have an IMU, the up directions of both level the joined graph. Two NCLT drives round the same block, in April and in June 2012, joined this way line up with each other to 0.16 m against the ground truth.

Edges by error colors every edge by how far it disagrees with the optimised graph (√χ², in standard deviations) in the color ramp, and Loops, worst first lists the loop edges by that error: a wrong loop shows up at the top. Click one to select its two nodes, ✕ to remove it and optimise again, or remove every loop with an error above a threshold with Remove loops. Scan colors shows a color per keyframe or colors by height; Show scans hides them to see the graph alone, and Pose axes draws each keyframe's axes (x red, y green, z blue). Play replays the drive: the keyframes' scans appear one after the other at Rate per second, the newest in white, with the camera following the vehicle (Follow), like watching LiDAR odometry run. Loops, optimisations, gravity and undo glide the map into its new place rather than jump (graphs of up to 3,000 keyframes); Go to A centres the view on Node A. This drive opens without corrections, to replay it or close its loop by hand (Node A 0, Node B 128). The scans are drawn without Eye-Dome Lighting, which would darken such thinned points, and the graph on top of them.

Save g2o writes the graph with its edges; KITTI and TUM write the optimised poses. Build map cloud adds every scan at its optimised pose to the cloud list, thinned to Map voxel, for measuring, comparing or exporting like any other cloud. Compare with the start builds the map with a correction field on every point: how far it moved from its place as loaded, and colors the map by it (a scalar field like any other, with its histogram and range filter): where the loops, floor, gravity and moves shifted the map, and by how much.

Tried on KITTI odometry sequences with real Velodyne scans and KISS-ICP odometry, against the ground truth, all with the default settings. Sequence 07 (1,101 scans, 2.1 GB) opens in about 20 s; Find loops brings the end back from 2.04 m to 0.14 m off (ATE RMSE 2.14 → 1.28 m). Sequence 09 (1,591 scans, 1.7 km over 38 m of hills) drifts 25 m by the time it returns to its start; Find loops finds that revisit (registered as the same place) and takes the end error to 4.0 m and the ATE RMSE from 16.4 to 6.5 m (SE(3)-aligned 7.3 → 1.8 m). Sequence 00 (4,541 scans, 8.6 GB, 3.7 km through a town with many revisits) opens in about 90 s using 1.3 GB; Find loops adds 121 loops in about 20 s and takes the end error from 24.8 to 3.2 m and the SE(3)-aligned ATE from 8.9 to 2.1 m (1.9 m with Robust loops). The floor constraint made sequence 09 worse, as said above. On NCLT session 2012-04-29 (a Segway round the University of Michigan campus: 2,777 keyframes, 3.2 km, prepared with scripts/prepare_nclt.py), Find loops adds 168 loops in 11 s and takes the SE(3)-aligned ATE from 4.83 to 1.64 m.

Vector map (Lanelet2)

To draft roads from measurements, open a point cloud and a TUM, KITTI or CSV trajectory, then choose them under Build from a trajectory. Both must use the same coordinate frame in metres. Set lane counts, width, traffic side and speed under Road options. The trajectory must follow the outside forward lane. Elevations come from the point cloud. The builder follows intensity peaks, curb steps and the end of ground coverage; boundaries without a detected feature use the configured width. Inferred lines are anchored to detected outer edges by default; clear the checkbox to keep them relative to the trajectory. They remain inferred after adjustment. The report gives the measured fraction for each boundary and warns about missing data. Scan gaps can look like road edges. Review the draft, especially repeated passes, lane changes and junctions; use Connect to add junction links. A whole build can be undone in one step. Use Frame map to focus the view on the road boundaries, then Edit vertices to drag the yellow boundary points. Each move keeps the vertex's height and updates every lane sharing that boundary, including lanes travelling in the opposite direction. A completed drag is one Undo step; Escape cancels an unfinished drag. Drag empty space to orbit as usual. Moving a section endpoint can leave a gap to the next section: review the validation messages and move its matching endpoint too. Inferred geometry still needs review after editing.

Open a point cloud, choose Top, and expand Vector map (Lanelet2). Use Draw road to click along a road and press Enter or double-click to build it. Backspace removes the last point; Esc cancels. Clicked cloud points supply the road's elevation; clicks without a cloud point use a horizontal plane at the map's height.

Set forward and backward lane counts, lane width, speed limit in km/h and the traffic side in Road options before drawing. Traffic keeps left by default. For a road with both directions, the drawn line separates them; for a one-way road, it is the road centre. A positive piece length divides the road into connected lane sections; zero keeps it whole. Adjacent lanes share boundaries.

The panel lists Autoware errors and warnings after each edit. Click a lane issue to inspect and frame that lane. Undo restores up to 100 edits, including Clear. Open… reads Lanelet2 .osm or vectormap JSON; an .osm file can also be dropped into the viewer. Opening replaces the current map and clears its undo history. Import notes lists the chosen coordinate projection and any map elements the reader cannot represent. Those elements will be absent from the saved map; retain the original file.

Save for Autoware downloads lanelet2_map.osm and map_projector_info.yaml. Allow multiple downloads if the browser prompts. New maps use the Local projector; maps opened with a supported georeference retain it. MGRS maps with mgrs_code node tags retain their UTM grid coordinates and export with projector_type: MGRS. Only a single 100 km UTM grid is supported; polar UPS grids and maps spanning multiple squares are rejected. Check reported issues and the coordinate frame before using the map in Autoware. Geometry and editing run locally through vectormap-rs.

Trajectories (ATE / RPE)

Open trajectories like clouds: TUM (timestamp tx ty tz qx qy qz qw, or without the quaternion), KITTI poses (12 numbers per line, a 3×4 matrix) and CSV with a header such as timestamp,x,y,z,qx,qy,qz,qw. .tum and .kitti files are always trajectories; a .txt or .csv file is one when its first line looks like it, and a point cloud otherwise. They are listed in Trajectories and drawn as lines, which you can hide or recolor.

Evaluate compares an estimate with a reference, with the same definitions as the Python CLI (ca traj-evaluate), so both give the same numbers:

The aligned estimate is added as a line colored by its ATE in the current color ramp. Save CSV writes the time, aligned estimate, reference and errors of every matched pose. An Origin or SE(3) alignment can also move a cloud, e.g. the map built from the estimate, into the reference's frame (undo with Ctrl+Z).

Filters and ground extraction

Each filter creates a new cloud and keeps the original:

Try: ground extraction

Rasterize (DEM / DSM)

Rasterize grids a cloud in XY: each cell takes the mean, minimum, maximum or a percentile of the heights in it, and empty cells can be filled from their neighbours. With a classified cloud, pick Ground for a terrain model (DTM) or all classes for a surface model (DSM); both use the same grid. The raster appears as a cloud colored by height. Save GeoTIFF writes a Float32 GeoTIFF placed by its pixel size and corner (no coordinate system is set: assign it in your GIS); Save PNG writes it in the current color ramp, optionally hillshaded.

Meshing (Delaunay 2.5D)

Mesh triangulates a cloud as seen from above (like CloudCompare's Delaunay 2.5D (XY plane)): the points' XY positions are joined by a Delaunay triangulation and keep their heights, which suits terrain and other surfaces with one height per position. Triangles with a horizontal edge longer than the max edge length are dropped; short bridges over gaps can remain. Left empty, it is four times the median edge, and 0 keeps every triangle up to the convex hull. Clouds of more than 5 million points are first thinned with a voxel filter, and the status says so. The mesh, named <cloud>_mesh, can be the reference of a C2M distance or a surface of a volume, and its ⤓ button saves it as binary PLY or OBJ. The mesh panel also offers wireframe inspection, OBJ/PLY export and a source/settings summary JSON. Prepare terrain mesh in Map coverage selects a ground cloud; Mesh compared After cells in Volume selects compared cell centers at After height. Use mesh as After surface prepares volume calculation with interpolation off. Review Before and grid settings before computing. See the terrain mesh workflow for scope and limitations.

Normals

Compute normals fits a plane to the k nearest neighbours of each point. The normals face up, or away from the cloud's centre. They are shown by the Normals color mode (direction as RGB) and the Hillshade mode (lit from the north-west). They are saved in PLY as nx ny nz.

Merge and split

Shapes and clusters

Shapes & clusters finds planes, cylinders or spheres with RANSAC (points within the distance of a shape, at least the minimum number of them), or groups points into clusters: points closer than the link distance belong together, and clusters smaller than the minimum are noise. The result is one cloud colored by segment, or a cloud per segment; the table lists each shape's parameters and fit (RMS). Normals are estimated first if the cloud has none. In a building, find the planes first, then look for cylinders in what is left.

Clipping and cropping

Clipping box limits the view to a box set with per-axis sliders. The X, Y and Z buttons cut a thin section and look straight at it. Crop copies the points inside the box into new clouds.

Lasso segmentation and undo

Segment (S) draws a lasso: click around the points to cut out (Backspace removes the last vertex), then Keep inside (Enter), Remove inside (Delete) or Split into both parts. It cuts every visible cloud through the depth of the view, and skips points hidden by the clipping box or a hidden class.

↶ / ↷ (Ctrl+Z / Ctrl+Shift+Z) undo and redo segmentation, cropping, filters, ground extraction, merge, split, M3C2, volume and ICP. Removing a cloud with ✕ is not undone: it frees the memory right away.

Cross-section profiles

Press Draw line and click points on the view (a top view works best). Finish with a double-click or Enter. Every visible cloud's points within the band width are plotted as distance along the line against height, in the cloud's color.

Hover over the plot to read values. 1:1 uses the same scale on both axes, and Save CSV writes cloud, distance, x, y, z. Drawing a profile across two surveys compares them directly.

Picking and measuring

Click a point to see its exact coordinates, color and distance value. Measure (M) takes two clicks and shows their distance and ΔX / ΔY / ΔZ. Esc cancels.

Label (L) puts a text label on the point you click. It starts as the point's height, and you edit the text in the Labels panel. Labels are part of share links and sessions. Image saves the view as a PNG, with the labels, measurements, colorbar and profile plot drawn on it.

Saving results

The ⤓ button next to a cloud offers PLY, LAS, LAZ and CSV; the Distance panel saves the cloud whose distances are shown. A PLY file includes colors and all attributes: intensity, classification, normals, M3C2 fields and distances as scalar_C2C_distance. CloudCompare reads these as scalar fields. A mesh's ⤓ offers binary PLY and OBJ.

LAS and LAZ files are LAS 1.4 (point format 6, or 7 with colors) with intensity and classification; distances, normals and M3C2 fields are stored as float extra bytes, which CloudCompare, PDAL and LAStools load as scalar fields. Coordinates keep their precision: a cloud read from LAS is written on its original grid (e.g. 1 cm), and other clouds at the finest power of ten that fits (down to 0.1 µm).

QA report and gates

The QA report panel collects the metrics of the latest results: distances (mean, median, RMS, 95th percentile…), volume, ICP and point-pair alignment, rasters, shapes and trajectories (ATE, RPE). Add gate sets a pass / fail threshold on any of them, with fail or warn severity; each gate shows its status as results change, and gates are kept in sessions and share links.

Save report writes a self-contained HTML page with the verdict, the gates, every result and the view. Save JSON writes the same metrics with a gate summary in the format of the command line's ca check (cloudanalyzer.gate_summary.v0.1), so CI can read it.

Sharing and sessions

Large files

Checked against CloudCompare

C2C, SOR and CSF give the same results as CloudCompare 2.13 on a public LiDAR tile. M3C2 agrees to a thousandth of a foot, and ICP, normals and volume are close. The validation report has the numbers and the differences in conventions.

Python

The same Rust core is available to Python as cloudanalyzer_core, multi-threaded. It covers reading, C2C/C2M, ICP, filters, CSF, volume, M3C2, profiles and normals. It is not on PyPI yet; build it from rust/crates/ca-py with maturin, as the README explains.