지정한 색의 원만 검출하기 (OpenCV 2.4.8, Ubuntu 12.04, Eclipse) :: Horizontal Grays S2

특정색을 지정한 뒤 화면에서 그 색의 원만 검출한다.


일정환경에서 그냥 원을 찾는 것은 쉬운데 이건 생각보다 쉽지 않다.


이유는 기본적으로 물체추적을 할 때 영상을 흑백으로 전환하고 이진화하고 엣지 디텍트를 하는데


흑백으로 변환하면 색정보를 잃어버리고 또한 색에 따라서 이진화할 때 옵셋을 다르게 해주어야 하기 때문이다.


그래서 폭풍검색 결과 색공간을 변환해서 사용한다는 것을 알아냄

(색추출 알고리즘 참고 링크 : http://newstyle.egloos.com/2707246)


1. 우선 특정 포인트의 색을 지정하기 위해서 마우스 핸들러 프로그래밍

2. 원 영상의 Smoothing 

3. 색공간 변환 RGB -> HSV

4. 지정한 포인트의 H,S,V 값 추출

5. 검출할 H,S,V 값의 범위 지정

6. HSV 이미지 마스킹

7. 모폴로지 

8. 이미지에서 원 검출


하기로 한다.


1. 마우스 핸들링

 OpenCV의 HighGui는 마우스 이벤트에 대한 핸들러도 제공한다. cvSetMouseCallback() 이라는 함수를 이용하는데 OpenCV에서는 아래와 같이 설명한다


Sets mouse handler for the specified window


C++: void setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata=0 )

C: void cvSetMouseCallback(const char* window_name, CvMouseCallback on_mouse, void* param=NULL )

Python: cv.SetMouseCallback(windowName, onMouse, param=None) → None

Parameters:

winname – Window name

onMouse – Mouse callback. See OpenCV samples, such as https://github.com/Itseez/opencv/tree/master/samples/cpp/ffilldemo.cpp, on how to specify and use the callback.

userdata – The optional parameter passed to the callback.


link : http://docs.opencv.org/modules/highgui/doc/user_interface.html?highlight=cvsetmousecallback#void cvSetMouseCallback(const char* window_name, CvMouseCallback on_mouse, void* param)


cvSetMouseCallback("PreviewImage",on_eventhandle,(void*)img); 

위와 같은 식으로 사용하였는데 img 영상에서 마우스 이벤트가 발생하면 on_eventhandle 라는 함수를 호출하도록 Setting 하는 것이다.

