영상처리 프로그램 작성하면서 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

Python에서 OpenCV 라이브러리를 이용하여 영상처리하는 과정에 대한 동영상 강좌입니다.

이제 시작으로, 가끔씩 천천히 업로드됩니다.

 

우선 1편은 이미지를 읽어서 화면에 표시하는 간단한 과정을 보여줍니다.

 

유튜브 동영상

반응형
LIST


ㅇ Gamma correction은 이미지의 밝기를 조절하는 방법입니다.
  - 일반적으로 이미지를 밝게 하려면 픽셀값 (pixel intensity)을 증가시키면 됩니다.
  - 반대로 어둡게 하려면 감소시키면 됩니다.
  - 가장 간단한 방법으로는 모든 픽셀값을 같은 크기 만큼 증가시키면 이미지가 밝아집니다.
  - 하지만 gamma correction은 동일한 값 만큼 증가시키지는 않습니다.
  - Gamma correction은 이미지의 어두운 부분에 집중합니다.
  - 밝게 하거나, 어둡게 할 때,어두운 부분의 변화폭을 밝은 부분보다 크게 합니다.
  - 이러한 특징에 대해서는 뒤에서 그래프와 함께 자세히 설명하겠습니다.
 
ㅇ Gamma correction에서 gamma 값은 양수로서, 그 크기에 따라 밝기가 조정됩니다.
 - Gamma > 1이면, 이미지를 밝게 하는 것이고,
  - 0 < Gamma < 1이면, 어두워집니다.
  - Gamma 값은 양수 ( > 0)여야 합니다.
  - 만약 Gamma = 1인 경우, 이미지 밝기는 변하지 않습니다..

ㅇ Gamma correction은 컬러와 gray 이미지에 모두 적용 가능합니다.
  - Intensity를 가진 gray 이미지는 물론,
  - RGB 컬러이미지에도 적용 가능합니다. 
  - 이 때는, 각 채널별로 gamma correction을 적용해야 합니다.




ㅇ Gamma correction 공식을 살펴보겠습니다.
  - I (x,y)를 이미지(x,y)에서의 pixel intensity라고 하고
  - \gamma > 0가 주어지면, 
  - 아래 식은 correction 후의 pixel intensity값을 나타냅니다.
ㅇ 이 때 I(x,y) 값은 [0, 1.0]에 있어야 합니다.
따라서, 0 <= pixel intensity <= 255 일 경우에는,
[0 1]로 scaling한 후에 correction을 수행하고,
 다시 [0 255]로 scaling합니다.




ㅇ Gamma correction 적용 이미지를 살펴봅시다.
  - Gamma < 1 일 때 (=0.6), 전반적으로 사진이 어두워지며
  - Gamma > 1 일 때 (= 2, 3), 사진이 밝아지는 것을 
  - 아래 2가지 사진 예에서 correction결과를 볼 수 있습니다.







ㅇ Pixel intensity별로 correction 후에 값이 어떻게 변하는지 살펴보겠습니다.
  - Gamma 값에 따라 달라지는 이미지 brightness를 그래프로 그려 보았습니다.

ㅇ gamma > 1 인 경우,
- 아래 그래프에서, I(x,y) = 0.3이 
- gamma correction 이후에 0.55로 변환
- 어두웠던 것 (0.3)이 밝은 것(0.55)로 변환되었습니다.



ㅇ 0 < gamma < 1 인 경우, 
- 밝았던 부분 (= 0.8)이 gamma correction에 의해
- 어두운 부분 (=0.31)로 되었습니다.
- 비율로 따지자면, 밝았던 부분이 어두워지는 비율이 훨씬 크다


ㅇ Gamma correction은 이미지의 어두운 부분을 밝은 부분보다 더 많이 변화시키는 특징을 가집니다. 
  - 즉, intensity가 낮은 부분, 즉 어두운 부분에 대해서
  - 값의 변화정도가 더 크다.

0 < Gamma < 1 일 때,
intensity I [0, 1.0]별로,
correction이후의 값 I'과의 ratio를 비교해보겠습니다.
아래 그래프는 I' / I를 보여준 것입니다.
Intensity가 낮은 pixel들이 변환 후에 더 낮은 비율을 가지게 됨을 알 수 있습니다.
이로부터, gamma correction은,
이미지를 어둡게 할 때, 어두운 쪽의 픽셀들이 더 많이 어두워진다는 것을 알 수 있습니다.



Gamma > 1 일때,
낮은 intensity의 부분들이 correction 후에
값이 증가되는 비율이 높은 intensity부분들보다 더 크다는 것을 알 수 있습니다.
아래 결과를 잘 보면,
밝은 부분 (intensity가 큰 부분)은 gamma 값의 변화에 크게 영향을 받지 않음을 볼 수 있습니다.
하지만, 어두운 부분은 gamma값의 영향에 따라 값이 크게 변하는 것을 볼 수 있습니다.




ㅇ Gamma correction 구현
  - MATLAB으로 gamma correction을 수행하는 함수를 구현해 보았습니다.
  - 인수로 correction을 수행할 컬러이미지 (I)와
  - gamma를 받습니다.
    . 이 함수는 color이미지에 대해서만 동작합니다. 
  - 반환값은 gamma correction된 이미지입니다. 

  
function [img] = myGammaCorrection(I, gamma)
    
    %
    % double precision으로 바꾼다.
    % 값들이 rescaling되어 0 <= pixel <= 1.0으로 바뀐다.
    %
    img = im2double(I);
   

    % gamma correction
    agamma = 1/gamma;
    for i=1:3
        img(:,:,i) = img(:,:,i).^agamma;
    end
    
   
    % 값들이 다시 [0, 255]로 rescaling된다.
    img = im2uint8(img);
    
end







반응형
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






ㅇ Contrast Enhancement
  - 이미지에서 너무 밝은 부분이나, 어두운 부분은 자세한 내용을 보기가 어렵다.
  - 비슷한 intensity를 갖는 픽셀들이 한 곳에 모여있기 때문이다.
    . intensity란, 예를 들어, gray image에서 픽셀은 0(검정색)부터 255(흰색) 값을 갖는데, 이 값을 의미하는 것이다.
    . color image는 RGB, 3개의 채널을 갖는다면, 하나의 픽셀은 각 채널별로 intensity를 갖게 된다.
    . 여기서는 gray image만 생각하자.
  - 이를 개선하는 것을 contrast enhancement라고 한다.
  - 차이 또는 대조 (contrast)를 개선 (enhance)한다는 뜻으로,
  - 차이가 안 나던 픽셀들의 intensity를 바꿔서 차이가 나도록 한다는 것이다.
  - 이것을 위해서 사용되는 방법이 histogram equalization인데, 이에 대해서 살펴보면,,,

ㅇ 히스토그램 (Histogram)
  - 이것은 intensity별로 픽셀들의 개수를 나타내는 그래프이다.

ㅇ Histogram modeling, Histogram transfer
  - 픽셀들의 intensity를 변경하게 되면,
  - 이에 따라 히스토그램도 변경되는데, 이를 histogram modeling 또는 histogram transfer라고 한다.
  - 이러한 변경은 앞에서 설명했듯이, 잘 안 보이는 부분을 잘 보이게 한다든지 하는 contrast enhancement를 하기 위해서이다.

ㅇ Histogram equalization
  - histogram modeling (tranfer)는 intensity를 변경하는 것이기 때문에,
  - 여러 가지 방법들을 생각해 볼 수 있는데,
  - 그 중에서도 contrast enhancement를 목적으로 하는 대표적인 방법이 histogram equalization이다.
  
ㅇUniform histogram 
  - histogram equalization의 목적은 intensity들이 골고루 사용되도록 하는데 있다.
  - 비슷한 intensity들만 많이 사용되면, 이미지에서 그 부분은 잘 안보이기 때문이다.
  - 그래서, 궁극적인 목적이자, 가장 바람직인 equalization은
  - intensity들이 모두 동일한 빈도수로 사용되는 것이다. (하지만 이것은 목표일 뿐이고, 실제는 그렇지 않다.)

