허프 변환 검출

허프 변환 직선 검출

  • 2차원 영상 좌표에서의 직선의 방정식을 파라미터 공간(parameter space)으로 변환하여 직선을 찾는 알고리즘

image

  • 빨강색, 노랑색 점을 오른쪽에 파라미터 공간에서는 빨강색, 노랑색 직선의 방정식으로 나타낼 수 있음
  • 파랑색 직선의 방정식 을 오른쪽 파라미터 공간에서는 점으로 나타낼 수 있음
  • 빨강색, 노랑색 점을 지나는 파랑색 직선의 방정식은 파라미터 공간에서 빨강색, 노랑색 직선의 방정식의 교점으로 나타낼 수 있음
축적 배열(accumulation array)
  • 직선 성분과 관련된 원소 값을 1씩 증가시키는 배열
  • 아래 그림에서 3이 있는 곳의 좌표가 a, b의 좌표로 x,y 좌표계의 직선의 방정식을 구할 수 있음

image

직선의 방정식 y = ax + b를 사용할 때의 문제점
  • y축과 평행한 수직선을 표현하지 못함 -> 극 좌표계 직선의 방정식을 사용

image

극 좌표계 직선의 방정식에 의한 파라미터 공간으로의 변환

image

허프 변환 직선 검출 함수
void HoughLines(InputArray image, OutPutArray lines, double rho, double theta, int threshold, double srn = 0, double stn = 0, double min_theta = 0, double max_theta = CV_PI);
  • image : 그레이스케일 에지 영상
  • lines : 직선의 파라미터(rho, theta) 저장할 출력 벡터 (vector)
  • rho : 축적 배열에서 rho 값의 간격(픽셀 단위)
    • 1.0 -> 1 픽셀 간격
    • 이 값이 커지면 속도는 증가하고 정확도는 내려감
  • theta : 축적 배열에서 theta 값의 간격(라디안 단위)
    • CV_PI / 180 -> 1도 간격
  • threshold : 축적 배열에서 직선으로 판단할 임계값
  • srn : 멀티스케일 허프 변환에서 rho 해상도를 나누는 값
    • 양의 실수를 지정하면 rho 해상도와 rho/srn 해상도를 각각 이용하여 멀티스케일 허프 변환을 수행
    • srn과 stn이 모두 0이면 일반 허프 변환을 수행
  • stn : 멀티스케일 허프 변환에서 theta 해상도를 나누는 값
  • min_theta : 검출할 직선의 최소 theta 값
  • max_theta : 검출할 직선의 최대 theta 값
확률적 허프 변환에 의한 선분 검출 함수
void HoughLinesP(InputArray image, OutPutArray lines, double rho, double theta, int threshold, double minLineLength = 0, double maxLineGap = 0);
  • image : 그레이스케일 에지 영상
  • lines : 선분의 시작, 끝 좌표(x1, x2, y1, y2)를 저장할 출력 벡터(vector)
  • rho : 축적 배열에서 rho 값의 간격(픽셀 단위)
    • 1.0 -> 1 픽셀 간격
    • 이 값이 커지면 속도는 증가하고 정확도는 내려감
  • theta : 축적 배열에서 theta 값의 간격(라디안 단위)
    • CV_PI / 180 -> 1도 간격
  • threshold : 축적 배열에서 직선으로 판단할 임계값
  • minLineLength : 검출할 선분의 최소 길이
  • maxLineGap: 직선으로 간주할 최대 에지 점 간격
