主要内容

从移动的汽车上追踪行人

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

概述

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

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

函数PedestrianTrackingFromMovingCameraExample ()
创建用于读取视频、加载前提数据文件、检测行人并显示结果的系统对象。videoFile =“vippedtracking.mp4”;scaleDataFile =“pedScaleTable.mat”一种辅助文件,帮助确定行人在不同像素位置的大小。。obj = setupSystemObjects(videoFile, scaleDataFile);探测器= peopleDetectorACF (加州理工学院的);创建一个空的音轨数组。跟踪= 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;一个调优参数,用于指定稳定音轨的置信度评分所需的帧数。选择。confidenceThresh = 2;用于确定一个音轨是真阳性还是假报警的阈值。。选择。年龄Thresh = 8;一个阈值,用来确定轨道为真正所需的最小长度。选择。visThresh = 0.6;确定轨迹为真正时的最小可见性值的阈值。。通过视频帧检测人和跟踪他们。。stopFrame = 1629;停在一个有趣的框架和几个行人。fNum = 1:stopFrame frame = readFrame(obj.reader);[centroids, box, scores] = detectPeople();predictNewLocationsOfTracks ();[assignments, unassignnedtracks, unassignneddetections] =...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 (videoFile scaleDataFile)初始化视频I/O创建对象,用于从文件中读取视频,绘制侦测和追踪每一帧中的人,并播放视频。。创建一个视频文件阅读器。obj。读者= VideoReader (videoFile);创建一个视频播放器。obj。放像机=愿景。放像机(“位置”, [29, 597, 643, 386]);加载比例数据文件ld =负载(scaleDataFile,“pedScaleTable”);obj。pedScaleTable = ld.pedScaleTable;结束

初始化跟踪

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

该结构包含以下字段:

  • id:整数形式。

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

  • bboxes:一个n × 4矩阵,表示对象的边界框,当前框在最后一行。每一行都有[x, y,宽,高]的形式。

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

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

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

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

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

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

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

检测人

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

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

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

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

函数[centroids, box, scores] = detectPeople()调整图像的大小以增加行人的分辨率。。这有助于发现远离镜头的人。。resizeRatio = 1.5;frame = imresize(frame, 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(无效,:)= [];Scores(无效,:)= [];应用非最大抑制来选择最强的边界框。[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 =预测(跟踪(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, unassignnedtracks, unassignneddetections] =...detectionToTrackAssignment ()%计算预测框与%的检测框,并计算分配每个检测的成本%到每个音轨。当预测箱体为时,成本最小%与检测到的bbox完全对齐(重叠率为1)predBboxes =重塑([跟踪(:)。predPosition], []) ';cost = 1 - bboxOverlapRatio(predBboxes, bboxes);强制优化步骤忽略一些匹配。%,将相关成本设置为一个较大的数字。请注意,这% number与下面的'costOfNonAssignment'不同。这在门控(删除不现实的匹配)时很有用运用了技巧。cost(cost > option.gatingThresh) = 1 + option.gatingCost;解决分配问题。[assignments, unassignnedtracks, unassignneddetections] =...assignDetectionsToTracks(成本、option.costOfNonAssignment);结束

更新指定的跟踪

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

函数updateassignnedtracks () numassignnedtracks = size(assignments, 1);i = 1: numassignnedtracks trackIdx = assignments(i, 1);detectionIdx = assignments(i, 2);centroid = centroids(detectionIdx,:);bbox = bboxes(detectionIdx,:);修正对物体位置的估计%使用新的检测。正确的(跟踪(trackIdx)。kalmanFilter、质心);通过取大小的平均值来稳定边界框轨道上最近(最多)4个箱子的%。T = min(size(tracks(trackIdx).bboxes,1), 4);w =意味着([跟踪(trackIdx)。bboxes (end-T + 1:最终,3);bbox (3)]);h =意味着([跟踪(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:length(unassignedTracks) idx = unassignedTracks(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(跟踪)返回结束计算轨迹可见的年龄的百分比。。年龄=(跟踪(:).age) ';totalVisibleCounts =(跟踪(:).totalVisibleCount) ';visibility = totalVisibleCounts ./ ages;%检查最大检测置信度评分。信心=重塑([跟踪(:)。信心]2 [])';maxConfidence = confidence(:, 1);查找“丢失的”轨迹的索引。。lostInds = (ages <= option。ageThresh& visibility <= option.visThresh) |...(maxConfidence < = option.confidenceThresh);删除丢失的轨迹。跟踪=跟踪(~ lostInds);结束

创建新的跟踪

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

函数createNewTracks() unassignedCentroids = centroids(unassignedDetections,:);unassignedBboxes = bboxes(unassignedDetections,:);unassignedScores =分数(unassignedDetections);i = 1:size(unassignedBboxes, 1) centroid = unassignecentroids (i,:);bbox = unassignigndbboxes (i,:);分数= unassignedScores(我);创建一个卡尔曼过滤器对象。kalmanFilter = configureKalmanFilter (“ConstantVelocity”...质心,[2,1],[5,5],100);创建一个新的音轨。newTrack =结构(...“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);不透明度=最小(0.5,最大(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。放像机、框架);结束
结束