ㅇ Histogram Equalization의 효과

- 아래와 같은 히스토그램이 있다고 하자.
- x축은 intensity이고, y축은 값들의 빈도수이다.
- 예를 들어, intensity 2는 빈도수가 30이고,
- 최대 빈도수를 갖는 intensity는 4인 것을 알 수 있다.
- 그런데,
- intensity들이 2 ~ 4에 몰려있는 것을 볼 수 있다. (1번으로 표시)
- Histogram equalization은 이러한 집중을 완화시키는 것으로, 다음 그림을 보자.


- 위의 히스토그램을 완벽하게 equalization하면, 
- 아래와 같이 모든 intensity들이 동일한 빈도수를 갖게된다.
- 하지만, 실제에서는 이러한 것을 달성하기가 어렵고,
- OpenCV에서는 CDF (cumulative distribution function)을 이용한 방법을 이용한다.


ㅇ OpenCV의 histogram equalization 지원
  - equalizeHist( )라는 함수를 제공하는데,
  - 이 함수에 이미지를 인수로 주면, equalized된 이미지를 돌려준다.

void equalizeHist( InputArray src, OutputArray dst): gray image, src 8 bit single channel image
  - 함수 링크
 - src: 입력이미지, dst: equalized된 출력이미지

  - 이 함수는 Cumulative Distribution Function (CDF)를 이용해서 equalization을 수행한다.


ㅇ CDF를 이용한 Histogram Equalization 원리
  - 단계별 수행내용을 그림으로 나타냈다.

- 1 단계
- Histogram에 대해 cumulative sum을 계산한다.
- 좌측에서 우측으로 가면서,
- 각 값들에 대해, 자신과 이전값들의 빈도수 합을 구하는 것이다.
- 값이 커지면서, 빈도수가 점차 증가하는데,
- 이는 이전값들의 빈도수에 자신의 빈도수를 더해 나가기 때문이다.


- 2단계
- 위의 결과에 대해 y축의 값을 normalize한 것으로,
- 빈도수들을 0 ~ 10까지로 rescaling한 것이다.
- 예를 들어, 위의 그램에서 값3에 해당하는 빈도수가 80정도 였는데,
- 이는 4로 normalize된다.
- 이렇게 하는 이유는.... 다음 그림을 보면


- 3단계
- 위에서 normalization을 하면
- 값에 대한 맵핑(mapping)을 얻을 수 있는데,
- 무슨 의미냐 하면,
- 아래 그림과 같이, 
- 값 2를 mapping해서 2로, (번호 1)
- 값 3은 4로 (번호 2)
- 값 4는 10으로 (번호 3)으로 바꾸는 대응을 알 수 있다는 것이다.
- 이 mapping을 이용해서,
- 기존 값을 새로운 값으로 변경시키는 것이다.
- 왜, 이런 mapping을 수행하냐면,,,, 다음 그림에서


- 새로운 값으로 바꾸고 난 후에,
- 히스토그램을 다시 그리면, 아래 그림과 같다.
- 기존에는 값 2 ~ 4에 모두 몰려있었는데,
- 새로운 그림에서는 2 ~ 10까지 펼쳐져 있게 된다.
- 즉, 완벽한 equalization은 아니지만, 
- 그래도, 히스토그램이 전체적으로 퍼지는 효과를 얻게 되는 것이다.



ㅇ MATLAB 소스코드
  - 위의 결과들을 얻기 위한 프로그램
 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
%
% 히스토그램 equalization
%
clear all;
close all;

% 테스트 데이터
% intensity - value
% data = [0 1;
%     1 2;
%     2 30;
%     3 50;
%     4 2;
%     5 2;
%     6 2;
%     7 30;
%     8 50;
%     9 2;
%     10 1;
%     ];

data = [0 1;
    1 2;
    2 30;
    3 50;
    4 100;
    5 2;
    6 2;
    7 1;
    8 1;
    9 2;
    10 1;
    ];

max_intensity = size(data,1)-1;

% 바그래프로 데이터를 출력
figure;
bar(data(:,1), data(:,2));
title('Original Histogram');
ylim([0, max(data(:,2)+10)]);

disp(sum(data(:,2)))

% cumulative 합을 계산
% intensity - cumulative 합
data_temp = data;
data_temp(:,2) = cumsum(data(:,2));
data1 = data;
data1(:,2) = round(max_intensity*cumsum(data(:,2))/sum(data(:,2)));

figure;
bar(data_temp(:,1), data_temp(:,2));
title('Cumulative Sum');
ylim([0, max(data_temp(:,2)+10)]);

% 바그래프로 cumulative합을 출력
figure;
bar(data1(:,1), data1(:,2));
title('Normalized Cumulative Sum');
ylim([0,max_intensity+1]);

% equalized data
equData = data;
equData(:,2) = zeros(max_intensity+1, 1);

for k=1:max_intensity+1
    newIntensity = data1(k,2);
    newIntensityIdx = newIntensity+1;
    equData(newIntensityIdx, 2) = equData(newIntensityIdx,2)+ data(k,2); 
end

figure;
bar(equData(:,1), equData(:,2));
title('Equalized Histogram');

disp(sum(equData(:,2)))






반응형
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





Mat 클래스에 저장된 이미지의 타입을 출력하는 utility함수는 다음과 같다.


The following is a utility function you can use to identify the types of OpenCV matrices at run-time.



void showType(Mat &m) 
{
	string r;
	int type = m.type();

	uchar depth = type & CV_MAT_DEPTH_MASK;
	uchar chans = 1 + (type >> CV_CN_SHIFT);

	switch (depth) 
	{
		case CV_8U:  r = "8U"; break;
		case CV_8S:  r = "8S"; break;
		case CV_16U: r = "16U"; break;
		case CV_16S: r = "16S"; break;
		case CV_32S: r = "32S"; break;
		case CV_32F: r = "32F"; break;
		case CV_64F: r = "64F"; break;
		default:     r = "User"; break;
	}

	r += "C";
	r += (chans + '0');

	printf("Matrix: %s %d(w)x%d(h) \n", r.c_str(), m.cols, m.rows);
}



반응형
LIST




Connected Component Analysis 


이미지 안의 물체를 찾아내어 분석하기 위한 목적으로 thresholding에 기반한 이진화 기술들이 개발되었으며, 이진화 결과에서 노이즈를 제거하고, 결과들을 보다 정교화하기 위해서 mathematical morphology 기술들이 개발되었다. Morphology까지 거친 이미지들을 이용하면 물체를 구분 (segmention)하고, 물체를 인식 (object recognition)할 수 있는데, 그 전에 픽셀들을 물체 단위로 구분하여야 한다. 

Connectedness

하나의 물체를 구성하는 픽셀들은 이미지에서 서로 연결된 형태로 나타나게 되는데, 픽셀들이 연결되었는지를 판단하는 방법에는 2가지가 있다. 하나는 4-adjacency인데, 이것은 픽셀의 상하좌우만 연결로 인정하는 것이고, 다른 하나인 8-adjacency는 대각선에 대해서도 연결을 인정하는 것이다. 

문제는 이미지에 4, 8 adjacency를 적용했을 때 제대로된 결과를 얻기가 어려운 경우가 생긴다는 것이다. 예를 들어, 다음과 같은 이미지가 있다고 하자.


여기에 4나 8 adjacency를 적용했을 때, 원하는 결과를 얻기가 어렵다. 아래에서 좌측에 있는 그림은 8 adjacency를 적용한 결과인데, 백그라운드들이 하나로 연결된 것으로 분류한다. 하지만, 명확히는 백그라운드는 2개의 중앙링에 의해서 3부분으로 나누어져야 한다. 우측에 있는 그림은 4 adjacency를 기반으로 해서 찾은 connected component들인데, 이것은 전혀 원하는 결과가 아니다.
 


