영상처리 프로그램 작성하면서 OpenCV 라이브러리를 많이 사용함

C++에서 지원되는 라이브러리 였는데, Python에서도 잘 됨

그래서, Python 사용할 줄 아는 사람들이 이미지 관련 프로그래밍 할 때 자주 사용함

Python의 OpenCV는 사실 C++ 라이브러리가 내장된 것임.

무슨 말이냐면, Python에서 OpenCV 함수를 호출하면, 해당되는 C++ OpenCV 함수가 호출되는 것임.

Python으로 작성한 이미지 프로그램과 C++로 동일한 것을 작성한 것 중 어느 것이 빠를까 하는 질문이 생긴다면,

당연히도, C++로 작성한 것이 조금 더 빠르지 않을까 예상하게 됨.

Python에서 C++ 라이브러리를 호출하는 오버헤드를 고려하면 당연한 예상임.

그런데, 예상이 늘 맞지는 않음.

간단한 예제 프로그램으로 실험을 해 보았음.

이것은 이미지 한 장을 읽어 들인 후에, 여기에 Gaussian blurring 을 500번 반복 적용하는 것임.

Gaussian blurring은 이미지 효과 처리 방법 하나로, 이미지를 흐릿하게 (blurring) 하는 것임.

이 프로그램을 python과 C++로 작성해 보았음.


Windows 11 환경에서 Python은 Visual studio code, C++는 Visual studio 2022에서 작성하였음.

Python은 3.12 버전에 OpenCV 4.10을 pip로 설치하였음.

C++도 OpenCV 4.10 compiled version을 다운로드 받아서 설치하였음.

특히, C++는 release 모드로 컴파일 하였음.


아래는 Python으로 작성한 것임. 

import cv2
import time

# Load image
image = cv2.imread("test.jpg")

# Start timer
start = time.time()

# Apply Gaussian Blur 500 times
for _ in range(500):
    image = cv2.GaussianBlur(image, (5, 5), 0)

# End timer
end = time.time()

print(cv2.getBuildInformation())
print(f"Time elapsed: {end - start:.4f} seconds")

test.jpg 사진을 읽어들여, 반복적으로 Gaussian blurring을 500번 적용하는데 걸리는 시간을 측정하는 것임.

test.jpg 사진의 크기는 대출 1400 x 1600 정도되는 컬러 사진임.

 


아래는 동일한 일을 수행하는 C++ 프로그램임.

#include <opencv2/opencv.hpp>
#include <iostream>
#include <chrono>

using namespace std;

int main() {

    // Load image
    cv::Mat image = cv::imread("test.jpg");
    if (image.empty()) {
        std::cerr << "Image not found!" << std::endl;
        return -1;
    }

    // Start timer
    auto start = std::chrono::high_resolution_clock::now();

    // Apply Gaussian Blur 500 times
    for (int i = 0; i < 500; ++i) {
        cv::GaussianBlur(image, image, cv::Size(5, 5), 0);
    }

    // End timer
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end - start;

    std::cout << "Time elapsed: " << elapsed.count() << " seconds" << std::endl;
    return 0;
}

수행시간은 Python 0.80 ~ 0.89초, C++ 1.2~1.3초 정도가 나옴. 

Python과 C++ 둘 다 같은 컴퓨터에서 여러 번 반복 실험하였음.

예상과 다르게 Python이 더 빠름.

Blurring 반복 회수를 500번에서 5,000번으로 증가시켜면, 소요되는 시간이 10배씩 증가함

그러나, Python과 C++의 격차는 줄어들지 않음.


이유를 검색해 보았음

Python이 사용하는 opencv-python 패키지가 고도로 최적화된 C++ 라이브러리일 가능성이 높다고 함.

따라서, Python에서 C++ 호출하는 오버헤드를 상쇄시키고도 남을 수 있다고 함.


OpenCV-Python을 깔보면 안 됨. 양(Python)의 탈을 쓴 호랑이(C++)가 opencv-python 임. 

반응형
LIST






ㅇ 히스토그램 매칭 (Histogram matching)
  - 이미지의 색분포를 다른 이미지와 유사하게 하는 것을 말합니다.
  - color mapping 또는 color transfer라고도 합니다.
  - 예를 들어 아래와 같이 두 장의 이미지가 주어졌다고 합시다.
    . 왼쪽의 이미지는 바닷가를 촬영한 이미지이고,  푸른색 계통이 주요 색입니다.
    . 오른쪽은 포도밭을 촬영한 것이고, 녹색이 주요 색입니다.
    . 이제, 두 사진간에 히스토그램 매칭을 시도해 봅시다.

 


ㅇ 아래는 포도밭 이미지를 바닷가 이미지의 주요색으로 바꾼 것입니다.


ㅇ 아래 이미지는 반대로, 바닷가 이미지를 포도밭의 주요색으로 바꾼 것입니다.
  - 옥색 같았던 바다가  포도밭의 녹색과 노랑색으로 바뀌었습니다. 녹조가 생긴 것처럼 말이죠


ㅇ 또 다른 예를 보겠습니다.
  - 왼쪽은 도시야경 이미지이고, 주요색이 검정색입니다.
  - 오른쪽은 정원 이미지이고, 주요색이 녹색입니다.

   


ㅇ 히스토그램 매칭을 한 아래 이미지는
  - 정원 이미지에 도시야경의 주요 색인 검정색을 씌운 것입니다.
  - 신록으로 푸르렀던 나뭇잎들이 밤하늘 처럼 되어버렸습니다.


ㅇ 이번에는 반대의 경우입니다. 
  - 도시야경 이미지에 정원의 주요 색인 녹색을 씌운 것입니다.
  - 어두웠던 밤하늘이 녹색 계열로 바뀌었습니다. 묘한 분위기를 느낄 수 있습니다.


ㅇ 히스토그램 매칭의 원리
  - 이것은 이미지 (T)의 주요색을 다른 이미지 (R)의 주요 색로 바꾸는 것으로 
  - T의 색을 발생빈도수에 따라서 R의 색으로 바꾸는 것입니다.
  - 예를 들어, T에서 가장 많이 나타나는 색을 R에서 가장 빈도수가 높은 색으로 바꾼다고 생각하면 됩니다.
  - 마찬가지로 두 번째로 자주 나타나는 색을 R에서 빈도수가 두 번째로 높은 색으로 바꾸고,
  - 이런 식으로 빈도수의 순서에 따라서 바꿔나가면 되는 것입니다.

ㅇ 함수 LUT( )
  - OpenCV에서는 히스토그램 매칭을 쉽게 할 수 있도록  함수 LUT( )를 제공합니다.
  - 이 함수는 Lookup Table의 약자입니다.
  - 함수 원형은 아래 그림와 같습니다.
    . src는 입력이미지이고,
    . lut는 크기 256인 1차원 배열이고
    . dst는 결과이미지입니다.
    . 여기서 중요한 것은 lut인데,
    . lut[i]는 i 번째 색에 대해서 대체할 색을 의미합니다.
    . 예를 들어 설명해 보겠습니다.
    . src(x,y)의 색이 i라면, dst(x,y)의 색은 lut[i]가 되는 것입니다.
    


ㅇ 히스토그램 매칭 상세과정
  - 컬러이미지에 대해서 히스토그램 매칭을 수행하는 과정을 살펴보겠습니다.
  - 아래 소스코드에서 해당 라인 번호도 참고로 적어 놓습니다.

  - 121-133:
    . 두 장의 이미지를 입력으로 받습니다.
    . 하나는 히스토그램 매칭이 수행되어야 할 타겟 (T)이미지이고,
    . 다른 하나는 주요색을 추출할 레퍼런스 (R) 이미지 입니다.

  - 14-17:
    . T와 R 이미지 모두 컬러이미지이므로 R, G, B 채널별로 나눕니다.

  - 29-44:
    . 그리고, 채널별로 히스토그램을 구합니다. 이 때 함수 calcHist( )를 사용합니다.
    . 계산된 히스토그램에 대해서 normalize를 수행해서 최대값이 1.0이 되도록 합니다. 

  - 49-67:
    . Normalized된 히스토그램에 대해서 cumulative distribution function (CDF)를 계산합니다.
    . CDF에 대해서 다시 normalize를 수행해서 최대값이 1.0이 되도록 합니다.

  - 72-93:
    . 이것을 이용해서 Lookup table을 만드는데,
    . 빈도수 비율이 가장 유사한 색끼리 매칭한다고 보면됩니다.
    . 보다 정확하게는
    . 이미지 T 색 c의 빈도수에 해당하다는 이미지 R의 색 d를 결정해서
    . lookup 테이블 lut에 채워넣습니다.
    . 이것을 코드로 쓰면 lut[c] = d; 와 같이 됩니다.

  - 95:
    . 함수 LUT()를 이용해서 채널별로 히스토그램 매칭을 수행

  - 98: 
    . 완성된 채널을 합성해서 이미지를 만들어 내는 과정

ㅇ OpenCV 소스코드
  - 위에서 설명한 히스토그램 매칭과정을 C++ 소스코드로 구현한 것입니다.

  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
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

