티스토리 뷰

영상처리/OpenCV

04.Scanning with pointer

빠리빵 2019. 4. 10. 22:15

openCV에서 포인터로 픽셀에 접근하는 법을 배워본다. 포인터로 접근하는 것은 영상의 사이즈가 커지거나, 빠른 처리 속도를 요구하는 환경에서 도움이 될 수 있을 것이다.

 

예제는 포인터를 이용하여 픽셀에 접근한 후, 영상의 color reduction을 진행한다.  color reduction 알고리즘 및 영상의 데이터를 효율적으로 접근하는 방법들을 중점으로 알아본다.

 

우선 8bit(256) 기준으로, R.G.B 3 색 영역에서 나올 수 있는 색의 수는 256 * 256 * 256으로 16,777,216 가지이다. 분석의 복잡도를 줄이기 위하여 때때로 이 색 영역을 줄여야 하는 상황이 있을 수 있다. 가장 쉬운 방법으로는 RGB space를 동일한 사이즈로 줄이는 것이다. 예로 8로 각 dimension을 나누게 되면, 32 * 32 * 32로 32,768 가지이다. 

 

그러므로, 기본적인 color reduction 알고리즘은 간단하다. 만약 N이 reduction factor라면, 각 픽셀의 값을 N으로 나눈 후(integer division이라 가정하며, 나머지는 버려진다.) N으로 다시 곱해준다. 이 결과로 픽셀은 처음 값보다 더 낮은 값을 갖게 된다. N/2를 더해서 두 인접한 N의 배수의 중앙값을 얻는다. (예로 N이 32라면, 31 이하의 값은 몫이 0이기 때문에 0 대신 16을 더해주어 16 값을 갖게 한다. domain은 줄이되, 영상 자체의 이상한 것은 줄이기 위함이라 생각된다.) 위의 과정을 모든 픽셀에 거치게 되면 (256 / N) * (256 / N) * (256 / N) color values가 가능하다.

 

예제 코드는 다음과 같다.

#include <opencv2/highgui.hpp>
#include <opencv2/core.hpp>
#include <iostream>

using namespace std;
using namespace cv;

void colorReduce(Mat image, int div = 64) {
	
	int nl = image.rows;	// number of iines
	// total number of elements per line
	int nc = image.cols * image.channels();

	for (int j = 0; j < nl; j++) {
		// get the address of row j
		uchar* data = image.ptr<uchar>(j);

		for (int i = 0; i < nc; i++) {
			// process each pixel
			data[i] = data[i] / div * div + div / 2;
			// end of pixel processing
		}
	}
}

int main() {
	// read the image
	Mat image = imread("castle.jpg");
	colorReduce(image, 16);
	namedWindow("Image");
	imshow("Image", image);
	waitKey(0);
	imwrite("colorReduce_16.jpg", image);
}

차례대로 original, div = 16, div = 64

결과는 위와 같다. 픽셀 하나를 자세히 살펴본다. original 값 (153, 203, 228)에서 div = 64 값 (160, 224, 224)로 변환되었다. 위의 식을 그대로 따라간다면 orignal의 각 채널을 64로 나눈 몫은 2, 3, 3이다. 다시 64를 곱하게 되면 128, 192, 192가 나온다. 여기에 64/2인 32를 더하게 되면 최종 결과 값 (160, 224, 224)가 나온다.

 

결국 8bit 기준으로 div가 64인 경우에, 몫은 0 ~ 3가 나올 수 있으며, 이것은 한 채널 당 4가지의 색상밖에 표현할 수 없다는 것을 의미한다. 따라서 3개의 채널(R, G, B)이라면 4 * 4 * 4 = 64이며, 가장 우측의 영상은 64개 색으로만 만들어진 영상이다. (앞부분에서 나왔단 (256/64) * (256/64) * (256/64) = 64와 동일하다.) 

 

div가 다른 경우일지라도 위와 같이 계산을 하면 되며, 실제 눈으로 영상을 비교하게 되면 우측으로 갈수록 점점 부자연스러워지는 것을 볼 수 있다. 이것은 몫이 변화되는 시점, 예로 div = 64인 경우에 191, 192 경계에서 pixel value 1 차이로 몫이 3과 4로 나뉘기 때문에 위의 알고리즘을 거쳐서는  160, 224가 나오게 된다. 따라서 영상의 특정 영역을 기준으로 색이 갑자기 변하는 현상이 생기는 것을 확인할 수 있다.

 

