본문 바로가기
프로젝트 모음/임베디드 Humanoid Project

[openCV] Houghline(허프변환)을 이용한 중앙선 및 각도 검출

by 하람 Haram 2022. 11. 12.
728x90

 

 

    yellow1 = np.array([16, 80,140])    #노랑색 최솟값
    yellow2 = np.array([90, 255,255])   #노랑색 최댓값
  
    while (True):
        ret, src = cap.read() #영상파일 읽어드리기
        hsv = cv2.cvtColor(src, cv2.COLOR_BGR2HSV)

        mask_yellow = cv2.inRange(hsv, yellow1, yellow2) # 노랑최소최대값을 이용해서 maskyellow값지정
        res_yellow = cv2.bitwise_and(src, src, mask=mask_yellow) # 노랑색만 추출하기
        srcs = res_yellow    #imgs에 추출한 노랑색 저장
        imgray = cv2.cvtColor(srcs, cv2.COLOR_BGR2GRAY)

 

=> 앞에 포스팅이랑 동일하여 생략  [openCV] Trackbar HSV범위를 이용한 Contour


<Canny Edge>

Gradient 를 이용한 edge 검출방법을 개선한 방법

https://deep-learning-study.tistory.com/206

 

[파이썬 OpenCV] 영상의 그래디언트와 에지 검출하기 -cv2.magnitude, cv2.phase

황선규 박사님의 과 패스트 캠퍼스 OpenCV 강의를 공부하면서 내용을 정리해 보았습니다. 예제 코드 출처 : 황선규 박사님 github홈페이지 『OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝』 예제 소스

deep-learning-study.tistory.com

 

(Canny Edge를 따기 전에 가우시안 필터링을 해주는 것이 좋다)

Canny Edge의 Gradient는 소벨 마스크를 사용(크기와 방향을 모두 고려)니

 

 방향은 4구역으로만 판단을 한다.

 45도를 한 구역으로 설정하고 180도에 대한 4구역을 구합니다.

 나머지 180도는 계산된 구역의 대칭부분이므로 4구역만 이용(직선 이니깐)

 

Non maximum suppression을 사용

(최대 크기의 픽셀만 골라내서 에지 픽셀로 설정하는 것)

=> 하나의 에지가 여러 개의 픽셀로 표현되는 현상을 없애기 위하여

Yolo에서의 Bounding Box에 NMS 적용

이런 식으로 최대값만 뽑아낸다고 한다(자세한건 필요할 때 더 공부하자...)

NMS

 Hysteresis edge tracking(히스테리시스 에지 트래킹)

Canny Edge는 임계값을 두개를 사용함

조명이나 이런 것들의 영향을 최소화 하기 위해서

 상한 임계값(Max val), 하한 임계값(min val)를 사용

1. Max val < x

이건 edge라고 판단 (strong edge)

 

2. min val < x < Max  val 

(weak edge)

Strong edge와 연결 되어 있는 경우에만 edge라 판다

 

3. x < min val

edge가 아님

 

<min val을 점점 낮추는 상황> 점점 더 많이 연결이 된다

(무시당하는 애들이 줄어 들면서 strong edge와 연결되는 애들이 늘어나는 상황 연출)

이건 찾다가 기가 막힌거 같아서 퍼옴&nbsp;https://engineer-mole.tistory.com/243
 
 
cv2.Canny(image, threshold1, threshold2, edges=None, apertureSize=None, L2gradient=None) -> edges
- image : 입력 영상
=> GrayScale영상을 넣는 것이 좋다 (RGB영상이면 3개의 channel에 대해서 각각 canny를 검출)
- threshold1 : min val (선이 아니라고 판단한 애들 중에서 살리고 싶으면 => 선이 뚝뚝 끊기면 이 값을 줄이면 된다)
- threshold2 : max_val (생각하는 임계값 그 자체 개념 => noise가 너무 끼면 이 값을 줄이면 된다)
- edges : 엣지 영상
- apertureSize : 소벨 연산을 위한 커널의 크기 (default 3)
- L2gradient : True 면 L2 norm ( √((dI/dx)^2+(dI/dx)^2) ), False면 L1 norm ( ∥dI/dx∥+∥dI/dy∥ )
=> 생각과는 다르게 L1 norm을 주로 사용

 

