Main Content

Highway Vehicle Tracking Using Multi-Sensor Data Fusion

Since R2024b

This example shows how to track vehicles on a highway with commonly used sensors such as radar, camera, and lidar. In this example, you configure and run a Joint Integrated Probabilistic Data Association (JIPDA) tracker to track vehicles using recorded data from a suburban highway driving scenario.

Download Recorded Data

You start by downloading the data set from https://ssd.mathworks.com/supportfiles/driving/data/MultiSensorDrivingData_Seg7_Seq30.zip. The data set includes recorded data from camera, radar, and lidar sensors, along with odometry data for the vehicle. Note that the entire data set is approximately 1.8 GB. Use the downloadDataset supporting function to download the data in a struct format. In the following sections, you will configure a multi-object tracking algorithm to track vehicles using this data.

if ~exist('highwayRecordedData','var')
    highwayRecordedData = downloadDataset();
end

Introduction

Object tracking in advanced driver assistance systems (ADAS) and automated driving (AD) applications involves understanding the environment around the host or ego vehicle. This requires estimating the number of objects, their positions and their kinematics by processing acquired sensor data in real time. Commonly used sensors for these applications include radars, cameras, and lidars. Depending on the application, multiple sensors are often used to increase the field of view and improve the estimate of the environment by using sensor fusion techniques to leverage the complementary strengths of each sensor. In this example, you will use a task-oriented approach to define a sensor fusion algorithm to track vehicles on a highway using a combination of radar, camera, and lidar sensors.

Traditionally, setting up a tracker requires engineers to navigate a complex series of steps to define and tune an effective tracking algorithm. These steps involve selecting a state model for the targets, defining a suitable state transition model, and tuning its parameters, such as process noise and initial error state covariance. Additionally, for each sensor, users must select an appropriate measurement model and tune its parameters, such as measurement noise, along with other parameters like detection probability, birth density, and clutter density. For an example showing this approach, refer to the Track Vehicles Using Lidar: From Point Cloud to Track List example. This process usually requires deep understanding in the field of multi-object tracking. Furthermore, it also makes it challenging to trace and distinguish parameters of a sensor from parameters of a tracking algorithm.

The task-oriented approach to multi-object tracking, introduced in R2024b, aims to simplify this process by providing a new API, which allows users to configure the tracker with minimal experience in the tracking domain. This task-oriented approach defines the high-level workflow to configure and run a tracking algorithm using the five steps shown below.

Specify what you want to track - In this step, you specify the type and the characteristics of the objects you intend to track. This step informs the tracker about choosing appropriate models and their parameters to define the target.

Specify what sensors you have - In this step, you provide a detailed description of the sensors that will be employed for tracking. This step informs the tracker about choosing appropriate models and their parameters to define the sensor.

Define tracker - With the targets and sensors defined, you configure a multi-target tracker to integrate these elements.

Understand data format - In this step, you understand the information requested by the tracker and its format. You use this information to assemble the sensor data in the required format.

Update tracker: Finally, you update the tracker sequentially with data obtained from each sensor to track objects.

In the rest of the example, you will use these steps to configure and run a Joint Integrated Probabilistic Data Association (JIPDA) tracker.

Joint Probabilistic Integrated Data Association Tracker

The Joint Integrated Probabilistic Data Association (JIPDA) is a multi-object tracking algorithm that uses weighted (probabilistic) associations between measurements and tracks to estimate the position and kinematics of objects. The JIPDA tracker offers a balance between computational complexity and resource usage, making it a good choice for various applications.

The JIPDA tracker assumes that each object generates at most one measurement or detection per sensor. However, high-resolution sensors such as radar, lidar, and camera often produce multiple returns from each object. For example, a radar outputs a sparse point cloud from the environment, a lidar outputs a dense point cloud, and a camera outputs an image. To use the JIPDA tracker for automotive applications, each sensor's data must be preprocessed to generate at most one detection per object. These detections are also called object-level detections. The following image shows the pipeline used in this example for processing each sensor.

Step 1 - Specify what you want to track

You use the trackerTargetSpec function to create to specify the type and characteristics of the objects to track. The function uses a library of prebuilt specifications provided in the toolbox. To check the complete library of target specifications, refer to the documentation of trackerTargetSpec function. In this example, you define the target specifications for cars and trucks driving on a highway. You also configure the specification for each type of object to change the typical maximum speed of the cars and trucks observed for your application. The display below shows the list of properties of each specification, which can be fine-tuned for your application. These properties impact the underlying prior information for the state transition model used by the target specification. The ReferenceFrame on the specifications is retained as "ego" to track the positions of the objects with respect to the ego vehicle. A "global" reference frame can be chosen if you want to estimate the position of the objects with respect to a stationary coordinate frame. For more details on choosing the reference frame and its impact on the tracker, check the ReferenceFrame property of the HighwayCar.

