Highway Vehicle Tracking Using Multi-Sensor Data Fusion
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 AutomotiveRadarClusteredPoints
documentation.
% 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.
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