본문 바로가기
OpenCV/OpenCV-Chapter

CH06 Opencv-Python 영상의 특징추출(Gradient)

by kiimy 2021. 12. 19.
728x90

edge검출(detection)과 미분

에지(edge)

• 영상에서 픽셀의 밝기 값이 급격하게 변하는 부분

• 일반적으로 배경과 객체, 또는 객체와 객체의 경계

1. Step Function= 보통 사용하지 않음
2. 서서히 바뀌는 형태(일반적으로 pixel를 서서히 바뀜)
3. Noise가 껴있는 상태
4. Gaussian(blur를 통해서 미분을 진행)

미분(derivative)

미분이란?

미세한 부분에서 어떤 변화가 얼마만큼 발생하고있는지 판단하는 기법(= 순간변화률)

==> 변화가 얼마만큼 나타내는 척도

- 그림1. 작아지거나 커지는 부분을 미분

- 값이 급격하게 변하는 부분을 미분(= 해당 부분의 Edge를 검출하기위해서)

 

*영상에서는 pixel 값이 연속함수가 아니기 때문에 그림3 처럼 나오게됨*

T(Threshold) 보다 큰 부분은 Edge로 판단하겠다는 것

그림3 = a(3개의 픽셀을 Edge로 판단하겠다), Threshold를 어떻게 설정하느냐가 중요

 

How 미분 계산?

1차 미분 근사화(approximation)

I(x+h), I(x-h) == 미분마스크(1, -1) / X방향 성분과 Y방향 성분

전진 차분 : 앞에 있는 pixel에서 나 자신을 뺀다
후진 차분 : 현재 위치에서 내 뒤에 있는 pixel을 뺀다
중앙 차분 : x위치에서 미분을 구하고 싶은데 x의 픽셀값을 사용하지 않아(x+h, x-h 사용)

* 일반적으로 미분은 전진차분 처럼

x(현재위치) + h(변화량) -x(현재위치) / h(변화량)

== 연속함수에서의 변화량이 lim 0에 가까워질때 0.000000...형태로 만들 수 있다.

 

* 영상에서의 미분

h(변화량)의 최소단위가 1 pixel이라고 생각하면 됨

중앙차분을 주로 사용함 = Mask를 설정해주는데 1/2은 생략을 함
Why?
Threshold를 잘 조절해줌으로써 Edge를 검출하는 것이기 때문에 

Sobel Filter

기본적으로 3*3을 사용하고 Sobel을 주로 사용(1:2:1)

cv2.Sobel(src, ddepth, dx, dy, dst=None, ksize=None, scale=None, 
			delta=None, borderType=None) -> dst
'''
ddepth: 출력 영상 데이터 타입. -1이면 입력 영상과 같은 데이터 타입을 사용
		cv2.CV_8U ... type 설정
대부분 dx=1, dy=0, ksize=3 또는
dx=0, dy=1, ksize=3 으로 지정

scale 연산 결과에 추가적으로 곱할 값. 기본값은 1.
delta: 연산 결과에 추가적으로 더할 값. 기본값은 0
'''

src = cv2.imread('ch06\\images\\lenna.bmp', cv2.IMREAD_GRAYSCALE)

if src is None:
    print('Image load failed!')
    sys.exit()

'''
*미분 마스크 sobel*
sobel_kernel = np.float32([[-1,0,1],
                    	   [-2,0,2],
                    	   [-1,0,1]])
dx = cv2.filter2D(src, -1, sobel_kernel)
'''
# -1을 주었기 때문에 grayscale 형태로 나옴
# ddepth = -1 이면 입력 영상 type과 똑같이 나옴
# 회색부분에서 검은색 부분으로 값이 낮아지는 경우 미분값이 음수값이 됨
# 그래서 출력영상에서 그냥 검은색으로 나옴 so, delta=128 할당
# delta = 연산 결과에 추가적으로 더할 값. 기본값은 0
# == 눈으로 잘 보이게 하기 위해?

dx = cv2.Sobel(src, -1, 1, 0, delta=128)
dy = cv2.Sobel(src, -1, 0, 1, delta=128) # 수직인 edge는 나오지 않음

dx, dy

흰색 부분은 값이 급격하게 증가한 부분
검은 부분은 값이 급격하게 감소한 부분

어느 방향으로 미분하느냐에 따라 값이 바뀐부분이 다름
so, 따로 미분을 하고 합치는 과정을 진행(Gradient = magintude, phase)


Gradient와 에지 검출

- 영상이란 것이 2차원 평면에서 정의된 함수 형태이기 때문에 dx, dy 따로 미분(편미분)

= 이렇게 따로 표현하게되면 수식이 지져분해질 수 있다.

==> So, 벡터라는 형태로 묶어서 하나로 표현 = Gradient
==> dx, dy 따로 미분 후 묶어서 표현

크기: magnitude(유클리디안 거리), 방향 = Theta

Gradient 크기와 방향의 의미

Gradient 크기: 픽셀 값의 차이 정도, 변화량

Gradient 방향: 픽셀 값이 가장 급격하게 증가하는 방향(밝아지는 방향)

                       = Edge에서 수직인 방향

빨간색: Gradient크기, 방향, 노란색: Edge방향

