В этом примере показано, как обрабатывать данные 3-D лидара от датчика, установленного на транспортном средстве, для постепенного построения карты с помощью показаний инерциального измерительного блока (IMU). Такая карта может облегчить планирование маршрута для навигации транспортного средства или может использоваться для локализации. Для оценки сформированной карты этот пример также показывает, как сравнивать траекторию транспортного средства с записью глобальной системы позиционирования (GPS).
Карты высокой четкости (HD) - это картографические сервисы, которые обеспечивают точную геометрию дорог с точностью до нескольких сантиметров. Этот уровень точности делает карты высокой четкости подходящими для автоматизированных рабочих процессов вождения, таких как локализация и навигация. Такие HD-карты генерируются путем построения карты из 3-D лидарных сканирований, в сочетании с высокоточными GPS и или IMU-датчиками и могут использоваться для локализации транспортного средства в пределах нескольких сантиметров. В этом примере реализуется подмножество функций, необходимых для построения такой системы.
В этом примере показано, как:
Загрузка, изучение и визуализация записанных данных о вождении
Построение карты с помощью сканирования lidar
Улучшение карты с помощью показаний IMU
Данные, используемые в этом примере, получены из этого репозитория GitHub ® и представляют приблизительно 100 секунд данных лидара, GPS и IMU. Данные сохраняются в виде MAT-файлов, каждый из которых содержит . Загрузите MAT-файлы из репозитория и загрузите их в рабочую область MATLAB ®.timetable
Примечание: Загрузка может занять несколько минут.
baseDownloadURL = 'https://github.com/mathworks/udacity-self-driving-data-subset/raw/master/drive_segment_11_18_16/'; dataFolder = fullfile(tempdir, 'drive_segment_11_18_16', filesep); options = weboptions('Timeout', Inf); lidarFileName = dataFolder + "lidarPointClouds.mat"; imuFileName = dataFolder + "imuOrientations.mat"; gpsFileName = dataFolder + "gpsSequence.mat"; folderExists = exist(dataFolder, 'dir'); matfilesExist = exist(lidarFileName, 'file') && exist(imuFileName, 'file') ... && exist(gpsFileName, 'file'); if ~folderExists mkdir(dataFolder); end if ~matfilesExist disp('Downloading lidarPointClouds.mat (613 MB)...') websave(lidarFileName, baseDownloadURL + "lidarPointClouds.mat", options); disp('Downloading imuOrientations.mat (1.2 MB)...') websave(imuFileName, baseDownloadURL + "imuOrientations.mat", options); disp('Downloading gpsSequence.mat (3 KB)...') websave(gpsFileName, baseDownloadURL + "gpsSequence.mat", options); end
Во-первых, загрузите данные об облаке пункта, сохраненные от лидара Velodyne® HDL32E. Каждое сканирование данных лидара сохраняется как 3-D облако точек с помощью объект. Этот объект внутренне организует данные с использованием древовидной структуры данных K-d для более быстрого поиска. Временная метка, связанная с каждым сканированием лидара, записывается в pointCloudTime переменная расписания.
% Load lidar data from MAT-file data = load(lidarFileName); lidarPointClouds = data.lidarPointClouds; % Display first few rows of lidar data head(lidarPointClouds)
ans =
8×1 timetable
Time PointCloud
_____________ ________________
23:46:10.5115 [1×1 pointCloud]
23:46:10.6115 [1×1 pointCloud]
23:46:10.7116 [1×1 pointCloud]
23:46:10.8117 [1×1 pointCloud]
23:46:10.9118 [1×1 pointCloud]
23:46:11.0119 [1×1 pointCloud]
23:46:11.1120 [1×1 pointCloud]
23:46:11.2120 [1×1 pointCloud]
Загрузите данные GPS из MAT-файла. Latitude, Longitude, и Altitude переменные timetable используются для хранения географических координат, записанных устройством GPS на транспортном средстве.
% Load GPS sequence from MAT-file data = load(gpsFileName); gpsSequence = data.gpsSequence; % Display first few rows of GPS data head(gpsSequence)
ans =
8×3 timetable
Time Latitude Longitude Altitude
_____________ ________ _________ ________
23:46:11.4563 37.4 -122.11 -42.5
23:46:12.4563 37.4 -122.11 -42.5
23:46:13.4565 37.4 -122.11 -42.5
23:46:14.4455 37.4 -122.11 -42.5
23:46:15.4455 37.4 -122.11 -42.5
23:46:16.4567 37.4 -122.11 -42.5
23:46:17.4573 37.4 -122.11 -42.5
23:46:18.4656 37.4 -122.11 -42.5
Загрузите данные IMU из MAT-файла. IMU обычно состоит из отдельных датчиков, которые сообщают информацию о движении транспортного средства. Они объединяют несколько датчиков, включая акселерометры, гироскопы и магнитометры. Orientation переменная сохраняет сообщенную ориентацию датчика IMU. Эти показания представлены в виде кватернионов. Каждое считывание задается как вектор 1 на 4, содержащий четыре части кватерниона. Преобразование вектора 1 на 4 в объект.quaternion (Automated Driving Toolbox)
% Load IMU recordings from MAT-file data = load(imuFileName); imuOrientations = data.imuOrientations; % Convert IMU recordings to quaternion type imuOrientations = convertvars(imuOrientations, 'Orientation', 'quaternion'); % Display first few rows of IMU data head(imuOrientations)
ans =
8×1 timetable
Time Orientation
_____________ ________________
23:46:11.4570 [1×1 quaternion]
23:46:11.4605 [1×1 quaternion]
23:46:11.4620 [1×1 quaternion]
23:46:11.4655 [1×1 quaternion]
23:46:11.4670 [1×1 quaternion]
23:46:11.4705 [1×1 quaternion]
23:46:11.4720 [1×1 quaternion]
23:46:11.4755 [1×1 quaternion]
Чтобы понять, как поступают показания датчика, для каждого датчика вычислите приблизительный кадр .duration
lidarFrameDuration = median(diff(lidarPointClouds.Time)); gpsFrameDuration = median(diff(gpsSequence.Time)); imuFrameDuration = median(diff(imuOrientations.Time)); % Adjust display format to seconds lidarFrameDuration.Format = 's'; gpsFrameDuration.Format = 's'; imuFrameDuration.Format = 's'; % Compute frame rates lidarRate = 1/seconds(lidarFrameDuration); gpsRate = 1/seconds(gpsFrameDuration); imuRate = 1/seconds(imuFrameDuration); % Display frame durations and rates fprintf('Lidar: %s, %3.1f Hz\n', char(lidarFrameDuration), lidarRate); fprintf('GPS : %s, %3.1f Hz\n', char(gpsFrameDuration), gpsRate); fprintf('IMU : %s, %3.1f Hz\n', char(imuFrameDuration), imuRate);
Lidar: 0.10008 sec, 10.0 Hz GPS : 1.0001 sec, 1.0 Hz IMU : 0.002493 sec, 401.1 Hz
GPS-датчик - самый медленный, работает со скоростью, близкой к 1 Гц. Лидар следующий самый медленный, работает со скоростью, близкой к 10 Гц, за ним следует IMU со скоростью почти 400 Гц.
Чтобы понять, что содержит сцена, визуализируйте записанные данные с помощью потоковых проигрывателей. Для визуализации показаний GPS используйте . Визуализация показаний лидара с помощью geoplayer (Automated Driving Toolbox).pcplayer
% Create a geoplayer to visualize streaming geographic coordinates latCenter = gpsSequence.Latitude(1); lonCenter = gpsSequence.Longitude(1); zoomLevel = 17; gpsPlayer = geoplayer(latCenter, lonCenter, zoomLevel); % Plot the full route plotRoute(gpsPlayer, gpsSequence.Latitude, gpsSequence.Longitude); % Determine limits for the player xlimits = [-45 45]; % meters ylimits = [-45 45]; zlimits = [-10 20]; % Create a pcplayer to visualize streaming point clouds from lidar sensor lidarPlayer = pcplayer(xlimits, ylimits, zlimits); % Customize player axes labels xlabel(lidarPlayer.Axes, 'X (m)') ylabel(lidarPlayer.Axes, 'Y (m)') zlabel(lidarPlayer.Axes, 'Z (m)') title(lidarPlayer.Axes, 'Lidar Sensor Data') % Align players on screen helperAlignPlayers({gpsPlayer, lidarPlayer}); % Outer loop over GPS readings (slower signal) for g = 1 : height(gpsSequence)-1 % Extract geographic coordinates from timetable latitude = gpsSequence.Latitude(g); longitude = gpsSequence.Longitude(g); % Update current position in GPS display plotPosition(gpsPlayer, latitude, longitude); % Compute the time span between the current and next GPS reading timeSpan = timerange(gpsSequence.Time(g), gpsSequence.Time(g+1)); % Extract the lidar frames recorded during this time span lidarFrames = lidarPointClouds(timeSpan, :); % Inner loop over lidar readings (faster signal) for l = 1 : height(lidarFrames) % Extract point cloud ptCloud = lidarFrames.PointCloud(l); % Update lidar display view(lidarPlayer, ptCloud); % Pause to slow down the display pause(0.01) end end


