본문 바로가기

프로그래밍 강좌/C++ - OpenCV

[OpenCV][C++] cv::Mat 클래스 총정리(3) - 컬러(color) 원소 접근, 3채널 16bit 32bit 64bit short int float double

지난번에는 gray scale 영상에서 원소에 접근하는 방법에 대해 알아봤는데,

오늘은 color 영상 원소에 어떻게 접근하는지 알아볼께요.

3채널, 즉 컬러 영상한 화소를 구성하는 게 3개라는 뜻입니다. 다시 말하면, 빛의 3원소인 R, G, B가 하나의 색깔을 표현한다는 것이에요

opencv에서는 보통 B, G, R 순서로 저장을 해요 (R, G, B 순서로 저장할 수도 있어요)

at 사용

가장 직관적으로 픽셀에 접근하는 방법입니다.

하나의 위치에 b, g, r 3개의 원소가 있기 때문에 cv::Vec3b type을 사용하여 원소에 접근할 수 있어요.

아래 matx.hpp 를 참고하시면 다른 data type도 접근하실 수 있을꺼에요. (나중에 다뤄볼께요.)

아래와 같이 원소 값을 가져올 수 있고 필요하면 계산을 할 수도 있어요.

// 1. at 사용
	for (int y = 0; y < src_color.rows; ++y) {
		for (int x = 0; x < src_color.cols; ++x) {
			uchar b = src_color.at<cv::Vec3b>(y, x)[0];
			uchar g = src_color.at<cv::Vec3b>(y, x)[1];
			uchar r = src_color.at<cv::Vec3b>(y, x)[2];

		}
	}
 

여기에서, channel은 B, G, R 순서로 [0], [1], [2] 가 됩니다.

ptr 사용

ptr도 1채널과 마찬가지로 각 행의 첫번째 원소 위치를 기준으로 접근하시면 됩니다.

ptr을 얻은 후 [] 나 주소값 증가(++) 을 통해 원소에 접근할 수 있습니다.

// 2. ptr 사용
	for (int y = 0; y < src_color.rows; ++y) {
		uchar* pSrc = src_color.ptr<uchar>(y);
		for (int x = 0; x < src_color.cols; ++x) {
			uchar b = pSrc[x * 3 + 0];
			uchar g = pSrc[x * 3 + 1];
			uchar r = pSrc[x * 3 + 2];
		}
	}

	// 2.1 ptr ++ 사용
	for (int y = 0; y < src_color.rows; ++y) {
		uchar* pSrc = src_color.ptr<uchar>(y);
		uchar* pSrc_end = pSrc + src_color.cols * src_color.channels();
		for(;pSrc < pSrc_end;) {
			uchar b = *pSrc++;
			uchar g = *pSrc++;
			uchar r = *pSrc++;			
		}
	}
 

[] 연산자를 사용할 경우 아래와 같은 규칙이 있어서

pSrc[x*3+0] 대신에 pSrc[x*src_color.channels() + 0] 을 써도 됩니다.

지난 시간에 [] 연산자를 사용하게 될 경우 변수 및 계산이 추가되기 때문에 속도적인 측면에서 손해를 볼 수 있다고 했죠?

https://blog.naver.com/dorergiverny/223037431607

 

[OpenCV][C++] cv::Mat 클래스 총정리(3) - 원소 ( 픽셀 ) 접근 속도 분석 빠른 확인 matrix 테스트 parallel_fo

오늘은 인터넷 어디에도 없는 정보를 드릴까 합니다. 그러니 귀 쫑긋 눈 활짝 떠 주시고. 이번에는 여러가...

blog.naver.com

그래서 주소값을 직접 ++을 하면서 접근하면 속도 최적화가 가능합니다.

data 사용

data 사용도 1채널과 마찬가지로 행렬의 첫번째 원소의 주소값을 가져옵니다.

그리고 모든 원소는 1열로 나열 되어 있다고 생각하시고 접근하시면 됩니다

// 3. data 사용
	uchar* pSrc = src_color.data;
	for (int y = 0; y < src_color.rows; ++y) {
		for (int x = 0; x < src_color.cols; ++x) {
			uchar b = pSrc[y * src_color.cols * 3 + x * 3 + 0];
			uchar g = pSrc[y * src_color.cols * 3 + x * 3 + 1];
			uchar r = pSrc[y * src_color.cols * 3 + x * 3 + 2];			
		}
	}
 

1열의 데이터를 접근해야 하기 때문에 [] 안에 있는 변수 개수가 ptr 보다 많아 보입니다.

하지만 ptr은 각 행마다 ptr을 가져와서 1차원으로 보고 접근하기 때문이고, 그걸 길게 써 놓은 것 뿐입니다.

data도 ptr 과 마찬가지로 주소값 ++로 접근 가능하겠죠? 그리고 3이란 숫자 대신 src_color.channels()를 사용해도 됩니다.


우리는 그동안 cv::Mat 원소 접근에 대해 살펴보았어요.

우리가 다루는 대부분의 영상이 8bit 영상이지만 필터링을 하거나 산술 덧셈, 산술 뺄셈 등을 하다보면 8bit 를 넘어가는 숫자가 생기게 됩니다.

opencv 에서는 산술 덧셈, 산술 뺄셈을 하면 입력 영상과 같은 type으로 결과를 내 줍니다.