하지만, 4와 8 adjancency를 번갈아가며 적용하는 방법을 사용하면 조금 낫다. 아래 그림이 원하는 결과이다. 백그라운드가 흰색으로 구분되었고, 초록색, 빨간색으로 두개의 링이 구분되었으며, 살구색과 파란색으로 링 사이의 공간이 제대로 구분되었음을 알 수 있다. 이것을 얻기 위해서, 4, 8 adjacency를 번갈아가며 적용하였다. 맨 바깥쪽에는 4, 바깥쪽 링에는 8, 이런 식으로 교대로 적용한 결과이다.


Connected Component 찾아내기
Binary image에서 물체를 구성하는 연결된 픽셀들을 찾아내는 알고리즘은 예상 외로 간단하다. 알고리즘은 2-pass로 구성되는데, 첫 번째 패스에서는 4 혹는 8 adjacency 어느 것을 이용하건 간에, 좌에서 우로, 위에서 아래로 한 픽셀씩 검사하면서 previous neighboring pixel과 동일한 label을 지정하면 된다. 만약 최초 픽셀이라면 새로운 label을 할당하면 된다. x로 표시된 픽셀의 previous neighboring pixel들은 아래 그림과 같이. 위쪽 혹은 왼쪽에 위치해있는 픽셀들로서, 왼쪽 그림은 8 adjacency의 경우, 오른쪽 그림은 4 adjacency의 경우의 previous neighboring pixel들을 보여준다.

두 번째 패스에서는 다른 label이지만 같은 영역에 속하는 label들을 찾아서, label을 하나로 합치는 단계를 수행한다. 통합할 label을 찾는 것은, 어느 픽셀에 대해서 previous neighborng label이 두 개 이상일 경우, 이 들 label들은 서로 같은 label이므로 하나로 합쳐져야 된다. label 합치는 과정을 쉽게 하기 위해서, label이 정수라고 한다면, 서로 다른 label들을 합칠 때는, 낮은 숫자를 갖는 label로 통합하도록 하면된다.

OpenCV에서는 connected component를 찾기 위한 함수로 findCountours를 제공한다. 이 함수의 원래 목적은 이미지에서 물체의 윤곽선을 구성하는 픽셀들을 구한다. 사용방법은 아래와 같다.

vector<vector<Point>> contours;
- 수행 결과 여러 개의 윤곽선들이 나오게 되는데, 이 윤곽선들이 배열을 구성하고, 각 배열의 요소는 윤곽선인데, 윤곽선은 이를 구성하는 픽셀들으 배열이다.

vector<Vec4i> hierarchy;
- 윤곽선마다 고유의 번호를 매겨, 윤곽선 간의 관계를 표시한다. 각 윤곽선은 4개의 정보를 갖는데, 첫 번째는 다음 윤곽선, 두 번째는 이전 윤곽선, 세 번째는 안에 포함된 윤곽선, 네 번째는 밖을 감싸고 있는 윤곽선. 만약 이러한 정보가 필요없다면 음수로 채워진다.

findContours(binary_image, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);
contour에 의해 물체 윤곽선을 구했다면, openCV함수를 이용해서 물체들을 각각 다른 색으로 칠할 수 있다. 예를 들어 다음 코드를 이용하면 된다.
Scalar colour(255, 0, 0);
blue색을 지정


drawContours(contours_image, contours, contour, colour, CV_FILLED, 8, hierarchy);
contours_image: 물체 윤곽선이 그려지는 이미지
contours: 함수 findContours를 이용해서 구한 윤곽선들
contour: 어느 윤곽선인지를 지정하는 숫자
CV_FILLED: 윤곽선 내부를 채운다.
hierarchy: 윤곽선 간의 관계로 함수 findContour에 의해 구해진 것이다. 이 정보는 윤곽선 안에 또 다른 윤곽선이 포함되어 있을 경우, 내부 윤곽선을 칠하는 범위에서 제외하기 위해서 사용된다.

위 함수들을 이용한 예는 아래와 같다. 맨 왼쪽 사진은 입력이미지 위에다가 발견된 contour들을 청색으로 표시한 것이다. 중간 것과 이진화 이미지이며, 여기에 closing morphology (20x20 isotropic structuring element 사용) 연산을 적용한 것이 오른쪽 결과이다.


위 결과를 얻기 위한 프로그램의 소스코드는 아래와 같다. line 34 ~ 36의 함수 imshowAfterResize( )는 인수로 주어진 (이 경우 320) 값으로 이미지의 폭을 resizing한 후 display하는 함수로 자체 제작한 함수이다.


 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
int main()
{
	Mat org_img;
	org_img = imread(IMG_NAME);

	if (org_img.empty() == true)
	{
		cout << "Unable to read " << IMG_NAME << endl;
		return 0;
	}

	Mat gray_img;
	cvtColor(org_img, gray_img, CV_BGR2GRAY);
	Mat bin_img;
	adaptiveThreshold(gray_img, bin_img, 255.0, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 101, 0);

	Mat closed_img;
	Mat structuring_element(20, 20, CV_8U, Scalar(1));
	morphologyEx(bin_img, closed_img, MORPH_CLOSE, structuring_element);

	// find contours
	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;

	Scalar colour(255, 0, 0);
	Mat copied_closed_img = closed_img.clone();
	findContours(copied_closed_img, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_NONE);

	for (int i = 1; i < contours.size(); i++)
	{
		drawContours(org_img, contours, i, colour, CV_FILLED, 8, hierarchy);
	}

	imshowAfterResize("original image", org_img, 320);
	imshowAfterResize("bin image", bin_img, 320);
	imshowAfterResize("closed image", closed_img, 320);

	waitKey(0);
	return 0;
}


반응형
LIST

Mathematical morphology (모폴로지)

Thresholding에 의해 얻어진 binary image를 좀 더 가공하여 완성도를 높이기 위해서는  morphology를 이용한다. 가공의 의미는 noise나 너무 작은 feature들을 제거하는 것을 의미한다. 

Morphology는 structuring element (SE)라는 것을 이용하는데, 이것은 마치 bit mask와 같은 것이다. Bit-mask가 1차원인데 반해, SE는 2차원이다. SE를 이미지 위에 슬라이딩 시켜가면서 logical operation을 수행하여 결과 이미지를 만든다. original image는 변하지 않고, 새로운 이미지가 만들어진다. 이 분야는 독자적인 학회를 가지고 있을 정도로 이론적으로 정립되어 있다. 

SE는 여러 가지 모양을 가질 수 있는데, 일반적으로 정사각형 모양이면 isotropic이라고 한다. SE를 구성하는 픽셀 들 중의 하나를 origin이라고 한다. 이 origin을 이미지의 각 픽셀마다 매칭시켜가면서 morphology가 수행된다. 보통은 이미지의 좌상단에 origin을 두고, 이후에는 좌에서 우로, 위에서 아래로 움직여가면서 순차적으로 수행한다. 수행결과가 나오면 origin 위치의 이미지 픽셀의 값이 결정된다. 아래 그림은 3x3, 5x5 isotropic SE들의 예를 보여주며, 가운데 x로 표시한 픽셀이 origin이다.


대표적인 morphological operation에는 dilation, erosion, opening, closing 등이 있다. 여기서는 각각에 대해서 알아본다.


1. Dilation

사전적인 의미로는 확장하다, 크게하다라는 의미로, 이미지 상의 물체를 확대시키는 효과를 가진다. 쉽게 이해하자면, SE origin과 매칭된 이미지 상의 픽셀을 SE의 모든 다른 픽셀들로 확대한다고 보면 된다. 아래 그림에서는 3x3 isotropic SE을 이용하여 dilation을 수행할 때 어떠한 결과가 생기는지를 간략히 보여준다. 

OpenCV 는 dilation을 위한 함수 dilate(binary_image, dilated_image, Mat())를 제공한다. 이 함수에 의해서, binary_image는 default 3x3 SE를 이용하여 dilation되고, 그 결과는 dialated_image에 저장된다. 다른 SE를 사용하려면, 예를 들어, 5x5 isotropic이라면, 아래와 같이 한다.
Mat structuring_element(5, 5, CV_8U, Scalar(1));
dilate(binary_image, dilated_image, structuring_element);

