캐니 에지 검출기

소벨 필터를 이용한 에지 검출의 장단점

  • 장점 : 비교적 간단한 필터링 연산과 임계값 계산으로 에지 위치를 검출할 수 있음
  • 단점 : 그래디언트 크기만을 사용하므로 에지 위치가 여러 개의 픽셀로 표현될 수 있음

J.Canny가 제안한 좋은 에지 검출기의 조건

  • 정확한 검출(Good detection) : 에지가 아닌 점을 에지로 찾거나 또는 에지를 검출하지 못하는 확률을 최소화
  • 정확한 위치(Good localization) : 실제 에지의 중심을 검출
  • 단일 에지(Single edge) : 하나의 에지는 하나의 점으로 표현

캐니 에지 검출방법

  1. 가우시안 필터링
  2. 그래디언트 계산(크기 & 방향)
  3. 비최대 억제
  4. 이중 임계값을 이용한 히스테리시스 에지 트래킹
  • 가우시안 필터링
    • 잡음 제거 목적
    • GaussianBlur() 함수 사용
  • 그래디언트 계산(크기 & 방향)
    • 주로 소벨 마스크를 사용

image

  • 그래디언트 방향을 4개 구역으로 단순화

    image

  • 비최대 억제

    • 하나의 에지가 여러개의 픽셀로 표현되는 현상을 없에기 위해 그래디언트 크기가 국지적 최대(local maximum)인 픽셀만을 에지 픽셀로 설정

    • 그레디언트 방향에 위치한 국지적 최대를 검사

image

  • 캐니 에지 검출 방법
    • 이중 임계값을 이용한 히스테리시스 에지 트래킹(Hysteresis edge tracking)
    • 다음과 같은 조건으로 최종 에지를 겜출함

image

캐니 에지 검출 과정

  1. 주어진 입력 영상을 X 방향과 Y 방향의 미분을 구하고 그래디언트 크기를 그레이스케일 영상 형식으로 만듬
  2. 그래디언트 크기 값에 대해서 그래디언트 크기 값에 대해서 Local Maximum인 값들만 선택
  3. 픽셀의 밝기 값이 흰색으로 표현된 것도 있고, 회색의 점들도 보이는데 이러한 것들을 히스테리시스 에지 트래킹으로 처리

image

image

캐네 에지 검출기 함수

void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize =3, bool L2gradient = false);
  • image : 입력 영상
  • edges : 에지 영상
  • threshold1 : 하단 임계값
  • threshold2 : 상단 임계값 (threshold1과 threshold2 의 비율은 1:2 또는 1:3 권장)
  • apertureSize : 소벨 연산을 위한 커널 크기
  • L2gradient : L2 norm 사용 여부

image

OpenCV에서 캐니 에지 직접 구현

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

#include "mycanny.h"

using namespace std;
using namespace cv;

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

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

	Mat dst1, dst2;
	Canny(src, dst1, 50, 150);
	myCanny(src, dst2, 50, 150);

	imshow("src", src);
	imshow("OpenCV Canny", dst1);
	imshow("My Canny", dst2);

	waitKey();
}
#include <iostream>
#include <algorithm>
#include "opencv2/opencv.hpp"
#include "mycanny.h"

using namespace std;
using namespace cv;

