본 블로그 포스팅은 수도권 ICT 이노베이션 스퀘어에서 진행하는 인공지능 고급-시각 강의의 CNN알고리즘 강좌 내용을 필자가 다시 복기한 내용에 관한 것입니다.
이번에는 위 사진처럼 원본 이미지에 Kernel 통하여 합성곱 연산을 수행하는 함수를 직접 구현해 보고자 한다.
import numpy as np
def Conv2D(img, kernel=None):
#이미지랑 커널의 높이, 너비 추출
h, w = img.shape
kh, kw = kernel.shape
output_h = h - kh + 1
output_w = w - kw + 1 #결과 이미지의 높이/너비 계산
#결과 이미지 초기화 하기
img_out = np.zeros((output_h, output_w), dtype=np.uint8)
for y in range(output_h):
for x in range(output_w):
#관심영역 추출 region of interest
roi = img[y:y+kh, x:x+kw]
filtered = roi * kernel #합성곱의 '곱'
conv_value = np.abs(np.sum(filtered)) #'곱'한거 '합성'
# 결과를 0-255 범위로 클립하고 uint8로 변환
img_out[y, x] = np.clip(conv_value, 0, 255).astype(np.uint8)
#실제 conv를 돌릴 때는 float32 데이터타입을 주로 씀
return img_out
코드 리뷰를 하자면 아래와 같다.
#이미지랑 커널의 높이, 너비 추출
h, w = img.shape
kh, kw = kernel.shape
output_h = h - kh + 1
output_w = w - kw + 1 #결과 이미지의 높이/너비 계산
위 부분은 Input_img, Kernel, Output_img의 높이/너비 를 계산하기 위한 함수로
그림으로 이해하면 편할 듯 하다.
여기서 h
가 5, kh
가 3이니 output_h
는 5-3+1 = 3이 된다.
그리고 이 값을 바탕으로
img_out = np.zeros((output_h, output_w), dtype=np.uint8)
결과 이미지를 담을 img_out 행렬을 생성한다.
for y in range(output_h):
for x in range(output_w):
이 이중 for문이 수행하는건 아래와 같은 연산을 반복하는 것이다.
위 사진처럼
Input_img
위를 Kernel이 한칸 한칸씩 훑어나감
그리고 훑어 나갈 때마다 합성곱 연산을 수행하는데
합성곱의 곱연산을 수행 후 다음 합성연산을 수행하여, Output_img
매트릭스의 인자값을 채워나간다.
라고 보면 된다.
import cv2
#합성곱 연산 대상 이미지 불러오기
img_file = 'opencv/mountain.jpg'
img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)
#커널을 만들기 -> 커널은 edge 검출 커널임
kernel = np.array([[0,0,0],
[-1,2,-1],
[0,0,0]])
#합성곱 연산함수를 불러와서 출력 이미지 생성
output_img = Conv2D(img, kernel=kernel)
#원본 이미지와 합성곱 연산결과 이미지 출력
cv2.imshow('origin', img)
cv2.imshow('res', output_img)
cv2.waitKey(0)
cv2.destroyAllWindows()
다음으로 설계한 함수로 Input_img
에 대하여
kernel = np.array([[0,0,0],
[-1,2,-1],
[0,0,0]])
위 설계한 커널을 활용하여 합성곱 연산을 수행하면, 아래의 그림처럼 합성곱 연산의 결과 Output_img
를 얻을 수 있다.
다음은 위 사진처럼 Stride
, Padding
개념을 도입하여 합성곱 연산을 수행하는 애니메이션이다.
파란색 ROI 영역이 2칸씩 넘어가고 있으며 이는 Stride = 2
하늘색 Input_img 바깥에 0
으로 채워진 픽셀들이 1줄씩 있으며, 이는 Padding = 1
로 볼 수 있다.
이 Stride
, Padding
인자값의 조절됨에 따라 합성곱의 연산 결과인 Output_img의 크기가 조절 될 수 있으며,
이는 아래의 수식으로 정리 할 수 있다.
위 수식을 활용하여 합성곱 연산(Stide, Padding)인자가 포함된 함수의 구현은 아래와 같다.
def Conv2D(img, kernel=None, stride=1, padding=1):
h, w = img.shape
kh, kw = kernel.shape
#-----padding을 적용하는 부분-------#
padded_h = h + 2*padding
padded_w = w + 2*padding
#원본이미지에 padding붙이기
padded_img = np.zeros((padded_h, padded_w)).astype(np.uint8)
padded_img[padding:h + padding, padding:w + padding] = img
#-----padding을 적용하는 부분-------#
#-----stride을 적용하는 부분-------#
output_h = ((padded_h - kh) // stride) + 1
output_w = ((padded_w - kw) // stride) + 1
#-----stride을 적용하는 부분-------#
img_out = np.zeros((output_h, output_w), dtype=np.uint8)
#출력 Feature Map(출력이미지) 초기화
for y in range(output_h):
for x in range(output_w):
#관심영역 추출 region of interest
step_y, step_x = y*stride, x*stride
roi = padded_img[step_y:step_y+kh, step_x:step_x+kw]
filtered = roi * kernel #합성곱의 '곱'
conv_value = np.abs(np.sum(filtered)) #'곱'한거 '합성'
# 결과를 0-255 범위로 클립하고 uint8로 변환
img_out[y, x] = np.clip(conv_value, 0, 255).astype(np.uint8)
#실제 conv를 돌릴 때는 float32 데이터타입을 주로 씀
return img_out, padded_img
이 함수를 바탕으로 원본 이미지, Padding이 적용된 이미지, 출력 이미지를 시각화 한다면 아래와 같다.
img_file = 'opencv/mountain.jpg'
img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)
kernel = np.array([[0,0,0],
[-1,2,-1],
[0,0,0]]) #커널 만들기
output, padded_img = Conv2D(img, kernel=kernel, stride=2, padding=1)
cv2.imshow('origin', img)
cv2.imshow('ped_img', padded_img)
cv2.imshow('res', output)
cv2.waitKey(0)
cv2.destroyAllWindows()
지금은
stride=2, padding=1
만을 적용하였지만 좀 극단적인 값을 적용하면 아래와 같아진다
stride=3, padding=20