主要内容

从移动的汽车中跟踪行人

这个例子展示了如何使用安装在移动汽车上的摄像头来跟踪行人。

概述

这个例子展示了如何自动检测和跟踪来自移动摄像机的视频中的人。它展示了适应移动摄像机的跟踪系统的灵活性,是汽车安全应用的理想选择。与静止相机的例子不同,基于运动的多目标跟踪,这个例子包含几个额外的算法步骤。这些步骤包括人员检测、定制的非最大抑制以及识别和消除虚假警报轨迹的启发式方法。有关更多信息,请参见多目标跟踪

这个例子是一个函数,其主体位于顶部,辅助例程的形式为什么是嵌套函数?在下面。

函数PedestrianTrackingFromMovingCameraExample ()
创建用于读取视频、加载先决条件数据文件、检测行人和显示结果的系统对象。videoFile =“vippedtracking.mp4”;scaleDataFile =“pedScaleTable.mat”一个辅助文件,帮助确定行人在不同像素位置的大小。obj = setupSystemObjects(视频文件,scaleDataFile);探测器=人加州理工学院的);创建一个空的音轨数组。tracks = initializeTracks();%下一个音轨ID。nextId = 1;%设置全局参数。选择。ROI= [40 95 400 140];一个矩形[x, y, w, h],它将处理区域限制在地面位置。选择。scThresh = 0.3;在估计检测到的行人的规模时控制误差容忍度的阈值。选择。gatingThresh = 0.9;拒绝检测与跟踪之间的候选匹配的阈值。选择。gatingCost = 100;分配成本矩阵的一个大值,它强制拒绝候选匹配。选择。costOfNonAssignment = 10;用于控制创建新轨道可能性的调优参数。选择。timeWindowSize = 16;一个调优参数,用于指定稳定音轨置信度分数所需的帧数。选择。confidence ethresh = 2;%一个阈值,用于确定一个轨道是真报警或假报警。选择。年龄Thresh = 8;用于确定轨道为真阳性所需的最小长度的阈值。选择。visThresh = 0.6;一个阈值,用于确定轨道为真阳性的最小可见性值。检测人,并跟踪他们在视频帧。。stopFrame = 1629;停在一个有几个行人的有趣的框架上fNum = 1:stopFrame frame = readFrame(obj.reader);[质心,盒子,分数]= detectPeople();predictNewLocationsOfTracks ();[assignments, unassigndtracks, unassignddetections] =...detectionToTrackAssignment ();updateAssignedTracks ();updateUnassignedTracks ();deleteLostTracks ();createNewTracks ();displayTrackingResults ();%如果视频播放器图形关闭,则退出循环。如果~ isOpen (obj.videoPlayer)打破结束结束

跟踪系统的辅助输入和全局参数

该跟踪系统需要一个数据文件,其中包含将图像中的像素位置与标记行人位置的包围框大小联系起来的信息。这种先验知识存储在一个向量中pedScaleTable.第n项pedScaleTable以像素为单位表示一个成年人的估计高度。该指数n参考行人脚的近似y坐标。

为了获得这样一个向量,在与测试环境相似的场景下,从相同的视点拍摄了一组训练图像。训练图像包含距离相机不同距离的行人图像。使用图片标志App中,对图像中行人的边界框进行手工标注。将边界框的高度与行人在图像中的位置结合,通过回归生成尺度数据文件。下面是一个辅助函数,用于显示拟合线性回归模型的算法步骤:helperTableOfScales.m

还有一组全局参数,可以调整以优化跟踪性能。您可以使用下面的描述来了解这些参数如何影响跟踪性能。

  • ROI:兴趣区域,形式为[x, y, w, h]。它将处理区域限制在地面位置。

  • scThresh:尺度估计的公差阈值。当检测尺度与期望尺度之间的差异超过容差时,认为候选检测是不现实的,并从输出中删除。

  • gatingThresh:距离测量的门控参数。当检测到的边界框与预测的边界框匹配的代价超过阈值时,系统将不再考虑跟踪两个边界框的关联。

  • gatingCost:分配成本矩阵的值,以阻止可能的跟踪到检测分配。

  • costOfNonAssignment:不分配检测或轨道的分配成本矩阵的值。设置过低会增加创建新轨道的可能性,并可能导致轨道碎片化。设置过高可能会导致单个轨道对应于一系列独立的移动对象。

  • timeWindowSize:估计轨迹置信度所需的帧数。

  • confidenceThresh:确定轨道是否为真阳性的置信阈值。

  • ageThresh:一个轨道的最小长度是一个真正的积极。

  • visThresh:最小能见度阈值,以确定轨道是否为真阳性。

