히스토그램 분석

  • 영상의 픽셀 값 분포를 보통 그래프의 형태로 표현한 것

    • 예를들어, 그레이스케일 영상에서 각 그레이스케일 값에 해당하는 픽셀의 개수를 구하고 이를 막대 그래프의 형태로 표현

    image

  • h : 함수의 이름

  • g : 그레이스케일을 나타내므로 g의 범위는 0부터 255 사이의 범위

  • Ng : 그레이스케일 값이 g인 픽셀의 개수를 나타냄

  • 픽셀 값이 0~7 사이인 단순한 형태의 영상의 히스토그램 예시

image

  • 어두운 픽셀(0~2)이 좀 많고, 중간 픽셀(3~4)은 많이 존재하지 않고, 밝은 픽셀(5~7)은 적당히 많은 것을 확인할 수 있음
  • 다만 히스토그램은 픽셀 값의 위치 정보는 표현하지 못함

정규화된 히스토그램(normalized histogram)

  • 히스토그램으로 구한 각 픽셀의 개수를 영상 전체 픽셀 개수로 나눈 것

image

  • 해당 그레이스케일 값을 갖는 픽셀의 비율 또는 확률 : image

image

  • p(g)의 값이 해당 그레이스케일 영상의 비율 값을 나타내는 것을 알 수 있음

  • 히스토그램으로 구한 각 픽셀의 개수를 영상 전체 픽셀 개수로 나눈 것

  • 직접 작성해서 구할 수도 있고, OpenCV 함수를 사용하여 구할 수도 있음

  • 직접 작성해서 구하는 코드

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

using namespace std;
using namespace cv;

int main(void)
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	// 히스토그램
	int hist[256] = {}; // 256개 크기의 
	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			hist[src.at<uchar>(y, x)]++;
		}
	}

	// 정규화된 히스토그램
	int size = (int)src.total();
	float nhist[256] = {};
	for (int i = 0; i < 256; i++) {
		nhist[i] = (float)hist[i] / size;
	}

	// 히스토그램 그래프 그리기
	int histMax = 0;
	for (int i = 0; i < 256; i++) {
		if (hist[i] > histMax) histMax = hist[i];
	}

	Mat imgHist(100, 256, CV_8UC1, Scalar(255));
	for (int i = 0; i < 256; i++) {
		line(imgHist, Point(i, 100),
			Point(i, 100 - cvRound(hist[i] * 100 / histMax)), Scalar(0)); // 가장 큰 히스토그램의 크기를 100 px로 지정
	}

	imshow("src", src);
	imshow("hist", imgHist);
	waitKey();
}
  • hist가 출력 되는 것을 확인할 수 있음
    • 가장 높은 길이가 100px로 출력 되고
    • lenna 영상의 경우 히스토그램이 0~255까지 full로 안 채워진 것을 알 수 있음 (아주 어두운 픽셀과 아주 밝은 픽셀은 존재하지 않음)

image

OpenCV 히스토그램 계산 함수

void 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 : 입력 영상의 배열 또는 입력 영상의 주소
  • nimages : 입력 영상의 개수
  • channels : 히스토그램을 구할 채널을 나타내는 정수형 배열
  • mask : 마스크 영상, 입력 영상 전체에서 히스토그램을 구하려면 Mat() 또는 noArray() 지정
  • hist : (출력) 히스토그램으로 dims-차원 배열로 Mat() 클래스 형식의 변수 이름을 지정
  • dims : 출력 히스토그램의 차원
  • histSize : 히스토그램의 각 차원의 크기를 나타내는 배열
  • ranges : 히스토그램 각 차원의 최솟값과 최댓값을 원소로 갖는 배열의 배열(uniform이 true인 경우)
  • uniform : 히스토그램의 빈 간격이 균등한지를 나타내는 플래그
  • accumulate : 누적 플래그로 이 값이 true 이면 hist 배열을 초기화 하지 않고 누적하여 히스토그램을 계산함
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

Mat calcGrayHist(const Mat& img) // 그레이스케일 영상 1개를 인자로 받음