허프 변환 직선 검출 예제 코드
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("building.jpg", IMREAD_GRAYSCALE);

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

	Mat src_edge;
	Canny(src, src_edge, 50, 150); // 캐니 엣지 결과를 src_edge에 저장

	TickMeter tm;
	tm.start();

	vector<Vec2f> lines1;
	HoughLines(src_edge, lines1, 1, CV_PI / 180, 250); // src_edge의 검출된 직선의 정보를 lines1로 저장
	// 1 픽셀 단위로 rho 값을 변경하고 1도 간격으로 theta를 변경
	// 축적 배열에서 250보다 큰 경우에 한에서만 직선으로 검출

	tm.stop();

	cout << "HoughLines(): " << tm.getTimeMilli() << "ms." << endl; // 시간측정 결과

	Mat dst1;
	cvtColor(src_edge, dst1, COLOR_GRAY2BGR);

	for (size_t i = 0; i < lines1.size(); i++) { // for를 이용해서 직선을 그려주는 코드
		float r = lines1[i][0], t = lines1[i][1]; // 직선의 극좌표계 r = rho, t = theta의 값
		double cos_t = cos(t), sin_t = sin(t); // cos_t는 cos theta, sin_t는 sin theta 값
		double x0 = r*cos_t, y0 = r*sin_t; // theta 각도로 rho 만큼 간 점의 좌표
		double alpha = 1000; // 알파값이 작으면 직선이 짧아지고, 크면 직선이 길어짐

		Point pt1(cvRound(x0 + alpha*(-sin_t)), cvRound(y0 + alpha*cos_t));
		Point pt2(cvRound(x0 - alpha*(-sin_t)), cvRound(y0 - alpha*cos_t));
		line(dst1, pt1, pt2, Scalar(0, 0, 255), 1, LINE_AA);
	}

	tm.reset();
	tm.start();

	vector<Vec4i> lines2;
	HoughLinesP(src_edge, lines2, 1, CV_PI / 180, 160, 50, 5); // src_edge의 직선의 성분을 찾아 lines2에 저장
	// 축적 배열에서의 threshold 값을 160으로 설정
	// 50 픽셀 이상의 선분만 구하고 5픽셀 이내로 끊어져있으면 그 선분들은 하나의 성분으로 인식

	tm.stop();

	cout << "HoughLinesP(): " << tm.getTimeMilli() << "ms." << endl;

	Mat dst2;
	cvtColor(src_edge, dst2, COLOR_GRAY2BGR);

	for (size_t i = 0; i < lines2.size(); i++) {
		Vec4i l = lines2[i];
		line(dst2, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 2, LINE_AA);
	}

	imshow("src", src);
	imshow("dst1", dst1);
	imshow("dst2", dst2);
	waitKey();
}
  • for 문을 이용해서 직선을 그려주는 코드 그림
    • 위에 주석 참고

image

  • 왼쪽이 HoughLines, 오른쪽이 HoughLinesP 함수 실행결과
    • 왼쪽 결과는 쭉 이어진 직선을 찾음
    • 오른쪽 결과는 시작 점과 끝 점이 있는 선분을 찾음
    • 좀 더 많은 허프 라인을 검출 하고 싶으면, threshold 값을 적절하게 낮추면 됨
    • HoughLinesP 함수가 HoughLine 보다 연산 시간이 오래 걸림

image

허프 변환 원 검출

  • 허프 변환을 응용하여 원을 검출할 수 있음
  • 속도 향상을 위해 Hough gradient method 사용
    • 입력 영상과 동일한 2차원 평면 공간에서 축적 영상을 생성
    • 에지 픽셀에서 그래디언트 계산
    • 그래디언트 방향에 따라 직선을 그리면서 값을 누적
    • 원의 중심을 먼저 찾고, 적절한 반지름을 검출
    • 단점 : 여러 개의 동심원을 검출 못하고 가장 작은 원만 검출함
허프 변환 원 검출 함수
void HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double param1 = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0);
  • image : 입력 영상 (에지 영상이 아닌 그레이스케일 영상)

  • circles : cx, cy, r (중심점의 좌표 x,y & 반지름) 정보 (Mat(CV_32FC3) or Vector)

  • method : HOUGH_GRADIENT 또는 HOUGH_GRADIENT_ALT 지정
    • HOUGH_GRADIENT
    • HOUGH_GRADIENT_ALT : OpenCV 4.3 버전부터 지원하며, HOUGH_GRADIENT 보다 더 좋은 성능을 냄
  • dp : 입력 영상과 축적 배열의 크기 비율
    • 1 : 동일크기
    • 2 : 입력 영상의 가로와 세로 크기의 1/2 크기의 축적 배열 사용
  • minDist : 검출된 원 중심점들의 최소 거리
  • param1 : Canny 에지 검출기의 높은 임계값
    • HOUGH_GRADIENT_ALT : 300~400
    • HOUGH_GRADIENT : 100~300
  • param2 :
    • HOUGH_GRADIENT_ALT : 원의 “perfectness” 값을 나타냄(원에 가까울수록 1) (0.8~1.0 사이의 값을 지정)
    • HOUGH_GRADIENT : 축적 배열 임계값(기본값 : 100)
  • minRadius: 검출된 원의 최소 반지름
  • maxRadius : 검출된 원의 최대 반지름