(http://yonghello.tistory.com/entry/%EB%A7%88%EC%9A%B0%EC%8A%A4-%EC%9D%B4%EB%B2%A4%ED%8A%B8  참조하였슴)


작성한 on_eventhandle는 아래와 같다.

void on_eventhandle(int event, int x ,int y, int flag, void* param)

{

switch(event)

{

case CV_EVENT_LBUTTONDOWN:

if(getFlag == 0)

{

getx = x;

gety = y;

getFlag = 1;

}else if(getFlag == 1)

{

getFlag = 2;

}

break;


case CV_EVENT_RBUTTONDOWN:

getFlag = 0;

break;

}

}


별다른건 없고 마우스 왼쪽 버튼 누르면 좌표를 얻어오고 한번 더 누르면 그 좌표로 셋팅, 우측 버튼 누르면 다시 처음부터 좌표 얻어올 수 있도록 초기화 하는거임.


3. 색공간 변환

색공간을 기존의 RGB(Red,Green,Blue) color space 에서 HSV(Hue, Saturation, Value - Brightness) 로 바꾼다 (http://ko.wikipedia.org/wiki/HSV_%EC%83%89_%EA%B3%B5%EA%B0%84)

OpenCV에서는  cvCvtColor(src,dest,CV_BGR2HSV); 로 간단하게 바꿀 수 있다.


4. 지정한 포인트의 H,S,V 추출

cvGet2D()함수를 이용하여 cvScalar로 저장하는 방법을 사용

cvGet2D()는 아래와 같이 설명하고 있다.

C: CvScalar cvGet1D(const CvArr* arr, int idx0)

C: CvScalar cvGet2D(const CvArr* arr, int idx0, int idx1)

C: CvScalar cvGet3D(const CvArr* arr, int idx0, int idx1, int idx2)

C: CvScalar cvGetND(const CvArr* arr, const int* idx)

Python: cv.Get1D(arr, idx) → scalar

Python: cv.Get2D(arr, idx0, idx1) → scalar

Python: cv.Get3D(arr, idx0, idx1, idx2) → scalar

Python: cv.GetND(arr, indices) → scalar

Return a specific array element.


Parameters:

arr – Input array

idx0 – The first zero-based component of the element index

idx1 – The second zero-based component of the element index

idx2 – The third zero-based component of the element index

idx – Array of the element indices

The functions return a specific array element. In the case of a sparse array the functions return 0 if the requested node does not exist (no new node is created by the functions).


link : http://docs.opencv.org/modules/core/doc/old_basic_structures.html?highlight=cvget2d#CvScalar cvGet2D(const CvArr* arr, int idx0, int idx1)


뭐 별건 없다. image에 지정한 x,y 좌표의 색정보를 가져오는 것임

CvScalar s = cvGet2D(HSVImg,gety,getx);

H = (int)s.val[0];

S = (int)s.val[1];

V = (int)s.val[2];


위와 같이 사용하였는데 여기서 중요한 것 하나!!!

cvGet2D의  첫번째 파라미터는 image, 두번째 파라미터는 y좌표, 세번째 파리미터는 x좌표 라는 것!! 

의례적으로 x,y 로 썼다가 한참 헤맸다. T.T

해당 영상이 HSV color space 이므로 자동으로 H,S,V 값으로 저장된다.

만약 RGB Color Space 라면

CvScalar v = cvGet2D(img,gety,getx);

getBlue = (int)v.val[0];

getGreen = (int)v.val[1];

getRed = (int)v.val[2];

역시 자동으로 Blue,Green,Red 값으로 받아들인다. 여기서 (Red,Green,Blue의 순이 아닌 Blue,Green,Red 순이라는 것 주의)


5. 검출할 H,S,V의 범위 지정

이건 같은 H,S,V 값만 가지는 픽셀을 선택하면 원의 색상 자체가 완전히 일정하지 않으므로 일정 범위를 줄 필요가 있겠다.

나는 그냥 원시적으로 다음과 같이 사용


//calculate HSV Boundary

lowH = H * LOW_BOUND;

highH = H * HIGH_BOUND;

if(highH >= 180) highH = 179;


lowS = S * LOW_BOUND;

highS = S * HIGH_BOUND;

if(highS >= 256) highS = 255;


lowV = V * LOW_BOUND;

highV = V * HIGH_BOUND;

if(highV >= 256) highV = 255;


위 소스를 보면 OpenCV의 HSV Color Space에서 각 H,S,V 값의 범위는 H = 0~179, S = 0~255, V = 0~255 라는 것을 알 수 있다.


6. HSV 이미지 마스킹

 지정한 범위의 색 외에 나머지는 모두 마스킹 시켜 원을 검출하기 용이하도록 한다.

 마스킹은 IplImage 가 아닌 Mat를 이용한다.


cvCreateMat(int rows, int cols, int type);

Creates a matrix header and allocates the matrix data.


C: CvMat* cvCreateMat(int rows, int cols, int type)

Python: cv.CreateMat(rows, cols, type) → mat

Parameters:

rows – Number of rows in the matrix

cols – Number of columns in the matrix

type – The type of the matrix elements in the form CV_<bit depth><S|U|F>C<number of channels> , where S=signed, U=unsigned, F=float. For example, CV _ 8UC1 means the elements are 8-bit unsigned and the there is 1 channel, and CV _ 32SC2 means the elements are 32-bit signed and there are 2 channels.

The function call is equivalent to the following code:


link : http://docs.opencv.org/modules/core/doc/old_basic_structures.html?highlight=cvcreatemat#CvMat* cvCreateMat(int rows, int cols, int type)


cvInRange(const CvArr* src, const CvArr* lower, const CvArr* upper, CvArr* dst)

Checks if array elements lie between the elements of two other arrays.


C++: void inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst)

Python: cv2.inRange(src, lowerb, upperb[, dst]) → dst

C: void cvInRange(const CvArr* src, const CvArr* lower, const CvArr* upper, CvArr* dst)

C: void cvInRangeS(const CvArr* src, CvScalar lower, CvScalar upper, CvArr* dst)

Python: cv.InRange(src, lower, upper, dst) → None

Python: cv.InRangeS(src, lower, upper, dst) → None

Parameters:

src – first input array.

lowerb – inclusive lower boundary array or a scalar.

upperb – inclusive upper boundary array or a scalar.

dst – output array of the same size as src and CV_8U type.

The function checks the range as follows:


For every element of a single-channel input array:


\texttt{dst} (I)= \texttt{lowerb} (I)_0  \leq \texttt{src} (I)_0 \leq  \texttt{upperb} (I)_0


For two-channel arrays:


\texttt{dst} (I)= \texttt{lowerb} (I)_0  \leq \texttt{src} (I)_0 \leq  \texttt{upperb} (I)_0  \land \texttt{lowerb} (I)_1  \leq \texttt{src} (I)_1 \leq  \texttt{upperb} (I)_1


and so forth.


That is, dst (I) is set to 255 (all 1 -bits) if src (I) is within the specified 1D, 2D, 3D, ... box and 0 otherwise.


When the lower and/or upper boundary parameters are scalars, the indexes (I) at lowerb and upperb in the above formulas should be omitted.


link : http://docs.opencv.org/modules/core/doc/operations_on_arrays.html?highlight=cvinranges#void cvInRangeS(const CvArr* src, CvScalar lower, CvScalar upper, CvArr* dst)


mask = cvCreateMat(size.height, size.width,CV_8UC1);

cvInRangeS(HSVImg, cvScalar(lowH,lowS,lowV),cvScalar(highH,highS,highV),mask);


이와 같이 mask Mat를 만들고 cvInRanges 하면 원래 image에 정하진 범위 안의 H,S,V 값만을 남기는 Masking 이 완료된다.


7. 모폴로지

Masking 한 이미지는 도형 내부의 그림자나 외부와의 간섭으로 인한 불필요한 것들을 보정해 줄 필요가 있다. 

내가 사용한 cvErode()와 cvDilate()외에  OpenCV에서는 그 외에 다른 모폴로지 함수(cvMorphologyEx와 같은)를 많이 제공한다.

cvErode는 침식, cvDelate는 팽창  http://bench87.tistory.com/47 여기에 예제와 이미지까지 잘 설명되어 있다.


Erodes an image by using a specific structuring element.


C++: void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue() )

