JINWOOJUNG

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

2024/Study

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

Jinu_01 2024. 4. 6. 21:52
728x90
반응형

Before This Episode

Image Processing은 매우 다양하다. 그 중, 각 픽셀값에 접근하고,

픽셀분포를 판단하는 Histogram을 살펴보며, 간단한 이미지 합성과 연산에 대해 알아보자.

Visual Studio 2024 환경에서 C++ 언어를 기반으로 OpenCV를 다루기에 해당 환경을 구현 후 따라오는 것이 좋다.

 

https://jinwoo-jung.tistory.com/38

 

[디영처] OpenCV 환경설정(Window, Visual Studio)

0. Background [ Version ] Window x64 OpenCV-4.9.0 Visual Studio 2022 [ Content ] 본 포스팅은 Visual Studio 2022가 깔렸다는 전제하에 OpenCV를 C++ 언어 기반으로 개발하기 위한 환경설정하는 내용을 포스팅합니다. 1. Open

jinwoo-jung.tistory.com


cv::Mat Class

OpenCV를 활용하여 이미지를 처리하기 위한 가장 기본적인 클래스는 cv::Mat이다.

이미지는 2차원 영상 데이터로써, 각각의 픽셀이 행렬 형태로 저장되어 있다. 이미지는 아래 그림과 같은 구조로, 왼쪽 상단이 (0,0)이고 오른쪽, 아래 방향으로 각 좌표가 증가하는 형태이다.

 

Image Processing

 

cv::Mat 객체를 생성하는 다양한 방법이 존재한다.

cv::Mat src1(height, width, CV_8UC1);
cv::Mat src1(cv::Size(width, height), CV_8UC1); 
cv::Mat src1(height, width, CV_8UC1, cv::Scalar(0));
cv::Mat src1 = imread("ImageRoot", ImageFlag);​

 

이때, CV_8UC1은 8-bit Unsigned Integer, Channel은 1개라는 의미이다.

cv::Scalar(0)는 모든 픽셀의 값을 0이라고 초기화 한다는 의미이다.

마지막으로, 가장 많이 사용하는 imread()로 이미지의 경로와 Flag가 인자로 된다.

Ex) Mat src1 = imread("img1.jpg", 0);
int flags = IMREAD_COLOR 또는 int flags = 1 -> 컬러 영상으로 읽음
int flags = IMREAD_GRAYSCALE 또는 int flags = 0 -> 흑백 영상으로 읽음
int flags = IMREAD_UNCHANGED 또는 int flags = -1 -> 원본 영상의 형식대로 읽음

 


Mat::at()

Mat 객체의 픽셀에 접근하는 가장 기본적인 방법은 Mat::at() 이다.

하나 주의해야 할 점은 Mat::at()의 반환형이다.

 

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

 

함수의 파라미터가 주소값을 참조하는 것이 아닌 일반 자료형으로 되어 있으면 단순히 값만 가져온다. 하지만 주소값을 참조하면 함수 내에서 해당 변수의 값을 변경하면 원본 변수의 값도 변한다. Mat 객체를 함수의 파라미터로 설정하고 참조하지 않으면 Mat 객체를 변경 하여도 원본 객체에는 영향을 주지 않는다. 하지만, Mat::at()의 반환형은 해당 필셀의 주소값을 참조한다.  따라서 함수에서 참조하여 인자를 받아오지 않아도, 함수 내에서 Mat::at()을 통해 해당 위치의 픽셀 Value를 변경하면 원본 객체의 픽셀 Value도 변경된다.

 

예제와 함께 살펴보자.

#include <iostream>
#include "opencv2/core/core.hpp" // Mat class와 각종 data structure 및 산술 루틴을 포함하는 헤더
#include "opencv2/highgui/highgui.hpp" // GUI와 관련된 요소를 포함하는 헤더(imshow 등)
#include "opencv2/imgproc/imgproc.hpp" // 각종 이미지 처리 함수를 포함하는 헤더

using namespace cv;
using namespace std;

void SpreadSalts(int num);					
void CalculateRGBPixelNumber(Mat img);		

int main() {

	SpreadSalts(3000);			// 생성할 R,G,B Pixel Number를 인자로 설정

	return 0;
}

