About me
home
Portfolio
home
📗

(김영준 발표) 영상 분할 및 특징 처리

[chap 10. 영상 분할 및 특징 처리]

발표자: 김영준 07/14
<특징점 추출 방법>
직선을 검출하는 허프 변환
객체 추적이나 영상 매칭에 사용되는 코너 검출기법
Image 폴더 안에 다음과 같은 이름으로 저장하기
hough.jpg
road.jpg
harris.jpg

10.1 허프 변환

여러 직선 검출 방법 중에서 가장 널리 사용되고 있는 방법이 바로 허프 변환(Hough transform)이다. 허프 변환은 영상 내의 선, 원 뿐만 아니라 임의의 형태를 지닌 물체를 감지해 내는 대표적인 기술로서 데이터 손실 or 왜곡이 있는 영상에서도 직선을 잘 검출한다.
허프 변환: 영상에서 에지로 인지되는 좌료들을 직교좌표계에서 극좌표계로 변환하여, 검출하고자 하는 물체의 파타미터(ρ,θ)를 추출하는 방법이다.
로우(rho)는 수직거리, 세타(theta)는 각도 이다.
직교 좌표계에서 한 점은 극 좌표계에서는 곡선이 되고, 극 좌표계에서의 한 점은 직교 좌표계에서 직선이 된다.

허프 변환 구현하기의 전체 과정

accumulate() 함수 : 직선 누적 행렬 계산

이미지 내의 모든 좌표에서 직선인지 여부를 점검한다.
입력변수 image는 사전에 전처리를 수행하여 잡음을 제거하고 캐니(canny) 에지를 추출한 후의 이미지로 받는다. 캐니 에지는 이진화(흑백)된 image이므로 0보다 큰 화소를 직선으로 간주한다.
따라서 이 함수는 입력된 image에서 직선으로 인지된 좌표들에 대해서 직교 좌표계에서 극좌표계로 변환하여 인덱스를 구성하고 누적 행렬의 인덱스에 값을 누적한다.
직선의 길이가 임계값 이상인 직선들을 (ρ,θ) 좌표로 찾을 수 있고 허프 변환 수식을 통해 (x,y) 좌표값을 구할 수 있어 이미지상에서 직선을 표현할 수 있다.
def accumulate(image, rho, theta): #rho랑 theta는 거리 간격, 각도 간격이다 h, w = image.shape[:2] rows, cols = (h+w)*2 //rho , int(np.pi / theta) #누적행렬 너비, 높이 accumulate = np.zeros((rows, cols), np.int32) #0으로 찬 누적행렬을 생성한다 #0~180도까지의 사인과 코사인 함수의 값을 미리 계산하여 sin_cos 리스트로 생성한다 sin_cos = [(np.sin(t*theta), np.cos(t*theta)) for t in range(cols)] #호소값이 0보다 큰 위치들(x,y)을 pst에 저장한다. 즉 직선좌표 찾는다 pts = np.where(image > 0) polars = np.dot(sin_cos, pts).T #행렬 곱으로 극좌표 계산 polars = (polars / rho + rows / 2).astype('int') #해상도 변경 및 위치 조정 for row in polars: for t, r in enumerate(row): #t는 각도, r은 수직거리(rho) accumulate[r,t] += 1 #극좌표에 누적 return accumulate #누적행렬을 출력
Python
복사

masking() 함수: 누적 행렬의 지역 최댓값 선정

누적 행렬에서 직선을 찾으려면 누적값이 임계값(thresh) 이상인 좌표들을 가져와야 한다.
문제점: 비슷한 위치에 비슷한 각도로 직선들이 겹쳐져 그려질 수 있다. 또한 다른 위치의 직선은 순위에 밀려 검출하지 못할 수 있다.
해결방법: 마스크를 이용하는 방법이 있다. 누적 행렬에서 블록 구간을 나누어 각 블록(즉 마스크)에서 가장 큰 값만 보존하고 나머지 제거. 마스크 내에서 가장 큰 값을 지역 최댓값이라 한다.
책과 코드에서는 7x3 크기의 마스크를 적용하여 지역 최댓값을 구하였다.
def masking(accumulate, h, w, thresh): #h, w는 마스크의 크기 rows, cols = accumulate.shape[:2] dst = np.zeros(accumulate.shape, np.int32) #누적 행렬과 같은 크기로 반환 행렬 생성 for y in range(0, rows, h): #누적 행렬 조회 for x in range(0, cols, w): roi = accumulate[y:y+h, x:x+w] # roi = 관심영역. 즉 마스크 영역 _, max, _, (x0, y0) = cv2.minMaxLoc(roi) #최댓값과 최댓값 좌표를 가져온다 dst[y+y0, x+x0] = max #최댓값 좌표에 최대값을 저장한다 return dst
Python
복사

