Home [데브코스] 5주차 - OpenCV Image Brightness Control and histogram
Post
Cancel

[데브코스] 5주차 - OpenCV Image Brightness Control and histogram


옛날에는 컴퓨터 비전 (영상 처리) 알고리즘이 그레일스케일을 사용했다. 컬러정보가 꼭 필요하지 않을 경우에는 그레이스케일로 변환하기도 한다. 컬러는 그레이스케일의 3배의 용량을 차지하기 때문이다.

그래서 여기서는 입력 영상이 Truecolor인 경우 grayscale로 변환하여 사용할 것이다.

1
2
3
4
5
6
7
8
Mat img1 = imread("lenna.bmp", IMREAD_GRAYSCALE);

Mat img2(rows,cols,CV_8UC1);

Mat img3("lenna.bmp", IMREAD_COLOR);

Mat img4;
cvtColor(img3,img4,COLOR_BGR2GRAY);


화소 처리 (point porcessing)

화소 처리

  • 입력 영상의 특정 좌표 픽셀 값을 변경하여 출력 영상의 해당 좌표 픽셀 값으로 설정하는 연산
  • 결과 영상의 픽셀 값이 정해진 범위(그레이스케일 = 0~255)에 있어야 한다. => 예외 처리
  • 반전, 밝기 조절, 명암비 조절, 이진화 등

입력을 출력으로 변환하는 함수를 변환 함수(transfer function)이라 한다.

  • y = x를 가진 함수를 항등함수, 입력과 출력의 밝기는 같다.
  • y = -(x-a)^2 + b 를 가진 함수는 입력보다 출력이 조금더 밝아짐
  • 0 or k 인 값을 가진 함수를 이진화, 이진 함수라 한다.


밝기 조절(brightness control)

밝기 조절 : 영상 전체 밝기를 일괄적으로 밝게 만들거나 어둡게 만드는 연산

이 때 y = x + n 의 형태를 가질 것이다. 여기서 중요한 것은 픽셀이 가질 수 있는 값이 0~255이므로 이것을 처리해줘야 한다. 이것을 처리하는 것을 saturate연산 또는 limit연산 이라 한다.

1
2
Mat dst1 = src + 50;
Mat dst2 = src - 50;

이 + 연산을 직접 선언하고자 한다.

1
2
3
4
5
6
7
Mat dst(src.rows,src.cols.CV_8UC1); // at을 사용하기 때문에 선언해줘야 함

	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) + 100; // unsigned char은 8개 자리만 가져가게 된다.  
		}
	}

assertion failed (data) => ()안이 참이어야 하는데, 현재는 data가 비어있다

  • unsigned char : 8bit

1,2,3,4,…255,256

(0,0,0,0,0,0,0,0)(0,0,0,0,0,0,0,1)(0,0,0,0,0,0,1,0)(0,0,0,0,0,0,1,1)…(1,1,1,1,1,1,1,1), (1,0,0,0,0,0,0,0,0)

위의 코드에서 더한 결과가 256이라면 1을 뺀 00000000만 가져오게 되므로 0이 들어가고, 257이면 1이 들어가게 된다.


따라서 다음과 같이 포화연산을 추가해야 한다.

1
2
3
4
5
6
7
8
9
10
11
	Mat dst(src.rows,src.cols,CV_8UC1); // at을 사용하기 때문에 선언해줘야 함

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			int v = src.at<uchar>(y,x) + 100;
            // 포화 되면 limit를 걸어서 낮춘다.
            //v = (v > 255) ? 255 : ((v < 0) ? 0 : v); // if (v > 255) v = 255; if (v<0) v=0;
			//dst.at<uchar>(y, x) = (uchar)v;
            dst.at<uchar>(y, x) = (uchar)((v > 255) ? 255 : ((v < 0) ? 0 : v));
        }
	}
  • (-) 연산 (max 사용)
1
2
3
4
5
6
7
	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) = (uchar)max(0,src.at<uchar>(y,x) - 100);
		}
	}