아래 두 개의 예는 dilation의 적용결과를 보여준다. 그림 1과 그림 2는 동일한 컬러 입력이미지에 대해서, 두 가지 thresholding방식을 적용하고, dilation (3x3 isotropic structuring element)을 수행한 결과이다.

그림 1은 왼쪽의 입력이미지에 대해 Otsu threshoding을 이용하여 중간결과를 얻었다. 입력 이미지 전체의 조명상황이 고르치 않기 때문에, 하나의 threshold를 이용하는 Otsu방법의 결과에는 한계가 있다. 좌하단 영역이 소실된 것을 알 수 있다. 여기에 dilation을 수행하여 오른쪽 결과를 얻었는데, 글자들이 굵어진 것을 알 수 있다.

[그림 1: Otsu thresholding을 이용한 binary image에 dilation을 적용한 결과]


그림 2는 영역별로 다른 threshold를 사용하는 adaptive thresholding을 적용한 결과로 고르지 않은 조명상황에도 불구하고, 이미지 전체가 고루 반영되는 것을 볼 수 있다. (중간이미지). 하지만 전체적으로 noise가 상당히 많은 것을 볼 수 있다. 여기에 dilation (3x3 isotropic SE)을 적용하면 noise의 상당부분이 사라진 것을 볼 수 있고, 글자체도 전반적으로 굵어진 것을 알 수 있다. 

[그림 2: Adaptive thresholding (block size=51)을 이용한 binary image에 dilation을 적용한 결과]


2. Erosion
Dilation의 반대로, 이미지상의 물체를 축소(shrinking)하는 역할을 한다. 이를 위해 SE를 완전히 포함할 수 있는 이미지상의 픽셀들만 결과 이미지에 남게 된다. 그 결과, 물체크기가 줄어들게 되고, 이미지상의 noise나 narrow feature 등이 사라지게 되는 효과가 있다. Erosion의 또다른 용도는 매칭으로도 사용할 수 있다는 것이다. SE를 찾고자 하는 패턴으로 지정하고 erosion을 실행하면결과이미지는 패턴이 발견되는 위치들이 된다.
OpenCV는 이를 지원하는 함수 erode를 가지고 있다. 예를 들어, default 3x3 isotropic structuring element 에 의한 erosion은 다음과 같이 수행한다.

erode(binary_image, eroded_image, Mat());

다른 형태의 SE를 사용하려면, 예를 들어 5x5 isotropic SE인 경우 아래와 같다. 
Mat structuring_element(5,5, CV_8U, Scalar(1));
erode(binary_image, eroded_image, structuring_element);

아래 그림 3은 dilation 예에서 사용한 동일한 입력이미지에 대해서 erosion을 수행한 결과이다. 중간이미지는 이전과 마찬가지로 adaptive thresholding (block size = 51)을 적용하였다. 오른쪽 결과는 erosion결과를 보여주는데, 3x3 SE를 온전히 품을 수 있는 흰색영역이 적다보니 대부분에서 글자가 사라지는 부정적인 결과를 보여주고 있다. 


[그림 3: Adaptive thresholding (block size=51)을 이용한 binary image에 erosion을 적용한 결과]

3. Opening

​Opening은 erosion과 dilation의 조합이다. 이미지에 erosion을 먼저 적용한 후, 그 결과에 다시 dilation을 적용하는 것이다. 이 때, 같은 structuring element를 이용한다. 그 결과, 이미지에서 noise와 narrow feature들이 제거되지만, 물체의 크기는 보존된다.


OpenCV는 opening을 위한 함수를 제공한다. 예를 들어, 5 by 5 isotropic structuring element를 이용한 opening은 다음과 같다.

Mat five_by_five_element (5, 5, CV_8U, Scalar(1));
- 5 x 5 isotropic structuring element만들기
morphologyEx(binary_image, opened_image, MORPH_OPEN, five_by_five_element);
- opening 수행


아래 그림 4는 opening을 적용한 결과이다. 위의 예제들과의 차이점이라면, 원본크기의 이미지에 opening을 모두 적용한 후 이미지 크기를 줄였다는 것이다. 중간이미지와 오른쪽 이미지를 비교해 보면, 중간이미지의 흰색이 많이 없어지고 검정색이 늘었다는 것을 볼 수 있다. 이것은 5x5 SE가 포함될 흰색부분이 많이 없기 때문에 그렇게 된 것이다.

[그림 4: Adaptive thresholding (block size=101)을 이용한 binary image에 opening을 적용한 결과. 5x5 isotropic structuring element를 적용.]


4. Closing

Mathematical morphology에서 erosion과 dilation은 서로 dual operation의 관계라고 하는데, 이는 erosion이 물체 픽셀들을 깍아내는 것이라면, dilation은 덧붙여나가는 것과 같이 서로 정반대의 역할을 하기 때문이다. Erosion에 의해서는 물체가 작아지지만, dilation은 물체가 커지게 된다. erosion과 dilation간의 dual 관계와 마찬가지인 것이 openning과 closing이다.

Closing은 opening과 유사하지만 두 동작의 적용순서가 거꾸로이다. 우선 dilation을 적용하여 결과를 얻은 후 거기에 erosion을 수행한다. 역시 마찬가지로 같은 structuring element를 이용한다. Closing은 narrow gap등을 메꾼 후, 물체 경계를 smoothing하는 효과가 있으되, 물체의 원래 크기가 보존된다.

OpenCV는 closing을 위한 함수를 제공한다. 예를 들어, 5 by 5 isotropic structuring element를 이용한 closing은 다음과 같다.

Mat five_by_five_element (5, 5, CV_8U, Scalar(1));
- 5 x 5 isotropic structuring element만들기
morphologyEx(binary_image, closed_image, MORPH_CLOSE, five_by_five_element);
- closing 수행

아래 그림 5는 closing 연산을 적용한 결과이다. 중간이미지는 위의 opening의 경우와 동일하지만, closing연산의 결과 (맨오른쪽 이미지)에서 글씨가 훨씬 더 잘 인식되는 것을 알 수 있고, 배경에 해당하는 검은 색이 대부분 사라진 것을 알 수 있다.

[그림 5: Adaptive thresholding 결과에 closing을 적용한 결과]


5. Closing followed by Opening
위의 closing의 결과에서 볼 수 있듯이, opening보다는 noise 제거 측면에서 효과가 좋다. 따라서 일반적으로 opening과 closing은 연속적으로 적용되어 이용된다. 이미지에 closing을 먼저 적용하고, 다시 opening을 수행하는데,  같은 structuring element를 이용한다. 이렇게 하면, 이미지를 전체적으로 깨끗하게 정리하는 효과가 있다.

아래 그림은 closing과 opening을 순차적으로 적용한 결과이다. 우측끝에서 두 번째가 closing을 적용한 결과이고, 맨 오른쪽이 opening까지 적용한 결과이다. 두 결과는 대체적으로 동일해 보이지만, 글자인식면에서 맨 끝의 "Test" 글자가 opening이 후에 보다 선명해진 것을 볼 수 있다. (아래 그림을 확대해서 확인)

[그림 6: closing과 opening을 차례로 적용한 결과]


6. Morphology on Gray or Color Image
기본적으로 morphology는 binary image에 적용된다. 하지만 grey-scale 이나 컬러이미지에도 적용이 가능하다. Grey-scale 이미지에 적용하는 방법은 조금 복잡한데, 각 grey-level별로 이미지를 따로 만든 후, morphology를 적용하고 이미지들을 하나로 합친다. 즉, grey-level 1을 가지는 픽셀들만 모아서 이미지1를 만들고, 이런 식으로 이미지2, 이미지3,..., 이미지 255를 만든다. 단, 이미지 0은 따로 만들 필요가 없다. 그리고 각 이미지i에 대해서 morphology 연산을 수행한 후, 각 결과이미지들을 ORing 하여 하나로 합친다.

