[OpenCV 7] 필터링과 히스토그램 분석

Sinaenjuni·2023년 7월 9일
0

OpenCV

목록 보기
7/25

흑백(Grayscale) 영상

영상 처리에서 흑백 영상이 많이 사용된다. 그 이유는 과거 알고리즘이 흑백 영상을 기준으로 개발 되었기 때문에다. 또한 흑백 영상을 사용하면 메모리 사용량을 줄이고 연산 속도를 크게 높일 수 있는데 이는 픽셀 당 발기 정보 하나만 가지고 있기 때문이다.

화소 처리(Point processing)

화소 처리는 한 픽셀을 변환하는 방법으로 dst=f(src)와 같이 함수를 정의해서 한 픽셀을 다른 픽셀로 변환하는 방법이다. 예를들어 입력값을 그대로 출력하는 항등 함수의 경우 원래 입력을 그대로 출력하기 때문에 원래 영상을 복사하는 형태와 같다. 그리고 반전, 밝기 혹은 명망비 조절, 이진화 등에 사용될 수 있다.

dst(x,y)=f(src(x,y))\text{dst}(x, y) = f(\text{src}(x, y))

위 함수의 경우 입력과 같은 값을 출력하는 함수로 원본 영상을 그대로 복사하는 기능을 수행한다.

위 함수의 경우 영상의 밝기가 증가되는 형태로 동작한다.

위 함수의 경우 0과 1로 구성된 이진화 영상을 만드는 역할을 수행한다.

영상 밝기(Brightness) 조절

흑백 영상에서의 픽셀 값은 밝기의 정도를 나타내기 때문에 단순하게 원래 픽셀값에서 정수를 더해주면 된다. 이때 흑백 영상의 표현 범위가 0에서 255까지 이므로 이를 넘어가는 영역을 처리해 주어야한다.

dst(x,y)=src(x,y)+ndst(x,y)=saturate(src(x,y))+n\text{dst}(x, y) = \text{src}(x, y)+n\\ \text{dst}(x, y) = \text{saturate}(\text{src}(x, y))+n

Mat 클래스의 연산자 오버로드를 이용한 방법으로 자동으로 saturate에 대한 처리를 수행해준다.

void ex_point_processing(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);

    imshow("src", src);
    imshow("dst", src - 50 );
    imshow("dst", src + 100 );

    waitKey();
    destroyAllWindows();
}

for문을 이용한 화소처리 방법이다. 포화에 대한 처리를 직접 해주어야 한다.

void ex_point_processing(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);
    Mat dst(src.rows, src.cols, CV_8UC1);

    for(int y=0; y<src.rows; y++){
        for(int x=0; x<src.cols; x++){
            // dst.at<uchar>(y, x) = src.at<uchar>(y, x) + 50;
            // 8bit를 넘어가는 경우 overflow에 의한 비트 잘림 현상으로 255를 넘어가면 (256)0부터 다시시작 된다.
            // 이러한 경우 포화에 대한 처리를 다음과 같이 수행한다.
            // Method1
            int v = src.at<uchar>(y, x) + 50;
            v = (v > 255) ? 255 : (v < 0) ? 0 : v;
            dst.at<uchar>(y, x) = v;
            // Method2
            // dst.at<uchar>(y, x) = saturate_cast<uchar>(src.at<uchar>(y, x) + 50);
        }
    }

    waitKey();
    destroyAllWindows();

}

영상 반전(Inverse)

픽셀이 갖는 최대값에서 각 픽셀값들을 빼주면 영상이 반전된다.

void ex_point_processing(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);

    imshow("inverse", 255-src);

    waitKey();
    destroyAllWindows();
}

영상 평균 변환

원본 영상을 평균 128의 밝기로 변환하는 방법이다.

void ex_point_processing(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);
    int m = (int)mean(src)[0];

    cout << m << endl;
    Mat dst = src + (128 - m);

    imshow("src", src);
    imshow("dst", dst);
    waitKey();
    destroyAllWindows();
}

명암비(Contrast) 조절

명암비는 밝은 곳과 어두운 곳의 밝기 정도의 차이를 의미한다.