인터넷을 검색하다가 이 방법에 제일 좋은 것 같다

1. 먼저 threshold1와 threshold2를 같은 값으로 한다.
2. 검출되길 바라는 부분에 엣지가 표시되는지 확인하면서 threshold2 값을 조정한다.
3. 2번의 조정이 끝나면, threshold1를 사용하여 엣지를 연결시킨다. 
 
 

dst = cv2.Canny(imgray, 50, 200, None, 3) #canny처리하기

canny 처리하기(결과 이미지)
cdst = cv.cvtColor(dst, cv.COLOR_GRAY2BGR)

흑백선 (추출한 선들만)을 컬처로 바꿔줌 (빨강이 더 보기 편하여서)
cdstP = np.copy(cdst) #위에 컬러로 변환한 영상 저장

확률적 허프변환과 비교를 하려고 따로 빼 두었다

 

 

 

허프변환 (Hough TransForm)

굉장히 어려워 보이니 위키백과 차근차근 읽어보자

[그림 1]

어떠한 점을 지나는 직선은 xcosθ + ysinθ = r 으로 표현할 수 있다.

지금 이 파트에서의 관심에 대해 주의깊게 생각해야 한다.

Hough는 이 문제를 아래와 같이 생각했다.

[그림 1]과 같이 어떤 점을 지나가는 무수히 많은 직선들이 존재한다.

그러면 xcosθ + ysinθ = r  에서 x,y를 상수로 잡고 theta와 r을 변수로 잡는다면???

(상수인 이유 : 점 (x,y)는 무조건 지나야 하기 때문에)

이렇게 되면 (x,y)를 지나는 무수히 많은 직선을 theta와 r로 표현 할 수 있다.

자 여기에서

sin@ = x/sqrt(x^2 + y^2) , cos@ = y/sqrt((x^2 + y^2))를 만족하는 @가 있다고 가정을 하면

위의 xcosθ + ysinθ = r 는

