Kitware Source Feature Article: January 2010

A Synthetic LiDAR Scanner for VTK

In recent years, Light Detection and Ranging (LiDAR) scanners have become more prevalent in the scientific community. They capture a “2.5-D” image of a scene by sending out thousands of laser pulses and using time-of-flight calculations to determine the distance to the first reflecting surface in the scene. However, they are still quite expensive, limiting the availability of the data which they produce. Even if a LiDAR scanner is available to a researcher, it can be quite time consuming to physically set up a collection of objects and scan them.

Here, we present a set of classes which allow researchers to compose a digital scene of 3D models and ”scan” the scene by finding the ray and scene intersections using techniques from ray tracing. This allows researchers to quickly and easily produce their own LiDAR data. The synthetic LiDAR scan data can also be used to produce datasets for which a ground truth is known. This is useful to ensure algorithms are behaving properly before moving to non-synthetic LiDAR scans. If more realistic data is required, noise can be added to the points to attempt to simulate a real LiDAR scan.

Scanner Model
We have based the synthetic scanner on the Leica HDS3000 LiDAR scanner. This scanner acquires points on a uniform angular grid. The first point acquired is in the lower left of the grid. The scanner proceeds bottom to top, left to right. The order of acquisition is usually not important, but the code could be easily changed to mimic a different scanner if desired.

Input Parameters
It is necessary to set several parameters before performing a synthetic scan. These parameters are:

  • A scene to scan (triangulated 3D mesh)
  • Scanner position (3D coordinate)
  • Min/Max Φ angle (how far ”up and down” the scanner should scan)
  • Min/Max θ angle (how far ”left and right” the scanner should scan)
  • Scanner “forward” (the Φ = 0, θ = 0 direction)
  • Number of θ points.
  • Number of Φ points.

Outputs
Two outputs are possible depending on the user’s requirements. The first is a PTX file. A PTX file implicitly maintains the structure of the scan. The scan points in the file are listed in the same order as they were acquired. This point list, together with the size of the scan grid, is sufficient to represent the grid of points acquired by the synthetic scanner.

The second type of output is simply an unorganized point cloud stored in a VTP file. This representation is useful for algorithms which do not assume any structure is known about the input.

New Classes
We introduce three new classes to implement a synthetic LiDAR scanner. The first two, vtkRay and vtkLidarPoint are supporting classes of vtkLidarScanner, which is the class that does the majority of the work.

vtkRay
This is a container class to hold a point (ray origin) and a vector (ray direction). It also contains functions to:

  • Get a point along the ray a specified distance from the origin of the ray (double* GetPointAlong(double))
  • Determine if a point is in the half-space that the ray is pointing “toward“ (bool IsInfront(double*))
  • Transform the ray (void ApplyTransform(vtkTransform* Trans))

vtkLidarPoint
This class stores all of the information about the acquisition of a single LiDAR point. It stores:

  • The ray that was cast to obtain the point (vtkRay* Ray)
  • The coordinate of the closest intersection of the ray with the scene (double Coordinate[3])
  • The normal of the scene triangle that was intersected to produce the point (double Normal[3])
  • A boolean to determine if the ray in fact hit the scene at all (bool Hit)

vtkLidarScanner
This class does all the work of acquiring the synthetic scan.

Coordinate System
The default scanner is located at (0,0,0) with the following orientation:

  • Z axis = (0,0,1) = up
  • Y axis = (0,1,0) = forward
  • X axis = (1,0,0) = right (a consequence of maintaining a right handed coordinate system)

Positioning (“aiming”) a vtkLidarScanner
A rigid transformation can be applied to the scanner via the SetTransform function which positions and orients the scanner relative to the scene.

Casting Rays
Rays cast by the scanner are relative to the scanner’s frame after the transformation is applied to the default frame.

3D View
Figure 1: 3D view of the scan.

To demonstrate the angles which must be specified, a (Φ = 5; θ = 4) scan of a flat surface was acquired, as shown in Figure 1. Throughout these examples the red sphere indicates the scanner location, cylinders represent the scan rays, and blue points represent scan points (intersections of the rays with the object/scene).

Order of Acquisition
The points were acquired in the order shown in Figure 2.

Scan Points
Figure 2: Scan points labeled in acquisition order.

Theta angle
The angle in the Forward-Right plane (a rotation around Up), measured from Forward. Its range is -Pi to Pi. –Pi/2 is left, Pi/2 is right.

Figure 3 shows a top view of the scan of a flat surface. The min and max θ angles are labeled.

Theta Angle Settings
Figure 3: Diagram of Theta angle settings.

Phi angle
The elevation angle, in the Forward-Up plane (a rotation around Right), measured from Forward. Its range is –Pi(down) to Pi(up). This is obtained by rotating around the “right” axis (AFTER the new right axis is obtained by setting Theta).

Figure 4 shows a side (left) view of the scan of a flat surface. The min and max Φ angles are labeled

Phi Angle Settings
Figure 4: Diagram of Phi angle settings.

Normals
Each LiDAR point stores the normal of the surface that it intersected. A scan of a sphere is shown in Figure 5 to demonstrate this. The normal vector coming from the scanner (red sphere) is the “up” direction of the scanner.

Outputs
vtkPolyData
A typical output from a VTK filter is a vtkPolyData. The vtkLidarScanner filter stores and returns the valid LiDAR returns in a vtkPolyData. If the CreateMesh flag is set to true, a Delaunay triangulation is performed to create a triangle mesh from the LiDAR points. vtkDelaunay2D is used to triangulate the 3D points utilizing the grid structure of the scan. Figure 6 shows the setup of a scan, the result with CreateMesh = false, and the result with CreateMesh = true.

