主要内容

基于运动的多目标跟踪

这个例子展示了如何对来自固定摄像机的视频中的移动物体执行自动检测和基于运动的跟踪。

运动目标检测和基于运动的跟踪是许多计算机视觉应用的重要组成部分,包括活动识别、交通监控和汽车安全。基于运动的目标跟踪问题可以分为两部分:

  1. 在每一帧中检测运动物体

  2. 在一段时间内关联与同一对象对应的检测

运动目标检测采用基于高斯混合模型的背景减法。将形态学运算应用于所得到的前景掩模以消除噪声。最后,斑点分析检测连接的像素组,这些像素组很可能对应于移动的物体。

探测与同一物体的关联仅仅基于运动。通过卡尔曼滤波估计每条轨迹的运动。该滤波器用于预测轨道在每一帧中的位置,并确定每个检测被分配给每个轨道的可能性。

轨道维护是本例的一个重要方面。在任何给定的帧中,一些检测可以分配给轨道,而其他检测和轨道可能仍然未分配。使用相应的检测更新指定的轨道。未分配的轨道被标记为不可见。一个未分配的探测开始一个新的轨道。

每个轨道保持连续帧数的计数,在那里它仍然未分配。如果计数超过指定的阈值,该示例假设对象离开了视场并删除轨道。

有关更多信息,请参见多目标跟踪

这个例子是一个主体位于顶部的函数,辅助例程以嵌套函数的形式出现。

函数MotionBasedMultiObjectTrackingExample ()

%创建系统对象用于读取视频,检测移动对象,%并显示结果。obj = setupSystemObjects();tracks = initializeTracks();创建一个空的音轨数组。nextId = 1;%下一个音轨ID检测移动物体,并在视频帧中跟踪它们。。hasFrame(obj.reader) frame = readFrame(obj.reader);[centroids, bboxes, mask] = detectObjects(frame);predictNewLocationsOfTracks ();[assignments, unassigndtracks, unassignddetections] =...detectionToTrackAssignment ();updateAssignedTracks ();updateUnassignedTracks ();deleteLostTracks ();createNewTracks ();displayTrackingResults ();结束

创建系统对象

创建用于读取视频帧、检测前景对象和显示结果的系统对象。

函数obj = setupSystemObjects()初始化视频I/O创建用于从文件中读取视频的对象,绘制跟踪对象%对象,并播放视频。创建一个视频阅读器。obj。阅读器=视频阅读器(“atrium.mp4”);创建两个视频播放器,一个用于显示视频,%和一个显示前景蒙版。obj。maskPlayer =视觉。放像机(“位置”, [740, 400, 700, 400]);obj。videoPlayer =视觉。放像机(“位置”, [20, 400, 700, 400]);创建用于前景检测和斑点分析的系统对象前景检测器用于分割移动的物体%背景。它输出一个二进制掩码,其中像素值1的%对应前台,0的值对应前台%到后台。Obj.detector =视觉。ForegroundDetector (“NumGaussians”3,...“NumTrainingFrames”现年40岁的“MinimumBackgroundRatio”, 0.7);前景像素的连接组很可能对应于移动%的对象。对象blob analysis System用于查找这样的组%(称为“blobs”或“连接组件”),并计算它们的%特征,如面积、质心和包围框。obj。blobAnalyser =视觉。BlobAnalysis (“BoundingBoxOutputPort”,真的,...“AreaOutputPort”,真的,“CentroidOutputPort”,真的,...“MinimumBlobArea”, 400);结束

初始化跟踪

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

该结构包含以下字段:

  • id:轨道的整数ID

  • bbox:对象的当前包围框;用于显示

  • kalmanFilter:用于基于运动跟踪的卡尔曼滤波器对象

  • 年龄:自第一次检测到轨道以来的帧数

  • totalVisibleCount:检测到轨道的总帧数(可见)

  • consecutiveInvisibleCount:未检测到轨道(不可见)的连续帧数。

噪声检测往往会导致短时间的轨迹。因此,该示例只在跟踪了一些帧数之后显示对象。这种情况发生在totalVisibleCount超过指定的阈值。

