6. [KhuCv] - FaceTracking, Using DeepSORT.

이원규·2023년 2월 9일
0

1. Using DeepSORT in FaceTracking.

1-1. DeepSort?

DeepSort란 가장 널리 사용되고 객체 추적 프레임워크 중 하나로, SORT(Simple Online and Realtime Tracking - 예시: IOU)을 보완 확장한 기술이다. 이전 KhuCv는 SORT만을 사용하여 오직 IOU로만 id를 부여하여 object를 추적하였는데, 이는 object를 Detection하지 못 할 경우 IOU추적을 어렵게 만들어 객체에 새로운 id를 부여하는 오류를 범한다. 또한 객체가 다른 객체에 의해 가려지면 다른 객체에 FaceRect가 옮겨져가는 오류도 범한다. 이러한 오류들을 해결하고 Tracking 성능을 높이기 위해 DeepSORT프레임 워크를 사용한다.

1-2. DeepSORT applied in KhuCv FaceTracking.

사용할 DeepSORT프레임워크는에는 다음의 3가지 특징을 포함한다.
1. IOU.
2. FaceSimilarity(Cosine Similarity - 참고).
3. 이동 속도를 측정하여 다음 FaceRect위치 예상하기.

현재 Detection된 FaceRect를 이전 Frame에서 Detection된 FaceRect와 비교하여 측정한다. 먼저, 이전 FaceRect와 현재 FaceRect의 IOU(또는 FaceSimilarity)를 측정하고, 측정이 안 될 경우 FaceSimilarity(또는 IOU)로 측정을 한다. 측정이 된다면 Tracker객체의 Rect의 좌표와 이외에 필요한 정보들을 업데이트한다.

이 두 가지 모두 측정이 안 될 경우, 이전 FaceRect의 이동 속도를 가지고 다음 FaceRect의 위치를 추적하도록 한다. 만약 15~20번 정도 추적이 안 될 경우, Frame에서 사라진 것으로 간주하고 해당 FaceRect에 대해 추적하는 Tracker객체를 삭제한다.




2. How to get Face feature?

2-1. DNN?

심층 신경망(Deep Neural Network, DNN)은 입력층(input layer)과 출력층(output layer) 사이에 여러 개의 은닉층(hidden layer)들로 이뤄진 인공신경망(Artificial Neural Network, ANN)이다. 심층 신경망은 일반적인 인공신경망과 마찬가지로 복잡한 비선형 관계(non-linear relationship)들을 모델링할 수 있다.

예를 들어, 사물 식별 모델을 위한 심층 신경망 구조에서는 각 객체가 이미지 기본 요소들의 계층적 구성으로 표현될 수 있다. 이때, 추가 계층들은 점진적으로 모여진 하위 계층들의 특징들을 규합시킬 수 있다. 심층 신경망의 이러한 특징은, 비슷하게 수행된 인공신경망에 비해 더 적은 수의 유닛(unit, node)들 만으로도 복잡한 데이터를 모델링할 수 있게 해준다.

출처 : 심층 신경망(Deep Neural Network, DNN)

2-2. How to get Face feature.

컴퓨터 비전에서는 활용되는 DNN은 미리 몇 천장의 사진으로 훈련되어 input layers로 받은 이미지에 있는 동물이 각각 어떤 동물인지 판별할 수 있다. 여기서 동물을 판별할 때, input layers에서 동물을 감지하고 각각의 특징들을 잡아내는데, 우린 이 특징을 잡아내는 기능을 이용하여 FaceRect의 특징을 찾아낼 것이다.

이후 FaceRect의 특징을 해당 Frame에서 Detection된 FaceRect와 cosine similarity를 이용하여 해당 Rect가 일치하는 비율을 확인할 것이다.

  1. 해당 DNN(onnx)파일을 다운받고, Run폴더에 넣어준다.
  2. CProject::CProject() - 생성자 에 해당 코드를 넣어주어 dnn의 onnx파일을 읽어온다.
// Project.cpp
CProject::CProject() {
	strcat(m_ExePt, "/mobilenetv2-7.onnx");// m_ExePt는 현재 작업 경로
	m_MobileNet = cv::dnn::readNetFromONNX(m_ExePt);
}