Scene Intersections and Normals
Figure 5: Scene intersections and their normals

PTX file
All of the LiDAR ray returns, valid and invalid, are written to an ASCII PTX file. The PTX format implicitly maintains the order of point acquisition by recording the points in the same order in which they were acquired. A “miss” point is recorded as a row of zeros. Upon reading the PTX file (not covered by this set of classes), the best test to see if a row of the file is valid is checking if the intensity of the return is 0. This prevents corner cases, such as a valid return from (0;0;0), from creating a problem or confusion.

Scanner coordinate frame
Using void vtkLidarScanner::WriteScanner(const std::string &Filename) const, a VTP file of the scanner can be written. A coordinate frame indicates the location and orientation of the scanner. The green axis is “up”, the yellow axis is “forward” and the red axis is “right”. Figure 7 shows the synthetic scan of a sphere along with the scanner that was used to perform the scan.

Data Structure Speed Up
Instead of intersecting each ray with every triangle in the scene, this problem immediately lends itself to using a spatial data structure to achieve an enormous speedup. We originally tried an octree (vtkOBBTree), but we found that a modified BSP tree (vtkModifiedBSPTree) gives a 45x speedup, even over the octree! The current implementation includes this speed up and is therefore very fast.

CreateMesh Effects
Figure 6: Effect of CreateMesh flag.

Synthetic Scan
Figure 7: Synthetic scan of a sphere with the scanner displayed

Noise Model
By default, a synthetic scan is “perfect” in the sense that the scan points actually lie on a surface of the 3D model as in Figure 8. In a real world scan, however, this is clearly not the case. To make the synthetic scans more realistic, we have modeled the noise in a LiDAR scan using two independent noise sources: line-of-sight and orthogonal.

Line-of-Sight (LOS) Noise
Line-of-Sight noise reads as an error in the distance measurement performed by the scanner. It is a vector parallel to the scanner ray whose length is chosen randomly from a Gaussian distribution. This distribution is zero mean and has a user specified variance (double LOSVariance). An example of a synthetic scan with LOS noise added is shown in Figure 9.The important note is that the orange (noisy) rays are exactly aligned with the gray (noiseless) rays.

Noiseless Synthetic Scan
Figure 8: A noiseless synthetic scan

Line of Sight Synthetic Scan
Figure 9: A synthetic scan with line-of-sight noise added

Orthogonal Noise
Orthogonal noise models the angular error of the scanner. It is implemented by generating a vector orthogonal to the scanner ray whose length is chosen from a Gaussian distribution. This distribution is also zero mean and has a user specified variance (double OrthogonalVariance). An example of a synthetic scan with orthogonal noise added is shown in Figure 10. Note that the green (noisy) rays are not aligned with the gray (noiseless) rays, but they are the same length.

Combined Noise
A simple vector sum is used to combine the orthogonal noise vector with the LOS noise vector.

Orthogonal Noise
Figure 10: A synthetic scan with orthogonal noise added

Example Scene
As an example, a car model with 20k triangles was scanned with a 100x100 grid. On a P4 3GHz machine with 2GB of ram, the scan took 0.6 seconds. Figure 11 shows the model and the resulting synthetic scan.

Car Model with Synthetic Scan
Figure 11: A car model and the resulting synthetic scan.

Example Code
An example (TestScanner.cpp) is provided with the code zip file, but the basics are demonstrated here below with hard coded values:

//read a scene
vtkXMLPolyDataReader* reader =
   vtkXMLPolyDataReader::New();
reader->SetFileName(InputFilename.c_str());
reader->Update();

//construct a vtkLidarScanner
vtkLidarScanner* Scanner =
   vtkLidarScanner::New();

//Set all of the scanner parameters

Scanner->SetPhiSpan(vtkMath::Pi()/4.0);
Scanner->SetThetaSpan(vtkMath::Pi()/4.0);

Scanner->SetNumberOfThetaPoints(5);
Scanner->SetNumberOfPhiPoints(6);

Scanner->SetStoreRays(true);

//”Aim” the scanner. This is a very simple
//translation, but any transformation will work
vtkTransform* transform = vtkTransform::New();
   transform->PostMultiply();
//this is so we can specify the operations in the
//order they should be performed (i.e. this is a
//rotation followed by a translation)

//(any of these 4 lines can be ommited if they
//are not required)
transform->RotateX(0.0);
transform->RotateY(0.0);
transform->RotateZ(1.0);
transform->Translate(Tx, Ty, Tz);

Scanner->SetTransform(transform);
//indicate to use uniform spherical spacing
Scanner->MakeSphericalGrid();

Scanner->SetCreateMesh(true);

Scanner->SetInput(reader->GetOutput());
Scanner->Update();

//create a writer and write the output VTP file
vtkSmartPointer<vtkXMLPolyDataWriter> writer =
   vtkSmartPointer<vtkXMLPolyDataWriter>::New();
writer->SetFileName(“test_scan.vtp”);
writer->SetInput(Scanner->GetOutput());
writer->Write();

David Doria  David Doria is a PhD student in Electrical Engineering at Rensselaer Polytechnic Institute. He received his BS in EE in 2007 and his MS in EE in 2008, both from RPI. David is currently working on 3D object detection in LiDAR data. He is passionate about reducing the barrier of entry into 3D data processing. Find out more about David on his website rpi.edu/~doriad or email him at daviddoria@gmail.com.