function multiObjectTracking()
% ビデオの読み取り、移動オブジェクトの検出、および結果の表示に使用されるシステム オブジェクトを作成します。
obj = setupSystemObjects();
tracks = initializeTracks(); % create an empty array of tracks
nextId = 1; % ID of the next track
% 動いている物体を検出し、ビデオ フレーム全体で追跡します
while ~isDone(obj.reader)
frame = readFrame();
[centroids, bboxes, mask] = detectObjects(frame);
predictNewLocationsOfTracks();
[assignments, unassignedTracks, unassignedDetections] = ...
detectionToTrackAssignment();
updateAssignedTracks();
updateUnassignedTracks();
deleteLostTracks();
createNewTracks();
displayTrackingResults();
end
%% Create System object% ビデオ フレームの読み取り、フォアグラウンド オブジェクトの検出、および結果の表示に使用される System object を作成します。
function obj = setupSystemObjects()
% ビデオ I/O の初期化 % ファイルからビデオを読み取り、追跡されたオブジェクトを各フレームに描画し、ビデオを再生するためのオブジェクトを作成します。
vid = videoinput('winvideo', 1, 'YUY2_320x240');
src = getselectedsource(vid);
vid.FramesPerTrigger = 1;
% TriggerRepeat は 0 ベースで、常にトリガ数より 1% 少ない値です。
vid.TriggerRepeat = 899;
preview(vid);
start(vid);
stoppreview(vid);
savedvideo = getdata(vid);
% ビデオ ファイル リーダーを作成する
obj.reader = vision.VideoFileReader(savedvideo);
% 2 つのビデオ プレーヤーを作成します。1 つはビデオを表示し、もう 1 つはフォアグラウンド マスクを表示します
obj.videoPlayer = vision.VideoPlayer('Position', [20, 400, 700, 400]);
obj.maskPlayer = vision.VideoPlayer('Position', [740, 400, 700, 400]);
obj.detector = vision.ForegroundDetector('NumGaussians', 3, ...
'NumTrainingFrames', 40, 'MinimumBackgroundRatio', 0.7);
obj.blobAnalyser = vision.BlobAnalysis('BoundingBoxOutputPort', true, ...
'AreaOutputPort', true, 'CentroidOutputPort', true, ...
'MinimumBlobArea', 400);
end
function tracks = initializeTracks()
% トラックの空の配列を作成
tracks = struct(...
'id', {}, ...
'bbox', {}, ...
'kalmanFilter', {}, ...
'age', {}, ...
'totalVisibleCount', {}, ...
'consecutiveInvisibleCount', {});
end
%% ビデオ フレームの読み取り % ビデオ ファイルから次のビデオ フレームを読み取ります。
function frame = readFrame()
frame = obj.reader.step();
end
function [centroids, bboxes, mask] = detectObjects(frame)
% 前景を検出
mask = obj.detector.step(frame);
% モルフォロジー演算を適用してノイズを除去し、穴を埋める
mask = imopen(mask, strel('rectangle', [3,3]));
mask = imclose(mask, strel('rectangle', [15, 15]));
mask = imfill(mask, 'holes');
% ブロブ解析を実行して連結成分を見つけます
[~, 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
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
% 割り当て問題を解く
costOfNonAssignment = 20;
[assignments, unassignedTracks, unassignedDetections] = ...
assignDetectionsToTracks(cost, costOfNonAssignment);
end
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);
% 予測されたバウンディング ボックスを検出されたバウンディング ボックスで% 置き換えます
tracks(trackIdx).bbox = bbox;
% 更新トラックの年齢
tracks(trackIdx).age = tracks(trackIdx).age + 1;
% 更新の可視性
tracks(trackIdx).totalVisibleCount = ...
tracks(trackIdx).totalVisibleCount + 1;
tracks(trackIdx).consecutiveInvisibleCount = 0;
end
end
%% 割り当てられていないトラックを更新 % 割り当てられていない各トラックを非表示としてマークし、その経過時間を 1 増やします。
function updateUnassignedTracks()
for i = 1:length(unassignedTracks)
ind = unassignedTracks(i);
tracks(ind).age = tracks(ind).age + 1;
tracks(ind).consecutiveInvisibleCount = ...
tracks(ind).consecutiveInvisibleCount + 1;
end
end
function deleteLostTracks()
if isempty(tracks)
return;
end
invisibleForTooLong = 10;
ageThreshold = 8;
% トラックが表示されていた経過時間の割合を計算します
ages = [tracks(:).age];
totalVisibleCounts = [tracks(:).totalVisibleCount];
visibility = totalVisibleCounts ./ ages;
% 「失われた」トラックのインデックスを見つける
lostInds = (ages < ageThreshold & visibility < 0.6) | ...
[tracks(:).consecutiveInvisibleCount] >= invisibleForTooLong;
% 失われたトラックを削除
tracks = tracks(~lostInds);
end
function createNewTracks()
centroids = centroids(unassignedDetections, :);
bboxes = bboxes(unassignedDetections, :);
for i = 1:size(centroids, 1)
centroid = centroids(i,:);
bbox = bboxes(i, :);
% カルマン フィルター オブジェクトを作成します
kalmanFilter = configureKalmanFilter('ConstantVelocity', ...
centroid, [200, 50], [100, 25], 100);
% 新しいトラックを作成
newTrack = struct(...
'id', nextId, ...
'bbox', bbox, ...
'kalmanFilter', kalmanFilter, ...
'age', 1, ...
'totalVisibleCount', 1, ...
'consecutiveInvisibleCount', 0);
% トラックの配列に追加
tracks(end + 1) = newTrack;
% 次の ID をインクリメントします
nextId = nextId + 1;
end
end
function displayTrackingResults()
% フレームとマスクを uint8 RGB に変換します
frame = im2uint8(frame);
mask = uint8(repmat(mask, [1, 1, 3])) .* 255;
minVisibleCount = 8;
if ~isempty(tracks)
% ノイズの多い検出は、短命のトラックになる傾向があります% 最小フレーム数を超えて表示された% トラックのみを表示します。
reliableTrackInds = ...
[tracks(:).totalVisibleCount] > minVisibleCount;
reliableTracks = tracks(reliableTrackInds);
% オブジェクトを表示します。このフレームでオブジェクトが検出されなかった場合、% 予測されたバウンディング ボックスを表示します。
if ~isempty(reliableTracks)
% 境界ボックスを取得
bboxes = cat(1, reliableTracks.bbox);
% 取得 ID
ids = int32([reliableTracks(:).id]);
% オブジェクトのラベルを作成して、実際の位置ではなく予測された位置を表示します。
labels = cellstr(int2str(ids'));
predictedTrackInds = ...
[reliableTracks(:).consecutiveInvisibleCount] > 0;
isPredicted = cell(size(labels));
isPredicted(predictedTrackInds) = {' predicted'};
labels = strcat(labels, isPredicted);
% フレームに描画
frame = insertObjectAnnotation(frame, 'rectangle', ...
bboxes, labels);
% マスクに描画
mask = insertObjectAnnotation(mask, 'rectangle', ...
bboxes, labels);
end
end
% マスクとフレームを表示
obj.maskPlayer.step(mask);
obj.videoPlayer.step(frame);
end
displayEndOfDemoMessage(mfilename)
end