[컴퓨터비전 STUDY / KOCW 한동대학교 황성수 교수님 강의 Review]
사용자 상호작용과 감지에 의해 ROI (관심영역)가 선택된다.
ROI를 히스토그램과 특징들로 표현한다.
다음 frame에서 ROI와 가장 유사한 Patch를 찾아낸다.
평균 이동이라고 하며, 중심을 데이터가 모여 있는 밀도가 가장 높은 곳으로 이동시키는 반복적인 과정이다.
여기서는 각 점들을 features라고 했을 때, 점들의 밀도가 가장 높은 곳으로 이동한다.

Histogram back-projection에 대해 알아보자
히스토그램 역투영이라고도 하며, 입력 이미지와 같은 크기이지만 하나의 채널만 가지는 이미지를 생성한다. 이 이미지의 픽셀은 특정 object에 속할 확률을 의미하며, 관심 오브젝트 영역에 속한 픽셀이 나머지 부분보다 더 흰색으로 표현된다.
첫번째 열 그림에서 특정한 Hue-Saturation 조합이 존재한다는 것을 알 수 있다.
(손 색깔 히스토그램이 높음)
추적하고자 하는 영역의 Hue-Saturation 히스토그램을 구하고, 입력 영상에서의 히스토그램을 비교해 가장 유사한 히스토그램을 가지는 영역을 찾는 방법인 histogram backprojection 방법을 적용하면 찾고자 하는 물체의 크기만큼을 관심 영역으로 지정할 수 있다.

다음은 mean shift를 이용한 추적 과정이다.

다음은 Meanshift를 수행하는 코드이다.
struct CallbackParam
{
Mat frame;
Point pt1, pt2; // 선택 영역의 시작점, 끝점
Rect roi; // Rest Of Interest
bool drag;
bool updated;
};
// 마우스 이벤트를 처리함
void onMouse(int event, int x, int y, int flags, void* param)
{
CallbackParam *p = (CallbackParam *)param;
if (event == EVENT_LBUTTONDOWN){
p->pt1.x = x;
p->pt1.y = y;
p->pt2 = p->pt1;
p->drag = true;
}
if (event == EVENT_LBUTTONUP){
int w = x - p->pt1.x;
int h = y - p->pt1.y;
p->roi.x = p->pt1.x;
p->roi.y = p->pt1.y;
p->roi.width = w;
p->roi.height = h;
p->drag = false;
if (w >= 10 && h >= 10){
p->updated = true;
}
}
// 마우스를 클릭하여 드래그하면 녹색사각형으로 보여줌
if (p->drag && event == EVENT_MOUSEMOVE){
if (p->pt2.x != x || p->pt2.y != y){
Mat img = p->frame.clone();
p->pt2.x = x;
p->pt2.y = y;
rectangle(img, p->pt1, p->pt2, Scalar(0, 255, 0), 1);
imshow("Tracker", img);
}
}
}
// 비디오 캡처를 초기화하고, 마우스 콜백을 설정
int main(int argc, char *argv[]){
VideoCapture cap(0);
CallbackParam param;
Mat frame, m_backproj, hsv;
Mat m_model3d;
Rect m_rc;
float hrange[] = { 0,180 };
float vrange[] = { 0,255 };
const float* ranges[] = { hrange, vrange, vrange }; // hue, saturation, brightness
int channels[] = { 0, 1, 2 };
int hist_sizes[] = { 16, 16, 16 };
// check if we succeeded
if (!cap.isOpened()){
cout << "can't open video file" << endl;
return 0;
}
// click and drag on image to set ROI
cap >> frame;
imshow("Tracker", frame);
param.frame = frame;
param.drag = false;
param.updated = false;
setMouseCallback("Tracker", onMouse, ¶m);
// ROI를 선택하면, 해당 영역에 대한 색상 히스토그램을 계산
// 이를 추후 프레임에서 해당 객체를 추적하는 데 사용
bool tracking = false;
while (true){
// image acquisition & target init
if (param.drag){
if (waitKey(33) == 27) break; // ESC key
continue;
}
cvtColor(frame, hsv, COLOR_BGR2HSV);
if (param.updated){
Rect rc = param.roi;
Mat mask = Mat::zeros(rc.height, rc.width, CV_8U);
ellipse(mask, Point(rc.width / 2, rc.height / 2), Size(rc.width / 2, rc.height / 2), 0, 0, 360, 255);
Mat roi(hsv, rc);
calcHist(&roi, 1, channels, mask, m_model3d, 3, hist_sizes, ranges);
m_rc = rc;
param.updated = false;
tracking = true;
}
cap >> frame;
if (frame.empty()) break;
// image processing
if (tracking){
//histogram backprojection
calcBackProject(&hsv, 1, channels, m_model3d, m_backproj, ranges);
//tracking -> meanShift와 CamShift에 따라서 쓰이는 함수가 달라진다.
meanShift(m_backproj, m_rc, TermCriteria(TermCriteria::EPS | TermCriteria::COUNT, 10, 1));
rectangle(frame, m_rc, Scalar(0, 0, 255), 3);
}
// image display
imshow("Tracker", frame);
// user input
char ch = waitKey(33);
if (ch == 27) break; // ESC Key (exit)
else if (ch == 32){ // SPACE Key (pause)
while ((ch = waitKey(33)) != 32 && ch != 27);
if (ch == 27) break;
}
}
return 0;
}
결과는 다음과 같다.
Camshift는 mean shift의 단점을 보완해서 만든 추적방법이다.
추적하는 객체의 크기가 변하더라도 search window가 고정되어 있는 mean shift 알고리즘의 단점을 보완했다.

