Home [데브코스] 6주차 - OpenCV Color Space
Post
Cancel
Preview Image

[데브코스] 6주차 - OpenCV Color Space


컬러 영상을 그레이 스케일로 변환

1
Y = 0.299R + 0.587G + 0.114B
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
int main()
{
	Mat src = imread("lenna.bmp");

#if 0
	Mat dst = Scalar(255, 255, 255) - src;
#else
	Mat dst(src.rows, src.cols, CV_8UC1);

	for (int y = 0; y < dst.rows; y++) {
		for (int x = 0; x < dst.cols; x++) {
			uchar b = src.at<Vec3b>(y, x)[0]; 
			uchar g = src.at<Vec3b>(y, x)[1]; 
			uchar r = src.at<Vec3b>(y, x)[2]; 

//			Vec3b v = src.at<Vec3b>(y, x); 실제로는 이 방법을 사용해야 시간이 덜 걸리나, 
//			uchar b = v[0];				visual studio에서 자체로 최적화를 해주어서 위의 식을 이 식처럼 변환해준다.
//			uchar g = v[1];
//			uchar r = v[2];

			dst.at<uchar>(y, x) = uchar(0.299 * b + 0.587 * g + 0.114 * r); 
			// 앞의 숫자들의 합이 1이 되어야 평균 밝기가 유지된다. 
            // 합이 절대 255가 넘지 않아서 포화 연산은 할 필요는 없다.
		}
	}
#endif
	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

src를 grayscale로 불러오는 것과 위의 식으로 진행하는 것과 같게 나온다.

또는 아래의 코드처럼 IMREAD_GRAYSCALE로 하는 것과 위의 코드로 하여 출력하는 것이나 같다.

1
2
Mat src = imread("lenna.bmp");
cvtColor(src, dst, COLOR_BGR2GRAY);


그레일 스케일로 변환하면 속도는 빨라지지만,

1
2
// dst.at<uchar>(y, x) = uchar(0.299 * b + 0.587 * g + 0.114 * r);
dst2.at<uchar>(y,x) = (r + g + b) / 3 // 이렇게 표현할수도 있다.

위의 식 대신 아래로 표현할 수 있지만, 이것이 정답이 아니라 단순하게 표현하기 위함이라 오류가 날 수도 있다.

그러나, 문제는 0.299 * b + 0.587 * g + 0.114 * r 이 식이, 매 픽셀마다 실수 연산을 넣어줘야 하기 때문에 느려질 수 있다. 그래서

1
dst.at<uchar>(y, x) = uchar((299 * b + 587 * g + 114 * r) / 1000);

실제로는 이렇게 사용하는데, 나눗셈이 사칙연산에서 제일 오래 걸린다. 따라서, 0.299,0.587,0.114에 2^14를 곱하여 연산하고 »(shift연산자)를 통해 빠르게 계산한다. 14라 하면 2^14로 나눠지게 된다.

1
2
3
#define RGB2GRAY(r,g,b) ((4899*r + 9617*g + 1868*b) >> 14);

    dst.at<uchar>(y,x) = (uchar)RGB2GRAY(r,g,b);

14인 이유는 2^16이 범위이므로 이를 절대 넘지 않는 가장 큰 숫자를 곱한다는 것이다.


🎈 가장 쉽고 빠른 방법은 cvtColor(src, dst, COLOR_BGR2GRAY)를 사용하는 것이다. 이 방법은 CPU 코어를 다 사용하는 것이고, for문을 하면 단일 코어를 사용하는 방법이다.



색 공간 변환(color space)

영상 처리에서는 특정한 목적을 위해 RGB 색 공간을 Gray, HSV, YCrCb, Lab 등의 다른 색 공간으로 변환하여 처리하는 것을 말한다. 변환하는 방식은 이 사이트를 참고 하면 된다.

영상 처리에서는 RGB보다는 HSV나 YCrCb를 많이 사용하고, 논문 같은 곳들을 보면 Lab도 사용한다. 디스플레이 용도로는 RGB를 사용한다.


  • 색 공간 변환 함수
1
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0);
  • src,dst : 입력, 출력 영상
  • code : 색 변환 코드, 참고
    • COLOR_BGR2GRAY / COLOR_GRAY2BGR : BGR <-> GRAY
    • COLOR_BGR2RGB / COLOR_RGB2BGR : BGR <-> RGB
    • COLOR_BGR2HSV / COLOR_HSV2BGR : BGR <-> HSV
    • COLOR_BGR2YCrCb / COLOR_YCrCb2BGR : BGR <-> YCrCb
  • dstCn : 결과 영상의 채널 수, 0이면 자동 결정된다.