select_lines() 함수: 직선 선택 및 정렬

중복이 제거된 누적행렬(dst) 원소중에서 임계값(thresh)보다 큰 값을 선별하여 lines 행렬에 저장한다. 이때, 긴 직선이 먼저 저장되도록 누적값을 기준으로 내림차순 정렬을 수행한다.
def select_lines(acc_dst, rho, theta, thresh): rows = acc_dst.shape[0] r, t = np.where(acc_dst > thresh) #임계값 이상 인덱스 가져옴. r은 로우 인데스, t는 각도 인덱스 rhos = ((r - (rows/2)) * rho) #인덱스로 실제 수직 거리 계산 radians = t * theta #인덱스로 실제 라디안 각도 계산 values = acc_dst[r, t] #인덱스로 누적값들을 가져옴. values는 누적값 행렬 idx = np.argsort(values)[::-1] #누적값 행렬을 내림차순으로 정렬하여 정렬 인덱스를 가져온다 lines = np.transpose([rhos, radians]) #리스트 전치하여 행렬 생성 lines = lines[idx, :] #직선 극좌표(ρ,θ)들만 남긴다 return np.expand_dims(lines, axis=1) #1번(열) 차원 증가
Python
복사

houghLines() 함수: 위 세 함수들을 순서대로 실행한다

즉 허프 변환 모든 과정을 순서대로 한번에 하는 함수라 할 수 있다.
def houghLines(src, rho, theta, thresh): # 허프 변환 함수 acc_mat = accumulate(src, rho, theta) # 직선 누적 행렬 계산 acc_dst = masking(acc_mat, 7, 3, thresh) #마스킹 처리 - 7행,3열 lines = select_lines(acc_dst, rho, theta, thresh) # 임계 직선 선택 return lines
Python
복사

draw_houghLines() 함수: 검출 직선 그리기 함수

허프 변환을 한 결과 행렬로 이미지에 직선을 그리는 함수를 구현한다.
def draw_houghLines(src, lines, nline): #검출 직선 그리기 함수 nline은 그릴 직선 최대 개수 dst = cv2.cvtColor(src, cv2.COLOR_GRAY2BGR) #컬러로 변환 (근데 직선만 컬러로 나옴) min_length = min(len(lines), nline) for i in range(min_length): #검출된 직선의 개수만큼 반복하여 직선을 그린다 rho, radian = lines[i, 0, 0:2] #수직 거리, 각도 - 검출 직선이 3차월 행렬이다 a, b = math.cos(radian), math.sin(radian) #수직 거리와 각도를 직선상의 좌표로 변환하려는 절차 pt = (a * rho, b * rho) # 검출 직선상의 한 좌표(pt) delta = (-1000 * b, 1000 * a) #직선상의 이동 위치. 직선상의 좌표 중 1000만큼 떨어진 2개 좌표를 계산 #pt에서 delta만큼 더한 좌표와 뺀 좌표를 구해서 두 좌표를 잇는 직선을 그린다. pt1 = np.add(pt, delta).astype('int') pt2 = np.subtract(pt, delta).astype('int') cv2.line(dst, tuple(pt1), tuple(pt2), (0,255,0), 2 ,cv2.LINE_AA) return dst
Python
복사

전체코드

실행 결과

과제

10.2 코너 검출

이미지에서 객체를 추적할 때이미지와 이미지를 매칭할 때 특징 정보를 서로 비교를 한다. 특징 정보에는 에지직선같은 것도 있지만, 이미지 매칭을 할 때에는 코너(corner)라고 하는 특징점이 중요 특징 정보로 사용된다.
대표적인 코너 검출기 중 하나인 해리스(Harris) 코너 검출기를 소개해보겠다.
코너 점은 상하좌우 4방향에서 영상의 밝기 변화가 커야 한다. A, C 지점은 모든 방향으로 밝기 변화가 크다. B는 상하 변화는 있지만 좌우 변화는 없으며 D는 모든 방향으로 밝기 변화가 없다.
아래의 수식은 모라벡 알고리즘으로 (u,v)를 변화시켜 E(u,v)맵을 구해서 코너 여부를 판단한다.
하지만 모라벡 알고리즘은 마스크가 0,1의 값만 가지고 상하좌우 4개방향으로 한정시켰기 때문에 노이즈에 취약하다.
해리스 알고리즘모라벡 알고리즘을 개선한 알고리즘이다. 미분을 사용하고 고유값을 계산하고 고유값 분해 등등의 복잡한 과정이 동반되고 이를 간략히 공식으로 만든 코너 응답 함수(cornerHarris()함수)를 통해 코너 응답 행렬(return corner)을 구한다.
Sobel Mask(소벨 마스크): 소벨 마스크는 영상에서 윤곽선을 검출하는데 자주 쓰이는 마스크이며, 모든 방향의 윤곽선, 즉 엣지를 추출할 수 있게 된다. 수직 마스크와 수평 마스크를 적용하여 미분행렬을 구하는 과정을 거치게 된다.(위 사진의 M 행렬) 이렇게 구한 미분행렬은 해리스 코너 검출에 쓰이게 된다.
의의: 해리스 코너 검출 방법은 이미지의 평행이동, 회전 변환에는 불변하는 특징이 있고 어파인(affine) 변환이나 조명(illumination) 변화에도 강인성이 있다는 개선점이 있다.