Лидары - это мощные датчики, которые можно использовать для восприятия в сложных средах, где другие датчики не полезны. Они обеспечивают детальный, полный 360-градусный обзор окружающей среды транспортного средства.
% Hide players hide(gpsPlayer) hide(lidarPlayer) % Select a frame of lidar data to demonstrate registration workflow frameNum = 600; ptCloud = lidarPointClouds.PointCloud(frameNum); % Display and rotate ego view to show lidar data helperVisualizeEgoView(ptCloud);

Лидары можно использовать для построения сантиметровых точных HD-карт, включая HD-карты целых городов. Эти карты впоследствии могут использоваться для локализации в транспортном средстве. Типичным подходом к построению такой карты является выравнивание последовательных лидарных сканирований, полученных от движущегося транспортного средства, и их объединение в одно большое облако точек. В остальном примере рассматривается этот подход к построению карты.
Align lidar scans: выравнивание последовательных lidar scans с использованием метода регистрации облака точек, такого как итерационный алгоритм ближайшей точки (ICP) или алгоритм преобразования нормальных распределений (NDT). Посмотрите и pcregistericp для получения более подробной информации о каждом алгоритме. В этом примере используется неразрушающий контроль, поскольку он обычно более точен, особенно при рассмотрении вращений. pcregisterndtpcregisterndt функция возвращает жесткое преобразование, которое выравнивает движущееся облако точек относительно облака опорных точек. Последовательно составляя эти преобразования, каждое облако точек преобразуется обратно в опорную рамку первого облака точек.
Объединение выровненных сканирований: как только новое сканирование облака точек зарегистрировано и преобразовано обратно в опорную рамку первого облака точек, облако точек может быть объединено с первым облаком точек с помощью .pcmerge
Начните с двух точечных облаков, соответствующих ближайшим сканированиям лидара. Чтобы ускорить обработку и накопить достаточно движения между сканированиями, используйте каждое десятое сканирование.
skipFrames = 10; frameNum = 100; fixed = lidarPointClouds.PointCloud(frameNum); moving = lidarPointClouds.PointCloud(frameNum + skipFrames);
Перед регистрацией обработайте облако точек, чтобы сохранить отличительные структуры в облаке точек. Эти этапы предварительной обработки включают в себя следующее: * Обнаружение и удаление наземной плоскости * Обнаружение и удаление эго-транспортного средства
Эти шаги более подробно описаны в примере «Наземная плоскость и обнаружение препятствий с помощью Lidar (Automated Driving Toolbox)». В этом примере helperProcessPointCloud функция помощника выполняет эти шаги.
fixedProcessed = helperProcessPointCloud(fixed); movingProcessed = helperProcessPointCloud(moving);
Отображение необработанных и обработанных облаков точек на виде сверху. Пурпурные точки были удалены во время обработки. Эти точки соответствуют наземной плоскости и эго-транспортному средству.
hFigFixed = figure; pcshowpair(fixed, fixedProcessed) view(2); % Adjust view to show top-view helperMakeFigurePublishFriendly(hFigFixed); % Downsample the point clouds prior to registration. Downsampling improves % both registration accuracy and algorithm speed. downsamplePercent = 0.1; fixedDownsampled = pcdownsample(fixedProcessed, 'random', downsamplePercent); movingDownsampled = pcdownsample(movingProcessed, 'random', downsamplePercent);