% Create target specification for a car driving on a highway
carSpec = trackerTargetSpec('automotive','car','highway-driving');
carSpec.MaxSpeed = 30;
disp(carSpec);
  HighwayCar with properties:

        ReferenceFrame: 'ego'              
              MaxSpeed: 30           m/s   
       MaxAcceleration: 4            m/s²  
            MaxYawRate: 5            deg/s 
    MaxYawAcceleration: 20           deg/s²
             YawLimits: [-10 10]     deg   
          LengthLimits: [3.6 5.6]    m     
           WidthLimits: [1.7 2]      m     
          HeightLimits: [1.4 2]      m     
% Create target specification for a truck driving on a highway
truckSpec = trackerTargetSpec('automotive','truck','highway-driving');
truckSpec.MaxSpeed = 20;
disp(truckSpec);
  HighwayTruck with properties:

        ReferenceFrame: 'ego'              
              MaxSpeed: 20           m/s   
       MaxAcceleration: 3            m/s²  
            MaxYawRate: 4            deg/s 
    MaxYawAcceleration: 10           deg/s²
             YawLimits: [-10 10]     deg   
          LengthLimits: [16 22]      m     
           WidthLimits: [2 2.6]      m     
          HeightLimits: [3.5 4.2]    m     

Step 2 - Specify what sensors you have

Similar to target specifications, the toolbox also provides a prebuilt library of commonly used sensors for tracking. To check the complete library of sensor specifications, refer to the trackerSensorSpec function. In the following steps, you use the trackerSensorSpec function to define the specification of each sensor used in this example.

Define Specification for Automotive Radar Sensor

To specify the radar sensor for the JIPDA tracker, you create a specification for radar that assumes object-level detections in the form of clustered points. You use the trackerSensorSpec function to create this radar specification. Furthermore, you configure the radar sensor's properties to define its mounting location, orientation with respect to the ego vehicle, and parameters such as field of view. These properties significantly impact the various models used by the tracker for the radar sensor and, consequently, the overall performance of the tracker. For more detailed information about each property, refer to the AutomotiveRadarClusteredPointsdocumentation.

% Create radar sensor specification
radarSpec = trackerSensorSpec('automotive','radar','clustered-points');

% Configure its properties
radarSpec.MaxNumMeasurements = 30;
radarSpec.MountingLocation = [4.68465 0.01270 -0.11430];
radarSpec.MountingAngles = [0 -1 0.2];
radarSpec.FieldOfView = [100 15];
radarSpec.MaxRange = 120;
radarSpec.MaxRangeRate = 85;
radarSpec.DetectionProbability = 0.9;
radarSpec.NumFalsePositivesPerScan = 6;
radarSpec.NumNewTargetsPerScan = 2;
disp(radarSpec);
  AutomotiveRadarClusteredPoints with properties:

              ReferenceFrame: 'ego'                         
          MaxNumMeasurements: 30                            
            MountingLocation: [4.6847 0.0127 -0.1143]    m  
              MountingAngles: [1.9416e-19 -1 0.2]        deg
                 FieldOfView: [100 15]                   deg
                    MaxRange: 120                        m  
                MaxRangeRate: 85                         m/s
        DetectionProbability: 0.9                           
    NumFalsePositivesPerScan: 6                             
        NumNewTargetsPerScan: 2                             
                HasElevation: 1                             

Define Specification for Camera Sensor

Next, you define the specification for the camera sensor, which reports bounding boxes for the objects. Similar to the radar sensor, you carefully set the properties of the camera specification to define the camera and detector used in this application. Since the camera reports bounding boxes in the image, you also specify the intrinsics matrix of the camera to track objects in 3-D space. For more details regarding each property, refer to the AutomotiveCameraBoxes documentation.

% Create camera sensor specification
cameraSpec = trackerSensorSpec('automotive','camera','bounding-boxes');