다음은 Camshift를 수행하는 코드이다.
Optical flow는 영상에서 밝기 패턴의 겉보기 움직임이다.
다음은 Optical flow의 크기 및 방향을 나타낸 것이다.
다시 말해, Optical Flow 방식은 밝기 값을 기준으로 tracking하는 방식이다.
Optical Flow를 이용한 많은 알고리즘이 있지만, 그 중에서 KLT 알고리즘에 대해서 알아보자.
KLT 알고리즘은 다음과 같은 가정을 한다.
영상 frame이 지나더라도, Object의 강도는 변하지 않는다.
인접한 pixel들의 움직임은 서로 유사하다.
여기에 테일러 급수를 적용하면 미분식의 합이 0이 되는 것을 알 수 있다.
이것을 적용해 tracking을 진행한다.
먼저 특징을 추출하고, 추출한 feature을 추적한다.
하지만, KLT 알고리즘은 인접 픽셀의 변화량은 서로 유사하다고 가정하기에 물체의 큰 움직임을 정확히 찾아낼 수 없다는 한계가 있다.
따라서 이를 극복하기 위해 image pyramid라는 방식을 사용한다.
image pyramid란 주어진 영상과 같은 비율을 가진 다른 크기의 영상 정보를 생성하고, 이에 대해 image feature를 찾는 과정을 여러 번 반복하는 방식이다.
(입력 영상들을 여러 개의 크기로 줄인 집합)
다음은 Optical Flow를 수행하는 코드이다.
struct feature {
Point2f pt;
int val;
};
bool initialization = false; // 추적 초기화 여부를 나타냄
void DrawTrackingPoints(vector<Point2f> &points, Mat &image); // 이미지 위에 주어진 점들을 그림
int main(int argc, char *argv[])
{
VideoCapture cap(0);
if (!cap.isOpened()) {
cout << "Cannot open cap" << endl;
return 0;
}
double fps = cap.get(CV_CAP_PROP_FPS);
Mat currImage, prevImage;
Mat frame, dstImage;
double qualityLevel = 0.01;
double minDistance = 10;
int blockSize = 3;
bool useHarrisDetector = false;
double k = 0.04;
int maxCorners = 500;
TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 10, 0.01);
Size winSize(11, 11);
vector<Point2f> prevPoints;
vector<Point2f> currPoints;
vector<Point2f> boundPoints;
int delay = 1000 / fps;
int nframe = 0;
while(1) {
cap >> frame;
if (frame.empty()) break;
frame.copyTo(dstImage);
/// Copy the source image
cvtColor(dstImage, currImage, CV_BGR2GRAY);
GaussianBlur(currImage, currImage, Size(5, 5), 0.5);
// feature detection
// goodFeaturesToTrack 함수를 사용해 좋은 특징점을 검출
if (initialization) {
goodFeaturesToTrack(prevImage, prevPoints, maxCorners, qualityLevel, minDistance, Mat(), blockSize,
useHarrisDetector, k);
// cornerSubPix로 이 특징점들의 정확도를 개선
cornerSubPix(prevImage, prevPoints, winSize, Size(-1, -1), criteria);
DrawTrackingPoints(prevPoints, dstImage);
initialization = false;}
if (prevPoints.size() > 0) {
vector<Mat> prevPyr, currPyr;
Mat status, err;
buildOpticalFlowPyramid(prevImage, prevPyr, winSize, 3, true);
buildOpticalFlowPyramid(currImage, currPyr, winSize, 3, true);
// calcOpticalFlowPyrLK 함수의 결과로 얻은 상태 벡터(status)를 확인하여 추적이 실패한 특징점을 제거
calcOpticalFlowPyrLK(prevPyr, currPyr, prevPoints, currPoints, status, err, winSize);
// delete invalid correspondinig points
for (int i = 0; i < prevPoints.size(); i++) {
if (!status.at<uchar>(i)) {
prevPoints.erase(prevPoints.begin() + i);
currPoints.erase(currPoints.begin() + i);
}
}
DrawTrackingPoints(currPoints, dstImage);
prevPoints= currPoints;
}
imshow("dstImage", dstImage);
currImage.copyTo(prevImage);
intch = waitKey(33);
if (ch == 27) break; // 27 == ESC key
if (ch == 32) initialization= true;
}
return0;
}
// OpenCV를 사용하여 이미지 상에 감지된 특징점들을 그리는 기능을 수행
void DrawTrackingPoints(vector<Point2f> &points, Mat&image) {
// Drawcornersdetected
for (int i= 0; i< points.size(); i++) {
int x = cvRound(points[i].x);
int y= cvRound(points[i].y);
// 이미지에 원을 그리는데 사용됨
circle(image, Point(x, y), 3, Scalar(255, 0, 0), 2);
}
}
결과는 다음과 같다.
