classdef MultiSignalVehicleDetector < vision.labeler.AutomationAlgorithm 
    % MultiSignalVehicleDetector Automation algorithm to detect
    % vehicles in images and estimating them in point cloud.
    %   MultiSignalVehicleDetector is an automation algorithm for
    %   detecting vehicles in images using ACF Vehicle Detector and
    %   estimating 3-D bounding boxes in the corresponding point cloud
    %   using camera-lidar calibration parameters.

    %   See also groundTruthLabeler, lidarLabeler
    %   vision.labeler.AutomationAlgorithm.
    
    %   Copyright 2020 The MathWorks, Inc.
    
    % ----------------------------------------------------------------------
    % Step 1: Define the properties required for describing the algorithm,
    % which include Name, Description, and UserDirections.
    properties(Constant)
        
        % Name Algorithm name
        %   Character vector specifying the name of the algorithm.
        Name = 'Multisignal Vehicle Detector';
        
        % Description Algorithm description
        %   Character vector specifying the short description of the algorithm.
        Description = ['Detect vehicles using ACF Vehicle Detector in ' ...
            'image and estimate them in point cloud.'];
        
        % UserDirections Algorithm usage directions
        %   Cell array of character vectors specifying directions for
        %   algorithm users to follow.
        UserDirections = {['Select one of the rectangle ROI labels to ' ...
            'label objects as Vehicle.'], ...
            ['Click Settings and on the Lidar Camera Calibration ' ...
            'Parameters tab, load the cameraIntrinsics and rigid3d ' ...
            'objects from the workspace.'], ...
            ['Specify additional parameters under Settings.'], ...
            ['Click Run to detect vehicles in each image and point cloud.'], ...
            ['Review automated labels manually. You can modify, delete ', ...
            'and add new labels.'], ...
            ['If you are not satisfied with the results, click Undo ' ...
            'Run. Click Settings to modify algorithm settings and click ', ...
            'Run again.'] ...
            ['When you are satisfied with the results, click Accept and ', ...
            'return to manual labeling.']};
    end

    % ---------------------------------------------------------------------
    % Step 2: Define properties to be used to manage algorithm execution.
    properties
        
        % SelectedLabelName Selected label name
        %   Name of the selected label. Vehicles detected by the algorithm 
        %   will be assigned this variable name.
        SelectedLabelName
        
        % Detector Detector
        %   Pretrained vehicle detector, an object of class
        %   acfObjectDetector.
        Detector        
        
        % VehicleModelName Vehicle detector model name
        %   Name of pretrained vehicle detector model.
        VehicleModelName = 'full-view';
        
        % OverlapThreshold Overlap threshold
        %   Threshold value used to eliminate overlapping bounding boxes
        %   around the reference bounding box, between 0 and 1. The
        %   bounding box overlap ratio denominator, 'RatioType', is set to
        %   'Min'.
        OverlapThreshold = 0.45;
        
        % ScoreThreshold Classification score threshold
        %   Threshold value used to reject detections with low detection
        %   scores.
        ScoreThreshold = 20;
        
        % ConfigureDetector Detection configuration flag
        %   Boolean value that determines whether the detector is
        %   configured using monoCamera sensor.
        ConfigureDetector = false;
        
        % SensorObj monoCamera sensor
        %   Monocular camera sensor object, monoCamera, used to configure
        %   the detector. A configured detector runs faster and can 
        %   potentially result in better detections.
        SensorObj = [];
        
        % SensorStr monoCamera sensor variable name
        %   Character vector specifying the monoCamera object variable name
        %   used to configure the detector.
        SensorStr = '';
        
        % VehicleWidth Vehicle width
        %   Vehicle width used to configure the detector, specified as
        %   [minWidth, maxWidth], which describes the approximate width of
        %   the object in world units.
        VehicleWidth = [1.5 2.5];
        
        % VehicleLength Vehicle length
        %   Vehicle length used to configure the detector, specified as
        %   [minLength, maxLength] vector, which describes the approximate
        %   length of the object in world units.
        VehicleLength = [];  
        
        % IntrinsicsObj Camera intrinsics
        %   cameraIntrinsics object, which represents a projective
        %   transformation from camera to image coordinates.
        IntrinsicsObj = [];
        
        % IntrinsicsStr cameraIntrinsics variable name
        %   cameraIntrinsics object variable name.
        IntrinsicsStr = '';
        
        % ExtrinsicsObj Camera-to-lidar rigid transformation
        %   rigid3d object representing the 3-D rigid geometric transformation 
        %   from the camera to the lidar.
        ExtrinsicsObj = [];
        
        % ExtrinsicsStr rigid3d variable name
        %   Camera-to-lidar rigid3d object variable name.
        ExtrinsicsStr = '';
        
        % ClusterThreshold Clustering threshold for two adjacent points
        %   Threshold specifying the maximum distance between two adjacent points
        %   for those points to belong to the same cluster.
        ClusterThreshold = 1;
        
    end
    
    % ----------------------------------------------------------------------
    % Step 3: Define methods used for setting up the algorithm. 
    methods 
        function this = MultiSignalVehicleDetector()
            requiresLIDARToolbox()
        end
    end
    
    methods(Static)
        function success = supportsMultisignalAutomation(~)
            % Supports MultiSignal.
            success = true;
        end
        
        function isValid = checkSignalType(signalType)
            % Only video/image sequence and point cloud signal data 
            % is valid.
            isValid = any(signalType == vision.labeler.loading.SignalType.Image) && ...
               any(signalType == vision.labeler.loading.SignalType.PointCloud);  
        end
    end
    
    methods
        function isValid = checkLabelDefinition(~, labelDef)            
            % Only Rectangular/Cuboid ROI Label definitions are valid for the
            % Vehicle Detector.
            isValid = (labelDef.Type == labelType.Cuboid || labelDef.Type == labelType.Rectangle);          
        end
        
        function isReady = checkSetup(algObj, ~)
            % Is there one selected ROI Label definition to automate?
            isReady = ~isempty(algObj.SelectedLabelDefinitions);
        end

        function settingsDialog(algObj)
            % Invoke dialog box to input camera intrinsics and
            % camera-to-lidar rigid transformation and options for choosing
            % a pretrained model, overlap threshold, detection score
            % threshold, and clustering threshold. Optionally, input a
            % calibrated monoCamera sensor to configure the detector.
            multiSignalVehicleDetectorSettings(algObj)
        end
    end
    
    % ----------------------------------------------------------------------
    % Algorithm execution. 
    methods

        function initialize(algObj, ~)
            
            % Store the name of the selected label definition. Use this
            % name to label the detected vehicles.
            algObj.SelectedLabelName = algObj.SelectedLabelDefinitions.Name;
            
            % Initialize the vehicle detector with a pretrained model.
            algObj.Detector = vehicleDetectorACF(algObj.VehicleModelName);
        end       
        
        function autoLabels = run(algObj, I)
            % autoLabels a cell array of length the same as the number of
            % signals.
            autoLabels = cell(size(I,1),1);
            
            % Get the index of Image and PointCloud frames.
            if isa(I{1,1},"pointCloud")
                pcIdx = 1;
                imIdx = 2;
            else
                imIdx = 1;
                pcIdx = 2;
            end
            
            % Detect bounding boxes on image frame.
            selectedBboxes = detectVehicle(algObj, I{imIdx,1});
            
            % Estimate bounding boxes on point cloud frame.
            if ~isempty(selectedBboxes)
                
                % Store labels from the image. 
                imageLabels = struct('Type', labelType.Rectangle, ...
                'Name', algObj.SelectedLabelDefinitions.Name, ...
                'Position', selectedBboxes);
                autoLabels{imIdx, 1} = imageLabels;
                
                % Remove the ground plane for the point cloud.
                groundPtsIndex = segmentGroundFromLidarData(I{pcIdx,1}, ...
                    "ElevationAngleDelta", 15, "InitialElevationAngle", 10);
                nonGroundPts = select(I{pcIdx,1}, ~groundPtsIndex);
                
                % Predict 3-D bounding boxes.
                pcBboxes = bboxCameraToLidar(selectedBboxes, nonGroundPts, algObj.IntrinsicsObj, ...
                    algObj.ExtrinsicsObj, "ClusterThreshold", algObj.ClusterThreshold);
                
                % Store labels from the point cloud.
                if(~isempty(pcBboxes))
                    pcLabels = struct('Type', labelType.Cuboid, ...
                    'Name', algObj.SelectedLabelDefinitions.Name, ...
                    'Position', pcBboxes);
                    autoLabels{pcIdx, 1} = pcLabels;
                else
                    autoLabels{pcIdx, 1} = {};
                end
            else
                autoLabels{imIdx, 1} = {};
                autoLabels{pcIdx, 1} = {};
            end                           
        end        
    end
    % ----------------------------------------------------------------------
    % Private
    % ----------------------------------------------------------------------
    
    methods(Access = private)
        
        function selectedBbox = detectVehicle(algObj,I)
            % Configure the detector.
            if algObj.ConfigureDetector && ~isa(algObj.Detector,'acfObjectDetectorMonoCamera')
                vehicleSize = [algObj.VehicleWidth;algObj.VehicleLength];
                algObj.Detector = configureDetectorMonoCamera(algObj.Detector, algObj.SensorObj, vehicleSize);                
            end
            
            % Detect vehicles using the initialized vehicle detector.
            [bboxes, scores] = detect(algObj.Detector, I, ...
                'SelectStrongest', false);
            
            [selectedBbox, selectedScore] = selectStrongestBbox(bboxes, scores, ...
                'RatioType', 'Min', 'OverlapThreshold', algObj.OverlapThreshold);
            
            % Reject detections with detection score lower than
            % ScoreThreshold.
            detectionsToKeepIdx = (selectedScore > algObj.ScoreThreshold);
            selectedBbox = selectedBbox(detectionsToKeepIdx,:);
        end
        
        function multiSignalVehicleDetectorSettings(algObj)
            % no calibration variable selected by default.
            importSensorDlgCanceled = true; 
             
            % Assign default values.
            modelName           = algObj.VehicleModelName;
            overlapThresh       = algObj.OverlapThreshold;
            scoreThresh         = algObj.ScoreThreshold;
            configureDetector   = algObj.ConfigureDetector;
            intrinsicsObj       = algObj.IntrinsicsObj;
            intrinsicsStr       = algObj.IntrinsicsStr;
            extrinsicsObj       = algObj.ExtrinsicsObj;
            extrinsicsStr       = algObj.ExtrinsicsStr;
            clusterThresh       = algObj.ClusterThreshold;
            sensorObj           = algObj.SensorObj;
            sensorStr           = algObj.SensorStr;
            vehicleWidth        = algObj.VehicleWidth;
            vehicleLength       = algObj.VehicleLength;
            
            % Position the modal dialog in the center of the screen.
            dlgSize = [600 500];
            monitorPos = get(0, 'MonitorPositions');
            monitorPos = monitorPos(1,:);
            center = monitorPos(1:2)+[monitorPos(3), monitorPos(4)]/2;
            dlgPos = round([center-dlgSize/2 dlgSize]);
            
            dlgName = sprintf('%s %s', algObj.Name, ...
                vision.getMessage('vision:labeler:Settings'));
            parentObj = dialog('Position',dlgPos, ...
                'Name', dlgName);
            
            % Settings panel Tabs
            tabGroupPos = [0.03 0.15 0.94 0.74]; 
            % keep gap of 0.03 or right and left end and 0.09 (OK/Cancel
            % buttons at bottom and user direction written in top)
            ParentTabGroup = uitabgroup(parentObj, ...
                'Position', tabGroupPos, ...
                'Units', 'normalized', ...
                'HandleVisibility', 'callback', ...
                'TabLocation', 'top');
            
            % Tab 1 panel.
            LaneBoundaryTab1Title = 'ACF Vehicle Detector Parameters';
            detectorTab = uitab(ParentTabGroup, ...
                            'Title', LaneBoundaryTab1Title, ...
                            'ForegroundColor', 'k', ...
                            'Units', 'normalized');
            % Tab 2 panel.
            LaneBoundaryTab2Title = 'Lidar-to-Camera Calibration Parameters';
            cameraParamsTab = uitab(ParentTabGroup, ...
                            'Title', LaneBoundaryTab2Title, ...
                            'ForegroundColor', 'k', ...
                            'Units', 'normalized');
                        
            % Settings user direction.
            settingsTipPanelPos = [0.03 0.89 0.94 0.08];
            settingsTipTextPanel = uipanel('Parent', parentObj, ...
                'Units', 'normalized', ...
                'Position', settingsTipPanelPos, ...
                'HandleVisibility', 'callback', ...
                'BorderType', 'none', ...
                'ForegroundColor', [1 0 0]);
            
            % Create panels to add uicontrols to the dialog.
            panelX = 0.02;
            panelWidth = 0.4;
            panelWidthOffset = 0.05;           
            panelPos = [panelX 0.03 1 0.1];
            buttonPanel = uipanel('Parent', parentObj, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth 0.3 0.7 0.1];
            clusterThresholdEditPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.27 panelWidth 0.1];
            clusterThresholdTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.51 panelWidth+panelWidthOffset 0.2];
            importExtrinsicsTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.51 0.7 0.1];
            importExtrinsicsButtonPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.4 0.96 0.1];
            importExtrinsicsHelpTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.70 panelWidth+panelWidthOffset 0.2];
            importIntrinsicsTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.7 0.7 0.1];
            importIntrinsicsButtonPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.6 0.96 0.1];
            importIntrinsicsHelpTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.82 0.97 0.13];
            importCameraParametersTextPanel = uipanel('Parent', cameraParamsTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth 0.06 1 0.1];
            vehicleLengthEditPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.05 panelWidth 0.1];
            vehicleLengthTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth 0.21 1 0.1];
            vehicleWidthEditPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.19 panelWidth 0.25];
            vehicleWidthTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth + panelWidthOffset 0.33 panelWidth+panelWidthOffset 0.2];
            importSensorTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.32 0.57 + panelWidthOffset 0.2];
            importSensorButtonPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.45 1 0.1];
            calibrateCheckBoxPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            
            panelPos = [panelX + panelWidth 0.59 1 0.1];
            classifyThreshEditPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.58 panelWidth 0.25];
            classifyThreshTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth 0.72 1 0.1];
            overlapThreshEditPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.71 panelWidth 0.25];
            overlapThreshTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX + panelWidth 0.85 0.57 0.2];
            modelPopupPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            panelPos = [panelX 0.84 panelWidth 0.2];
            modelTextPanel = uipanel('Parent', detectorTab, ...
                'Units', 'normalized', ...
                'Position', panelPos, ...
                'HandleVisibility','callback', ...
                'BorderType', 'none');
            
            % Set settings panel text.
            textPos = [0.01 0.01 0.98 0.98];            
            settingsTipText = uicontrol('Parent', settingsTipTextPanel, ...
                'Style', 'text', ...
                'HorizontalAlignment', 'left', ...
                'Units', 'normalized', ...
                'FontWeight', 'normal', ...
                'Position', textPos, ...
                'HandleVisibility', 'callback', ...
                'String', ['Configure the multisignal vehicle detector by modifying various ' ...
                    'calibration parameters for detecting and estimating vehicle positions.']);
            
            % Set lidar-to-camera calibration parameters panel text.
            textPosEdit = textPos;
            textPosEdit(2) = 0.02;
            textPosEdit(3) = 0.96;
            uicontrol('Parent',importCameraParametersTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','normalized', ...
                'Position',textPosEdit, ...
                'HandleVisibility','callback', ...
                'String',['Import lidar-to-camera calibration parameters required to estimate ' ...
                    '3-D bounding boxes in point clouds from 2-D bounding boxes in images.']);
            
            % 'Set clustering threshold' text
            textPosEdit = textPos;
            textPosEdit(1) = 0.02;
            uicontrol('Parent',clusterThresholdTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','normalized', ...
                'Position',textPosEdit, ...
                'HandleVisibility','callback', ...
                'TooltipString', ['Set threshold for clustering. Increase threshold to get larger ' ...
                    '3-D bounding boxes.'], ...
                'String','Clustering threshold');
            
            % Set clustering threshold edit box
            textPosEdit = textPos;
            textPosEdit(3) = 10;
            textPosEdit(4) = 2;
            uicontrol('Parent',clusterThresholdEditPanel, ...
                'Style','edit', ...
                'Units','characters', ...
                'Position',textPosEdit, ...
                'String',clusterThresh, ...
                'HandleVisibility','callback', ...
                'Callback', @validateClusteringThreshold, ...
                'Tag','clusterThresholdVal');
            
            function validateClusteringThreshold(src,~)
                
                cluster = str2num(src.String); %#ok<ST2NM>
                
                % Overwrite users classification score if they gave a bad
                % value.
                if isempty(cluster) || ~isreal(cluster) || cluster < 1
                    cluster = algObj.ClusterThreshold;
                end
                
                if ~isscalar(cluster)
                    cluster = algObj.ClusterThreshold(1);
                end
                
                src.String = num2str(cluster);
            end
            
            % Set import extrinsics panel text.
            strPosText = textPos;
            strPosText(2) = 0;
            strPosText(3) = 0.97;
            uicontrol('Parent',importExtrinsicsHelpTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','normalized', ...
                'Position',strPosText, ...
                'HandleVisibility','callback', ...
                'String',['Note: Use 3-D rigid geometric transformation – can be obtained from rigid3d(T) ' ...
                    'only, where T is a transformation matrix.']);
            
            % Import extrinsics from workspace push button.
            hExtrinsicsImportButton = uicontrol('Parent', importExtrinsicsButtonPanel, ...
                'Style','pushbutton', ...
                'Callback', @doImportExtrinsics, ...
                'Units', 'normalized', ...
                'Position', [0.02 0.01 0.96 0.8], ...
                'HorizontalAlignment', 'left', ...
                'TooltipString', 'Import camera-to-lidar transformation as a rigid3d object.', ...
                'String', 'Import camera-to-lidar transformation from workspace', ...
                'HandleVisibility','callback', ...
                'Enable','on', ...
                'Tag','ExtrinsicsImportButton');
            
            function doImportExtrinsics(varargin)
                variableTypes = {'rigid3d'};
                variableDisp =  {'camera-to-lidar transformation'};
                [extrinsicsObj,extrinsicsObjStr,importextrinsicsDlgCanceled] = vision.internal.uitools.getVariablesFromWS(variableTypes, variableDisp);
                if (~importextrinsicsDlgCanceled)
                    hImportExtrinsicsText.String = extrinsicsObjStr;
                end                
            end
            
            % Import extrinsics variable name
            strPosText = textPos;
            strPosText(1) = strPosText(1)+4;
            strPosText(3) = 100;
            hImportExtrinsicsText = uicontrol('Parent',importExtrinsicsTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'FontWeight','bold', ...
                'Enable','off', ... 
                'HandleVisibility','callback', ...
                'String',extrinsicsStr, ...
                'Tag','extrinsicsText');
            
            % Set import camera parameters panel text.
            strPosText = textPos;
            strPosText(2) = 0;
            strPosText(3) = 0.97;
            uicontrol('Parent',importIntrinsicsHelpTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','normalized', ...
                'Position',strPosText, ...
                'HandleVisibility','callback', ...
                'String',['Note: Use cameraIntrinsics object – can be obtained by cameraIntrinsics([fx, fy], ' ...
                '[cx, cy],imageSize) only.']);
            
            % Import cameraIntrinsics from workspace push button.
            hIntrinsicsImportButton = uicontrol('Parent', importIntrinsicsButtonPanel, ...
                'Style','pushbutton', ...
                'Callback', @doImportIntrinsics, ...
                'Units', 'normalized', ...
                'Position', [0.02 0.1 0.96 0.8], ...
                'HorizontalAlignment', 'left', ...
                'TooltipString', 'Import intrinsic camera calibration parameters as a cameraIntrinsics object.', ...
                'String', 'Import camera intrinsics from workspace', ...
                'HandleVisibility','callback', ...
                'Enable','on', ...
                'Tag','IntrinsicsImportButton');
            
            function doImportIntrinsics(varargin)
                variableTypes = {'cameraIntrinsics'};
                variableDisp =  {'camera intrinsics'};
                [intrinsicsObj,intrinsicsObjStr,importIntrinsicsDlgCanceled] = vision.internal.uitools.getVariablesFromWS(variableTypes, variableDisp);
                if (~importIntrinsicsDlgCanceled)
                    hImportIntrinsicsText.String = intrinsicsObjStr;
                end                
            end
            
            % Import intrinsics variable name.
            strPosText = textPos;
            strPosText(1) = strPosText(1)+4;
            strPosText(3) = 100;
            hImportIntrinsicsText = uicontrol('Parent',importIntrinsicsTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'FontWeight','bold', ...
                'Enable','off', ... 
                'HandleVisibility','callback', ...
                'String',intrinsicsStr, ...
                'Tag','intrinsicsText');            
            
            modelPromptStr  = vision.getMessage('vision:labeler:ACFModelName');
            modelPromptStrWidth = numel(modelPromptStr);
            textPos = [2 0 modelPromptStrWidth+4 2];
            % Model name prompt.
            uicontrol('Parent',modelTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',textPos, ...
                'TooltipString', vision.getMessage('vision:labeler:ModelNameTooltip'), ...
                'HandleVisibility','callback', ...
                'String',modelPromptStr);
            
            % Model name popup.
            popupTextPos = textPos;
            popupTextPos(3) = popupTextPos(3)-2;
            validModelNames = {'full-view';'front-rear-view'};
            modelNamePopup = uicontrol('Parent',modelPopupPanel, ...
                'Style','popup', ...
                'String',validModelNames, ...
                'Units','characters', ...
                'Position',popupTextPos, ...
                'Callback',@modelPopupCallback, ...
                'HandleVisibility','callback', ...
                'Tag','modelName');
            
            % Set popup menu value to previously selected or default model
            % name.
            [~,indx] = ismember(validModelNames,modelName);
            modelNamePopup.Value = find(indx==1);
            
            function modelPopupCallback(~,~)
                % Enable vehicle length input only for full-view models.
                idx = modelNamePopup.Value;
                popupItems = modelNamePopup.String;
                modelNameSelect = char(popupItems(idx,:));
                
                if strcmpi(modelNameSelect,'full-view') && ...
                        calibrateCheckBox.Value == 1
                    hVehicleLengthText.Enable = 'on';
                    hVehicleLengthEdit.Enable = 'on';
                else
                    hVehicleLengthText.Enable = 'off';
                    hVehicleLengthEdit.Enable = 'off';
                end
            end
            
            % Calibration check box.
            strPosText = textPos;
            strPosText(3) = 100;
            calibrateCheckBox = uicontrol('Parent',calibrateCheckBoxPanel, ...
                'Style','checkbox', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'Callback',@calibrationCheckBoxCallback, ...
                'Value',configureDetector, ...
                'TooltipString', vision.getMessage('vision:labeler:ConfigureDetectorTooltip'), ...
                'HandleVisibility','callback', ...
                'String',vision.getMessage('vision:labeler:ConfigureWithSensor'), ...
                'Tag','checkBoxVal');            
 
            function calibrationCheckBoxCallback(~,~)
                % Enable calibration options only when checkbox is checked.
                if calibrateCheckBox.Value == 1
                    hImportSensorButton.Enable = 'on';
                    hVehicleWidthText.Enable = 'on';
                    hVehicleWidthEdit.Enable = 'on';
                    hImportSensorText.Enable = 'on';
                    if ~isempty(strtrim(hImportSensorText.String)) && ...
                            ~isempty(strtrim(hVehicleWidthEdit.String))
                        hOkButton.Enable = 'on';
                    else
                        hOkButton.Enable = 'off';
                    end
                    
                    % Enable vehicle length input only for full-view
                    % models.
                    idx = modelNamePopup.Value;
                    popupItems = modelNamePopup.String;
                    modelNameSelect = char(popupItems(idx,:));
                    
                    if strcmpi(modelNameSelect,'full-view')
                        hVehicleLengthText.Enable = 'on';
                        hVehicleLengthEdit.Enable = 'on';
                    else
                        hVehicleLengthText.Enable = 'off';
                        hVehicleLengthEdit.Enable = 'off';
                    end
                else
                    hImportSensorButton.Enable = 'off';
                    hVehicleWidthText.Enable = 'off';
                    hVehicleWidthEdit.Enable = 'off';
                    hVehicleLengthText.Enable = 'off';
                    hVehicleLengthEdit.Enable = 'off';
                    hImportSensorText.Enable = 'off';
                    hOkButton.Enable = 'on';
                end
            end
                                        
            % Import sensor from workspace push button
            hImportSensorButton = uicontrol('Parent', importSensorButtonPanel, ...
                'Style','pushbutton', ...
                'Callback', @doImportSensor, ...
                'Units', 'normalized', ...
                'Position', [0.028 0.1 0.70 0.4], ...
                'HorizontalAlignment', 'left', ...
                'TooltipString', vision.getMessage('vision:labeler:ImportMonoCameraTooltip'), ...
                'String', vision.getMessage('vision:labeler:ImportSensorFromWS'), ...
                'HandleVisibility','callback', ...
                'Enable','off', ...
                'Tag','ImportButton');
            
            function doImportSensor(varargin)
                variableTypes = {'monoCamera'};
                variableDisp =  {'Monocular Camera Sensor'};
                [sensorObj,sensorObjStr,importSensorDlgCanceled] = vision.internal.uitools.getVariablesFromWS(variableTypes, variableDisp);
                if (~importSensorDlgCanceled)
                    hImportSensorText.String = sensorObjStr;
                    if ~isempty(strtrim(hVehicleWidthEdit.String))
                        % Enable OK button only after all inputs are set.
                        hOkButton.Enable = 'on';
                    end
                end                
            end
            
            % Import sensor variable name
            strPosText = textPos;
            strPosText(1) = strPosText(1)+4;
            strPosText(3) = 100;
            hImportSensorText = uicontrol('Parent',importSensorTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'FontWeight','bold', ...
                'Enable','off', ... 
                'HandleVisibility','callback', ...
                'String',sensorStr, ...
                'Tag','sensorText');
            
            % Set vehicle length range text
            strPosText = textPos;
            strPosText(1) = strPosText(1);
            strPosText(3) = 100;
            hVehicleLengthText = uicontrol('Parent',vehicleLengthTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'Enable','off', ...
                'HandleVisibility','callback', ...
                'TooltipString', vision.getMessage('vision:labeler:VehicleLengthRangeTooltip'), ...                
                'String',vision.getMessage('vision:labeler:SensorVehicleLength'));
            
            % Set Vehicle length edit box
            textPosEdit = textPos;
            textPosEdit(1) = textPosEdit(1)+4;
            textPosEdit(3) = 15; 
            if ~isempty(vehicleLength)
                vehicleLengthStr = ['[' num2str(vehicleLength(1)) '   ' num2str(vehicleLength(2)) ']'];
            else
                vehicleLengthStr = '[]';
            end
            hVehicleLengthEdit = uicontrol('Parent',vehicleLengthEditPanel, ...
                'Style','edit', ...
                'Units','characters', ...
                'Position',textPosEdit, ...
                'String', vehicleLengthStr, ...
                'Enable','off', ...
                'HandleVisibility','callback', ...
                'Tag','vehicleLengthVal');
            
            % 'Set vehicle width range' text
            strPosText = textPos;
            strPosText(1) = strPosText(1);
            strPosText(3) = 100;
            hVehicleWidthText = uicontrol('Parent',vehicleWidthTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'Enable','off', ...
                'HandleVisibility','callback', ...
                'TooltipString', vision.getMessage('vision:labeler:VehicleWidthRangeTooltip'), ...                
                'String',vision.getMessage('vision:labeler:SensorVehicleWidth'));
            
            % Set classification score threshold edit box
            textPosEdit = textPos;
            textPosEdit(1) = textPosEdit(1)+4;
            textPosEdit(3) = 15;            
            hVehicleWidthEdit = uicontrol('Parent',vehicleWidthEditPanel, ...
                'Style','edit', ...
                'Units','characters', ...
                'Position',textPosEdit, ...
                'String',['[' num2str(vehicleWidth(1)) '   ' num2str(vehicleWidth(2)) ']'] , ...
                'Enable','off', ...
                'HandleVisibility','callback', ...
                'Tag','vehicleWidthVal');
            
            % 'Set overlap threshold' text
            uicontrol('Parent',overlapThreshTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',textPos, ...
                'HandleVisibility','callback', ...
                'TooltipString', vision.getMessage('vision:labeler:OverlapThresholdTooltip'), ...
                'String',vision.getMessage('vision:labeler:ACFOverlapThreshold'));
            
            % Set overlap threshold edit box
            textPosEdit = textPos;
            textPosEdit(3) = 10;
            uicontrol('Parent',overlapThreshEditPanel, ...
                'Style','edit', ...
                'Units','characters', ...
                'Position',textPosEdit, ...
                'String',overlapThresh, ...
                'HandleVisibility','callback', ...
                'Callback', @validateOverlap, ...
                'Tag','thresholdVal');
            
            function validateOverlap(src, ~)
                
                overlapThreshold = str2num(src.String); %#ok<ST2NM>
                
                % Overwrite users overlap threshold if they gave a bad
                % value.
                if isempty(overlapThreshold) || ~isreal(overlapThreshold) || overlapThreshold < 0 || overlapThreshold > 1
                    overlapThreshold = algObj.OverlapThreshold;
                end
                
                if ~isscalar(overlapThreshold)
                    overlapThreshold = overlapThreshold(1);
                end
                
                overlapThreshold = max(min(overlapThreshold,1),0);
                
                src.String = num2str(overlapThreshold);
            end
            
            % 'Set classification score threshold' text
            strPosText = textPos;
            strPosText(3) = 100;
            uicontrol('Parent',classifyThreshTextPanel, ...
                'Style','text', ...
                'HorizontalAlignment','left', ...
                'Units','characters', ...
                'Position',strPosText, ...
                'HandleVisibility','callback', ...
                'TooltipString', vision.getMessage('vision:labeler:ScoreThresholdTooltip'), ...
                'String',vision.getMessage('vision:labeler:ACFClassifyScoreThreshold'));
            
            % Set classification score threshold edit box
            textPosEdit = textPos;
            textPosEdit(3) = 10;
            uicontrol('Parent',classifyThreshEditPanel, ...
                'Style','edit', ...
                'Units','characters', ...
                'Position',textPosEdit, ...
                'String',scoreThresh, ...
                'HandleVisibility','callback', ...
                'Callback', @validateClassificationScore, ...
                'Tag','scoreThresholdVal');
            
            function validateClassificationScore(src,~)
                
                score = str2num(src.String); %#ok<ST2NM>
                
                % Overwrite users classification score if they gave a bad
                % value.
                if isempty(score) || ~isreal(score) 
                    score = algObj.ScoreThreshold;
                end
                
                if ~isscalar(score)
                    score = algObj.ScoreThreshold(1);
                end
                
                src.String = num2str(score);
            end
            
            % Ok and cancel buttons
            btnText = vision.getMessage('vision:labeler:SettingsCancelButton');
            btnTextWidth = numel(btnText)+5;
            % OK
            okButtonPos = [30 0.6 btnTextWidth 2];
            hOkButton = uicontrol('Parent',buttonPanel, ...
                'Units','characters', ...
                'Position',okButtonPos, ...
                'HandleVisibility','callback', ...
                'String',vision.getMessage('vision:labeler:SettingsOKButton'), ...
                'Callback',@okButtonCallback);
            
            function okButtonCallback(okBtn,~)
                % Set model name
                popup = findobj(ancestor(okBtn,'figure'),'Tag','modelName');
                idx = popup.Value;
                popupItems = popup.String;
                modelName = char(popupItems(idx,:));
                
                % Set confidence threshold
                threshEditBox = findobj(ancestor(okBtn,'figure'),'Tag','thresholdVal');
                overlapThresh = str2double(threshEditBox.String);
                                
                % Set score threshold
                scoreThreshEditBox = findobj(ancestor(okBtn,'figure'),'Tag','scoreThresholdVal');
                scoreThresh = str2double(scoreThreshEditBox.String);
                
                % Set vehicle width
                vehicleWidthEditBox = findobj(ancestor(okBtn,'figure'),'Tag','vehicleWidthVal');
                try
                    vehicleWidthStr = vehicleWidthEditBox.String;
                    vehicleWidth = eval(vehicleWidthStr);
                    
                    validateattributes(vehicleWidth, {'single', 'double'}, ...
                        {'real', 'nonsparse', 'finite', 'vector', 'increasing', 'numel', 2}, mfilename);
                    
                catch ME
                    errordlg(ME.message, ...
                        vision.getMessage('vision:labeler:InvalidVehicleWidthDlgTitle'),'modal')
                    return
                end
                
                % Set vehicle length
                vehicleLengthEditBox = findobj(ancestor(okBtn,'figure'),'Tag','vehicleLengthVal');
                try
                    vehicleLengthStr = vehicleLengthEditBox.String;
                    vehicleLength = eval(vehicleLengthStr);
                    
                    if ~isempty(vehicleLength)
                        validateattributes(vehicleLength, {'single', 'double'}, ...
                            {'real', 'nonsparse', 'finite', 'vector', 'increasing', 'numel', 2}, mfilename);
                    end
                    
                catch ME
                    errordlg(ME.message, ...
                        vision.getMessage('vision:labeler:InvalidVehicleLengthDlgTitle'),'modal')
                    return
                end                
                
                % Set calibration check box status
                calibCheckBox = findobj(ancestor(okBtn,'figure'),'Tag','checkBoxVal');
                configureDetector = (calibCheckBox.Value == 1);
                
                % Set sensor text
                sensorTextBox = findobj(ancestor(okBtn,'figure'),'Tag','sensorText');
                sensorStr = sensorTextBox.String;
                
                % Set intrinsic text
                intrinsicsTextBox = findobj(ancestor(okBtn,'figure'),'Tag','intrinsicsText');
                intrinsicsStr = intrinsicsTextBox.String;
                
                % Set extrinsics text
                extrinsicsTextBox = findobj(ancestor(okBtn,'figure'),'Tag','extrinsicsText');
                extrinsicsStr = extrinsicsTextBox.String;
                
                if isempty(intrinsicsStr)
                    errordlg('Import cameraIntrinsics object from workspace','Error dialog')
                    return
                end
                
                if isempty(extrinsicsStr)
                    errordlg('Import rigid3d object from workspace','Error dialog')
                    return
                end
                
                % Save user settings to the object
                algObj.VehicleModelName = modelName;                
                algObj.OverlapThreshold = overlapThresh;
                algObj.ScoreThreshold = scoreThresh;
                algObj.VehicleWidth = vehicleWidth;
                algObj.VehicleLength = vehicleLength;
                algObj.ConfigureDetector = configureDetector;
                algObj.SensorObj = sensorObj;
                algObj.SensorStr = sensorStr;
                algObj.IntrinsicsObj = intrinsicsObj;
                algObj.IntrinsicsStr = intrinsicsStr;
                algObj.ExtrinsicsObj = extrinsicsObj;
                algObj.ExtrinsicsStr = extrinsicsStr;
                
                delete(gcf)
            end
            
            % Cancel
            cancelButtonPos = [55 0.6 btnTextWidth 2];
            uicontrol('Parent',buttonPanel, ...
                'Units','characters', ...
                'Position',cancelButtonPos, ...
                'HandleVisibility','callback', ...
                'String',vision.getMessage('vision:labeler:SettingsCancelButton'), ...
                'Callback',@cancelButtonCallback);
            
            function cancelButtonCallback(~,~)
                delete(gcf)
            end
            
            % Enable/disable calibration based on stored value
            if configureDetector
                calibrationCheckBoxCallback()
            end
            
            % Wait for dialog to close before running to completion
            uiwait(parentObj)
            
         end
    end
end

function requiresLIDARToolbox(~)
    % Verify that the Lidar Toolbox is available.

    % check if Lidar toolbox is installed.
    try
        toolboxdir('lidar');
    catch ME
        if strcmp(ME.identifier, 'MATLAB:toolboxdir:DirectoryNotFound')
            exception = MException(message('vision:validation:lidarNotInstalled',"MultiSignalVehicleDetector"));
            throwAsCaller(exception)
        else
            rethrow(ME)
        end
    end

    % check out a license.
    [isLicensePresent, ~] = license('checkout','Lidar_Toolbox');

    if ~isLicensePresent
        exception = MException(message('vision:validation:lidarLicenseUnavailable',"MultiSignalVehicleDetector"));
        throwAsCaller(exception)
    end
end
