본문 바로가기

MATLAB/ㄴ 영상 딥러닝

예제_물체의 자동 감지(Detection) 및 동작 기반 추적(Tracking)

참고 문서: https://kr.mathworks.com/help/vision/ug/motion-based-multiple-object-tracking.html

 

Motion-Based Multiple Object Tracking - MATLAB & Simulink - MathWorks 한국

이 예제의 수정된 버전이 있습니다. 사용자가 편집한 내용을 반영하여 이 예제를 여시겠습니까?

kr.mathworks.com

 

다음은 정지한 카메라에서의 비디오에서 움직이는 물체의 자동 감지 및 동작 기반 추적을 수행하는 방법을 보여주는

예제입니다. 움직이는 물체의 감지와 동작 기반 추적은 활동 인식, 교통 모니터링 및 자동차 안전과 같은 다양한 컴퓨터 

비전 응용 프로그램의 중요한 구성 요소입니다. 동작 기반 물체 추적 문제는 다음 두 부분으로 나눌 수 있습니다.


1. 각 프레임에서 움직이는 물체의 감지
2. 시간에 따라 동일한 물체에 해당하는 감지들을 연결


움직이는 물체의 감지는 가우시안 혼합 모델(Gaussian mixture models)을 기반으로 한 배경 차분 알고리즘을 

사용합니다. 결과적인 전경 마스크에 모폴로지 연산을 적용하여 잡음을 제거합니다. 마지막으로, 덩어리 분석을 통해 

연결된 픽셀 그룹을 감지하며, 이 그룹은 움직이는 물체에 해당할 가능성이 높습니다.
감지를 동일한 물체와 연결하는 것은 움직임만을 기반으로 합니다. 각 추적의 움직임은 칼만 필터(Kalman filter)를 

사용하여 추정됩니다. 필터는 각 프레임에서 추적의 위치를 예측하고 각 감지가 각 추적에 할당될 확률을 결정하는 데 

사용됩니다.

 

추적 유지는 이 예제의 중요한 측면이 됩니다. 주어진 프레임에서 일부 감지는 추적에 할당되고, 다른 감지와 추적은 

할당되지 않을 수 있습니다. 할당된 추적은 해당 감지를 사용하여 업데이트됩니다. 할당되지 않은 추적은 보이지 않는 

것으로 표시됩니다. 할당되지 않은 감지는 새로운 추적을 시작합니다.
각 추적은 할당되지 않은 상태가 연속적으로 일정한 임계값을 초과하면 물체가 시야에서 벗어났다고 가정하고 해당 

추적을 삭제합니다.

 

더 자세한 정보는 "다중 물체 추적(Multiple Object Tracking)"을 참조하십시오.
이 예제는 메인 코드가 상단에 있고 중첩된 함수 형태로 도우미 루틴이 있는 함수입니다.

 
 
 
 
1. 메인 코드
 
 

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


 

2. Create System Objects
 
비디오 프레임을 읽고, 전경 물체를 감지하고, 결과를 표시하기 위한 시스템 객체를 MATLAB에서 생성합니다.

 


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


 

3. Initialize Tracks
 
initializeTracks 함수는 비디오에서 움직이는 물체를 나타내는 각 트랙을 포함하는 배열을 생성합니다. 
각 트랙은 추적된 물체의 상태를 유지하기 위한 구조체입니다. 이 상태에는 감지와 추적 할당, 추적 종료 및 표시에 
사용되는 정보가 포함됩니다. 구조체에는 다음과 같은 필드가 포함되어 있습니다:

     1. id: 트랙의 정수 ID
     2. bbox: 현재 물체의 바운딩 박스; 표시에 사용됨
     3. kalmanFilter: 움직임 기반 추적에 사용되는 칼만 필터 객체
     4. age: 트랙이 처음 감지된 후 프레임 수
     5. totalVisibleCount: 트랙이 감지된(가시적인) 전체 프레임 수
     6. consecutiveInvisibleCount: 트랙이 감지되지 않은 연속된 프레임 수

잡음이 많은 감지는 짧은 수명의 트랙을 생성하는 경향이 있습니다. 이러한 이유로 예제는 특정 프레임 수를 초과한 
후에만 객체를 표시합니다. 이는 totalVisibleCount가 지정된 임계값을 초과할 때 발생합니다.
트랙에 대한 연속된 프레임에 감지가 연결되지 않을 때, 예제는 물체가 시야에서 벗어났다고 가정하고 해당 트랙을 
삭제합니다. 이는 consecutiveInvisibleCount가 지정된 임계값을 초과할 때 발생합니다. 트랙은 또한 대부분의 프레임에 
대해 가시적이지 않도록 표시되고 짧은 시간 동안 추적되었다면 잡음으로 삭제될 수 있습니다.
 
 
 

function tracks = initializeTracks()
     % 트랙을 포함하는 빈 배열을 생성합니다.
     tracks = struct( ... % struct(): 빈 배열을 생성하는 함수
          'id',{}, ...
          'bbox', {}, ...
          'kalmanFilter', {}, ...
          'age', {}, ...
          'totalVisibleCount', {}, ...
          'consecutiveInvisibleCount', {});
end


 
4. Detect Objects
 
detectObjects 함수는 감지된 물체의 중심점과 바운딩 박스를 반환합니다. 
또한 입력 프레임과 동일한 크기를 갖는 이진 마스크를 반환합니다. 값이 1인 픽셀은 전경에 해당하고, 값이 0인 픽셀은 
배경에 해당합니다.
이 함수는 전경 감지기를 사용하여 움직임 분할을 수행합니다. 그런 다음 결과 이진 마스크에 대한 모폴로지 연산을 
수행하여 잡음 픽셀을 제거하고 남은 덩어리의 구멍을 메우게 됩니다.
 

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


 