// reference와 target은 변경시키지 않을 것이므로 const
// result는 변경결과를 담는다.
void histMatch(const Mat &reference, const Mat &target, Mat &result)
{
	const float HISTMATCH = 0.000001;  // threshold of histogram difference
	double min, max;

	vector<Mat> ref_channels;  // 벤치마킹 이미지를 색채널별로 분리해서 저장
	split(reference, ref_channels);
	vector<Mat> tgt_channels;  // 색변경 대상 이미지를 채널별로 분리
	split(target, tgt_channels);
	
	int histSize = 256;  // histogram을 256 level로 작성
	float range[] = { 0, 256 };  // 최소와 최대 bin
	const float *histRange = { range };
	bool uniform = true;

	for (int i = 0; i < 3; i++)
	{
		Mat ref_hist, tgt_hist;  // 생성된 히스토그램 저장
		Mat ref_hist_accum, tgt_hist_accum;  // 히스토그램의 CDF계산

		calcHist(&ref_channels[i], 1, 0, Mat(), ref_hist, 1, &histSize, &histRange, uniform, false);
		calcHist(&tgt_channels[i], 1, 0, Mat(), tgt_hist, 1, &histSize, &histRange, uniform, false);

		minMaxLoc(ref_hist, &min, &max);  // 히스토그램에서 최소값과 최대값을 구한다.
		if (max == 0)
		{
			cout << "ERROR: max is 0 in ref_hist" << endl;
		}
		normalize(ref_hist, ref_hist, min / max, 1.0, NORM_MINMAX);  // 히스토그램 값을 0 ~ 1까지로 normalize한다.

		minMaxLoc(tgt_hist, &min, &max);  // 히스토그램에서 최소값과 최대값을 구한다.
		if (max == 0)
		{
			cout << "ERROR: max is 0 in tgt_hist" << endl;
		}
		normalize(tgt_hist, tgt_hist, min / max, 1.0, NORM_MINMAX);  // 히스토그램 값을 0 ~ 1까지로 normalize한다.

		//
		// CDF를 계산한다.
		//
		ref_hist.copyTo(ref_hist_accum);  // 복사본을 만든다.
		tgt_hist.copyTo(tgt_hist_accum);

		float *src_cdf_data = ref_hist_accum.ptr<float>();  // pointer로 access하면 성능을 높일 수 있다.
		float *dst_cdf_data = tgt_hist_accum.ptr<float>();

		for (int j = 1; j < 256; j++)
		{
			src_cdf_data[j] += src_cdf_data[j - 1];
			dst_cdf_data[j] += dst_cdf_data[j - 1];
		}

		//
		// 계산된 CDF를 normalize한다.
		//
		minMaxLoc(ref_hist_accum, &min, &max);
		normalize(ref_hist_accum, ref_hist_accum, min / max, 1.0, NORM_MINMAX);
		minMaxLoc(tgt_hist_accum, &min, &max);
		normalize(tgt_hist_accum, tgt_hist_accum, min / max, 1.0, NORM_MINMAX);

		// 
		// Histogram matching을 수행
		//
		Mat lut(1, 256, CV_8UC1);  // Lookup table을 만든다.
		uchar *M = lut.ptr<uchar>();
		uchar last = 0;
		for (int j = 0; j < tgt_hist_accum.rows; j++)
		{
			float F1 = dst_cdf_data[j];

			//
			// 벤치마킹이미지에서 유사한 CDF 값을 갖는 픽셀 intensity를 찾는다.
			//
			for (uchar k = last; k < ref_hist_accum.rows; k++)
			{
				float F2 = src_cdf_data[k];
				if (abs(F2 - F1) < HISTMATCH || F2 > F1)  // 유사한 CDF이거나, 
				{
					M[j] = k;  // 변경대상 이미지의 intensity j는 intensity k로 변환
					last = k;  // 다음 검색을 시작할 위치
					break;  // 다음 intensity로
				}

			}
		}

		LUT(tgt_channels[i], lut, tgt_channels[i]);  // Lookup table을 이용한 색깔 변화
	}  // end of for

	merge(tgt_channels, result);  // 3개 채널들을 합쳐서 이미지를 재생성

}

// test 1
#define REF_IMG "img_osaka_night.jpg"
#define TGT_IMG "img2_garden.jpg"

// test 2
//#define REF_IMG "img2_garden.jpg"
//#define TGT_IMG "img_osaka_night.jpg"

// test 3
//#define REF_IMG "img2_beach.jpg"
//#define TGT_IMG "img2_grapefarm.jpg"

// test 4
//#define REF_IMG "img2_grapefarm.jpg"
//#define TGT_IMG "img2_beach.jpg"

int main(int argc, char** argv)
{

	Mat ref = imread(REF_IMG, IMREAD_COLOR); 	// 색깔을 벤치마킹할 이미지, BGR 포맷으로 읽는다.
	if (ref.empty() == true)
	{
		cout << "Unable to read reference image" << endl;
		return -1;
	}
	Mat tgt = imread(TGT_IMG, IMREAD_COLOR); 	// 색깔을 변경할 이미지
	if (tgt.empty() == true)
	{
		cout << "Unable to read target image" << endl;
		return -1;
	}
	Mat dst = tgt.clone();  // 색깔 변경 결과를 담을 이미지

	namedWindow("Reference", WINDOW_KEEPRATIO);  // 벤치마킹 이미지 표시창, 비율따라 크기 조정
	namedWindow("Target", WINDOW_KEEPRATIO); // 변경대상 이미지
	namedWindow("Result", WINDOW_KEEPRATIO);  // 변경결과를 담을 이미지
	
	imshow("Reference", ref);
	imshow("Target", tgt);

	histMatch(ref, tgt, dst);
	imshow("Result", dst);

	waitKey();
	return 0;
}









반응형
LIST


ㅇ Brightness(밝기)와 Contrast조절

ㅇ 영상처리 기술들 중에서 간단하면서도 직관적인 것이 brightness와 contrast 조절입니다.
  - 우선 둘 중에 직관적으로 이해하기 쉬운 brightness부터 살펴 보겠습니다.

ㅇ Brightness는 영상을 밝기를 뜻합니다.
  - 밝기를 조절하기 위해서는 픽셀의 intensity를 바꾸면 됩니다.
  - Intensity값을 증가시키면, 영상이 밝아집니다.
  - 반대로 감소시키면, 어두워집니다.

ㅇ Contrast는 영상에서 밝은 부분과 어두운 부분의 차이를 의미합니다.
  - 방금 설명했던 brightness보다는 직관적으로 이해가 하기가 쉽지 않을 겁니다.
  - 예를 들어 보겠습니다.
  - 이미지 안의 대부분 픽셀들의 intensity가 비슷하다면,
  - 어떤 물체가 있는지 쉽게 알 수가 없습니다.
  - 이런 경우가 contrast가 낮은 것입니다.
  - 반대로, 이것이 높다는 것은 픽셀들간의 Intensity 차이가 많이 난다는 겁니다.
  - 이 경우, 물체의 모양이나 윤곽을 한 눈에 알아볼 수 있을 겁니다.
  - 해가 비치는 부분과 그림자 진 부분을 쉽게 구분할 수 있는 것 처럼 말입니다.

ㅇ 그러면, brightness 조절은 어떻게 할까요?
  - 쉽습니다. 모든 픽셀들의 intensity에 모두 같은 값만큼 더하거나 빼면 됩니다.
  - 더하면 이미지가 환해져서 brightness가 증가됩니다.
  - 빼는 것은 더하는 것의 반대 경우이구요.

ㅇ Brightness를 조절한 이미지의 예를 보겠습니다.
  - 우선 아래 그림은 조절 전의 이미지입니다.
  - 이미지가 두 구역으로 나뉘어져 있고, 왼쪽 편 픽셀들의 intensity가 오른편보다 큰것을 알 수 있습니다.

- 입력이미지


  - Intensity에 같은 양수를 더해 brightness를 증가시킨 결과입니다.
  - 왼편은 거의 흰색에 가까울 정도로 밝아졌고,
  - 오른편도 그만큼 밝아졌습니다.
  - 이렇듯 brightness 조절은 쉽습니다.
- brightness를 증가한 경우
- 좌측과 우측 모두 환해진 것을 알 수 있다.


ㅇ Contrast 증가의 예를 살펴보겠습니다.
  - 동일한 입력이미지를 사용했을 때, 
  - 왼편은 흰색으로 매우 밝아졌지만,
  - 오른편은 별 차이가 없습니다.
  - 대신 왼쪽과 오른쪽의 intensity 차이가 분명해져서,  선명해졌습니다.

- contrast를 증가한 경우,
- 좌측은 환해졌지만, 오른쪽은 원래보다 더 어두워졌다.
- 밝았던 부분은 더 밝아지고, 
- 어두웠던 부분은 더 어두워지기 때문에,
- 두 부분간의 대비 (contrast)가 증가했다.


ㅇ Contrast 증가의 원리
  - 이것은 밝은 부분과 어두운 부분의 차이를 크게 하는 것을 목적으로 하여,
  - 핵심은 곱하기입니다.
  - 즉, intensity에 값을 곱하는 겁니다.
  - 왜 이러한 곱셈이 contrast를 증가시키는지 알아보겠습니다.
  - 예를 들어, 두 개의 픽셀 A, B가 있고, 
  - 각각의 intensity A = 100, B = 50 라고 하겠습니다.
  - Contrast조절을 위해 두 픽셀에 같은 1.5를 곱한다고 하면
  - A = 150, B= 75이 됩니다.
  - 곱셈에 의해 A는 50만큼 증가하였고, B는 25만큼 증가하였습니다.
  - 이로 인해 밝은 부분인 A가 B보다 더 환해졌기 때문에, A와 B간의 차이가 증가하여 contrast가 증가됩니다.
  -자세히 살펴보면, 조절전에는 두 픽셀간의 intensity차이 (A - B)가 50 = (100- 50)이었다면,
  - 조절후에는 75로 1.5배 만큼 늘어났습니다.


ㅇ 실제 이미지를 이용해서 brightness와 contrast 조절 실험을 진행해 보겠습니다.
  - 입력이미지는 아래와 같습니다.
- 도시의 야경을 gray로 촬영한 입력이미지 


  - Brightness를 증가시켜보겠습니다.
- Brightness를 증가시키면
- 이미지가 전체적으로 밝아진다.
- 이는 모든 픽셀의 intensity값이 증가했기 때문입니다.


  - 위의 실험에서 조절 전후 이미지의 히스토그램이 어떻게 변하는지 살펴보겠습니다.
  - 이것을 보면, brightness 조절이 어떤 역할을 하는지 보다 정확히 알 수 있습니다.
- 아래 그림에서
- 1번 히스토그램은 입력이미지의 히스토그램이고,
- 2번은 brightness 증가 후의 것입니다.
- 봉우리가 오른쪽으로 이동한 것이 관찰되는데,
- 픽셀들의 intensity가 전반적으로 증가했기 때문입니다.


  - 이번에는 contrast를 증가시켜보겠습니다.
- Contrast가 증가되면,
- 어두웠던 부분은 더 어두워지고,
- 밝았던 부분은 더 밝아지면서
- 두 부분이 좀 더 극명하게 대비되는 효과를 볼 수 있습니다.


  - Contrast 변화가 히스토그램에 어떠한 영향을 끼치는지 살펴보겠습니다.
- 아래 히스토그램들은
- 1번은 입력이미지, 2번은 contrast 증가 후의 것입니다.
- 2번에서 어두운 픽셀의 개수가 크게 증가하였다는 것을 볼 수 있다.
- 밝은 픽셀의 수는 별로 증가하지 않았는데,
- 이는 contrast 적용공식의 작동 방식 때문입니다.
- 여기에 대해서 좀 더 자세히 알아보겠습니다.


ㅇ Contrast와 Brightness를 조절하는 공식
  - 위의 예에서 보았듯이,
  - 조절하는 공식의 기본 아이디어는 
  - brightness의 경우는 픽셀 intensity를 전체적으로 증가 혹은 감소하는 것이고,
  - contrast는 intensity에 비례하여 값을 변화시키는 것입니다.
  - 이것을 수학공식으로 표현하면 다음과 같습니다.

- x가 픽셀의 intensity라고 할 때
- 1) contrast를 조절하기 위해서는 적절한 alpha를 계산하여 곱한다.
- 2) brightness를 조절하기 위해서는 beta를 더한다.
- 그렇다면, alpha와 beta는 어떻게 계산하는 것일까요?