为跟踪系统初始化创建系统对象

setupSystemObjects函数创建用于读取和显示视频帧的系统对象,并加载缩放数据文件。

pedScaleTable矢量,存储在尺度数据文件中,编码我们对目标和场景的先验知识。一旦你从样本中训练了回归函数,你就可以计算出图像中每个可能的y位置的期望高度。这些值存储在向量中。第n项pedScaleTable表示一个成年人的估计身高,单位为像素。该指数n参考行人脚的近似y坐标。

函数obj = setupSystemObjects(视频文件,scaleDataFile)初始化视频I/O创建用于从文件中读取视频的对象%检测和跟踪每个帧中的人,并播放视频。创建一个视频文件阅读器。obj。reader = VideoReader(videoFile);创建一个视频播放器。obj。videoPlayer =视觉。放像机(“位置”, [29, 597, 643, 386]);加载比例数据文件ld = load(scaleDataFile,“pedScaleTable”);obj。pedScaleTable = ld.pedScaleTable;结束

初始化跟踪

initializeTracks函数创建一个轨道数组,其中每个轨道是一个表示视频中移动对象的结构。该结构的目的是维护被跟踪对象的状态。状态由用于检测到轨迹分配、轨迹终止和显示的信息组成。

该结构包含以下字段:

  • id:轨道的整数ID。

  • 颜色:用于显示的轨道颜色。

  • bboxes: n × 4矩阵,表示对象的包围框,当前框位于最后一行。每一行都有一个[x, y, width, height]的形式。

  • 分数: n乘1向量,记录来自人检测器的分类评分,最后一行是当前检测评分。

  • kalmanFilter:用于基于运动跟踪的卡尔曼滤波器对象。我们在图像中跟踪物体的中心点;

  • 年龄:自轨道初始化以来的帧数。

  • totalVisibleCount:检测到对象的总帧数(可见)。

  • 信心:两个数字代表我们对赛道的信心。它在预定义的时间窗口内存储过去的最大和平均检测分数。

  • predPosition:下一帧预测的边界框。

函数tracks = initializeTracks()创建一个空的音轨数组Tracks = struct(...“id”{},...“颜色”{},...“bboxes”{},...“分数”{},...“kalmanFilter”{},...“年龄”{},...“totalVisibleCount”{},...“信心”{},...“predPosition”, {});结束

检测人

detectPeople函数返回检测到的人员的质心、包围框和分类分数。它对检测器返回的原始输出进行滤波和非最大抑制peopleDetectorACF

  • 重心: n × 2矩阵,每行形式为[x,y]。

  • bboxes: n × 4矩阵,每一行的形式为[x, y, width, height]。

  • 分数:每个元素的n乘1向量为对应帧的分类评分。

函数[质心,盒子,分数]= detectPeople()调整图像大小以增加行人的分辨率。。这有助于发现离相机较远的人。。resizeRatio = 1.5;帧= imresize(帧,resizeRatio,抗锯齿的、假);在感兴趣的区域内运行ACF人员检测器来生成%检测候选。[bboxes, scores] = detect(检测器,帧,选项。投资回报率,...“WindowStride”2,...“NumScaleLevels”4...“SelectStrongest”、假);根据行人脚的位置来估计他们的高度。。height = bboxes(:, 4) / resizeRatio;y = (bboxes(:,2)-1) / resizeRatio + 1;yfoot = min(length(obj.pedScaleTable), round(y + height));estHeight = obj.pedScaleTable(yfoot);去除大小偏离预期大小的检测,%%由校准刻度估计提供。无效= abs(estHeight-height)>estHeight*option.scThresh;Bboxes(无效,:)= [];分数(无效,:)= [];应用非最大抑制来选择最强的包围框。[bboxes, scores] = selectstronggestbbox (bboxes, scores,...“RatioType”“最小值”“OverlapThreshold”, 0.6);计算质心如果Isempty (bboxes) centroids = [];其他的质心= [(bboxes(:, 1) + bboxes(:, 3) / 2),...(bboxes(:, 2) + bboxes(:, 4) / 2)];结束结束

预测现有轨道的新位置

使用卡尔曼滤波器预测当前帧中每个轨道的质心,并更新其边界框。我们将前一帧中边界框的宽度和高度作为我们当前对尺寸的预测。

