그런 다음, 태양계 프로그램 ./solarsystem을 실행하고 RecordMyDesktop을 사용하여 데스크톱 화면을 녹화 (10~30 초면 충분합니다) 하고, ~/Code/camshift에 video라는 이름으로 저장합니다:
녹화를 완료하려면, 오른쪽 하단의 Stop 버튼을 클릭하면 됩니다. 그러면 video.ogv 파일이 생성됩니다:
디지털 이미지 기본
OpenCV 는 오픈 소스 크로스 플랫폼 컴퓨터 비전 라이브러리입니다. OpenGL 의 이미지 렌더링과 달리, OpenCV 는 이미지 처리 및 컴퓨터 비전을 위한 많은 일반적인 알고리즘을 구현합니다. OpenCV 를 배우기 전에, 컴퓨터에서 이미지와 비디오의 몇 가지 기본 개념을 이해해야 합니다.
우선, 컴퓨터에서 사진 또는 이미지가 어떻게 표현되는지 이해해야 합니다. 사진을 저장하는 두 가지 일반적인 방법이 있습니다: 하나는 벡터 맵이고 다른 하나는 픽셀 맵입니다.
벡터 맵에서 이미지는 선으로 연결된 일련의 점으로 수학적으로 정의됩니다. 벡터 맵 파일의 그래픽 요소는 객체라고 합니다. 각 객체는 자체 포함된 엔티티이며, 색상, 모양, 윤곽선, 크기 및 화면 위치와 같은 속성을 갖습니다.
더 일반적인 것은 픽셀 맵입니다. 예를 들어, 이미지의 크기는 종종 1024*768 입니다. 이것은 사진이 가로 방향으로 1024 픽셀, 세로 방향으로 768 픽셀을 가지고 있음을 의미합니다.
픽셀은 픽셀 맵의 기본 단위입니다. 일반적으로 픽셀은 세 가지 기본 색상 (빨강, 녹색 및 파랑) 의 혼합입니다. 컴퓨터의 본질이 숫자의 인식이기 때문에, 일반적으로 기본 색상을 0 에서 255 까지의 밝기로 표현합니다. 즉, 기본 빨간색의 경우, 0은 가장 어두운 색, 즉 검정색을 의미하고, 255는 가장 밝은 색, 즉 순수한 빨간색을 의미합니다.
따라서 픽셀은 삼중항 (R,G,B)로 표현될 수 있으며, 흰색은 (255,255,255)로, 검정색은 (0,0,0)으로 표현될 수 있습니다. 그러면 이 이미지를 RGB 색상 공간의 이미지라고 부릅니다. R, G 및 B는 이미지의 세 가지 채널이 됩니다. RGB 색상 공간 외에도 HSV, YCrCb 등과 같은 다른 많은 색상 공간이 있습니다.
픽셀이 픽셀 맵의 기본 단위인 것처럼, 이미지는 비디오의 기본 단위입니다. 비디오는 일련의 이미지로 구성되며, 각 이미지를 프레임이라고 부릅니다. 그리고 우리가 일반적으로 비디오 프레임 속도라고 부르는 것은 이 비디오가 초당 많은 프레임 이미지를 포함하고 있음을 의미합니다. 예를 들어, 프레임 속도가 25 이면, 이 비디오는 초당 25 프레임을 재생합니다.
1 초에 1000 밀리초가 있고 프레임 속도를 rate라고 하면, 프레임 이미지 간의 시간 간격은 1000/rate입니다.
이미지 색상 히스토그램
색상 히스토그램은 이미지를 설명하기 위한 도구입니다. 일반적인 히스토그램과 유사하지만, 색상 히스토그램은 특정 이미지에서 계산해야 합니다.
사진이 RGB 색상 공간에 있다면, R 채널에서 모든 색상의 발생 횟수를 셀 수 있습니다. 따라서 256 개의 길이 (색상 확률 조회 테이블) 의 배열을 얻을 수 있습니다. 이미지의 총 픽셀 수 (너비 x 높이) 로 모든 값을 동시에 나누고, 결과 시퀀스를 히스토그램으로 변환합니다. 결과는 R 채널의 색상 히스토그램입니다. 유사한 방식으로, G 채널 및 B 채널의 히스토그램을 얻을 수 있습니다.
히스토그램 역투영
RGB 색상 공간에서 히스토그램은 조명 변화에 민감하다는 것이 증명되었습니다. 이러한 변화가 추적 효과에 미치는 영향을 줄이기 위해, 히스토그램을 역투영해야 합니다. 이 과정은 세 단계로 나뉩니다:
먼저, 이미지를 RGB 공간에서 HSV 공간으로 변환합니다.
그런 다음, H 채널의 히스토그램을 계산합니다.
이미지의 각 픽셀 값을 색상 확률 조회 테이블에서 해당 확률로 대체하여 색상 확률 분포 맵을 얻습니다.
C++ 의 기본 문법을 이미 알고 있다고 가정합니다. 거의 모든 프로그램이 #include <iostream> 헤더 파일과 using namespace std; 또는 std::cout을 사용한다는 것을 알고 있습니다. OpenCV 에도 자체 네임스페이스가 있습니다.
OpenCV 를 사용하려면 다음 헤더 파일을 포함하기만 하면 됩니다:
#include <opencv2/opencv.hpp>
그런 다음:
using namespace cv;
OpenCV 네임스페이스를 활성화합니다 (또는 모든 API 에 대해 직접 cv:: 접두사를 사용합니다).
OpenCV 를 처음 사용하는 경우 OpenCV 인터페이스에 익숙하지 않을 수 있으므로 OpenCV API 를 배우기 위해 cv:: 접두사를 사용하는 것이 좋습니다.
녹화된 비디오를 읽는 첫 번째 프로그램을 작성해 보겠습니다:
//
// main.cpp
//
#include <opencv2/opencv.hpp> // OpenCV 헤더 파일
int main() {
// 비디오 캡처 객체 생성
// OpenCV 는 VideoCapture 객체를 제공하며
// 파일에서 비디오를 읽는 것을 카메라에서 읽는 것과 동일하게 처리합니다.
// 입력 매개변수가 파일 경로이면 비디오 파일을 읽습니다.
// 카메라의 식별 번호 (일반적으로 0) 인 경우
// 카메라를 읽습니다.
cv::VideoCapture video("video.ogv"); // 파일에서 읽기
// cv::VideoCapture video(0); // 카메라에서 읽기
// 읽은 이미지 프레임을 위한 컨테이너, OpenCV 의 Mat 객체
// OpenCV 의 핵심 클래스는 Mat 이며, Matrix 를 의미합니다.
// OpenCV 는 이미지를 설명하기 위해 행렬을 사용합니다.
cv::Mat frame;
while(true) {
// 비디오 데이터를 프레임에 쓰기, >>는 OpenCV 에 의해 덮어쓰기됩니다.
video >> frame;
// 프레임이 없으면 루프를 종료합니다.
if(frame.empty()) break;
// 현재 프레임 시각화
cv::imshow("test", frame);
// 비디오 프레임 속도는 15 이므로 부드럽게 재생하려면 1000/15를 기다려야 합니다.
// waitKey(int delay) 는 OpenCV 의 대기 함수입니다.
// 이 시점에서 프로그램은 키보드 입력을 위해 `delay` 밀리초 동안 대기합니다.
int key = cv::waitKey(1000/15);
// 키보드에서 ECS 버튼을 클릭하면 루프를 종료합니다.
if (key == 27) break;
}
// 메모리 해제
cv::destroyAllWindows();
video.release();
return 0;
}
이 main.cpp 파일을 ~/Code/camshift에서 video.ogv와 동일한 폴더에 넣고 프로그램을 컴파일합니다:
g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o main
프로그램을 실행하면 비디오가 재생되는 것을 볼 수 있습니다:
./main
참고
다음과 같은 오류가 발생할 수 있습니다:
libdc1394 error: Failed to initialize libdc1394
이것은 OpenCV 의 버그이며 실행에는 영향을 미치지 않습니다.
문제를 제거하려면 프로그램을 실행하기 전에 다음 코드를 실행할 수 있습니다:
sudo ln /dev/null /dev/raw1394
Meanshift 및 Camshift 알고리즘
Meanshift
Camshift
추적 대상 선택을 위해 마우스 콜백 (mouse callback) 이벤트를 설정
비디오 스트림에서 이미지를 읽기
Camshift 구현
Meanshift (평균 이동) 알고리즘
Meanshift 및 Camshift 알고리즘은 객체 추적을 위한 두 가지 고전적인 알고리즘입니다. Camshift 는 Meanshift 를 기반으로 합니다. 수학적 해석은 복잡하지만 기본 아이디어는 비교적 간단합니다. 따라서 이러한 수학적 사실은 건너뛰고 먼저 Meanshift 알고리즘을 소개합니다.
화면에 빨간색 점 집합이 있다고 가정하면 파란색 원 (창) 은 가장 밀도가 높은 영역 (또는 점의 수가 가장 많은 곳) 으로 이동해야 합니다.
위 이미지에서 파란색 원을 C1으로 표시하고 원의 중심을 C1_o로 표시합니다. 그러나 이 원의 무게 중심은 C1_r이며, 파란색 실선 원으로 표시됩니다.
C1_o와 C1_r이 겹치지 않으면 원 C1을 원 C1_r의 중심으로 반복해서 이동합니다. 결국 가장 밀도가 높은 원 C2에 머물게 됩니다.
이미지 처리를 위해 일반적으로 이미지의 역투영 히스토그램을 사용합니다. 추적 대상이 이동하면 이 이동 프로세스가 역투영 히스토그램에 의해 반영될 수 있다는 것이 분명합니다. 따라서 Meanshift 알고리즘은 결국 선택한 창을 이동 대상의 위치로 이동시킵니다. (알고리즘은 결국 수렴함을 증명했습니다.)
Camshift (캠시프트) 알고리즘
이전 설명을 통해 Meanshift 알고리즘이 항상 고정된 창 크기를 추적한다는 것을 알 수 있었습니다. 이는 비디오에서 대상 객체가 반드시 클 필요는 없기 때문에 우리의 요구 사항과 일치하지 않습니다.
그래서 Camshift 는 이 문제를 개선하기 위해 만들어졌습니다. 이는 Camshift 의 Continuously Adaptive Meanshift 에서도 확인할 수 있습니다.
기본 아이디어는 다음과 같습니다. 먼저 Meanshift 알고리즘을 적용합니다. Meanshift 결과가 수렴되면 Camshift 는 창 크기를 업데이트하고, 창에 맞게 방향성 타원을 계산한 다음, 해당 타원을 새로운 창으로 적용하여 Meanshift 알고리즘을 적용합니다.
첫 번째 매개변수 probImage는 대상 히스토그램의 역투영입니다. 두 번째 매개변수 window는 Camshift 알고리즘의 검색 창입니다. 세 번째 매개변수 criteria는 알고리즘 종료 (종료) 조건입니다.
분석 (Analysis) 단계
Camshift 알고리즘의 기본 아이디어를 이해한 후, 이 코드의 구현이 주로 몇 단계로 나뉜다는 것을 분석할 수 있습니다.
추적할 대상을 선택하기 위해 마우스 콜백 이벤트 (mouse callback event) 를 설정합니다.
비디오 스트림에서 이미지를 읽습니다.
Camshift 프로세스를 구현합니다.
아래에서 main.cpp의 코드를 계속 수정합니다.
마우스 콜백 함수로 추적 대상 객체 선택하기 (객체 추적)
OpenCV 는 OpenGL 과 다릅니다. 마우스 콜백 함수에는 다섯 개의 매개변수가 지정됩니다. 처음 세 개가 우리에게 가장 필요한 것입니다. event의 값을 통해 마우스 왼쪽 버튼이 눌린 이벤트 (CV_EVENT_LBUTTONDOWN), 마우스 왼쪽 버튼이 떼어진 이벤트 (CV_EVENT_LBUTTONUP) 등을 얻을 수 있습니다.
bool selectObject = false; // 객체가 선택되었는지 여부를 나타내는 데 사용
int trackObject = 0; // 1 은 추적 객체가 있음을 의미하고, 0 은 객체가 없음을 의미하며, -1 은 Camshift 속성을 계산하지 않았음을 의미합니다.
cv::Rect selection; // 마우스로 선택한 영역을 저장합니다.
cv::Mat image; // 비디오에서 이미지를 캐시합니다.
// OpenCV 의 마우스 콜백 함수:
// void onMouse(int event, int x, int y, int flag, void *param)
// 네 번째 매개변수 `flag` 는 추가 상태를 나타냅니다.
// param 은 사용자 매개변수를 의미하며, 필요하지 않으므로 이름을 지정하지 않습니다.
void onMouse( int event, int x, int y, int, void* ) {
static cv::Point origin;
if(selectObject) {
// 선택된 높이, 너비 및 왼쪽 상단 모서리 위치 결정
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
// &는 cv::Rect 에 의해 덮어쓰여집니다.
// 이는 두 영역의 교차점을 의미합니다.
// 여기의 주요 목적은 선택된 영역 외부의 영역을 처리하는 것입니다.
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
// 왼쪽 버튼이 눌린 경우 처리
case CV_EVENT_LBUTTONDOWN:
origin = cv::Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
// 왼쪽 버튼이 떼어진 경우 처리
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1; // 추적 객체가 Camshift 속성을 계산하지 않았습니다.
break;
}
}
비디오 스트리밍에서 이미지 읽어오기 (영상 스트리밍, 이미지 로드)
비디오 스트리밍을 읽는 구조를 구현했습니다. 더 자세한 내용을 작성해 보겠습니다.
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
// 1. 마우스 이벤트 콜백 등록
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame;
// 2. 비디오에서 이미지 읽기
while(true) {
video >> frame;
if(frame.empty()) break;
// 캐싱을 위해 프레임에서 전역 변수 이미지로 이미지 쓰기
frame.copyTo(image);
// 객체를 선택하는 경우 사각형 그리기
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
// 할당된 메모리 해제
cv::destroyAllWindows();
video.release();
return 0;
}
참고:
ROI (관심 영역, Region of Interest): 이미지 처리에서 처리할 모든 영역은 관심 영역, 즉 ROI 가 될 수 있습니다.
OpenCV 를 이용한 Camshift 구현 (Camshift, OpenCV)
추적 대상 계산을 위한 역투영 히스토그램은 cvtColor 함수를 사용해야 하며, 이 함수는 RGB 색상 공간의 원본 이미지를 HSV 색상 공간으로 변환할 수 있습니다. 히스토그램 계산은 초기 대상을 선택한 후에 수행되어야 하므로 다음과 같습니다.
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame;
cv::Mat hsv, hue, mask, hist, backproj;
cv::Rect trackWindow; // 추적 윈도우
int hsize = 16; // 히스토그램용
float hranges[] = {0,180}; // 히스토그램용
const float* phranges = hranges; // 히스토그램용
while(true) {
video >> frame;
if(frame.empty()) break;
frame.copyTo(image);
// HSV 공간으로 변환
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 객체가 있는 경우 처리
if(trackObject) {
// H: 0~180, S: 30~256, V: 10~256 만 처리하고 나머지는 필터링하여 나머지를 마스크에 복사합니다.
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
// hsv 에서 채널 h 분리
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
// 추적 객체가 계산되지 않은 경우 속성 추출
if( trackObject < 0 ) {
// 채널 h 및 마스크 ROI 설정
cv::Mat roi(hue, selection), maskroi(mask, selection);
// ROI 히스토그램 계산
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
// 히스토그램 정규화
normalize(hist, hist, 0, 255, CV_MINMAX);
// 추적 객체 설정
trackWindow = selection;
// 추적 객체가 계산되었음을 표시
trackObject = 1;
}
// 역투영 히스토그램
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
// 공통 영역 가져오기
backproj &= mask;
// Camshift 알고리즘 호출
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
// 그리기에는 영역이 너무 작음
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
}
// 추적 영역 그리기
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
요약 (요약, Summary)
다음은 이 프로젝트에서 작성한 모든 내용을 보여줍니다.
#include <opencv2/opencv.hpp>
bool selectObject = false; // 객체 선택 여부 사용
int trackObject = 0; // 1 은 추적 객체가 있음을 의미하고, 0 은 객체가 없음을 의미하며, -1 은 Camshift 속성을 계산하지 않았음을 의미합니다.
cv::Rect selection; // 마우스로 선택한 영역 저장
cv::Mat image; // 비디오에서 이미지 캐시
// OpenCV 의 마우스 콜백 함수:
// void onMouse(int event, int x, int y, int flag, void *param)
// 네 번째 매개변수 `flag` 는 추가 상태를 나타냅니다.
// param 은 사용자 매개변수를 의미하며, 필요하지 않으므로 이름이 없습니다.
void onMouse( int event, int x, int y, int, void* ) {
static cv::Point origin;
if(selectObject) {
// 선택된 높이와 너비 및 왼쪽 상단 모서리 위치 결정
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
// &는 cv::Rect 에 의해 덮어쓰기됩니다.
// 이는 두 영역의 교차점을 의미합니다.
// 여기의 주요 목적은 선택된 영역 외부의 영역을 처리하는 것입니다.
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
// 왼쪽 버튼이 눌린 경우 처리
case CV_EVENT_LBUTTONDOWN:
origin = cv::Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
// 왼쪽 버튼이 해제된 경우 처리
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1; // 추적 객체가 Camshift 속성을 계산하지 않음
break;
}
}
int main( int argc, const char** argv ) {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at LabEx");
cv::setMouseCallback("CamShift at LabEx", onMouse, NULL);
cv::Mat frame, hsv, hue, mask, hist, backproj;
cv::Rect trackWindow; // 추적 윈도우
int hsize = 16; // 히스토그램용
float hranges[] = {0,180}; // 히스토그램용
const float* phranges = hranges; // 히스토그램용
while(true) {
video >> frame;
if(frame.empty()) break;
frame.copyTo(image);
// HSV 공간으로 변환
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 객체가 있는 경우 처리
if(trackObject) {
// H: 0~180, S: 30~256, V: 10~256 만 처리하고 나머지는 필터링하여 나머지를 마스크에 복사합니다.
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
// hsv 에서 채널 h 분리
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
// 추적 객체가 계산되지 않은 경우 속성 추출
if( trackObject < 0 ) {
// 채널 h 및 마스크 ROI 설정
cv::Mat roi(hue, selection), maskroi(mask, selection);
// ROI 히스토그램 계산
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
// 히스토그램 정규화
normalize(hist, hist, 0, 255, CV_MINMAX);
// 추적 객체 설정
trackWindow = selection;
// 추적 객체가 계산되었음을 표시
trackObject = 1;
}
// 역투영 히스토그램
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
// 공통 영역 가져오기
backproj &= mask;
// Camshift 알고리즘 호출
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
// 그리기에는 영역이 너무 작음
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
}
// 추적 영역 그리기
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at LabEx", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
main.cpp를 다시 컴파일해 보겠습니다.
g++ main.cpp $(pkg-config opencv --libs --cflags opencv) -o main