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');
    
        % 두 개의 비디오 플레이어 생성, 하나는 비디오를 표시하고, 다른 하나는 전경 마스크를 표시합니다.
        obj.maskPlayer =  vision.VideoPlayer('Position', [740, 400, 700, 400]);
        obj.videoPlayer = vision.VideoPlayer('Position', [20, 400, 700, 400]);
    
    
        % 전경 감지 및 덩어리 분석을 위한 시스템 객체 생성
    
        % 전경 감지기는 움직이는 물체를 배경으로부터 분리하는 데 사용됩니다.
        % 이것은 이진 마스크를 출력하며, 픽셀 값이 1은 전경을 나타내고 값이 0은 배경을 나타냅니다.
        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 [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

        % cost 행렬로 표현된 할당 문제를 해결.
        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;

            % 트랙의 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;
        ageThreshold = 8;

        % visible한 트랙의 나이를 1 증가시킴
        ages = [tracks(:).age];
        totalVisibleCounts = [tracks(:).totalVisibleCount];
        visibility = totalVisibleCounts ./ ages;

        % 'lost' 트랙의 index를 찾음
        lostInds = (ages < ageThreshold & visibility < 0.6) | [tracks(:).consecutiveInvisibleCount] >= invisibleForTooLong;

        % lost 트랙을 삭제
        tracks = tracks(~lostInds);
    end

    function createNewTracks()
        centroids = centroids(unassignedDetections, :);
        bboxes = bboxes(unassignedDetections, :);

        for i = 1:size(centroids, 1)
            centroid = centroids(i,:);
            bbox = bboxes(i, :);

            % Kalman filter 객체를 생성
            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;

            % 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;
        if ~isempty(tracks)

            % 잡음이 많은 감지는 짧은 수명의 트랙을 생성하는 경향이 있습니다.
            % 최소한 일정한 수의 프레임 동안 가시적인 트랙만 표시합니다.
            reliableTrackInds = [tracks(:).totalVisibleCount] > minVisibleCount;
            reliableTracks = tracks(reliableTrackInds);

            
            % 물체를 표시합니다. 만약 현재 프레임에서 물체가 감지되지 않았다면 예측된 바운딩 박스를 표시합니다.
            if ~isempty(reliableTracks)
                % 바운딩 박스를 표시
                bboxes = cat(1, reliableTracks.bbox);

                % ids를 표시
                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
end