5. Predict New Locations of Existing Tracks
 
칼만 필터를 사용하여 현재 프레임에서 각 트랙의 중심점을 예측하고 그에 따라 해당 트랙의 바운딩 박스를
업데이트합니다.



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


 

6. Assign Detections to Tracks
 
현재 프레임에서 객체 감지를 기존 트랙에 할당하는 작업은 비용을 최소화하는 방식으로 수행됩니다.
비용은 각 트랙에 대한 감지의 음의 로그 우도로 정의됩니다.

이 알고리즘은 두 단계로 진행됩니다:

단계 1: vision.KalmanFilter System 객체의 distance 메서드를 사용하여 모든 감지를 각 트랙에 할당하는 비용을 
계산합니다. 비용은 트랙의 예측된 중심점과 감지의 중심점 간의 유클리드 거리를 고려합니다. 또한 칼만 필터에서 
유지되는 예측의 신뢰도를 포함합니다. 결과는 MxN 행렬에 저장되며, 여기서 M은 트랙 수이고 N은 감지 수입니다.

단계 2: cost 행렬과 어떤 트랙에 대한 감지를 할당하지 않은 경우의 비용을 사용하여 cost 행렬로 표현된 할당 문제를 
해결합니다. assignDetectionsToTracks 함수는 cost 행렬과 비할당 트랙에 대한 비용을 사용합니다.

감지를 트랙에 할당하지 않은 경우의 비용 값은 vision.KalmanFilter의 distance 메서드가 반환하는 값 범위에 따라 
결정됩니다. 이 값은 실험적으로 조정해야 합니다. 값이 너무 낮으면 새 트랙을 생성할 가능성이 높아지며 
트랙 조각화의 결과를 초래할 수 있습니다. 값이 너무 높으면 별개의 움직이는 객체 시리즈에 해당하는 단일 트랙이 
생길 수 있습니다.

assignDetectionsToTracks 함수는 전체 비용을 최소화하는 할당을 계산하기 위해 Munkres의 헝가리 알고리즘을
사용합니다. 이 함수는 할당된 트랙과 감지의 해당 인덱스를 포함하는 M x 2 행렬을 반환하며,
두 열에는 할당된 트랙 및 감지의 인덱스가 포함됩니다. 또한 할당되지 않은 트랙 및 감지의 인덱스도 반환합니다.
 

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


 

7. Update Assigned Tracks
 
updateAssignedTracks 함수는 각 할당된 트랙을 해당 감지로 업데이트합니다. 
이 함수는 vision.KalmanFilter의 correct 메서드를 호출하여 위치 추정값을 보정합니다. 
그 다음, 새로운 바운딩 박스를 저장하고, 트랙의 age와 totalVisibleCount를 1씩 증가시킵니다. 
마지막으로, 함수는 invisible count를 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);

 
          % 새로운 바운딩 박스를 저장
          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


 

8. Update Unassigned Tracks
 
할당되지 않은 각 트랙을 보이지 않는 것으로 표시하고, 그 나이를 1 증가시킵니다.
 

function updateUnassignedTracks()
     for i = 1:length(unassignedTracks)
          ind = unassignedTracks(i);
          tracks(ind).age = tracks(ind).age + 1;
          tracks(ind).consecutiveInvisibleCount + 1;
     end
end


 

9. Delete Lost Tracks
 
deleteLostTracks 함수는 일련의 연속된 프레임 동안 보이지 않았던 트랙을 삭제합니다.
또한 전체적으로 너무 많은 프레임 동안 보이지 않은 최근에 생성된 트랙도 삭제합니다.
 

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


 

10. Create New Tracks
 
할당되지 않은 감지로부터 새로운 트랙을 생성합니다. 실제로는 크기, 위치 또는 외관과 같은 다른 힌트를 사용하여
잡음이 있는 감지를 제거할 수 있습니다. 여기서는 할당되지 않은 감지가 새로운 트랙의 시작으로 가정합니다.

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


 

11. Display Tracking Results
 
displayTrackingResults 함수는 각 트랙에 대한 바운딩 박스와 레이블 ID를 비디오 프레임과 전경 마스크에 그립니다.
그런 다음 프레임과 마스크를 각각의 비디오 플레이어에 표시합니다.
 

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


 

Summary
 
이 예제는 여러 움직이는 물체를 감지하고 추적하기 위한 움직임 기반 시스템을 생성했습니다. 
다른 비디오를 사용하거나 감지, 할당 및 삭제 단계의 매개변수를 수정하여 다른 객체를 감지하고 추적해 보실 수 
있습니다.

이 예제에서의 추적은 모든 물체가 직선으로 일정한 속도로 이동하는 가정에만 기반하고 있으므로 물체의 움직임이 
이 모델에서 크게 벗어나면 추적 오류가 발생할 수 있습니다. 특히 나무에 가려져서 번호 12로 표시된 사람의 추적에서 
오류를 볼 수 있습니다. 추적 오류의 가능성은 상수 가속도와 같은 더 복잡한 움직임 모델을 사용하거나, 
각 객체마다 별도의 칼만 필터를 사용함으로써 줄일 수 있습니다. 또한 크기, 모양 및 색상과 같은 다른 힌트를 사용하여 
시간에 따른 감지를 연결하는 데 다른 방법을 통합할 수도 있습니다.

 

MotionBasedMultiObjectTrackingExample.m
0.01MB