{: note} 명암비를 조절하는 간단한 방법으로 각 픽셀에 일정 비율의 상수(s)를 곱하는 방법이다.

dst(x,y)=saturate(ssrc(x,y))\text{dst}(x, y) = \text{saturate}(s*\text{src}(x, y))

하지만 이 방법은 원점을 기준으로 증가하기 때문에 밝은 부분에 많은 포화 상태가 나타날 수 있다.

dst(x,y)=saturate(src(x,y)+(src(x,y)128)s)\text{dst}(x, y) = \text{saturate}(\text{src}(x, y)+(\text{src}(x, y)-128)*s)

위와 같이 밝기의 중심인 128을 기준으로 변할 수 있도록 만들면 효과적으로 명암비를 조절할 수 있다. 하지만 이 경우에도 영상의 평균에 따른 차이가 나타날 수 있다.

void ex_contrast(){
    Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

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

	float alpha = 1.0f;
	Mat dst = src + (src - 128) * alpha;

	imshow("src", src);
	imshow("dst", dst);

	waitKey();

}

영상의 평균이 128보다 작은 경우 어두운 부분이 모두 포화되어 구분하기 어려울 정도로 어두워지고 반대로, 평균이 높은 영상은 지나치게 밝아지는 문제가 나타난다.

dst(x,y)=saturate(src(x,y)+(src(x,y)m)s)\text{dst}(x, y) = \text{saturate}(\text{src}(x, y)+(\text{src}(x, y)-m)*s)

위 방법에서 사용한 상수(128) 대신 평균을 빼주면 영상의 평균을 고려한 명암비 조절이 가능하다.

void ex_contrast(){
    Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

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

	float alpha = 1.0f;
	int m = (int)mean(src)[0];
	Mat dst = src + (src - m) * alpha;

	imshow("src", src);
	imshow("dst", dst);

	waitKey();

}

히스토그램(Histogram) 분석

영상의 픽셀 값 분포를 그래프의 형태로 표현하여 분석하는 방법이다. 흑백 영상 기준 0부터 255까지의 밝기 정보에 대해서 픽셀의 개수를 새는 것이다.

또한 구한 히스토그램을 전체 픽셀의 개수로 나누어 정규화(Nomalize)하면 영상이 갖는 픽셀의 분포에 대하여 비율 혹은 확률로 표현할 수 있다.

Example code

// 직접 구현하는 방법
void ex_histogram(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);

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

	int hist[256] = {};
	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			hist[src.at<uchar>(y, x)]++;
		}
	}

	int size = (int)src.total();
	float normed_hist[256] = {};
	for (int i = 0; i < 256; i++) {
		normed_hist[i] = (float)hist[i] / size;
	}

	int histMax = 0;
	for (int i = 0; i < 256; i++) {
		if (hist[i] > histMax) histMax = hist[i];
	}

	Mat imgHist(100, 256, CV_8UC1, Scalar(255));
	for (int i = 0; i < 256; i++) {
		line(imgHist, Point(i, 100),
			Point(i, 100 - cvRound(hist[i] * 100 / histMax)), Scalar(0));
	}

	imshow("src", src);
	imshow("hist", imgHist);
	waitKey();
}
// calcHist() 함수를 이용하는 방법
Mat calcGrayHist(const Mat& img){
    Mat hist;
    int channels[] = {0};
    int dims = 1;
    const int hist_size[] = { 256 };
    float level[] = {0,256};
    const float* ranges[] = {level};

    calcHist(&img, 1, channels, Mat(), hist, dims, hist_size, ranges);
    return hist;
}

Mat getGrayHistImage(const Mat& hist)
{
	double histMax = 0.;
	minMaxLoc(hist, 0, &histMax);

	Mat imgHist(100, 256, CV_8UC1, Scalar(255));
	for (int i = 0; i < 256; i++) {
		line(imgHist, Point(i, 100),
			Point(i, 100 - cvRound(hist.at<float>(i, 0) * 100 / histMax)), Scalar(0));
	}

	return imgHist;
}