// Project.h
class CProject{
	cv::dnn::Net m_MobileNet;
}
  1. Detection된 faceRect마다 해당 코드를 실행시켜, faceRect특징을 가져온다. 즉, DNN의 foward함수를 이용하여 DNN(mobilenetv2-7.onnx) 파일의 "onnx_node!mobilenetv20_features_pool0_fwd" layer까지만 실행한 FaceRect의 특징을 가져온다. 가져온 특징을 cv::Mat(matrix) 형태로 저장하고 40*32(1280pixel)의 크기로 resize하여 통일성 있게 faceRect의 크기를 조정한다.
  • 해당 코드는 detected된 faceRect의 특징을 가져오는 기능을 수행한다.
  • dnn.foward는 해당 DNN을 입력된 파일명까지만 실행하여, cv::Mat형태로 반환하며, 우리가 사용하는 mobilenetv2-7.onnx의 "onnx_node!mobilenetv20_features_pool0_fwd" 파일은 input된 cv::Mat에서 특징을 집어내 데이터로 반환한다.
for(auto list : detected_faceList) {
    cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
    cv::Mat DnnInput;
    cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
    cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
    m_MobileNet.setInput(inputBlob);
    // features cv::Mat 받아들이기
    #ifndef  __APPLE__
            cv::Mat features = m_MobileNet.forward("mobilenetv20_features_pool0_fwd");
    #else
            cv::Mat features = m_MobileNet.forward("onnx_node!mobilenetv20_features_pool0_fwd");
    #endif

    cv::Mat cvFeature(40, 32, CV_32FC1);
    memcpy(cvFeature.data, features.data, 1280 * sizeof(float));
}

각 FaceRect의 특징이 담긴 cvFeature에 CosineSimilarity를 적용하여 Face Similarity를 검사한다.




3. Errors and How to fix them.

  • Project.h - 고정(Fixed)
#pragma once
#include "cv_dnn_ultraface.h"
#include <iostream>
#include <sstream>
#include <algorithm>
#include <vector>

class CProject
{
    cv::Mat m_PreviousImage;
public:
    char m_ExePath[256];
    wchar_t m_ExePathUnicode[256];

    CProject();
    ~CProject();
    void GetExecutionPath();
    void Run(cv::Mat Input, cv::Mat& Output, bool bFirstRun, bool bVerbose);

    UltraFace *m_pUltraface;
    cv::dnn::Net m_MobileNet;
};


class Point{
public:
    float x,y;
    Point(){}
    Point(float a, float b): x(a), y(b){}
    Point operator-(Point other){return {x-other.x, y-other.y};}
    Point operator+(Point other){return {x+other.x, y+other.y};}
    Point operator*(float a){return {x * a, y * a};}
    bool operator==(Point other){return (other.x==x && other.y==y) ? true : false;}
};

class Rect{
public:
    Point LT,RB;
    Rect(){}
    Rect(Point lt, Point rb): LT(lt), RB(rb){}
    float iou(Rect other){ // iou가 0을 가질 경우, 이 사각형은 inter을 갖지 않음. 즉, 겹치지 않음.
        Rect inter = this->intersection(other);
        float thisArea = this->width() * this->height();
        float otherArea = other.width() * other.height();
        float interArea = inter.width() * inter.height();
        return interArea/(thisArea+otherArea-interArea);
    }
    Rect intersection(Rect other){
        float x1 = std::max(LT.x, other.LT.x);
        float y1 = std::max(LT.y, other.LT.y);
        float x2 = std::min(RB.x, other.RB.x);
        float y2 = std::min(RB.y, other.RB.y);
        return {{x1,y1},{x2,y2}};
    }
    float width(){
        return RB.x-LT.x >= 0 ? RB.x-LT.x : 0;
    }
    float height(){
        return RB.y-LT.y >= 0 ? RB.y-LT.y : 0;
    }
    Point center(){
        return {(LT.x + RB.x)/2, (LT.y + RB.y)/2};
    }
};

class Tracker{
public:
    static int cnt;
    int T_id;
    int UnTracked = 0;
    
    Point offset;
    Rect rt;
    std::vector<cv::Mat> featureList;
    