sin(θ + @) = r /sqrt((x^2 + y^2)) 로 바꿀수 있다 (θ + @ = x1 ,  r /sqrt((x^2 + y^2) = y1로 치환하면)

=>    y1 =  k * sin(x1)

즉 ,  (r과 θ를 축으로 하는 극좌표계에서) sin곡선으로 표현 할 수 있는 것이다

 

아래의 3개의 점이 주어졌다고 가정을 하자

우리가 목표하는 선은 핑크색 선이고

첫번째, 두번째, 세번째, 점에 대해 각각

θ을 1 ~ 180까지 변화를 하면서 원점에서 (x,y)까지의 거리(r)을 구한다.(위의 예시는 30씩 증가)

 

위키 피디아 (주어진 점 3개 & 우리가 목표하는 선 = 핑크색 선)

해당 좌표( r, θ )를 가지고 xy평면에 직선을 그리면 다음과 같은 sin곡선을 그릴수 있고

세 곡선의 교점 핑크색 점의 값이 우리가 찾는 선의 극좌표  이다. ( r, θ )

이를 확장 하여서 n개의 점에 대해 그려보면 n개의 점을 지나는 직선을 유사하게 그릴 수 있는 것이다.

(n개를 모두 지나는 직선은 그리 많지는 않기 때문에 threshold를 n보다 작은 숫자로 하는 것이 국룰)

 

lines = cv2.HoughLines(img, rho, theta, threshold, lines, srn=0, stn=0, min_theta, max_theta)
img: 입력 이미지, 1 채널 바이너리 스케일

rho , theta거리와 각도를 얼마나 세밀하게 할 것인지
rho: 거리 측정 해상도, 0~1

theta: 각도, 라디안 단위 (np.pi/0~180)
threshold: 직선으로 판단할 최소한의 동일 개수 (작은 값: 정확도 감소, 검출 개수 증가 / 큰 값: 정확도 증가, 검출 개수 감소)
lines: 검출 결과, N x 1 x 2 배열 (batch, ? , (r, θ) )
srn, stn: 멀티 스케일 허프 변환에 사용, 선 검출에서는 사용 안 함
min_theta, max_theta: 검출을 위해 사용할 최대, 최소 각도

출처 : Opencv - 공식 문서

점진성 확률적 허프변환 (Progressive Probabilistic Hough TransForm)

앞에 일반적인 허프변환은 모든 점을 탐색하므로 시간 소요가 많이 된다 (최적화 필요)

=> 모든 점이 아닌 임의의 점 일부만 계산

lines = HoughLinesP(검출 영상, 거리, 각도, 임계값, 최소 선의 길이, 최대 선의 간격) 

임계값까지는 hough 변환과 동일한 parameter지만 뒤에 두개가 다르다

 

최소 선의 길이 : 검출된 직선이 가져야하는 최소한의 선 길이

최대 선 간격 : 검출된 직선들 사이의 최대 허용 간격

 

return 값 lines 는 (N, 1, 4) 차원의 형태를 가진다

마지막 차원인 lines[i][0][0], lines[i][0][1], lines[i][0][2], lines[i][0][3] 은 각각 x1, y1, x2, y2

즉, 시작점 (lines[i][0][0], lines[i][0][1])  끝점 (lines[i][0][2], lines[i][0][3])이다 

(왼) : Multi scale hough transform                                                   (오) : Progressive Probabilistic hough transform

 

lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 0, 0) 
허프변환 적용 


    if lines is not None:
        for i in range(0, len(lines)):

        len()문자열의 길이구하기-허프변환으로 검출된 선의 개수 만큼
            rho = lines[i][0][0] 

            극좌표(r, θ) 중에 길이 r 를 가져옴
            theta = lines[i][0][1]

            극좌표(r, θ) 중에 각도 θ를 가져옴
            a = math.cos(theta)
            b = math.sin(theta)

            x0 = a * rho

            y0 = b * rho

            x0 = rcosθ , y0 = rsinθ
            pt1 = (int(x0 + 1000 * (-b)), int(y0 + 1000* (a))) #시작점 
            pt2 = (int(x0 - 1000 * (-b)), int(y0 - 1000 * (a))) #종료점

            #그냥 이미지 밖으로 보내고 싶어서 1000을 곱했지만 좀더 젠틀한 방법은 아래의 코드를 이용

scale = src.shape[0] + src.shape[1]

x1 = int(x0 + scale * -b)
y1 = int(y0 + scale * a)
x2 = int(x0 - scale * -b)
y2 = int(y0 - scale * a)

                
            cv.line(cdst, pt1, pt2, (0, 0, 255), 3, cv.LINE_AA)

            #라인그리기

    #점진성 확률적 허프변환
    linesP = cv.HoughLinesP(dst, 1, np.pi / 180, 50, None, 50, 10)

 


    if linesP is not None:
        for i in range(0, len(linesP)):
            l = linesP[i][0]

           밑에 깔끔하게 적고 싶어서
            cv.line(cdstP, (l[0], l[1]), (l[2], l[3]), (0, 0, 255), 3, cv.LINE_AA)
           시작점 : ( I[0] , I[1] )     끝점 : ( I[2] , I[3] ) 이고 (0,0,255) -> 빨강색으로 그려라
            if (l[2]-l[0]) == 0:
                continue
            else:
                grad = (l[3] - l[1])/(l[2]-l[0])
                grad_theta = (math.atan(grad))
                회전각(로봇이 방향이 틀어짐을 해결하기 위해 중심기준 틀어진 각도 θ를 구한다)
            if 0 <grad_theta < 1:
                print("Warning :: 좌회전 필요")
            elif -1 < grad_theta < 0:
                print("Warning :: 우회전 필요")
            else:
                print("회전 각도 양호")
            sleep(0.3)

      
    cv.imshow("Original", src)  #원본파일

    cv.imshow("yellow_det", hsv)  #노랑감지
    cv.imshow("Detected Lines (in red) - Standard Hough Line Transform", res_yellow)  #허프변환라인
    cv.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP)  #확률적허프변환라인
        
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break


cap.release()
cv2.destroyAllWindows()

 

 

 

<추가 설명>

cv2.line