% Configure its properties
cameraSpec.MaxNumMeasurements = 20;
cameraSpec.MountingLocation = [2.1398 -0.3180 1.2014]; 
cameraSpec.MountingAngles = [0 2 0.2];
cameraSpec.WidthAccuracy = 10;
cameraSpec.CenterAccuracy = 10;
cameraSpec.HeightAccuracy = 5;
cameraSpec.MaxRange = 120;
cameraSpec.DetectionProbability = 0.95;
cameraSpec.EgoOriginHeight = 0.4826;
cameraSpec.Intrinsics = [1176.29501,    0.     ,  520.32287;
                            0      , 1176.28913,  389.60511;
                            0.     ,    0.     ,    1     ];
cameraSpec.ImageSize = [768 1024];
cameraSpec.NumFalsePositivesPerImage = 1e-2;
cameraSpec.NumNewTargetsPerImage = 1e-2;
disp(cameraSpec);
  AutomotiveCameraBoxes with properties:

               ReferenceFrame: 'ego'                           
           MaxNumMeasurements: 20                              
             MountingLocation: [2.1398 -0.318 1.2014]    m     
               MountingAngles: [0 2 0.2]                 deg   
              EgoOriginHeight: 0.4826                    m     
                   Intrinsics: [3⨯3 double]                    
                    ImageSize: [768 1024]                pixels
                     MaxRange: 120                       m     
               CenterAccuracy: 10                        pixels
               HeightAccuracy: 5                         pixels
                WidthAccuracy: 10                        pixels
         DetectionProbability: 0.95                            
    NumFalsePositivesPerImage: 0.01                            
        NumNewTargetsPerImage: 0.01                            

Define Specification for Lidar Sensor

Finally, you define the specification for the lidar sensor, which reports 3-D bounding boxes, and configure the properties of the specification to describe the lidar used in this example. For more details regarding each property, refer to the AutomotiveLidarBoxes documentation.

% Create lidar sensor specification
lidarSpec = trackerSensorSpec('automotive','lidar','bounding-boxes');

% Configure its properties
lidarSpec.MaxNumMeasurements = 20;
lidarSpec.MountingLocation = [2.12090 0.01270 1.10712];
lidarSpec.MountingAngles = [0 2 0.2];
lidarSpec.ElevationLimits = [-25 25];
lidarSpec.AzimuthLimits = [-90 90];
lidarSpec.MaxRange = 150;
lidarSpec.DetectionProbability = 0.9;
lidarSpec.DimensionAccuracy = 0.25;
lidarSpec.CenterAccuracy = 0.25;
lidarSpec.OrientationAccuracy = 5;
lidarSpec.DetectionProbability = 0.9;
lidarSpec.NumFalsePositivesPerScan = 2;
lidarSpec.NumNewTargetsPerScan = 1;
disp(lidarSpec);
  AutomotiveLidarBoxes with properties:

              ReferenceFrame: 'ego'                        
          MaxNumMeasurements: 20                           
            MountingLocation: [2.1209 0.0127 1.1071]    m  
              MountingAngles: [0 2 0.2]                 deg
               AzimuthLimits: [-90 90]                  deg
             ElevationLimits: [-25 25]                  deg
                    MaxRange: 150                       m  
        DetectionProbability: 0.9                          
              CenterAccuracy: 0.25                      m  
           DimensionAccuracy: 0.25                      m  
         OrientationAccuracy: 5                         deg
    NumFalsePositivesPerScan: 2                            
        NumNewTargetsPerScan: 1                            

Step 3 - Configure the tracker

In this step, you use the defined target and sensor specifications to configure a multi-object JIPDA tracker using the multiSensorTargetTracker function. For convenience in this example, you also define a few checkboxes to enable or disable a sensor for tracking. This demonstrates the ease of adding and removing sensors from the tracker using the task-oriented approach.

% Target specifications as a cell array of all targets 
targetSpecifications = {carSpec, truckSpec};

% Choose a combination of sensors for fusion.
UseRadar = true;
UseLidar = true;
UseCamera = true;

% A logical array to select the right specifications
sensorFlags = [UseRadar, UseLidar, UseCamera];

% Sensor specifications as a cell array of all sensors
sensorSpecifications = {radarSpec, lidarSpec, cameraSpec};
sensorSpecifications = sensorSpecifications(sensorFlags);

% Define a tracker that can track the target using the defined sensors.
tracker = multiSensorTargetTracker(targetSpecifications, sensorSpecifications, 'jipda');

% Fine-tune the tracker to allow an assignment when Mahalanobis distance
% between a track and measurement is less than 7.
tracker.MaxMahalanobisDistance = 7;

