에지 검출과 영상의 미분

에지(edge)

  • 영상에서 픽셀의 밝기 값이 급격하게 변하는 부분
  • 일반적으로 객체와 배경, 또는 객체와 객체의 경계
  • 에지 검출은 객체 분할 및 인식을 위한 기본적인 과정
  • 입력 영상을 에지를 검출하기 위해 그래디언트 정보를 추출하여 에지 검출 결과를 도출
    • 그래디언트 정보 : 어떤 함수가 급격하게 변하는 부분
  • 경계의 선들 중에 검출되지 않은 곳들은 픽셀 값의 변화가 상당히 작아 검출되지 않음

image

  • (a) : 어두운 객체와 배경이 교차하면서 급격하게 값이 증가
  • (b) : 카메라의 아날로그 특성과 포커스가 맞지 않았을 때
  • (c) : 카메라 노이즈
  • (d) : 가우시안 블러링을 적용

image

기본적인 에지 검출 방법

  • 영상을 (x, y) 변수의 함수로 간주했을 때, 이 함수의 1차 미분(순간 변화율) 값이 크게 나타나는 부분을 검출
  • Threshold 값을 어떻게 정하는지에 따라 엣지 검출 결과 값이 달라짐
    • Threshold 값이 너무 높으면 아예 검출을 못하고 너무 낮으면 픽셀의 개수가 많아지므로 적정한 값을 찾아야 함

image

가우시안 블러와 에지 검출

  • 입력 영상에 가우시안 블러를 적용하여 잡음을 제거한 후 에지를 검출하는 것이 바람직함
  • h ★ f 는 가우시안 필터 h 와 입력 신호 f 를 convolution 함 -> 가우시안 필터링 작업을 수행

image

다양한 에지 검출 마스크

  • Prewitt : 1 : 1 : 1
  • Sobel : 1 : 2 : 1
  • Scharr : 3 : 10 : 3

image

image

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

using namespace std;
using namespace cv;

void sobel_impl();
void sobel_func();

int main()
{
	sobel_impl();
	sobel_func();
}

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

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

	Mat dx = Mat::zeros(src.size(), CV_8UC1);
	Mat dy = Mat::zeros(src.size(), CV_8UC1);

	for (int y = 1; y < src.rows - 1; y++) {
		for (int x = 1; x < src.cols - 1; x++) {
			int v1 = src.at<uchar>(y - 1, x + 1)
				+ src.at<uchar>(y, x + 1) * 2
				+ src.at<uchar>(y + 1, x + 1)
				- src.at<uchar>(y - 1, x - 1)
				- src.at<uchar>(y, x - 1) * 2
				- src.at<uchar>(y + 1, x - 1);
				
			int v2 = src.at<uchar>(y + 1, x + 1)
				+ src.at<uchar>(y + 1, x) * 2
				+ src.at<uchar>(y + 1, x + 1)
				- src.at<uchar>(y - 1, x + 1)
				- src.at<uchar>(y - 1, x) * 2
				- src.at<uchar>(y - 1, x + 1);
			dx.at<uchar>(y, x) = saturate_cast<uchar>(v1 + 128); 
			dy.at<uchar>(y, x) = saturate_cast<uchar>(v2 + 128);
			// 그레이스케일 중간값인 128을 더함, 미분값이 큰 부분과 작은 부분 둘다 확인할 수 있음
		}
	}

	imshow("src", src);
	imshow("dx", dx);
	imshow("dy", dy);
	imshow("mag", mag);

	waitKey();
	destroyAllWindows();
}
  • dx.at(y, x) = saturate_cast(v1);

image

  • dx.at(y, x) = saturate_cast(v1+128);
    • 변화가 없는 부분에 대해서 변화량이 0 에 가까운 값이 나타나는데 그 0에 +128을 하니 128에 가까운 값이 나타남
    • 갑자기 어두워지는 부분 : 검정색으로 표현
    • 갑자기 밝아지는 부분 : 흰색으로 표현
    • 흰색에 가까울 정도로 밝거나 검정색에 가까울 정도로 어두운 부분은 픽셀 값의 변화가 큰 곳

image

  • 세로 방향 추가분석
    • 위에서 아래 방향으로 어두운 픽셀에서 밝은 픽셀로 갈 때 변화량이 커져 하얀색으로 표시
    • 위에서 아래 방향으로 밝은 픽셀에서 어두운 픽셀로 갈 때 변화량이 작아져 검정색으로 표시

image

  • 가로 방향과 세로방향
    • 가로 방향에서는 변화량의 차이가 있어 가로 방향 결과(2) 에서는 에지가 검출 o
    • 세로 방향에서는 변화량의 차이가 없어 세로 방향 결과(3) 에서는 에지를 검출 x
  • 문제점 : x 방향과 y 방향으로의 미분을 모두 종합하고 반영해야 입력 영상의 임의의 방향으로의 큰 변화량을 검출 할 수 있음

image

그래디언트

  • 함수 f(x,y)를 x축과 y축으로 각각 편미분하여 벡터 형태로 표현한 것

image

  • 실제 영상에서 구한 그래디언트 크기와 방향
    • 그래디언트 크기 : 픽셀 값과의 차이 정도, 변화량
    • 그래디언트 방향 : 픽셀 값이 가장 급격하게 증가하는 방향
  • 빨강색 화살표가 그래디언트 방향을 나타내고, 빨간색 화살표의 길이가 그래디언트 크기를 나타냄