ㅇ alpha와 beta의 결정
  - 우선 contrast와 brightness는 둘 다 1 ~ 200까지 변동가능하다고 가정하겠습니다.
  - 그리고 100은 입력이미지 상태그대로를 의미한다고 하겠습니다. 
  - 따라서, < 100 이면, contrast와 brightness가 감소되는 것을 의미하고,
  - > 100이면, 증가하는 것을 의미합니다.
  - 아래 코드는 alpha (=a)와 beta (=b)를 결정하는 소스코드입니다. 


ㅇ Brightness 조절에 따른 alpha와 beta값의 변화
  - brightness를 1부터 200까지 조절할 때, alpha와 beta가 어떻게 변하는지 그래프를 그려보겠습니다.
  - (brightness == 100)은 조절하지 않음.
  - (brightness > 100)은 이미지를 환하게
  - (brightness < 100)은 이미지를 어둡게
  - 아래 그래프에서 x축은 brightness (1 ~ 200)을 나타내고,
  - y축은 alpha와 beta값입니다.

- brightness 조절은 alpha값을 변화시키지는 않습니다.
- alpha = 1을 유지합니다.



- beta값은 brightness에 정비례하여 증가 혹은 감소합니다.


ㅇ Contrast 변화에 따른 alpha와 beta의 조절
  - 이번에는 brightness는 100으로 고정시키고 ( 변경시키지 않는다는 뜻입니다.),
  - contrast를 1 ~ 200까지 변화시키면서, alpha와 beta가 어떻게 변하는지 보겠습니다.
  - 실제 그래프에서는 1 ~ 180까지만 표시했습니다.
  - 이번에도 contrast = 100은 변화가 없다는 것입니다.
  - x축은 contrast의 값, y축은 alpah와 beta값입니다.

- Contrast가 증가할 수록
- alpha값이 빠르게 증가합니다.


- beta값은 contrast와는 반대로 움직입니다.
- 이것은 alpha값에 의한 변화를 보상하기 위해서입니다.
- 예를 들어, contrast > 100인 경우,
- alpha값이 1보다 커져서, intensity에 곱해서 나오는 결과도 커집니다.
- 따라서, beta는 음수가 되어 이 결과를 조금 작게 할 필요가 있습니다.
- contrast < 100인 경우는 반대로 이해하면 되겠습니다.


ㅇ 위그래프를 생성해내기 위한 MATLAB 소스코드

ㅇ 구현
  - Brightness와 contrast 이미지 처리기술을 OpenCV를 이용해서 구현해 보겠습니다.
  - 앞서 설명했듯이 픽셀 intensity에 alpha를 곱하고, beta를 더하는 연산이 필요합니다.
  - 이러한 연산을 편하게 할 수 있는 함수를 OpenCV가 제공하는데,
  - Mat 클래스의 메소드인 convertTo( )입니다.

- convertTo 메소드는, 각 픽셀에 곱할 alpha와 더할 beta를 인수로 받습니다.
- 인수 m은 계산결과를 담은 Mat 객체입니다.
- 이 함수를 이용하면 위에서 설명한 조절을 쉽게 구현할 수 있습니다.



ㅇ Brightness와 Contrast 이미지 프로세싱 C++ 구현 코드 
  - 슬라이드바 (Slide bar)를 이용해서 brightness와 contrast를 조절하는 프로그램을 짜 보았습니다.
  - 각각의 조절에 따른 이미지 변화를 관찰할 수 있는 편리한 프로그램입니다. 

  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
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;

//
// 슬라이드 바의 위치에 해당하는 값
// 최소 0에서 최대 200까지 움직인다.
//
int init_brightness = 100;
int init_contrast = 100;

Mat image;

const char* keys = { "{@imageName1| img_osaka_night.jpg |input image file} \\
{@imageName2| fruits.jpg|input image file} \\
{N | 1.0 | this is double value} \\
{help h ? | | this is help}" };


void updateBrightnessContrast(int, void*)
{
	int histSize = 255;
	int var_brightness = init_brightness - 100;
	int var_contrast = init_contrast - 100;

	double a, b;
	if (var_contrast > 0)
	{
		double delta = 127.0*var_contrast / 100;
		a = 255.0 / (255.0 - delta * 2);
		b = a*(var_brightness - delta);
	}
	else
	{
		double delta = -128.0*var_contrast / 100;
		a = (256.0 - delta * 2) / 255.0;
		b = a*var_brightness + delta;
	}

	cout << "a: " << a << " b: " << b << endl;

	Mat dst, hist;
	//
	// image의 각 픽셀값을 변경하여, dst를 만들어 화면에 표시
	// pixel =  a*pixel + b
	//
	image.convertTo(dst, CV_8U, a, b);
	imshow("image", dst);

	// dst의 히스토그램을 계산한다.
	calcHist(&dst, 1, 0, Mat(), hist, 1, &histSize, 0);

	Mat histImage = Mat::ones(200, 320, CV_8U) * 255;  // 320 * 200의 흰색이미지를 만든다.

	normalize(hist, hist, 0, histImage.rows, CV_MINMAX, CV_32F);
	
	int binW = cvRound((double)histImage.cols / histSize);

	for (int i = 0; i < histSize; i++)
	{
		Point leftUp = Point(i*binW, histImage.rows);
		Point rightDown = Point((i + 1)*binW, histImage.rows - cvRound(hist.at<float>(i)));
		rectangle(histImage, leftUp, rightDown, Scalar::all(0), -1, 8, 0);
		imshow("histogram", histImage);
	}
}


/*
   read in an image as a gray
*/
int main(int argc, char** argv)
{
	CommandLineParser parser(argc, argv, String(keys));
	parser.about("This is test program for commandlineparser of opencv");
	string inputImage1 = parser.get<string>("@imageName1");

	image = imread(inputImage1, IMREAD_GRAYSCALE);
	if (image.empty() == true)
	{
		cout << "Unable to read the image " << inputImage1 << "." << endl;
		return -1;
	}
	namedWindow("image", WINDOW_KEEPRATIO);
	namedWindow("histogram", WINDOW_KEEPRATIO);

	createTrackbar("brightness", "image", &init_brightness, 200, updateBrightnessContrast);
	createTrackbar("contrast", "image", &init_contrast, 200, updateBrightnessContrast);

	// 이 함수를 호출해야
	// 이미지를 화면에 표시할 수 있다.
	//
	updateBrightnessContrast(0, 0);

	waitKey();
	return 0;
}














반응형
LIST






ㅇ 명령행 인수 (Command line argument)


  - 예를 들어, 어떤 프로그램이 실행될 때 사용자가 파일 이름을 주면
  - 그 파일을 열어 작업한다고 가정하자. 
  - 이름을 넘겨주는 가장 쉬운 방법은 파일 이름을 소스코드에 하드코딩하는 것인데,
  - 단점은 이름을 바꿀 때 마다 프로그램 빌드를 다시 해야 하기 때문에 불편하다. 
  - 더 쉬운 방법은 실행할 때마다 '인수 (argument)'로 주는 것이다.
  - 예를 들어 'text.exe'라는 프로그램이 있다고 하고, 실행할 때, 아래와 같이 입력하여 인수를 넘겨줄 수 있다. 

>> test.exe image1 image2 -help -N=10.0

  - 위 명령어 라인을 하나하나 분석해 보면,
  - 실행파일 이름은 test.exe이고,
  - 여기에 인수로 image1, image2, -help, -N=10.0 등을 넘겨준다는 뜻이다.
  - 이렇게 실행명령과 함께 주어지는 인수들을 '명령행 인수 (command line argument)'라고 한다.
  - 그러면 프로그램은 이들 인수들을 인식해서,
  - 첫 번째와 두 번째 인수는 각각 image1과 image2이고,
  - -help라는 인수가 주어졌으며,
  - -N 인수로 지정되는 값은 10.0이라는 것을 알아햐 한다. 
  - 이러한 인식을 프로그래밍하는 것은 어렵지는 않지만 문자열 처리를 해야해서 귀찮기만 한다.
  - 하지만, 희소식이 있다.

ㅇ OpenCV에서는 이러한 처리를 위해 CommandLineParser 클래스를 제공하는데,

  - 사용하는 법은, 이 클래스에 미리 어떠한 인수들이 들어올 수 있는지를 알려주면
  - 실제로 입력된 인수들 중에 해당하는 것들을 추출해 준다.
  - 우선, 가능한 인수들이 어떤 것들인지 지정하는 방식에 대해서는 아래 설명을 보자

ㅇ 인수처리를 지정하는 방법

  - 아래 설명하는 포맷에 따라 지정하는데, 다소 복잡하니, 
  - 우선 예부터 살펴보자.
"{@imageName1| fruits.jpg|input image file} \\
{@imageName2| fruits.jpg|input image file} \\
{N count |1.0|this is double value} \\
{help h ?||this is help}"
  - @imageName1과 @imageName2는 첫 번째와 두 번째 입력되는 인수와 매칭되며,
  - 만약 입력이 안되면 fruits.jpg의 값을 default로 갖는다.
  - 그리고 'input image file'은 이 인수에 대한 설명으로 help 메시지에 의해 출력된다.
  - 인수 N과 count는 서로 같은 것이고, 
  - 명령행에서 입력될 때는 앞에 dash ('-')를 반드시 붙여서, -N=1.0 혹은 -count=1.0과 같이 입력해야 한다.
  - 특히, 여기서는 -N (혹은 -count)가 입력되지 않아도 -N=1.0이 입력된 것으로 처리된다.
  - 인수 help, h, ?는 help message 출력과 관계가 있고, 
  - 명령행에서  -help, -h, -?는 모두 동일하며, 
  - 이 인수의 경우 default value가 없으나, 
  - 이 인수가 입력되면  자동으로 생성되는 인수별 설명을 출력하는 함수 printMessage()를 출력하는 것이 일반적이다.
  - 이러한 help인수가 입력되었는지를 알기 위해서는 함수 has()를 이용하여
  - 예를 들어, has("help"), has("h"), has("?") 의 반환값이 true인지를 확인하면 된다.

ㅇ 인수처리 지정방법 

  - 이제 포맷을 살펴보면서 전체적으로 이해해 보자
  - 지정내용은 큰 따옴표 (")로 둘러쳐진 문자열로 표현되는데,
  - 각 인수들은 중괄호 ({  })로 구분된다.
  - 그리고 중괄호 내부는 vertical bar(|)에 의해 3부분으로 구분되는데,
  - 첫 번째 부분은 '인수 이름', 두 번째 부분은 'default value',  세 번째 부분은 설명이다.
  - default value라는 것은 인수를 입력하지 않았을 경우에 그 인수의 값이 된다.
  - 설명이라는 것은, 그 인수에 대한 설명으로 이것은 help 메시지에 의해 자동 출력된다.
  - 인수이름 중에서 @으로 시작하는 것은 인수 위치와 관련된 것으로
  - 가장 앞에 있는 인수는 첫 번째 나타나느 @이름의 인수가 되는 것이다.
  - 인수이름에 여러 개의 이름을 공백으로 구분하여 입력할 수 있는데, 이들은 모두 동일한 인수를 가리킨다. 
  - 그리고 @이름이 아닌 인수이름은 명령행에서 입력할 때 -이름으로 입력된다.
  - 인수지정의 한 예는 아래 그림과 같다.

ㅇ CommandLineParser 클래스의 메소드들

  - 지정된 인수내용에 따라 보다 쉽게 인수들을 구분해 낼 수 있고,
  - 보기 편한 help message를 자동생성하기 위해서,
  - 이 클래스는 다음과 같은 메소드들을 제공한다.
  - help message는 인수사용방법을 설명하는 출력문을 말한다.
  - 이에 대해서는 앞에서 간략히 설명했었고, 뒤의 예에서는 실제 help message가 어떻게 나오는지 볼 수 있다.
  - 우선 help message 생성에 관한 메소드들부터 살펴보면,

ㅇ 메소드 about(string msg) of CommandLineParser

  - 이 메소드를 이용하면, 
  - help 메시지를 출력할 때 보이는 초반부의 설명 메시지를 설정할 수 있으며,
  - 이것은 아래 실행 예에 나와 있다.

ㅇ 메소드 printMessage() of CommandLineParser

  - 각 인수에 대한 설명을 자동으로 생성하여 출력하는 메소드로,
  - 각 인수별로 지정한 설명들과 default value가 설명에 포함된다. 

ㅇ 메소드 get<type>() of CommandLineParser 

  - 입력된 인수 중에서 특정 인수값을 추출할 때 사용되며
  - 예를 들어, get<double>("N")은
  - -N=100.0 이라면,  인수 N에 의해 설정된 인수 100.0을 반환한다.
  - '<double>'은 추출된 값의 data type을 말한다.
  - 요약하자면, 이 메소드는 명령행에 입력된 문자열에서 인수들의 값을 추출하는 역할을 하게 된다.

ㅇ 예제 프로그램

   - 명령행에서 다음과 같은 인수들을 입력했다고 가정하자

- 1) 첫 번째 인수이고 값은 test.jpg
- 2) 두 번째 인수이고, 값은 test2.jpg
- 3) 세 번째 인수는 --help 로 이는 -help와 같다.

  
  - 이러한 인수를 처리하는 프로그램은 아래와 같다.
    . line 12- 15: 처리하는 인수에 대한 지정 문자열
 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