코드에서 pointer로 픽셀을 접근하는 것과 관련하여, openCV에서는 image data buffer의 처음 3 bytes는 좌측 상단의 첫 픽셀의 3개 채널의 데이터이다.(물론 영상의 type에 따라 달라지겠지만, 한 채널을 8bit로 가정한다.) 다음 데이터는 두 번째 다음 픽셀에 해당하며, 이 규칙이 반복된다.(openCV는 default로 BGR channel 순서이다.) 이미지의 너비가 W, 높이가 H라면 영상은 W*H*3 uchars가 필요할 것이다. 그러나, efficiency 측면에서 row가 몇 개의 더미 픽셀로 padding 될 수 있다. 이것은 image processing이 때때로 row가 8의 배수일 경우 더 효율적이기 때문이다. (메모리 측면에서 더 빠르게 접근하기 위함) 물론 이 픽셀은 보이거나 저장되지 않는다. 

 

Mat class가 구현된 코드의 일부분을 보자.

    @param step Number of bytes each matrix row occupies. The value should include the padding bytes at
    the end of each row, if any. If the parameter is missing (set to AUTO_STEP ), no padding is assumed
    and the actual step is calculated as cols*elemSize(). See Mat::elemSize.
    */
    Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP);
    
    /** @brief Returns the matrix element size in bytes.

    The method returns the matrix element size in bytes. For example, if the matrix type is CV_16SC3 ,
    the method returns 3\*sizeof(short) or 6.
     */
    size_t elemSize() const;

Mat class의 생성자 부분과 elemSize() 함수의 설명을 가져왔다. 

Mat class의 parameter 중 size_t step=AUTO_STEP이 있다. 설명을 읽어보면 step 값을 생략하면 AUTO_STEP이 default로 들어가게 되며, 이 경우 padding이 없는 것으로 간주되며, actual step은 cols * elemSize()로 계산이 된다. 

elemSize()를 살펴보면 matrix element의 byte size를 리턴한다. 예로 CV_16SC3의 경우 한 픽셀에 16 * 3 = 48 bit를 사용하기 때문에 6 bytes이며, 따라서 6을 리턴한다. 

다시 Mat class의 step으로 돌아가서, AUTO_STEP을 사용하는 경우 actual step은 padding이 없는 실제 이미지 너비 데이터 byte 수를 갖게 된다. (width byte 값)

 

다시 코드로 넘어와서 한 줄씩 분석해본다.

void colorReduce(Mat image, int div = 64) {
	int nl = image.rows;	// number of iines
	// total number of elements per line
	int nc = image.cols * image.channels();

	for (int j = 0; j < nl; j++) {
		// get the address of row j
		uchar* data = image.ptr<uchar>(j);

		for (int i = 0; i < nc; i++) {
			// process each pixel
			data[i] = data[i] / div * div + div / 2;
			// end of pixel processing
		}
	}
}

image.rows를 통해서 height을 얻었으며,
image.cols * image.channels()를 통해 row마다 처리할 픽셀 값들의 수를 계산했다. (한 픽셀에 3개의 채널이 있다면 3개 모두 각각 color reduction을 진행해야 한다.)

 

바깥 loop는 계산할 row만큼 지정하고, image.ptr<uchar>(j)를 통해서 행의 첫 번째 데이터를 가리키는 주소를 얻었다. ptr method는 template method이며, 행 j의 주소를 return 한다. ptr은 아래와 같이 구현되어 있다.

template<typename _Tp> inline
_Tp* Mat::ptr(int y)
{
    CV_DbgAssert( y == 0 || (data && dims >= 1 && (unsigned)y < (unsigned)size.p[0]) );
    return (_Tp*)(data + step.p[0] * y);
}

위에서 step은 width의 byte 값을 나타냈다. 포인터 계산으로 data + step.p[0] * y는 영상 데이터에서 y번째 행의 첫 부분을 가리키게 된다.

 

안쪽 loop에서는 처리해야 할 pixel values만큼 loop를 돌면서 color reduction을 진행한다.

 

 

추가적인 이야기

1. Other color reduction formulas

color reduction으로 우리는 data[i] = (data[i]/div) * div + div/2 방식으로 구현하였다. 이것은 integer division에서 나머지는 버림이 되는 것을 이용한다. floor 처리를 하여 가까운 낮은 integer를 취하는 방식으로 reduction을 진행한다. 다른 방식으로는 다음과 같은 방법으로 구현할 수 있다.

 

