1. 캐니 에지 검출알고리즘

**

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

비교적 간단한 형태의 필터링 마스크를 통해 x방향 미분과 y방향 미분 을 수행 후 그 결과로부터 그레디언트 크기값을 계산하여 간단한 임계값 연산을 수행 후 최종적으로 에지 이미지를 형성할 수 있 었다

단점으로는 그레디언크기만을 사용하고, 단순한 형태의 임계값 연산만 수행하였기 떄문에  경우에 따라 에지 픽셀이 하나의 픽셀이 아닌 여러개의 픽셀로 표현된 다는 단점을 가지고 있다.

주어진 영상의 픽셀값 변화가 급격하게 변하는게 아닌, 어느정도의 간격을 두고 증가하기에 (스무스)

그레디언트 크기값도 또한 간격을 두고 증가하게 된다. 이때 넉넉한 크기로 threshold 값 지정 시 여러개의 픽셀간격으로 하여 에지 픽셀이 검출된다.

이러한 단점을 캐니에지를 통해 해결 할 수 있다.

2. 케니 에지 직접 구현

1) Good detection, Good localization, Single edge 3개의 좋은 에지 검출기 조건을 내세웠다. 단일 에지가 소벨에지의 단점인 다중 에지를 지적한다.

 

쓰레스 홀드 위에 있는 가장 꼭지에 있는 점을 에지로 검출함으로써 단일에지 형성함.

 

2) 캐니에지 검출방법

a. 가우시안 필터링 (Gaussian blur) b. 그래디언트 계산(크기[피타고라스] & 방향[arc tan]) (주로 소벨마스크 사용[x축 방향 편미분, y축방향 편미분 계산] , 1: 2: 1) c. 비최대 억제  d. 이중 임계값을 이용한 히스테리시스 에지 트래킹

 

a,b,c,d 순으로 진행한다.

 

a. 잡음이 없을 경우 가우시안 필터링을 제외해도 좋다.

b. 소벨마스크 사용 -> 그래디언트 크기, 방향 계산

c. 비최대억제 (Non maximum suppression)

 1) 그레디언트 크기값이 충분 히 큼에도 주변 픽셀값과 비교시 최대값아닐 경우 그 픽셀을 엣지픽셀로 설정하지 않음. 

최대값을 가질 경우 엣지 픽셀로 설정.

2) 그래디언트 방향에 위치한 두개의 픽셀과 국지적 최대를 검사.,

d. 이중 임계값 이용한 히스테리시스 에지 트래킹(Hysteresis edge tracking)

두개의 임계값을 사용해 강한에지, 에지 아님, 약한 에지를 판별한다. (T_high 보다 그레디언트 크기값이  크면 무조건 에지 픽셀로 선정)

T LOW 보다 낮은 값인 경우 에지가 아니다 라고 판별하며, 임계값 사이에 있는 약한에지 중에서 강한에지와 연결유무에 따라 강한에지와 함께 최종에지로 선정한다.

 

비최대억제 적용한 이미지에서 회색으로 되있는 부분들은 약한에지 픽셀이다. 해당 픽셀은 흰 픽셀과 연결되지 않았기에 히스테리시스 에지트래킹에서 해당 약한에지 픽셀을 없앤다.

 

void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false);

image: 입력영상

edges: 에지영상

th1 : 하단임계값

th2: 상단임계값

th1:th2 = 1:2 || 1:3 비율 권장

apertureSize: 소벨 연산 위한 커널 크기

L2gradient: L2 norm 사용여부 ( 유클리디안 거리 공식과 동일)

circuit.bmp
0.06MB

 

mycanny.h

#pragma once // 헤더파일이 중복적으로 include되지 않게 하는 전처리
#include "opencv2/core.hpp"  
//opencv.hpp   include 시 여러 파일들을 include하게 되는 형태이다. 이런 구조가 좋지 않기에
//Mat 클래스만 사용하기에 core.hpp 만 불러오게 셋팅함.

//using namespace cv;   헤더파일에 namespace 사용은 권장하지 않음. 경우에 따라 컴파일 안됨

void myCanny(const cv::Mat& src, cv::Mat& dst, double threshold1, double threshold2);

main.cpp

#include <iostream>
#include "opencv2/opencv.hpp"
#include "mycanny.h"
using namespace std;
using namespace cv;  // 메인에서는 using namespace cv 사용해도 무방

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

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

	Mat dst1, dst2;
	Canny(src, dst1, 50, 150); // opencv 함수 사용
	myCanny(src, dst2, 50, 150); // 캐니에지 직접구현
	//myCanny  는 opencv Canny 함수 내 CANNY_PUSH 함수  참고. 

	resize(dst1, dst1, Size(500, 500));
	resize(dst2, dst2, Size(500, 500));
	imshow("opencv_canny", dst1);
	imshow("myCanny", dst2);  //가우시안 블러링을 한번 더했기에 보다 부드러워지고 잡음없는 영상이 나옴.
	waitKey();
}

 

mycanny.cpp

#include <iostream>
#include <algorithm>
#include "opencv2/opencv.hpp"
#include "mycanny.h"

using namespace std;
using namespace cv; // cpp 파일에서 using namespace cv 사용해도 무방

void myCanny(const Mat& src, Mat& dst, double threshold1, double threshold2)
{
	// 1. Gaussian filter
	Mat gauss;
	GaussianBlur(src, gauss, Size(), 0.5);

	// 2. Gradient by Sobel operator   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);  //arch tan
		}
	}

	// 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;
					}
				}
			}
		}
	}
	imshow("temp", dst);
	waitKey();

#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;
		}
	}
}

 

 

 

참고할 만 한 사이트

https://gaussian37.github.io/vision-concept-edge_detection/

'프로그래머스 > OPENCV' 카테고리의 다른 글

코너 검출 기법  (2) 2022.12.08
허프변환 알고리즘  (0) 2022.12.08
에지검출과 소벨필터(2)  (0) 2022.12.07
에지검출과 소벨필터(1)  (0) 2022.12.07
색상범위 관련  (0) 2022.12.07

+ Recent posts