그러나 opencv에서 포화 연산 템플릿 함수를 따로 정의해놓았다.

1
2
3
4
5
for (int y = 0; y < src.rows; y++) {
    for (int x = 0; x < src.cols; x++) {
        dst.at<uchar>(y, x) = saturate_cast<uchar>(src.at<uchar>(y,x) + 100);
    }
}

(int)v == int(v)

둘다 가능하다.

main 함수만은 마지막에 return을 하지 않아도 된다.


dst = src + 50 대신 add 라는 함수를 사용해도 된다.

1
2
// dst = src + 50;
add(src, 50, dst);

중요한 것은 add를 사용하면 더하면서 포화처리를 하지만, 그냥 +를 사용하면 다 더한 후 포화처리를 하게 된다. 그래서 다른 결과가 도출될 수도 있다.


시간 측정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	TickMeter tm;
	tm.start();

#if 1
	Mat dst;
	
	dst = src + 50;
#else
	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) = saturate_cast<uchar>(src.at<uchar>(y, x) + 100);
		}
	}
#endif

	tm.stop();

	cout << "Elapsed time: " << tm.getTimeMilli() << endl;

위의 함수를 사용하면 0.2ms~0.3ms , 아래 직접 구현했을 때는 0.5ms~0.6ms 정도 나온다.

차이가 나는 이유는 cpu칩은 1개지만, 논리적, 물리적 코어가 여러 개 들어있다.


평균 밝기 구하기 및 변경

그레이스케일의 중간값이 128로 맞추는 것이 좋다.

1
2
3
4
5
// 평균 구하기
int m = mean(src)[0];

// 3. 평균 밝기가 128이 되도록 밝기 보정하기
Mat dst = src + (128 - m);
  • 평균이 124 : src + (128 - 124) = src + 4
  • 평균이 211 : src + (128 - 211) = src - 83


반전(inverse)

영상 내의 모든 픽셀 값을 각각 그레이스케일 최댓값에서 뺀 값으로 설정한다. 즉 밝은 픽셀은 어둡게, 어두운 픽셀은 밝게 변경

컬러 영상에 대해 각각의 색상 성분에 대해 반전한다. 영상 처리에서는 대체로 관심있는 부분을 흰색(255), 배경을 검은색(0)으로 두는 것이 일반적이기에 이 때 사용한다.

y = 255 - x

1
2
Mat dst = 255 - src;
Mat dst = Scalar(255) - src;

트랙바를 이용해서 밝기 조절 정도를 지정하는 프로그램

밝기 조절 값 -100~100 범위로 지정할 수 있도록 하기 위해 트랙바 최댓값을 200으로 설정하고 트랙바는 초기값이 무조건 0이므로 -100~100으로 범위를 두기 위해 초기값은 100으로 둔다.

초기값을 100으로 두기 위해서 setTrackbarPos()를 사용해도 된다.

제출: 파일을 백업할 때 brightness 폴더에서 x64, .vs, .user를 지워라

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

void on_level_change(int pos, void* userdata);

int main(void)
{
	Mat img = imread("lenna.bmp", IMREAD_GRAYSCALE);
	Mat src;
	img.copyTo(src);
	namedWindow("image");
	int initvalue = 100;
	createTrackbar("level", "image", &initvalue, 200, on_level_change, (void*)&img);  //	setTrackbarPos("level", "image", 100);

	imshow("src", src);
	waitKey();

}

void on_level_change(int pos, void* userdata)
{
	Mat img = *(Mat*)userdata;
	Mat dst;
	dst = img + pos - 100; // img.convertTo(dst,-1,1,pos - 100);

	imshow("image", dst);
}



관심 영역의 평균 밝기를보정한 동영상 재생 프로그램

조명의 변화가 심한 base_camera_dark.avi 동영상을 재생하면서 차선 위치의 평균 밝기가 균일하게 유지되도록 밝기를 보정하여 재생해라

ROI 사각형 꼭짓점 좌표 : [240,280], [400,280],[620,440],[20,440]