disp(tracker);
  fusion.tracker.JIPDATracker with properties:

                TargetSpecifications: {[1×1 HighwayCar]  [1×1 HighwayTruck]}
                SensorSpecifications: {[1×1 AutomotiveRadarClusteredPoints]  [1×1 AutomotiveLidarBoxes]  [1×1 AutomotiveCameraBoxes]}
              MaxMahalanobisDistance: 7
    ConfirmationExistenceProbability: 0.9000
        DeletionExistenceProbability: 0.1000

Step 4 - Understand Data Format

In this step, you understand the data format required by the tracker. The data format represents the structure of the data needed by the tracker to update it. You use the dataFormat function to understand the inputs required from each sensor as well as any additional data needed for updating the target models. By default, the example uses radar, camera, and lidar fusion, as specified by the checkboxes in the previous section. Therefore, the tracker requires inputs from all three sensors as well as ego motion information for compensating ego motion for tracking in the moving ego reference frame.

trackerDataFormats = dataFormat(tracker)
trackerDataFormats=1×4 cell array
    {1×1 struct}    {1×1 struct}    {1×1 struct}    {1×1 struct}

The first input represents the data required from the first sensor specification, the radar sensor. Note that the radar specification requires the azimuth, elevation, range, and range-rate of each clustered point as well as its accuracy defined in terms of standard deviation. The data format is populated with zeros to indicate the maximum number of measurements. For clustered radar points, the accuracy of each point is typically different, and hence you specify a different accuracy for each point. For a radar 2-D radar without elevation information, you can disable elevation by setting HasElevation property of the radar specification to false.

disp(trackerDataFormats{1});
                  Time: 0
               Azimuth: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
             Elevation: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
                 Range: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
             RangeRate: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
       AzimuthAccuracy: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
     ElevationAccuracy: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
         RangeAccuracy: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
     RangeRateAccuracy: [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
              EgoSpeed: 0
    EgoAngularVelocity: [0 0 0]

The second input represents the data required from the second sensor specification, the lidar sensor. Note that the lidar sensor specification requires the detected bounding boxes as an array. Each column represents the [x;y;z;L;W;H;roll;pitch;yaw] information of the bounding box, where (x,y,z) defines the coordinate of the box center in the sensor's coordinate system, (L,W,H) represents the dimensions of the box, and (roll, pitch, yaw) defines the orientation of the box.

% Only display when the sensor is used according to checkbox
if numel(trackerDataFormats) > 2
    disp(trackerDataFormats{2});
end
           Time: 0
    BoundingBox: [9×20 double]

The third input represents the data required from the third sensor specification, the camera sensor. Note that the camera sensor specification also requires the detected bounding boxes as an array. Each column represents the [u;v;w;h] information of the bounding box, where (u,v) defines the coordinate of the top-left corner, and (w,h) denotes the width and height of the box.

% Only display when the sensor is used according to checkbox
if numel(trackerDataFormats) > 3 
    disp(trackerDataFormats{3});
end
           Time: 0
    BoundingBox: [4×20 double]

In addition to sensor data, the tracker also requires ego motion information to compensate for the ego motion in the tracks and to track in the moving ego vehicle frame.

% Ego motion format display
disp(trackerDataFormats{end});
                         Time: 0
    EgoPositionalDisplacement: [0 0 0]
    EgoRotationalDisplacement: [0 0 0]

Step 5 - Update the tracker

In this section, you use the following supporting functions to implement the entire processing chain from raw sensor data to tracks.

streamData - This function parses the sensor data logs and returns the data captured in a given time interval. It replicates the sensor stream in a real-time system using a recording.

detectRadarClusters - This function preprocesses the raw point cloud from the radar sensor into clustered points using doppler analysis and the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) algorithm. This supporting function requires Radar Toolbox™. By default, the example uses recorded clusters from the radar.

detectLidarBoundingBoxes - This function preprocesses the raw point cloud from the lidar sensor into 3-D bounding box detections using a Voxel RCNN Object Detector. This supporting function requires Lidar Toolbox™ and the Lidar Toolbox Interface for OpenPCDet Library support package.

detectImageBoundingBoxes - This function preprocesses the camera image into image bounding box detections using a YOLO v4 Object Detector. This supporting function requires Computer Vision Toolbox™ and the Computer Vision Toolbox™ Model for YOLO v4 Object Detection support package.

estimateEgoMotion and resetEgoMotion - These functions estimate the ego motion information using vehicle speed and yaw-rate acquired from wheel encoders.