cv2.line(img, pt1, pt2, color, thickness = 1, lineType = cv.Line_8, shift = 0)

img : 이미지 파일

pt1 : 시작점의 좌표 (x1,y1)

pt2 : 종료점의 좌표 (x2,y2)

color : 색상 (B,G,R) : (0~255, 0~255, 0~255)

lineType

** Bresenham's algorithm **

- Line_8 : 8-connected line -> 오른쪽, 왼쪽, 위쪽, 아래쪽, 대각선 영역 고려(선분에 픽셀을 할당할 때)

- Line_4 : 4-connected line -> 오른쪽, 왼쪽, 위쪽, 아래쪽 영역만 고려 (선분에 픽셀을 할당할 때)

 

** Anti-Aliasing ** : 영상신호의 결함을 없애기 위해 가장자리 부분에서 발생하는 계단현상 없애고 부드럽게 보이게 함

가우스 필터링을 사용하여서 넓은 선의 경우 항상 끝이 동그래진다

- Line_AA : antialiased line (나라면 이거 쓸 듯)

 

이게 필요한 이유 직선의 방정식으로 점들을 연결하면 실수형태의 점이 발생하게 되지만

이미지는 사각격자 구조로 이루어져있어 모두 정수값으로 구성되기에 필요함

 

shift : fractional bit -> 이걸 사용하면 소숫점 이하의 값이 포함된 실숫값 좌표도 그릴 수 있다

(sub pixel(서비 픽셀)정렬을 이용해서 소숫점 이하 자릿수 표현이 가능)

 

<완성코드>

import cv2
import math
import cv2 as cv
import numpy as np
from time import sleep 


def detect_angle(cap):
    yellow1 = np.array([16, 80,140])    #노랑색 최솟값
    yellow2 = np.array([90, 255,255])   #노랑색 최댓값
    
    while (True):
        ret, src = cap.read() #영상파일 읽어드리기
        hsv = cv2.cvtColor(src, cv2.COLOR_BGR2HSV)

        mask_yellow = cv2.inRange(hsv, yellow1, yellow2) # 노랑최소최대값을 이용해서 maskyellow값지정
        res_yellow = cv2.bitwise_and(src, src, mask=mask_yellow) # 노랑색만 추출하기
        srcs = res_yellow    #imgs에 추출한 노랑색 저장

        imgray = cv2.cvtColor(srcs, cv2.COLOR_BGR2GRAY)
        dst = cv2.Canny(imgray, 50, 200, None, 3) #canny처리하기
        cdst = cv.cvtColor(dst, cv.COLOR_GRAY2BGR) #흑백 ---->컬러 선을 빨갛게 보이기 위
        
        cdstP = np.copy(cdst) #위에 컬러로 변환한 영상 저장
    
        lines = cv.HoughLines(dst, 1, np.pi / 180, 150, None, 0, 0) #허프변환

        if lines is not None:
            for i in range(0, len(lines)): #len()문자열의 길이구하기-허프변환으로 검출된 선의 개수 만큼
                rho = lines[i][0][0]
                theta = lines[i][0][1]
                a = math.cos(theta)
                b = math.sin(theta)
                x0 = a * rho #이걸로 교차점에서 턴하는 값 지정가능
                y0 = b * rho #이걸로 좌우 정할수 있는데 비슷한 맥락으로 나는 tan를 씀
                pt1 = (int(x0 + 1000 * (-b)), int(y0 + 1000* (a))) #시작점 
                pt2 = (int(x0 - 1000 * (-b)), int(y0 - 1000 * (a))) #종료점
                
                cv.line(cdst, pt1, pt2, (0, 0, 255), 3, cv.LINE_AA) #라인그리기

        #확률적 허프변환
        linesP = cv.HoughLinesP(dst, 1, np.pi / 180, 50, None, 50, 10)
        if linesP is not None:
            for i in range(0, len(linesP)):
                l = linesP[i][0]
                cv.line(cdstP, (l[0], l[1]), (l[2], l[3]), (0, 0, 255), 3, cv.LINE_AA)

                if (l[2]-l[0]) == 0:
                    continue
                else:
                    grad = (l[3] - l[1])/(l[2]-l[0])
                    grad_theta = (math.atan(grad))
                    print(grad_theta)
                if 0 <grad_theta < 1:
                    print("Warning :: 좌회전 필요")

                elif -1 < grad_theta < 0:
                    print("Warning :: 우회전 필요")

                else:
                    print("회전 각도 양호")

                sleep(0.3)

      
        cv.imshow("Original", src) #원본파일
        cv.imshow("yellow_det", hsv) #노랑감지
        cv.imshow("Detected Lines (in red) - Standard Hough Line Transform", res_yellow) #허프변환라인
        cv.imshow("Detected Lines (in red) - Probabilistic Line Transform", cdstP) #확률적허프변환라인
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break


    cap.release()
    cv2.destroyAllWindows()