허프 변환 원 검출 예제 코드
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("coins.jpg", IMREAD_GRAYSCALE);

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

	Mat dst1, dst2;
	cvtColor(src, dst1, COLOR_GRAY2BGR);
	cvtColor(src, dst2, COLOR_GRAY2BGR);

	TickMeter tm;

	// HOUGH_GRADIENT

	Mat blr;
	GaussianBlur(src, blr, Size(), 1.0); // Gaussian Blur 한 결과 blr을 만듬

	tm.start();

	vector<Vec3f> circles1;
	HoughCircles(blr, circles1, HOUGH_GRADIENT, 1, 10, 150, 30, 10, 50); // HOUGH_GRADIENT 방법 사용
	// 가우시안 블러작업을 한 blr 영상을 넣어줌, HoughCircles 함수가 입력영상에서 각각의 엣지 픽셀 위치를 상당히 민감하게 반응하는 부분이 있어 오리지널 영상을 그대로 사용할 경우 아주 미세한 차이로 원을 검출하지 못할 수 있음 -> 블러를 통해 영상을 부드럽게 만듬
	// 10 : 중심점의 최소 거리
	

	tm.stop();
	cout << "HOUGH_GRADIENT:     " << tm.getTimeMilli() << "ms." << endl;

	for (size_t i = 0; i < circles1.size(); i++) {
		Vec3i c = circles1[i];
		circle(dst1, Point(c[0], c[1]), c[2], Scalar(0, 0, 255), 2, LINE_AA);
	}

	// HOUGH_GRADIENT_ALT

	tm.reset();
	tm.start();

	vector<Vec3f> circles2;
	HoughCircles(src, circles2, HOUGH_GRADIENT_ALT, 1.5, 10, 300, 0.9, 10, 50); // HOUGH_GRADIENT_ALT 방법 사용  
	// 축적배율의 크기를 1.5배 작게.. 위 HOUGH_GRADIENT 방법에서 블러링된 영상을 입력으로 준 것과 비슷한 느낌
	// 너무 민감하게 검출하지 않고 약간의 잡음이 포함되어있거나 원이 살짝 튀어도 원을 잘 검출할 수 있게 해줌 (적정: 1.5~2.0)
	// 10은 중심점의 최소 거리
	// 1은 완벽한 원을 검출, 0.9는 살짝 찌그러진 것도 검출, 0.8은 더 찌그러진 것도 검출

	tm.stop();
	cout << "HOUGH_GRADIENT_ALT: " << tm.getTimeMilli() << "ms." << endl;

	for (size_t i = 0; i < circles2.size(); i++) { // 결과 표시
		Vec3i c = circles2[i];
		circle(dst2, Point(c[0], c[1]), c[2], Scalar(0, 0, 255), 2, LINE_AA);
	}

	imshow("src", src);
	imshow("dst1", dst1);
	imshow("dst2", dst2);
	waitKey();
}
  • 왼쪽(dst1) : HOUGH_GRADIENT / 오른쪽(dst2) : HOUGH_GRADIENT_ALT
    • HOUGH_GRADIENT_ALT가 더 잘 검출하는 것을 확인할 수 있음

image

  • HOUGH_GRADIENT_ALT 의 연산 시간이 더 빠른 것을 볼 수 있음

image

  • blr 영상이 아닌 src 영상을 그대로 사용했을 때 결과
    • 원이 아닌 것들도 원으로 검출함

image

코너 검출 방법

코너의 정의와 특징
  • 두 에지의 교차점. 서로 다른 방향의 에지가 공전하는 점
  • 평탄한 영역(flat)과 에지(edge) 영역은 고유한 위치를 찾기 어려움
  • 코너는 변별력이 높은 편이며, 영상의 이동 및 회전 변환에 강인함
  • A : Flat / B : Edge / C : Corner

image

해리스 코너 검출 방법
  • 1988년 C.Harris가 제안
  • 영상 내에 정의된 윈도우(window)안의 픽셀 값이 상하좌우 방향으로 모두 급격하게 변하는 위치를 코너로 규정하고 이를 효과적으로 구하는 방법을 제시
  • 다양한 수학적 기법을 이용하여 입력 영상으로부터 해리스 코너 응답 함수 R를 구하고 R(x,y) 값이 충분히 큰 위치를 코너로 지정

image

cornerHarris 함수
void cornerHarris(InputArray src, OutputArray dst, int blockSize, int ksize, double k, int borderType = BORDER_DEFAULT);
  • src : (입력) 단일 채널, 8비트 또는 실수형 연산
  • dst : (출력) 해리스 코너 응답 계수 값 행렬. 입력 영상 src와 같은 크기(CV_32FC1)

image

  • blockSize : 내부 연산에서 사용되는 이웃 픽셀 크기. 주변 blockSize x blockSize 이웃 픽셀을 이용
  • ksize : (미분을 위한) 소벨 연산자 커널 크기
  • k : 해리스 코너 검출 상수 (보통 0.04)
  • borderType : 가장자리 픽셀 확장 방식
