[w6d4] 특정 색상 영역 추출과 Edge detection

76343 단어 opencvopencv

특정 색상 영역 추출

밝기의 영향에 따라서 RGB space는 특정 색상을 찾을 때 적절하지 않게 동작하는 경우가 많다.
HSV, YCrCb 공간에서는 각각 V(max(R,G,B)), Y(Gray scale image 산출 공식)값을 제외한 정보를 이용해 색상 영역을 추출한다. 조명 변화에 대해 비교적 강인한 특성을 가진다.(robustness)

HSV에서 처리할 때는 S 값이 일정 값 이상이어야 색상 구분에 어려움이 없으며, V값은 밝기.
원색에 가까운 색을 구분할 때 편리하다.

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

lowerb, upperb에 cv::Scalar를 적용할 경우, 모든 픽셀에 같은 기준을 적용한다.
lowerb, upperb에 입력영상과 동일한 크기의 cv::Mat객체를 주는 경우 각각의 픽셀마다 다른 상한값과 하한값을 설정할 수 있다. OutputArray는 Mask image 형태로 나타난다.

Hue 값을 적절히 설정하여 코드를 작성하면 src에서 원하는 영역만 추출할 수 있음. 붉은색 영역부터 시계방향으로 숫자가 커짐. (0~180)

#include <iostream>
#include "opencv2/opencv.hpp"

int pos_hue1=5, pos_hue2=30,pos_sat1=200,pos_sat2=255;
void on_hsv_changed(int , void*);

cv::Mat src,src_hsv,dst,dst_mask,temp_mask;

int main(int argc, char* argv[])
{   
    if (argc < 2){
        src = cv::imread("./resources/flower1.png",cv::IMREAD_COLOR);
    }
    else {
        src = cv::imread(argv[1], cv::IMREAD_COLOR);
    }

    if (src.empty()){
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }
    
    cv::cvtColor(src,src_hsv,cv::COLOR_BGR2HSV);
    cv::imshow("src",src);
    cv::namedWindow("dst");
    cv::createTrackbar("Lower Hue: ","dst",&pos_hue1,180,on_hsv_changed);
    cv::createTrackbar("Upper Hue: ","dst",&pos_hue2,180,on_hsv_changed);
    cv::createTrackbar("Lower Sat: ","dst",&pos_sat1,255,on_hsv_changed);
    cv::createTrackbar("Upper Sat: ","dst",&pos_sat2,255,on_hsv_changed);
    on_hsv_changed(0,0);
    while (cv::waitKey()!=27) continue;
    cv::destroyAllWindows();
    return 0;
}

void on_hsv_changed(int ,void*)
{
    if (pos_hue1>pos_hue2){
        cv::Scalar lowerb1(pos_hue1,pos_sat1,0);
        cv::Scalar upperb1(180,pos_sat2,255);
        cv::Scalar lowerb2(0,pos_sat1,0);
        cv::Scalar upperb2(pos_hue2,pos_sat2,255);
        cv::inRange(src_hsv,lowerb1,upperb1,temp_mask);
        cv::inRange(src_hsv,lowerb2,upperb2,dst_mask);
        dst_mask = temp_mask+dst_mask;
    }
    else{
        cv::Scalar lowerb(pos_hue1,pos_sat1,0);
        cv::Scalar upperb(pos_hue2,pos_sat2,255);
        cv::inRange(src_hsv,lowerb,upperb,dst_mask);
    }

    cv::cvtColor(src,dst,cv::COLOR_BGR2GRAY);
    cv::cvtColor(dst,dst,cv::COLOR_GRAY2BGR);
    src.copyTo(dst,dst_mask);

    cv::imshow("dst",dst);
    cv::imshow("dst_mask",dst_mask);
}



Lower Hue 값이 Upper Hue값보다 커졌을 때는 Upper Hue~180, 0~Lower Hue 범위를 포함하게 코드를 작성하였다. 코드 실행 결과를 보았을 때, HSV color space로 색상을 선택하는 경우 결과에 영향을 주는 주된 인자는 Hue 값이라는 것을 확인할 수 있었다.

https://en.wikipedia.org/wiki/HSL_and_HSV

히스토그램 역투영(histogram backprojection)