// 매번 calcHist의 함수는 범용성을 위해 상당히 복잡한 형태의 인자를 받도록 구성되어있는데 단순히 그레이스케일 영상 한장으로부터 히스토그램을 계산할 때 calcHist함수의 인자를 매번 코드로 작성하는 것은 번거로움 따라서 그레이스케일 한정으로 calcHist를 랩핑하는 별도의 함수를 따로 만들어 사용하는 것이 편리함

{
	CV_Assert(img.type() == CV_8U);

	Mat hist;
	int channels[] = { 0 };
	int dims = 1;
	const int histSize[] = { 256 };
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	calcHist(&img, 1, channels, noArray(), hist, dims, histSize, ranges); 
	// 1. img 변수의 주소를 받아 
	// 2. 영상 1장을 
	// 3. 0번 채널로
    // 4. 영상 전체에 대해서 히스토그램을 계산하여
    // 5. hist라는 CV_32FC1 타입의 256 x 1 의 1차원 행렬을 만들어
    // 6. 1차원의
    // 7. hist gray 사이즈의 단계 { 256 }을 만들어 지정
    // 8. 최솟값 0, 최댓값 256 인 graylevel로 지정하고 이 배열의 이름을 인자로 받는 ranges를 지정
	
	return hist; 
	// calcHist 함수 호출 하여 hist 행렬에 값을 저장하여 hist 행렬 반환
}

Mat getGrayHistImage(const Mat& hist)
{
	CV_Assert(hist.type() == CV_32FC1);
	CV_Assert(hist.size() == Size(1, 256));

	double histMax = 0.;
	minMaxLoc(hist, 0, &histMax);

	Mat imgHist(100, 256, CV_8UC1, Scalar(255)); // 세로가 100px , 가로가 256px 크기로 되어있는 imgHist의 흰색 배경을 생성한 후에
	for (int i = 0; i < 256; i++) {
		line(imgHist, Point(i, 100), // line 함수를 이용하여 히스토그램을 막대형태 그래프로 그림
			Point(i, 100 - cvRound(hist.at<float>(i, 0) * 100 / histMax)), Scalar(0));
	}

	return imgHist;
}

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat hist = calcGrayHist(src);
	Mat imgHist = getGrayHistImage(hist);

	imshow("src", src);
	imshow("hist", imgHist);

	waitKey();
}
  • 다양한 영상의 히스토그램 결과

image

히스토그램 분석

  • 밝기와 명암비가 조절된 영상의 히스토그램

image

히스토그램 스트레칭

  • 히스토그램 스트레칭 변환 함수 :

imageimage

  • minMaxLoc() 함수를 이용하여 Gmin과 Gmax를 구하고, 히스토그램 스트레칭 수식을 이용하여 구현
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

Mat calcGrayHist(const Mat& img)
{
	CV_Assert(img.type() == CV_8U);

	Mat hist;
	int channels[] = { 0 };
	int dims = 1;
	const int histSize[] = { 256 };
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	calcHist(&img, 1, channels, noArray(), hist, dims, histSize, ranges, true);

	return hist;
}

Mat getGrayHistImage(const Mat& hist)
{
	CV_Assert(!hist.empty());
	CV_Assert(hist.type() == CV_32F);

	double histMax = 0.;
	minMaxLoc(hist, 0, &histMax); 

	Mat imgHist(100, 256, CV_8UC1, Scalar(255));
	for (int i = 0; i < 256; i++) {
		line(imgHist, Point(i, 100),
			Point(i, 100 - cvRound(hist.at<float>(i) * 100 / histMax)), Scalar(0));
	}

	return imgHist;
}

int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}
	
	double gmin, gmax;
	minMaxLoc(src, &gmin, &gmax); // minMaxLoc() 함수를 이용하여 Gmin과 Gmax를 구함

	Mat dst = (src - gmin) * 255 / (gmax - gmin); // 히스토그램 스트레칭 수식을 코드로 변환하여 dst에 저장

	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));

	waitKey();
}
  • src의 히스토그램의 결과가 dst에서 확인해보면 streteching 된 것을 확인할 수 있음
    • src 영상보다 dst 영상이 명암비가 다소 높아진 것을 확인할 수 있음