Предварительно обработав облака точек, зарегистрируйте их с помощью неразрушающего контроля. Визуализация выравнивания до и после регистрации.
regGridStep = 5; tform = pcregisterndt(movingDownsampled, fixedDownsampled, regGridStep); movingReg = pctransform(movingProcessed, tform); % Visualize alignment in top-view before and after registration hFigAlign = figure; subplot(121) pcshowpair(movingProcessed, fixedProcessed) title('Before Registration') view(2) subplot(122) pcshowpair(movingReg, fixedProcessed) title('After Registration') view(2) helperMakeFigurePublishFriendly(hFigAlign);

Обратите внимание, что облака точек хорошо выровнены после регистрации. Несмотря на то, что облака точек тесно выровнены, выравнивание все еще не идеально.
Затем объедините облака точек с помощью pcmerge.
mergeGridStep = 0.5;
ptCloudAccum = pcmerge(fixedProcessed, movingReg, mergeGridStep);
hFigAccum = figure;
pcshow(ptCloudAccum)
title('Accumulated Point Cloud')
view(2)
helperMakeFigurePublishFriendly(hFigAccum);

Теперь, когда конвейер обработки для одной пары точечных облаков хорошо понятен, сложите это в цикл по всей последовательности записанных данных. helperLidarMapBuilder класс складывает все это вместе. updateMap метод класса принимает в новом облаке точек и проходит через шаги, описанные ранее:
Обработка облака точек путем удаления наземной плоскости и эго-транспортного средства с помощью processPointCloud способ.
Понижение дискретизации облака точек.
Оценка жесткого преобразования, необходимого для объединения предыдущего облака точек с текущим облаком точек.
Преобразование облака точек обратно в первый кадр.
Объединение облака точек с накопленной картой облака точек.
Кроме того, updateMap способ также принимает начальную оценку преобразования, которая используется для инициализации регистрации. Хорошая инициализация может значительно улучшить результаты регистрации. И наоборот, плохая инициализация может отрицательно сказаться на регистрации. Обеспечение хорошей инициализации также может улучшить время выполнения алгоритма.
Общий подход к предоставлению начальной оценки для регистрации заключается в использовании предположения о постоянной скорости. Используйте преобразование из предыдущей итерации в качестве начальной оценки.
updateDisplay метод дополнительно создает и обновляет 2-й вид сверху, текущий показ облака пункта.
% Create a map builder object mapBuilder = helperLidarMapBuilder('DownsamplePercent', downsamplePercent); % Set random number seed rng(0); closeDisplay = false; numFrames = height(lidarPointClouds); tform = rigid3d; for n = 1 : skipFrames : numFrames - skipFrames % Get the nth point cloud ptCloud = lidarPointClouds.PointCloud(n); % Use transformation from previous iteration as initial estimate for % current iteration of point cloud registration. (constant velocity) initTform = tform; % Update map using the point cloud tform = updateMap(mapBuilder, ptCloud, initTform); % Update map display updateDisplay(mapBuilder, closeDisplay); end