void SpreadSalts(int num)
{
	Mat OriginalImage = imread("img1.jpg", 1);

	for (int n = 0; n < num; n++)
	{
		int x = rand() % OriginalImage.cols;
		int y = rand() % OriginalImage.rows;

		// 임의로 생성된 Pixel 좌표에 대하여 R,G,B 각각의 색상을 특정한 개수로 나누어 색상을 적용
		if (n < 1000)
		{
			// Blue
			OriginalImage.at<Vec3b>(y, x)[0] = 255;
			OriginalImage.at<Vec3b>(y, x)[1] = 0;
			OriginalImage.at<Vec3b>(y, x)[2] = 0;
		}
		else if (n >= 1000 && n < 1800)
		{
			// Green
			OriginalImage.at<Vec3b>(y, x)[0] = 0;
			OriginalImage.at<Vec3b>(y, x)[1] = 255;
			OriginalImage.at<Vec3b>(y, x)[2] = 0;
		}
		else
		{
			// Red
			OriginalImage.at<Vec3b>(y, x)[0] = 0;
			OriginalImage.at<Vec3b>(y, x)[1] = 0;
			OriginalImage.at<Vec3b>(y, x)[2] = 255;

		}
	}

	CalculateRGBPixelNumber(OriginalImage);
}

void CalculateRGBPixelNumber(Mat ResultImage)
{
	int32_t s32_I, s32_J, s32_CountR = 0, s32_CountG = 0, s32_CountB = 0;

	// R,G,B 색상이 Random한 Pixel 좌표에 적용된 이미지를 받아와 각 픽셀에 대하여 색상을 판단 후 각 색상의 개수를 계산
	for (s32_I = 0; s32_I < ResultImage.cols; s32_I++)
	{
		for (s32_J = 0; s32_J < ResultImage.rows; s32_J++)
		{
			if (ResultImage.at<Vec3b>(s32_J, s32_I)[0] == 255 && ResultImage.at<Vec3b>(s32_J, s32_I)[1] == 0 && ResultImage.at<Vec3b>(s32_J, s32_I)[2] == 0)
			{
				s32_CountB += 1;
			}
			else if (ResultImage.at<Vec3b>(s32_J, s32_I)[0] == 0 && ResultImage.at<Vec3b>(s32_J, s32_I)[1] == 255 && ResultImage.at<Vec3b>(s32_J, s32_I)[2] == 0)
			{
				s32_CountG += 1;
			}
			else if (ResultImage.at<Vec3b>(s32_J, s32_I)[0] == 0 && ResultImage.at<Vec3b>(s32_J, s32_I)[1] == 0 && ResultImage.at<Vec3b>(s32_J, s32_I)[2] == 255)
			{
				s32_CountR += 1;
			}
		}
	}

	cout << "--------------Result Analyze--------------" << endl;
	cout << "Original Pixel Number: Blue - 1000 , Green - 800 , Red - 1200 " << endl;
	cout << "Random   Pixel Number: Blue - " << s32_CountB << " , Green - " << s32_CountG << " , Red - " << s32_CountR << endl;
	cout << "------------------------------------------" << endl;

	imshow("ResultImage", ResultImage);
	waitKey(0);
	destroyWindow("ResultImage");
}

 

SpreadSalts()는 인자로 임의로 생성할 픽셀의 개수를 받는다.

img1.jpg를 불러와 Mat 객체인 OriginalImage에 저장한 후 생성할 픽셀의 개수만큼 반복문을 돈다. 이때, rand()를 사용하여 임의의 Pixel Position (x,y)를 선정한다. 만약 읽어오는 이미지의 체널이 1개 즉, Grayscale 이라면 해당 픽셀값은 255 즉 흰색으로 설정한다.

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

 

이때 Mat::at()으로 접근 시 GrayScale은 1개의 체널이기 때문에 at<uchar>로 접근한다. 또한, 접근하는 픽셀의 좌표는 (y,x)임을 유의해야 한다.

 

만약 컬러이미지라면 RGB 각각의 체널에 접근해야 한다. 이때, OpenCV에서는 RGB가 아닌 BGR 순서임을 기억해야 한다.

// Blue
OriginalImage.at<Vec3b>(y, x)[0] = 255;
OriginalImage.at<Vec3b>(y, x)[1] = 0;
OriginalImage.at<Vec3b>(y, x)[2] = 0;

 

단일 체널이 아닌 3체널이기 때문에 at<Vec3b>로 접근해야 하며 좌표는 동일하게 (y,x) 이다.

BGR 순서이기 때문에 파란색으로 설정하고 싶으면 OriginalImage.at<Vec3b>(y, x)[0] = 255이고, 1,2 체널은 0이 되어야 한다.

 

