본문 바로가기
Computer Vision

15. Hough Transform Circle

by 꿈꾸는 띵땅근 2021. 2. 7.
학습 내용
1. Hough Transform Line Fitting
2. Hough Transform Circle
3. Hough Transform Circle C++ 코드구현

 


 

1. Hough Transform Line Fitting


딥러닝에서도 쓰이는 최소자승법(Least Square Method)

- E는 편차 제곱의 평균. E가 최소가 되게 하는 m과 c를 찾아라!

→ 편미분하여 극소인 점을 찾는것!

- Modeling(=Parameterization)을 y=mx+c로 했다. 

y=mx+c로 parameterization하면 위와 같이 파란색 선이 생겨버린다;;
y값의 차이 말고, 점과 직선사이의 거리가 좋을 것 같다.
rho, theta로 parameterization하자. 
curve는 어쩔 수 없이 y값 차이로 fitting 해야할것이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

2. Hough Transform Circle


기본적인 Hough Transform 개념. 

- x-y 영역(Image Space)의 한 직선을 rho-theta 영역(Hough Space)으로 옮기면, 한 점이 된다. 

- 점 하나 찍으면, Voting(투표) 했다고 말한다.

- Image Space에서 가장 많이 밀집되어 있는 점(가장 많이 voting된 점)이 Image Space에서 직선일 확률이 가장 높다. 

 

- 원을 찾기 위해서 parameterization을 (x-a)^2+(y-b)^2=r^2으로 했다.

- 여기서, r을 알고있다고 하면, 미지수는 a,b이므로, hough space는 a,b점들, 즉, 원의 중심들의 값이 voting된다.

• x-y좌표계에서의 점들이 있다. A(xi, yi)라는 점을 예시로 들면, A점을 지나는 원들은 무수히 많다. 
• 그 원들의 중심점을 (a,b)라 하면, a-b좌표계에 점을 찍을 경우, 똑같이 ‘원’이 그려진다.
• 즉, x-y좌표계에서의 한 원은 a-b좌표계에서 한 점으로 대응되는 것이다.
• 즉, x-y좌표계에서의 한 점이 a-b좌표계에서 한 원으로 대응되는 것이다. 

• a-b좌표계에서 원들이 가장 많이 겹치는 부분을 주목해야한다. 
• 이 점은, x-y좌표계의 점들을 가장 많이 지나는, x-y좌표계 상의 원이다.
• 여기까지, 반지름을 알고 있을때의 방법이고, 아래는 반지름을 모를때, 반지름을 조금씩 키워나가면서 확인하는 방법이다.

 

- 반지름을 모르면, r을 키워가면서 a,b를 voting해야한다.

 

a=x-rcos(th), b=y-rsin(th) 라는 식이 발견되었다.

Hough Circle Detector를 코딩으로 구현하려면, 
• 일단, (Xi, Yi)들 즉, 엣지 포인트들을 알고 있어야 한다.
• 또, 엣지 포인트 마다 gradient방향을 알고 있어야 한다.
• 반지름을 알고있다면, (Xi, Yi)점을 지나는 원의 중심(a,b)을 위의 수식을 통해 구할 수 있다.
• (a,b)가 산출되면, Accumulator에 하나 축적한다. 
• 나중에 Accumulator에서 Local Maxima를 찾으면, 그게 우리가 찾는 원이 된다.

 

 

 

Gradient Information알고있으면, 저렇게 원을 많이 그릴 필요 없다. 금방 찾아낼 수 있다. 

Canny Edge를 하면 엣지의 Gradient 방향을 알 수 있다. 

 

파란색 화살표가 엣지의 방향이다. 
반지름 R만큼 안으로 또는 밖으로 Voting 하면 Voting 많이 모인점이 있다. 그곳이 원의 중심이다. 

 

 

 

 

 

 

3. Hough Transform Circle C++ 코드구현

"양자화" 라는 개념이 있는데, 아래 그림에서 설명
자세히 보면, 점을 지나지 않는데, 직선을 그었다. 점 주변으로 일정 범위 픽셀 뭉치를 만들어서 "양자화" 했기 때문이다.

 

