참고 문서: https://kr.mathworks.com/help/vision/ug/motion-based-multiple-object-tracking.html
다음은 정지한 카메라에서의 비디오에서 움직이는 물체의 자동 감지 및 동작 기반 추적을 수행하는 방법을 보여주는
예제입니다. 움직이는 물체의 감지와 동작 기반 추적은 활동 인식, 교통 모니터링 및 자동차 안전과 같은 다양한 컴퓨터
비전 응용 프로그램의 중요한 구성 요소입니다. 동작 기반 물체 추적 문제는 다음 두 부분으로 나눌 수 있습니다.
1. 각 프레임에서 움직이는 물체의 감지
2. 시간에 따라 동일한 물체에 해당하는 감지들을 연결
움직이는 물체의 감지는 가우시안 혼합 모델(Gaussian mixture models)을 기반으로 한 배경 차분 알고리즘을
사용합니다. 결과적인 전경 마스크에 모폴로지 연산을 적용하여 잡음을 제거합니다. 마지막으로, 덩어리 분석을 통해
연결된 픽셀 그룹을 감지하며, 이 그룹은 움직이는 물체에 해당할 가능성이 높습니다.
감지를 동일한 물체와 연결하는 것은 움직임만을 기반으로 합니다. 각 추적의 움직임은 칼만 필터(Kalman filter)를
사용하여 추정됩니다. 필터는 각 프레임에서 추적의 위치를 예측하고 각 감지가 각 추적에 할당될 확률을 결정하는 데
사용됩니다.
추적 유지는 이 예제의 중요한 측면이 됩니다. 주어진 프레임에서 일부 감지는 추적에 할당되고, 다른 감지와 추적은
할당되지 않을 수 있습니다. 할당된 추적은 해당 감지를 사용하여 업데이트됩니다. 할당되지 않은 추적은 보이지 않는
것으로 표시됩니다. 할당되지 않은 감지는 새로운 추적을 시작합니다.
각 추적은 할당되지 않은 상태가 연속적으로 일정한 임계값을 초과하면 물체가 시야에서 벗어났다고 가정하고 해당
추적을 삭제합니다.
더 자세한 정보는 "다중 물체 추적(Multiple Object Tracking)"을 참조하십시오.
이 예제는 메인 코드가 상단에 있고 중첩된 함수 형태로 도우미 루틴이 있는 함수입니다.
function MotionBasedMultiObjectTrackingExample() |
% 비디오 읽기, 움직이는 물체 감지 및 결과 표시에 사용되는 시스템 객체를 생성합니다.
obj = setupSystemObjects();
tracks = initializeTracks(); % 빈 배열을 생성합니다.
nextId = 1;
% 움직이는 물체를 감지하고 비디오 프레임 간에 그들을 추적합니다.
while hasFrame(obj.reader)
frame = readFrame(obj.reader);
[centroids, bboxes, mask] = detectObjects(frame);
predictNewLocationsOfTracks();
[assignments, unassignedTracks, unassignedDetections] = detectionToTrackAssignment();
updateAssignedTracks();
updateUnassignedTracks();
deleteLostTracks();
createNewTracks();
displayTrackingResults();
end
|
function obj = setupSystemObjects()
% 비디오 I/O 초기화
% 파일에서 비디오를 읽고 각 프레임에서 추적된 물체를 그리고 비디오를 재생하기 위한 객체를 생성합니다.
% 비디오 리더 객체 생성
obj.reader = VideoReader('atrium.mp4'); % VideoReader(): 비디오파일을 읽어오는 함수.
% 두 개의 비디오 플레이어 생성, 하나는 비디오를 표시하고, 다른 하나는 전경 마스크를 표시합니다.
obj.maskPlayer = vision.VideoPlayer('Position', [740, 400, 700, 400]); % 700,400의 위치에서 700x400 크기의 영상을 실행
obj.videoPlayer = vision.VideoPlayer('Position', [20, 400, 700, 400]); % 20,400의 위치에서 700x400 크기의 영상을 실행
% 전경 감지 및 덩어리 분석을 위한 시스템 객체 생성
% 전경 감지기는 움직이는 물체를 배경으로부터 분리하는 데 사용됩니다.
% 이것은 이진 마스크를 출력하며, 픽셀 값이 1은 전경을 나타내고 값이 0은 배경을 나타냅니다.
obj.detector = vision.ForegroundDetector('NumGaussians', 3, ...
% vision.ForegroundDetector(): 전경감지기(System object)를 생성하는데 사용됨 'NumTrainingFrames', 40, 'MinimumBackgroundRatio', 0.7);
% NumGaussians: 가우시안 혼합 모델을 사용하여 배경과 전경을 분리. % NumTrainingFrames: 초기 배경 모델을 학습하기 위해 사용되는 프레임 수를 지정 % MinimumBackgroundRatio: 배경 모델의 최소 비율을 지정 % 연결된 전경 픽셀 그룹은 움직이는 물체에 해당할 가능성이 높습니다.
% 덩어리 분석 시스템 객체는 이러한 그룹을 찾고 ('블롭' 또는 '연결 요소'라고 함) 그 특성을 계산합니다.
% 이러한 특성에는 면적, 중심점 및 바운딩 박스가 포함됩니다. obj.blobAnalyser = vision.BlobAnalysis('BoundingBoxOutputPort', true, ...
% vision.BlobAnalysis(): 덩어리 분석기(System object)를 생성하는데 사용됨 'AreaOutputPort', true, 'CentroidOutputPort', true, ... % BoundingBoxOutputPort: 바운딩 박스 정보를 추출
% AreaOutputPort: 덩어리(블롭)의 면적을 출력 포트로 설정 % CentroidOutputPort: 덩어리(블롭)의 중심점을 출력 포트로 설정 % MinimumBlobArea: 덩어리(블롭)로 간주되기 위한 최소 면적을 지정 'MinimumBlobArea', 400);
end
|
1. id: 트랙의 정수 ID
2. bbox: 현재 물체의 바운딩 박스; 표시에 사용됨
3. kalmanFilter: 움직임 기반 추적에 사용되는 칼만 필터 객체
4. age: 트랙이 처음 감지된 후 프레임 수
5. totalVisibleCount: 트랙이 감지된(가시적인) 전체 프레임 수
6. consecutiveInvisibleCount: 트랙이 감지되지 않은 연속된 프레임 수
잡음이 많은 감지는 짧은 수명의 트랙을 생성하는 경향이 있습니다. 이러한 이유로 예제는 특정 프레임 수를 초과한
트랙에 대한 연속된 프레임에 감지가 연결되지 않을 때, 예제는 물체가 시야에서 벗어났다고 가정하고 해당 트랙을
function tracks = initializeTracks()
% 트랙을 포함하는 빈 배열을 생성합니다.
tracks = struct( ... % struct(): 빈 배열을 생성하는 함수
'id',{}, ...
'bbox', {}, ...
'kalmanFilter', {}, ...
'age', {}, ...
'totalVisibleCount', {}, ...
'consecutiveInvisibleCount', {});
end
|
이 함수는 전경 감지기를 사용하여 움직임 분할을 수행합니다. 그런 다음 결과 이진 마스크에 대한 모폴로지 연산을
function [centroids, bboxes, mask] = detectObjects(frame)
% 전경 감지
mask = obj.detector.step(frame);
% 모폴로지 연산을 적용하여 잡음 픽셀을 제거하고 남은 덩어리의 구멍을 메움
mask = imopen(mask, strel('rectangle', [3,3]));
% imopen(): 영상에 모폴로지 열기 연산 수행(작은 객체를 제거하고 노이즈를 감소) % strel(): 모폴로지 평탄구조를 생성 mask = imclose(mask, strel('rectangle', [15, 15]));
% imclose(): 영상에 모폴로지 닫기 연산 수행(작은 구멍을 메우고 객체를 연결) mask = imfill(mask, 'holes'); % imfill(): 영상의 구멍을 메우는 작업을 수행
% 블롭 분석을 수행하여 연결된 요소를 찾음
[~, 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: vision.KalmanFilter System 객체의 distance 메서드를 사용하여 모든 감지를 각 트랙에 할당하는 비용을
단계 2: cost 행렬과 어떤 트랙에 대한 감지를 할당하지 않은 경우의 비용을 사용하여 cost 행렬로 표현된 할당 문제를
감지를 트랙에 할당하지 않은 경우의 비용 값은 vision.KalmanFilter의 distance 메서드가 반환하는 값 범위에 따라
assignDetectionsToTracks 함수는 전체 비용을 최소화하는 할당을 계산하기 위해 Munkres의 헝가리 알고리즘을
function [assignments, unassignedTracks, unassignedDetections] = detectionToTrackAssignment()
nTracks = length(tracks); % nTracks: 트랙의 수
nDetections = size(centroids, 1); % nDetections: 감지의 수
% 모든 감지를 각 트랙에 할당하는 비용을 계산. 비용이 낮을수록 감지가 트랙에 할당될 가능성이 높음.
cost = zeros(nTracks, nDetections);
for i = 1:nTracks
cost(i, :) = distance(tracks(i).kalmanFilter, centroids);
% distance(): kalmanFilter가 예측한 위치와 감지된 객체의 위치 사이의 거리를 계산 end
% cost 행렬로 표현된 할당 문제를 해결.
costOfNonAssignment = 20;
[assignments, unassignedTracks, unassignedDetections] = assignDetectionsToTracks(cost, costOfNonAssignment);
% assignDetectionsToTracks(): 감지를 트랙에 할당하는 작업을 수행 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;
% 트랙의 age를 업데이트
tracks(trackIdx).age = tracks(trackIdx).age + 1;
% visibility를 업데이트
tracks(trackIdx).totalVisibleCount = tracks(trackIdx).totalVisibleCount + 1;
tracks(trackIdx).consecutiveInvisibleCount = 0;
end
end
|
function updateUnassignedTracks()
for i = 1:length(unassignedTracks)
ind = unassignedTracks(i);
tracks(ind).age = tracks(ind).age + 1;
tracks(ind).consecutiveInvisibleCount + 1;
end
end
|
function deleteLostTracks()
% 이 함수는 추적 중에 누락된 트랙을 정리하고, 불필요한 트랙 정보를 삭제하여 추적 시스템의 성능을 유지하고 메모리를 관리합니다. if isempty(tracks) % 트랙(객체) 목록이 비어있는지를 확인하며, 삭제할 트랙이 없다면 함수를 종료함.
return;
end
invisibleForTooLong = 20; % 20프레임동안 감지되지 않은 트랙은 삭제 대상
ageThreshold = 8; % 나이(트랙이 처음 감지된 이후 프레임의 수)가 8 미만인 트랙은 삭제 대상
% visible한 트랙의 나이를 1 증가시킴
ages = [tracks(:).age]; % 트랙의 나이
totalVisibleCounts = [tracks(:).totalVisibleCount]; % 총 가시 횟수
visibility = totalVisibleCounts ./ ages; % 가시성을 나타내는 배열
% 'lost' 트랙의 index를 찾음
lostInds = (ages < ageThreshold & visibility < 0.6) | [tracks(:).consecutiveInvisibleCount] >= invisibleForTooLong;
% lost 트랙은 두 조건 중 하나의 조건을 충적하는 경우에 해당함. % ages가 ageThreshold 미만이면서 visibility가 0.6 미만인 경우 or consecutiveInvisibleCount가
invisibleForTooLong 이상인 경우 % lost 트랙을 삭제
tracks = tracks(~lostInds);
end
|
function createNewTracks()
% 이 함수는 할당되지 않은 감지를 기반으로 새로운 트랙을 생성하고 이를 tracks 배열에 추가하여 객체 추적 시스템을 업데이트합니다. centroids = centroids(unassignedDetections, :); % 할당되지 않은 감지들의 중심점
bboxes = bboxes(unassignedDetections, :); % 할당되지 않은 감지들의 바운딩박스 정보
for i = 1:size(centroids, 1) % for 루프를 사용하여 centroids 배열의 각 행을 반복하면서 새로운 트랙을 생성합니다.
centroid = centroids(i,:);
bbox = bboxes(i, :);
% Kalman filter 객체를 생성
kalmanFilter = configureKalmanFilter('ConstantVelocity', centroid, [200, 50], [100, 25], 100);
% 새 트랙을 생성. newTrack : 새로운 트랙을 나타내는 구조체
newTrack = struct( ...
'id', nextId, ...
'bbox', bbox, ...
'kalmanFilter', kalmanFilter, ...
'age', 1, ...
'totalVisibleCount', 1, ...
'consecutiveInvisibleCount', 0);
% 트랙의 배열에 이것을 더함
tracks(end+1) = newTrack;
% next id에 1씩 더함
nextId = nextId + 1;
end
end
|
function displayTrackingResults()
% 프레임과 마스크를 uint8 RGB로 변경
frame = im2uint8(frame);
mask = uint8(repmat(mask, [1,1,3])) .* 255;
minVisibleCount = 8; % minVisibleCount 변수 = 트랙을 표시하기 위한 최소한의 가시 횟수를 나타냅니다.
if ~isempty(tracks)
% 잡음이 많은 감지는 짧은 수명의 트랙을 생성하는 경향이 있습니다.
% 최소한 일정한 수의 프레임 동안 가시적인 트랙만 표시합니다.
reliableTrackInds = [tracks(:).totalVisibleCount] > minVisibleCount;
relibaleTracks = tracks(reliableTrackInds);
% 물체를 표시합니다. 만약 현재 프레임에서 물체가 감지되지 않았다면 예측된 바운딩 박스를 표시합니다.
if ~isempty(reliableTracks) % tracks 배열이 비어 있지 않은 경우에만 추적 결과를 표시
% 바운딩 박스를 표시
bboxes = cat(1, reliableTracks.bbox);
% reliableTracks 배열에 포함된 트랙들의 바운딩 박스 정보를 수직 방향(열 방향)으로 연결하여 % 하나의 행렬(bboxes)로 만드는 작업 % ids를 표시
ids = int32([reliableTracks(:).id]);
% 예측된 위치가 실제 위치 대신 표시되는 객체를 나타내는 레이블을 생성합니다.
labels = cellstr(int2str(ids')); % 추적된 객체의 ID를 문자열로 변환
predictedTrackInds = [reliableTracks(:).consecutiveInvisibleCount] >0;
% 예측된 위치에 있는 트랙을 식별하기 위한 논리값 배열을 생성 isPredicted = cell(size(labels)); % labels와 같은 크기의 셀 배열을 생성합니다.
isPredicted(predictedTrackInds) = {' predicted'};
% 예측된 위치에 있는 트랙인 경우 해당 셀에 ' predicted' 문자열을 할당하고, 나머지 셀은 비워둡니다. labels = strcat(labels, isPredicted);
% labels와 isPredicted를 결합하여 labels에 예측된 위치에 있는 객체를 나타내는 ' predicted' 레이블을 추가 % 프레임에 그림
frame = insertObjectAnnotation(frame, 'rectangle', bboxes, labels);
% 마스크에 그림
mask = insertObjectAnnotation(mask, 'rectangle', bboxes, labels);
end
end
% 프레임과 마스크를 각각의 비디오 플레이어에 표시
obj.maskPlayer.step(mask);
obj.videoPlayer.step(frame);
end
|
이 예제에서의 추적은 모든 물체가 직선으로 일정한 속도로 이동하는 가정에만 기반하고 있으므로 물체의 움직임이