우리가 영상처리를 하다보면 어떠한 처리 결과 영상이 같은지를 확인하고 싶을 때가 있습니다.
이번에는 쉽고 빠르게 두 영상이 같은지를 확인하는 방법에 대해 알아보겠습니다.
countNonZero() 로 알아내기
우리가 가장 쉽게 생각할 수 있는 방법이 두 영상의 차영상을 구해서 모든 픽셀이 0인지 아닌지를 확인하는 방법입니다.
영상을 먼저 입력 받습니다. 그리고 동일한 영상을 만들기 위해 clone을 합니다.
cv::Mat src = cv::imread("lena_color.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat src_clone = src.clone();
그 이후 cv::subtract() 또는 cv::absdiff() 를 이용하여 두 영상의 차영상을 계산합니다.
cv::Mat src_sub, src_sub_1;
//cv::subtract(src, src_clone, src_sub);
cv::absdiff(src, src_clone, src_sub_1);
그 이후 차 영상에서 zero가 아닌 픽셀의 개수를 계산(cv::countNonZero() 사용)하여
NonZeroCount 가 1 이상이면 다른 영상으로 간주할 수 있습니다.
int nNonZeroCount = cv::countNonZero(src_sub);
픽셀을 순회하면서 영상 차이 계산하기
때로는 픽셀 차이가 1 또는 특정 값 이하이면 같은 영상으로 간주하고 싶을 때가 있죠?
그러기 위해서는 픽셀을 전체 순회하면서 차이 값을 계산해야 합니다.
픽셀값 접근하는 빠른 방법을 제가 알려드린 적이 있죠?
이 방법을 사용해서 8bit 영상 뿐만 아니라 color 영상, 그리고 16bit 이상의 영상까지도 차이를 확인할 수 있는 방법에 대해 같이 살펴보아요.
다양한 타입의 영상에 대응하기 위해 template을 사용하였습니다.
각 픽셀 별로 src와 dst의 원소들을 뺀 후에 그 값이 diffVal 이상이면 false를 반환하게 하여 같은 영상인지 check 하는 함수를 하나 생성합니다.
template<typename T>
bool checkImageDiff(cv::Mat& src, cv::Mat& dst, double diffVal)
{
for (int y = 0; y < src.rows; ++y) {
T* pSrc = src.ptr<T>(y);
T* pSrc_end = pSrc + src.cols * src.channels();
T* pDst = dst.ptr<T>(y);
T* pDst_end = pDst + dst.cols * dst.channels();
for (; pSrc < pSrc_end;) {
for (int i = 0; i < src.channels(); ++i) {
if (fabs(*pSrc++ - *pDst++) > diffVal) return false;
}
}
return true;
}
}
그리고 가장 많이 사용되는 타입들만 골라서 switch~case 문으로 specialize하여 구현하였습니다. 일단 두 영상의 사이즈와 type이 같아야 픽셀을 순회하면서 체크할 수 있겠죠?
bool compareEqual(cv::Mat& src, cv::Mat& dst, double diffVal = 0.)
{
if (src.size() != dst.size()) return false;
if (src.type() != dst.type()) return false;
switch (src.type()) {
case CV_8UC1:
return checkImageDiff<uchar>(src, dst, diffVal);
case CV_8UC3:
return checkImageDiff<uchar>(src, dst, diffVal);
case CV_16UC1:
return checkImageDiff<ushort>(src, dst, diffVal);
case CV_16UC3:
return checkImageDiff<ushort>(src, dst, diffVal);
case CV_32SC1:
return checkImageDiff<int>(src, dst, diffVal);
case CV_32FC1:
return checkImageDiff<float>(src, dst, diffVal);
case CV_64FC1:
return checkImageDiff<double>(src, dst, diffVal);
default:
break;
}
}
이제 제대로 동작되는지 확인해봐야겠죠?
main() 함수에서 하나씩 check를 해 볼께요.
일단 8bit 영상 src 를 하나 만들고 하나는 같은 영상을, 다른 하나는 필터링을 한 영상을 만듭니다.
cv::Mat src = cv::imread("lena_color.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat src_clone = src.clone();
cv::Mat src_box;
cv::boxFilter(src, src_box, -1, cv::Size(3, 3));
위에서 만든 함수로 두 영상이 같은지 확인합니다.
if (compareEqual(src, src_clone)) std::cout << "Equal_1" << std::endl;
if (compareEqual(src, src_box)) std::cout << "Equal_2" << std::endl;
src 와 src_clone은 같은 영상으로 true를 return 하고, src와 src_box 는 다른 영상으로 false를 return 하게 되는 것을 보실 수 있습니다.
그럼 컬러인 3channel 영상도 동작되는지 확인해볼께요.
cv::Mat src_color = cv::imread("lena_color.bmp", cv::IMREAD_COLOR);
cv::Mat src_color_clone = src_color.clone();
cv::Mat src_color_box;
cv::boxFilter(src_color, src_color_box, -1, cv::Size(3, 3));
이것도 같은 방법으로 compare를 해 보겠습니다.
if(compareEqual(src_color, src_color_clone)) std::cout << "Equal_3" << std::endl;
if (compareEqual(src_color, src_color_box)) std::cout << "Equal_4" << std::endl;
첫번째인 src_color 와 src_color_clone은 같은 영상으로 true가 반환되고, src_color와 src_color_box 영상은 다른 영상으로 false가 반환됩니다.
그럼 16bit 영상이나 32 bit 영상도 확인해볼께요.
일단 src 8bit 영상을 convertTo로 16bit 영상으로 변환합니다.
그 이후에 같은 방식으로 clone을 한 영상과 Laplacian 을 통과한 영상을 확인해볼께요.
cv::Mat src_16U;
src.convertTo(src_16U, CV_16UC1);
cv::Mat src_16U_clone = src_16U.clone();
cv::Mat src_lap;
cv::Laplacian(src, src_lap, CV_16S, 7);
Laplacian filter를 한 영상은 아래와 같이 음수와 양수가 섞여 있는 signed 16 bit 영상이 됩니다.
이것도 한번 동작 Test를 해 보면, 동일한 결과가 나옵니다.
if(compareEqual(src_16U, src_16U_clone)) std::cout << "Equal_5" << std::endl;
if(compareEqual(src_16U, src_lap)) std::cout << "Equal_6" << std::endl;
동작이 잘 되는 것을 확인할 수 있습니다.
이번에 Test 하는 것 중 두 영상이 같은지 확인하는 함수의 인자에 최소 픽셀 차이값을 입력으로 받고 있는데, 이것을 활용하면 어떻게 되는지 확인해보겠습니다.
32 bit 영상으로 변환한 후 그 영상에서 100을 뺀 영상을 만들어보겠습니다.
src_32S_sub가 그건데요.
그럼 기존 lena 영상에서 100보다 작은 밝기를 가진 픽셀은 음수가 나오겠죠?
cv::Mat src_32S;
src.convertTo(src_32S, CV_32SC1);
cv::Mat src_32S_clone = src_32S.clone();
cv::Mat src_32S_sub = cv::Mat(src_32S.size(), CV_32SC1);
src_32S_sub = src_32S - 100;
image watch로 일부 영역을 확대해보면, 아래와 같이 음수가 나온 픽셀들이 보입니다.
처음에는 compareEqual 함수에 마지막 인자로 값을 넣지 않으면 0이 들어가서 정확히 같은 영상이 아니면 false를 반환하는데, 마지막 인자에 101을 넣게 되면 같은 영상으로 분류가 되어 true가 반환되는 것을 보실 수 있습니다.
if (compareEqual(src_32S, src_32S_clone)) std::cout << "Eqaul_7" << std::endl;
if (compareEqual(src_32S, src_32S_sub)) std::cout << "Equal_8" << std::endl;
if (compareEqual(src_32S, src_32S_sub, 101)) std::cout << "Equal_9" << std::endl;
이 함수는 다음 강의부터 사용할 예정이며, 일반적으로 UnitTest를 할 때 사용할 수 있습니다. (현재 내 함수의 결과가 예상한 결과값이 나오는지를 확인할 때 사용합니다.)
전체 소스는 아래와 같습니다.
아래 소스는 필요하시다면 저작권 없이 사용하시면 됩니다.
#include <iostream>
#include "opencv2/opencv.hpp"
#include <Windows.h>
template<typename T>
bool checkImageDiff(cv::Mat& src, cv::Mat& dst, double diffVal)
{
for (int y = 0; y < src.rows; ++y) {
T* pSrc = src.ptr<T>(y);
T* pSrc_end = pSrc + src.cols * src.channels();
T* pDst = dst.ptr<T>(y);
T* pDst_end = pDst + dst.cols * dst.channels();
for (; pSrc < pSrc_end;) {
for (int i = 0; i < src.channels(); ++i) {
if (fabs(*pSrc++ - *pDst++) > diffVal) return false;
}
}
return true;
}
}
bool compareEqual(cv::Mat& src, cv::Mat& dst, double diffVal = 0.)
{
if (src.size() != dst.size()) return false;
if (src.type() != dst.type()) return false;
switch (src.type()) {
case CV_8UC1:
return checkImageDiff<uchar>(src, dst, diffVal);
case CV_8UC3:
return checkImageDiff<uchar>(src, dst, diffVal);
case CV_16UC1:
return checkImageDiff<ushort>(src, dst, diffVal);
case CV_16UC3:
return checkImageDiff<ushort>(src, dst, diffVal);
case CV_32SC1:
return checkImageDiff<int>(src, dst, diffVal);
case CV_32FC1:
return checkImageDiff<float>(src, dst, diffVal);
case CV_64FC1:
return checkImageDiff<double>(src, dst, diffVal);
default:
break;
}
}
int main()
{
// 8bit 1ch
cv::Mat src = cv::imread("lena_color.bmp", cv::IMREAD_GRAYSCALE);
cv::Mat src_clone = src.clone();
cv::Mat src_box;
cv::boxFilter(src, src_box, -1, cv::Size(3, 3));
// 8bit 3ch
cv::Mat src_color = cv::imread("lena_color.bmp", cv::IMREAD_COLOR);
cv::Mat src_color_clone = src_color.clone();
cv::Mat src_color_box;
cv::boxFilter(src_color, src_color_box, -1, cv::Size(3, 3));
// 16bit 1ch
cv::Mat src_16U;
src.convertTo(src_16U, CV_16UC1);
cv::Mat src_16U_clone = src_16U.clone();
cv::Mat src_lap;
cv::Laplacian(src, src_lap, CV_16S, 7);
// 32bit 1ch
cv::Mat src_32S;
src.convertTo(src_32S, CV_32SC1);
cv::Mat src_32S_clone = src_32S.clone();
cv::Mat src_32S_sub = cv::Mat(src_32S.size(), CV_32SC1);
src_32S_sub = src_32S - 100;
// 8bit 1ch
if (compareEqual(src, src_clone)) std::cout << "Equal_1" << std::endl;
if (compareEqual(src, src_box)) std::cout << "Equal_2" << std::endl;
// 8bit 3ch
if(compareEqual(src_color, src_color_clone)) std::cout << "Equal_3" << std::endl;
if (compareEqual(src_color, src_color_box)) std::cout << "Equal_4" << std::endl;
// 16bit 1ch
if(compareEqual(src_16U, src_16U_clone)) std::cout << "Equal_5" << std::endl;
if(compareEqual(src_16U, src_lap)) std::cout << "Equal_6" << std::endl;
// 32bit 1ch
if (compareEqual(src_32S, src_32S_clone)) std::cout << "Eqaul_7" << std::endl;
if (compareEqual(src_32S, src_32S_sub)) std::cout << "Equal_8" << std::endl;
if (compareEqual(src_32S, src_32S_sub, 101)) std::cout << "Equal_9" << std::endl;
return 0;
}