Регистрация облака точек сама по себе создает карту среды, пересекаемой транспортным средством. Хотя карта может казаться локально согласованной, она могла иметь значительный дрейф по всей последовательности.
Для визуальной оценки качества построенной карты используйте записанные GPS-показания в качестве наземной истинной траектории. Сначала преобразуйте показания GPS (широта, долгота, высота) в локальную систему координат. Выберите локальную систему координат, совпадающую с началом первого облака точек в последовательности. Это преобразование вычисляется с использованием двух преобразований:
Преобразование координат GPS в локальные декартовы координаты Восток-Север-Вверх с помощью функция. Местоположение GPS от начала траектории используется в качестве опорной точки и определяет начало локальной системы координат x, y, z.latlon2local (Automated Driving Toolbox)
Поверните декартовы координаты так, чтобы локальная система координат была выровнена с координатами первого лидарного датчика. Поскольку точная конфигурация установки лидара и GPS на транспортном средстве неизвестна, они оцениваются.
% Select reference point as first GPS reading origin = [gpsSequence.Latitude(1), gpsSequence.Longitude(1), gpsSequence.Altitude(1)]; % Convert GPS readings to a local East-North-Up coordinate system [xEast, yNorth, zUp] = latlon2local(gpsSequence.Latitude, gpsSequence.Longitude, ... gpsSequence.Altitude, origin); % Estimate rough orientation at start of trajectory to align local ENU % system with lidar coordinate system theta = median(atan2d(yNorth(1:15), xEast(1:15))); R = [ cosd(90-theta) sind(90-theta) 0; -sind(90-theta) cosd(90-theta) 0; 0 0 1]; % Rotate ENU coordinates to align with lidar coordinate system groundTruthTrajectory = [xEast, yNorth, zUp] * R;
Наложите траекторию истинности земли на построенную карту.
hold(mapBuilder.Axes, 'on') scatter(mapBuilder.Axes, groundTruthTrajectory(:,1), groundTruthTrajectory(:,2), ... 'green','filled'); helperAddLegend(mapBuilder.Axes, ... {'Map Points', 'Estimated Trajectory', 'Ground Truth Trajectory'});