frame은 트루컬러, gray는 그레이스케일, dst는 적절하게 밝기를 조절해준 화면이어야 함, 평균 밝기가 128 정도로 되도록

1
dst = gray + (128 - m);

이나, 저 사각형 안에서의 평균을 구해서 해야 할 것이다. 그에 대한 방법으로는

  1. mask를 생성하여 지정 사각형 크기만큼만 흰색으로 지정하여 mask를 생성
  2. 포토샵이나 그림판으로 grayscale로 생성해서 bmp파일로 mask를 만들어서 mean(gray,mask)를 해라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

Mat calcGrayHist(const Mat& img)
{
	CV_Assert(img.type() == CV_8UC1);

	Mat hist;
	int channels[] = { 0 };
	int dims = 1;
	const int histSize[] = { 256 };
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	calcHist(&img, 1, channels, noArray(), hist, dims, histSize, ranges);

	return hist;
}

Mat getGrayHistImage(const Mat& hist)
{
	CV_Assert(hist.type() == CV_32FC1);
	CV_Assert(hist.size() == Size(1, 256));

	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;
}

int main()
{
	VideoCapture cap("base_camera_dark.avi");

	if (!cap.isOpened()) {
		cerr << "Camera open failed!" << endl;
		return -1;
	}

	Mat frame;      // frame이란 한장의 정지 영상

	/* coordinate vector */
	vector<Point> pts;
	pts.push_back(Point(240, 280));
	pts.push_back(Point(400, 280));
	pts.push_back(Point(620, 440));
	pts.push_back(Point(20, 440));

	while (true) {
		cap >> frame;     // == cap.read(frame);
		if (frame.empty()) {
			cerr << "Frame empty!" << endl; // 동영상 맨 마지막으로 가도 이것이 실행되고 종료 될 것이다.
			break;
		}

		/* grayscale */
		Mat gray;
		cvtColor(frame, gray, COLOR_BGR2GRAY);

		/* mk mask file */
		Mat mask = Mat::zeros(frame.rows, frame.cols, CV_8UC1);
		fillPoly(mask, pts, Scalar(255, 255, 255),LINE_AA);

		/* be mask image */
		Mat src = Mat::zeros(frame.rows, frame.cols, CV_8UC1);
		copyTo(frame, src, mask);

		/* get mean value */
		int m = mean(src,mask)[0];
		cout << "mean: " << m << endl;
		
		/* normalize brightness */
		Mat dst;
		gray.convertTo(dst, -1, 1.5, 128 - m); //	Mat dst = gray + (128 - m);
		
		/* drawing */
		polylines(frame, pts, true, Scalar(0, 0, 255), 2);

		Mat hist = calcGrayHist(gray);
		Mat imgHist = getGrayHistImage(hist);
		imshow("histogram", imgHist);

		imshow("gray", gray);
		imshow("dst", dst);
//		imshow("src", src);
//		imshow("mask", mask);
		imshow("frame", frame);


		if (waitKey(1) == 27) // ESC
			break;
		
	}

	cap.release();       // 비디오 종료
	destroyAllWindows(); // 생성했던 창을 닫아줌
}



영상의 명암비 조절

명암비(constrast) : 밝은 곳과 어두운 곳 사이에 드러나는 밝기 정도의 차이, 대비

평균 밝기를 고려한 명암비를 조절하고자 한다. 명암비를 높일 때는 평균 밝기보다 낮으면 더 낮추고, 평균 밝기보다 높으면 더 높이면 된다.

1
dst(x, y) = saturate(s*src(x,y))

s는 int일 것이고, s = 0.5라면 출력 크기가 0~255 에서 0~128로 줄어든다. 그러나 s = 2라면 출력 크기가 0~255이지만, 128~255의 입력들은 다 포화가 되므로 255로 통일될 것이다.

1
2
dst = src * 2;
dst = src / 2;


그러므로 조금 더 효과적인 방법을 사용해야 한다.

y = x + (x-128) * α 의 그래프를 사용하면 α가 1이면 y = 2x -128