전체코드

import numpy as np, cv2 def put_string(frame, text, pt, value, color=(120,200,90) ): #문자열 출력함수. 교재 134p text += str(value) shade = (pt[0] + 2, pt[1] + 2) font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(frame, text, shade, font, 0.7, (0,0,0), 2) cv2.putText(frame, text, pt, font, 0.7, color, 2) def cornerHarris(image, ksize, k): #해리스 코너 검출 함수 dx = cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize) #수평 소벨마스크로 미분행렬을 구함 dy = cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize) #수직 소벨마스크로 미분행렬을 구함 a = cv2.GaussianBlur(dx * dx, (5, 5), 0) #가우시안 블러링 수행 b = cv2.GaussianBlur(dy * dy, (5, 5), 0) c = cv2.GaussianBlur(dx * dy, (5, 5), 0) corner = (a * b -c**2) -k *(a+b)**2 # 코너 응답 함수 - 연산 적용 return corner # 코너 응답 행렬이다 #코너 응답 행렬에서 임계값 이상인 좌표를 특징점으로 이미지에 표시한다. def drawCorner(corner, image, thresh): #임계값 이상 코너 표시 cnt = 0 corner = cv2.normalize(corner, 0, 100, cv2.NORM_MINMAX) #코너응답행렬의 값을 0~100 사이값으로 정규화한다 corners=[] for i in range(1, corner.shape[0]-1): for j in range(1, corner.shape[1]-1): neighbor = corner[i-1:i+2, j-1:j+2].flatten() #이웃 화소 9개 가져옴 max = np.max(neighbor[1::2]) #상하좌우 값들 중에서 최대값을 구한다 #현재 코너 좌표값이 임계값(tresh)보다 크고, 주위 원소의 최대값(max)보다 크면 코너로 확정하여 corners 리스트에 저장한다. if thresh < corner[i,j] > max: corners.append((j,i)) #코너 확정 좌표 저장 for pt in corners: #코너 확정 좌표 순회. pt는 좌표 cv2.circle(image, pt, 3, (0,230,0), -1) # 좌표(pt)에 초록색 점 표시 print("임계값: %2d, 코너 개수: %2d" %(thresh, len(corners))) return image def onCornerHarris(thresh): # 트랙바 콜백 함수 img1 = drawCorner(corner1, np.copy(image), thresh) img2 = drawCorner(corner2, np.copy(image), thresh) put_string(img1, "USER", (10,30), "") #영상에서 문자표시 함수 호출 put_string(img2, "OPENCV", (10, 30), "") dst = cv2.repeat(img1, 1, 2) #두 개 이미지를 한 윈도우에 표시 dst[:, img1.shape[1]:, :] = img2 cv2.imshow("harris detect", dst) # main 함수 코드 image = cv2.imread("Image/harris.jpg", cv2.IMREAD_COLOR) if image is None: raise Exception("영상파일 읽기 에러") blockSize = 4 # 이웃 화소 범위 apertureSize = 3 #소벨 마스크 크기 k = 0.04 #k는 상수값으로 일반적으로 0.04~0.06 정도가 적당하다고 함 thresh = 2 #코너 응답 임계값 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) corner1 = cornerHarris(gray, apertureSize, k) #우리가 정의한 함수 corner2 = cv2.cornerHarris(gray, blockSize, apertureSize ,k) #OpenCV 제공 함수 onCornerHarris(thresh) #초기 설정값으로 해리스 검출을 수행 #트랙바 이벤트가 발생할 때마다 onCornerHarris() 함수를 호출한다 cv2.createTrackbar("Threshold", "harris detect", thresh, 20, onCornerHarris) cv2.waitKey(0)
Python
복사

실행 결과

(: 우리가 정의한 해리스 코너검출 함수, : OpenCV에서 제공하는 함수)
<임계값이 0일 때 >
<임계값이 1일 때 >
<임계값이 2일 때 >
Threshold 트랙바를 변경할 때마다 출력되는 로그