1. modular 연산을 하여 구현할 수 있다. (이와 같은 경우에는 기존 방식 대비 domain은 급격하게 줄어들지 않지만, 색상을 어느 정도 덜 부자연스럽게 만들 수 있을 것으로 예상된다.)

data[i] = data[i] - data[i] % div + div/2

 

2. bitwise operation을 사용하여 구현할 수 있다. 만약 reduction factor가 2의 거듭제곱이라면(div = pow(2, n)), 처음 n비트의 픽셀 값을 masking하는 방식으로 가까운 낮은 div의 배수 값을 얻을 수 있다. bitwise operation은 매우 효율적인 코드로 연산 속도가 다른 방식에 비해 빠르다. 따라서 효율성이 요구되는 환경에서 사용하기에 적합하다.

// mask used to round the pixel value
uchar mask = 0xFF<<n; // e.g. for div = 16, mask = 0xF0
*data &= mask;		// masking 
*data++ += div>>1;	// add div/2
// bitwise OR could also be used above instead of +

참고로 0xFF는 uchar type(8bit)에 맞게 masking 하기 위한 틀이라고 생각하면 된다. (0xFF (hex) -> 1111 1111 (binary)) 0xFF << n을 수행하게 되면, n개만큼 좌측으로 shift 되면서 좌측의 1은 버려지게 되며, 우측은 0으로 새롭게 채워지게 될 것이다. 위에서 div = 16인 경우에 mask = 0xF0라는 예가 있는데, div = 16인 경우에는 div = pow(2, n)에서 n은 4가 되며, 0xFF << 4를 수행하게 되면 0xF0이다. 0xF0와 data를 &(bit level and 연산)를 진행하게 되면, 뒤의 4자리 bit는 0이 된다. 즉, 원본 value에서 bit 관점에서 0 ~ 15 value를 구성하는 부분은 모두 0으로 만드는 것이다. 우리가 예제로 사용했던 data[i] = (data[i]/div) * div + div/2 방식을 속도 측면에서 더 개선시킨 방법이라고 생각하면 되겠다.   

 

2. Having input and output arguments

위의 예제에서 color reduction 함수로 original 영상이 넘어가고, 함수가 끝난 후 원본 영상은 변경이 된 것을 확인할 수 있다. 이런 방식으로 구현을 한다면, output 영상을 위한 공간이 필요 없으므로, memory를 아낄 수 있다. 그러나 상황에 따라서 original 영상 보존이 필요할 때가 있다. 이러한 경우 함수를 부르기 전에, original 영상을 복사 후 그 영상을 넘겨줘야 한다. 예전에 이와 관련하여 clone() method를 사용한 적이 있다. 따라서 아래와 같이 구현하면 된다. 

// read the image
image= cv::imread("boldt.jpg");
// clone the image
cv::Mat imageClone= image.clone();
// process the clone
// orginal image remains untouched
colorReduce(imageClone);
// display the image result
cv::namedWindow("Image Result");
cv::imshow("Image Result",imageClone);

 

또 다른 방법으로는 함수의 정의를 조금 바꾸면 가능하다. 

void colorReduce(const cv::Mat &image,	 // input image
		cv::Mat &result,	// output image
		int div=64);
        
// option1
colorReduce(image,image);
// option2
cv::Mat result;
result.create(image.rows,image.cols,image.type());
colorReduce(image,result);

 const 키워드를 이용하여, input 영상을 보존하고, output 영상의 reference로 넘겨주어 결과를 반영할 수 있다. original 영상의 수정을 원하는 경우 option1과 같이 사용하며, 보존을 원하는 경우 option2를 사용하면 된다. option2를 사용하는 경우, result matrix가 input matrix와 같은 사이즈의 data buffer를 갖고 있는지 체크할 필요가 있다. 따라서 create 함수를 사용하여 꼭 함수 호출 전에 원본과 같은 사이즈로 맞춰주는 과정을 넣는다.

 

color reduce 함수에서는 2개의 포인터를 사용하여 scanning을 진행한다.

for (int j=0; j<nl; j++) {
	// get the addresses of input and output row j
	const uchar* data_in= image.ptr<uchar>(j);
	uchar* data_out= result.ptr<uchar>(j);
		for (int i=0; i<nc*nchannels; i++) {
		// process each pixel ---------------------
		data_out[i]= data_in[i]/div*div + div/2;
		// end of pixel processing ----------------
	} // end of line
}