컬러이미지에 morphology연산을 적용하는 방법도 gray-level 이미지와 유사하되, R, G, B채널별로 나누어서 각 채널을 grey-scale 이미지인 것 처럼 처리하고, 그 결과들을 합친다.

OpenCV에서 제공하는 morphology 관련 함수들은 grey-scale과 컬러이미지에 대해서도 동작한다. 방식은 위에서 설명한 방식을 따른다. 이 함수들을 사용하기 위해서는 입력영상으로 grey-scale과 컬러이미지으로 지정하면 된다.


아래 그림 7과 8은 컬러이미지에 각각 erosion (그림 7)과 dilation (그림 8)을 적용한 결과이다. 우선 그림 7부터 살펴보면, erosion은 흰색 (밝은 색)을 깍아내는 경향이 있으므로 어두운 색인 글씨들이 더 굵어지고 선명해진 것을 볼 수 있다. 그림 8의 dilation은 흰색이 확장되는 경향이 있으므로 어두운 색인 글씨들이 가늘어지고 끊어지는 것을 볼 수 있다.


[그림 7: Erosion on Color image]


[그림 8] Dilation on Color Image









반응형
LIST


Image Binarization (이미지 이진화)


Grey-scale image는 한 픽셀당 8비트 밖에 차지하지 않기 때문에 컬러이미지보다 크기가 작아 처리하기 쉽다. Binary image의 경우, 픽셀이 가질 수 있는 색은 단 2가지, 흰색 혹은 검정색뿐이다. 따라서 한 픽셀당 1비트만 있어도 되기 때문에, Grey-scale image보다 처리가 더 간편하다. 실제로 많은 영상처리 기술들이 binary image를 대상으로 하고 있기 때문에, 입력이미지가 컬러일 경우, 이를 binary image로 변환하는 것이 필요하다. Binary image는 컬러이미지 뿐만 아니라 다양한 영상 (비컬러 메디컬 이미지)들로부터 생성이 가능하므로, 응용범위가 매우 넓다고 할 수 있다.



우선, 컬러 사진을 흑백사진으로 바꾸는 것만 생각해 보자. 우리가 흔히 생각하는 흑백사진은 실은 gray 사진이고, 진정한 흑백사진은 흰색과 검정색, 단 2가지 색으로만 이루어진 영상이다. 컬러이미지를 흑백사진으로 바꾸기 위해서는 그림 1에서 보여진 바와 같이 2단계를 거쳐야 한다. 첫 번째 단계에서는 컬러를 gray-level로 바꾸고, 두 번째 단계에서 gray-level을 흑백(black/white)로 바꾼다. 그림 1.5는 red, green, blue의 컬러이미지가 gray-level을 거쳐 흑백이미지로 바뀌는 과정을 보여준다.



[그림 1. 컬러이미지를 흑백사진으로 바꾸는 단계]



[그림 1.5. 컬러이미지가 gray-level을 거쳐 흑백으로 변화되는 과정]


Thresholding과 Threshold


그림 1.5에서 컬러사진을 gray 사진으로 바꾸는 것은 간단하다. OpenCV에서는 함수 cvtColor( )를 제공한다. 예를 들어 입력 컬러이미지 input_img를 출력 gray-level 이미지 output_img로 바꾸는 것은 다음과 같다.


cvtColor(input_img, output_img, CV_BGR2GRAY);


생성된 gray image의 각  픽셀들은 0~255까지의 값을 갖는다. 0은 검정색, 255는 흰색, 127은 중간정도의 회색으로 생각하면 된다. 이제 gray image를 black & white image (흑백이미지)로 바꾸는 방법에 대해 알아보자.


Gray image를 흑백 이미지로 바꾸는 쉬운 방법은, 각 픽셀값을 검정색 (0) 혹은 흰색 (255)으로 전환하는 것이다. 즉, 0과 255 이 외에는 다른 값을 가지지 못하게 하면 흑백 이미지가 될 것이다. 하지만 어떤 기준으로 픽셀을 검정색 혹은 흰색으로 바꿀 것인가? 한 가지 방법은 어떤 기준값을 세우고, 그 값보다 작으면 검정색, 크면 흰색으로 할 수 있을 것이다. 이렇게 하는 것을 thresholding이라고 하고, 기준값을 threshold(우리말로는 임계값)라고 한다. 아래 그림 2는 주어진 컬러이미지에 대해 threshold = 127로 설정하고 thresholding한 결과이다. 




[그림 2. Threshold=127을 이용한 binary threshing binarization 결과]



Thresholding 결과 생성되는 binary image는 바로 사용되기 보다는 후처리 과정 (post processing)과정을 거쳐서 noise나 불필요한 feature들을 제거하게 된다. 이를 위해 mathematical morphology 방법 등을 이용한다. 후처리 과정이 끝나면, 이미지에서 특정 부분만 골라서 추출하는 segmentation 기술 등을 적용할 수 있는데, 이와 관련해서는 connected component analysis 처리기술이 사용된다. 우선은 thresholding방법에 대해 자세히 알아보고 후처리 방법들은 추후에 설명한다.


Thresholding의 성능개선

Thresholding은 하드웨어 기반으로 수행할 수도 있고, 소프트웨어적으로는 lookup table을 이용해서 할 수도 있다. Lookup table은 크기 256인 일차원 배열인데, 배열의 인덱스는 grey-level값에 해당한다. 각 배열요소의 값은 thresholdinig에 의해 지정할 새로운 값(0 혹은 1, 0 혹은 255)이 된다. 이렇게 되면, 픽셀값을 비교하는 과정없이 배열인덱스로부터 thresholding값을 바로 찾을 수 있기 때문에 속도를 개선시킬 수 있다. 한 장의 이미지 할지라도, 256개 grey-level에 대한 테이블을 만드는 수고는 초대형 이미지의 각 픽셀에 대해 thresholding을 수행할 때의 연산횟수를 고려하면 충분한 가치가 있다. 예를 들어, 1080 * 720 이미지일 경우, thresholding에 필요한 비교 연산의 횟수가 충분히 크기 때문에 테이블을 구성해서 사용하는 편이 훨씬 유리할 수 있다. <<추후 성능비교하는 실험결과를 공개하겠음>>


Threshold의 결정


Thresholding에서 가장 중요한 문제는 threshold V를 어떻게 설정하느냐 하는 것이다. 너무 큰 V값을 설정하면, gray-scale image의 픽셀들이 대부분 0 (검정색)값을 갖게 될 것이고, 반대의 경우에는 255(흰색)가 될 것이다. 여기서는 사진에 따라 적정한 threshold값을 결정하는 방법 들 중 많이 사용되는 대표적인 두 가지에 대해서 살펴보자. 



1. Otsu 방법


이 방법은 threshold V에 의해 이미지의 픽셀들이 두개의 집합(흰색이 될 것과, 검정색이 될 것)으로 나뉘어 질 때, 각 집합에 속한 픽셀들의 값 (원래 픽셀값)으로부터 구해지는 분산이 최소화되는 그런 threshold V를 구하는 것을 목적으로 한다. 말이 좀 어렵기 한데, 분산이 최소화 된다는 것은 비슷한 픽셀값을 가지는 것끼리 같은 집합에 모일 수 있는 threshold V라는 뜻이다.


Otsu방법에 의해 threshold V가 계산되는 과정을 순차적으로 살펴보자. 우선 임의의 값으로 threshold V를 설정한다. 이 때 threshold V에 의해서 이미지의 픽셀들이 검정색으로 될 것과 흰색으로 될 것으로 나뉠 것이다. 검정색이 될 픽셀들의 집합을 black_pixel_set이라고 하고, 흰색이 될 픽셀들의 집합을 white_pixel_set이라고 하자.