函数predictNewLocationsOfTracks ()I = 1:长度(音轨)获取此轨道上的最后一个包围框。Bbox =轨道(i)。bboxes (,);预测轨道的当前位置。predictedCentroid = predict(tracks(i).kalmanFilter);移动边框,使其中心位于预测位置。跟踪(i)。predPosition = [predictedCentroid - bbox(3:4)/2, bbox(3:4)];结束结束

将检测分配给轨道

将当前帧中的对象检测分配给现有轨道是通过最小化成本来完成的。方法计算成本bboxOverlapRatio函数,为预测边界框与检测边界框的重叠率。在这个例子中,我们假设由于视频的高帧率和人的低运动速度,人将在连续的帧中逐渐移动。

算法包括两步:

步骤1:计算将每个检测分配给每个轨道的成本bboxOverlapRatio衡量。当人们移动到或离开相机,他们的运动不会被准确描述的质心点单独。该代价考虑了图像平面上的距离以及边界框的尺度。这可以防止将远离摄像机的探测分配到靠近摄像机的轨道上,即使它们的质心重合。这个代价函数的选择将简化计算,而无需求助于更复杂的动态模型。结果存储在MxN矩阵中,其中M是轨道数,N是检测数。

步骤2:求解由代价矩阵表示的分配问题assignDetectionsToTracks函数。该函数采用成本矩阵和不将任何检测分配给轨道的成本。

不将检测分配给轨道的代价值取决于代价函数返回的值的范围。这个值必须经过实验调整。设置过低会增加创建新轨道的可能性,并可能导致轨道碎片化。设置过高可能会导致单个轨道对应于一系列独立的移动对象。

assignDetectionsToTracks函数使用Munkres版本的匈牙利算法来计算使总成本最小化的分配。它返回一个M x2矩阵,在它的两列中包含指定轨道和探测的对应索引。它还返回未分配的航迹和探测的索引。

函数[assignments, unassigndtracks, unassignddetections] =...detectionToTrackAssignment ()计算预测框与预测框的重叠率%检测到的箱子,并计算分配每个检测的代价%到每个轨道。当预测箱为时,成本最小%与检测到的盒子完全对齐(重叠率为1)predBboxes =重塑([轨道(:)。predPosition], 4, [])';成本= 1 - bboxOverlapRatio(predBboxes, bboxes);强制优化步骤忽略某些匹配%,将相关成本设置为一个较大的数字。注意这一点% number与下面的'costOfNonAssignment'不同。这在门控(删除不现实的匹配)时很有用%的技术应用。cost(cost > option.gatingThresh) = 1 + option.gatingCost;解决分配问题。[assignments, unassigndtracks, unassignddetections] =...assignDetectionsToTracks(成本、option.costOfNonAssignment);结束

更新已分配曲目

updateAssignedTracks功能更新每个指定的轨道与相应的检测。它调用正确的的方法愿景。KalmanFilter修正位置估计。接下来,它通过取最近(最多)4个盒子大小的平均值来存储新的包围框,并将轨道的年龄和总可见计数增加1。最后,该函数根据之前的检测分数调整我们对轨道的置信度分数。

函数updateAssignedTracks() numAssignedTracks = size(assignments, 1);trackIdx =赋值(i, 1);detectionIdx =赋值(i, 2);质心=质心(detectionIdx,:);bbox = bboxes(detectionIdx,:);修正物体位置的估计%使用新的检测。正确的(跟踪(trackIdx)。kalmanFilter、质心);通过取大小的平均值来稳定边界框轨道上最近(最多)4个箱子的百分比。T = min(size(tracks(trackIdx).bboxes,1), 4);w = mean([轨迹(trackIdx)。bboxes (end-T + 1:最终,3);bbox (3)]);h = mean([轨迹(trackIdx)。bboxes (end-T + 1:最终,4);bbox (4)]);跟踪(trackIdx)。bboxes(end+1, :) = [centroid - [w, h]/2, w, h];%更新轨道的年龄。跟踪(trackIdx)。年龄= tracks(trackIdx).age + 1;更新赛道的得分历史跟踪(trackIdx)。分数= [tracks(trackIdx).scores; scores(detectionIdx)];%更新可见性。跟踪(trackIdx)。totalVisibleCount=...跟踪(trackIdx)。totalVisibleCount+ 1;%根据最大检测量调整航迹置信度%在过去的'timeWindowSize'帧中的得分。T = min(选项。timeWindowSize,length(tracks(trackIdx).scores)); score = tracks(trackIdx).scores(end-T+1:end); tracks(trackIdx).confidence = [max(score), mean(score)];结束结束

更新未分配曲目