    Tracker(){}
    Tracker(Rect current, cv::Mat cvFeature):T_id(cnt++), rt(current), offset(0,0){
        featureList.push_back(cvFeature);
        matched = true;
    }
    double GetCosineSimilarity(cv::Mat feature){
        double maxSimilarity = 0;
        for(int i = featureList.size() - 1; i >= 0 && i >= featureList.size() - 5;--i){
            double Similarity = 0;
            double A = 0, B = 0;
            for(int row = 0; row < featureList[i].rows; ++row)
                for(int col = 0; col < featureList[i].cols; ++col){
                    Similarity += featureList[i].at<float>(row,col) * feature.at<float>(row,col); // 내적
                    A += featureList[i].at<float>(row, col) * featureList[i].at<float>(row, col); // A크기
                    B += feature.at<float>(row, col) * feature.at<float>(row,col); // B크기
                }
            if(sqrt(A) * sqrt(B) > 0) Similarity /= sqrt(A) * sqrt(B);
            else Similarity = 0;
            
            if(Similarity > maxSimilarity) maxSimilarity = Similarity;
        }
        return maxSimilarity;
    }
};



3-1. First Error - One Person Two faceRect Error.

- 오류 이미지

- 설명

: 맨 앞의 남자를 보면 ID 2번과 4번이 겹쳐지는 One Person Two faceRect Error가 발생함을 볼 수 있다. 해당 오류는 현재 Frame의 Detected된 Rect에 대해서, 이전 Frame의 조건을 만족하는 모든 Rect에 현재 Rect를 부여해줌으로써 발생한 문제이다.

- 오류 발생 코드(초기 코드)

: Project.cpp - CProject::Run()
-> 밑의 <문제 발생 코드> 참조

// <문제 발생 코드>

// tracking vector initialize
std::vector<Tracker> m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat& Output, bool bFirstRun, bool bVerbose) {
    std::vector<FaceInfo> faceList;
    m_pUltraface->detect(Input, faceList);
 
    cv::Mat OutImage = Input.clone();
    
    for(auto list : faceList) {
        
        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward("mobilenetv20_features_pool0_fwd");
#else
        cv::Mat features = m_MobileNet.forward("onnx_node!mobilenetv20_features_pool0_fwd");
#endif
        
        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));
        
        
        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;
        
        for(int i = 0; i < m_idTrackers.size(); ++i){
            bool identified = false;
            
         
            if(m_idTrackers[i].GetCosineSimilarity(cvFeature) > 0.98) identified = true;
            else if(m_idTrackers[i].rt.iou(currentRt) > 0.15) identified = true;
            
            
            if(identified){
                has_id = true;
                m_idTrackers[i].featureList.push_back(cvFeature);
                m_idTrackers[i].UnTracked = (m_idTrackers[i].UnTracked > 1) ? -1 : 0;
                m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (currentRt.center() - m_idTrackers[i].rt.center()) * 0.5;
                m_idTrackers[i].rt = currentRt;
            }
        }
        
        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }
    
    for(int i = 0; i < m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked > 15) m_idTrackers.erase(m_idTrackers.begin() + i);
        
        // 그리기
        cv::Scalar color;
        std::stringstream m_id;
        m_id << m_idTrackers[i].T_id << "-" << m_idTrackers[i].UnTracked;
        
        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;
            m_idTrackers[i].UnTracked++;
        }
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }
    
    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();
    
    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}



3-1) 문제 해결

- 해결 이미지

- 설명

: 현재 Detected된 FaceRect에 대하여 이전 프레임에서 검출된 FaceRect와의 Iou혹은 similarity가 일치할 경우, 비교 과정을 계속 진행하지 않고 break를 넣어 줌으로써 끝내어 하나의 Detected FaceRect에 한 개의 Rect만 대입하도록 한다. 해당 오류는 밑의 코드처럼 break를 작성함으로써 조건을 만족하는 하나의 Rect를 인지할 경우, FaceRect의 비교 과정을 끝낸다.

- 수정 코드

: Project.cpp - parts of CProject::Run()
-> 밑의 코드 참조

// 수정 코드 (parts of CProject::Run())
if(identified){
	has_id = true;
    m_idTrackers[i].featureList.push_back(features);
    m_idTrackers[i].UnTracked = (m_idTrackers[i].UnTracked > 1) ? -1 : 0;
    m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (currentRt.center() - m_idTrackers[i].rt.center()) * 0.5;
    m_idTrackers[i].rt = currentRt;
    break; // break 작성
}



3-2. Second Error - late Tracking

- 오류 영상

- 설명