1
2
float alpha = 1.f; // 0.5f,0.8f // 1.0으로 하면 double타입으로 한다.
Mat dst = saturate_cast(src+(src-128)*alpha);

그러나 사진 자체가 밝으면 포화가 너무 많아진다. 따라서 128 대신 평균을 구해서 (m,m)을 지나는 직선으로 변환하면 된다.

y = x + (x-m) * α

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int m = mean(src)[0];
cout << "mean: " << m << endl;

float alpha = 1.f;
Mat dst1 = src + (src - m) * alpha;
Mat dst2 = src + (src - 128) * alpha;

tm.stop();

cout << "Elapsed time: " << tm.getTimeMilli() << endl;

imshow("src", src);
imshow("dst1", dst1);
imshow("dst2", dst2);



히스토그램

히스토그램(histogram) : 영상의 픽셀 값 분포를 그래프의 형태로 표현한 것이다. 예를 들어 그레이스케일 영상에서 각 그레이스케일 값에 해당하는 픽셀의 개수를 구하고, 이를막대 그래프의 형태로 표현한다.

정규화된 히스토그램(normalized histogram) : 히스토그램으로 구한 각 픽셀의 개수를 영상 전체 픽셀 개수로 나누어준 것이다. 해당 스레이스케일 값을 갖는 픽셀이 나타날 확률이다.

그냥 히스토그램은 정수로 표현이 가능하고, 정규화되면 0~1사이 값으로 표현되므로 실수가 된다. 이 정규화를 하게되면 확률로 바뀌게 되어 전체를 다 더하면 1이 된다.

1
2
3
int hist[256] = { 0, }; // int hist[256]; 으로 선언해도 된다. 
// 이렇게 하면 전역변수는 0으로 초기화되지만, 지역변수는 값이 다 0이 아닌 가비지 변수로 된다. 그래서 int hist[256] = {}; 라고해야 초기화 된다.
// 0, 을 넣어도 되지만, 굳이 안넣어도 된다. 

이를 반드시 256으로 지정해야할 필요는 없다. 픽셀 값이 0~1, 2~3, 4~5 로 해서 128개로 할 수도 있다.

1
2
3
4
5
6
7
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)]++;
	}
}

이 코드는 히스토그램을 구하는 for문이다. 여기서 중요한 것은 int hist를 사용할 때, 자신이 사용하는 화소의 크기를 알아야 한다. 이 화소가 전체가 int의 크기를 넘어가면 에러가 날 것이다. int가 가지는 범위는 2^31이다. 넘어가면 long long을 사용하면 된다.


1
2
3
4
5
6
// 히스토그램 직접 생성 //
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));
}

이는 히스토그램을 직접 생성하는 것이지만 함수가 따로 있다.

1
void 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);
  • images : 입력 영상 배열, 입력 영상의 주소, 영상의 배열인 경우 모두 깊이와 크기가 같아야함
  • nimages : 영상의 개수


내부에서 calcHist()함수를 사용하여 반환된 히스토그램 hist는 256x1 크기의 CV_32FC1 타입의 행렬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Mat calcGrayHist(const Mat& img)
{
	CV_Assert(img.type() == CV_8U);

	Mat hist;
	int channels[] = { 0 };
	int dims = 1;
	const int histSize[] = { 256 };			// 이를 128로 하면 0~1,1~2,, 64로 하면 0~3,4~7,, 로 구할 수 있다
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	calcHist(&img, 1, channels, noArray(), hist, dims, histSize, ranges); // hist: 256x1, CV_32FC1 ,, float로 저장된다.

	return hist;
}

이를 활용해서 그림을 그리는 함수는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Mat getGrayHistImage(const Mat& hist)
{
	CV_Assert(hist.type() == CV_32F);
	CV_Assert(hist.size() == Size(1, 256));

	double histMax = 0.;
	minMaxLoc(hist, 0, &histMax); // 최대값만 구함 , 100x256 일 때 최댓값이 100pixel이 되도록 하려고 구함

	Mat imgHist(100, 256, CV_8U, 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)); // cvRound()안이 100이 되도록하려고 작성 한 것이다.
	}

	return imgHist;
}


