반응형



직선 y = mx + b을 매개변수식으로 바꾸면 r = xcosθ + ysinθ이다. 여기서 r은 원점으로부터 직선까지의 수직거리이며 θ는 이 직선과 수직인 직선과 x축 사이의 각도를 시계 반대방향으로 측정한 값이다.




좌표 (300,300)을 지나는 모든 직선에 대한 r과 θ를 구해서 극좌표계에 그려보면 사인곡선이 그려진다. 식 r = xcosθ + ysinθ에 좌표 (300,300)을 대입하고 θ를 0에서 180까지 증가시키면서 r값을 구한 결과이다.


좌표(100,100), (200,200), (300,300) 세 점을 지나는 모든 직선에 대한 r과 θ를 구하여 극좌표계에 그려보면 세 개의 사인곡선이 그려지는데 한 점에서 만나는 것을 확인할 수 있다.


교차점의 r,θ 값을 구하여 직선의 방정식으로 변환 후, 그려보면 세 점 (100,100), (200,200), (300,300)을 지나는 직선임을 확인 할 수 있다.



다음은 hough line transform 구현 과정이다.

1.입력 영상을 그레이스케일 영상으로 변환한다.


2. 에지를 검출한다. sobel 또는 canny를 사용하면 된다.


3. 에지에 속하는 모든 점에 대해 다음을 수행한다.  

해당 점을 지나는 모든 직선에 대한 r, θ를 구하여 accumulator의 (r,θ) 위치값을 1씩 증가시킨다. accumulator는 2차원 배열로 구현된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            if ( img_edge.at<uchar>(y,x) > 0 )
                for (int angle = 0; angle < 180; angle++)
                {
                    int r = (x-centerX)*table_cos[angle] + (y-centerY)*table_sin[angle];
 
                    r = r + hough_height;//r이 음수인 경우 때문 -r ~ r 범위를 0 ~ 2r 범위로 변경
                    accumulator[r*accumulator_width + angle] += 1;
                }
        }
    }
cs


4. accumulator를 이미지화 해보면 수많은 곡선들이 그려진다. 하나의 곡선은 하나의 점을 지나는 모든 직선들이다. 곡선들의 교점이 직선일 가능성이 있는 후보점인데 많은 곡선이 겹쳐질수록 실제 에지 위를 지나는 직선일 가능성이 높아진다.

일정 threshold값 이상이면서 local maxima인 경우를 찾아 파란색 원을 그려보면 4개의 위치가 찾아진다. 



5. 찾은 r,θ값을 직선으로 변환한다. 입력영상에 직선을 그리기 위해서 직선의 양끝점을 구해야 한다.

식 r = xcosθ + ysinθ은  다음 두 식으로 변환할 수 있다.


45 <= θ <= 135 인 경우 ( 수평선에 가까운 경우 )         

y = (r- xcosθ)/ sinθ


45 > θ 또는 135 < θ 경우 ( 수직선에 가까운 경우 )

x = (r- ysinθ)/ cosθ


6. 입력 영상에 직선을 그린다. 정사각형 주위로 4개의 파란색 직선들이 그려졌다.

 



다른 실행 결과들




전체 소스코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#include <opencv2/highgui.hpp>  
#include <opencv2/imgproc.hpp>  
#include <iostream>
#include <stdio.h>
 