Python: cv2.erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) → dst

C: void cvErode(const CvArr* src, CvArr* dst, IplConvKernel* element=NULL, int iterations=1)

Python: cv.Erode(src, dst, element=None, iterations=1) → None

Parameters:

src – input image; the number of channels can be arbitrary, but the depth should be one of CV_8U, CV_16U, CV_16S, CV_32F` or ``CV_64F.

dst – output image of the same size and type as src.

element – structuring element used for erosion; if element=Mat() , a 3 x 3 rectangular structuring element is used.

anchor – position of the anchor within the element; default value (-1, -1) means that the anchor is at the element center.

iterations – number of times erosion is applied.

borderType – pixel extrapolation method (see borderInterpolate() for details).

borderValue – border value in case of a constant border (see createMorphologyFilter() for details).

The function erodes the source image using the specified structuring element that determines the shape of a pixel neighborhood over which the minimum is taken:


\texttt{dst} (x,y) =  \min _{(x',y'):  \, \texttt{element} (x',y') \ne0 } \texttt{src} (x+x',y+y')


The function supports the in-place mode. Erosion can be applied several ( iterations ) times. In case of multi-channel images, each channel is processed independently.


link : http://docs.opencv.org/modules/imgproc/doc/filtering.html?highlight=cverode#void cvErode(const CvArr* src, CvArr* dst, IplConvKernel* element, int iterations)


Dilates an image by using a specific structuring element.


C++: void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor=Point(-1,-1), int iterations=1, int borderType=BORDER_CONSTANT, const Scalar& borderValue=morphologyDefaultBorderValue() )

Python: cv2.dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) → dst

C: void cvDilate(const CvArr* src, CvArr* dst, IplConvKernel* element=NULL, int iterations=1 )

Python: cv.Dilate(src, dst, element=None, iterations=1) → None

Parameters:

src – input image; the number of channels can be arbitrary, but the depth should be one of CV_8U, CV_16U, CV_16S, CV_32F` or ``CV_64F.