void myCanny(const Mat& src, Mat& dst, double threshold1, double threshold2)
{
	// 1. 가우시안 블러
	Mat gauss;
	GaussianBlur(src, gauss, Size(), 0.5);

	// 2. 소벨함수를 이용해 x축방향, y축방향으로 미분
	Mat dx, dy;
	Sobel(gauss, dx, CV_32F, 1, 0);
	Sobel(gauss, dy, CV_32F, 0, 1);

	Mat mag = Mat::zeros(src.rows, src.cols, CV_32F);
	Mat ang = Mat::zeros(src.rows, src.cols, CV_32F);
	
	for (int y = 0; y < src.rows; y++) {
		float* pDx = dx.ptr<float>(y);
		float* pDy = dy.ptr<float>(y);
		float* pMag = mag.ptr<float>(y);
		float* pAng = ang.ptr<float>(y);

		for (int x = 0; x < src.cols; x++) {
			// mag는 그래디언트의 크기
			pMag[x] = sqrt(pDx[x] * pDx[x] + pDy[x] * pDy[x]);

			// ang는 그래디언트의 방향 (값이 가장 급격하게 변하는 방향)
			if (pDx[x] == 0)
				pAng[x] = 90.f;
			else
				pAng[x] = float(atan(pDy[x] / pDx[x]) * 180 / CV_PI); // arctan를 이용해 각도를 구함
		}
	}

	// 3. Non-maximum suppression
	enum DISTRICT { AREA0 = 0, AREA45, AREA90, AREA135, NOAREA };
	const int ang_array[] = { AREA0, AREA45, AREA45, AREA90, AREA90, AREA135, AREA135, AREA0 };

	const uchar STRONG_EDGE = 255;
	const uchar WEAK_EDGE = 128;

	vector<Point> strong_edges;
	dst = Mat::zeros(src.rows, src.cols, CV_8U);

	for (int y = 1; y < src.rows - 1; y++) {
		for (int x = 1; x < src.cols - 1; x++) {
			// 그래디언트 크기가 th_low보다 큰 픽셀에 대해서만 국지적 최대 검사.
			// 국지적 최대인 픽셀에 대해서만 강한 엣지 또는 약한 엣지로 설정.
			float mag_value = mag.at<float>(y, x);
			if (mag_value > threshold1) {
				// 그래디언트 방향에 90도를 더하여 엣지의 방향을 계산 (4개 구역)
				int ang_idx = cvFloor((ang.at<float>(y, x) + 90) / 22.5f);

				// 국지적 최대 검사
				bool local_max = false;
				switch (ang_array[ang_idx]) {
				case AREA0:
					if ((mag_value >= mag.at<float>(y - 1, x)) && (mag_value >= mag.at<float>(y + 1, x))) {
						local_max = true;
					}
					break;
				case AREA45:
					if ((mag_value >= mag.at<float>(y - 1, x + 1)) && (mag_value >= mag.at<float>(y + 1, x - 1))) {
						local_max = true;
					}
					break;
				case AREA90:
					if ((mag_value >= mag.at<float>(y, x - 1)) && (mag_value >= mag.at<float>(y, x + 1))) {
						local_max = true;
					}
					break;
				case AREA135:
				default:
					if ((mag_value >= mag.at<float>(y - 1, x - 1)) && (mag_value >= mag.at<float>(y + 1, x + 1))) {
						local_max = true;
					}
					break;
				}

				// 강한 엣지와 약한 엣지 구분.
				if (local_max) {
					if (mag_value > threshold2) {
						dst.at<uchar>(y, x) = STRONG_EDGE;
						strong_edges.push_back(Point(x, y));
					} else {
						dst.at<uchar>(y, x) = WEAK_EDGE;
					}
				}
			}
		}
	}

#define CHECK_WEAK_EDGE(x, y) \
	if (dst.at<uchar>(y, x) == WEAK_EDGE) { \
		dst.at<uchar>(y, x) = STRONG_EDGE; \
		strong_edges.push_back(Point(x, y)); \
	}

	// 4. Hysterisis edge tracking
	while (!strong_edges.empty()) {
		Point p = strong_edges.back();
		strong_edges.pop_back();

		// 강한 엣지 주변의 약한 엣지는 최종 엣지(강한 엣지)로 설정
		CHECK_WEAK_EDGE(p.x + 1, p.y)
		CHECK_WEAK_EDGE(p.x + 1, p.y + 1)
		CHECK_WEAK_EDGE(p.x, p.y + 1)
		CHECK_WEAK_EDGE(p.x - 1, p.y + 1)
		CHECK_WEAK_EDGE(p.x - 1, p.y)
		CHECK_WEAK_EDGE(p.x - 1, p.y - 1)
		CHECK_WEAK_EDGE(p.x, p.y - 1)
		CHECK_WEAK_EDGE(p.x + 1, p.y - 1)
	}

	// 끝까지 약한 엣지로 남아있는 픽셀은 모두 엣지가 아닌 것으로 판단.
	for (int y = 0; y < src.rows; y++) {
		for (int x = 0; x < src.cols; x++) {
			if (dst.at<uchar>(y, x) == WEAK_EDGE)
				dst.at<uchar>(y, x) = 0;
		}
	}
}
#pragma once

#include "opencv2/core.hpp"

void myCanny(const cv::Mat& src, cv::Mat& dst, double threshold1, double threshold2);
  • MyCanny에서 가우시안 필터를 한번 더 진행해서 OpenCV에서 제공하는 Canny보다 조금 부드러워 보임

image