영상과 histogram의 관계

이를 봤을 때 위에가 원본인데, 밝게하면 전체적으로 오른쪽으로 이동하고, 어둡게 하면 전체적으로 왼쪽으로 이동한다.

명암비를 높게 하면 중간중간 빈칸이 생긴다. 그리고 양 끝이 없었는데, 생기게 된다. 명암비가 낮게 되면 중간중간 솓아오르는게 생기고, 양 끝의 빈공간이 늘어난다.



히스토그램 스트레칭

영상의 히스토그램이 그레이스케일 전 구간에서 걸쳐 나타나도록 변경하는 선형 변환 기법이다. 특정 구간에 집중되어 나타난 히스토그램을 마치 고무줄 늘이듯이 그레이스케일 범위 전구간에서 히스토그램이 골고루 나타나도록 변환한다. 예를 들어 한 이미지가 픽셀값이 40~220 사이에만 분포한다면 이를 늘려서 0~255로 분포하도록 만든다. 늘리면 중간중간 비어 있는 분포가 만들어진다.

가장 낮은 픽셀값을 Gmin, 가장 높은픽셀값을 Gmax라 할 때, 이 둘을 지나는 직선의 방정식을 구하고자 한다.

  • 히스토그램 스트레칭 변환 함수의 기울기 : 255/(Gmax - Gmin)
  • y절편 : -(255xGmin) / (Gmax - Gmin)

g(x,y) = 255(Gmax - Gmin) x f(x,y) x 255/(Gmin - Gmax)

dst(x,y) = 255(Gmax - Gmin) / (Gmin - Gmax)