SpreadSalts()은 결국 특정 개수만큼의 랜덤한 픽셀 좌표의 색을 R,G,B로 설정하는 코드이다.

 

 

 

이제 실제 각 픽셀의 값을 R,G,B의 색상과 비교하여 정확히 생성 되었는지 비교해보자.

CalculateRGBPixelNumber()는 이미지를 인자로 받은 후 이미지의 모든 픽셀에 접근하여 각 픽셀의 값을 R,G,B와 비교하여 해당 픽셀 수를 계산하는 코드이다.

 

 

실제 계산 결과 거의 유사함을 알 수 있다. 차이가 나는 이유는 SpreadSalts()에서 생성한 픽셀 좌표는 Random이다. 따라서 동일한 좌표도 생성될 수 있기 때문에 개수에서 차이 발생하였다.


Image Gradation & Image Histogram

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

using namespace cv;
using namespace std;

void MakeGradationImage();					
Mat GetHistogram(Mat src);					

int main() {

	MakeGradationImage();
	
	return 0;
}

void MakeGradationImage()
{
	// Load Each Image
	Mat ResultImage;
	Mat OriginalImage = imread("img2.jpg", 0);
	Mat GradationImage_Upper = imread("img2.jpg", 0);
	Mat GradationImage_Lower = imread("img2.jpg", 0);

	int32_t s32_Width, s32_Height;

	// 위로 갈수록 어두운 이미지 생성
	for (s32_Width = 0; s32_Width < GradationImage_Upper.cols; s32_Width++)
	{
		for (s32_Height = 0; s32_Height < GradationImage_Upper.rows; s32_Height++)
		{
			// Pixel Value가 0에 가까울수록 작아지므로 전체 Height에 대하여 현재 픽셀의 위치를 고려한 Delta를 계산
			int32_t s32_Delta = (GradationImage_Upper.rows - s32_Height) * 255 / GradationImage_Upper.rows;
			// Grayscale이기 때문에 0보다 작은 경우 0으로 설정
			GradationImage_Upper.at<uchar>(s32_Height, s32_Width) = (GradationImage_Upper.at<uchar>(s32_Height, s32_Width) - s32_Delta) > 0 ? GradationImage_Upper.at<uchar>(s32_Height, s32_Width) - s32_Delta : 0;
		}
	}

	// 아래로 갈수록 어두운 이미지 생성
	for (s32_Width = 0; s32_Width < GradationImage_Lower.cols; s32_Width++)
	{
		for (s32_Height = 0; s32_Height < GradationImage_Lower.rows; s32_Height++)
		{
			// 아래로 갈수록 Delta 값이 커져야 하므로 Height에 대하여 현재 위치를 빼는것이 아닌 현재 위치만 고려하여 Height에 대한 Delta 값을 계산
			int32_t s32_Delta = s32_Height * 255 / GradationImage_Lower.rows;
			GradationImage_Lower.at<uchar>(s32_Height, s32_Width) = (GradationImage_Lower.at<uchar>(s32_Height, s32_Width) - s32_Delta) > 0 ? GradationImage_Lower.at<uchar>(s32_Height, s32_Width) - s32_Delta : 0;
		}
	}

	// 한번에 보기 위한 이미지를 행방향으로 합성
	hconcat(OriginalImage, GradationImage_Upper, ResultImage);
	hconcat(ResultImage, GradationImage_Lower, ResultImage);

	// Make Each Histogram
	Mat ResultHistogram;
	Mat OriginHistogram = GetHistogram(OriginalImage);
	Mat UpperHistogram = GetHistogram(GradationImage_Upper);
	Mat LowerHistogram = GetHistogram(GradationImage_Lower);

	hconcat(OriginHistogram, UpperHistogram, ResultHistogram);
	hconcat(ResultHistogram, LowerHistogram, ResultHistogram);

	imshow("ResultImage", ResultImage);
	imshow("ResultHistogram", ResultHistogram);
	waitKey(0);
	destroyWindow("ResultImage");
	destroyWindow("ResultHistogram");
}