当连续几帧没有检测到轨道时,该示例假设对象已经离开了视野并删除轨道。这种情况发生在consecutiveInvisibleCount超过指定的阈值。一个轨迹也可能被删除为噪音,如果它被跟踪了很短的时间,并标记为不可见的大多数帧。

函数tracks = initializeTracks()创建一个空的音轨数组Tracks = struct(...“id”{},...“bbox”{},...“kalmanFilter”{},...“年龄”{},...“totalVisibleCount”{},...“consecutiveInvisibleCount”, {});结束

检测对象

detectObjects函数返回检测到的对象的质心和包围框。它还返回二进制掩码,其大小与输入帧相同。值为1的像素对应前景,值为0的像素对应背景。

该函数使用前景检测器进行运动分割。然后,它对生成的二进制掩码执行形态学操作,以去除噪声像素,并填充剩余斑点中的空洞。

函数[质心,框,掩码]= detectObjects(帧)%检测前景。掩码= obj.detect .step(帧);应用形态学操作去除噪声和填充洞。。掩码= imopen(掩码,strel(“矩形”[3 3]));掩码= imclose(掩码,strel(“矩形”, [15,15]));掩码=填充(掩码,“黑洞”);执行blob分析以查找连接的组件。[~, centroids, bboxes] = obj.blobAnalyser.step(mask);结束

预测现有轨道的新位置

使用卡尔曼滤波器预测当前帧中每个轨道的质心,并更新其边界框。

函数predictNewLocationsOfTracks ()I = 1:长度(轨道)bbox =轨道(I).bbox;预测轨道的当前位置。predictedCentroid = predict(tracks(i).kalmanFilter);移动边框,使其中心位于%预测的位置。predictedCentroid = int32(predictedCentroid) - bbox(3:4) / 2;跟踪(i)。bbox= [predictedCentroid, bbox(3:4)];结束结束

将检测分配给轨道

将当前帧中的对象检测分配给现有轨道是通过最小化成本来完成的。成本定义为与轨道对应的检测的负对数似然值。

算法包括两步:

步骤1:计算将每个检测分配给每个轨道的成本距离方法愿景。KalmanFilter系统对象™。该成本考虑了轨道的预测质心和检测质心之间的欧几里得距离。它还包括预测的置信度,这是由卡尔曼滤波器维持的。结果存储在MxN矩阵中,其中M是轨道数,N是检测数。

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

类返回的值的范围决定了未将检测分配给轨道的代价值距离方法愿景。KalmanFilter.这个值必须经过实验调整。设置过低会增加创建新轨道的可能性,并可能导致轨道碎片化。设置过高可能会导致单个轨道对应于一系列独立的移动对象。

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

函数[assignments, unassigndtracks, unassignddetections] =...detectionToTrackAssignment() nTracks =长度(轨道);nDetections = size(质心,1);计算分配每个检测到每个轨道的成本。。成本=零(nTracks, nDetections);i = 1:nTracks成本(i,:) =距离(轨道(i)。kalmanFilter、质心);结束解决分配问题。costOfNonAssignment = 20;[assignments, unassigndtracks, unassignddetections] =...assignDetectionsToTracks(成本、costOfNonAssignment);结束

更新已分配曲目

updateAssignedTracks功能更新每个指定的轨道与相应的检测。它调用正确的的方法愿景。KalmanFilter修正位置估计。接下来,它存储新的包围框,并将音轨的年龄和总可见计数增加1。最后,函数将不可见计数设置为0。

函数updateAssignedTracks() numAssignedTracks = size(assignments, 1);trackIdx =赋值(i, 1);detectionIdx =赋值(i, 2);质心=质心(detectionIdx,:);bbox = bboxes(detectionIdx,:);修正物体位置的估计%使用新的检测。正确的(跟踪(trackIdx)。kalmanFilter、质心);将预测的包围框替换为检测到的%包围框。跟踪(trackIdx)。Bbox = Bbox;%更新轨道的年龄。跟踪(trackIdx)。年龄= tracks(trackIdx).age + 1;%更新可见性。跟踪(trackIdx)。totalVisibleCount=...跟踪(trackIdx)。totalVisibleCount+ 1; tracks(trackIdx).consecutiveInvisibleCount = 0;结束结束