dst – output image of the same size and type as src.

element – structuring element used for dilation; if element=Mat() , a 3 x 3 rectangular structuring element is used.

anchor – position of the anchor within the element; default value (-1, -1) means that the anchor is at the element center.

iterations – number of times dilation is applied.

borderType – pixel extrapolation method (see borderInterpolate() for details).

borderValue – border value in case of a constant border (see createMorphologyFilter() for details).

The function dilates the source image using the specified structuring element that determines the shape of a pixel neighborhood over which the maximum is taken:


\texttt{dst} (x,y) =  \max _{(x',y'):  \, \texttt{element} (x',y') \ne0 } \texttt{src} (x+x',y+y')


The function supports the in-place mode. Dilation can be applied several ( iterations ) times. In case of multi-channel images, each channel is processed independently.



cvErode(mask,mask,NULL);

cvDilate(mask,mask,NULL);


위와 같이 사용하였는데 3번째 파라미터에 따라 그 침식 또는 팽창의 정도를 수정할 수 있다.


이렇게 완성한 영상을 마지막으로 기존에 해왔던 것 처럼 cvHoughCircle 함수를 이용하여 원을 찾아내면 끝


이렇게 완성한 소스는 다음과 같다.

(역시 대충 삽질해가며 작성하였기에 주석처리해버린 것들이라던지 좀 무식한 방법이라던지 그대로다 ㅋ)

//============================================================================

// Name        : GetColor.cpp

// Author      : 

// Version     :

// Copyright   : Your copyright notice

// Description : Hello World in C++, Ansi-style

//============================================================================


#include <iostream>

#include <stdio.h>

#include <sys/time.h>



#include "opencv2/opencv.hpp"


#define LOW_BOUND 0.7

#define HIGH_BOUND 1.3


struct timeval start_time;

struct timeval end_time;

double elapsed_time;

float fps;

double total_fps = 0.0,average_fps = 0.0;

long cnt=1;

char buffer[25];

CvFont font;

char getFlag = 0;

long getx,gety;

int getRed,getGreen,getBlue;

int Bval,Rval,Gval;


using namespace std;


void on_eventhandle(int event, int x ,int y, int flag, void* param)

{

switch(event)

{

case CV_EVENT_LBUTTONDOWN:


if(getFlag == 0)

{

getx = x;

gety = y;

getFlag = 1;

}else if(getFlag == 1)

{

getFlag = 2;

}

break;


case CV_EVENT_RBUTTONDOWN:

getFlag = 0;

break;

}




}