% Create a viewer to visualize raw sensor data, object-level detections for
% each sensor, and tracks. 
viewer = HelperAutomotiveSpecTrackingViewer.createViewer...
    (radarSpec, lidarSpec, cameraSpec, sensorFlags);

% The loop is executed at every odometry update. This ensures ego motion is
% up to date. The tracks are updated when either sensor is reporting. In
% the example, we will run about 20 seconds of recorded data.
timestamps = highwayRecordedData.Odometry.Timestamps(1:2000);

% Last odometry data captured used to ego motion estimation
lastOdometry.Vehicle = highwayRecordedData.Odometry.Vehicle(1);
lastOdometry.Timestamp = highwayRecordedData.Odometry.Timestamps(1);

% Initialize ego motion
egoMotion = struct('Time',timestamps(1),...
    'EgoPositionalDisplacement',[0 0 0],...
    'EgoRotationalDisplacement',[0 0 0]);

% Loop over all tracker events
for i = 2:numel(timestamps)
    % Stream data since last timestamp
    sensorData = streamData(highwayRecordedData, timestamps(i-1), timestamps(i));
    
    % Update ego motion
    [egoMotion, lastOdometry] = estimateEgoMotion(egoMotion, lastOdometry, sensorData.Odometry);
    
    % Object detection on radar point cloud. Vehicle odometry is used to
    % filter static returns
    radarDetectionData = detectRadarClusters(radarSpec, sensorData.Radar, sensorData.Odometry);
    
    % Object detection on camera images using YOLO detection
    cameraDetectionData = detectImageBoundingBoxes(cameraSpec, sensorData.Camera);
    
    % Object detection on lidar point cloud using Voxel RCNN
    lidarDetectionData = detectLidarBoundingBoxes(lidarSpec, sensorData.Lidar);
    
    % Collect sensor data in the order of sensor specifications
    sensorDetectionData = {radarDetectionData, lidarDetectionData, cameraDetectionData};

    % Update tracker with sensors used and ego motion
    tracks = tracker(sensorDetectionData{sensorFlags}, egoMotion);
    
    % If the tracker was updated, reset ego motion with 0
    % displacement at latest detection time.
    egoMotion = resetEgoMotion(egoMotion, sensorDetectionData{sensorFlags});

    % Visualize the data and estimates using helper
    HelperAutomotiveSpecTrackingViewer.updateViewer...
        (viewer, tracks, radarSpec, lidarSpec, cameraSpec, ...
        sensorDetectionData, sensorData, sensorFlags);
end

Results

The animation below shows a brief snippet of the raw sensor data, object-level detections, and estimated tracks from the tracker in both bird's-eye view and projected on the camera. The red dots represent the radar point clouds, and the red circles indicate the corresponding clustered detections. The small gray dots show the lidar point cloud, and the boxes with triangular centers depict the lidar bounding boxes. The orange rectangles on the images represent the detected bounding boxes on the images. The green boxes indicate tracks in both views.

Notice that the tracker maintains a track on the vehicles, and the tracks align well with the data captured by the sensors. This shows that the tracker associates and fuses data accurately from all the sensors.

The tracker does report a few false positives beyond the road barriers on the left and right. Those are primarily due to ghost or multipath measurements reported by the radar sensor. For an example on how to filter multipath measurements, refer to Highway Vehicle Tracking with Multipath Radar Reflections.

HighwayTracking.gif

Summary

In this example, you learned how to use a task-oriented approach to configure a multi-object tracker based on the Joint Integrated Probabilistic Data Association (JIPDA) algorithm. You configured a tracker to fuse data from camera, radar, and lidar sensors to robustly track vehicles on a highway. Additionally, you gained insights into implementing a processing chain for high resolution sensors to adapt them for the JIPDA algorithm.

Supporting Functions

Radar Object Detection

function radarClusters = detectRadarClusters(radarSpec, currentRadarData, currentOdomData)
% This is a supporting function for the automotive tracking example for
% clustering radar point clouds. 

% The format of radar clusters defined by the sensor specification.
radarClusters = dataFormat(radarSpec);

if isempty(currentRadarData.Timestamps) || isempty(currentOdomData.Timestamps)
    radarClusters.Time = zeros(0,1);
    return;
end

% If detections are already loaded, just return them. Otherwise, use DBSCAN
% for clustering defined below.
if isfield(currentRadarData,'Detections')
    radarClusters = currentRadarData.Detections(end);
    return;
end

persistent dbscan