32
33
34
35
36
37
38
39
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int init_brightness = 100;
int init_contrast = 100;

Mat image;

const char* keys = { "{@imageName1| fruits.jpg|input image file} \\
{@imageName2| fruits.jpg|input image file} \\
{N | 1.0 | this is double value} \\
{help h ? | | this is help}" };


int main(int argc, char** argv)
{
	CommandLineParser parser(argc, argv, String(keys));
	parser.about("This is test program for commandlineparser of opencv by kyungkoo jun");
	string inputImage1 = parser.get<string>("@imageName1");
	string inputImage2 = parser.get<string>("@imageName2");

	cout << "argument1 is : " << inputImage1 << endl;
	cout << "argument2 is : " << inputImage2 << endl;
	cout << "argument N is : " << parser.get<double>("N") << endl;

	if (parser.has("help") == true)
	{
		cout << "have help" << endl;
		parser.printMessage();
	}
	else
	{
		cout << "no help" << endl;
	}
	return 0;
}

ㅇ 위 프로그램의 실행결과
- 1) line 22, 23에서 추출한 두 개의 인수를 출력
- 2) 인수 N은 입력되지 않았지만, default value로 1이 지정
- 3) --help가 입력되었고 (line 29)
- 4) 자동생성되는 help 메시지는 각 인수별 설명을 자동으로 만들어 낸다.
특히, 메시지의 초반부에는 line 21에서 메소드 about()로 설정한 문자열이 출력된다.







반응형
LIST





ㅇ OpenCV에서 제공하는 함수 중에 normalize( )가 있다.
  - 이 함수는 값들을 새로운 범위로 변환해주는 역할을 한다.
  - 예를 들어, 원래 값들이 1부터 10부터 사이에 있다면, 
  - 이들을 0부터 1사이 값들로 변환한다.
  - 변환 값들은 원래 값에 비례한 크기를 갖게 된다.

ㅇ 아래 프로그램은 원래 값들이 2 ~ 7일 때, 이를 1 ~ 10사이의 값으로 변환하는 예이다.
- 1) 실수형 값 10개가 주어졌을 때 이를 Mat로 만든다.
- 2) normalize함수의 인수로 입력값을 의미
- 3) 출력값을 의미
- 4) 변환방식이 NORM_MINMAX일 때, 변환 최소값
- 5) 변환 최대값
- 6) 변환 방식을 의미한다.


ㅇ 함수 normalize에 의해 변환된 결과는 아래와 같다.
- 변환 전의 원래 데이터이다
- 값들이 2 ~ 7까지의 값들을 갖는다.

- 변환범위가 0 ~ 1일 때의 결과이다.
- 원래 값에서 최소값이었던 2가 변환 후에는 0으로, 
- 최대값이었던 7이 1로 변환된 것을 볼 수 있다.

- 변환범위가 1 ~ 10일 때의 결과이다.
- 원래 값에서 최소값이었던 2가 변환 후에는 1으로, 
- 최대값이었던 7이 10로 변환된 것을 볼 수 있다.


ㅇ normalize를 이용한 예제 프로그래이다.
 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
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{
	float data[10] = { 3, 4, 5, 6, 7, 3, 4, 2, 5, 3 };

	Mat m(1, 10, CV_32F, data);

	normalize(m, m, 1.0, 10.0, NORM_MINMAX);

	for (int r = 0; r < m.rows; r++)
	{
		for (int c = 0; c < m.cols; c++)
		{
			printf("%.2f ", m.at<float>(r, c));
		}
	}
	cout << endl;
	
	return 0;
}

  - line 9-11: 1차원배열을 이용하여 Mat 값을 만든다
  - line 13: normalize 수행
  - line 15-19: 변환 후의 값을 출력한다

ㅇ normalize()의 간단한 용도
  - Gray image의 픽셀값의 범위를 조절하여, 이미지를 전체적으로 밝게 할 수 있다.
  - 아래 야경이미지는 전체적으로 어둡다.
  - normalize를 이용해서 픽셀들의 값을 128 ~ 255로 바꾸면, 이미지 전체가 환해지도록 할 수 있다.

- 함수 normalize()를 이용하여
- 위 이미지의 픽셀값들을 128 ~ 255로 변환한 결과


ㅇ 위 결과 관련한 소스코드

 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
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;


int main(int argc, char** argv)
{

	Mat gimage, nimage;
	Mat src = imread("img_osaka_night.jpg");
	resize(src, src, Size(), 0.15, 0.15);

	cvtColor(src, gimage, CV_BGR2GRAY); 

	normalize(gimage, nimage, 128, 255, NORM_MINMAX);

	imshow("Original", gimage);
	imshow("Normalized:128-255", nimage);

	cout << ("Press any key to exit...\n");
	waitKey();

	return 0;
}

  - line 17: 픽셀값을 128 ~ 255로 변환하기 때문에, 전체적으로 환해진다.



반응형
LIST





ㅇ OpenCV (3.2.0)을 이용한 파노라마 생성

  - OpenCV에서는 여러 장의 사진을 합쳐서 하나의 파노라마 사진을 만들어내기 위한
  - Stitcher 클래스를 제공한다.
  - 이 클래스는 사진을 하나로 합치는 옵션을 2개 제공하는데,
  - SCANS와 PANORAMA이다.

ㅇ 다음 3장의 사진들이 주어졌을 때,


ㅇSCANS 모드로 파노라마를 만들면, 
 


ㅇ PANORAMA 모드로 만들면,


ㅇ C++ 구현 소스코드

#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <opencv2/stitching.hpp>

int main()
{
     // 파노라마를 구성하는 사진들을 읽어서 vector에 저장한다.
     vector<Mat> imgs;

     // 3장의 이미지 이름들. sIMG_4939, ..., sIMG_4941
     int img_start_num = 4939;
     int img_end_num = 4941;

     // 파일들을 읽어서 이미지 벡터에 저장
     for (int i = img_start_num; i <= img_end_num; i++)
     {
          string _fName = "sIMG_";
          _fName = "panorama_sea/"+_fName + to_string(i) + ".jpg";
          Mat _img = imread(_fName.c_str());

          if (_img.empty() == true)
          {
               cout << "unable to read " << _fName << endl;
               return -1;
          }
          imgs.push_back(_img);
     }

     Mat pano; // 파노라마 결과
   
     // 파노라마 이미지 생성 
     Ptr<Stitcher> stitcher = Stitcher::create(Stitcher::PANORAMA, false);
     Stitcher::Status status = stitcher->stitch(imgs, pano);

     if (status != Stitcher::OK)
     {
          cout << "Can't stitch images, error code = " << int(status) << endl;
          return -1;
     }

     // 결과를 파일로 저장
     imwrite("panorama_320_scans.jpg", pano);
     cout << "Panorama creation ended successfully." << endl;
     return 0;
}



반응형
LIST

'Computer Vision by OpenCV' 카테고리의 다른 글

Histogram Equalization의 원리  (0) 2017.04.11
OpenCV normalize  (0) 2017.04.07
OpenCV Mat 이미지 형식 출력하기  (0) 2017.01.14
Connected Component Analysis  (1) 2016.07.11
Mathematical morphology (모폴로지 연산)  (0) 2016.07.11

Histogram을 이용한 이미지 유사도 측정: 

Evaluating image similarity using histogram


히스토그램 (histogram)을 이용하면 사진들이 서로 얼마나 비슷한지 측정할 수 있다. 히스토그램간의 유사도를 측정하면 된다. OpenCV는 이러한 측정을 위해 함수 compareHist( )를 제공한다. 비교대상인 두 개의 히스토그램을 인자로 전달하면 유사도를 수치로 반환한다. 비교방식은 7가지가 있는데, 주로 사용되는 것은 correlation, chi-square, intersection과 Bhattachayya가 있다.


 

예를 들어 아래와 같이 다섯 장의 사진 (위에 있는 것이 이름)이 주어졌을 때, 각 사진간의 유사도를 측정해 보자.


img_dotonbori.jpg

img_eiffel.jpg

img2_beach.jpg

img2_garden.jpg

img2_grapefarm.jpg


