Skip to content

3D with 2D LiDARs

Compatible with: picoScan100

The picoScan measures distance and intensity in a single plane. Mounting it on a motorised axis (tilt, rotation, or a linear stage) sweeps that plane through 3D space. If an incremental encoder is attached to the axis, the rotation angle at the moment of each scan is known precisely. This means every 2D point can be rotated into a common 3D frame, and a full sweep becomes a dense 3D point cloud.

Two approaches exist. Choose based on where you want the geometry code to run.

Approach A (host-side) Approach B (on-device)
Angle source Encoder wired to the sensor Encoder (or IMU) wired to the sensor
Processing Host receives raw UDP frames and applies the 3D transform Device accumulates and saves a PCD file internally
Result One host-generated .pcd per sweep Single .pcd from GET /cloud.pcd
Best for Custom transforms, real-time streaming, maximum flexibility Simple integration, no host-side geometry code

The sensor tags each UDP scan packet with the encoder tick at capture time, giving frame-accurate synchronisation without timing jitter between the scan and position streams.

Hardware and ports

A typical example is a sensor on a motorised tilt axis with a SICK DFS60-series incremental encoder wired to the sensor's encoder input. The same idea applies to rotation or linear stages: what matters is that an encoder on the moving axis feeds the sensor so each scan frame carries a position value.

Stream Protocol Port Content
Scan data Compact UDP 2115 2D distance + intensity per frame, with frame sequence number
Encoder data Compact UDP 7504 Encoder tick counter, reference tick, frame sequence number

Enable UDP output on port 7504 and reset the encoder counter at the reference position before you collect data. The Configuration backup & restore flow is a convenient way to apply a validated encoder configuration.

Approach A: host-side transform

Synchronise by frame sequence number

Both UDP streams carry a frameSequenceNumber. Build a lookup table from that number to the encoder tick, so you can retrieve the exact axis angle for any scan frame even when the two streams arrive slightly out of order:

on encoder packet:  encoder_map[packet.frameSequenceNumber] = packet.tickCounter
on scan frame:      tick = encoder_map.lookup(frame.frameSequenceNumber)  # or most-recent tick

Keep the table bounded in size to avoid unbounded memory growth over long acquisitions.

Coordinate transform

Convert each encoder tick to an angle, then rotate the 2D scan point around the axis. For a tilt axis along the world Y-axis with the scan plane horizontal at the reference:

theta   = (tick / ticks_per_revolution) * 2 * PI      # radians
x_world = x_scan * cos(theta)
y_world = y_scan                                       # lateral axis unchanged
z_world = x_scan * sin(theta)

Set ticks-per-revolution correctly. This value is specific to your encoder model. A mismatch produces a distorted point cloud. Reset the encoder counter at the reference position so tick 0 is your angular origin, and adapt the transform for a different axis orientation or a linear stage.

End-to-end pseudocode

Reset the encoder counter to establish tick 0:

POST /api/mResetEncoderIncrement

Process the two UDP streams and transform each scan frame:

encoder_map = {}          # frame_seq_num -> tick
cloud_pts   = []

def on_encoder_packet(pkt):
    encoder_map[pkt.frameSequenceNumber] = pkt.tickCounter

def on_scan_frame(frame, points_2d):
    tick  = encoder_map.get(frame.frameSequenceNumber, last_known_tick)
    theta = (tick / TICKS_PER_REV) * 2 * PI
    for (x, y, intensity) in points_2d:
        cloud_pts.append({ x: x*cos(theta), y: y, z: x*sin(theta), intensity })

udp_listen(port=2115, callback=on_scan_frame)
udp_listen(port=7504, callback=on_encoder_packet)
run_until_interrupted()
save_pcd(cloud_pts, "cloud.pcd")

The sick_perception_sdk (C++17) is a good starting point for a host-side implementation.

Approach B: on-device scan merger

The on-device scan merger runs entirely on the device. While the sensor rotates, it combines consecutive 2D profiles into a 3D point cloud and saves a .pcd file in device storage. The host only needs to trigger, poll, and download the result with one HTTP GET; no UDP parsing or transform code is required. The angle source is configurable (encoder, or the built-in IMU when no encoder is available).

3D scan recording interface

Live capture of the 3D scan recording interface on a picoScan100.

Configuration parameters

Parameter Endpoint Values Description
ScanMergerEnabled POST /api/ScanMergerEnabled true / false Enable the scan merger
ScanMergerSource POST /api/ScanMergerSource "ENCODER" / "IMU" Rotation reference
ScanMergeTrigger POST /api/ScanMergeTrigger 0 (METHOD) Start/stop via method calls
RotationOffset POST /api/RotationOffset {x, y, z} mm Sensor offset from the rotation axis

Setting parameters and calling merger methods requires challenge-response authentication.

State fields

Field Endpoint Meaning
MergeRunning GET /api/MergeRunning true while actively recording
MergeSaving GET /api/MergeSaving true while writing the PCD file
SaveProgress GET /api/SaveProgress 0-100; 100 means the file is ready

Workflow

  1. Configure the merger parameters and persist with WriteEeprom.
  2. POST /api/ResetScanMergeView: clear any previous recording.
  3. POST /api/StartScanMerge: MergeRunning becomes true within a few seconds.
  4. Poll MergeRunning until the desired duration, then POST /api/StopScanMerge.
  5. Poll MergeSaving / SaveProgress until SaveProgress = 100.
  6. GET /cloud.pcd: download the finished point cloud.

The scan merger is a one-shot recorder: it accumulates on-board and stores a PCD file retrieved by a single REST download. There is no continuous 3D push. For continuous 3D, use Approach A or the Compact stream with your own accumulation.