程序运行截图如下:
程序可以分为两部分:
1.每一帧检测运动objects;
2.实时的将检测得到的区域匹配到相同一个物体;
检测部分,用的是基于高斯混合模型的背景剪除法;
所谓单高斯模型,就是用多维高斯分布概率来进行模式分类
其中μ用训练样本均值代替,Σ用样本方差代替,X为d维的样本向量。通过高斯概率公式就可以得出类别C属于正(负)样本的概率。
而混合高斯模型(GMM)就是数据从多个高斯分布中产生的。每个GMM由K个高斯分布线性叠加而成。
P(x)=Σp(k)*p(x|k) 相当于对各个高斯分布进行加权(权系数越大,那么这个数据属于这个高斯分布的可能性越大)
而在实际过程中,我们是在已知数据的前提下,对GMM进行参数估计,具体在这里即为图片训练一个合适的GMM模型。
那么在前景检测中,我们会取静止背景(约50帧图像)来进行GMM参数估计,进行背景建模。分类域值官网取得0.7,经验取值0.7-0.75可调。这一步将会分离前景和背景,输出为前景二值掩码。
然后进行形态学运算,并通过函数返回运动区域的centroids和bboxes,完成前景检测部分。
跟踪部分,用的是卡尔曼滤波。卡尔曼是一个线性估计算法,可以建立帧间bboxs的关系。
跟踪分为5种状态: 1,新目标出现 2,目标匹配 3,目标遮挡 4,目标分离 5,目标消失。
卡尔曼原理在这儿我就不贴了,网上很多。
状态方程: X(k+1)=A(K+1,K)X(K)+w(K) 其中 X(k)=[x(k),y(k),w(k),h(k),v(k)], x,y,w,h,分别表示bboxs的横纵坐标,长,宽。
观测方程: Z(k)=H(k)X(k)+v(k) w(k), v(k),不相关的高斯白噪声。
定义好了观测方程与状态方程之后就可以用卡尔曼滤波器实现运动目标的跟踪,步骤如下:
1)计算运动目标的特征信息(运动质心,以及外接矩形)。
2)用得到的特征信息初始化卡尔曼滤波器(开始时可以初始为0)。
3)用卡尔曼滤波器对下一帧中对应的目标区域进行预测,当下一帧到来时,在预测区域内进行目标匹配。
4)如果匹配成功,则更新卡尔曼滤波器
在匹配的过程中,使用的是匈牙利匹配算法,匈牙利算法在这里有很好的介绍:匈牙利匹配算法
匈牙利匹配算法在此处是将新一帧图片中检测到的运动物体匹配到对应的轨迹。匹配过程是通过最小化卡尔曼预测得到的质心与检测到的质心之间的欧氏距离之和实现的
具体可以分为两步:
1, 计算损失矩阵,大小为[M N],其中,M是轨迹数目,N是检测到的运动物体数目。
2, 求解损失矩阵
function multiObjectTracking()
%% 创建系统对象
%创建用于读取视频帧,检测前景对象和显示结果的系统对象。
function obj = setupSystemObjects()
%初始化视频I/O
%创建对象以从文件中读取视频,在每个帧中绘制跟踪的对象,然后播放视频。
obj.reader = vision.VideoFileReader('v1.MP4'); %读入视频
%obj.reader = vision.VideoFileReader('Sample4.mp4');
% 创建两个视频播放器,一个显示视频,另一个显示处理后的二值图
obj.videoPlayer = vision.VideoPlayer('Position', [20, 400, 700, 400]);%创建两个窗口,设置显示窗口的位置
obj.maskPlayer = vision.VideoPlayer('Position', [740, 400, 700, 400]);
% 创建用于前景检测和分析的系统对象
% 前景检测器用于从背景分割运动对象。它输出一个二进制代码,其中像素值1对应于前景,而值0对应于背景。
% 设置检测子为混合高斯模型GMM进行前景检测,高斯核数目为3,前40帧为背景帧,域值为0.7
obj.detector = vision.ForegroundDetector('NumGaussians', 3, ...
'NumTrainingFrames', 40, 'MinimumBackgroundRatio', 0.7);
%相连的前景像素组可能对应于运动对象。 blob分析系统对象用于查找此类组(称为“blob”或“连接的组件”),
%并计算其特征,例如面积,质心和边界框。
%设置blob分析子,寻找连通模型,最大区域面积:400
obj.blobAnalyser = vision.BlobAnalysis('BoundingBoxOutputPort', true, ... %输出质心和外接矩形
'AreaOutputPort', true, 'CentroidOutputPort', true, ...
'MinimumBlobArea', 400);
end
%% 初始化轨迹
% | initializeTracks |函数会创建一个轨道数组,其中每个轨道都是代表视频中移动对象的结构。
% 该结构的目的是保持被跟踪对象的状态。状态包括用于检测以跟踪分配,跟踪终止和显示的信息。
% ?该结构包含以下字段:
% * | id | :轨道的整数ID
% * | bbox | :对象的当前边界框;用于显示
% * | kalmanFilter | :用于基于运动的跟踪的卡尔曼滤波器对象
% * |年龄| :自首次检测到轨道以来的帧数
% * | totalVisibleCount | :检测到轨道的帧总数(可见)
% * | conecutiveInvisibleCount | :未检测到轨道的连续帧数(不可见)。
% 嘈杂的检测往往会导致音轨寿命短。因此,该示例仅在跟踪了一定数量的帧后才显示该对象。
% |totalVisibleCount |时会发生这种情况超过指定的阈值。
% 当没有检测到与几个连续帧的轨迹相关联时,该示例假定该对象已离开视场并删除了该轨迹。
% 当ContinuousInvisibleCount |超过指定的阈值。如果跟踪的时间很短,则也可能会删除它作为噪声,
% 并且在大多数帧中都标记为不可见。
function tracks = initializeTracks()
% 创建一个空的轨迹矩阵数组
tracks = struct(...
'id', {}, ... %轨迹ID
'bbox', {}, ... %外接矩形
'kalmanFilter', {}, ...%轨迹的卡尔曼滤波器
'age', {}, ...%总数量
'totalVisibleCount', {}, ...%可见总帧数
'consecutiveInvisibleCount', {});%连续不可见帧数
end
%% 读取视频帧
%从视频文件中读取下一个视频帧。
function frame = readFrame()
frame = obj.reader.step();%激活读图函数
end
%% 目标检测
% | detectObjects | 函数返回检测到的对象的质心和边界框。 它还返回二进制掩码,其大小与输入帧相同。
% 值为1的像素对应于前景,值为0的像素对应于背景。
%
% 该功能使用前景检测器执行运动分割。 然后,它对生成的二进制蒙版执行形态学操作,
% 以去除噪点像素并填充其余斑点中的孔。
function [centroids, bboxes, mask] = detectObjects(frame)
% detect foreground
mask = obj.detector.step(frame);%使用检测子(混合高斯模型)得到前景图
% 对图像进行腐蚀和膨胀,最后补洞,消除物体中间的空洞
mask = imopen(mask, strel('rectangle', [3,3]));%开运算
mask = imclose(mask, strel('rectangle', [15, 15])); %闭运算
mask = imfill(mask, 'holes');%填洞
% 使用blob分析得到的所有连通域的中心,跟踪矩形框大小
[~, centroids, bboxes] = obj.blobAnalyser.step(mask);
end
%% 预测已跟踪轨迹的新位置
%使用卡尔曼滤波器预测当前帧中每个轨道的质心,并相应地更新其边界框。
function predictNewLocationsOfTracks()
for i = 1:length(tracks)%遍历已跟踪轨迹
bbox = tracks(i).bbox;
% 使用卡尔曼滤波器,根据以前的轨迹,预测当前位置中心
predictedCentroid = predict(tracks(i).kalmanFilter);
% 调整好预测中心位置后,将结果作为轨迹的跟踪矩形框
predictedCentroid = int32(predictedCentroid) - bbox(3:4) / 2;
tracks(i).bbox = [predictedCentroid, bbox(3:4)];%真正的当前位置
end
end
%% 分配新检测目标给轨迹
% 通过将成本最小化,可以将当前帧中的物体检测分配给现有轨道。成本定义为对应于轨道的检测的负对数似然性。
% 该算法包括两个步骤:
% 步骤1:使用| distance |计算将每个检测分配给每个轨道的成本| vision.KalmanFilter |的方法系统对象。
% 该成本考虑了轨道的预测质心和检测质心之间的欧几里得距离。它还包括预测的置信度,
% 该置信度由卡尔曼滤波器保持。结果存储在MxN矩阵中,其中M是磁道数,N是检测数。
% 步骤2:使用| assignDetectionsToTracks |解决由成本矩阵表示的分配问题。功能。
% 该函数获取成本矩阵和不为轨道分配任何检测的成本。
% 不将检测分配给轨道的成本的值取决于| distance |返回的值范围。 vision.KalmanFilter |的方法。
% 该值必须通过实验进行调整。设置得太低会增加创建新轨道的可能性,并可能导致轨道碎片。
% 将其设置得过高可能会导致与一系列单独的移动对象相对应的单个轨道。
% | assignDetectionsToTracks |函数使用匈牙利算法的Munkres版本来计算分配,从而最大程度地降低总成本。
% 它返回一个M x 2矩阵,该矩阵在其两列中包含分配的轨道和检测的相应索引。它还返回未分配的轨道和检测的索引。
function [assignments, unassignedTracks, unassignedDetections] = ...
detectionToTrackAssignment()
%获取轨迹个数和新检测目标的个数
nTracks = length(tracks);
nDetections = size(centroids, 1);
% 创建损失函数矩阵,行代表轨迹,列代表新检测目标
cost = zeros(nTracks, nDetections);
% 对每个轨迹来说,使用他们的卡尔曼滤波器预测的结果,与每个新检测目标的中心计算欧氏距离,
% 存入损失函数矩阵中
for i = 1:nTracks
cost(i, :) = distance(tracks(i).kalmanFilter, centroids);%损失矩阵计算
end
% 设置阈值为20,意思是当根据算法得到的分数低于20时就不分配
costOfNonAssignment = 20;
% 匈牙利匹配算法根据损失函数和阈值分配好轨迹和检测目标
[assignments, unassignedTracks, unassignedDetections] = ...
assignDetectionsToTracks(cost, costOfNonAssignment);
end
%% 更新已分配的轨迹
% | updateAssignedTracks | 功能使用相应的检测更新每个分配的轨道。 它称为|正确|。
% | vision.KalmanFilter |的方法 更正位置估计。
% 接下来,它存储新的边界框,并将轨道的寿命和总可见计数增加1。
% 最后,该函数将不可见计数设置为0。
function updateAssignedTracks()
numAssignedTracks = size(assignments, 1);
for i = 1:numAssignedTracks
trackIdx = assignments(i, 1);
detectionIdx = assignments(i, 2);
centroid = centroids(detectionIdx, :);
bbox = bboxes(detectionIdx, :);
% 根据轨迹对应的检测目标位置中心修正他的卡尔曼滤波器
correct(tracks(trackIdx).kalmanFilter, centroid);
% replace predicted bounding box with detected
% bounding box
tracks(trackIdx).bbox = bbox;
% 轨迹年龄+1
tracks(trackIdx).age = tracks(trackIdx).age + 1;
% 轨迹总可见帧数+1,轨迹连续不可见帧数清零
tracks(trackIdx).totalVisibleCount = ...
tracks(trackIdx).totalVisibleCount + 1;
tracks(trackIdx).consecutiveInvisibleCount = 0;
end
end
%% 更新未分配的轨迹
% Mark each unassigned track as invisible, and increase its age by 1.
function updateUnassignedTracks()
for i = 1:length(unassignedTracks)
ind = unassignedTracks(i);
tracks(ind).age = tracks(ind).age + 1;% 轨迹年龄+1
tracks(ind).consecutiveInvisibleCount = ...
tracks(ind).consecutiveInvisibleCount + 1;% 轨迹总可见帧数+1
end
end
%% Delete Lost Tracks
% | deleteLostTracks | 函数删除连续太多帧不可见的轨迹。
% 它还会删除最近创建的轨道,这些轨道对于总体上太多帧是不可见的。
function deleteLostTracks()
if isempty(tracks)
return;
end
% 设置阈值为20,意思是当前连续不可见帧数大于等于20时就丢弃轨迹
invisibleForTooLong =20;
% 设置阈值为8,指的是当年龄小于8时,根据总可见的帧数与年龄的比值
%(该比值阈值设为0.6)丢弃轨迹。否则根据连续不可见帧数丢弃轨迹
ageThreshold = 8;
% compute the fraction of the track's age for which it was visible
ages = [tracks(:).age];
totalVisibleCounts = [tracks(:).totalVisibleCount];
visibility = totalVisibleCounts ./ ages;
% find the indices of 'lost' tracks
lostInds = (ages < ageThreshold & visibility < 0.6) | ...
[tracks(:).consecutiveInvisibleCount] >= invisibleForTooLong;
% delete lost tracks
tracks = tracks(~lostInds);
end
%% 创建新轨迹
% 根据未分配的检测创建新轨道。 假定所有未分配的检测都是新轨道的开始。
% 实际上,您可以使用其他提示来消除嘈杂的检测,例如大小,位置或外观。
function createNewTracks()
centroids = centroids(unassignedDetections, :);
bboxes = bboxes(unassignedDetections, :);
for i = 1:size(centroids, 1)
centroid = centroids(i,:);
bbox = bboxes(i, :);
% 创建卡尔曼滤波器
% 动态模型:匀速 初始化位置:新检测目标中心
% 初始化估计误差(位置误差、速度误差):[200,50]
% 动态噪声(位置误差,速度误差):[100,25]
% 测量噪声(位置噪声):100
kalmanFilter = configureKalmanFilter('ConstantVelocity', ...
centroid, [200, 50], [100, 25], 100);
% create a new track
newTrack = struct(...
'id', nextId, ...
'bbox', bbox, ...
'kalmanFilter', kalmanFilter, ...
'age', 1, ...
'totalVisibleCount', 1, ...
'consecutiveInvisibleCount', 0);
% add it to the array of tracks
tracks(end + 1) = newTrack;
% increment the next id
nextId = nextId + 1;
end
end
%% 显示跟踪结果
% | displayTrackingResults | 函数为视频帧和前景蒙版上的每个轨道绘制边界框和标签ID。
% 然后,它将在各自的视频播放器中显示帧和遮罩。
function displayTrackingResults()
% convert the frame and the mask to uint8 RGB
frame = im2uint8(frame);
mask = uint8(repmat(mask, [1, 1, 3])) .* 255;
minVisibleCount = 8;
if ~isempty(tracks)
% noisy detections tend to result in short-lived tracks
% only display tracks that have been visible for more than
% a minimum number of frames.
reliableTrackInds = ...
[tracks(:).totalVisibleCount] > minVisibleCount;
reliableTracks = tracks(reliableTrackInds);
% 当总可见帧数大于阈值minVisibleCount=8时才显示出来,防止一些噪声产生
% display the objects. If an object has not been detected
% in this frame, display its predicted bounding box.
if ~isempty(reliableTracks)
% get bounding boxes
bboxes = cat(1, reliableTracks.bbox);
% get ids
ids = int32([reliableTracks(:).id]);
% create labels for objects indicating the ones for
% which we display the predicted rather than the actual
% location
labels = cellstr(int2str(ids'));
predictedTrackInds = ...
[reliableTracks(:).consecutiveInvisibleCount] > 0;
isPredicted = cell(size(labels));
isPredicted(predictedTrackInds) = {' predicted'};
labels = strcat(labels, isPredicted);
% draw on the frame
frame = insertObjectAnnotation(frame, 'rectangle', ...
bboxes, labels);
% draw on the mask
mask = insertObjectAnnotation(mask, 'rectangle', ...
bboxes, labels);
end
end
% display the mask and the frame
obj.maskPlayer.step(mask);
obj.videoPlayer.step(frame);
end
displayEndOfDemoMessage(mfilename)
end
% 总结
% 本示例创建了一个基于运动的系统,用于检测和跟踪多个运动对象。
% 尝试使用其他视频,看看是否能够检测和跟踪对象。 尝试修改检测,分配和删除步骤的参数。
%
% 本示例中的跟踪仅基于运动,并假设所有对象均以恒定速度沿直线运动。
% 当对象的运动明显偏离此模型时,该示例可能会产生跟踪误差。 注意被树遮挡的跟踪#12的人时出现错误。
%
% 可以通过使用更复杂的运动模型(例如恒定加速度)或通过对每个对象使用多个卡尔曼滤波器来降低跟踪错误的可能性。
% 此外,您还可以合并其他线索,以便随时间关联检测结果,例如大小,形状和颜色。
静止背景下的卡尔曼多目标跟踪
Matlab多目标跟踪示例
运动目标检测_混合高斯背景建模