JINWOOJUNG

[ 영상 처리 ] Part1-2. OpenCV Image Processing(C++) 본문

2024/Study

[ 영상 처리 ] Part1-2. OpenCV Image Processing(C++)

Jinu_01 2024. 4. 6. 23:38
728x90
반응형

Before This Episode

지난 포스팅에서 가장 기본적인 OpenCV 기반의 Image Processing 과정을 공부하였다.

이미지 처리를 위한 cv::Mat 객체를 처음 접하는 과정에서 약간의 혼동이 있을 것 같아

Mat 객체의 픽셀에 대한 접근 방법에 대해서 세부적으로 포스팅 하고자 한다.

 

https://jinwoo-jung.com/48

 

[ 영상 처리 ] Part1. OpenCV Image Processing

Before This Episode Image Processing은 매우 다양하다. 그 중, 각 픽셀값에 접근하고, 픽셀분포를 판단하는 Histogram을 살펴보며, 간단한 이미지 합성과 연산에 대해 알아보자. Visual Studio 2024 환경에서 C++

jinwoo-jung.com


 

Mat::at()

template<typename_ Tp> _Tp& Mat::at(int y, int x)

x : 참조할 픽셀의 Row Index
y : 참조할 픽셀의 Column Index

Return : Mat 객체의 y번째 행, x번째 열 픽셀의 Value(참조)

 

가장 기본적으로 Mat::at()으로 특정 픽셀의 Value에 접근할 수 있다. 여기서 중요한 점은 Return Value가 (x,y) 픽셀의 참조형이다. 즉, 주소를 공유하고 있기 때문에 원본 픽셀 값에 접근 가능하다.

 

또한, template 함수로 지정되어 있기 때문에 Mat 객체에 따른 자료형을 명시해 줘야 한다.

만약 Mat객체가 CV_8UC1 Type 즉, GrayScale Image인 경우 <uchar>형을, CV_8UC3 Type 즉, 3체널 Color Image인 경우 <Vec3b>로 명시 해 줘야 한다.

 

예제를 통해 확인해보자.

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

using namespace cv;
using namespace std;

void MakeCircle(Mat st_Image);

int main()
{
	cv::Mat st_TmpImage = Mat::zeros(255, 255, CV_8UC1);

	cv::imshow("Before st_TmpImage", st_TmpImage);

	MakeCircle(st_TmpImage);

	cv::imshow("After st_TmpImage", st_TmpImage);
	cv::waitKey(0);

	return 0;
}

void MakeCircle(Mat st_Image)
{
	for (int i = 0; i < 50; i++)
	{
		int x = rand() % st_Image.cols;
		int y = rand() % st_Image.rows;

		st_Image.at<uchar>(y, x) = 255;
	}
}

 

한줄 한줄 자세히 살펴보자.

cv::Mat st_TmpImage = Mat::zeros(255, 255, CV_8UC1);

 

255x255의 크기이고, CV_8UC1이므로 GrayScale Image이며, Mat::zeros()를 통해 생성된 Mat 객체이기 때문에 모든 픽셀 값은 0이다.

 

void MakeCircle(Mat st_Image)
{
	for (int i = 0; i < 50; i++)
	{
		int x = rand() % st_Image.cols;
		int y = rand() % st_Image.rows;

		st_Image.at<uchar>(y, x) = 255;
	}
}

 

MakeCircle 함수는 파라미터로 Mat st_Image를 가진다. 해당 파라미터는 참조형이 아니기 때문에 MakeCircle 함수 내부에서 값을 변경 하여도 main문에서 생성된 원본 Mat 객체에는 영향을 주지 않는다. 

 

st_Image.at<uchar>(y, x) = 255;

 

하지만 위와 같이 Mat::at()으로 접근하면 해당 픽셀 값을 참조한다. 따라서 원본의 동일한 위치의 픽셀 Value는 255로 변경되는 것이다. 따라서 결과는 다음과 같다.

 

 

원본 이미지 역시 변경되었음을 확인할 수 있다.

 

그렇다면 우리는 분명 주소값을 참조하도록 함수의 파라미터를 설정하지 않아서 원본 Mat 객체가 변하지 않을 것으로 생각 하였지만, 변경되었다. 이를 방지하는 가장 쉬운 방법은 const를 이용하는 것이다.

 

 

실제로 const를 사용하면 다음과 같이 Error가 발생함을 알 수 있다. 이를 통해 원본 객체의 Pixel이 참조되어 변경됨을 다시한번 확인할 수 있다.

 


cv::Mat::data