: 3-1 one person two faceRect의 문제를 해결한 뒤, late Detection문제에 직면하였다. 해당 그림의 3-0 여성을 보면, 1-0의 남성의 뒤를 지난 후 ID 3번이 제 때에 FaceDetection을 하지 못하여 ID 5번이 할당된 후, 뒤늦게 FaceDetection하여 3번으로 교체되는 late Tracking현상이 발생하였다.

이 오류는 3번 여성이 가려졌다가 나타났을 때 밑의 코드에서처럼 인물의 유사도를 판별하는 유사도가 0.98을 넘지 못하여 발생한다.

// 오류 설명 참조 코드
if(m_idTrackers[i].GetCosineSimilarity(cvFeature) > 0.98) identified = true;
else if(m_idTrackers[i].rt.iou(currentRt) > 0.15) identified = true;



3-2) 문제 해결

- 해결 영상

- 설명

: late Tracking을 해결하기 위해선 유사도 판별의 임계치를 0.98 밑으로 설정을 해줘야 하는데, 그렇게 되면 임계치 유사도를 만족하는 FaceRect가 여러 개가 되어 제대로 된 Tracking을 못할 수 있다. 따라서 가장 유사도가 높은 FaceRect를 선정하여 원하는 FaceRect가 매칭되도록 해준다. 이 때, FaceRect가 화면에서 사라질 경우를 대비하여 유사도의 임계치를 0.945로 설정해준다. 또한, FaceRect가 5번 동안 matched되지 않았다면 유사도 값이 더 낮을 것이므로 임계치를 0.93으로 설정해주어 match되도록 한다.

- 수정 코드

: Project.cpp - CProject::Run()
-> 밑의 코드 참조

// 수정 코드 (Project.cpp - CProject::Run())

// tracking vector initialize
std::vector<Tracker> m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat& Output, bool bFirstRun, bool bVerbose) {
    if(bFirstRun){
        Tracker::cnt = 0;
        m_idTrackers.clear();
        
        for(auto layer : m_MobileNet.getLayerNames()){
            DlgPrintf("%s", layer.c_str());
        }
    }
    
    std::vector<FaceInfo> faceList;
    m_pUltraface->detect(Input, faceList);
 
    cv::Mat OutImage = Input.clone();
    
    for(auto list : faceList) {
        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward("mobilenetv20_features_pool0_fwd");
#else
        cv::Mat features = m_MobileNet.forward("onnx_node!mobilenetv20_features_pool0_fwd");
#endif
        
        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));
        
        
        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;
        bool identified = false;
        
        double maxSimilarity = 0;
        Tracker *maxTracker;
        
        for(int i = 0; i < m_idTrackers.size(); ++i){
            if(m_idTrackers[i].rt.iou(currentRt) > 0.15) {
                identified = true;
                maxTracker = &m_idTrackers[i];
                break;
            }
            else{
                double similarity = m_idTrackers[i].GetCosineSimilarity(cvFeature);
                if(similarity > maxSimilarity) {
                    maxSimilarity = similarity;
                    maxTracker = &m_idTrackers[i];
                }
            }
        }
        if(maxSimilarity > 0.945) identified = true;
        else if(maxSimilarity > 0.93 && maxTracker->UnTracked > 5) identified = true;
        
        if(identified){
            has_id = true;
            maxTracker->featureList.push_back(cvFeature);
            maxTracker->UnTracked = (maxTracker->UnTracked > 1) ? -1 : 0;
            maxTracker->offset = maxTracker->offset * 0.5 + (currentRt.center() - maxTracker->rt.center()) * 0.5;
            maxTracker->rt = currentRt;
        }
        
        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }
    
    for(int i = 0; i < m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked > 20 || (m_idTrackers[i].offset == Point{0,0} && m_idTrackers[i].UnTracked > 5)) m_idTrackers.erase(m_idTrackers.begin() + i);
        
        // id 부여
        cv::Scalar color;
        std::stringstream m_id;
        m_id << m_idTrackers[i].T_id << "-" << m_idTrackers[i].UnTracked;
        
        // 초기화
        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;
            m_idTrackers[i].UnTracked++;
        }
        
        // rect, id 그리기
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }
    
    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();
    
    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}




4. Performance improving.

4-1. First improving.