Mat GetHistogram(Mat src) {
	Mat histogram;
	const int* channel_numbers = { 0 };
	float channel_range[] = { 0.0, 255.0 };
	const float* channel_ranges = channel_range;
	int number_bins = 255;

	calcHist(&src, 1, channel_numbers, Mat(), histogram, 1, &number_bins, &channel_ranges);

	int hist_w = 513;
	int hist_h = 400;
	int bin_w = cvRound((double)hist_w / number_bins);

	Mat histImage(hist_h, hist_w, CV_8UC1, Scalar(0, 0, 0));
	normalize(histogram, histogram, 0, histImage.rows, NORM_MINMAX, -1, Mat());

	for (int i = 1; i < number_bins; i++) {
		line(histImage, Point(bin_w * (i - 1), hist_h - cvRound(histogram.at<float>(i - 1))),
			Point(bin_w * (i), hist_h - cvRound(histogram.at<float>(i))),
			Scalar(255, 0, 0), 2, 8, 0);
	}

	return histImage;
}

 

MakeGradationImage()는 원본 이미지에서 Gradation 효과를 주는 함수이다.

 

Grayscale Image의 Pixel Value는 0~255를 가진다. 검은색이 0이고, 흰색이 255이다. 

Gradiation 효과는 다양한 방법으로 적용할 수 있는데, 먼저 아래에서 위로 밝아지도록 구현 해 보자. 이때, 우리는 원본 이미지의 Gray Value를 유지하면서 Gradation 효과를 적용해야 한다.

Mat OriginalImage = imread("img2.jpg", 0);
Mat GradationImage_Upper = imread("img2.jpg", 0);

int32_t s32_Width, s32_Height;

// 위로 갈수록 어두운 이미지 생성
for (s32_Width = 0; s32_Width < GradationImage_Upper.cols; s32_Width++)
{
	for (s32_Height = 0; s32_Height < GradationImage_Upper.rows; s32_Height++)
	{
		// Pixel Value가 0에 가까울수록 작아지므로 전체 Height에 대하여 현재 픽셀의 위치를 고려한 Delta를 계산
		int32_t s32_Delta = (GradationImage_Upper.rows - s32_Height) * 255 / GradationImage_Upper.rows;
		// Grayscale이기 때문에 0보다 작은 경우 0으로 설정
		GradationImage_Upper.at<uchar>(s32_Height, s32_Width) = (GradationImage_Upper.at<uchar>(s32_Height, s32_Width) - s32_Delta) > 0 ? GradationImage_Upper.at<uchar>(s32_Height, s32_Width) - s32_Delta : 0;
	}
}

 

위로 갈수록 어두운 이미지를 생성하기 위해선 원본 픽셀 값에서 Delta 만큼 감소시켜야 한다. 이때, 위로 갈수록 더 어두워지려면, 픽셀 좌표의 y값이 커질수록 Delta 값이 작아져야 한다. 따라서 Delta를 이미지의 Height에서 현재 픽셀의 y값을 뺀 값으로 설정하면, y값이 커질수록 즉, 아래로 갈수록 Delta 값이 커져 해당 이미지가 더 어두워진다. 이때, 픽셀의 Height에 대해 정규화를 하기 위해서 255를 곱하고 Height만큼 나눠줬다.

 

따라서 GradationImage_Upper의 픽셀 값은 원본에서 Delta만큼 뺀 값으로 설정하며, 만약 해당 값이 0보다 작으면 0으로 설정하였다.

 

아래로 갈수록 어두운 이미지를 생성하려면, 반대로 아래로 갈수록 Delta 값이 커져야 하기 때문에 분자를 해당 픽셀의 y좌표로 설정 하였다. 

 

각각의 이미지의 Histogram은 GetHistogram()로 생성하였다. 결과는 다음과 같다.

 

 

원본 이미지의 Histogram을 살펴보면 픽셀 값이 대부분 0~255의 중앙에 많이 분포함을 알 수 있다. Gradation을 적용한 이미지의 Histogram을 보면 대부분 0에 근접하는 값인데, 이는 원본 이미지의 픽셀 분포가 중앙에 많이 분포하기 때문이다.


Image Composition

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

using namespace cv;
using namespace std;

void MakeCompositionImage();				// For HW3

int main() {

	MakeCompositionImage();

	return 0;
}