3. Efficient scanning of continuous images

효율성의 이유로 image에는 dummy pixel들이 padding될 수 있다고 언급하였다. 만약 image가 unpadded라면, WxH 2차원 data를 긴 1차원의 data로 볼 수 있다. 따라서 image가 padding되었는지 여부에 따라 영상을 처리하는 구현이 달라질 수 있다. padding 여부를 알기 위하여 isContinuous method를 사용한다.

설명은 위의 문서를 참고하면 될 듯하다. 요약하면, unpadded 이미지라면 very long single-row vectors로 처리할 수 있는 operation들이 있다. (1차원으로 빠르게 처리할 수 있다면, padding 여부를 검사 후 빠르게 처리하는 것이 효율적이다.) 따라서 padding 여부를 체크하기 위해 isContinuous method가 사용되며, continuous하다면, true를 리턴한다.

* 참고로 조건은  return m.rows == 1 || m.step == m.cols*m.elemSize() 이다. 1차원 행렬이거나, step == cols*elemSize(). padding이 되었다면 step이 더 커지겠다. (기억이 안난다면 윗 부분의 step을 다시 읽자.)

 

이것을 활용하여 위의 예제를 변경하면 다음과 같다.

void colorReduce(cv::Mat image, int div=64) {
	int nl= image.rows; // number of lines
	// total number of elements per line
	int nc= image.cols * image.channels();
	if (image.isContinuous()) {
		// then no padded pixels
		nc= nc*nl;
		nl= 1; // it is now a 1D array
	}
	int n= static_cast<int>(log(static_cast<double>(div))/log(2.0) + 0.5);
	// mask used to round the pixel value
	uchar mask= 0xFF<<n; // e.g. for div=16, mask= 0xF0
	uchar div2 = div >> 1; // div2 = div/2
	// this loop is executed only once
	// in case of continuous images
	for (int j=0; j<nl; j++) {
		uchar* data= image.ptr<uchar>(j);
		for (int i=0; i<nc; i++) {
			*data &= mask;
			*data++ += div2;
		} // end of line
	}
}

continuous 검사를 한 후, true(no padded pixels)라면 col = rows * cols * channels, row = 1로 생각하여 1D array 시선으로 처리한다. 이러한 방식 말고도 memory copying 또는 reallocation 없이 matrix dimension을 변경하는 reshape() method가 있다.

 

reshape method를 적용하여 코드를 수정하면 아래와 같다.

if (image.isContinuous()){
	// no padded pixels
	image.reshape(1, // new number of channels
			1); // new number of rows
	}
	int nl= image.rows; // number of lines
	int nc= image.cols * image.channels();
}

1 번째 parameter는 channel의 수, 2 번째 parameter는 row이다. 

예로 row : 240, col : 320, channel : 3이라면 위와 같이 parameter를 지정했을 때 col은 230,400이 되며, row와 channel은 1이 된다.

 

4. Low-level pointer arithmetic

pointer로 memory block에 접근하는 것과 관련하여, 위의 예제의 경우 block의 type이 unsigned chars였기 때문에 pointer의 type은 uchar 형태를 사용했다. 
uchar *data = image.data;

 

만약 row를 다음 행으로 옮기기 위해서는 유효 너비를 이용하여 다음과 같이 구현할 수 있다.
data += image.step; // next line

 

step attribute는 padding pixel을 포함하여 line에서 총 byte를 리턴한다. 일반적으로 j행의 i열은 아래와 같이 얻을 수 있다.

// address of pixel at (j, i) that is &image.at(j, i)

data = image.data + j*image.step + i*image.elemSize();

 

pointer를 사용하여 image data에 자유롭게 접근하기 위해서는 2차원 -> 1차원 변환과 step, elemSize, channel 등에 대해서 숙지하고 실수하지 말자. (위에서 j*image.step 대신 j*image.cols*image.elemSize()를 사용했다면 padding처리 된 image에서는 전혀 다른 결과가 나올 수 있을 것이다.)

'영상처리 > OpenCV' 카테고리의 다른 글

06. Writing efficient scanning loops  (0) 2019.04.16
05. Scanning with iterator  (0) 2019.04.15
03. Accessing pixel values  (0) 2019.04.07
02. Mat 클래스 structure  (0) 2019.04.01
01. 이미지 read, imread 함수  (1) 2019.03.28
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함