추적하기 좋은 특징(Good features to track) 코너 방법
  • 1994년 Shi & Tomasi가 제안
  • 해리스 코너 검출 방법을 향상 시킨 코너 검출 방법
  • 코너 품질 함수 값이 큰 순서대로 정렬하여 코너 점을 반환
  • 비최대 억제 수행(하나의 코너 포인트에 여러 개의 코너포인트가 검출되는 것을 방지)
goodFeaturesToTrack 함수
void goodFeaturesToTrack(InputArray image, OutputArray corners, int maxCorners, double qualityLevel, double minDistance, InputArray mask = noArray(), int blockSize = 3, bool useHarrisDectector = false, double k = 0.04)
  • image : (입력) 8비트 또는 32비트 실수, 단일채널 영상
  • corners : (출력) 검출된 코너점 좌표 (Vector)
  • maxCorners : 최대 코너 개수 (maxCorners<=0 이면 무제함 검출)
  • qualityLevel : 코너점 결정을 위한 값(보통 0.01~0.1)으로 이 값이 커질수록 더욱 엄격하게 코너를 검출함
  • minDistance : 코너점 사이의 최소 거리
  • mask : 마스크 영상
  • blockSize : 내부 연산에서 사용되는 이웃 픽셀 크기. 주변 blockSize x blockSize 이웃 픽셀을 이용
  • useHarrisDectector : 해리스 코너 응답 함수 사용 여부
  • k : 해리스 코너 응답 함수 사용시 사용되는 파라미터
FAST(Features from Accelerated Segment Test) 코너 검출 방법
  • 2006년 Rosten & Drummond가 제안
  • 각각의 픽셀에 대해 주변 16개 픽셀 값 크기를 분석하여 해당 픽셀(p)보다 충분히 밝거나(>p+t) 또는 충분히 어두운 (<p-t) 픽셀이 n개 이상 연속으로 나타나면 코너로 인식 (n은 보통 9)

image

  • 비최대 억제 수행(하나의 코너 포인트에 여러 개의 코너포인트가 검출되는 것을 방지)
  • 해리스, GFTT 방법보다 매우 빠르게 동작(대략적으로 30배 정도 빠름)
    • FAST 방법의 반복 검출률이 대체로 높음
    • 다만 FAST 방법은 노이즈에 민감함

image

FAST 함수
void FAST(InputArray image, std::vector<KeyPoint>& keypoints, int threshold, bool nonmaxSuppression = true);
  • image : (입력) 그레이스케일 영상
  • keypoints : (출력) 검출된 코너점 좌표를 반환하는 출력 키포인트. KeyPoint 클래스의 pt 멤버를 이용하여 코너 좌표에 접근 가능
  • threshold : 중심 픽셀 값과 주변 픽셀 값과의 차이 임계값(t)
  • nonmaxSuppression : 비최대 억제 수행 여부
코너 검출 방법 예제 코드(GFTT, FAST)
#include <iostream>
#include "opencv2/opencv.hpp"

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("building.jpg", IMREAD_GRAYSCALE);

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

	// goodFeaturesToTrack

	TickMeter tm1;
	tm1.start();

	vector<Point2f> corners;
	goodFeaturesToTrack(src, corners, 400, 0.01, 10); // goodFeaturesToTrack

	tm1.stop();
	cout << "goodFeaturesToTrack() took " << tm1.getTimeMilli() << "ms." << endl;

	Mat dst1;
	cvtColor(src, dst1, COLOR_GRAY2BGR);

	for (size_t i = 0; i < corners.size(); i++) { // 코너점 위치에 원을 그림
		circle(dst1, corners[i], 5, Scalar(0, 0, 255), 2);
	}

	// FAST

	TickMeter tm2;
	tm2.start();

	vector<KeyPoint> keypoints;
	FAST(src, keypoints, 60); 	// FAST 현재 점보다 60만큼 밝거나 60만큼 어두운 픽셀이 연속으로 9개 나오면 검출
//	Ptr<FeatureDetector> detector = FastFeatureDetector::create(60);
//	detector->detect(src, keypoints);

	tm2.stop();
	cout << "FAST() took " << tm2.getTimeMilli() << "ms." << endl;

	Mat dst2;
	cvtColor(src, dst2, COLOR_GRAY2BGR);

	for (const KeyPoint& kp : keypoints) { // 코너점 위치에 원을 그림
		circle(dst2, Point(kp.pt.x, kp.pt.y), 5, Scalar(0, 0, 255), 2, LINE_AA);
	}

//	imshow("src", src);
	imshow("dst1", dst1);
	imshow("dst2", dst2);
	waitKey();
}

image

  • GFTT 보다 FAST 검출 방법이 30 배 이상은 빠르게 연산하는 것을 확인할 수 있음
    • FAST 방법이 월등히 빨라 현재로써는 FAST 방법을 사용하는 것이 더 유리함

image