После первоначального разворота расчётная траектория значительно отклоняется от наземной истинной траектории. Траектория, оцененная только с помощью регистрации облака точек, может дрейфовать по ряду причин:
Шумное сканирование датчика без достаточного перекрытия
Отсутствие достаточно сильных элементов, например, вблизи протяженных дорог
Неточное начальное преобразование, особенно когда поворот значителен.
% Close map display
updateDisplay(mapBuilder, true);
IMU - электронное устройство, установленное на платформе. IMU содержат множество датчиков, которые сообщают различную информацию о движении транспортного средства. Типичные IMU включают акселерометры, гироскопы и магнитометры. IMU может обеспечить надежную меру ориентации.
Используйте показания IMU, чтобы получить лучшую первоначальную оценку для регистрации. Показания датчика, представленные IMU, использованные в этом примере, уже отфильтрованы на устройстве.
% Reset the map builder to clear previously built map reset(mapBuilder); % Set random number seed rng(0); initTform = rigid3d; for n = 1 : skipFrames : numFrames - skipFrames % Get the nth point cloud ptCloud = lidarPointClouds.PointCloud(n); if n > 1 % Since IMU sensor reports readings at a much faster rate, gather % IMU readings reported since the last lidar scan. prevTime = lidarPointClouds.Time(n - skipFrames); currTime = lidarPointClouds.Time(n); timeSinceScan = timerange(prevTime, currTime); imuReadings = imuOrientations(timeSinceScan, 'Orientation'); % Form an initial estimate using IMU readings initTform = helperComputeInitialEstimateFromIMU(imuReadings, tform); end % Update map using the point cloud tform = updateMap(mapBuilder, ptCloud, initTform); % Update map display updateDisplay(mapBuilder, closeDisplay); end % Superimpose ground truth trajectory on new map hold(mapBuilder.Axes, 'on') scatter(mapBuilder.Axes, groundTruthTrajectory(:,1), groundTruthTrajectory(:,2), ... 'green','filled'); helperAddLegend(mapBuilder.Axes, ... {'Map Points', 'Estimated Trajectory', 'Ground Truth Trajectory'}); % Capture snapshot for publishing snapnow; % Close open figures close([hFigFixed, hFigAlign, hFigAccum]); updateDisplay(mapBuilder, true);