bmp파일 - B.File.Header - B.I.H - Palette ( RGBQUAD ==> BGR 순서) - Pixel

jpg의 경우 압축을 풀 경우 IJPG이라는 라이브러리를 사용한다. 이는 RGB순서로 되어 있다.


RGB 색 공간

빛의 삼원색인 R,G,B를 혼합하여 색상을 표현한다. e.g. TV/모니터, 카메라 센서 Bayer 필터, 비트맵

RGB 색 공간에서는 두 가지의 색에 대한 좌표상의 거리를 따진다고 하면, 한 점을 기준으로 특정 반지름을 가진 구를 지나는 점들은 다 같다고 계산된다.


HSV 색 공간

  • Hue : 색상, 색의 종류
  • Saturation : 채도, 색의 탁하고 선명한 정도
  • Value : 명도, 빛의 밝기

HSV 값 범위

  • CV_8U 영상의 경우
    • 0 <= H <= 179
    • 0 <= S <= 255
    • 0 <= V <= 255

무지개 색을 표현하기에 적합한 방법이다.

hue의 경우 각도로 표현하는 방법을 사용하기 때문에 [0,360)도로 표현이 될 수 있다. 그러나 HSV는 uchar로 표현되는데, 이는 0~255 사이에 존재해야 한다. 그래서 이렇게 적용하기 위해 H/2로 사용하는 것이다. 그래서 빨간색을 보자면 만약 빨간색이 0 ~ 30도 또는 330 ~ 360도라고 하면 범위를 다음과 같이 표현할 수 있다.

` 0 <= H <= 15 or 165 <= H < 180` 가 될 것이다.

v의 경우 맨 아래가 0이고 ,S의 경우 중앙이 0이다. 그리고 빨간색이라 하면 S와 V도 어느정도 커야 빨간색이라는 것을 인지할 수 있다.


YCrCb(YCbCr, YUV)

HSV는 원색들을 비교하기 좋지만, 세상에는 원색이 아닌 것이 더 많기 때문에 YCrCb를 사용하기도 한다. YCrCb란 PAL,NTSC,SECAM 등의 컬러 비디오 표준에 사용되는 색 공간이다. 영상의 밝기 정보와 색상 정보를 따로 분리하여 부호화한다. Y가 그레이스케일을 표현하기 위한 것이다. 그렇기에 YCrCb는 흑백 TV 호환이 된다.

  • Y : 밝기 정보(iuma)
  • Cr, Cb : 색차(색상 성분)(Chroma)
    • Cr : red에 대한 색차
    • Cb : blue에 대한 색차

YCrCb 값 범위

CV_8U 영상의 경우

  • 0 <= Y,Cr,Cb <= 255

1번째 그림의 경우 CbCr 평면이다. 또한, Y = 128로 고정한 그림이다.

여기서는 거리 계산시에 y성분은 무시한 채로 (Cr,Cb) 에 대한 distance를 본다. 이것도 거리 계산에 애매한 부분이 있으나 RGB보다는 훨씬 좋다.


  • 채널 분리
1
2
void split(const Mat& src, Mat* mvbegin);
void split(InputArray src, OutpuqArrayofArrays mv);
  • src : 입력, 다채널 행렬
  • mv


  • 합치기
1
void merge()



BGR, HSV, YCrCb 각각의 채널을 분리해보고 살펴보았다.

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
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

void split_bgr();
void split_hsv();
void split_ycrcb();

int main()
{
    for (auto i=0; i<3;++i) {
        if (i ==0)
            split_bgr();
        else if (i == 1)
            split_hsv();
        else
            split_ycrcb();
    }
}

void split_bgr()
{
	Mat src = imread("candies.jpg", IMREAD_COLOR);

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

	vector<Mat> bgr_planes;

	split(src, bgr_planes);

	imshow("src", src);
	imshow("B", bgr_planes[0]);
	imshow("G", bgr_planes[1]);
	imshow("R", bgr_planes[2]);
	waitKey();
}

void split_hsv()
{
	Mat src = imread("candies.jpg", IMREAD_COLOR);

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

	cvtColor(src, src, COLOR_BGR2HSV);
	
	vector<Mat> hsv_planes;

	split(src, hsv_planes);

	imshow("src", src);
	imshow("H", hsv_planes[0]);
	imshow("S", hsv_planes[1]);
	imshow("v", hsv_planes[2]);
	waitKey();
	
}