이미지 샘플을 가지고 있을 때 유사한 영역을 찾는 방식으로 특정한 Hue값을 지정하기 어려운 경우 히스토그램 역투영 방식을 고려해볼 수 있다. 일반적으로 픽셀값 추출 후 가우시안을 적용하면 specific한 정보를 일반화시켜 보다 나은 결과를 얻을 수 있다. 여기서 픽셀값 추출 후 색 검출에는 YCrCb 방식을 이용하며 밝기 성분인 Y값은 무시하고 Cr, Cb 평면에 히스토그램을 얻는다. 최대값 성분을 255로 표준화해주어 gray scale image 형태를 얻을 수 있으며 0~255의 값을 가진다.

Rect cv::selectROI 	( 	const String &  	windowName,
		InputArray  	img,
		bool  	showCrosshair = true,
		bool  	fromCenter = false 
	) 		

OpenCV에서는 영역 선택하기 위해 cv::selectROI 함수를 사용할 수 있다. windowName을 입력하고 img에 source가 될 영상을 넣어준다. 실행 후 마우스로 영역을 선택한뒤 space나 enter를 치면 영역 선택을 완료할 수 있으며, c를 누르면 영역선택을 취소해 zero값을 리턴할 수 있다.

void cv::calcHist 	( 	const Mat *  	images,
		int  	nimages,
		const int *  	channels,
		InputArray  	mask,
		OutputArray  	hist,
		int  	dims,
		const int *  	histSize,
		const float **  	ranges,
		bool  	uniform = true,
		bool  	accumulate = false 
	) 		

cv::calcHist를 이용해 bgr 히스토그램을 표현한 코드와 결과는 아래와 같다.

#include <iostream>
#include "opencv2/opencv.hpp"
#include <vector>
#include <algorithm>

int main()
{
    cv::Mat src = cv::imread("../resources/lenna.bmp",cv::IMREAD_COLOR);

    if (src.empty()){
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }

    std::vector<cv::Mat> bgr_planes;
    cv::split(src,bgr_planes);

    int number_bins = 255; // number of bins in histogram

    float range[] = {0.0f , 256.0f}; // between 0~255, upper boundary is exclusive
    const float* hist_range[] = {range};

    const int* channel_numbers = {0}; // 0th plane will be used.
    cv::Mat b_hist,g_hist,r_hist;

    cv::calcHist(&bgr_planes[0],1,channel_numbers,cv::Mat(),b_hist, 1, &number_bins, hist_range);
    cv::calcHist(&bgr_planes[1],1,channel_numbers,cv::Mat(),g_hist, 1, &number_bins, hist_range);
    cv::calcHist(&bgr_planes[2],1,channel_numbers,cv::Mat(),r_hist, 1, &number_bins, hist_range);
    // source array, number of source array, channel to be measured, Mask, output Mat, histogram dimensionality, number of bins per each used dimensions
    
    double b_max, g_max, r_max;
    cv::minMaxLoc(b_hist,0,&b_max);
    cv::minMaxLoc(g_hist,0,&g_max);
    cv::minMaxLoc(r_hist,0,&r_max);

    int hist_maxval = (int)std::max(std::max(b_max,g_max),r_max);
    int hist_rows = 200, hist_cols = 256*3;
    cv::Mat hist_image(hist_rows+1,hist_cols,CV_8UC3,cv::Scalar(255,255,255));

    for (int i=0;i<b_hist.rows-1;i++){
        cv::line(hist_image,cv::Point(hist_cols/256*i,hist_rows-(int)((b_hist.at<float>(i,0))*hist_rows/hist_maxval)),cv::Point(hist_cols/256*(i+1),hist_rows-(int)(b_hist.at<float>(i+1)*hist_rows/hist_maxval)),cv::Scalar(255,0,0));
        cv::line(hist_image,cv::Point(hist_cols/256*i,hist_rows-(int)((g_hist.at<float>(i,0))*hist_rows/hist_maxval)),cv::Point(hist_cols/256*(i+1),hist_rows-(int)(g_hist.at<float>(i+1)*hist_rows/hist_maxval)),cv::Scalar(0,255,0));
        cv::line(hist_image,cv::Point(hist_cols/256*i,hist_rows-(int)((r_hist.at<float>(i,0))*hist_rows/hist_maxval)),cv::Point(hist_cols/256*(i+1),hist_rows-(int)(r_hist.at<float>(i+1)*hist_rows/hist_maxval)),cv::Scalar(0,0,255));
    }

    cv::imshow("src",src);
    cv::imshow("hist_img",hist_image);

    while (cv::waitKey()!=27) continue;
    cv::destroyAllWindows();
    return 0;
}