우선 결과부터 제시하면 아래와 같이 img2_garden(위에서부터 4번째)과 img2_grapefarm(마지막 사진) 이 사용한 4가지 방법 모두에서 가장 유사한 사진으로 나온다.



이러한 비교에 사용된 프로그램은 아래와 같다.

순서도

  1. 비교대상 사진 5장을 읽어들이면서 HLS format으로 전환한다. (24~32)

  2. 각 HLS사진에 대해서 3차원 histogram을 구한다. (40 ~ 52)

  3. 각 histogram끼리 유사도를 계산한다. (54 ~ 62)


 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

#define SCALE 0.2
#define NUM 5
#define BINS 8

int main(int argc, char** argv)
{
	// the names of images to be compared each other
	string imgNames[NUM] = { "img_dotonbori.jpg", "img_eiffel.jpg", "img2_beach.jpg", "img2_garden.jpg", "img2_grapefarm.jpg" };

	//for (int i = 0; i < NUM; i++)
	//{
	//	cout << imgNames[i] << endl;
	//}

	// read all images and convert to HLS format
	Mat imgs[NUM];
	Mat imgsHLS[NUM];
	for (int i = 0; i < NUM; i++)
	{
		imgs[i] = imread(imgNames[i], IMREAD_COLOR);
		if (imgs[i].data == 0)
		{
			cout << "Unable to read " << imgNames[i] << endl;
			return 0;
		}
		cvtColor(imgs[i], imgsHLS[i], COLOR_BGR2HLS);
	}

	//cout << "Succeeded to read all images" << endl;

	// compute 3D histogram
	Mat histogram[NUM];

	int channel_numbers[] = { 0, 1, 2 };
	for (int i = 0; i < NUM; i++)
	{
		int* number_bins = new int[imgsHLS[i].channels()];
		for (int ch = 0; ch < imgsHLS[i].channels(); ch++)
		{
			number_bins[ch] = BINS;
		}
		float ch_range[] = { 0.0, 255.0 };
		const float *channel_ranges[] = { ch_range, ch_range, ch_range };
		calcHist(&imgsHLS[i], 1, channel_numbers, Mat(), histogram[i], imgsHLS[i].channels(), number_bins, channel_ranges);
		normalize(histogram[i], histogram[i], 1.0);
	}

	cout << "Image Comparison by HISTCMP_CORREL   " << endl;
	for (int i = 0; i < NUM; i++)
	{
		for (int j = i + 1; j < NUM; j++)
		{
			double matching_score = compareHist(histogram[i], histogram[j], HISTCMP_CORREL);
			cout << imgNames[i] << "-" << imgNames[j] << ", " << 1matching_score << endl;
		}
	}
	return 0;
}


반응형
LIST



Histogram equalization


영상처리기법 중에 histogram equalization이라는 것이 있다. Histogram은 <<여기>>에서 자세히 설명하고 있다. Equalization이라는 것은 특정값을 가진 픽셀들이 너무 많지 않도록 골고루 퍼트리는 기술이다.  비유를 들자면, 서울에 거주하는 사람이 너무 많으면, 서울과 수도권으로 사람들을 이주시키는 식이다. 자세한 원리는 이 <<블로그를>> 참조하면 된다.




Histogram equalization을 하게 되면, 단일 색으로 보이던 부분들이 구분되어 보여지는 효과가 있다. 예를 들어, 아래 사진을 보자. 어느 도시의 야경을 촬영한 것이다. 왼쪽은 원래 영상이고, 오른쪽이 equalization이 후의 영상이다. 차이점은 밤하늘이 전체적인 검은색 일변도에서 여러 가지 단계를 가진 검정색으로 보이게 된 것이다.




Luminance Histogram Equalization


특히, 영상의 밝기 (Luminance)를 equalization하면 안 보이던 부분들이 아주 잘 보이게 된다. 예를 들어 아래 사진을 보자. 왼쪽 사진에서 집 입구가 전체적으로 어두운 색이서 물체들이 제대로 구분되지 않는다. 이 때  luminance를 기준으로 equalization을 하게 되면 오른쪽 결과와 같이 집 입구가 환해지는 효과를 얻을 수 있다.


이러한 환해지는 효과는 아래 사진들에서도 확인할 수 있다. 다양한 상황을 촬영한 사진들에 대해 Luminance histogram equalization을 수행한 결과들이다.




반대로 너무 환해서 잘 보이지 않았던 부분들은 Luminance histogram equalization을 하게 되면, 어두워지면서 세부특징들을 잘 관찰할 수 있다. 예를 들어 아래 사진에서 바닥의 디테일들이 아주 잘 보이는 것을 알 수 있다.



Implementation


OpenCV가 제공하는 기능들을 이용해서 luminance histogram equalization을 구현해 보자. 우선 영상을 읽어들인다. imread( )는 컬러이미지인 경우 BGR 포맷으로 읽기 때문에, 이것을 HLS 포맷으로 변환할 필요가 있다. 이를 위해 cvtColor( )함수를 사용한다. 라인 36 ~ 39은 HLS 포맷 영상에 대해서 Luminance채널에 대해서만 equalization을 수행하고, 이를 다시 HLS 영상에 넣는 과정이다. 마지막으로 HLS 포맷 영상을 BGR 포맷으로 재변환해야 한다.(라인 40) 왜냐하면 OpenCV는 BGR 영상만 표시가능하기 때문이다. 



 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

#define SCALE 0.2


int main(int argc, char** argv)
{
	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg, dispImg, disp2Img;
	Mat hlsImg;

	inputImg = imread(argv[1], IMREAD_COLOR); // read in BGR format

	if (inputImg.data == 0)
	{
		cout << "Unable to read " << argv[1] << endl;
		return 0;
	}

	// inputImg is too large to be shown. 
	// use scaled-down dispImg instead just for display
	// use inputImg for the histogram calculation
	resize(inputImg, dispImg, Size(), SCALE, SCALE, CV_INTER_AREA);

	cvtColor(dispImg, hlsImg, COLOR_BGR2HLS_FULL);

	vector<Mat> channels(hlsImg.channels());
	split(hlsImg, channels);
	equalizeHist(channels[1], channels[1]);
	merge(channels, hlsImg);
	cvtColor(hlsImg, disp2Img, COLOR_HLS2BGR_FULL);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("After luminacense equalization", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("After luminacense equalization", 120, 120);

	imshow("Original", dispImg);
	imshow("After luminacense equalization", disp2Img);

	waitKey(0);
	return 0;
}


반응형
LIST

컬러이미지를 B, G, R채널별로 히스토그램 구하기


각 채널별로 히스토그램을 별도로 계산해야 한다. Gray이미지의 채널 1개에 대해 하던 방법대로 하되, 다만 B, G, R 채널별로 동일한 작업을 반복해야 한다.


<그림 1> 컬러이미지

<그림 2> Blue 채널의 히스토그램: 하늘이 진한 파랑과 중간 파랑이 많아서 해당 부분의 빈도수가 높은 것을 볼 수 있다.

<그림 3> Green채널의 히스토그램

<그림 4> Red 채널의 히스토그램: 원본이미지의 바위색깔이 전반적으로 불그스레한 계통이므로 진한 빨간 계통의 색이 많이 분포하는 것을 볼 수 있다.


<소스코드 1> 위의 결과를 만들기 위한 소스프로그램

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

#define SCALE 0.2

int main(int argc, char** argv)
{


	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), SCALE, SCALE, CV_INTER_AREA);

	MatND histogramB, histogramG, histogramR;
	const int channel_numbersB[] = { 0 };  // Blue
	const int channel_numbersG[] = { 1 };  // Green
	const int channel_numbersR[] = { 2 };  // Red
	float channel_range[] = { 0.0, 255.0 };
	const float* channel_ranges = channel_range;
	int number_bins = 255;

	// R, G, B별로 각각 히스토그램을 계산한다.
	calcHist(&inputImg, 1, channel_numbersB, Mat(), histogramB, 1, &number_bins, &channel_ranges);
	calcHist(&inputImg, 1, channel_numbersG, Mat(), histogramG, 1, &number_bins, &channel_ranges);
	calcHist(&inputImg, 1, channel_numbersR, Mat(), histogramR, 1, &number_bins, &channel_ranges);

	// Plot the histogram
	int hist_w = 512; int hist_h = 400;
	int bin_w = cvRound((double)hist_w / number_bins);

	Mat histImageB(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
	normalize(histogramB, histogramB, 0, histImageB.rows, NORM_MINMAX, -1, Mat());

	Mat histImageG(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
	normalize(histogramG, histogramG, 0, histImageG.rows, NORM_MINMAX, -1, Mat());

	Mat histImageR(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
	normalize(histogramR, histogramR, 0, histImageR.rows, NORM_MINMAX, -1, Mat());

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

		line(histImageR, Point(bin_w*(i - 1), hist_h - cvRound(histogramR.at<float>(i - 1))),
			Point(bin_w*(i), hist_h - cvRound(histogramR.at<float>(i))),
			Scalar(0, 0, 255), 2, 8, 0);
	
	}


	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("HistogramB", CV_WINDOW_AUTOSIZE);
	namedWindow("HistogramG", CV_WINDOW_AUTOSIZE);
	namedWindow("HistogramR", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("HistogramB", 110, 110);
	moveWindow("HistogramG", 120, 120);
	moveWindow("HistogramR", 130, 130);

	imshow("Original", inputImg);
	imshow("HistogramB", histImageB);
	imshow("HistogramG", histImageG);
	imshow("HistogramR", histImageR);

	waitKey(0);
	return 0;
}



반응형
LIST


Histogram 그리기


이미지에서 픽셀들이 가지는 값들의 출현빈도를 히스토그램 (histogram)이라고 한다. 

예를 들어, gray 이미지에서 각 픽셀은 0부터 255까지의 값을 갖는다.

이미지의 크기를 300 x 300 이라고 한다면, 총 90,000개의 픽셀들을 0~255 값에 따라 분류하여

각 개별값을 갖는 픽셀들이 몇 개씩인지 알아낸 것이 히스토그램이다.


히스토그램은 contranst enhancement (안 보이는 부분을 잘 보이게) 등에 사용되는데,

이 때 사용되는 기술이 histogram equalization이다. 이것의 원리는 이 <<블로그>>를 참조한다.


OpenCV에서는 이미지의 히스토그램 계산이 쉽도록 함수 calcHist( )를 제공한다.


void cv::calcHist(const Mat * images,
int nimages,
const int * channels,
InputArray mask,
OutputArray hist,
int dims,
const int * histSize,
const float ** ranges,
bool uniform = true,
bool accumulate = false 
)



images: Histogram을 계산할 이미지들에 대한 배열이다.

nimages: images 배열에 포함된 이미지의 개수 이다.

channels: Histogram을 계산할 채널 번호들의 배열이다. 예를 들어, 아래 그림과 같이 BGR 이미지 2장에 대해, 첫 번째 이미지는 B 채널, 두 번째 이미지는 G 채널에 대해서 histogram을 구하고자 한다면 {0, 4}를 배열에 넣어서 전달해야 한다.



mask: Histogram을 계산할 영역을 지정할 수 있다. 옵션사항이고, emptry mask (즉, Mat( ))를 전달하면 아무런 동작도 하지 않는다.

hist: Histogram 계산결과를 저장한다.

dims: Histogram 계산결과를 저장한 hist의 차원을 가리킨다. 

histSize: 각 차원의 bin 개수, 즉 빈도수를 분류할 칸의 개수를 의미한다.

ranges: 각 차원의 분류 bin의 최소값과 최대값을 의미한다.


아래는 gray 이미지에 대해 픽셀값 0 ~ 255에 대해 histogram을 계산하여 그래프로 표시한 결과이다.


<그림 1> Histogram을 계산할 대상 이미지 (gray)


<그림 2> 그림 1에 대한 Histogram 계산결과를 시각화한 결과


위의 결과를 만들어 내기 위한 프로그램의 소스코드이다.


<소스코드 1> 히스토그램을 계산하고 그래프로 시각화하는 프로그램

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

#define SCALE 0.2

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat greyImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), SCALE, SCALE, CV_INTER_AREA);

	// inputImg를 gray영상(greyImg)으로 변환한다.
	cvtColor(inputImg, greyImg, CV_BGR2GRAY);

	MatND histogram;
	const int* channel_numbers = { 0 };
	float channel_range[] = { 0.0, 255.0 };
	const float* channel_ranges = channel_range;
	int number_bins = 255;

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

	// Plot the histogram
	int hist_w = 512; 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);
	}

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("Histogram", CV_WINDOW_AUTOSIZE);
	
	moveWindow("Original", 100, 100);
	moveWindow("Histogram", 120, 120);

	imshow("Original", greyImg);
	imshow("Histogram", histImage);

	waitKey(0);
	return 0;
}