void ex_histogram(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return ;
	}
	Mat hist = calcGrayHist(src);
	Mat imgHist = getGrayHistImage(hist);
    
	imshow("src", src);
	imshow("hist", imgHist);
	waitKey();
}



히스토그램 스트레칭(Histogram stretching)

영상의 히스토그램이 전 구간에 걸쳐 나타나도록 변경하는 선형 변환 기법이다.

기울기=255GmaxGminy 절편=255GminGmaxGmindst(x,y)=255GmaxGminsrc(x,y)255GminGmaxGmin\text{기울기} = \frac{255}{G_{max} - G_{min}}\\ \text{y 절편} = \frac{255*G_{min}}{G_{max} - G_{min}}\\ \text{dst(x,y)} = \frac{255}{G_{max} - G_{min}} * \text{src(x,y)} - \frac{255*G_{min}}{G_{max} - G_{min}}
void ex_histogram_stretching(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);

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

	double gmin, gmax;
	minMaxLoc(src, &gmin, &gmax);

	Mat dst = (src - gmin) * 255 / (gmax - gmin);

	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));

	waitKey();
}


히스토그램 평활화(Histogram equalize)

히스토그램의 전체 구간에서 균일한 분포로 나타나도록 변환하는 명암비 향상 방법중 하나이다. 히스토그램 평활화를 위해서는 정규화된 히스토그램의 누적 분포 함수(CDF)를 이용한다.

dst=round(cdf(src(x,y))Lmax)\text{dst} = round(cdf(\text{src}(x,y))*L_{max})

변환 함수는 위와 같은 형태이다. LmaxL_{max}의 경우 픽셀의 최대값이 255이며, 원본 영상의 픽셀값을 cdf의 값으로 치환 후 반올림한 픽셀로 변환하는 형태이다.

void ex_histogram_equalization(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);
	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return ;
	}

	Mat dst(src.rows, src.cols, src.type());
	int hist[256] = {};
	for (int y = 0; y < src.rows; y++)
		for (int x = 0; x < src.cols; x++)
			hist[src.at<uchar>(y, x)]++;

	int size = (int)src.total();
	float cdf[256] = {};
	cdf[0] = float(hist[0]) / size;
	for (int i = 1; i < 256; i++)
		cdf[i] = cdf[i - 1] + float(hist[i]) / size;

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			dst.at<uchar>(y, x) = uchar(cdf[src.at<uchar>(y, x)] * 255);
		}
	}

	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));

	waitKey();
}
void ex_histogram_equalization(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE);
	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return ;
	}

    Mat dst;
	equalizeHist(src, dst);
    
	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));

	waitKey();
}

히스토그램 스트레칭과 평활화 비교

두 방법 모두 명암비를 개선해 준다. 평활화는 픽셀 크기의 분포가 균일하게 분포되는 반면, 스트레칭의 경우 픽셀 분포가 한쪽에 몰릴 수 있다.


히스토그램 스트레칭 개선

히스토그램 스트레칭을 수행하면 픽셀이 적은 영역에서 여전히 적은 수의 픽셀을 가지는 것을 볼 수 있다. 이러한 문제를 해결하기 위해 전체 픽셀의 n% 개의 픽셀을 제외한 상태에서 스트레칭을 수행하였다.


void histogram_stretching_mod(const Mat& src, Mat& dst){
    Mat hist = calcGrayHist(src);;
    
    int gmin=255, gmax=0;
    int ratio = int(src.cols * src.rows * 0.01);

    for(int i=0,s=0; i<255; i++){
        s+=(int)hist.at<float>(i);
        if(s>ratio){
            gmin=(int)i;
            break;
        }
    }

    for(int i=255,s=0; i>=0; i--){
        s+=(int)hist.at<float>(i);
        if(s>ratio){
            gmax=(int)i;
            break;
        }
    }
    dst = (src - gmin) * 255 / (gmax - gmin);

}

void main(){
    Mat src = imread("../data/lenna.bmp", IMREAD_GRAYSCALE), dst;
    
    histogram_stretching_mod(src, dst);

	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));


    waitKey();
    destroyAllWindows();
}

0개의 댓글