void split_ycrcb()
{
	Mat src = imread("candies.jpg", IMREAD_COLOR);

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

	cvtColor(src, src, COLOR_BGR2HSV);

	vector<Mat> ycrcb_planes;

	split(src, ycrcb_planes);

	imshow("src", src);
	imshow("Y", ycrcb_planes[0]);
	imshow("cr", ycrcb_planes[1]);
	imshow("cb", ycrcb_planes[2]);
	waitKey();
}


첫번째로 BGR에 대한 채널 분리이다. RGB 의 경우 각각에 해당하는 색들이 높게 나온다.


두번째로 HSV에 대한 채널 분리이다. HSV 의 경우 H에서 빨간색은 0보다 조금 더 큰 것일 수도 있고, 360도에 가까운 값을 수도 있기에 크게 나올수도 있고, 작게 나올 수도 있다. 그리고는 빨주노초파남보로 갈수록 밝아진다. S나 V에서 되게 밝은 부분들이 있지만, H는 대체로 어둡다. 왜냐하면 H는 최대가 179이기 때문에 좀 어둡고, S,V는 최댓값이 255이기 때문이다. S는 사진이 거의 원색이므로 다 크게 나온다. V는 grayscale에 가까운 화면이다.


세번째로 YCrCb에 대한 채널 분리이다. YCrCb 의 경우 Y는 순수하게 grayscale이고, 나머지는 크게 중요하지 않다. 단지 Cr은 red에 대한 것이므로 red가 높게 나오고, Cb는 blue에 대한 색차이므로 blue가 높게 나올 것이다.


보통은 HSV, YCrCb로 변환을 해서 분리한다. V나 Y는 밝기 정보이다.



추가적으로 YCrCb에서 CrCb를 합친 것을 추출하는 코드를 실행하여 살펴보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void split_ycrcb_merge()
{
	Mat src = imread("candies.jpg", IMREAD_COLOR);

	cvtColor(src, src, COLOR_BGR2HSV);

	Mat cpsrc;
	src.copyTo(cpsrc);
	
	vector<Mat> ycrcb_planes;
	split(cpsrc, ycrcb_planes);

	Mat gray = ycrcb_planes[0].clone();
	ycrcb_planes[0] = 128;


	Mat dst;
	merge(ycrcb_planes, dst);

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


  • White balance(색 온도) 조절에 따른 색 차이

사진을 색 온도에 따라서 따듯해보이기도, 차가워보이기도 한다. 이것은 Cr과 Cb의 차이로 인한 것인데, 이 둘을 grayscale로 변환하면 동일하게 나온다. 카메라를 사용할 때 화질의 측면에서 3가지 알고리즘이 들어 있다. Auto W.B,Auto Focus,Auto Expo 각각 색온도 조절, 포커스 조절, 노출 조절이다. 이 때 색온도가 잘못되면 그 컬러 사진의 색상에 대해 모든 값이 잘못되어 있을 수 있다.

그래서 사진의 정보를 조금 수정하고 싶다면 YCrCb 중에서 Y값만 변경해준다.


컬러 영상을 YCrCb 각 채널을 분리하여 표현해보면 Cr과 Cb는 디테일이 표현되지 않는다. 그래서 Cr,Cb 영상의 크기를 가로1/2, 세로 1/2로 줄이고, DCT(discrete cosine transform)를 통해 압축하면 저주파 성분이 크게 나오고, 고주파는 작게 나오기 때문에 0으로 나온다. 그래서 0은 버리고, 값이 있는 것만 가지고 가기 때문에 jpg가 압축률이 좋은 것이다.



컬러 영상의 히스토그램 평활화

직관적 방법 : R,G,B 각 색 평면에 대해 히스토그램 평활화

R,G,B 각각을 히스토그램 평활화하여 합칠 수 있다. 그러나 이를 수행하면 이상하게 나온다.

1
2
3
equalizeHist(bgr[0], bgr[0]);
equalizeHist(bgr[1], bgr[1]);
equalizeHist(bgr[2], bgr[2]);

이유는 각각의 색상의 그래프는 다 비율이 다르기 때문이다.


따라서 밝기 성분에 대해서만 히스토그램 평활화를 수행해야 한다.

즉, YCrCb에서 Y만 명암비를 조절해줘야 한다.

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
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

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

	Mat src_ycrcb;
	cvtColor(src, src_ycrcb, COLOR_BGR2YCrCb);

	vector<Mat> planes;
	split(src_ycrcb, planes);
	
	equalizeHist(planes[0], planes[0]);

//	planes[0] += 50; // y값 증가

	Mat dst_ycrcb;
	merge(planes, dst_ycrcb);

	Mat dst;
	cvtColor(dst_ycrcb, dst, COLOR_YCrCb2BGR);

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



리매핑(remapping)

영상의 특정 위치 픽셀을 다른 위치에 재배치하는 일반적인 프로세스를 의미한다.

1
dst(x,y) = src(mapx(x,y),mapy(x,y))

어파인 변환, 투시 변환을 포함한 다양한 변환을 리매핑으로 표현이 가능하다.

  • 이동 변환

mapx(x,y) = x’ - 200

mapy(x,y) = y’ - 100

중요한 것은 우항의 x’와 y’는 출력 영상에서의 좌표이고, mapx가 반환하는 값은 입력 영상의 x, mapy가 반환하는 값은 입력 영상의 y이다.

쉽게 표기하면

x = x’ - 200

y = y’ - 100


  • 상하 대칭

mapx(x,y) = x

mapy(x,y) = h - 1 - v (h는 영상의 세로, v는 가로 크기)


  • 크기 변환

mapx(x,y) = x / 2

mapx(x,y) = y / 2



  • 리매핑 함수
1
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode = BORDER_CONSTANT, const Scalar& borderValue = Scalar());
  • src : 입력 영상
  • dst : 결과 영상, 이는 map1과 크기와 타입이 같아야 함
  • map1 : 결과 영상의 각 픽셀이 참조할 입력 영상의 (x,y) 좌표 또는 x좌표를 담고 있는 행렬
    • CV_16SC2, CV_32FC2, CV_32FC1
  • map2 : 결과 영상의 (x,y) 좌표가 참조할 입력 영상의 y좌표를 담고 있는 행렬, CV_16UC1, CV_32F1
  • interpolation : 보간법
    • INTER_LINEAR
    • INTER_CUBIC
  • borderMode : 가장자리 픽셀 확장 방식
  • borderValue : BORDER_CONSTANT일 때 사용할 상수 값



  • 코드 구현