우선 black_pixel_set에 속하게 된 픽셀들만을 생각하자. 이 픽셀들은 검정색이 될 것이지만, 원래는 gray-level값을 가지고 있다. 다만, gray-level값이 threshold V보다 작아서 검정색이 될 운명이 된 것이다. 이제 이 가엾은 픽셀들의 원래 gray-level값들을 가지고 평균을 구해보자. 그리고 이 평균을 이용하면 분산도 쉽게 구할 수 있을 것이다. 만약 픽셀값들이 서로 천차만별이면, 분산은 커질 것이고, 서로 비슷비슷하면 분산은 작아질 것이다. 어쨌든 black_pixel_set의 분산은 구했다. 이제 white_pixel_set 픽셀들에 대하여 동일한 계산을 해보면, white_pixel_set의 분산도 구할 수 있다. 이러한 두 개의 분산들은 threshold V를 이용했을 때 구한 것이다. 그런데, 만약 threshold V 대신 다른 threshold W를 사용하면 두 집합의 분산들이 달라질 것이다. threshold V일 때의 분산들이 threshold W일 때의 분산보다 작으면, Otsu방법은 threshold V를 선택한다. 이런 식으로 가능한 모든 threshold 값들에 대해서 분산을 모두 구해보고, 그 중에서 가장 작은 분산을 가지는 threshold를 선택하여 이진화를 진행하는 방법이 Otsu 방법이다.


OpenCV 라이브러리는 함수 threshold( )에서 Otsu 방법을 지원한다. 이 함수를 이용하여 앞의 그림 2의 컬러이미지에 대하여 이진화 결과는 아래 그림 3과 같다. 그림 2와 비교하면, 보다 많은 내용들을 읽을 수 있다.


[그림 3. Otsu방법을 이용한 그림 2의 이진화 결과]



2. Adaptive threshold 방법


두 번째로 살펴볼 threshold를 결정하는 방법은 adaptive threshold 방법이다. 이 방법은 이미지를 같은 크기의 여러 소구역으로 나누고, 각 구역별로 다른 threshold를 이용하는 방법이다. 그야말로 이미지 위치별로 "adaptive" (적응적)인 방법이다. 이런 방법이 나온 이유는 양지와 음지가 모두 들어있는 사진을 상상해보면 이해하기 쉽다. 양지에서는 모든 것이 밝으므로 threshold 값이 높아야 뭔가 구별해 낼 수 있을 것이다. 반대로 음지에서는 모든 것이 어두우므로 threshold값을 어느 정도 낮추는 것이 맞다. 이 경우에 양지와 음지에 각각 따로 threshold를 사용할 수 있도록 하면 좋을 것이다. 이것이 바로 adaptive threshold의 기본 아이디어다.


이를 위해서 이미지를 같은 크기의 여러 구역으로 나누고, 각 구역별로 threshold값을 구한다. 이 때 threshold는 앞서 설명한 Otsu방법에 의해서 구할 수도 있고, 다른 방법을 써도 된다. 각 구역별로 threshold가 정해졌으면, 픽셀에 적용할 threshold는, 그 픽셀에서 가까운 4개 구역의 threshold값들을 interpolation해서 계산한다.


OpenCV's adaptive threshold


OpenCV는 위에서 설명한 adaptive threshold 방법과는 다른 방식으로 adaptive threshold를 구현하고 있다. OpenCV에서 제공하는 함수 adaptiveThreshold( )는 thresholding을 적용하고자 하는 픽셀에 대해 그 주위의 픽셀값들의 평균값과의 차이를 이용한다. 이 차이가 0보다 크면, thresholding결과는 255가 되고, 그렇지 않으면 0으로 설정한다. 주변 픽셀의 범위는 함수의 인수로 지정할 수 있으며, 3, 5, 7,,, 등등의 값을 쓸 수 있다.


함수 adaptiveThreshold( )의 원형은 다음과 같다. (OpenCV 3.1.0, 링크)



- maxValue: thresholding결과 흰색으로 설정할 때 사용하는 값

- adaptiveMethod: 주위 픽셀값들의 평균을 구할 때 사용하는 방법으로 단순평균(ADAPTIVE_THRESH_MEAN_C)과 가우시안 평균 방법(ADAPTIVE_THRESH_GAUSSIAN_C), 두 가지가 가능하다. 가우시안 평균은 weight를 이용하는 방법으로 중앙픽셀에 가까울수록 높은 weight를, 멀어질수록 낮은 weight를 적용하여 평균을 구하는 방법니다.

- tresholdType: thresholding방법을 지정하는 것으로 THRESH_BINARY 혹은 THRESH_BINARY_INV 둘 중 하나가 되어야 한다. THRESH_BINARY_INV는 THRESH_BINARY가 할당하는 값을 반대로 하는 것이다.

- blockSize: 주위 블럭들의 범위 크기

- c: 상수로서 평균값과의 차이를 구하기 전에 빼는 값으로, 보정을 위해 사용한다.


아래 그림은 adaptive thresholding (block size = 101)을 위 그림 2에 적용한 결과이다. 기존 Otsu 방법보다 게시판에 있는 글들이 훨씬 더 많이 드러나는 것을 볼 수 있다.

[그림 3.5. Adaptive thresholding을 그림 2에 적용한 결과]


아래 두 장의 그림은 동일한 이미지에 대해 OpenCV의 adaptiveThreshold( )를 수행한 결과이다. 차이가 있다면 위의 그림은 block size = 101이고, 아래 그림은 block size = 501이다. Block size를 크게 하면, detail이 최소화되는 것을 알 수 있다. 


[그림 4. Adaptive thresholding: block size = 101 ]


[그림 5. Adaptive thresholding: block size = 501]



Band thresholding과 Semi thresholding


이진화 관련해서는 이 외에도 band thresholding 과 semi-thresholding 등이 있다. band thresholding이란 threshold가 범위로 주어지는 thresholding 방법이다. Semi-thresholding은 일반 thresholding의 결과가 흑/백인 반면, gray level로 나오는 것이 특징이다. 예를 들어, threshold값보다 큰 경우에 흰색으로 설정하는 것이 아니라, 픽셀의 원래 gray-level값으로 설정한다.


Multispectral thresholding

컬러 이미지에 직접적으로 thresholding을 어떻게 적용할 것인지에 대해서는 명확히 정해진 방법이 없다. 예를 들어, 컬러 이미지의 r, g, b 채널별로 각각 thresholding을 적용할 수 있고, 컬러 이미지를 그대로 사용하여 3D 컬러공간에서 threshold를 설정할 수도 있다.



반응형
LIST

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

3차원 히스토그램 (3 dimensional histogram)


지금까지는 1차원 히스토그램을 이용해왔다. 즉 픽셀이 가질 수 있는 값 별로 몇 개씩의 픽셀들이 실제로 그 값을 가졌는지를 계산했다.


그러나, 컬러이미지의 경우, 하나의 픽셀은 R, G, B 혹은 H, L, S의 3가지 값을 가지기 때문에 1차원 히스토그램이 적절하지 않다. 그래서 3차원 히스토그램을 이용해야 한다. 3차원은 아래 그림과 같이 생각하면 된다. 아래는 HLS 포맷의 이미지의 픽셀들을 H, L, S를 축으로 하는 3차원 히스토그램으로 나타낸 것이다. 좌표 (H, L, S)의 값은 그 조합을 가지는 픽셀들의 갯수이다. 




그런데, H, L, S가 가질 수 있는 값들의 조합이 너무나 많다. 예를 들어, 각각이 20개씩의 값만 가진다 하더라도 20*20*20 = 8000개의 조합이 나온다. 그래서 각 축의 histogram bin의 개수를 4개, 8개, 혹은 적절한 값으로 하면 적당한 복잡도의 히스토그램을 얻을 수 있다.


3D 히스토그램을 그리는 순서도

  1. RGB이미지를 읽어들인다.
  2. HLS포맷으로 전환한다.
  3. H, L, S를 각 축으로 하고, 각 축마다 8개의 bin이 있는 3차원 histogram을 계산한다.
  4. 계산된 histogram의 값을 출력한다.


 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;

