Hough Transform 코드 분석
Hough Transform
Hough transform이란
\[y=ax+b ↔ r=acos\theta + bsin\theta\]다음과 같이 직선의 방정식을 r과 θ 로 나타내는 것부터 시작한다.
직교좌표계에서는 수직선일 경우 기울기가 무한대여서 표현하기 적합하지 않기 때문에,
영상의 에지 점들을 극 좌표계로 옮겨 r 과 θ 로 직선을 표현한다.
그림1
그림2
그림 1의 a1,a2,a3를 지나는 모든 직선을 극좌표계로 표현하면 그림 2와 같이 된다.
즉, 그림2의 a1 곡선은 a1을 지나면서 θ=0인 직선부터, θ=π인 직선까지 극 좌표계로 표현한 그래프인 것이다.
그렇다면 저 세 곡선이 만나는 점은 무엇을 의미할까를 생각해보면,
점 a1을 지나면서 거리가 r, 각도가 θ인 직선,
=점 a2를 지나면서 거리가 r, 각도가 θ인 직선,
=점 a3를 지나면서 거리가 r, 각도가 θ인 직선
인 것이다.
그림3
a1에서 θ를 증가시키면서 직선을 관찰하다 보면, 그 직선이 a2, a3를 지날 때가 있을 것이다.
a2,a3도 마찬가지이다. 그 때의 직선의 r과 θ는 모두 같을 것이다.
그렇기 때문에 r,θ 그래프에서 한 점에서 만나는 것이다.
그림4
점 3개를 예로 들어 극좌표계에 표시해보면, 그림4와 같다.
각도가 0부터 $\pi$까지 증가하면서 해당하는 (r,θ) 좌표값을 1 증가시킨다.
그러면 a1,a2,a3를 지나는 직선에 해당하는 누적된 좌표값은 3이 될 것이다.
이 누적된 값이 가장 큰 좌표가 가장 긴 직선이라 할 수 있겠다.
이렇게 누적된 값을 이용하여 직선을 추출하는 데 이용하는 것이다.
이미지로 적용
자 이제 x,y 그래프의 1사분면을 image 공간이라고 생각해보자.
각 점들은 이미지의 픽셀을 의미한다.
단계는 다음과 같다.
-
각 픽셀마다 θ=0인 직선부터, θ=π인 직선을 돌며 극좌표계 행렬에 표시
- 직선이 겹치지 않도록 local maximum값 선정
- 임계값 이상의 누적 좌표값 선별
- 직선을 누적값 기준으로 내림차순 정렬
허프 변환 좌표계를 위한 행렬
$-\rho_{max}\leq \rho\leq \rho_{max}$ , $\rho_{max} = height + width, acc_h = (\rho_{max}*2)/\Delta\rho$
$0\leq \theta\leq\theta_{max}, \theta_{max}=\pi, acc_\omega=\pi/\Delta\theta$
(근데 왜 $\rho_{max} = height + width$ 인지 모르겠다..)
void hough_coord(Mat image, Mat& acc_mat, double rho, double theta)
{
int acc_h = int((image.rows + image.cols) * 2 / rho);
int acc_w = int(CV_PI / theta);
acc_mat = Mat(acc_h, acc_w, CV_32S, Scalar(0));
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++)
{
Point pt(j, i);
if (image.at<uchar>(pt) > 0) {
for (int t = 0; t < acc_w; t++)
{
double radian = t * theta;
double r = pt.x * cos(radian) + pt.y * sin(radian);
r = cvRound(r / rho + acc_mat.rows / 2.0);
acc_mat.at<int>( (int)r, t)++;
}
}
}
}
}
image 픽셀이 0보다 크면, 그 점에서 각도를 $\pi$까지 증가시키며 acc_mat 행렬에 그 때의 (r, θ) 좌표값을 1 증가시킨다. canny edge 함수로 edge만 뽑아낸 image를 활용하기 때문에 연산량이 생각보다 많지 않을것이다. acc_mat 행렬에 넣을 때는 r값이 마이너스인 경우를 고려하여 acc_mat.rows/2를 더해준다.
Local maximum 추출
그림5
그림5와 같이 3x7 사이즈의 마스크를 이용하여 그 마스크의 중앙값이 최대일 경우 그 값만 빼고 나머지는 0으로 만드는 과정이다.
void acc_mask(Mat acc_mat, Mat& acc_dst, Size size, int thresh)
{
acc_dst = Mat(acc_mat.size(), CV_32S, Scalar(0));
Point h_m = size / 2; // 마스크 크기 절반
for (int r = h_m.y; r < acc_mat.rows - h_m.y; r++) {
for (int t = h_m.x; t < acc_mat.cols - h_m.x; t++)
{
Point center = Point(t, r) - h_m;
int c_value = acc_mat.at<int>(center); // 중심화소
if (c_value >= thresh)
{
double maxVal = 0;
for (int u = 0; u < size.height; u++) {
for (int v = 0; v < size.width; v++)
{
Point start = center + Point(v, u);
if (start != center && acc_mat.at<int>(start) > maxVal)
maxVal = acc_mat.at<int>(start);
}
}
Rect rect(center, size);
if (c_value >= maxVal)
{
acc_dst.at<int>(center) = c_value;
acc_mat(rect).setTo(0);
}
}
}
}
}
acc_mat 행렬에서 마스크를 돌면서 maxVal을 찾고, center 값이 maxVal보다 크면 acc_dst의 행렬에 center값을 넣고 acc_mat행렬에 해당 마스크 안의 좌표값을 다 0으로 만든다. 이것을 반복하면 그림5의 두번째 그림처럼 직선이 겹치지 않게 local maximum을 추출할 수 있다.
Line 그리기
void thres_lines(Mat acc_dst, Mat& lines, double _rho, double theta, int thresh)
{
for (int r = 0; r < acc_dst.rows; r++) {
for (int t = 0; t < acc_dst.cols; t++)
{
float value = (float)acc_dst.at<int>(r, t); // 누적값
if (value >= thresh) // 직선 길이 임계값
{
float rho = (float)((r - acc_dst.rows / 2) * _rho); // 수직거리
float radian = (float)(t * theta); // 각도
Matx13f line(rho, radian, value); // 단일 직선
lines.push_back((Mat)line);
}
}
}
}
Line을 그리기 위해 threshold값 이상의 누적값을 가진 좌표값들을 line이라는 1*3 행렬에 저장한 후 다시 lines라는 행렬에 (r,θ,value) 행렬을 넣어준다. 나중에 value가 큰 순서부터 정렬하기 위해서이다.
lines 행렬 정렬
void sort_lines(Mat lines, vector<Vec2f>& s_lines )
{
Mat acc = lines.col(2), idx;
sortIdx(acc, idx, SORT_EVERY_COLUMN + SORT_DESCENDING);
for (int i = 0; i < idx.rows; i++)
{
int id = idx.at<int>(i);
float rho = lines.at<float>(id, 0); // 수직거리
float radian = lines.at<float>(id, 1);
s_lines.push_back( Vec2f(rho,radian));
}
}
lines 행렬의 2번째 값인 value(누적값)을 기준으로 내림차순 정렬하는 과정이다.
idx 행렬에 value 값이 큰 순서부터 들어간다.
그 후, s_lines 벡터에 (r,θ) 벡터를 넣어 허프라인을 그릴 벡터를 생성한다.
houghLines
void houghLines(Mat src, vector<Vec2f>& s_lines, double rho, double theta, int thresh)
{
Mat acc_mat, acc_dst, lines;
hough_coord(src, acc_mat, rho, theta);
acc_mask(acc_mat, acc_dst, Size(3, 7), thresh);
thres_lines(acc_dst, lines, rho, theta, thresh);
sort_lines(lines, s_lines);
}
위의 함수들을 한 함수에 정리한 것이다.
허프라인 그리기
void draw_houghLines(Mat image, Mat& dst, vector<Vec2f> lines, int nline)
{
if (image.channels() == 3) image.copyTo(dst);
else cvtColor(image, dst, COLOR_GRAY2BGR);
for (int i = 0; i < min((int)lines.size(), nline); i++)
{
float rho = lines[i][0], theta = lines[i][1];
double a = cos(theta), b = sin(theta);
Point2d delta(1000 * -b, 1000 * a);
Point2d pt(a*rho, b*rho);
line(dst, pt + delta, pt - delta, Scalar(0, 255, 0), 1, LINE_AA);
}
}
min((int)lines.size(),nline)은 최대 nline 값만큼만 line을 그리겠다는 것이다. lines 벡터는
dst 행렬에 pt+delta 포인트부터 pt-delta까지 그리는데, 왜 그런지 한번 생각해보았다.
그림6
위 그림과 같이 pt에서 1000픽셀만큼 떨어진 A1, A2 점을 잡아 라인을 그리겠다는 것이다.
전체 코드
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
void hough_coord(Mat image, Mat& acc_mat, double rho, double theta)
{
int acc_h = int((image.rows + image.cols) * 2 / rho);
int acc_w = int(CV_PI / theta);
acc_mat = Mat(acc_h, acc_w, CV_32S, Scalar(0));
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++)
{
Point pt(j, i);
if (image.at<uchar>(pt) > 0) {
for (int t = 0; t < acc_w; t++)
{
double radian = t * theta;
double r = pt.x * cos(radian) + pt.y * sin(radian);
r = cvRound(r / rho + acc_mat.rows / 2.0);
acc_mat.at<int>( (int)r, t)++;
}
}
}
}
}
void acc_mask(Mat acc_mat, Mat& acc_dst, Size size, int thresh)
{
acc_dst = Mat(acc_mat.size(), CV_32S, Scalar(0));
Point h_m = size / 2; // 마스크 크기 절반
for (int r = h_m.y; r < acc_mat.rows - h_m.y; r++) {
for (int t = h_m.x; t < acc_mat.cols - h_m.x; t++)
{
Point center = Point(t, r) - h_m;
int c_value = acc_mat.at<int>(center); // 중심화소
if (c_value >= thresh)
{
double maxVal = 0;
for (int u = 0; u < size.height; u++) {
for (int v = 0; v < size.width; v++)
{
Point start = center + Point(v, u);
if (start != center && acc_mat.at<int>(start) > maxVal)
maxVal = acc_mat.at<int>(start);
}
}
Rect rect(center, size);
if (c_value >= maxVal)
{
acc_dst.at<int>(center) = c_value;
acc_mat(rect).setTo(0);
}
}
}
}
}
void thres_lines(Mat acc_dst, Mat& lines, double _rho, double theta, int thresh)
{
for (int r = 0; r < acc_dst.rows; r++) {
for (int t = 0; t < acc_dst.cols; t++)
{
float value = (float)acc_dst.at<int>(r, t); // 누적값
if (value >= thresh) // 직선 길이 임계값
{
float rho = (float)((r - acc_dst.rows / 2) * _rho); // 수직거리
float radian = (float)(t * theta); // 각도
Matx13f line(rho, radian, value); // 단일 직선
lines.push_back((Mat)line);
}
}
}
}
void sort_lines(Mat lines, vector<Vec2f>& s_lines )
{
Mat acc = lines.col(2), idx;
sortIdx(acc, idx, SORT_EVERY_COLUMN + SORT_DESCENDING);
for (int i = 0; i < idx.rows; i++)
{
int id = idx.at<int>(i);
float rho = lines.at<float>(id, 0); // 수직거리
float radian = lines.at<float>(id, 1);
s_lines.push_back( Vec2f(rho,radian));
}
}
void houghLines(Mat src, vector<Vec2f>& s_lines, double rho, double theta, int thresh)
{
Mat acc_mat, acc_dst, lines;
hough_coord(src, acc_mat, rho, theta);
acc_mask(acc_mat, acc_dst, Size(3, 7), thresh);
thres_lines(acc_dst, lines, rho, theta, thresh);
sort_lines(lines, s_lines);
}
void draw_houghLines(Mat image, Mat& dst, vector<Vec2f> lines, int nline)
{
if (image.channels() == 3) image.copyTo(dst);
else cvtColor(image, dst, COLOR_GRAY2BGR);
for (int i = 0; i < min((int)lines.size(), nline); i++)
{
float rho = lines[i][0], theta = lines[i][1];
double a = cos(theta), b = sin(theta);
Point2d delta(1000 * -b, 1000 * a);
Point2d pt(a*rho, b*rho);
line(dst, pt + delta, pt - delta, Scalar(0, 255, 0), 1, LINE_AA);
}
}
int main()
{
Mat image = imread("../image/hough_test.jpg", 0);
CV_Assert(image.data);
Mat canny, dst1, dst2;
GaussianBlur(image, canny, Size(5, 5), 2, 2);
Canny(canny, canny, 100, 150, 3);
double rho = 1, theta = CV_PI / 180;
vector<Vec2f> lines1, lines2;
houghLines(canny, lines1, rho, theta, 50);
HoughLines(canny, lines2, rho, theta, 50);
draw_houghLines(canny, dst1, lines1, 10);
draw_houghLines(canny, dst2, lines2, 10);
imshow("source", image);
imshow("canny", canny);
imshow("detected lines", dst1);
imshow("detected lines_OpenCV", dst2);
waitKey();
}
결과물
그림 7 원본,canny edge, hough lines