반응형
LIST

Median Filtering을 이용한 잡음제거 (Noise Removal)


잡음을 없애기 위해 local averaging, gaussian smoothing 등을 사용해 보았지만 별로 신통치 않았다.

여기서는 기적처럼 작동하는 방법을 한 가지 소개해 본다. 물론 완벽하게 원래 영상을 복원하지는 못하지만, 최소한 보기 싫은 잡음들은 없앨 수 있다. 다만 사진의 sharpness (선 같은 것들이 명료하게 보이는 정도)가 훼손되기는 한다. 방법에 대해 설명하기 전에 우선 기적같은 결과부터 확인해보자.


그림 1과 2는 원본 영상과 salt and pepper 잡음으로 오염된 영상을 각각 보여준다.

그리고 그림 3은 기적같은 결과를 보여준다. Noise가 싹 다 사라졌다. 그리고 얼핏 보기에 원래 영상처럼 보인다. 하지만 자세히 들여다 보면 에펠탑의 선 같은 것들이 뭉개진 것을 알 수 있다. 


<그림 1> 원본 이미지


<그림 2> Salt & Pepper 잡음이 추가된 영상


<그림 3> Median filtering을 실시한 결과


Median Filtering

이것을 가능하게 한 방법은 주변 픽셀들의 값과 자신의 값들을 크기에 따라 정렬하고 중간값(median)을 선택해서 자신의 픽셀값으로 하는 것이다. 그래서 median filtering이라 불리운다. Noise는 주변 픽셀들과 차이가 많이 나는 값을 가지고 있으므로 local averaging 같이 단순 평균을 구하게 되면, noise에 의해 값이 왜곡되는 정도가 커서 제대로 noise 제거가 되지 않는다. 하지만, 중간값은 주변 픽셀들과 제일 유사한 값이 되기 때문에 noise를 없앨 수 있다. 대신, sharp한 선이나 edge등은 뭉개져 버리는 단점이 있다. OpenCV에서는 meidan filtering을 지원해주는 함수 medianBlur( )를 제공하고 있다.


<소스코드 1> Meidan filtering을 수행하는 코드

line 57-58이 median filtering에 해당하는 부분이고, 주변 픽셀의 범위를 3x3으로 하고 있다.

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

/*
img의 전체 픽셀중 noise_ratio 퍼센트 만큼의
픽셀을 salt & pepper noise로 바꾼다.
*/
void addSaltAndPepperNoise(Mat& img, double noise_ratio)
{
	int rows = img.rows;
	int cols = img.cols;
	int ch = img.channels();
	int num_of_noise_pixels = (int)((double)(rows * cols * ch)*noise_ratio);

	for (int i = 0; i < num_of_noise_pixels; i++)
	{
		int r = rand() % rows;  // noise로 바꿀 행을 임의로 선택
		int c = rand() % cols;  // noise로 바꿀 열을 임의로 선택
		int _ch = rand() % ch;  // noise로 바꿀 채널의 임의로 선택

		// img.ptr<uchar>(r)은 r번째 행의 첫번째 픽셀, 첫번째 채널에 대한 주소값을 반환한다.
		uchar* pixel = img.ptr<uchar>(r) +(c*ch) + _ch; // noise로 바꿀 정확한 위치를 계산

		*pixel = (rand() % 2 == 1) ? 255 : 0; // black(0) 혹은 white(255)로 교체
	}
}

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat spImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), 0.25, 0.25, CV_INTER_AREA);

	spImg = inputImg.clone();
	addSaltAndPepperNoise(spImg, 0.05);

	Mat localAvgImg;
	// Noise 제거를 위해 3 by 3 매트릭스를 이용하여
	// local averaing을 시행.
	blur(spImg, localAvgImg, Size(5, 5));  

	Mat gaussianSmoothedImg;
	GaussianBlur(spImg, gaussianSmoothedImg, Size(5, 5), 1.5);

	Mat medianFilteredImg;
	medianBlur(spImg, medianFilteredImg, 3);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("SaltAndPepper", CV_WINDOW_AUTOSIZE);
	namedWindow("LocalAveraging", CV_WINDOW_AUTOSIZE);
	namedWindow("GaussianSmoothing", CV_WINDOW_AUTOSIZE);
	namedWindow("MedianFiltered", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("SaltAndPepper", 120, 120);
	moveWindow("LocalAveraging", 140, 140);
	moveWindow("GaussianSmoothing", 150, 150);
	moveWindow("MedianFiltered", 160, 160);

	imshow("Original", inputImg);
	imshow("SaltAndPepper", spImg);
	imshow("LocalAveraging", localAvgImg);
	imshow("GaussianSmoothing", gaussianSmoothedImg);
	imshow("MedianFiltered", medianFilteredImg);

	waitKey(0);
	return 0;
}














반응형
LIST

이미지에서 Noise 제거하기


Noise 픽셀들만 정확히 찾아내서 원래 색깔로 돌려놓는 방법이 있으면 좋으련만...

이런 방법을 구현하기는 거의 불가능하다. 왜냐면 어떤 픽셀이 noise인지 알기 쉽지 않을 뿐더러, 설령 알 수 있다하더라도 원래 색깔이 어떤 것인지를 알기는 더더욱 어렵기 때문이다.


Noise를 완전히 없애기는 어려우니, 눈에 덜 띄도록 하는 것이 한 가지 방법이 될 수 있다.

Noise 픽셀의 특징은 주변 픽셀들과 다른 색깔값을 갖는다는 것이다.

그렇다면 주변 픽셀들과 비슷한 색깔을 갖도록 하면 될 것이다.

그런데, 어떤 픽셀이 noise인지 모르기 때문에, 모든 픽셀에 대해서 주변 픽셀들과 비슷한 색을 갖도록 하는 것이 noise를 완화시키는 한 가지 방법이다.


Local Averaging

픽셀이 주변 픽셀들과 유사한 색깔값을 갖도록 하는 한 가지 방법은 자신과 주변픽셀들의 색깔값의 평균값을 구해서 그것으로 자신의 색깔값으로 하는 것이다.

예를들어, 아래 그림과 같이 E픽셀의 값은 주변 8개 픽셀 (A ~ I)들과 자신의 색깔값의 평균으로 하는 것이다.

혹은 이웃의 범위를 조금 더 넓혀서 '파'픽셀의 경우 주변 24개 픽셀 (가 ~ 캬)들과의 평균값으로 한다.


Gaussian Smoothing


Local Averaging은 모든 픽셀들의 값을 같은 비율로 반영한다. 반면에, Gaussian smoothing은 픽셀별로 반영비율을 달리 한 것이다. 동기는 이렇다. 어느 픽셀의 값을 결정하는데, 자기자신의 픽셀값이 가장 많이 반영되는 것이 더 합리적이기 때문이다. 그리고 주변 이웃픽셀들도 멀리 있는 것보다는 가까이 있는 것의 반영비율을 높이는 것이 원래 색깔값을 유지하는 적절한 방법이 될 것이다.


예를 들면 아래 그림은 중앙픽셀값을 계산하기 위한 각 픽셀들의 반영비율을 보여준다. 중앙에 있는 자기자신이 4/16으로 가장 많이 반영되고, 대각선으로 멀이 있는 픽셀들은 1/16로 덜 반영한다. 여기서는 3x3 경우만 보이지만 5x5도 가능하며, 반영비율의 차이도 조절할 수 있다. 


실험결과

Salt and Pepper Noise가 들어간 이미지에 대해 잡음제거 작업을 시도한 결과를 살펴보자.

그림 1과 2는 각각 원본과 인위적으로 noise를 추가한 이미지이다.

그리고 그림 3은 5x5 방식의 local averaging을 noise이미지에 시도한 결과이고, 그림 4는 5x5 방식의 Gaussian smoothing을 시도한 결과이다.

똑같은 5x5 방식일지라도 Gaussain smoothing이 local averaging보다 좀 더 선명한 것을 알 수 있다. 왜냐하면 Gaussian smoothing은 자기자신의 색깔이 좀 더 많이 반영되기 때문에, 어느 정도 윤곽이 남기 때문이다.


<그림1> 원본 이미지


<그림 2> Salt and pepper 잡음이 들어간 이미지


<그림 3> Local averaging (5x5)을 적용한 결과


<그림 4> Gaussian smoothing (5x5)을 적용한 결과


소스코드 1은 그림 <1,2,3,4> 생성에 사용된 것으로, local averaging을 위해 OpenCV가 제공하는 함수 blur와 Gaussain smoothing을 위해서도 OpenCV 함수 GaussianBlur( )를 이용하였다.


<소스코드 1> 

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