updateUnassignedTracks函数将每个未分配的轨道标记为不可见,将其年龄增加1,并将预测的边界框附加到轨道上。置信度被设置为零,因为我们不确定为什么它没有被分配到一个轨道。

函数updateUnassignedTracks ()i = 1:长度(unassigndtracks) idx = unassigndtracks (i);跟踪(idx)。年龄= tracks(idx).age + 1; tracks(idx).bboxes = [tracks(idx).bboxes; tracks(idx).predPosition]; tracks(idx).scores = [tracks(idx).scores; 0];%根据最大检测量调整航迹置信度%在过去的'timeWindowSize'帧中的得分T = min(选项。timeWindowSize,length(tracks(idx).scores)); score = tracks(idx).scores(end-T+1:end); tracks(idx).confidence = [max(score), mean(score)];结束结束

删除丢失的轨迹

deleteLostTracks函数删除连续太多帧不可见的轨道。它还删除了最近创建的轨迹,这些轨迹在很多帧中都是不可见的。

有噪声的探测往往会产生错误的轨迹。对于这个例子,我们在以下条件下删除一个轨道:

  • 这个物体被追踪了很短的时间。这通常发生在错误检测出现几帧时,并为它启动了一个跟踪。

  • 这条轨迹在大多数画面中都被标记为不可见。

  • 在过去几帧内没有收到强检测,表示为最大检测置信度分数。

函数deleteLostTracks ()如果isempty(跟踪)返回结束计算轨道可见年龄的百分比。%Ages = [tracks(:).age]';totalVisibleCounts = [tracks(:).totalVisibleCount]';可见度= totalVisibleCounts ./ ages;检查最大检测置信度分数。信心=重塑([轨迹(:))。信心],2,[])';maxConfidence = confidence(:, 1);查找“丢失”曲目的索引。lostInds = (ages <= option.)ageThresh& visibility <= option.visThresh) |...(maxConfidence <= option.confidenceThresh);删除丢失的轨道。tracks = tracks(~lostInds);结束

创建新轨道

从未分配的检测中创建新轨道。假设任何未分配的检测都是一个新轨道的开始。在实践中,您可以使用其他线索来消除噪声检测,例如大小、位置或外观。

函数createNewTracks() unassignedCentroids = centroids(unassignedDetections,:);unassignedBboxes = bboxes(unassignedDetections,:);unassigndscores = scores(unassignddetections);i = 1:size(unassignedBboxes, 1) centroid = unassignedCentroids(i,:);bbox = unassigndbboxes (i,:);score = unassigndscores (i);创建一个卡尔曼过滤器对象。kalmanFilter = configureKalmanFilter(“ConstantVelocity”...质心,[2,1],[5,5],100);创建一个新轨道。newTrack = struct(...“id”nextId,...“颜色”, 255 *兰德(1、3)...“bboxes”bbox,...“分数”分数,...“kalmanFilter”kalmanFilter,...“年龄”, 1...“totalVisibleCount”, 1...“信心”,[得分,得分],...“predPosition”, bbox);将其添加到曲目数组中。。tracks(end + 1) = newTrack;% #好< AGROW >自增下一个id。nextId = nextId + 1;结束结束

显示跟踪结果

displayTrackingResults函数为视频帧上的每个轨道绘制一个彩色包围框。方框的透明度水平以及显示的分数表明检测和跟踪的置信度。

函数displayTrackingResults() displayRatio = 4/3;frame = imresize(frame, displayRatio);如果~isempty(tracks) ages = [tracks(:).age]';信心=重塑([轨迹(:))。信心],2,[])';maxConfidence = confidence(:, 1);avgConfidence = confidence(:, 2);不透明度= min(0.5,max(0.1,avgConfidence/3));noDispInds =(年龄<选项。ageThresh& maxConfidence < option.confidenceThresh) |...(年龄<选项。ageThresh / 2);I = 1:长度(音轨)如果~ noDispInds(我)用于显示的%比例包围框Bb =轨迹(i)。bboxes(end, :); bb(:,1:2) = (bb(:,1:2)-1)*displayRatio + 1; bb(:,3:4) = bb(:,3:4) * displayRatio; frame = insertShape(frame,...“FilledRectangle”bb,...“颜色”,跟踪.color(我),...“不透明度”,透明度(i));帧= insertObjectAnnotation(帧,...“矩形”bb,...num2str (avgConfidence(我)),...“颜色”,跟踪(i) .color);结束结束结束帧= insertShape(帧,“矩形”,选择。ROI* displayRatio,...“颜色”, [255, 0,0],“线宽”3);步骤(obj。放像机、框架);结束
结束