[w 6 d 4]特定色領域抽出とエッジ検出


特定の色の領域を抽出


輝度の影響により、RGB空間は特定の色を探す際に不適切な動作を起こすことが多い.
HSV,YCRCB空間では,V(最大(R,G,B)とY(階調画像計算式)の値以外の情報を用いて色領域を抽出する.光照射変化に対して強い靭性を有する.(robustness)
HSVで処理する場合、S値は一定値より大きくなければ色の区別に影響しない;V値は輝度である.
原色に近い色を区別するのに便利です.
void cv::inRange 	( 	InputArray  	src,
		InputArray  	lowerb,
		InputArray  	upperb,
		OutputArray  	dst 
	) 		
lowerb、upper bにcv::Scalarを適用すると、すべてのピクセルに同じ基準が適用されます.
入力ビデオサイズと同じcv::Matオブジェクトをlowerb、upper bに指定すると、各ピクセルに異なる上限と下限を設定できます.OutputArrayはMask画像として表示されます.

コードを記述するために適切な中心値を設定すると、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として色を選択すると、影響結果の主な要因が中心値であると判断できます.
https://en.wikipedia.org/wiki/HSL_and_HSV

ヒストグラム逆投影


画像サンプルを持つ場合、類似領域を探すことで特定の中心値を指定することが困難である場合、ヒストグラムを用いて逆投影する方法を考慮することができる.一般に,画素値を抽出してGaussモデルを適用すると,特定の情報を一般化し,より良い結果を得ることができる.ここで、画素値抽出後の色検出はYCRCB方式を採用し、輝度成分Y値を無視してCRC、Cb平面上にヒストグラムを得る.最大値成分を255に標準化すると、0〜255の階調画像の形態が得られる.
Rect cv::selectROI 	( 	const String &  	windowName,
		InputArray  	img,
		bool  	showCrosshair = true,
		bool  	fromCenter = false 
	) 		
OpenCVは、cv::selectROI関数を使用して領域を選択できます.WindowNameを入力し、imgにビデオソースを追加します.実行後、マウスで領域を選択し、spaceまたはenterを押すと領域選択が完了し、cを押すと領域選択がキャンセルされ、ゼロ値が返されます.
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に対してヒストグラムbackプロジェクトを実行できます.
#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)の関数として扱う場合、一度の微分値が設定した閾値値より高い場合は、境界で検出することができる.
微分を求める前に,ノイズ除去に適したGaussホワイトノイズは,1回目の微分の近似は前方差分,後方差分,中心差分で行うことができる.

通常は以下のフィルタを用いて計算され、主に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が出力配列の深さ-1に設定されている場合、srcと同じです.
フィルタを適用する場合、一般にdx=1,dy=0からx方向成分を計算し、dx=0,dy=1でy方向成分を計算することができる.
void cv::magnitude 	( 	InputArray  	x,
		InputArray  	y,
		OutputArray  	magnitude 
	) 	
cv::震度は同じ大きさのx,yを入力アレイとして用い,各座標の大きさを計算し,震度の関数を入れる.
#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


ガウスろ過(オプション)
ノイズを除去するために,グラデーションを計算する際のフィルタにはGauss光の概念は含まれない.
グラデーション計算
主にSobellフィルタを用い,Gaussian概念を含め,ある程度ノイズを除去した.
グラデーションの大きさと方向を求め,方向を4領域に簡略化した.(左右、上、下の2本の対角線)
最大でないサポート
ローカル極大値のみをedgeと見なし、1つのedgeが複数の直線として表されることを防止します.
ランプ方向の比較のみを行い、非最大値を0にします.
ヒステリーエッジトラッキング
2つの閾値T high,T lowを用いる.
  • Tより高い場合は強い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イメージ、Output arrayを入力すると、2つのしきい値を指定するだけで使用できます.次はコードと結果です.
    #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/