/*
img의 전체 픽셀중 noise_ratio 퍼센트 만큼의
픽셀을 salt & pepper noise로 바꾼다.
*/
void addSaltAndPepperNoise(Mat& img, double noise_ratio)
{
	int rows = img.rows;
	int cols = img.cols;
	int ch = img.channels();
	int num_of_noise_pixels = (int)((double)(rows * cols * ch)*noise_ratio);

	for (int i = 0; i < num_of_noise_pixels; i++)
	{
		int r = rand() % rows;  // noise로 바꿀 행을 임의로 선택
		int c = rand() % cols;  // noise로 바꿀 열을 임의로 선택
		int _ch = rand() % ch;  // noise로 바꿀 채널의 임의로 선택

		// img.ptr<uchar>(r)은 r번째 행의 첫번째 픽셀, 첫번째 채널에 대한 주소값을 반환한다.
		uchar* pixel = img.ptr<uchar>(r) +(c*ch) + _ch; // noise로 바꿀 정확한 위치를 계산

		*pixel = (rand() % 2 == 1) ? 255 : 0; // black(0) 혹은 white(255)로 교체
	}
}

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat spImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), 0.25, 0.25, CV_INTER_AREA);

	spImg = inputImg.clone();
	addSaltAndPepperNoise(spImg, 0.05);

	Mat localAvgImg;
	// Noise 제거를 위해 3 by 3 매트릭스를 이용하여
	// local averaing을 시행.
	blur(spImg, localAvgImg, Size(5, 5));  

	Mat gaussianSmoothedImg;
	GaussianBlur(spImg, gaussianSmoothedImg, Size(5, 5), 1.5);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("SaltAndPepper", CV_WINDOW_AUTOSIZE);
	namedWindow("LocalAveraging", CV_WINDOW_AUTOSIZE);
	namedWindow("GaussianSmoothing", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("SaltAndPepper", 120, 120);
	moveWindow("LocalAveraging", 140, 140);
	moveWindow("GaussianSmoothing", 150, 150);

	imshow("Original", inputImg);
	imshow("SaltAndPepper", spImg);
	imshow("LocalAveraging", localAvgImg);
	imshow("GaussianSmoothing", gaussianSmoothedImg);

	waitKey(0);
	return 0;
}


반응형
LIST


Salt and Pepper Noise 추가하기


영상잡음의 다른 종류로 salt and pepper noise라는 것이 있다. 

이것은 Gaussian noise와 달리 잡음이 주변 픽셀들과 색깔이 확연히 다르다.

마치 사진 위에 소금과 후추를 떨어뜨려 놓은 것 처럼 보인다고 해서 salt & pepper noise라고 한다.


아래 사진에 salt and pepper noise를 추가해보자 한다.

전체 픽셀중에 약 5%를 무작위로 골라서 해당 위치를 noise로 바꾸는 것이다. 

결과는 아래 사진과 같다.


위 이미지 결과를 만들어내기 위한 소스코드는 아래와 같다.


 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

/*
   img의 전체 픽셀중 noise_ratio 퍼센트 만큼의
   픽셀을 salt & pepper noise로 바꾼다.
*/
void addSaltAndPepperNoise(Mat& img, double noise_ratio)
{
	int rows = img.rows;
	int cols = img.cols;
	int ch = img.channels();
	int num_of_noise_pixels = (int)((double)(rows * cols * ch)*noise_ratio);
	
	for (int i = 0; i < num_of_noise_pixels; i++)
	{
		int r = rand() % rows;  // noise로 바꿀 행을 임의로 선택
		int c = rand() % cols;  // noise로 바꿀 열을 임의로 선택
		int _ch = rand() % ch;  // noise로 바꿀 채널의 임의로 선택

		// img.ptr<uchar>(r)은 r번째 행의 첫번째 픽셀, 첫번째 채널에 대한 주소값을 반환한다.
		uchar* pixel = img.ptr<uchar>(r) +(c*ch) + _ch; // noise로 바꿀 정확한 위치를 계산

		*pixel = (rand() % 2 == 1) ? 255 : 0; // black(0) 혹은 white(255)로 교체
	}
}

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat spImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), 0.3, 0.3, CV_INTER_AREA);

	spImg = inputImg.clone();
	addSaltAndPepperNoise(spImg, 0.05);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("SaltAndPepper", CV_WINDOW_AUTOSIZE);


	moveWindow("Original", 100, 100);
	moveWindow("SaltAndPepper", 120, 120);


	imshow("Original", inputImg);
	imshow("SaltAndPepper", spImg);

	waitKey(0);
	return 0;
}


반응형
LIST


잡음이 들어간 사진 만들기 (Gaussian Noise)


사진은 원래 잡음이 없는 것이 좋지만, 일부러 잡음을 만들어 넣더도

새로운 느낌이 난다.

잡음, 즉 noise를 만드는 방법 중, 여기서는 Gaussian Noise를 만들어 넣는 방법을 생각해보자.


우선 결과부터 살펴보면,

아래는 잡음이 없는 깨끗한 영상이다.

아래는 위의 깨끗한 영상에 일부러 잡음을 만들어 넣은 것이다.

특히 이 잡음은 Gaussian noise이다.


Gaussian noise

이것은 평균과 분산을 갖는 정규분포에 따라 임의로 발생한 noise를 말한다.

잡음값이 임의로 막 생겨나기는 하는데, 그 잡음들의 빈도수를 살펴보면 정규분포를 가지고 있다.

즉, 평균값에 가까운 잡음들의 개수가 평균에서 먼 잡음값들보다 많다.


아래는 위의 사진들을 만들어낸 소스코드이다.

line 24: Gaussian noise를 만들어 낸다.

line 28: Gaussian noise를 이미지에 적용하여 잡음이 낀 이미지를 만들어내는 과정이다.

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	resize(inputImg, inputImg, Size(), 0.3, 0.3, CV_INTER_AREA);

	Mat noise_image(inputImg.size(), CV_16SC3);
	double average = 0.0;
	double std = 30.0;
	randn(noise_image, Scalar::all(average), Scalar::all(std));

	Mat temp_image;
	inputImg.convertTo(temp_image, CV_16SC3);
	addWeighted(temp_image, 1.0, noise_image, 1.0, 0.0, temp_image);
	temp_image.convertTo(temp_image, inputImg.type());

	
	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("GaussianNoise", CV_WINDOW_AUTOSIZE);


	moveWindow("Original", 100, 100);
	moveWindow("GaussianNoise", 120, 120);


	imshow("Original", inputImg);
	imshow("GaussianNoise", temp_image);

	waitKey(0);
	return 0;
}


반응형
LIST


이미지에서 사람 피부영역만 골라서 표시하기


사람의 피부색은 HLS영상의 기준으로 볼 때 다음과 같은 조건을 만족한다.

Saturation의 값은 0.2보다 크거나 같고,

Hue값은 28도보다 작거나 같고, 또는 330도 보다 크거나 같으며,

Luminance를 Saturation값으로 나눈 값이 0.5와 3.0 사이에 있어야 한다.


이것은 2007년에 Kakumanu라는 사람이 Pattern Recognition 논문지에 "A survey of skin-color modeling and detection methods"라는 제목으로 실은 논문에 나오는 내용이다.


아래와 같은 이미지에서 피부색은 그대로 두고, 나머지 영역은 모두 검게 바꾼다고 하자.

결과는 아래와 같다. 흑인, 백인, 황인에 상관없이 얼굴영역이 많이 검출되었다. 물론 피부인데도 불구하고 제대로 검출되지 못한 부분도 있고, 피부가 아님에도 피부로 오검출된 부분도 있다.

또 다른 이미지를 가지고 실험한 결과이다. 여기서는 각기 다른 인종의 여성 3명을 대상으로 한다. 

결과는 아래와 같이 대부분의 피부 영역이 올바르게 검출되었지만, 일부 머리카락도 피부로 오검출되었다.


위의 피부검출실험에 사용된 소스코드는 아래와 같다.

입력받은 이미지를 HLS영상으로 바꾸어, H, L, S의 값을 이용하였다.

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat hlsImg;
	Mat skinImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	//resize(inputImg, inputImg, Size(), 0.4, 0.4, CV_INTER_AREA);
	skinImg = inputImg.clone();

	cvtColor(inputImg, hlsImg, CV_BGR2HLS);
	vector<Mat> hls_images(3);
	split(hlsImg, hls_images);

	for (int row = 0; row < hlsImg.rows; row++)
	{
		for (int col = 0; col < hlsImg.cols; col++)
		{
			uchar H = hlsImg.at<Vec3b>(row, col)[0];
			uchar L = hlsImg.at<Vec3b>(row, col)[1];
			uchar S = hlsImg.at<Vec3b>(row, col)[2];

			double LS_ratio = ((double)L) / ((double)S);
			bool skin_pixel = (S >= 50) && (LS_ratio > 0.5) && (LS_ratio < 3.0) && ((H <= 14) || (H >= 165));

			if (skin_pixel == false)
			{
				skinImg.at<Vec3b>(row, col)[0] = 0;
				skinImg.at<Vec3b>(row, col)[1] = 0;
				skinImg.at<Vec3b>(row, col)[2] = 0;
			}
		}
	}

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("SkinDetected", CV_WINDOW_AUTOSIZE);


	moveWindow("Original", 100, 100);
	moveWindow("SkinDetected", 120, 120);


	imshow("Original", inputImg);
	imshow("SkinDetected", skinImg);

	waitKey(0);
	return 0;
}



반응형
LIST


OpenCV 성능향상을 위한 포인터(Pointer) 사용


함수 at( )의 문제점


이미지의 각 픽셀값을 바꿀 때

함수 at( )을 사용했다.

기능상으로는 아무 문제가 없지만, 속도가 느린 단점이 있다.


예를 들어, 다음과 같은 컬러이미지를 반전하는 경우를

생각해보자.


원본 컬러영상


반전한 컬러이미지


함수 at( )을 이용한 이미지 반전코드

	for (int r = 0; r < image_rows; r++)
	{
		for (int c = 0; c < image_cols; c++)
		{
			for (int ch = 0; ch < 3; ch++)
			{
				outputImg.at<Vec3b>(r, c)[ch] = 255 - outputImg.at<Vec3b>(r, c)[ch];
			}
		}
	}

각 픽셀에 대해,

픽셀별로 R, G, B 채널에 대해서 반전시키면 된다.

그런데, 사실 이 방법은 

시간이 오래 걸린다.


시간측정방법

코드가 수행하는데 걸리는 시간을 측정하기 위해서는

함수 getTickCount( )를 이용하면 된다.

	const int64 start = getTickCount();
        .....
	int64 elapsed = (getTickCount() - start);


함수 at( )을 이용하여, 반전이미지 만드는데 걸리는 시간

함수 getTickCount( )를 이용해서

측정한 시간은 평균적으로 210,000 tick 정도 된다.


포인터를 이용한 속도 개선

함수 at ( )을 사용하지 않고,