更新未分配曲目

将每个未分配的轨道标记为不可见,并将其年龄增加1。

函数updateUnassignedTracks ()i = 1:长度(unassigndtracks) ind = unassigndtracks (i);跟踪(印第安纳州)。年龄= tracks(ind).age + 1; tracks(ind).consecutiveInvisibleCount =...跟踪(印第安纳州)。consecutiveInvisibleCount + 1;结束结束

删除丢失的轨迹

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

函数deleteLostTracks ()如果isempty(跟踪)返回结束invisibleForTooLong = 20;ageThreshold = 8;计算轨道可见年龄的百分比。%年龄= [tracks(:).age];totalVisibleCounts = [tracks(:).totalVisibleCount];可见度= totalVisibleCounts ./ ages;查找“丢失”曲目的索引。lostInds =(年龄<年龄阈值&能见度< 0.6)|...(跟踪(:)。consecutiveInvisibleCount] >= invisibleForTooLong;删除丢失的轨道。tracks = tracks(~lostInds);结束

创建新轨道

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

函数createNewTracks() centroids = centroids(unassignddetections,:);bboxes = bboxes(unassignddetections,:);I = 1:大小(质心,1)质心=质心(I,:);Bbox = bboxes(i,:);创建一个卡尔曼过滤器对象。kalmanFilter = configureKalmanFilter(“ConstantVelocity”...质心,[200,50],[100,25],100);创建一个新轨道。newTrack = struct(...“id”nextId,...“bbox”bbox,...“kalmanFilter”kalmanFilter,...“年龄”, 1...“totalVisibleCount”, 1...“consecutiveInvisibleCount”, 0);将其添加到曲目数组中。。tracks(end + 1) = newTrack;自增下一个id。nextId = nextId + 1;结束结束

显示跟踪结果

displayTrackingResults函数为视频帧上的每个轨道和前景蒙版绘制一个边界框和标签ID。然后它在各自的视频播放器中显示帧和蒙版。

函数displayTrackingResults ()将帧和蒙版转换为uint8 RGB。Frame = im2uint8(Frame);掩码= uint8(repmat(掩码,[1,1,3])).* 255;minVisibleCount = 8;如果~ isempty(跟踪)噪声检测往往会导致短时间的轨迹。。只显示已可见超过。%最小帧数。reliableTrackInds =...(跟踪(:)。totalVisibleCount] > minVisibleCount;reliableTracks = tracks(reliableTrackInds);显示对象。如果未检测到对象%,显示其预测的边界框。如果~ isempty (reliableTracks)获取边界框。bboxes = cat(1, reliableTracks.bbox);获取id。ids = int32([reliableTracks(:).id]);为对象创建标签我们展示的是预测值而不是实际值。%的位置。标签= cellstr(int2str(ids'));predictedTrackInds =...[reliableTracks(:)。consecutiveInvisibleCount] > 0;isexpected = cell(size(labels));ispredict (predictedTrackInds) = {“预测”};标签= strcat(标签,isexpected);在框架上绘制物体。帧= insertObjectAnnotation(帧,“矩形”...bboxes、标签);在蒙版上绘制对象。掩码= insertObjectAnnotation“矩形”...bboxes、标签);结束结束显示掩码和帧。obj.maskPlayer.step(面具);obj.videoPlayer.step(框架);结束

总结

这个例子创建了一个基于运动的系统,用于检测和跟踪多个运动对象。尝试使用不同的视频,看看你是否能够检测和跟踪物体。尝试修改检测、分配和删除步骤的参数。

本例中的跟踪仅基于运动,假设所有物体都以匀速直线移动。当一个物体的运动明显偏离这个模型时,示例可能会产生跟踪误差。注意跟踪标记为#12的人的错误,当他被树挡住时。

跟踪误差的可能性可以通过使用更复杂的运动模型来降低,比如恒定加速度,或者对每个物体使用多个卡尔曼滤波器。此外,您还可以结合其他线索,以便随时间关联检测,例如大小、形状和颜色。

结束