#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);
		cout << imgNames[i] << "-----------------------" << endl;
		for (int x = 0; x < BINS; x++)
		{
			for (int y = 0; y < BINS; y++)
			{
				for (int z = 0; z < BINS; z++)
				{
					cout << histogram[i].at<float>(x, y, z) << " ";
				}
			}
		}
		cout << 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


HLS 모델과 RGB 모델간의 관계


RGB 모델은 red, green, blue를 섞어서 색깔을 만들어내므로 직관적으로 이해가 쉽다. 그러나 같은 식으로 HLS 모델을 생각하는 것은 쉽지 않다. 

그래서 HLS에서 RGB로의 변환을 통해서 두 모델 간에 어떤 관계가 있는지를 살펴보는 것은 HLS모델 이해에 도움이 된다.

두 모델간의 변환을 쉽게 보여주는 유용한 사이트를 통해 아래와 같은 실험을 진행해보았다.


<실험 1>

Hue를 0으로 하고, 채도에 해당하는 S(Saturation)를 100%하여 원색을 만들고, 밝기에 해당하는 L(luminance)는 50%로 설정하였다. 이렇게 되면 Hue의 각 값에 해당하는 색을 볼 수 있다. 이 경우, 빨간색인 것을 알 수 있다.


<실험 2>

L=100으로 하였다. 이것은 색을 최대로 밝게 만든 것이다. 이 조건에서는 H와 S를 아무리 변화시켜도 흰색밖에는 얻을 수가 없다.  


<실험 3>

L=0으로 하였다. 색을 최대한 어둡게 한 것이다. 이 경우에도 H와 S를 아무리 변화시켜도 검정색 밖에는 얻을 수가 없다. 비유적으로 이해하자면, L은 전등스위치라고 생각하면 되겠다. 전등을 켜면 (L=100) 모든 것이 하얗게 보이고, 끄면 (L= 0) 모든 것이 어둡게 보이는 것이다.


<실험 4>

전등은 절반만 켜놓고 (L=50), 원색정도를 나타내는 S=0으로 설정하여, 색을 갖지 못하도록 하였다. 이 조건에서는 H를 아무리 변화시켜도 회색계통 밖에는 얻을 수가 없다. 비유적으로 S=0이면 흑백사진이고, S값이 커질수록 컬러사진이 되는 것으로 이해하면 된다. 


<실험 5>

전등은 절반만 켜고 (L=50), 컬러사진으로(S=100) 만들어 놓은 상태에서는 H를 변화시키면 다양한 원색들을 볼 수 있다. 따라서 H는 색을 결정하는 주요인자인 것이다.   


HLS와 RGB간의 대응관계


아래 그림 1은 주요색들에 대해서 HLS와 RGB간의 대응관계를 보여준다.


<그림 1> 주요색들의 HLS와 RGB 대응관계 



반응형
LIST




사진에서 어떤 계열의 색이 가장 많은지 알아보자.


RGB모델을 사용하는 영상에서는 이 질문에 쉽게 답하기가 어렵다. 왜냐하면 R, G, B 각각이 256가지씩 값을 가질 수가 있어서 

즉 이만큼의 색 각각에 대해서 몇 개의 픽셀들이 그 값을 갖는지를 알아봐야 하기 때문이다.


HLS모델 영상이라면 문제가 조금은 쉬워진다. 우선 HLS모델이 어떤 것인지는  여기를 보면 된다. 사진에서 어떤 계통의 색이 가장 많은지는 픽셀들이 가지고 있는 Hue, Luminance, Saturation 중에서 Hue 정보만 보면 된다. 게다가 이것은 0 ~ 360까지 값을 갖기 때문에 픽셀들을 이것에 따라 분류하기만 하면 된다. 물론 Hue 정보만 가지고는 정확히 어떤 색이라고 말할 수는 없다. 다만 어떤 계열( 빨간색, 파란색, 초록색 등)의 색인지 만을 판별할 수 있다. 이 정도 정보만 하더라도 이미지를 검색하거나 분류할 때 요긴하게 사용할 수 있다.


좀 더 구체적으로 얘기를 하자면 Hue값에 대해서 사진의 히스토그램을 구하고, 가장 높은 빈도수를 갖는 Hue 값 영역의 색을 사진의 대표 색깔로 하면 되는 것이다. 


복숭아와 레몬

예를 들어, 그림 1은 복숭아와 레몬을 찍은 사진이다. 사람은 대표 색깔을 쉽게 알 수 있는데, 사진에서 빨간색과 노란색 계열이 가장 많다. Hue채널에 대한 히스토그램도 그렇게 나왔을까? 그림 2는 해당하는 Hue채널에 대한 히스토그램으로, 예상한대로 빨간 색 계통의 빈도수가 가장 높고, 그 다음은 노란색이라는 것을 알 수 있다. 히스토그램 선을 그릴 때, 해당 Hue 채널의 색으로 표시했기 때문에 그 영역이 무슨 색인지를 쉽게 알 수 있다. 


<그림 1> 복숭아와 레몬을 찍은 사진. 


<그림 2> 그림 1의 Hue채널에 대한 히스토그램. 각 Hue영역에 해당하는 빈도수를 해당하는 색으로 그렸다.


포도밭

그림 3은 포도밭 사진으로 녹색 계열의 색이 대표색이라고 할 수 있다. 물론 지붕의 빨간색, 하늘의 파란 색도 있다. 이 사진에 대한 Hue 히스토그램은 그림 4이다. 가장 많은 색은 붉은 색, 노란색과 하늘색이고, 오히려 녹색은 그다지 많지 않다. 결과가 직관적으로 이해되지는 않지만, 일단 넘어가자.


<그림 3> 포도밭을 찍은 사진


<그림 4> 그림 3 사진의 Hue채널에 대한 히스토그램


해변

그림 5는 해변 이미지이고, 그림 6은 해당하는 히스토그램이다. 결과에서 보듯이 하늘색 계통이 압도적으로 가장 많다는 것을 알 수 있다.

<그림 5> 해변사진


<그림 6> 그림 5 해변사진에 대한 Hue 히스토그램


녹색실험

그림 3(포도밭)에 대한 히스토그램 (그림 4) 결과가 미심쩍어서 한 가지 실험을 해보았다. 그림 7과 같이 녹색 이미지에 대해서 Hue 히스토그램을 구해본 것이다. 포도밭 녹색을 제대로 검출할 수 있는지 검증하기 위한 목적이다. 결과 히스토그램에서는 녹색이 압도적으로 많이 나왔기 때문에 녹색을 검출하는데 문제가 없었다는 것을 확인할 수 있다. 그렇다면 왜 포도밭에서는 붉은색과 노란색이 왜 그리 많이 나왔던 것일까? 좀 더 생각해 볼 문제다. 

<그림 7> 실험용 녹색 이미지

<그림 8> 그림 7에 대한 Hue 히스토그램


소스코드

위의 실험에 사용된 소스 프로그램이다. 


  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
148
149
150
151
152
153