void Jeong::HoughCircle(const KImageGray& igIn, KImageGray &igOut, KImageGray &igVoting, int R)
{

    double  dAngleRAD;
    Point2d ptCen1, ptCen2;
    int     nGridCx, nGridCy;
    int     dx[8] = {1,1,0,-1,-1,-1,0,1};
    int     dy[8] = {0,1,1,1,0,-1,-1,-1};
    KImageGray igEdge(this->_jRow, this->_jCol);
    qDebug("%d,%d", igIn.Row(), igIn.Col());
    qDebug("check1");
    CannyEdgeDetection(igIn, igEdge, 30, 130);

    // create voting table
    _dSx = 3;
    _dSy = 3;
    int nNumGridCy = (int)(igIn.Row()/_dSy); // 양자화 → 몇개의 양자가 있을것인가
    int nNumGridCx = (int)(igIn.Col()/_dSx); // 양자화
    _arVotes.Create(nNumGridCy, nNumGridCx); // row, col 순서
    qDebug("check2");
    //Cx = k*_dSx
    //Cy = k*_dSy

    // 반지름 크기 알고있다고 가정하고 한다.
    // 반지름 모를때는 3차원 배열을 선언해야함. 내 블로그에 소스 나와있음.


    double MaxV = 0;

    // Hough Transform Circle
    while(!(Edge.empty())){ // Edge는 EdgeInfo* 벡터로, Canny Edge한 결과이다.
        //complete circle centers for voting
        dAngleRAD = _RADIAN(Edge.back()->_ang); // 라디안 값으로 바꿔줘야함
        ptCen1._nx = Edge.back()->_nx - R*cos(dAngleRAD); // 엣지가 바깥으로 향한다고 가정하고 한다. 이게 아니라면 양쪽 다 해줘야한다.
        ptCen1._ny = Edge.back()->_ny - R*sin(dAngleRAD);
        ptCen2._nx = Edge.back()->_nx + R*cos(dAngleRAD); // 엣지가 안쪽을 향한다고 했을 때
        ptCen2._ny = Edge.back()->_ny + R*sin(dAngleRAD); // 엣지가 안쪽을 향한다고 했을 때
        Edge.pop_back();

        // 엣지가 안쪽을 향한다고 했을 때
        if(ptCen1._nx>=0 || ptCen1._ny>=0 || ptCen1._nx <igIn.Col() || ptCen1._ny <igIn.Row()){
            nGridCx = (int)(ptCen1._nx/_dSx);    // k번째를 구한거다.
            nGridCy = (int)(ptCen1._ny/_dSy);    // k번째를 구한거다.

            // votes to avoid edge noises
            for(int i=-5; i<6; i++)
                for(int j=-5; j<6; j++){
                    if(nGridCx+j<0 || nGridCy+i<0 || nGridCx+j>=nNumGridCx || nGridCy+i>=nNumGridCy) continue;
                    _arVotes[nGridCy+i][nGridCx+j] += 0.7;
                    if(_arVotes[nGridCy+i][nGridCx+j] > MaxV) MaxV = _arVotes[nGridCy+i][nGridCx+j];
                }
            _arVotes[nGridCy][nGridCx] += 0.3; // 결국 얘는 1을 더해준꼴
        }

        // 엣지가 바깥쪽을 향한다고 했을 때
        if(ptCen2._nx>=0 || ptCen2._ny<0 || ptCen2._nx <igIn.Col() || ptCen2._ny <igIn.Row()){
            nGridCx = (int)(ptCen2._nx/_dSx);    // k번째를 구한거다.
            nGridCy = (int)(ptCen2._ny/_dSy);    // k번째를 구한거다.

            // votes to avoid edge noises
            for(int i=-5; i<6; i++)
                for(int j=-5; j<6; j++){
                    if(nGridCx+j<0 || nGridCy+i<0 || nGridCx+j>=nNumGridCx || nGridCy+i>=nNumGridCy) continue;
                    _arVotes[nGridCy+i][nGridCx+j] += 0.7;
                    if(_arVotes[nGridCy+i][nGridCx+j] > MaxV) MaxV = _arVotes[nGridCy+i][nGridCx+j];
                }
            _arVotes[nGridCy][nGridCx] += 0.3; // 결국 얘는 1을 더해준꼴
        }
    }
    qDebug("check3");
    // Voting 이미지 보기 위해서 다음과 같이 색칠함.
    for(int i=0; i<igIn.Row(); i++)
        for(int j=0; j<igIn.Col(); j++){
            igVoting[i][j] = (int)((_arVotes[(int)(i/_dSy)][(int)(j/_dSx)]) / MaxV * 255);
        }
    qDebug("check4");

    // 8방향 Non-maxima suppression (그냥 제일 voting 많이 된 local maxima를 찾는것)
    // 그와중에 Threshold를 넘어야 함.
    for(int i=0; i<nNumGridCy; i++)     // 행
        for(int j=0; j<nNumGridCx; j++){ // 열
            int cnt = 0;
            if(_arVotes[i][j] < MaxV-0.5) // threshold 걸어줬다. threshold보다 작으면 local maxima여도 의미가 없다.
                continue;
            for(int k=0; k<8; k++){
                int ny = i+dy[k]; // 행
                int nx = j+dx[k]; // 열
                if(nx<0 || ny<0 || nx>=nNumGridCx || ny>=nNumGridCy)
                    continue;
                if(_arVotes[i][j]<=_arVotes[ny][nx])
                    break;
                cnt++;
                if(cnt==8){
                    CircleCenter.push_back(Point2i((int)(i*_dSy)+(int)(_dSy/2), (int)(j*_dSx)+(int)(_dSx/2)));
                }
            }
        }

    // 원 검출한 결과를 표시하기
    while(!CircleCenter.empty()){
        int a = CircleCenter.back()._nx; // 원의 중심
        int b = CircleCenter.back()._ny; // 원의 중심
        qDebug("(x, y) = (%d, %d)", a, b);
        CircleCenter.pop_back();

        for(int y=b-R; y<=b+R; y++)
            for(int x=a-R; x<=a+R; x++){
                if(x<0 || y<0|| x>=igOut.Col() || y>=igOut.Row())
                    continue;
                if(_SQR(x-a) + _SQR(y-b) <= R*R+50 && _SQR(x-a) + _SQR(y-b) >= R*R-50)
                    igOut[y][x] = 255;
            }
    }
}

 

 

 

 

 

 

출처

댓글