if isempty(dbscan)
    dbscan = clusterDBSCAN;
    dbscan.Epsilon = [5 2 10];
    dbscan.MinNumPoints = 2;
end


% Radar point cloud
radarPtCloud = currentRadarData.PointClouds(end);

% Compute sensor velocity in the global frame
sensorLoc = radarSpec.MountingLocation(:);
odomData = currentOdomData.Vehicle(end);
sensorVel = [odomData.Speed;0;0] + cross([0;0;odomData.YawRate],sensorLoc);

% Provide ego information on clusters for doppler compensation by the
% tracker.
radarClusters.EgoSpeed = odomData(end).Speed;
radarClusters.EgoAngularVelocity = [0 0 rad2deg(odomData(end).YawRate)];

% Compute doppler assuming stationary points
x = radarPtCloud.x';
y = radarPtCloud.y';
z = radarPtCloud.z';
pts = [x;y;z];
vel = repmat(-sensorVel,1,numel(x));
r = sqrt(x.^2 + y.^2 + z.^2);
staticrr = dot(pts,vel,1)./r;

% Compare with reported points
rr = radarPtCloud.radial_speed';

% Points with similar doppler are classified as static points and filtered
% out.
isStatic = abs(rr - staticrr) < 2.5;

% Dynamic points
x = x(~isStatic);
y = y(~isStatic);
z = z(~isStatic);
rr = rr(~isStatic);