#include <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std; #define SCALE 0.2 /* given hue value, which is original_hue/2 calculate a corresponding RGB. It is assumed that luminance is 0.5 (50%), and saturation is 1.0(100%) */ Scalar cvtHLStoRGB(double hue) { double _hue = 2 * hue; double C = 1.0; double X = (double)(1.0 - abs((double)(((int)(_hue/60.0) % 2) - 1))); double m = 0.0; double R = 0.0; double G = 0.0; double B = 0.0; if (_hue >= 0 && _hue < 60) { R = C*255.0; G = X*255.0; } else if (_hue >= 60 && _hue < 120) { R = X*255.0; G = C*255.0; } else if (_hue >= 120 && _hue < 180) { G = C*255.0; B = X*255.0; } else if (_hue >= 180 && _hue < 240) { G = X*255.0; B = C*255.0; } else if (_hue >= 240 && _hue < 300) { R = X*255.0; B = C*255.0; } else if (_hue >= 300 && _hue < 360) { R = C*255.0; B = X*255.0; } return Scalar(B, G, R); } int main(int argc, char** argv) { if (argc != 2) { cout << " Provide image name to read" << endl; return -1; } Mat inputImg, dispImg; Mat hlsImg; inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 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(inputImg, hlsImg, CV_BGR2HLS); // separate into three images with only one channel, H, L, and S vector<Mat> hls_images(3); split(hlsImg, hls_images); //-------------------------------------- // H, L, S 채널별로 histogram을 계산 //-------------------------------------- // Prepare three histograms for H, L, S MatND* histogram = new MatND[hlsImg.channels()]; // Each image has only one channel. const int* channel_numbers = { 0 }; float channel_range[] = { 0.0, 255.0 }; const float* channel_ranges = channel_range; // 255-bins for histogram int number_bins = 255; // calculate histograms one at a time, H, L, and S // histogram[0] contains H's // histogram[1] contains L's // histogram[2] contains S's for (int chan = 0; chan < hlsImg.channels(); chan++) { calcHist(&(hls_images[chan]), 1, channel_numbers, Mat(), histogram[chan], 1, &number_bins, &channel_ranges); } //------------------------ // Plot the histograms in each image //------------------------ int hist_w = 512; int hist_h = 400; int bin_w = cvRound((double)hist_w / number_bins); // histogram image for H channel Mat histImageH(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0)); normalize(histogram[0], histogram[0], 0, histImageH.rows, NORM_MINMAX, -1, Mat()); // histogram image for L channel Mat histImageL(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0)); normalize(histogram[1], histogram[1], 0, histImageL.rows, NORM_MINMAX, -1, Mat()); // histogram image for S channel Mat histImageS(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0)); normalize(histogram[2], histogram[2], 0, histImageS.rows, NORM_MINMAX, -1, Mat()); for (int i = 1; i < number_bins; i++) { line(histImageH, Point(bin_w*(i - 1), hist_h - cvRound(histogram[0].at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(histogram[0].at<float>(i))), cvtHLStoRGB((double)i), 2, 8, 0); line(histImageL, Point(bin_w*(i - 1), hist_h - cvRound(histogram[1].at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(histogram[1].at<float>(i))), Scalar(0, 255, 0), 2, 8, 0); line(histImageS, Point(bin_w*(i - 1), hist_h - cvRound(histogram[2].at<float>(i - 1))), Point(bin_w*(i), hist_h - cvRound(histogram[2].at<float>(i))), Scalar(0, 0, 255), 2, 8, 0); } namedWindow("Original", CV_WINDOW_AUTOSIZE); namedWindow("histogramH", CV_WINDOW_AUTOSIZE); namedWindow("histogramL", CV_WINDOW_AUTOSIZE); namedWindow("histogramS", CV_WINDOW_AUTOSIZE); moveWindow("Original", 100, 100); moveWindow("histogramH", 120, 120); moveWindow("histogramL", 140, 140); moveWindow("histogramS", 160, 160); imshow("Original", dispImg); imshow("histogramH", histImageH); imshow("histogramL", histImageL); imshow("histogramS", histImageS); waitKey(0); return 0; }


소스코드 설명


line 13-54

함수 cvtHLStoRGB( )는 Hue값을 BGR 색으로 만들어준다. 이 함수는 Hue 히스토그램에서 Hue영역에 상응하는 색으로 선을 그리는데 사용된다. Hue를 BGR로 변환하는 아래 공식을 프로그램 한 것이다.



line 77-78

HLS영상을 채널별로 구분하여 별도의 영상으로 나누는 과정이다.


line 85-103

채널별로 나뉘어진 영상들에 대해 각각 히스토그램을 계산하는 부분이다. 채널별로 3번에 걸쳐 함수 calcHist( )를 이용하여 히스토그램을 계산하고, 배열 histogram에 저장한다.


line 125-127

Hue 히스토그램에서 해당 Hue 영역의 색으로 선을 그리는 부분이다.

반응형
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

HLS형식의 이미지 채널별 분리


HLS영상은 Hue, Luminance, Saturation 채널을 가진 영상으로, 각각이 의미하는 바는 다음과 같다.


Hue: 색조라는 뜻. Color와 동의어. 일상적으로 color 단어가 많이 쓰임.

       Hue is synonymous with color.

Luminance: 밝기라는 뜻.

Saturation

1. 색채의 강도, 또는 색깔의 포화도. 즉, 흰색이나 회색이 들어있지 않은 색의 순수한 정도 또는 색의 선명도에 관한 언급.
2. 색의 3속성의 하나로 색의 강도나 순수한 정도.
3. 같은 색 계열에서 나타나는 색의 선명한 정도. 또는 그 차이. 아무 것도 섞지 않아 맑고 깨끗하며 원색에 가까운 것을 채도가 높다고 표현한다.
4. 색의 채도는 검정과 흰색이 포함된 양과 관련한다. 그러나 검정과 흰색, 그리고 회색은 색상이 없고 단지 ‘밝기(brightness)’만 있기 ‘때문에 무채색(achromatic color)’이라고 한다.

[네이버 지식백과] 채도 [saturation, chroma, 彩度] (만화애니메이션사전, 2008. 12. 30., 한국만화영상진흥원)



Hue와 Saturation간의 관계

Saturation이 높을수록 원색에 가깝고, 낮을 수록 회색에 가까와진다.



<그림 1> HLS원본영상

<그림 2> Hue 채널만 분리한 결과

<그림 3> Luminance 채널만 분리한 결과. 흰색영역이 밝은 부분이다.

<그림 4> Saturation 채널영상. 흰색으로 표시한 부분이 원색에 가까운 부분이다.




소스프로그램


 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

#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; inputImg = imread(argv[1], CV_LOAD_IMAGE_COLOR); cvtColor(inputImg, hlsImg, CV_BGR2HLS); vector<Mat> hls_images(3); split(hlsImg, hls_images); // H, L, S로 분리하는 부분 namedWindow("Original", CV_WINDOW_AUTOSIZE); namedWindow("H", CV_WINDOW_AUTOSIZE); namedWindow("L", CV_WINDOW_AUTOSIZE); namedWindow("S", CV_WINDOW_AUTOSIZE); moveWindow("Original", 100, 100); moveWindow("H", 120, 120); moveWindow("L", 140, 140); moveWindow("S", 160, 160); imshow("Original", inputImg); imshow("H", hls_images[0]); imshow("L", hls_images[1]); imshow("S", hls_images[2]); waitKey(0); return 0; }


반응형
LIST


YUV형식의 이미지를 채널별로 출력하기


YUV형식의 이미지는

3개의 채널, Y, U, V를 가지고 있지만

RGB와 달리 

Y채널에는 영상의 밝기,

U, V채널에는 색차정보를 가지고 있다.


아래는 YUV 영상과 이를 각 채널별로 나누어 출력한 것이다.


원본영상


Y채널 영상 (밝기에 대한 정보를 가지고 있다.)


U채널 영상


V채널 영상


소스코드


다음 프로그램은 YUV형식을 가진

이미지를 Y, U, V 채널요소별로 분리하여 출력한다.


 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
#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 yuvImg;

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

	cvtColor(inputImg, yuvImg, CV_BGR2YUV);
	vector<Mat> yuv_images(3);
	split(yuvImg, yuv_images);

	namedWindow("Original", CV_WINDOW_AUTOSIZE);
	namedWindow("Y", CV_WINDOW_AUTOSIZE);
	namedWindow("U", CV_WINDOW_AUTOSIZE);
	namedWindow("V", CV_WINDOW_AUTOSIZE);

	moveWindow("Original", 100, 100);
	moveWindow("Y", 120, 120);
	moveWindow("U", 140, 140);
	moveWindow("V", 160, 160);

	imshow("Original", inputImg);
	imshow("Y", yuv_images[0]);
	imshow("U", yuv_images[1]);
	imshow("V", yuv_images[2]);

	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

+ Recent posts