기존의 UnTracked시에 FaceRect를 추적하여 다음 위치를 예상하기 위한 이동 속도를 조절해주었다. 기존의 FaceRect 추적 속도는 현재 offset과 이전 프레임의 offset의 비율이 5대 5였으나 개선 후에 그 비율을 6.5대 3.5로 변경해주었다.

// 변경 전 = 5:5
maxTracker->offset = maxTracker->offset * 0.5 + (currentRt.center() - maxTracker->rt.center()) * 0.5;
m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;

// 변경 후 = 3.5: 6.5
maxTracker->offset = maxTracker->offset * 0.35 + (currentRt.center() - maxTracker->rt.center()) * 0.65;
m_idTrackers[i].offset = m_idTrackers[i].offset * 0.35 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.65;

- Before improving.

  1. 20번 남성을 제대로 Tracking하지 못하여 22번 ID를 새로 부여하고 있다.

  1. 기존의 8번 남성이 위치 추적중인 6번의 FaceRect를 가져간다.

- after improving.

  1. 개선 전보다 20번 남성의 속도에 맞추어 FaceRect가 움직임을 볼 수 있다. 따라서 제대로된 Tracking이 가능해졌다.

  1. 추적 속도를 조절해주어 8번 남성이 6번 FaceRect를 가져가지 못하도록 해주었다. 즉, 두 FaceRect가 만나지 못하도록 하여 두 사각형 간의 IOU를 0으로 만들어주었다.




5. Final code.

5-1. Project.h

  • '3. Errors and How to fix them.' 의 Project.h 참조

5-2. Project.cpp

// 1. Project.cpp
//  Project.cpp: implementation of CProject (main project class you will write)
//    Dept. Software Convergence, Kyung Hee University
//    Prof. Daeho Lee, nize@khu.ac.kr
//
#include "KhuCvApp.h"
#include "Project.h"

#ifdef _MSC_VER
#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW
#endif
#endif

CProject::CProject() {
    GetExecutionPath();
    
    char m_ExePt[256];
    strcat(m_ExePt, m_ExePath);
    
    strcat(m_ExePath, "/version-RFB-320_without_postprocessing.onnx");
    strcat(m_ExePt, "/mobilenetv2-7.onnx");
    
    m_pUltraface = new UltraFace(m_ExePath, 320, 240);
    m_MobileNet = cv::dnn::readNetFromONNX(m_ExePt);
}

CProject::~CProject() {
    delete m_pUltraface;
}

void CProject::GetExecutionPath() {
    wxFileName f(wxStandardPaths::Get().GetExecutablePath());
    wxString appPath(f.GetPath());

    wcscpy(m_ExePathUnicode, appPath);
    strcpy(m_ExePath, appPath.c_str());
}

// tracking vector initialize
std::vector<Tracker> m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat& Output, bool bFirstRun, bool bVerbose) {
    if(bFirstRun){
        Tracker::cnt = 0;
        m_idTrackers.clear();
        
        for(auto layer : m_MobileNet.getLayerNames()){
            DlgPrintf("%s", layer.c_str());
        }
    }
    
    std::vector<FaceInfo> faceList;
    m_pUltraface->detect(Input, faceList);
 
    cv::Mat OutImage = Input.clone();
    
    for(auto list : faceList) {
        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward("mobilenetv20_features_pool0_fwd");
#else
        cv::Mat features = m_MobileNet.forward("onnx_node!mobilenetv20_features_pool0_fwd");
#endif
        
        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));
        
        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;
        bool identified = false;
        
        double maxSimilarity = 0;
        Tracker *maxTracker;
        
        for(int i = 0; i < m_idTrackers.size(); ++i){
            if(m_idTrackers[i].rt.iou(currentRt) > 0.15) {
                identified = true;
                maxTracker = &m_idTrackers[i];
                break;
            }
            else{
                double similarity = m_idTrackers[i].GetCosineSimilarity(cvFeature);
                if(similarity > maxSimilarity) {
                    maxSimilarity = similarity;
                    maxTracker = &m_idTrackers[i];
                }
            }
        }
        if(maxSimilarity > 0.945) identified = true;
        else if(maxSimilarity > 0.93 && maxTracker->UnTracked > 5) identified = true;
        
        
        if(identified){
            has_id = true;
            maxTracker->featureList.push_back(cvFeature);
            maxTracker->UnTracked = (maxTracker->UnTracked > 1) ? -1 : 0;
            maxTracker->offset = maxTracker->offset * 0.65 + (currentRt.center() - maxTracker->rt.center()) * 0.35;
            maxTracker->rt = currentRt;
        }
        
        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }
    
    for(int i = 0; i < m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked > 20 || (m_idTrackers[i].offset == Point{0,0} && m_idTrackers[i].UnTracked > 5)) m_idTrackers.erase(m_idTrackers.begin() + i);
        
        // id 부여
        cv::Scalar color;
        std::stringstream m_id;
        m_id << m_idTrackers[i].T_id << "-" << m_idTrackers[i].UnTracked;
        
        // 초기화
        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.35 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.65;
            m_idTrackers[i].UnTracked++;
        }
        
        // rect, id 그리기
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }
    
    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();
    
    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}

