[2023-04-20] 3. 히스토그램 분석, 스트레칭과 평활화
히스토그램 분석
-
영상의 픽셀 값 분포를 보통 그래프의 형태로 표현한 것
- 예를들어, 그레이스케일 영상에서 각 그레이스케일 값에 해당하는 픽셀의 개수를 구하고 이를 막대 그래프의 형태로 표현
-
h : 함수의 이름
-
g : 그레이스케일을 나타내므로 g의 범위는 0부터 255 사이의 범위
-
Ng : 그레이스케일 값이 g인 픽셀의 개수를 나타냄
-
픽셀 값이 0~7 사이인 단순한 형태의 영상의 히스토그램 예시
- 어두운 픽셀(0~2)이 좀 많고, 중간 픽셀(3~4)은 많이 존재하지 않고, 밝은 픽셀(5~7)은 적당히 많은 것을 확인할 수 있음
- 다만 히스토그램은 픽셀 값의 위치 정보는 표현하지 못함
정규화된 히스토그램(normalized histogram)
- 히스토그램으로 구한 각 픽셀의 개수를 영상 전체 픽셀 개수로 나눈 것
- 해당 그레이스케일 값을 갖는 픽셀의 비율 또는 확률 :
-
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로 안 채워진 것을 알 수 있음 (아주 어두운 픽셀과 아주 밝은 픽셀은 존재하지 않음)
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();
}
- 다양한 영상의 히스토그램 결과
히스토그램 분석
- 밝기와 명암비가 조절된 영상의 히스토그램
히스토그램 스트레칭
- 히스토그램 스트레칭 변환 함수 :
- 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 영상이 명암비가 다소 높아진 것을 확인할 수 있음
- 히스토그램 스트레칭으로 인해 특정 부분은 밝고, 특정 부분은 어둡게 나타나 영상의 사물이 좀 더 잘 구분됨
히스토그램 평활화
- 히스토그램이 그레이스케일 전체 구간에서 균일한 분포로 골고루 픽셀들이 나타나도록 변경하는 명암비 형상 기법
-
히스토그램 균등화, 균일화, 평탄화 라고 부름
- 히스토그램 평활화를 위한 변환 함수 구하기
- 히스토그램 평활화 계산
- 평활화 완료된 결과
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) 모두에 히스토그램이 뚜렷하게 나타나고 중간 부분의 밝기에 대해서도 골고루 분포하는 것을 확인할 수 있음
- 결과 영상은 밝은 영상과 어두운 영상이 골고루 나타나면서 명암비가 높아진 것을 확인할 수 있음
히스토그램 스트레칭 vs 평활화
-
입력 영상의 픽셀 값 분포를 통해 적절한 형태의 변환 함수를 생성하여 두 영상 모두 입력 영상보다 명암비가 좋아진 결과 영상을 만듬
- 히스토그램 스트레칭 같은 경우 픽셀이 존재하지 않은 그레이스케일 값이 균일하게 발생함
- 히스토그램 평활화 같은 경우 어떤 부분에서는 비어있는 부분이 많고 어떤 부분에서는 비어있는 부분이 아예 발생하지 않음
- 히스토그램 스트레칭의 변환 함수 직선 함수
- 히스토그램 평활화의 변환 함수는 곡선 함수
- 히스토그램 평활화는 기존 히스토그램의 높이가 높은 부분(개수가 많은 부분)에 대해서 넓게 펼치고 높이가 작은 부분(개수가 적은 곳)에 대해서는 많이 펼치지 않음
- 히스토그램의 평활화 결과는 4등분 해도 모든 등분마다 픽셀의 개수 합이 같음
- 히스토그램의 평활화의 결과가 항상 스트레칭 결과보다 좋다고 말을 못하고, 사람마다 영상마다 달라짐