bit를 넘어가는 수, 예를 들어 200 + 150 = 350 이 되어야 하는데, 255로 표시가 되는 거죠.

아래와 같이 강제로 16bit 영상을 선언한 후 연산을 해도 다시 8bit로 바뀌네요.

계산 값들이 아래와 같이 음수 값도 갖고, 255를 넘어가는 값도 갖습니다.

만약 이 결과를 8bit 영상에 담아내면

아래와 같이 255 이상 값은 255로 0 이하 값은 0으로 되기 때문에 정보의 왜곡이 발생합니다.

이런 경우 16 bit 영상을 사용해야 하겠죠? 또한 열영상 같이 16 bit 영상을 사용하는 경우도 있죠.

8bit 영상은 픽셀 밝기값이 0~255 사이이지만 signed 16bit 영상은 픽셀 밝기 값이 -32768 ~ 32767 사이 값을 갖고,

unsigned 16bit 영상은 픽셀 밝기 값이 0 ~ 65535 사이의 값을 갖습니다. 나머지 32bit, 64bit도 위와 같이 계산해보면 됩니다. 실제로 16bit 영상을 읽어와서 ImageWatch로 픽셀 값을 살펴보면

큰 값을 갖고 있죠? 3채널도 마찬가지 입니다.

기본 접근 법만 확실하게 아시면 모든 방법으로 signed/unsigned 16bit, 32bit, 64bit에 접근할 수 있습니다.

 

자, 이제부터 unsigned 16bit(ushort)를 기준으로 원소 접근법을 설명해드리면, 나머지는 변수 type만 잘 설정 해 주시면 정상적으로 읽으실 수 있습니다.

at 사용

16bit gray 영상과 color 영상 값을 읽어오는 예제입니다.

// 1. at 사용 (16 bit)
	for (int y = 0; y < src16.rows; ++y) {
		for (int x = 0; x < src16.cols; ++x) {
			ushort val = src16.at<ushort>(y, x);			
		}
	}

	for (int y = 0; y < src16.rows; ++y) {
		for (int x = 0; x < src16.cols; ++x) {
			ushort b = src16_color.at<cv::Vec3w>(y, x)[0];
			ushort g = src16_color.at<cv::Vec3w>(y, x)[1];
			ushort r = src16_color.at<cv::Vec3w>(y, x)[2];			
		}
	}
 

여기에서 cv::Vec3w를 사용했죠?

matx.hpp를 보면 ushort, 3 원소(3채널) 짜리는 Vec3w를 쓰면 된다네요.

나머지 타입들도 아래 정의에 맞게 사용하시면 쉽게 원소 접근이 가능합니다.

ptr 사용

ptr도 16bit gray 영상과 16bit color 영상을 기준으로 예제를 보면 아래와 같습니다.

자세한 설명들은 이전 글을 참조해 주세요.

// 2. ptr 사용
	for (int y = 0; y < src16.rows; ++y) {
		ushort* pSrc = src16.ptr<ushort>(y);
		for (int x = 0; x < src16.cols; ++x) {
			ushort val = pSrc[x];			
		}
	}

	for (int y = 0; y < src16_color.rows; ++y) {
		ushort* pSrc = src16_color.ptr<ushort>(y);
		for (int x = 0; x < src16_color.cols; ++x) {
			ushort b = pSrc[x * 3 + 0];
			ushort g = pSrc[x * 3 + 1];
			ushort r = pSrc[x * 3 + 2];			
		}
	}

	// 2.1 ptr ++ 사용
	for (int y = 0; y < src16.rows; ++y) {
		ushort* pSrc = src16.ptr<ushort>(y);
		ushort* pSrc_end = pSrc + src16_color.cols;
		for (int x = 0; x < src16.cols; ++x) {
			ushort val = *pSrc++;			
		}
	}

	for (int y = 0; y < src16_color.rows; ++y) {
		ushort* pSrc = src16_color.ptr<ushort>(y);
		ushort* pSrc_end = pSrc + src16_color.cols * src16_color.channels();
		for(;pSrc < pSrc_end;) {
			ushort b = *pSrc++;
			ushort g = *pSrc++;
			ushort r = *pSrc++;					
		}
	}
 

ptr도 마찬가지로 ptr 얻어올 때 ushort*, short*, int*, float*, double*로 받아오시면 됩니다.

data 사용

data 사용도 마찬가지 입니다. 하지만 src.data 는 uchar*를 반환하기 때문에 이것은 타입 캐스팅을 해주시면 됩니다.

// 3. data 사용
	{
		ushort* pSrc = reinterpret_cast<ushort*>(src16.data);
		for (int y = 0; y < src16.rows; ++y) {
			for (int x = 0; x < src16.cols; ++x) {
				ushort val = pSrc[x];				
			}
		}
	}	

	{
		ushort* pSrc = reinterpret_cast<ushort*>(src16_color.data);
		for (int y = 0; y < src16_color.rows; ++y) {
			for (int x = 0; x < src16_color.cols; ++x) {
				ushort b = pSrc[y * src16_color.cols * 3 + x * 3 + 0];
				ushort g = pSrc[y * src16_color.cols * 3 + x * 3 + 1];
				ushort r = pSrc[y * src16_color.cols * 3 + x * 3 + 2];				
			}
		}
	}	
 

reinterpret_cast를 해 주지 않으면 값을 제대로 읽어올 수 없으니 주의하세요.