이미지 데이터에 대해 포인터를 이용하여 직접 픽셀값을

바꾸면 속도를 엄청나게 개선시킬 수 있다.

for (int r = 0; r < image_rows; r++) { uchar* value = inputImg.ptr<uchar>(r); uchar* result = outputImg.ptr<uchar>(r); for (int c = 0; c < image_cols; c++) { *result++ = *value++ ^ 0xff; *result++ = *value++ ^ 0xff; *result++ = *value++ ^ 0xff; } }

위의 코드는 반전이미지를 동일한 방식으로 만들되,

포인터를 이용하여 각 픽셀값을 직접 접근하는 방식이다.

이렇게 했을 때, 측정시간은

평균적으로 4,300 tick이다.


비교 그래프

아래는 함수 at( )과 포인터를 이용했을 때의

각각의 소요시간이다.

포인터를 이용했을 때, 약 50배 정도 속도가 빨라지는 것을 알 수 있다.

따라서, 성능이 중요할 경우에는 포인터 사용을 적극 고려해야 한다.



소스코드

위의 실험수행을 위한 프로그램이다.

중간에 매크로 USE_AT을 #define하면 함수 at( )을 이용하는 방식으로 동작하고,

comment-out하면 포인터를 이용한 방식으로 동작한다.

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;
	Mat outputImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);
	outputImg = inputImg.clone();

	const int64 start = getTickCount();

	int image_rows = inputImg.rows;
	int image_cols = inputImg.cols;

//#define USE_AT
#ifdef USE_AT
	for (int r = 0; r < image_rows; r++)
	{
		for (int c = 0; c < image_cols; c++)
		{
			for (int ch = 0; ch < 3; ch++)
			{
				outputImg.at<Vec3b>(r, c)[ch] = 255 - outputImg.at<Vec3b>(r, c)[ch];
			}
		}
	}
#else if
	for (int r = 0; r < image_rows; r++)
	{
		uchar* value = inputImg.ptr<uchar>(r);
		uchar* result = outputImg.ptr<uchar>(r);
		for (int c = 0; c < image_cols; c++)
		{
			*result++ = *value++ ^ 0xff;
			*result++ = *value++ ^ 0xff;
			*result++ = *value++ ^ 0xff;
		}
	}
#endif
	int64 elapsed = (getTickCount() - start);

	cout << "Elapsed time " << elapsed << endl;

	namedWindow("input", CV_WINDOW_AUTOSIZE);
	namedWindow("output", CV_WINDOW_AUTOSIZE);
	moveWindow("input", 100, 100);
	moveWindow("output", 120, 120);
	imshow("input", inputImg);
	imshow("output", outputImg);

	waitKey(0);
	return 0;
}


반응형
LIST



컬러이미지 (Color image)를 R, G, B 요소로 분리하기


컬리이미지의 각 픽셀은 3개  색채널 (red, green, blue)로 구성되어 있다.

따라서 이미지에서 각 채널별 요소를 분리해 낼 수 있다.


아래와 같은 컬러이미지가 있다고 하자.

 

Red 채널만 뽑아낸 결과

위의 영상에서 G와 B는 무시하고, R에 해당하는 값만 뽑아낸 결과는

아래와 같다.

왼쪽 잔의 음료수 색이 빨간 색에 가까웠기 때문에,

Red만 뽑아낸 영상에서는 해당 부분이 하얗게 나타난다.

그리고 원래 하얀색이었던 부분은 R, G, B값이 모두 컸기 때문에

Red만 뽑아낸 영상에서도 하얗게 나타난다.

Green 채널만 뽑아낸 결과

Blue 채널만 뽑아낸 결과


함수 split( )


OpenCV 라이브러리에서는 컬러이미지를 

각 채널별로 분리해내는 함수 split( )을 제공한다.

이것을 이용하여 구현한 프로그램은 아래와 같다.


 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
32
33
34
35
36
37
38
39
40
41
42
43
#include <opencv2/opencv.hpp>
#include <iostream>


using namespace cv;
using namespace std;

int main(int argc, char** argv)
{

	if (argc != 2)
	{
		cout << " Provide image name to read" << endl;
		return -1;
	}

	Mat inputImg;

	inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR);

	vector<Mat> bgr_images(3);
	split(inputImg, bgr_images);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("Blue", CV_WINDOW_AUTOSIZE);
	namedWindow("Green", CV_WINDOW_AUTOSIZE);
	namedWindow("Red", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("Blue", 120, 120);
	moveWindow("Green", 140, 140);
	moveWindow("Red", 160, 160);

	imshow("Original", inputImg);
	imshow("Blue", bgr_images[0]);
	imshow("Green", bgr_images[1]);
	imshow("Red", bgr_images[2]);


	waitKey(0);

	return 0;
}


line 21-22:

	vector<Mat> bgr_images(3);
	split(inputImg, bgr_images);

inputImg는 RGB 3개의 채널을 가진 컬러이미지이다.

이를 R,G,B로 분리해 내기 위해서는,

각 채널별로 저장할 공간을 만들어야 한다.

이를 위해, Mat의 vector인 bgr_images를 만든다.

이것은 크기 3인 배열이라고 생각하면 된다.


함수 split( )호출은 inputImg를 R,G,B로 분리한다.

이 때, 

bgr_images[0]은 Blue

bgr_images[1]은 Green

bgr_images[2]는 Red 채널에 해당하게된다.

RGB 순서가 거꾸로 저장된다고 이해하면 쉽다.


line 29-37:
	moveWindow("Original", 100, 100);
	moveWindow("Blue", 120, 120);
	moveWindow("Green", 140, 140);
	moveWindow("Red", 160, 160);

	imshow("Original", inputImg);
	imshow("Blue", bgr_images[0]);
	imshow("Green", bgr_images[1]);
	imshow("Red", bgr_images[2]);

4개의 이미지 창을 만들어

원래 이미지와 함께, R, G, B 분리 결과를 보여준다.

함수 moveWindow( )는 해당 윈도우의 표시위치를 지정할 수 있도록 한다.


반응형
LIST




반전 이미지 만들기 (Make inverted image)


컬러 이미지는 하나의 픽셀마다 

빨간색, 녹색, 청색 (Red, green, blue)의 3가지 정보를 가지고 있다.

각각의 색깔 정보를 전문용어로 채널(channel)이라고 한다.


일반적으로 각 채널은 8 bit을 사용하므로

256 가지 의 색을 표시할 수 있다.

즉, 0부터 255까지의 값을 가질 수 있다.


이론적으로 보자면,

각 채널이 256가지의 색을 표현하기 때문에,

3개 채널을 모두 고려하면 256 * 256 *256 가지의 색깔 표현이 가능한 것이다.


반전 이미지 (inverted image)는 

각 채널의 값 v를 255-v로 대체하는 것을 말한다.

쉽게 말하면, 0을 255로 바꾸는 것이다.

직관적으로 이해하자면, 검정색이 흰색이 되는 것이다.

이를 구현한 것이 아래 코드이다.


void InvertColor(Mat& input_img, Mat& inverted_img)

{

   CV_Assert(input_img.type() == CV_8UC3);

   inverted_img = input_img.clone();


   for (int row = 0; row < input_img.rows; row++)

   {

      for (int col = 0; col < input_img.cols; col++)

      {

         for (int ch = 0; ch < input_img.channels(); ch++)

{

   inverted_img.at<Vec3b>(row, col)[ch] = 255 - inverted_img.at<Vec3b>(row, col)[ch];

 }

      }

   }

}


우선, 인수로 주어지는 input_img가 3개 채널을 갖는 컬러영상이고,

각 채널이 8bit인지를 확인한다.

그리고 나서는, 모든 픽셀에 대해 (2개의 바깥 for-loop 부분), 

각 채널값을 꺼내서 반전시켜서 다시 넣는다. (가장 안쪽의 for-loop 부분)


각 픽셀의 3개 채널값을 한 번에 읽는 방법은 다음과 같다.

inverted_img.at<Vec3b>(row, col)

이후에 개별 채널값은 배열 인덱스를 이용하여 읽어내면 된다.


inverted_img.at<Vec3b>(row, col)[ch]


웹캠으로부터 들어오는 컬러 영상을 

함수 InvertColor( )를 이용하여 반전시키는 프로그램은 아래와 같다.

코드의 함수 onmouse( )와 헤더, 매크로 선언 부분은 이전 포스트를 참조하면 되겠다.



프로그램 수행결과는 아래와 같이, 반전을 하기 전의 원래 영상과 반전 영상이다.


<< 원래영상>>


<< 반전 영상>>







반응형
LIST




웹캠(webcam) 영상을 Grey로 출력하기


웹캠으로부터 들어오는 영상을 회색영상으로 변환하여 출력하는 프로그램이다.

아래 그림에서 위의 것은 webcam에서 들어오는 원래 컬러영상이고,

아래 것은 이를 grey로 바꾼 것이다. (사실 grey변환 외에도 quantisation을 수행한 것으로, 이에 대해서

밑에서 좀 더 자세히 설명한다.)



line 15

USB로 연결된 웹캠을 이용하려면 OpenCV에서 제공하는

VideoCapture 객체를 사용하면 편리하다.

각 카메라마다 ID를 붙이는데, 기본으로 붙어있는 카메라는 0, USB로 연결한 웹캠은 1 정도 된다.

이건 ID를 바꿔가면서 테스트해보면 된다.


line 26

마우스 클릭을 처리하기 위해서, 클릭 이벤트시 수행할 함수를 설정하는 부분이다.

"cam"은 마우스 클릭을 감지할 윈도우이고,

onmouse는 수행할 함수이고, 

&greyImg는 이 함수에 넘겨줄 인수이다.


line 30

웹캠에서 들어오는 영상을 frame에 저장한다.

OpenCV에서 웬만한 부분은 다 처리하기 때문에, read 함수만 호출하면 된다.


line 32

컬러영상을 담고 있는 frame을 회색조 (grey) 영상인 greyImg로 변경한다.

이 때 OpenCV에서 제공하는 함수 cvtColor ( )를 이용한다.


line 33

Quantisation하는 부분이다. Grey영상의 픽셀들은 256 단계를 갖는데,

이를 지정하는 단계수( 2의 3승)로 바꾸는 과정이다.

여기서는 8 (= 2^3)단계로 바꾼다.


마우스 이벤트 핸들러 함수

왼쪽 버튼이 눌렸을 때, 

그 위치에서의 픽셀값과 위치를 출력하는 함수이다.


Quantisation 함수

256단계의 grey 이미지를 2^num_bits 단계로 바꾸는 단계이다.

만약 num_bits = 1이면, 2단계, 즉 흑백 영상으로 표시한다.

num_bits = 1일 경우에, 흑백으로 quantisation된 예이다.


반응형
LIST

+ Recent posts