int main() {


IplImage* img;

IplImage* GrayImg=0;

IplImage* SmoothImg=0;

IplImage* Edge=0;

IplImage* HSVImg;

IplImage* HImg = 0;

IplImage* SImg = 0;

IplImage* VImg = 0;

CvMat* mask;

int key;

int k;

double dp = 1;

double min_dist = 300;

int H,S,V;

int lowH,lowS,lowV,highH,highS,highV;

CvSize size;

int cx,cy,radius,ccnt;

cvInitFont(&font,CV_FONT_HERSHEY_SIMPLEX,0.5,0.5);



CvCapture* capture = cvCaptureFromCAM(0);

cvSetCaptureProperty( capture, CV_CAP_PROP_FRAME_WIDTH, 640);

cvSetCaptureProperty( capture, CV_CAP_PROP_FRAME_HEIGHT, 480);

cvSetCaptureProperty( capture, CV_CAP_PROP_FPS, 65);



while(1)

{

gettimeofday(&start_time,NULL);


cvGrabFrame(capture);

img = cvRetrieveFrame(capture);


cvSetMouseCallback("PreviewImage",on_eventhandle,(void*)img);


if(getFlag == 0)

{

sprintf(buffer,"Picking up color by left click");

cvPutText(img,buffer,cvPoint(200,20),&font,CV_RGB(255,255,255));

}else if(getFlag == 1)

{

sprintf(buffer,"If below color is right you chosen, start detect by left click again");

cvPutText(img,buffer,cvPoint(20,20),&font,CV_RGB(255,255,255));

size = cvGetSize(img);

CvScalar v = cvGet2D(img,gety,getx);

getBlue = (int)v.val[0];

getGreen = (int)v.val[1];

getRed = (int)v.val[2];

//Convert get RGB to HSV

SmoothImg = cvCreateImage(size,IPL_DEPTH_8U,3);

cvSmooth(img,SmoothImg,CV_GAUSSIAN,9,9,2,2);

HSVImg = cvCreateImage(size,IPL_DEPTH_8U,3);

cvCvtColor(SmoothImg,HSVImg,CV_BGR2HSV);

CvScalar s = cvGet2D(HSVImg,gety,getx);

H = (int)s.val[0];

S = (int)s.val[1];

V = (int)s.val[2];


sprintf(buffer,"X= %d,Y= %d,R= %d G= %d, B= %d, H= %d, S= %d, V= %d",getx,gety,getRed,getGreen,getBlue,H,S,V);

cvPutText(img,buffer,cvPoint(getx,gety),&font,CV_RGB(255,255,255));

//printf("X= %d,Y= %d,Red = %d, Green = %d, Blue = %d\n",getx,gety,getRed,getGreen,getBlue);


}else{

sprintf(buffer,"You can picking up again by right click");

cvPutText(img,buffer,cvPoint(100,20),&font,CV_RGB(255,255,255));

//SmoothImg = cvCreateImage(size,IPL_DEPTH_8U,3);

cvSmooth(img,SmoothImg,CV_GAUSSIAN,9,9,2,2);

//Change Color Space

cvCvtColor(SmoothImg,HSVImg,CV_BGR2HSV);


//calculate HSV Boundary

lowH = H * LOW_BOUND;

highH = H * HIGH_BOUND;

if(highH >= 180) highH = 179;


lowS = S * LOW_BOUND;

highS = S * HIGH_BOUND;

if(highS >= 256) highS = 255;


lowV = V * LOW_BOUND;

highV = V * HIGH_BOUND;

if(highV >= 256) highV = 255;

//Masking

mask = cvCreateMat(size.height, size.width,CV_8UC1);

cvInRangeS(HSVImg, cvScalar(lowH,lowS,lowV),cvScalar(highH,highS,highV),mask);

//CvMat* tempMat;

//tempMat = cvCreateMat(size.height, size.width,CV_8UC1);



//cvMorphologyEx(mask,mask,tempMat,NULL,CV_MOP_OPEN);

//IplConvKernel *se21 = cvCreateStructuringElementEx(21,21,10,10,CV_SHAPE_RECT,NULL);

//IplConvKernel *se11 = cvCreateStructuringElementEx(11,11,5,5,CV_SHAPE_RECT,NULL);

cvErode(mask,mask,NULL);

cvDilate(mask,mask,NULL);


//cvErode(mask,mask,se21);

//cvDilate(mask,mask,se11);

//cvErode(mask,mask,se21);

//cvReleaseStructuringElement(&se21);

//cvReleaseStructuringElement(&se11);

//cvReleaseMat(&tempMat);


//Hough Tranform

GrayImg = cvCreateImage(size,8,1);

cvCopy(mask,GrayImg,NULL);

cvSmooth(GrayImg,GrayImg,CV_GAUSSIAN,15,15,2,2);


CvMemStorage* storage = cvCreateMemStorage(0);


CvSeq* Circles = cvHoughCircles(GrayImg,storage,CV_HOUGH_GRADIENT,2,size.height/10,100,40,5,60);

//CvSeq* Circles = cvHoughCircles(GrayImg,storage,CV_HOUGH_GRADIENT,1,100,100,50,0,0);


for(k=0;k<Circles->total;k++)

{

float* circle;

circle = (float*)cvGetSeqElem(Circles,k);


ccnt = Circles->total;


cx = cvRound(circle[0]);

cy = cvRound(circle[1]);

radius = cvRound(circle[2]);

cvCircle(img,cvPoint(cx,cy),radius,CV_RGB(0,255,0),3,8,0);


sprintf(buffer,"X=%d, Y=%d, R=%d",cx,cy,radius);

cvPutText(img,buffer,cvPoint(cx-80,cy+radius+15),&font,CV_RGB(255,255,255));

}


cvReleaseMemStorage(&storage);


cvNamedWindow( "HSVImage", 1 );

cvShowImage( "HSVImage", HSVImg);

cvMoveWindow("HSVImage",850,200);


cvNamedWindow( "Mask", 1 );

cvShowImage( "Mask", mask);

cvMoveWindow("Mask",850,700);



}


cvNamedWindow( "PreviewImage", 1 );

cvShowImage( "PreviewImage", img );

cvMoveWindow("PreviewImage",200,200);


//cvNamedWindow( "GrayImage", 1 );

//cvShowImage( "GrayImage", GrayImg );

//cvMoveWindow("GrayImage",850,400);



key = cvWaitKey(1);

if(key > 10) break;


gettimeofday(&end_time, NULL);



elapsed_time = (double)(end_time.tv_sec) + (double)(end_time.tv_usec)/1000000.0 - (double)(start_time.tv_sec) - (double)(start_time.tv_usec)/1000000.0;

fps = 1.0 / elapsed_time;

total_fps += fps;

average_fps = total_fps / (double)(cnt);

cnt++;

if(cnt == 100)

{

cnt = 1;

total_fps = 0.0;

}

elapsed_time *= 1000.0;


printf("Elapsed T = %2.3f ms, Frame = %2.3f (fps), Avrg Frame Rate = %2.3f\n",elapsed_time,fps,average_fps);



//cvReleaseMemStorage(&storage);

} //end of while(1)

cvReleaseImage(&img);

cvReleaseCapture( &capture );

cvReleaseMat(&mask);

cvReleaseImage(&SmoothImg);

cvReleaseImage(&GrayImg);

cvReleaseImage(&HSVImg);

return 0;

}


그래서 잘 찾느냐.. 그 결과는 다음과 같았다.




왼쪽 위의 영상이 원래의 이미지, 녹색의 원이 파란색의 물체를 잘 찾았슴을 보이고 아래에 마우스로 찍은 X,Y 좌표가 표시되고 있다.

우측 위의 영상은 원래의 이미지를 HSV Color Space로 변환한 이미지

우측 아래의 영상은 마스킹하고 모폴로지 까지 완료한 영상 :)


끝! ㅋ


+ Recent posts