void cv::calcBackProject 	( 	const Mat *  	images,
		int  	nimages,
		const int *  	channels,
		InputArray  	hist,
		OutputArray  	backProject,
		const float **  	ranges,
		double  	scale = 1,
		bool  	uniform = true 
	) 		

cv::calcHist와 cv::calcBackProject 함수는 주로 같이 사용되며 calchist에서 계산된 hist를 calcBackProject에 이용해 histogram back project를 수행할 수 있다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main()
{
    cv::Mat src = cv::imread("../resources/candy.jpg",cv::IMREAD_COLOR);

    if (src.empty()){
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }

    cv::resize(src,src,cv::Size(),0.4,0.4,cv::INTER_AREA);
    cv::Rect rc = cv::selectROI(src);
    cv::Mat src_ycrcb,crop,hist;

    cv::cvtColor(src,src_ycrcb,cv::COLOR_BGR2YCrCb);
    crop = src_ycrcb(rc);

    int channels[] = {1,2};
    int cr_bins = 256, cb_bins = 256; // number of bins in histogram
    int histSize[] = {cr_bins,cb_bins};
    float cr_range[] = {0,256};
    float cb_range[] = {0,256};
    const float* ranges[] = {cr_range, cb_range};

    cv::calcHist(&crop,1,channels,cv::Mat(),hist,2,histSize,ranges);

    cv::Mat backproj;
    cv::calcBackProject(&src_ycrcb,1,channels,hist,backproj,ranges,1,true);

    cv::Mat dst(src.rows,src.cols,CV_8UC3,cv::Scalar(0,0,0));
    src.copyTo(dst,backproj);

    cv::imshow("backproj",backproj);
    cv::imshow("src",src);
    cv::imshow("dst",dst);

    while (cv::waitKey()!=27) continue;
    cv::destroyAllWindows();
    return 0;
}

edge detection

1st derivative
영상을 (x,y)에 대한 함수로 간주하였을 때, 1차 미분값이 설정된 역치값보다 높은 경우 경계로 검출할 수 있다.
미분을 구하기 전에 노이즈 제거를 위해 가우시안 블러를 적용하는 것이 적합하며, 1차 미분의 근사화는 forward difference, backward difference, centered difference를 통해 할 수 있다.

일반적으로 아래와 같은 필터를 이용해 계산하며 Sobel 필터가 주로 이용된다. 아래와 같은 필터 적용시 주변항의 값을 이용해 노이즈를 어느 정도 제거할 수 있다.

최종적으로는 그래디언트의 크기를 이용해 검출할 수 있다.

void cv::Sobel 	( 	InputArray  	src,
		OutputArray  	dst,
		int  	ddepth,
		int  	dx,
		int  	dy,
		int  	ksize = 3,
		double  	scale = 1,
		double  	delta = 0,
		int  	borderType = BORDER_DEFAULT 
	) 	

cv::Sobel의 ddepth는 output array의 depth로 -1로 할 경우 src와 동일하게 설정된다.
필터 적용에는 일반적으로 dx=1, dy=0에서 x방향 성분, dx=0, dy=1에서 y방향 성분을 계산할 수 있다.

void cv::magnitude 	( 	InputArray  	x,
		InputArray  	y,
		OutputArray  	magnitude 
	) 	

