티스토리 뷰

영상처리/OpenCV

03. Accessing pixel values

빠리빵 2019. 4. 7. 19:51

opencv에서 영상을 read 후 그 영상의 pixel에 접근하여 처리하는 경우가 있다. 이전 단계에서 이미지를 read 하고, Mat class에 대해서 간단하게 배워보았는데, 이번에는 image의 pixel의 값에 접근 후 수정하는 법을 알아본다.

 

예제는 영상을 read 후 n개의 픽셀을 white로 변경하는 fuction을 작성 후 효과를 확인할 것이다.

 

우선 image의 type에 따라서 white로 바꾸는 방법이 달라질 것이다.

1. 영상이 일반적인 grayscale이라면 8 bit unsigned를 사용한다. 따라서 pixel value의 range는 0 ~ 255로 해당 pixel의 값을 255로 바꾸는 방식으로 만들 수 있다. (case에 따라서 bit 수는 변경될 수 있겠다.) 

2. 영상이 grayscale이 아니라면, 3개의 channel의 값을 모두 변경해주어야 하며, type이 floating, unsigned, signed 등에 따라서 값은 다르게 변경해야 한다. (예로 floating이라면 0.0 ~ 1.0의 range를 갖기 때문에 1로 변경해주어야 한다.)

 

코드는 다음과 같다.

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

using namespace std;
using namespace cv;

void salt(Mat image, int n) {
	default_random_engine generator;
	uniform_int_distribution<int> randomRow(0, image.rows - 1);
	uniform_int_distribution<int> randomCol(0, image.cols - 1);

	int i, j;
	for (int k = 0; k < n; k++) {
		// random image coordinate
		i = randomCol(generator);
		j = randomRow(generator);

		if (image.type() == CV_8UC1) { // gray-level image 
			// single-channel 8-bit image
			image.at<uchar>(j, i) = 255;
		}
		else if (image.type() == CV_8UC3) { // color image
			// 3-channel image
			image.at<Vec3b>(j, i)[0] = 255;
			image.at<Vec3b>(j, i)[1] = 255;
			image.at<Vec3b>(j, i)[2] = 255;
		}
	}
}

int main() {
	Mat image = imread("lena.tif", IMREAD_COLOR);

	salt(image, 3000);

	namedWindow("Image");
	imshow("Image", image);
	waitKey(0);
}

 

salt함수를 살펴보면 parameter는 Mat 객체와 white로 변경할 pixel의 갯수 n이다.

먼저 난수를 생성하는 부분이 등장한다.

default_random_engine의 경우 검색해보니, seed와 관련된 부분이다. C++11의 표준으로 등록된 난수 추출 방식이라고 한다.

seed 생성 후 타입을 결정하는데, 위의 경우는 정수 난수를 생성하였다. (실수 -> std::uniform_real_distribution, 정규분포 랜덤 값 -> std::normal_distribution 등도 방식도 있다고 한다.) 범위는 [0 ~ row - 1], [0 ~ col - 1]로 정했다.

이후 n번의 loop를 돌면서 난수를 생성, 값을 변경한다. 값을 변경하기 전에 영상이 gray image인지, color image 검사 후 case에 맞게 진행한다. gray의 경우 value를 255로 변경하고, color의 경우 3개 각 채널의 value를 255로 변경한다.

 

type 검사를 위해서 type()이라는 함수를 사용하였고, CV_8UC1, CV_8UC3 case에 대해서 처리를 해주었다.

 

이후 image의 (j, i)에 접근하는 함수가 등장한다. j는 row이며, i는 col이다. CV_8UC1의 경우엔 image.at<uchar>(j, i) = 255로 접근하였다. < > 안에 type이 등장하는데, 이와 같은 문법을 template method라 한다. type 함수는 compile time에 return을 해야 하며, Mat class은 모든 type을 갖기 때문에 at 함수는 template method로 작성되어 우리는 image element type을 명시할 필요가 있다.

 

at 함수가 작성된 것을 따라들어가보면 아래와 같이 작성되어 있다.

template<typename _Tp> inline
_Tp& Mat::at(int i0, int i1)
{
    CV_DbgAssert(dims <= 2);
    CV_DbgAssert(data);
    CV_DbgAssert((unsigned)i0 < (unsigned)size.p[0]);
    CV_DbgAssert((unsigned)(i1 * DataType<_Tp>::channels) < (unsigned)(size.p[1] * channels()));
    CV_DbgAssert(CV_ELEM_SIZE1(traits::Depth<_Tp>::value) == elemSize1());
    return ((_Tp*)(data + step.p[0] * i0))[i1];
}