Использование оценки ориентации от IMU значительно улучшило регистрацию, что привело к гораздо более близкой траектории с меньшим дрейфом.
helperAlignPlayers выравнивает массив ячеек потоковых проигрывателей таким образом, чтобы они располагались слева направо на экране.
function helperAlignPlayers(players) validateattributes(players, {'cell'}, {'vector'}); hasAxes = cellfun(@(p)isprop(p,'Axes'),players,'UniformOutput', true); if ~all(hasAxes) error('Expected all viewers to have an Axes property'); end screenSize = get(groot, 'ScreenSize'); screenMargin = [50, 100]; playerSizes = cellfun(@getPlayerSize, players, 'UniformOutput', false); playerSizes = cell2mat(playerSizes); maxHeightInSet = max(playerSizes(1:3:end)); % Arrange players vertically so that the tallest player is 100 pixels from % the top. location = round([screenMargin(1), screenSize(4)-screenMargin(2)-maxHeightInSet]); for n = 1 : numel(players) player = players{n}; hFig = ancestor(player.Axes, 'figure'); hFig.OuterPosition(1:2) = location; % Set up next location by going right location = location + [50+hFig.OuterPosition(3), 0]; end function sz = getPlayerSize(viewer) % Get the parent figure container h = ancestor(viewer.Axes, 'figure'); sz = h.OuterPosition(3:4); end end
helperVisualizeEgoView визуализирует данные облака точек в эго-перспективе путем поворота вокруг центра.
function player = helperVisualizeEgoView(ptCloud) % Create a pcplayer object xlimits = ptCloud.XLimits; ylimits = ptCloud.YLimits; zlimits = ptCloud.ZLimits; player = pcplayer(xlimits, ylimits, zlimits); % Turn off axes lines axis(player.Axes, 'off'); % Set up camera to show ego view camproj(player.Axes, 'perspective'); camva(player.Axes, 90); campos(player.Axes, [0 0 0]); camtarget(player.Axes, [-1 0 0]); % Set up a transformation to rotate by 5 degrees theta = 5; R = [ cosd(theta) sind(theta) 0 0 -sind(theta) cosd(theta) 0 0 0 0 1 0 0 0 0 1]; rotateByTheta = rigid3d(R); for n = 0 : theta : 359 % Rotate point cloud by theta ptCloud = pctransform(ptCloud, rotateByTheta); % Display point cloud view(player, ptCloud); pause(0.05) end end
helperProcessPointCloud обрабатывает облако точек путем удаления точек, принадлежащих наземной плоскости или эго-транспортному средству.
function ptCloudProcessed = helperProcessPointCloud(ptCloud) % Check if the point cloud is organized isOrganized = ~ismatrix(ptCloud.Location); % If the point cloud is organized, use range-based flood fill algorithm % (segmentGroundFromLidarData). Otherwise, use plane fitting. groundSegmentationMethods = ["planefit", "rangefloodfill"]; method = groundSegmentationMethods(isOrganized+1); if method == "planefit" % Segment ground as the dominant plane, with reference normal vector % pointing in positive z-direction, using pcfitplane. For organized % point clouds, consider using segmentGroundFromLidarData instead. maxDistance = 0.4; % meters maxAngDistance = 5; % degrees refVector = [0, 0, 1]; % z-direction [~,groundIndices] = pcfitplane(ptCloud, maxDistance, refVector, maxAngDistance); elseif method == "rangefloodfill" % Segment ground using range-based flood fill. groundIndices = segmentGroundFromLidarData(ptCloud); else error("Expected method to be 'planefit' or 'rangefloodfill'") end % Segment ego vehicle as points within a given radius of sensor sensorLocation = [0, 0, 0]; radius = 3.5; egoIndices = findNeighborsInRadius(ptCloud, sensorLocation, radius); % Remove points belonging to ground or ego vehicle ptsToKeep = true(ptCloud.Count, 1); ptsToKeep(groundIndices) = false; ptsToKeep(egoIndices) = false; % If the point cloud is organized, retain organized structure if isOrganized ptCloudProcessed = select(ptCloud, find(ptsToKeep), 'OutputSize', 'full'); else ptCloudProcessed = select(ptCloud, find(ptsToKeep)); end end
helperComputeInitialEstimateFromIMU оценивает начальное преобразование для NDT с использованием показаний ориентации IMU и предварительно оцененного преобразования.
function tform = helperComputeInitialEstimateFromIMU(imuReadings, prevTform) % Initialize transformation using previously estimated transform tform = prevTform; % If no IMU readings are available, return if height(imuReadings) <= 1 return; end % IMU orientation readings are reported as quaternions representing the % rotational offset to the body frame. Compute the orientation change % between the first and last reported IMU orientations during the interval % of the lidar scan. q1 = imuReadings.Orientation(1); q2 = imuReadings.Orientation(end); % Compute rotational offset between first and last IMU reading by % - Rotating from q2 frame to body frame % - Rotating from body frame to q1 frame q = q1 * conj(q2); % Convert to Euler angles yawPitchRoll = euler(q, 'ZYX', 'point'); % Discard pitch and roll angle estimates. Use only heading angle estimate % from IMU orientation. yawPitchRoll(2:3) = 0; % Convert back to rotation matrix q = quaternion(yawPitchRoll, 'euler', 'ZYX', 'point'); R = rotmat(q, 'point'); % Use computed rotation tform.T(1:3, 1:3) = R'; end
helperAddLegend добавляет легенду к осям.
function helperAddLegend(hAx, labels) % Add a legend to the axes hLegend = legend(hAx, labels{:}); % Set text color and font weight hLegend.TextColor = [1 1 1]; hLegend.FontWeight = 'bold'; end
helperMakeFigurePublishFriendly корректирует рисунки таким образом, чтобы снимок экрана, снятый при публикации, был правильным.
function helperMakeFigurePublishFriendly(hFig) if ~isempty(hFig) && isvalid(hFig) hFig.HandleVisibility = 'callback'; end end
pcplayer | pointCloud | geoplayer (Автоматизированная панель инструментов вождения)