1
2
3
4
5
6
7
8
9
/*
double gmin, gmax;
minMaxLoc(src, &gmin, &gmax);

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

Mat dst;
normalize(src, dst, 0, 255, NORM_MINMAX);

두 코드는 같은 형태로 동작한다. 위의 주석 안의 방법이 좀 더 이론적인 방법이다.


1
void normalize(src, dst, alpha=None, beta=None, norm_type=None, dtype=None, mask=None)

• src: 입력 영상 • dst: 결과 영상 • alpha: (노름 정규화인 경우) 목표 노름 값, (원소 값 범위 정규화인 경우) 최솟값 • beta: (원소 값 범위 정규화인 경우) 최댓값 • norm_type: 정규화 타입. NORM_INF, NORM_L1, NORM_L2, NORM_MINMAX, 히스토그램 스트레칭은 NORM_MINMAX • dtype: 결과 영상의 타입 • mask: 마스크 영상 https://deep-learning-study.tistory.com/121

lenna.bmp의 경우 25픽셀부터 나오기 시작하여 245픽셀까지 존재한다. 따라서 히스토그램으로는 양쪽의 빈공간이 크기가 같아보이지만, 실제로는 다르다. 1~2개인 픽셀이 있다면 이를 무시해야 변화가 보일 것이다. 즉 상위/하위 1%를 무시하도록 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "opencv2/opencv.hpp"
#include <iostream>

using namespace cv;
using namespace std;

void histogram_stretching(const Mat& src, Mat& dst);
void histogram_stretching_mod(const Mat& src, Mat& dst);

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

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

	Mat dst1, dst2;
	histogram_stretching(src, dst1);
	histogram_stretching_mod(src, dst2);

	imshow("src", src);
	imshow("dst1", dst1);
	imshow("dst2", dst2);
	waitKey();
}

// 기존의 스트레칭 방식
void histogram_stretching(const Mat& src, Mat& dst)
{
	double gmin, gmax;
	minMaxLoc(src, &gmin, &gmax);

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

// 개선한 스트레칭 방식
void histogram_stretching_mod(const Mat& src, Mat& dst)
{
	int hist[256] = {0,};

	// src 영상 전체를 스캔하면서 히스토그램을 hist에 저장하세요.
	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			hist[src.at<uchar>(y, x)]++;
		}
	}
	

	int gmin, gmax;
	int ratio = int(src.cols * src.rows * 0.01);  // 전체 픽셀 개수의 1%

	for (int i = 0, s = 0; i < 255; i++) {
		s += hist[i];

		// 히스토그램 누적 합 s가 ratio보다 커지면,
		// 해당 인덱스를 gmin에 저장하고 반복문을 빠져나옵니다.
		if (s > ratio) {
			gmin = i;
			break;
		}
	}

	for (int i = 255, s = 0; i >= 0; i--) {
		s += hist[i];

		// 히스토그램 누적 합 s가 ratio보다 커지면,
		// 해당 인덱스를 gmax에 저장하고 반복문을 빠져나옵니다.
		if (s > ratio) {
			gmax = i;
			break;
		}
	}

	// gmin과 gmax 값을 이용하여 히스토그램 스트레칭을 수행하고,
	// 그 결과를 dst에 저장합니다.

	cout << gmin << ", " << gmax << endl;

#if 0
	normalize(src, dst, gmin, gmax, NORM_MINMAX);
#else
	dst = (src - gmin) * 255 / (gmax - gmin);
#endif
}



이 변환함수가 직선으로 사용했지만, 꼭 직선이 아니라 곡선이 될 수도 있다.

히스토그램 평활화 (histogram equalization)

히스토그램이 그레이스케일 전체 구간에서 균일분포로 나타나도록 변경하는 명암비 향상 기법이다. 변환함수는 곡선이 된다.

  • 히스토그램 균등화, 균일화, 평탄화

변환 함수 구하기

  • 히스토그램 함수 구하기 : h(g) = Ng
  • 정규화된 히스토그램 함수 구하기 : p(g) = h(g) / (w x h)
  • 누적 분포 함수(cdf) 구하기 : cdf(g) = 시그마(0<=i<=g)p(i)
  • 변환 함수 : dst(x,y) = round(cdf(src(x,y))X Lmax)


앞에서부터 현재위치까지 각 픽셀에 대한 p, 확률을 다 더한 것이다. cdf의 맨 마지막은 항상 1이다.

         
bin01234567
h(g)43210231
p(g)4/163/162/161/1602/163/161/16
cdf(g)4/167/169/1610/1610/1612/1615/161
cdf(g) x L28/1649/1670/1670/1684/16105/167 


  • 평등화 히스토그램
1
void equalizeHist( InputArray src, OutputArray dst );
  • src : 단일 이미지 입력 영상
  • dst : 출력 영상


스트레칭은 그냥 쭉 펴서 되어 있고, 평탄화는 갯수가 많이 있는 부분은 사이의 간격을 넓게, 갯수가 별로 없는 부분은 간격을 좁히는 형식이다. 그래서 전체를 4등분을 했을 때, 갯수가 동일하게 나누어진다.


영상의 산술 연산

덧셈 연산

두 영상의 같은 위치에 존재하는 픽셀 값을 더하여 결과 영상의 픽셀 값으로 설정한다. 덧셈 결과가 255보다 크면 픽셀값을 255로 설정한다.

dst(x,y) = saturate(src1(x,y) + src2(x,y))

이렇게 하면 포화가 너무 많아져서 좋지않다.


가중치 합

두 영상의 같은 위치에 존재하는 픽셀 값에 대하여 가중합을 계산하여 결과 영상의 픽셀밧으로 설정한다. 보통 a + b = 1이 되도록 설정한다. 두 입력 영상의 평균 밝기를 유지하기 위해서이다.

dst(x,y) = saturate(a * src1(x,y) + b * src2(x,y))


평균 연산

가중치를 a=b=0.5로 설정한 가중치 합

dst(x,y) = saturate(2/1(src1(x,y) + src2(x,y)))


평균 연산의 응용

  • 잡음 제거

어둡게 여러 장 찍어서 합성을 한다.

뺄셈 연산

두 영상의 같은 위치에 존재하는 픽셀 값에 대해서 뺄셈 연산을 수행하여 결과 영상의 픽셀값으로 설정한다. 뺄셈 결과가 0보다 작으면 픽셀값을 0으로 설정

dst(x,y) = saturate(sr1(x,y) - src2(x,y))


차이 연산

두 입력 연산에 대해 뺄셈을 하는데, 절댓값을 이용하여 결과 영상을 생성하는 연산이다. 뺄셈과 달리 입력 영상의 순서에 영향을 받지 않는다.

dst(x,y) = |sr1(x,y) - src2(x,y)|



1
2
void add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1);
void subtract(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1);
  • src1 : 첫번째 입력 행렬 또는 스칼라
  • src2 : 두번째 입력 행렬 또는 스칼라
  • dst : 출력 행렬, dst의 깊이는 두 개가 같거나

덧셈 연산의 경우 두 개의 타입이 같아야 한다. 그러나 add는 출력 영상의 타입을 고를 수 있다.


  • 가중치 추가하여 출력
1
void addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype = -1);
  • src1 : 첫번째 입력 영상
  • alpha : 첫번째 배열에 가할 가중치
  • src2 : 두번째 입력 영상
  • beta : 두번째 배열에 가할 가중치
  • gamma : 각 합에 더해질 스칼라
  • dst : 출력 영상
  • dtype : optional, 두 입력 영상이 동일한 깊이, 타입을 가졌을 때의 출력 영상의 깊이


  • 1번과 2번의 차이를 출력
1
void absdiff(InputArray src1, InputArray src2, OutputArray dst);
  • src1 : 첫번째 입력 영상
  • src2 : 두번째 입력 영상
  • dst : 출력 영상


행렬의논리 연산

1
2
3
4
void bitwise_and(InputArray, InputArray, OutputArray)
void bitwise_or(InputArray, InputArray, OutputArray)
void bitwise_xor(InputArray, InputArray, OutputArray)
void bitwise_not(InputArray, InputArray)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	VideoCapture.cap("../data/base_camera_dark.avi");

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return -1;
	}
	
//	int height = CAP_PROP_FRAME_HEIGHT;
//	int width = CAP_PROP_FRAME_WIDTH;

	Mat mask(480, 640, CV_8UC1, Scalar(0));

	vector<Point> pts(4);
	pts.push_back(Point(240, 200));
	pts.push_back(Point(400, 280));
	pts.push_back(Point(620, 440));
	pts.push_back(Point(20, 440));

	fillPoly(mask, pts, Scalar(255));

	Mat frame, dst, gray;
	while (true) {
		cap >> frame;

		cvtColor(frame, dst, COLOR_BGR2GRAY);

		bitwise_and(gray, mask, dst);

		imshow("frame", frame);
		imshow("mask", mask);
		imshow("img", dst);
		waitKey();
	}
}



필터링

필터링 : 영상에서 필요한 정보만 통과시키고 원치 않는 정보는 걸러내는 작업

비네팅이라 하여 가운데 중앙부에 대한 빛과 주변부에 대한 빛에 대해 작업하는 것이 있다.


주파수 공간에서의 필터링

퓨리에 변환(fourier transform)을 이용하여 영상을 주파수 공간으로 변환하여 필터링하는 방법을 말한다.

주변부를 고주파(high frequency)라 하고 중앙부를 저주파(low frequency)라 한다. 변화량이 큰 것을 고주파, 변화가 적은 것을 저주파라 한다. 웅앙에 저주파를 제거하고 다시 영상으로 만들면 영상이 조금 더 날카로워진다.

band pass filter 이라는 것도 있다.


공간적 필터링

영상의 픽셀 값을 직접 이용하는 필터링 방법이 있다. 주로 마스크 연산을 이용한다. 3x3,5x5,7x7 정도의 작은 필터링은 공간적 필터링을 많이 사용하고 좀 더 큰 것들은 주파수 필터링을 사용한다.


  • 다양한 모양과 크기의 마스크

필터링에 사용되는 마스크는 다양한 크기, 모양을 지정할 수 있지만 대부분 3x3 정방형 필터 사용한다. 마스크는 필터, 커널, window 등으로 불리기도 한다.

필터의 중간점을 anchor이라 한다.

마스크의 형태와 값에 따라 필터의 역할이 결정된다.

  • 영상 부드럽게
  • 영상 날카롭게
  • 엣지 검출
  • 잡음 제거


convolution을 하는 것과 같이 마스크 3x3 과 입력 영상 3x3을 연산해서 출력 영상의 1칸으로 출력된다. 이를 correlation이라 한다. correlation와 convolution은 비슷한 의미지만, 신호 처리의 관점으로 보면 좀 다르다. 그러나 통용적으로 convolution을 많이 사용하기도 하지만, 엄밀히 말하면 correlation이 더 적절하다.

좌측 상단의 위치부터 마스크를 이동시키면서 연산을 한다. 0x0과 같이 테두리 부분들은 0x0의 zero padding을 적용해서 필터링하면 외곽과 중앙의 연산을 동일시할 수 있다. 그러나 openCV에서는

   
abc
def
ghi

가 있다고 하면

     
bcabc
efdef
highi

와 같이 대칭으로 만들어 줄 수 있다.

이 때 추가하는 것에는 대칭으로 b,c로 넣어줄수도 있고, a를 반복으로 넣어줄 수 있고, c,b처럼 넣어줄 수도 있다. 중요한 것은 바깥쪽에 중요한 객체가 있더라도 외곽을 신경쓰는 일은 불필요하다. 중앙에 있는 것들에 대해 판별을 해야 하는데 외곽을 신경쓰기보다 zero padding을 적용해놓는 것이 좋다.


주변 패딩 생성

1
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value = Scalar() );
  • src : 입력 영상
  • dst : 출력 영상
  • top/bottom/left/right : 위,아래,왼,오른쪽 추가 픽셀
  • bordertype : 패딩의 타입, 0, aaabc , bcabc , cbabc
  • value : bordertype == BORDER_CONSTANT일 때의 border value


필터 생성

1
void filter2D(InputArray src, OutputArray dst, int ddepth, InputArray, Point anchor = Point(-1,-1), double delta = 0, int bordertype = BORDER_DEFAULT);
  • src : 입력 영상
  • dst : 출력 영상
  • ddepth : 원하는 결과 영상의 깊이를 지정, -1이면 src와 같은 깊이를 사용
  • kernel : 필터 마스크 행렬, 1채널 실수형
  • anchor : 고정점 위치 , (-1,-1)이면 필터 중앙을 고정점으로 사용
  • delta : 추가적으로 더할 값
  • bordertype : 가장자리 픽셀값 설정 방법

kernel까지만 설정하고, 나머지는 디폴트값쓰면 된다.


현재로는 그레이스케일만 하는데, 트루컬러로 할 수도 있고, 트루 컬러중에서도 특정 색깔에 대해서만 필터를 걸 수도 있다.

엠보싱(embossing)

직물이나 종이, 금속판 등에 올록볼록한 형태로 만든 객체의 윤과 또는 무늬

  • 엠보싱 필터

엠보싱 필터는 입력 영상을 엠보싱 느낌이 나도록 변환하는 필터이다.

   
-1-10
-101
011

왼쪽위는 어둡게, 오른쪽 아래는 밝게 하는 마스크

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

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

	if (src.empty()) {
		cerr << "Image laod failed!" << endl;
		return -1;
	}
	float data[] = { -1, -1 ,0 ,
					-1, 0, 1,
					0, 1, 1
	};

	Mat kernel(3, 3, CV_32FC1, data);

	/* Mat kernel = (Mat_<float>(3,3,CV_32FC1, ...) */

	Mat dst;
	filter2D(src, dst, -1, kernel, Point(-1, -1), 128);


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

This post is licensed under CC BY 4.0 by the author.