template<typename _Tp> inline
_Tp& Mat::at(Point pt)
{
    CV_DbgAssert(dims <= 2);
    CV_DbgAssert(data);
    CV_DbgAssert((unsigned)pt.y < (unsigned)size.p[0]);
    CV_DbgAssert((unsigned)(pt.x * DataType<_Tp>::channels) < (unsigned)(size.p[1] * channels()));
    CV_DbgAssert(CV_ELEM_SIZE1(traits::Depth<_Tp>::value) == elemSize1());
    return ((_Tp*)(data + step.p[0] * pt.y))[pt.x];
}

template<typename _Tp> inline
_Tp& Mat::at(int i0, int i1, int i2)
{
    CV_DbgAssert( elemSize() == sizeof(_Tp) );
    return *(_Tp*)ptr(i0, i1, i2);
}

template<typename _Tp, int n> inline
_Tp& Mat::at(const Vec<int, n>& idx)
{
    CV_DbgAssert( elemSize() == sizeof(_Tp) );
    return *(_Tp*)ptr(idx.val);
}

다시 image.at<uchar>(j, i) = 255로 돌아가서, 이 코드는 위의 코드 중 가장 윗 부분에 해당되는 부분이 동작하며, DbgAsset는 size, type 등의 검사 후 return ((_Tp*)(data + step.p[0] * i0))[i1]; 부분이 실행되게 된다. 이 코드는 2차원으로 주어진 좌표의 pixel을 1차원으로 변환 후 해당 부분의 주소에서 uchar 크기만큼 data를 리턴하는 것이다.

참고로 2차원 -> 1차원 변환은 다음과 같이 생각하면 이해하기 쉽다.

 

(0,0) , 0 (0,1) , 1 (0,2) , 2 (0,3) , 3 (0,4) , 4 (0,5) , 5
(1,0) , 6 (1,1) , 7 (1,2) , 8 (1,3) , 9 (1,4) , 10 (1,5) , 11
(2,0) , 12 (2,1) , 13 (2,2) , 14 (2,3) , 15 (2,4) , 16 (2,5) , 17

 

순서대로 2차원 좌표, 1차원 좌표를 적어두었다. (2차원 배열을 이어붙이면 1차원으로 쭉 늘릴 수 있다.)

여기서 column은 6이다. 만약 (1,4) 좌표를 10으로 변환하는 공식은 (1 * column) + 4 = 10 이다.

공식으로 변환하면 (x, y) -> (x * column) + y로 생각할 수 있다. 우리는 2차원으로 생각하는 것이 더 직관적이지만, 메모리 및 속도 효율성을 생각하면 1차원으로 생각하는 것에 더 익숙해져야 할 것이다.

 

image.at<Vec3b>(j, i)[0] = 255도 비슷하게 color image이기 때문에 vector로 리턴 후 각 channel에 대해서 255로 값을 변경해주었다.

* 혹은 image.at(j, i) = Vec3b(255, 255, 255); 로 변경할 수 있다.

 

추가로 Vec3b에서 3은 3-element를 의미하고, b는 byte를 의미한다. b 대신 s(signed), f(float), i(integer), d(double)이 가능하며, 3 대신 2 혹은 4 element도 template로 작성되어 가능하다. 

 

main 함수에서 이미지를 read 후 해당 함수를 거친 결과는 다음과 같다. 

영상에 random하게 white pixel이 생겨난 것을 확인할 수 있다. 추가로 parameter로 넘어간 image가 따로 value가 복사되지 않고, 원본과 공유된다는 사실에 주의하자. 

 

* 추가로 template type 지정하는 것이 번거롭다면 cv::Mat class의 sub class인 cv::Mat_ 를 사용하면 해결할 수 있다. 

// use image with a Mat_ template
cv::Mat_<uchar> img(image);
img(50,100)= 0; // access to row 50 and column 100

위처럼 선언 시 type을 지정해주고, 접근 시 바로 type을 지정하지 않아도 접근이 가능하다.

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

05. Scanning with iterator  (0) 2019.04.15
04.Scanning with pointer  (0) 2019.04.10
02. Mat 클래스 structure  (0) 2019.04.01
01. 이미지 read, imread 함수  (1) 2019.03.28
00. OpenCV 설치  (0) 2019.03.26
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함