void MakeCompositionImage()
{
	Mat OriginalImage3 = imread("img3.jpg", 1);
	Mat OriginalImage4 = imread("img4.jpg", 1);
	Mat OriginalImage5 = imread("img5.jpg", 1);
	Mat GrayImage5 = imread("img5.jpg", 0);
	Mat ResultImage;

	// Image3,4 를 이용하여 배경이미지 생성을 위한 크기 동일화
	resize(OriginalImage4, OriginalImage4, Size(OriginalImage3.cols, OriginalImage3.rows));
	// 비네팅과 유사한 배경을 생성하기 위해서 subtract
	subtract(OriginalImage3, OriginalImage4, ResultImage);

	// Logo가 들어갈 위치 선정. 원본이미지의 Width의 중앙, Height의 3/5 지점
	Mat RoI_Logo(ResultImage, Rect((ResultImage.cols - OriginalImage5.cols) / 2, 3 * (ResultImage.rows - OriginalImage5.rows) / 5, OriginalImage5.cols, OriginalImage5.rows));
	// 이미지 합성을 하기 전 Logo의 배경을 제거하기 위해 Threshold를 설정하여 Threshold 보다 크면 0으로 작으면 픽셀값을 유지하도록 설정
	threshold(GrayImage5, GrayImage5, 200, 0, THRESH_TOZERO_INV);
	// 이미지 합성을 위한 copyTo(). OriginalImage5에서 GrayImage5의 픽셀 값이 0이 아닌 픽셀들만 OriginalImage5에서 픽셀값을 유지한 채 RoI_Logo에 복사
	OriginalImage5.copyTo(RoI_Logo, GrayImage5);

	imshow("ResultImage", ResultImage);
	waitKey(0);
	destroyWindow("ResultImage");
}

 

Image 3,4,5는 다음과 같다.

 

Image 3,4
Image 5

 

 

배경 이미지를 Image 3,4를 이용하여 구성하고자 한다.

resize(OriginalImage4, OriginalImage4, Size(OriginalImage3.cols, OriginalImage3.rows));
subtract(OriginalImage3, OriginalImage4, ResultImage);

 

cv::resize( Input Image, Output Image, Size) 

 

두 이미지의 연산을 위해서는 크기가 동일해야 하기 때문에 Image 3와 동일한 Size로 크기를 변경하였다.

 

cv::MatOp::subtract( Image1, Image2, Result Image)

 

Image4는 중앙에는 작은 값이, 바깥쪽으로는 큰 값이 존재한다. 따라서 Image 3에서 Image 4를 뺀다면 비네팅 효과처럼 바깥쪽이 어두워 질 것이다.

 

이후 배경 이미지에 Image5의 로고를 넣는 과정을 진행 해 보자.

먼저 배경 이미지에서 로고가 들어갈 위치를 설정해야한다. 이처럼 전체 이미지에서 내가 관심있는 영역만 추출하는 것을 RoI(Region of Interest)라고 한다.

 

Mat RoI_Logo(ResultImage, Rect((ResultImage.cols - OriginalImage5.cols) / 2, 3 * (ResultImage.rows - OriginalImage5.rows) / 5, OriginalImage5.cols, OriginalImage5.rows));

 

원본 이미지에서 영역을 Rect()를 사용하여 이미지 Width의 중앙과 Height 3/5 지점으로 설정하였다. 

 

Image5의 로고는 배경이 존재한다. 하지만, 배경 이미지에서 로고를 합성할 때는 배경 부분을 제거해야 한다. 따라서 두 이미지 연산을 하기 전, Thresholding 작업을 통해 로고의 배경을 제거하고자 한다.

 

threshold(GrayImage5, GrayImage5, 200, 0, THRESH_TOZERO_INV);
cv.threshold(Mat src, Mat dst, double thresh, double maxval, int type)

 

threshold()는 특정한 임계값(thresh)보다 큰 픽셀과 작은 픽셀로 구분하여 작업을 할 수 있다. 해당 작업은 type에 따라 달라진다. 

 

우리가 원하는 값은 배경이 아닌 오직 로고이고, 배경은 거의 흰색(255)에 가깝다. 따라서 임계값을 200으로 설정하고 임계값 보다 큰 픽셀은 0으로, 작은 픽셀은 원본 값을 유지하기 위해서 THRESH_TOZERO_INV로 설정하였다.

 

이후 배경 이미지와 로고를 합치기 위해서 copyTo()를 사용하였다.

 

src.copyTo(dst, mask)

 

copyTo는 src Image에서 mask의 픽셀 값이 0이 아닌 픽셀에 해당되는 src의 픽셀을 픽셀값을 유지한 채 dst에 복사한다.

위에 Thresholding 처리 한 로고에서 배경은 모두 0이기 때문에, 배경이 아닌 실제 로고만 배경 이미지에 복사된다.

 

Result Image

728x90
반응형