해당 프레임에서 detection된 FaceRect에 대하여 먼저 이전 프레임의 FaceRect와의 IOU값을 판별하여 Tracking을 시도한다. 만약 IOU로 Tracking이 되지 않을 경우, FaceSimilarity로 Tracking을 시도한다. 임계치의 IOU 혹은 FaceSimilarity를 만족하는 FaceRect가 발견될 경우 Tracker의 Rect, offset, UnTracked 등을 업데이트 해준다.

그러나 만약 OU 혹은 FaceSimilarity를 만족하는 FaceRect가 없을 경우, 새로운 객체로 판단하여 새로운 Tracker객체를 만들어준다.

판별 과정이 끝나면 각 Tracker의 UnTracked에 따라 색깔을 달리하여 Rectangle을 그려준다. UnTracked는 기본적으로 해당 FaceRect정보가 담긴 Tracker가 연속적으로 몇 번 Tracking하지 못하였는지를 의미한다. UnTracked가 0일 경우 이전 프레임에서 UnTracked된 적 없이 연속적으로 Tracking됐음을 뜻하며, -1일 경우 추적을 놓친 FaceRect를 찾았다는 것을 의미한다. 그리고 UnTracked가 1이상일 경우 이전 프레임에서 x번 이상 Tracking되지 못하였음을 뜻하고 이 UnTracked가 18번 이상이면 해당 Tracker는 삭제한다.



6. 느낀 점

외부 FaceDetection DNN을 활용하여 FaceTracking하는 활동을 해보았다. 알고리즘을 짜는 데에만 일주일을 쓴 거 같다. 어떤 식으로 코드를 짜야 간결하고 잘 작동할까?라는 생각에 더 시간을 오래 쓴 거 같다. 솔직히 대략적인 기능을 어찌저찌 구현만 하면 이후에는 수월할 것이라 생각했는데, 위에 언급한 Error들에 직면했다.

3-1 Error는 한 3일간 고민을 했는데, 그 방법을 모르겠어서 교수님께 여쭤보았다. 교수님은 Tracking된 object는 따로 표시를 하여 2번 중복되지 않도록 하라고 하셨다. 그러나 나는 이 방법보다 더 간결한 방법이 있을 것이라 생각하였고, 3-1의 해결방법에 언급한 방법으로 해결하였다. 이후 3-2의 에러를 만났는데, 이 에러는 하루 정도 고민하여 해결하였다. 이전의 교수님이 설명해주신 maxSimilarity 아이디어에 조금 코드를 추가하여 에러를 해결할 수 있었다.

이번 프로젝트를 통해 원하는 기능을 내 방식대로 구현해보는 값진 경험을 할 수 있었으며, 아직은 부족하지만 코드를 간결하게 짜는 스킬도 조금은 터득할 수 있었다. 그리고 마주한 에러들을 혼자 생각해보고 (주변의 도움을 조금 받긴 했지만) 스스로 해결해보며, 문제를 마주했을 때 어떤 부분이 문제인지 정확히 인지하는 방식과 문제마다 어떤 식으로 해결해 나아가야 하는지에 대한 방식을 배울 수 있었다.

아직 Face Tracking 성능이 많이 부족하긴 하지만, 처음 한 것 치곤 만족스러운 결과를 낸 프로젝트였다. 추후, 컴퓨터 비전 및 딥러닝 관련 수업을 들으며, 이 성능을 계속 보완해나갈 계획이다.

profile
github: https://github.com/WKlee0607

0개의 댓글