using namespace cv;
using namespace std;
 
 
int main(int argc, char** argv)
{
 
    Mat img_original, img_gray, img_edge;
 
    //이미지파일 로드 
    img_original = imread("test2.jpg", IMREAD_COLOR);
    //img_original = imread("test2.png", IMREAD_COLOR);
    //img_original = imread("test.jpg", IMREAD_COLOR);
    if (img_original.empty())
    {
        cout << "Could not open or find the image" << std::endl;
        return -1;
    }
 
 
    //그레이스케일 이미지로 변환  
    cvtColor(img_original, img_gray, COLOR_BGR2GRAY);
 
    //에지 검출
    GaussianBlur(img_gray, img_edge, cvSize(55),0,0);
    Canny(img_edge, img_edge, 501503);
 
 
    int height = img_edge.rows;
    int width = img_edge.cols;
 
    double hough_height = ((sqrt(2.0* (double)(height>width ? height:width)) / 2.0);
    int centerX = width / 2.0;
    int centerY = height / 2.0;
 
 
    int accumulator_width = 180;
    int accumulator_height = hough_height * 2;
    int accumulator_size = accumulator_height * accumulator_width;
    
    int *accumulator = (int*)malloc(sizeof(int)*accumulator_size);
    memset(accumulator, 0sizeof(int)*accumulator_size);
 
 
    double table_sin[180], table_cos[180];
    double DEG2RAD = CV_PI / 180;
    for (int angle = 0; angle < 180; angle++)
    {
        table_sin[angle] = sin(angle*DEG2RAD);
        table_cos[angle] = cos(angle*DEG2RAD);
    }
 
 
    
 
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            if ( img_edge.at<uchar>(y,x) > 0 )
                for (int angle = 0; angle < 180; angle++)
                {
                    int r = (x-centerX)*table_cos[angle] + (y-centerY)*table_sin[angle];
 
                    r = r + hough_height;//r이 음수인 경우 때문 -r ~ r 범위를 0 ~ 2r 범위로 변경
                    accumulator[r*accumulator_width + angle] += 1;
                }
        }
    }
        
 
    //accumulator에서 MAX값 찾기
    int max = -1;
    for (int y = 0; y < accumulator_height; y++)
    for (int x = 0; x < accumulator_width; x++)
    {
        if (accumulator[y*accumulator_width + x] > max)
            max = accumulator[y*accumulator_width + x];
    }
    
 
 
    //accumulator를 이미지화
    Mat img_accumulator = Mat(accumulator_height, accumulator_width, CV_8UC3);
 
    for (int y = 0; y < accumulator_height; y++)
    for (int x = 0; x < accumulator_width; x++)
    {
        int value = 255 * ((double)accumulator[y*accumulator_width + x]) / max;
        
        img_accumulator.at<Vec3b>(y, x)[0= 255-value;
        img_accumulator.at<Vec3b>(y, x)[1= 255-value;
        img_accumulator.at<Vec3b>(y, x)[2= 255-value;
 
    }
 
 
 
    
    int count = 0;
    for (int r = 0; r < accumulator_height; r++)
    for (int angle = 0; angle < 180; angle++)
    {
 
        if (accumulator[r * accumulator_width + angle] > 50)
        {
 
            // 11 x 11 영역내에서 현재 위치가 local maxima 인지 검사
            int max = accumulator[r * accumulator_width + angle];
            for (int y = -5; y <= 5; y++)
            {
                for (int x = -5; x <= 5; x++)
                {
                    int new_r = r + y;
                    int new_angle = angle + x;
 
                    if ( new_angle  < 0 )
                    {
                        new_angle = 180 + new_angle;
                    }
                    else if (new_angle >= 180) {
                        new_angle = new_angle - 180;
                    }
                    
                    
                    if ( new_r >= 0 && new_r < accumulator_height) 
                    {
                        if (accumulator[ new_r * accumulator_width + new_angle] > max)
                        {
                            max = accumulator[ new_r * accumulator_width + new_angle];
                            x = y = 6;//loop 종료
                        }
                    }
                }
            }
 
            if (max > accumulator[r * accumulator_width + angle])
            {
                continue//현재 위치는 local maxima가 아님
            }
 
 
            // r = x*cos(theta) + y*sin(theta)
            // y = (r- x*cos(theta))/ sin(theta) 수평선인 경우
            // x = (r- y*sin(theta))/ cos(theta) 수직선인 경우
 
            int x1, x2, y1, y2;
 
            if (angle >= 45 && angle <= 135//수평선
            {
                x1 = 0;
                x2 = width;
                y1 = ((r - hough_height) - (x1 - centerX)*table_cos[angle]) / table_sin[angle] + centerY;
                y2 = ((r - hough_height) - (x2 - centerX)*table_cos[angle]) / table_sin[angle] + centerY;
            }
            else //수직선
            {
                y1 = 0;
                y2 = height;
                x1 = ((r - hough_height) - (y1 - centerY)*table_sin[angle]) / table_cos[angle] + centerX;
                x2 = ((r - hough_height) - (y2 - centerY)*table_sin[angle]) / table_cos[angle] + centerX;
 
            }
 
            circle(img_accumulator, cvPoint(angle, r), 3, Scalar(25500));
            line(img_original, cvPoint(x1, y1), cvPoint(x2, y2), Scalar(25500), 1);
            count++;
 
            cout << count << " (" << x1 << "," << y1 << ") (" << x2 << "," << y2 << ")  angle=" << angle<<" r="<<r<< " accumulator값="<< accumulator[r * accumulator_width + angle]<<endl;
        }
 
    }
 
    
 
    imshow("img_result", img_original);
    imshow("img_gray", img_gray);
    imshow("img_edge", img_edge);
    imshow("img_accumulator", img_accumulator);
 
    imwrite("img_gray.jpg", img_gray);
    imwrite("img_edge.jpg", img_edge);
    imwrite("img_accumulator.jpg", img_accumulator);
    imwrite("img_result.jpg", img_original);
 
 
    //키보드 입력이 될때까지 대기  
    waitKey(0);
 
 
    return 0;
}
cs



반응형

포스트 작성시에는 문제 없었지만 이후 문제가 생길 수 있습니다.
질문을 남겨주면 가능한 빨리 답변드립니다.

여러분의 응원으로 좋은 컨텐츠가 만들어집니다.
지금 본 내용이 도움이 되었다면 유튜브 구독 부탁드립니다. 감사합니다 ~~

유튜브 구독하기


제가 쓴 책도 한번 검토해보세요 ^^

  1. 감사맨 2018.07.14 18:45

    감사합니다~

  2. 2020.04.01 10:56

    비밀댓글입니다

    • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2020.04.01 11:29 신고

      허프 변환의 목적이 이미지에서 직선 방향의 에지를 찾는 것이라서 그렇습니다.

      그레이스케일 이미지에는 에지에 대한 정보가 없기 때문에 캐니 등을 사용하여 에지를 검출한 결과를 가지고 허프변환을 하게 됩니다.

  3. 2020.04.01 17:49

    비밀댓글입니다

    • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2020.04.01 18:09 신고

      에지를 구성하는 픽셀들을 극좌표계로 변환하면 각각 사인곡선이 됩니다. 이 곡선들의 교점이 일정값 이상인 경우를 찾으면 직선을 구할수 있게 됩니다

  4. 2020.04.02 10:40

    비밀댓글입니다

    • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2020.04.02 10:58 신고

      에지를.구성하는 하나하나의 픽셀이 허프공간에서 곡선이 됩니다. 이 곡선의 교점을 찾아서 직선의 방정식으로 변환하는 것입니다.

      C++로 작성한 코드입니다.

  5. 2020.07.31 16:45

    비밀댓글입니다

    • Favicon of https://webnautes.tistory.com BlogIcon webnautes 2020.07.31 18:54 신고

      극좌표로 바꾸면 0~180도 사이의 각도값과 상수값을 얻을 수 있습니다.

      각도를 가로축으로하고 상수값을 세로축으로 한건데.. 세로축의 최대값을 정확히 정하는 방법을 알지 못해서 코드에선 임의로 정한겁니다

  6. 2020.08.11 22:29

    비밀댓글입니다

+ Recent posts