a, b는 바깥 배경과 안에 배경이 똑같기 때문에 Gradient 크기는 같다
c는 바깥 배경보다 안에가 더 밝기 때문에 값이 더 클 것
방향은 바뀌는 부분에서의 수직인 방향으로 가는것(밝은 쪽으로)
cv2.magnitude(x, y, magnitude=None) -> magnitude
'''
magnitude: 2D 벡터의 크기 행렬. x와 같은 크기, 같은 타입
			magnitude = 유클리디안 거리
'''

# Gradient를 사용하기위해 float로 변환하는 것이 좋음
# delta 제거 (연산을 위함)
dx = cv2.Sobel(src, cv2.CV_32F, 1, 0)
dy = cv2.Sobel(src, cv2.CV_32F, 0, 1)

# 2D 벡터의 크기 계산
# 후에 이 크기가 내가 정한 Threshold보다 큰 부분을 엣지라고 판단
mag = cv2.magnitude(dx, dy) # --> float 형태 so, imshow하면 흰부분으로 나옴
mag = np.clip(mag, 0, 255).astype(np.uint8)
print(mag)
'''
# 2D 벡터의 방향계산 = Canny에서 사용됨
pha = cv2.phase(dx, dy, angle=, angleInDegrees=)
angleInDegrees= True이면 angle 단위, False이면 radian 단위
'''

# Threshold 설정
dst = np.zeros(src.shape[:2], np.uint8)
dst[mag > 120] = 255 # edge 부분만 흰색으로 나머진 검은색

#_, dst = cv2.threshold(mag, 120, 255, cv2.THRESH_BINARY)

mag , dst

dy했을때는 수직 기둥이 보이지 않았는데 magnitude를 진행하면 기둥이 보인다


Canny 검출

좋은 에지 검출기의 조건 (J. Canny)

• 정확한 검출(Good detection): edge가 아닌 점을 edge로 찾거나 또는

                                         edge인데 edge로 찾지 못하는 확률을 최소화

• 정확한 위치(Good localization): 실제 edge의 중심을 검출

• 단일 에지(Single edge): 하나의 에지는 하나의 점으로 표현

Sobel의 단점
1. Edge가 두껍게 표현됨
2. 조명의 영향 or 픽셀 값의 변화가 미세하게 바뀌면 Threshold를 어떻게 설정하느냐에
   따라 Edge가 달라짐

Canny 검출 단계

1. Gaussian filtering = 잡음제거 목적
2. Gradient(크기, 방향)
== Theta를 4구역으로 단순화 시킴

2. Gradient 방향을 4구역으로 단순화


3. 비최대 억제(Non-maximum suppression) - 최대가 아닌 것들을 없애버린다.(최대만 골라내자)
* Threshold를 설정하였을 때 가장 큰 pixel 값을 최종적으로 Edge pixel로 선정하자
  = 미분값을  좌, 우 비교하면서 가장 큰 부분을 local minimum으로 선정

Threshold를 설정하고 가장큰 pixle값을 Edge로
그림1, 그림2 미분 값들

* Gradient 방향에 위치한 두 개의 픽셀을 조사하여 국지적 최대를 검사

* Edge는 거의비슷한 값을 가지고 있고 Gradient는 Edge의 수직인 방향

빨간색 : Gradient 방향

노란색 : Edge 방향

ex) 그림1 = 135미분값과 245 미분값과 비교 135가 작으니 버린다는 것이다.

결국 하나의 Edge는 하나의 점으로 표현하게됨(얇게)

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

- 임계값을 두개 사용 ( T Low, T High )

why?

Threshold를 하나만 설정하니 조명의 영향 때문에 영상의 Gradient가 바뀔 수 있음

= 조명으로 인해 pixel의 미분 값이 작게 나올 수 있는데 이런 부분이 Edge로 판단이 안될 수 있음

Edge란 보통 이어져있다 근데 조명으로 인해 판단이 어려울 수 있다.
weak Edge 부분에서의 pixel이 Strong Edge와 만나면 Edge로 판단
cv2.Canny(image, threshold1, threshold2, edges=None, apertureSize=None, 
			L2gradient=None) -> edges
'''
threshold1: 하단 임계값( Low )
threshold2: 상단 임계값( High )
(threshold1:threshold2 = 1:2 또는 1:3)
edges: 에지 영상
apertureSize: 소벨 연산을 위한 커널 크기. 기본값은 3
L2gradient: True이면 L2 norm 사용, False이면 L1 norm 사용. 기본값은 False
gradient 크기를 계산할때 L2norm이 좋아 but 연산 시간이 오래걸림
'''

dst1 = cv2.Canny(src, 50, 150)

def onChange(pos):
    low = cv2.getTrackbarPos('threshold1', 'canny')
    high = cv2.getTrackbarPos('threshold2', 'canny')

    img_canny = cv2.Canny(src, low, high)
    cv2.imshow('canny', img_canny)

# Trackbar (사용할 함수 파라미터 지정)
cv2.namedWindow('canny') 
cv2.createTrackbar('threshold1', 'canny', 0, 1000, onChange)
cv2.createTrackbar('threshold2', 'canny', 0, 1000, onChange)

# 각 파라미터 초기값
cv2.setTrackbarPos('threshold1', 'canny', 50)
cv2.setTrackbarPos('threshold2', 'canny', 150)

비최대억제에서 회색부분이 있는데 히스테리시스에선 그 선을 최종적으로 Edge로 판단

* 비최대 억제에서 중간중간 흰색 부분이 있는데 해당 부분은 Edge로 판단이 되지 않음

728x90

댓글