image

  • 그래디언트 크기와 적정한 임계값을 이용한 이진화 결과

image

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

using namespace std;
using namespace cv;

void sobel_impl();
void sobel_func();

int main()
{
	sobel_impl();
	sobel_func();
}

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

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

	Mat dx = Mat::zeros(src.size(), CV_8UC1);
	Mat dy = Mat::zeros(src.size(), CV_8UC1);
	Mat mag = Mat::zeros(src.size(), CV_8UC1); // mag 추가

	for (int y = 1; y < src.rows - 1; y++) {
		for (int x = 1; x < src.cols - 1; x++) {
			int v1 = src.at<uchar>(y - 1, x + 1)
				+ src.at<uchar>(y, x + 1) * 2
				+ src.at<uchar>(y + 1, x + 1)
				- src.at<uchar>(y - 1, x - 1)
				- src.at<uchar>(y, x - 1) * 2
				- src.at<uchar>(y + 1, x - 1);
				
			int v2 = src.at<uchar>(y + 1, x + 1)
				+ src.at<uchar>(y + 1, x) * 2
				+ src.at<uchar>(y + 1, x + 1)
				- src.at<uchar>(y - 1, x + 1)
				- src.at<uchar>(y - 1, x) * 2
				- src.at<uchar>(y - 1, x + 1);
			dx.at<uchar>(y, x) = saturate_cast<uchar>(v1 + 128); 
			dy.at<uchar>(y, x) = saturate_cast<uchar>(v2 + 128);
			// 그레이스케일 중간값인 128을 더함, 미분값이 큰 부분과 작은 부분 둘다 확인할 수 있음
			mag.at<uchar>(y, x) = saturate_cast<uchar>(sqrt(v1 * v1 + v2 * v2));
		}
	}

	Mat edge = mag > 120; // 120 보다 크면 255 아니면 0으로 구성된 것을 edge 객체에 저장

	imshow("src", src);
	imshow("dx", dx);
	imshow("dy", dy);
	imshow("mag", mag);
	imshow("edge", edge);

	waitKey();
	destroyAllWindows();
}

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

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

	Mat dx, dy;
	Sobel(src, dx, CV_32FC1, 1, 0);
	Sobel(src, dy, CV_32FC1, 0, 1);

	Mat mag;
	magnitude(dx, dy, mag);
	mag.convertTo(mag, CV_8UC1);

	Mat edge = mag > 150;

	imshow("src", src);
	imshow("mag", mag);
	imshow("edge", edge);

	waitKey();
	destroyAllWindows();
}
  • x축 방향의 변화량과 y축 방향의 변화량을 같이 혼합하여 사용하므로 결과 영상에서도 적용된 것을 확인할 수 있음

image

image

  • Mat edge = mag > 120;
    • Threshold 값을 120으로 설정한 것으로, 120 보다 크면 255로 지정하고 아니면 0으로 지정해서 edge 객체에 저장
    • 희미한 선들을 표현하기 위해 Threshold 값을 낮추면 edge가 아닌 자잘한 형태도 검출할 수 있으므로 Threshold 값을 잘 결정해야 함

image

소벨 필터

void Sobel(InputArray src, OutputArray dst, input ddepth, int dx, int dy, int ksize = 3, double scale = 1, double delta =0, int borderType = BORDER_DEFAULT);
  • src : 입력 영상
  • dst : 출력 영상 (src와 같은 크기, 채널 수)
  • ddepth : 출력 영상 깊이
    • -1을 지정하면 입력영상과 같은 자료형을 갖는 출력영상을 생성
    • 입력영상 : 그레이스케일 -> 출력영상 : Float 타입 만들고 싶을 경우 CF_32F와 같이 입력
  • dx, dy : x 방향과 y 방향으로의 미분 차수
    • 0, 1 : x축으로 미분을 안하고 y축으로 미분을 함
    • 1, 0 : x축으로 미분을 하고 y축으로 미분을 안함
  • ksize : 커널 크기
    • 1 : 3 x 1 또는 1 x 3 커널 사용
    • CV_SCHARR : 3 x 3 Scharr 커널 사용
    • 3, 5, 7 : 3 x 3, 5 x 5, 7 x 7 커널 사용
  • scale : 연산 결과에 추가적으로 곱할 값
  • delta : 연산 결과에 추가적으로 더할 값
  • borderType : 가장자리 픽셀 확장 방식
void sobel_func()
{
	Mat src = imread("lenna.bmp", IMREAD_GRAYSCALE);

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

	Mat dx, dy;
	Sobel(src, dx, CV_32FC1, 1, 0); // x축으로 1차미분을 계산
	Sobel(src, dy, CV_32FC1, 0, 1); // y축으로 1차미분을 계산

	Mat mag;
	magnitude(dx, dy, mag); // mag도 CV_32FC1으로 타입이 바뀜
	mag.convertTo(mag, CV_8UC1); // mag 값을 CV_8UC1으로 바꿈

	Mat edge = mag > 150;

	imshow("src", src);
	imshow("mag", mag);
	imshow("edge", edge);

	waitKey();
	destroyAllWindows();
}

image

  • 소벨 마스크를 이용한 에지 검출

image