% Clustering in Cartesian
[~,clusters] = dbscan(double([x;y;z]'));

% Spherical coordinates for final reporting
[az, el, r] = cart2sph(x, y, z);
az = rad2deg(az);
el = rad2deg(el);

% Unique and number of clusters
uqClusters = unique(clusters);
nClusters = numel(uqClusters);

% Assemble clusters in the format required by the tracker
nZeros = zeros(1,nClusters);
radarClusters.Time = radarPtCloud.Timestamp;
fields = {'Azimuth','Elevation','Range','RangeRate',...
    'AzimuthAccuracy','RangeAccuracy','ElevationAccuracy','RangeRateAccuracy'};
for i = 1:numel(fields)
    radarClusters.(fields{i}) = nZeros;
end

% Fill clustered detections into the output by taking their mean and
% accuracy per cluster.
for i = 1:nClusters
    inCluster = clusters == uqClusters(i);
    % Azimuth and error covariance
    azi = az(inCluster);
    azC = mean(azi);
    azCov = 4 + (azi - azC)*(azi - azC)';
    radarClusters.Azimuth(i) = azC;
    radarClusters.AzimuthAccuracy(i) = sqrt(azCov);
    
    % Elevation and error covariance
    eli = el(inCluster);
    elC = mean(eli);
    elCov = 10 + (eli - elC)*(eli - elC)';
    radarClusters.Elevation(i) = elC;
    radarClusters.ElevationAccuracy(i) = sqrt(elCov);
    
    % Range and error covariance
    ri = r(inCluster);
    rC = mean(ri);
    rCov = 1.25^2 + (ri - rC)*(ri - rC)';
    radarClusters.Range(i) = rC;
    radarClusters.RangeAccuracy(i) = sqrt(rCov);
    
    % Range-rate and error covariance
    rri = rr(inCluster);
    rrC = mean(rri);
    rrCov = 1.25^2 + (rri - rrC)*(rri - rrC)';
    radarClusters.RangeRate(i) = rrC;    
    radarClusters.RangeRateAccuracy(i) = sqrt(rrCov);
end

end

Lidar Object Detection

function lidarData = detectLidarBoundingBoxes(lidarData, currentLidarData)
% This is a supporting function for the automotive tracking example for
% bounding box detection on lidar point clouds. 

% The format of radar clusters defined by the sensor specification.
lidarData = dataFormat(lidarData);

if isempty(currentLidarData.Timestamps)
    lidarData.Time = zeros(0,1);
    return;
end

% If detections are already loaded, just return them. Otherwise, run a
% Voxel RCNN detector on it
if isfield(currentLidarData,'Detections')
    lidarData = currentLidarData.Detections(end);
    return;
end

% Load the detector and corresponding parameters. 
persistent detector params

if isempty(params)
    params = lidarParameters('HDL64E',1024);
    detector = voxelRCNNObjectDetector('kitti');
end

% Get the point cloud
ptCloud = currentLidarData.PointClouds{end};

% Organize the point cloud
ptCloud = pcorganize(ptCloud,params);

% Remove invalid points
ptCloud = removeInvalidPoints(ptCloud);

% Detect using Voxel RCNN detector
bboxes = detect(detector,ptCloud,'Threshold',0.5);

% Assemble on the lidar data format
lidarData.Time = currentLidarData.Timestamps(end);
lidarData.BoundingBox = double(bboxes');

end

Camera Object Detection

function cameraData = detectImageBoundingBoxes(cameraSpec, currentCameraData)
% This is a supporting function for the automotive tracking example for
% bounding box detection on camera images. 

% The format of camera bounding boxes defined by the sensor specification.
cameraData = dataFormat(cameraSpec);

if isempty(currentCameraData.Timestamps)
    cameraData.Time = zeros(0,1);
    return;
end

% If detections are already loaded, just return them. Otherwise, run a YOLO
% detector on the image. Images are assumed to be undistorted.
if isfield(currentCameraData,'Detections')
    cameraData = currentCameraData.Detections(end);
    return;
end

% Load YOLO detector 
persistent detector

if isempty(detector)
    % Create YOLOv4 Detector
    detector = yolov4ObjectDetector;
end

% Bounding box detection
img = currentCameraData.Images{end};
[boundingBoxes, ~, labels] = detect(detector, img, 'Threshold',0.7);

% Keep bounding boxes of cars and trucks
carTruckDetections = string(labels)=="car" | string(labels) == 'truck';
boundingBoxes = boundingBoxes(carTruckDetections,:);

% FIlter partially occluded boxes using a aspect ratio test
valid = boundingBoxes(:,3) > 0.75*boundingBoxes(:,4);
boundingBoxes = boundingBoxes(valid,:);

% Assemble camera data
cameraData.Time = currentCameraData.Timestamps(end);
cameraData.BoundingBox = double(boundingBoxes');

end

Ego Motion Estimation

function [egoMotion, lastOdometry] = estimateEgoMotion(egoMotion, lastOdometry, currentOdometry)

tLast = egoMotion.Time(end);
tCurrent = currentOdometry.Timestamps;
if tCurrent > tLast
    dT = tCurrent - tLast;
    egoMotion.Time = [egoMotion.Time tCurrent];
    s = lastOdometry.Vehicle.Speed;
    w = lastOdometry.Vehicle.YawRate;
    if abs(w) > 0
        dx = s/w*sin(w*dT);
        dy = s/w*(1 - cos(w*dT));
    else
        dx = s*dT;
        dy = 0;
    end
    egoMotion.EgoPositionalDisplacement(end+1,:) = [dx dy 0];
    egoMotion.EgoRotationalDisplacement(end+1,:) = [rad2deg(w*dT) 0 0];
end
lastOdometry = currentOdometry;
end

function egoMotion = resetEgoMotion(egoMotion, varargin)
t = 0;
for i = 1:numel(varargin)
    if ~isempty(varargin{i}.Time)
        t = max(t,varargin{i}.Time);
        egoMotion.Time = t;
        egoMotion.EgoPositionalDisplacement = [0 0 0];
        egoMotion.EgoRotationalDisplacement = [0 0 0];
    end
end
end

Data Streaming and Loading

function data = downloadDataset()
% Download dataset from supporting files
seqName = 'MultiSensorDrivingData_Seg7_Seq30';

datasetZipFile = matlab.internal.examples.downloadSupportFile('driving',['data/',seqName,'.zip']);
datasetFolder = fullfile(fileparts(datasetZipFile),seqName);
if ~exist(datasetFolder,'dir')
    unzip(datasetZipFile,datasetFolder);
end

% Path of the dataset
dataPath = fullfile(datasetFolder,'30');

% Load data into a struct
data = struct;

% Load radar data
data = loadRadarData(data, dataPath);

% Load camera data
data = loadCameraData(data, dataPath);

% Load lidar data
data = loadLidarData(data, dataPath);

% Load Vehicle Odometry Data
data = loadOdometryData(data, dataPath);
end

function data = loadRadarData(data, dataPath)
% Load radar data from a MAT file
s = load(fullfile(dataPath,'Radar','RadarData.mat'));

% Store it in the struct
data.Radar.PointClouds = [s.Data{:}];
data.Radar.Timestamps = cellfun(@(x)x.Timestamp, s.Data);

% If detections are present, load them too
detFileName = ['RadarDetectionsSeq',dataPath(end-1:end),'.mat'];
if exist(fullfile(pwd,detFileName),'file')
    s = load(fullfile(pwd,detFileName));
    data.Radar.Detections = s.RadarData;
end
end

function data = loadCameraData(data, dataPath)

% Path of camera data
cameraPath = fullfile(dataPath,'FrontCamera');

% Create camera d for undistorting the image. This must be done
% before object detection
k = [1176.29501,    0.     ,  520.32287;
    0      , 1176.28913,  389.60511;
    0.     ,    0.     ,    1     ];
d = [-0.21760;0.215736;0.001291;0.001232];

I = cameraIntrinsics([k(1,1) k(2,2)],...
    [k(1,3) k(2,3)],...
    [768 1024],'RadialDistortion',d(1:2),'TangentialDistortion',d(3:4));

% Loop over all images, undistort them, and store.
counter = 1;
recordedCameraData = cell(0,1);
while exist(fullfile(cameraPath,num2str(counter,"%04d.jpeg")),'file')
    im = imread(fullfile(cameraPath,num2str(counter,"%04d.jpeg")));
    im = undistortImage(im, I);
    recordedCameraData{counter,1} = im;
    counter = counter + 1;
end

% Store it in the struct
s = load(fullfile(cameraPath,'Timestamps.mat'));
data.Camera.Images = recordedCameraData;
data.Camera.Timestamps = s.Timestamps;

% If detections are present, load them too
detFileName = ['CameraDetectionsSeq',dataPath(end-1:end),'.mat'];
if exist(fullfile(pwd,detFileName),'file')
    s = load(fullfile(pwd,detFileName));
   data.Camera.Detections = s.CameraData;
end

end

function data = loadLidarData(data, dataPath)
% Path of LS lidar
lidarPath = fullfile(dataPath,'Lidar2');

% Lidar axes is rotated by 90, make X forward to facilitate pre-trained
% deep learning detectors.
tform = rigidtform3d([0 0 -90],[0 0 0]);

% Loop over each file and load the data
counter = 1;
recordedLidarData = cell(0,1);
while exist(fullfile(lidarPath,num2str(counter,"%03d.pcd")),'file')
    pc = pcread(fullfile(lidarPath,num2str(counter,"%03d.pcd")));
    pc = pctransform(pc,tform);
    recordedLidarData{counter,1} = pc;
    counter = counter + 1;
end

% Store it in the struct
s = load(fullfile(lidarPath,'Timestamps.mat'));
data.Lidar.PointClouds = recordedLidarData;
data.Lidar.Timestamps = s.Timestamps;

% If detections are present, load them too
detFileName = ['LidarDetectionsSeq',dataPath(end-1:end),'.mat'];
if exist(fullfile(pwd,detFileName),'file')
    s = load(fullfile(pwd,detFileName));
   data.Lidar.Detections = s.LidarData;
end
end

function data = loadOdometryData(data, dataPath)
odomPath = fullfile(dataPath,'VehicleOdometry','VehicleOdometry.mat');
s = load(odomPath);
data.Odometry.Vehicle = [s.odometry{:}];
data.Odometry.Timestamps = cellfun(@(x)x.Timestamp,s.odometry);
end

function currentData = streamData(data, t1, t2)
    % Radar data
    t = data.Radar.Timestamps;
    valid = t > t1 & t <= t2;
    currentData.Radar.PointClouds = data.Radar.PointClouds(valid);
    currentData.Radar.Timestamps = data.Radar.Timestamps(valid);
    if isfield(data.Radar,'Detections')
        currentData.Radar.Detections = data.Radar.Detections(valid);
    end

    % Lidar data
    t = data.Lidar.Timestamps;
    valid = t > t1 & t <= t2;
    currentData.Lidar.PointClouds = data.Lidar.PointClouds(valid);
    currentData.Lidar.Timestamps = data.Lidar.Timestamps(valid);
    if isfield(data.Lidar,'Detections')
        currentData.Lidar.Detections = data.Lidar.Detections(valid);
    end

    % Camera data
    t = data.Camera.Timestamps;
    valid = t > t1 & t <= t2;
    currentData.Camera.Images = data.Camera.Images(valid);
    currentData.Camera.Timestamps = data.Camera.Timestamps(valid);
    if isfield(data.Camera,'Detections')
        currentData.Camera.Detections = data.Camera.Detections(valid);
    end

    % Vehicle odometry data
    t = data.Odometry.Timestamps;
    valid = t > t1 & t <= t2;
    currentData.Odometry.Vehicle = data.Odometry.Vehicle(valid);
    currentData.Odometry.Timestamps = data.Odometry.Timestamps(valid);
end