#HSV H(Hue; 색조), S(Saturation; 채도), V(Value; 명도),powerpoint에서 찾은값에 H는 1/2해줘야함
#H:0~179 S:0~255 V:0~255


#cv2.Canny(가져올파일,임계값1,임계값2,커널크기,L2그라디언트)
#임계값1이하에 포함된 가장자리는 가장자리에서 제외
#임계값2이상에 포함된 가장자리는 가장가지로 간주
#커널크기 : Aperture size
#L2그라디언트 :L2방식 √((dI/dx)^2+(dI/dx)^2)의 사용 유무 없으면
#L1 ∥dI/dx∥+∥dI/dy∥사용 간주



#cv2.line(img,pt1,pt2,color,thickness,linetype,shift)
#img이미지 파일 pt1시작점 좌표(x,y) pt2종료점 좌표(x,y)
#color(blue,green,red) thickness(선두께 default 1)
#lineType(선 종류 default cv.Line_8)
#Line_8 : 8 connected line , Line_4 : 4 connected line , Line_AA antialiased line
#shift fractional bit (default 0)


#허프변환
    #cv2.HoughLines(image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]])
    #image - Output of the edge detector,회색조 이미지여야
    #rho - r값의 범위 (0~1 실수) 주로 1 사용  , 매개변수의 해상도
    #theta -θ값의 범위 (0~180 정수) pi/180=1
    #threshold - 만나는 점의 기준, 숫자가 작으면 많은 선이 검출되지만 정확도가 떨어짐
    #srn 및 stn 기본 매개 변수는 0



#확률적허프변환
    #linesP = cv.HoughLinesP (dst, 1, np.pi / 180, 50, None , 50, 10)
    #dst-  edge 변환의 출력 (회색이여야함)
    #lines: A vector that will store the parameters (xstart,ystart,xend,yend) of the detected lines
    #rho : The resolution of the parameter r in pixels. We use 1 pixel.
    #theta: The resolution of the parameter θ in radians. We use 1 degree (CV_PI/180)
    #threshold: 선을 감지하기위한 최소 교차 수 
    #minLinLength: 선을 형성 할 수있는 최소 포인트 수. 이 포인트 수보다 적은 라인은 무시됩니다..
    #maxLineGap: 같은 선에서 고려할 두 점 사이의 최대 간격.

 

 

참고

https://engineer-mole.tistory.com/243

 

[python/OpenCV] cv2.Canny():Canny방법을 이용하여 물체의 외곽선(엣지) 추출하기

외곽선(엣지)란 물체간 혹은 배경과의 경계를 일컫는 것으로, 외곽선(엣지) 검출이란 일반적으로 이미지 안의 화소치의 변화, 휘도의 변화가 커다란 부분을 검출하여 엣지를 추출하는 이미지 처

engineer-mole.tistory.com

https://deep-learning-study.tistory.com/207

 

[파이썬 OpenCV] 영상의 윤곽선 검출하기 - 캐니 에지 검출 - cv2.Canny

 

deep-learning-study.tistory.com

https://076923.github.io/posts/Python-opencv-28/

 

Python OpenCV 강좌 : 제 28강 - 직선 검출

직선 검출(Line Detection)

076923.github.io

https://076923.github.io/posts/Python-opencv-18/

 

Python OpenCV 강좌 : 제 18강 - 도형 그리기

도형 그리기(Drawing)

076923.github.io

 

728x90