cv::magnitude는 동일한 크기의 x,y를 Input Array로 이용해 각 좌표마다 크기를 계산하여 magnitude에 넣어주는 함수이다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main()
{
    cv::Mat src = cv::imread("./resources/lenna.bmp",cv::IMREAD_GRAYSCALE);

    if (src.empty()){
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }

    //Perwitt는 함수 없이 직접 구현했음. 128은 음수값을 고려하기 위해 더해줌.
    cv::Mat dst1(src.rows,src.cols,src.type());
    cv::Mat dst2(src.rows,src.cols,src.type());
    cv::Mat Prewitt_Mag(src.rows,src.cols,src.type());
    for (int y=1;y<src.rows-1;y++){
        for (int x=1;x<src.cols-1;x++){
            int v1 = src.at<uchar>(y-1,x+1)+src.at<uchar>(y,x+1)+src.at<uchar>(y+1,x+1)
                    -src.at<uchar>(y-1,x-1)-src.at<uchar>(y,x-1)-src.at<uchar>(y+1,x-1);
            dst1.at<uchar>(y,x) = cv::saturate_cast<uchar>(v1+128);
            int v2 = src.at<uchar>(y+1,x-1)+src.at<uchar>(y+1,x)+src.at<uchar>(y+1,x+1)
                    -src.at<uchar>(y-1,x-1)-src.at<uchar>(y-1,x)-src.at<uchar>(y-1,x+1);
            dst2.at<uchar>(y,x) = cv::saturate_cast<uchar>(v2+128);
            int magnitude = (int)sqrt(v1*v1+v2*v2);
            Prewitt_Mag.at<uchar>(y,x) = cv::saturate_cast<uchar>(magnitude);

        }
    }


    cv::Mat SOBEL_x,SOBEL_y,SOBEL_mag;
    cv::Sobel(src,SOBEL_x,CV_32FC1,1,0);
    cv::Sobel(src,SOBEL_y,CV_32FC1,0,1);
    // Sobel은 (0,1) (1,0)으로 둠. 중간 값 계산시 플로트형으로 하면 보다 정확한 결과
    cv::magnitude(SOBEL_x,SOBEL_y,SOBEL_mag);
    SOBEL_x.convertTo(SOBEL_x,CV_8UC1);
    SOBEL_y.convertTo(SOBEL_y,CV_8UC1);
    SOBEL_mag.convertTo(SOBEL_mag,CV_8UC1);

    cv::Mat Prewitt_edge = Prewitt_Mag > 50;
    cv::Mat SOBEL_edge = SOBEL_mag > 50;
    //binarization

    cv::imshow("src",src);
    cv::imshow("Prewitt_MAG",Prewitt_Mag);
    cv::imshow("Prewitt_edge",Prewitt_edge);
    cv::imshow("SOBEL_MAG",SOBEL_mag);
    cv::imshow("SOBEL_edge",SOBEL_edge);
    while (cv::waitKey()!=27) continue;
    cv::destroyAllWindows();
    return 0;
}

Canny edge detection

가우시안 필터링(Optional)
노이즈 제거 용도로 그래디언트 계산 시 필터에 가우시안 개념 포함으로 필수는 아니다.

그래디언트 계산
주로 소벨 필터 이용하며, 가우시안 개념 포함으로 노이즈가 어느 정도 제거된다.
그래디언트의 크기와 방향 구하며, 방향은 4개 구역으로 단순화한다. (좌우, 상하, 두 대각선)

비최대 억제(Non-maximum Suppresion)
하나의 edge가 여러 개의 선으로 표현되는 것을 막기 위해 local maximum만을 edge로 간주.
그래디언트 방향에 대해서만 비교를 수행하여 비최대값을 0으로 억제한다.

히스테리시스 에지 트래킹(Hysteresis edge tracking)
두 개의 임계값 T_high, T_low를 사용한다.

  • T_high보다 크면 강한 edge
  • T_high와 T_low 사이 값은 주변부 따라 갔을 때 T_high와 연결되면 최종 edge로 설정.
  • T_low보다 작은 값은 edge

T_low와 T_high 비율은 1:3 정도로 준다.

void cv::Canny 	( 	InputArray  	image,
		OutputArray  	edges,
		double  	threshold1,
		double  	threshold2,
		int  	apertureSize = 3,
		bool  	L2gradient = false 
	) 	

cv::Canny의 코드의 실제는 단순한데, Input image, Output array를 입력 후 threshold 값만 두 개 지정하면 사용할 수 있다. 아래는 코드와 결과이다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main()
{
    cv::Mat src = cv::imread("../resources/chameleon.jpg",cv::IMREAD_COLOR);

    if (src.empty()){
        std::cerr << "Image load failed!" << std::endl;
        return -1;
    }
    cv::Mat dst;
    cv::Canny(src,dst,50,150);
    cv::imshow("src",src);
    cv::imshow("dst",dst);
    while (cv::waitKey()!=27) continue;
    cv::destroyAllWindows();
    return 0;
}


https://pixabay.com/photos/chameleon-head-green-lizard-6307349/

좋은 웹페이지 즐겨찾기