호수 사진을 불러와 이 사진에 대한 기하학적 변환을 수행하는 코드

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
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("tekapo.bmp");

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

	int w = src.cols; // 가로
	int h = src.rows; // 세로 크기

	Mat map1 = Mat::zeros(h, w, CV_32FC1);
	Mat map2 = Mat::zeros(h, w, CV_32FC1); 

	for (int y = 0; y < h; y++) {
		for (int x = 0; x < w; x++) {
			
			map1.at<float>(y, x) = (float)x; // 가로는 그대로

			// 1. 상하 대칭
			//map2.at<float>(y, x) = (float)h - 1 - y; // 상하 대칭

			// 2. sin함수 모양으로 울퉁불퉁하게
			map2.at<float>(y,x) = (float)y + 10 * sin(x / 32.f);
		}
	}

	Mat dst;
	remap(src, dst, map1, map2, INTER_LINEAR);
	//remap(src, dst, map1, map2, INTER_LINEAR, BORDER_DEFAULT);

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


  1. 상하 대칭

  1. sin파형

여기서 빈 공간에 검정색부분을 바꿀 수 있다.

1
remap(src, dst, map1, map2, INTER_LINEAR, BORDER_DEFAULT);

default를 하면, 영상의 가장자리를 0이 아닌 픽셀값이 가장자리에서 대칭적으로 발생하다고 판단해서 자동으로 채워진다.


  1. 영상 확대

영상을 확대해서 보고 싶은 경우 map과 for문을 수정해야 한다.

1
2
3
4
5
6
7
8
9
10
	Mat map1 = Mat::zeros(h*2, w*2, CV_32FC1); // 입력 영상의 2배 크기로 생성
	Mat map2 = Mat::zeros(h*2, w*2, CV_32FC1); // 입력 영상의 2배 크기로 생성

	for (int y = 0; y < h*2; y++) {
		for (int x = 0; x < w*2; x++) {	// 이는 출력 영상의 좌표이므로 *2를 해야 한다.
			
			map1.at<float>(y, x) = (float)x/2; // 좌항이 입력, 우항이 출력 좌표이므로 확대하려면 출력을 /2해야 한다. 
			map2.at<float>(y, x) = (float)y/2;
		}
	}


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