image

  • 히스토그램 스트레칭으로 인해 특정 부분은 밝고, 특정 부분은 어둡게 나타나 영상의 사물이 좀 더 잘 구분됨

image

히스토그램 평활화

  • 히스토그램이 그레이스케일 전체 구간에서 균일한 분포로 골고루 픽셀들이 나타나도록 변경하는 명암비 형상 기법
  • 히스토그램 균등화, 균일화, 평탄화 라고 부름

  • 히스토그램 평활화를 위한 변환 함수 구하기

image

  • 히스토그램 평활화 계산

image

  • 평활화 완료된 결과

image


int main()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat dst(src.rows, src.cols, src.type());

	int hist[256] = {};
	for (int y = 0; y < src.rows; y++)
		for (int x = 0; x < src.cols; x++)
			hist[src.at<uchar>(y, x)]++;
	// 입력 영상의 히스토그램을 계산하여 정수형 배열 hist에 저장

	float cdf[256] = {};
	int size = (int)src.total();
	cdf[0] = float(hist[0]) / size; // cdf[0]의 값을 정규화된 히스토그램 형태로 변경
	for (int i = 1; i < 256; i++) // 나머지의 값들은 아래와 같이 누적해나감
		cdf[i] = cdf[i - 1] + float(hist[i]) / size; // 이전 위치의 cdf 값에 현재 위치의 정규화된 히스토그램 값을 더함
	// 누적 분포 함수를 계산하여 float형 배열 cdf에 저장

	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			dst.at<uchar>(y, x) = uchar(cdf[src.at<uchar>(y, x)] * 255); // 입력 영상의 y,x좌표에서의 픽셀 값을 인덱스로 지정하고, 그 값에  그레이 스케일의 최대값인 255를 곱함
		}
	}

	imshow("src", src);
	imshow("dst", dst);
	imshow("hist_src", getGrayHistImage(calcGrayHist(src)));
	imshow("hist_dst", getGrayHistImage(calcGrayHist(dst)));

	waitKey();
}

OpenCV 히스토그램 평활화 함수

void equalizeHist(InputArray src, OutputArray  dst);
  • src : 그레이스케일인 8 비트 1 채널 입력 영상
  • dst : src와 같은 크기와 같은 타입의 출력 영상

  • 결과 영상은 밝은 부분(255)과 어두운 부분(0) 모두에 히스토그램이 뚜렷하게 나타나고 중간 부분의 밝기에 대해서도 골고루 분포하는 것을 확인할 수 있음

image

  • 결과 영상은 밝은 영상과 어두운 영상이 골고루 나타나면서 명암비가 높아진 것을 확인할 수 있음

image

히스토그램 스트레칭 vs 평활화

  • 입력 영상의 픽셀 값 분포를 통해 적절한 형태의 변환 함수를 생성하여 두 영상 모두 입력 영상보다 명암비가 좋아진 결과 영상을 만듬

  • 히스토그램 스트레칭 같은 경우 픽셀이 존재하지 않은 그레이스케일 값이 균일하게 발생함
  • 히스토그램 평활화 같은 경우 어떤 부분에서는 비어있는 부분이 많고 어떤 부분에서는 비어있는 부분이 아예 발생하지 않음
  • 히스토그램 스트레칭의 변환 함수 직선 함수
  • 히스토그램 평활화의 변환 함수는 곡선 함수
  • 히스토그램 평활화는 기존 히스토그램의 높이가 높은 부분(개수가 많은 부분)에 대해서 넓게 펼치고 높이가 작은 부분(개수가 적은 곳)에 대해서는 많이 펼치지 않음
  • 히스토그램의 평활화 결과는 4등분 해도 모든 등분마다 픽셀의 개수 합이 같음
  • 히스토그램의 평활화의 결과가 항상 스트레칭 결과보다 좋다고 말을 못하고, 사람마다 영상마다 달라짐

image

image