uchar* cv::Mat::data

 

cv::Mat 클래스에는 data 멤버변수가 존재한다. 이는 uchar* 형태로, 0,0 픽셀 좌표의 주소를 의미한다. 

 

C++의 Array 변수의 이름 역시 배열의 시작 주소이고, 해당 배열의 Index에 접근하기 위해서는 배열이름[Index]와 같이 접근하였다. 어떻게 보면 cv::Mat 객체 즉, 이미지 역시 2차원 배열 형태이다. 그리고 data 멤버 변수는 시작 주소를 가르킨다. 그렇다면 배열에 접근하는 것처럼 이미지의 각 픽셀에 동일하게 접근할 수 있다.

 

예제를 통해 확인 해 보자.

Mat MakeCopy(Mat st_OriginalImage) {
    int s32_Y, s32_X;
    int s32_Width = st_OriginalImage.cols;
    int s32_Height = st_OriginalImage.rows;

    Mat st_ResultImage(st_OriginalImage.size(), CV_8UC1);

    uchar* srcData = st_OriginalImage.data;
    uchar* dstData = st_ResultImage.data;

    for (s32_Y = 0; s32_Y < s32_Height; s32_Y++) {
        for (s32_X = 0; s32_X < s32_Width; s32_X++) {
            dstData[s32_Y * s32_Width + s32_X] = srcData[s32_Y * s32_Width + s32_X];
        }
    }

    return st_ResultImage;
}

 

MakeCopy()는 파라미터로 Mat 객체를 받는다. 이후 받아온 인자와 동일한 크기, GrayScale 이미지인 st_ResultImage를 생성하였다.

 

uchar* srcData = st_OriginalImage.data;
uchar* dstData = st_ResultImage.data;

 

srcData는 원본 이미지의 픽셀 시작 주소를, dstData는 이미지 처리 후 결과 이미지의 픽셀 시작 주소를 의미한다.

 

for (s32_Y = 0; s32_Y < s32_Height; s32_Y++) {
    for (s32_X = 0; s32_X < s32_Width; s32_X++) {
        dstData[s32_Y * s32_Width + s32_X] = srcData[s32_Y * s32_Width + s32_X];
    }
}

 

원본 이미지의 Height, Width 범위 내에서 이중 for문을 돌면서 각 픽셀에 접근한다. s32_Y는 열을, s32_X는 행의 좌표를 의미한다. 따라서 열이 증가하는 것은 s32_Width만큼 Index가 더해진다고 생각할 수 있다. 따라서 위와 같이 모든 픽셀에 접근 가능하다.

 

Mat::data는 픽셀의 시작 주소를 가르킨다고 하였는데, 쉽게 생각해서 2차원 배열 이미지를 각 순서에 맞게 1차원으로 늘려 놨다고 생각하면 된다.


Mat::ptr()

template<typename _Tp>

_Tp* Mat::ptr(int y)

y : 참조할 Mat 객체의 행 번호
반환값 : y번째 행의 시작 주소

 

Mat::ptr()은 Mat 객체의 특정 행의 첫번째 열 원소의 주소를 반환한다.

Mat::at()과 동일하게 자료형을 지정해야 하며, 반환되는 주소를 이용하여 특정 행의 픽셀에 접근 가능하다.

 

예제를 통해 확인 해 보자.

 

Mat MakeCopy(Mat st_OriginalImage) {
    int s32_Y, s32_X;
    int s32_Width = st_OriginalImage.cols;
    int s32_Height = st_OriginalImage.rows;

    Mat st_ResultImage(st_OriginalImage.size(), CV_8UC1);

    uchar* dstData = st_ResultImage.data;

    for (s32_Y = 0; s32_Y < s32_Height; s32_Y++) {
		uchar* srcData = st_OriginalImage.ptr<uchar>(s32_Y);
        for (s32_X = 0; s32_X < s32_Width; s32_X++) {
			dstData[s32_Y * s32_Width + s32_X] = srcData[ s32_X];
        }
    }

    return st_ResultImage;
}

 

Mat::data에서 본 코드와 동일한 기능을 하는 코드이다. 하지만 원본 이미지의 픽셀에 접근하는 방법에 차이가 존재한다.

 

uchar* srcData = st_OriginalImage.ptr<uchar>(s32_Y);

 

cv::Mat::data는 Mat 객체의 픽셀 시작 주소를 반환한다고 하였다. Mat::ptr()은 특정 행의 시작 주소를 반환한다. 따라서 Width Index만 고려해서 픽셀에 접근하면 되는 것이다.

 

 

728x90
반응형