--- 본 포스팅은 데이콘 서포터즈 "데이크루 2기" 활동의 일환입니다 ---
- 안녕하세요 데이콘 서포터즈 데이크루 2기 포스(POS)팀의 Rahites입니다 :)
- POS팀은 Python OpenCV Study의 약자로 활동 기간동안 저희 팀은 '파이썬으로 만드는 OpenCV 프로젝트' 책을 가지고 OpenCV를 공부해보고 프로젝트를 진행할 것입니다.
- 자세한 스터디 계획과 운영 방안은 아래의 포스팅에서 확인하실 수 있습니다.
https://dacon.io/codeshare/4759?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
https://dacon.io/codeshare/4875?utm_source=dacrew&utm_medium=432727&utm_campaign=dacrew_2
지금부터 4주차 활동 시작하겠습니다!! ( •̀ ω •́ )✧
5. 기하학적 변환¶
이번 장에서는 영상의 모양을 바꾸는 방법을 알아보겠습니다. 기하학적 변환이란 영상의 좌표에 기하학적인 연산을 가해서 변환된 새로운 좌표를 얻는 것입니다. 영상에 기하학적인 연산을 가하면 이동, 확대, 축소, 회전 등 일상생활에서 흔히 접하는 변환, 볼록 거울에 비친 모습, 물결에 비친 모습과 같은 여러 왜곡된 모양으로 변환할 수 있습니다.
5.1. 이동, 확대/축소, 회전¶
영상의 기하학적 변환은 변환하고자 하는 새로운 좌표인 x', y'를 구하는 연산이 필요합니다. 이를 위해서는 픽셀 전체를 순회하면서 각 좌표에 대해 연산식을 적용하여 새로운 좌표를 구합니다. 이 때 행렬식을 통해 가장 효과적으로 연산식을 표현할 수 있습니다.
이미지의 어떤 점 p(x,y)를 dx와 dy만큼 옮기면 새로운 위치의 좌표 p(x', y')을 구할 수 있습니다.
위의 방정식을 행렬식으로 바꾸어 표현하면 다음과 같습니다.
5.1.1. 이동¶
- dst = cv2.warpAffine(src, mtrx, dsize [, dst, flags, borderMode, borderValue])
- src : 원본 영상, NumPy 배열
- mtrx : 2 x 3 변환행렬
- dsize : 결과 이미지 크기
- flags : 보간법 알고리즘 선택 플래그
- cv2.INTER_LINEAR : 기본 값, 인접한 4개 픽셀 값에 거리 가중치 사용
- cv2.INTER_NEAREST : 가장 가까운 픽셀 값 사용
- cv2.INTER_AREA : 픽셀 영역 관계를 이용한 재샘플링
- cv2.INTER_CUBIC : 인접한 16개 픽셀 값에 거리 가중치 사용
- cv2.INTER_LANCZOS4 : 인접한 8개 픽셀을 이용한 란초의 알고리즘
- borderMode : 외곽 영역 보정 플래그
- cv2.BORDER_CONSTANT : 고정 색상 값
- cv2.BORDER_REPLICATE : 가장 자리 복제
- cv2.BORDER_WRAP : 반복
- cv2.BORDER_REFLECT : 반사
- borderValue : cv2.BORDR_CONSTANT의 경우 사용할 색상 값
- dst : 결과 이미지 ( NumPy 배열 )
이 함수는 src 영상을 mtrx 행렬에 따라 변환해서 dsize 크기로 만들어 반환합니다. 이 때 변환에 대부분 나타나는 픽셀 탈락 현상을 보정해 주는 보간법 알고리즘과 경계 부분의 보정 방법도 선택할 수 있습니다.
# 영상을 이동변환하는 예제
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread('../img/rat.jpg')
rows,cols = img.shape[0:2] # 영상의 크기
dx, dy = 100, 50 # 이동할 픽셀 거리
# ---① 변환 행렬 생성
mtrx = np.float32([[1, 0, dx],
[0, 1, dy]])
# ---② 단순 이동
dst = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy))
# ---③ 탈락된 외곽 픽셀을 파랑색으로 보정
dst2 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
cv2.INTER_LINEAR, cv2.BORDER_CONSTANT, (255,0,0) )
# ---④ 탈락된 외곽 픽셀을 원본을 반사 시켜서 보정
dst3 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
cv2.INTER_LINEAR, cv2.BORDER_REFLECT)
cv2.imshow('original', img)
cv2.imshow('trans',dst)
cv2.imshow('BORDER_CONSTANT', dst2)
cv2.imshow('BORDER_FEFLECT', dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()
imgs = {'original':img, 'trans':dst, 'BORDER_CONSTANT':dst2, 'BORDER_FEFLECT':dst3}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.1.2. 확대/축소¶
영상을 확대 또는 축소하려면 원래 있던 좌표에 원하는 비율만큼 곱해서 새로운 좌표를 구합니다.
import cv2
import numpy as np
img = cv2.imread('../img/rat.jpg')
height, width = img.shape[:2]
# --① 0.5배 축소 변환 행렬
m_small = np.float32([[0.5, 0, 0],
[0, 0.5,0]])
# --② 2배 확대 변환 행렬
m_big = np.float32([[2, 0, 0],
[0, 2, 0]])
# --③ 보간법 적용 없이 확대 축소
dst1 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)))
dst2 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)))
# --④ 보간법 적용한 확대 축소
dst3 = cv2.warpAffine(img, m_small, (int(height*0.5), int(width*0.5)), \
None, cv2.INTER_AREA)
dst4 = cv2.warpAffine(img, m_big, (int(height*2), int(width*2)), \
None, cv2.INTER_CUBIC)
# 결과 출력
imgs = {'small':dst, 'big':dst2, 'small INTER_AREA':dst3, 'big INTER_CUBIC':dst4}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
print(img.shape) # 원본
print(dst1.shape) # 0.5배 축소
print(dst2.shape) # 2배 확대
print(dst3.shape)
print(dst4.shape)
(256, 256, 3) (128, 128, 3) (512, 512, 3) (128, 128, 3) (512, 512, 3)
보간법 알고리즘에서 축소는 cv2.INTER_AREA가 효과적이고, 확대는 cv2.INTER_CUBIC과 cv2.INTER_LINEAR가 효과적인 것으로 알려져 있습니다. 위와 같이 변환행렬을 작성하지 않고도 OpenCV에서는 따로 확대와 축소 기능을 사용할 수 있는 resize함수를 따로 제공합니다.
- dst= cv2.resize(src, dsize, dst, fx, fy, interpolation)
- src : 입력 영상, NumPy 배열
- dsize : 출력 영상 크기
- fx, fy : 크기 배율, 생략시 dsize 적용
- interpolation : 보간법 알고리즘 선택 플래그 ( cv2.warpAffine()과 동일 )
- dst : 결과 영상
img = cv2.imread('../img/rat.jpg')
height, width = img.shape[:2]
#--① 크기 지정으로 축소
#dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)),\
# None, 0, 0, cv2.INTER_AREA)
dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)), \
interpolation=cv2.INTER_AREA)
#--② 배율 지정으로 확대
dst2 = cv2.resize(img, None, None, 2, 2, cv2.INTER_CUBIC)
#--③ 결과 출력
plt.figure(figsize = (10,6))
imgs = {'original':img, 'small':dst1, 'big':dst2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
print(img.shape) # 원본
print(dst1.shape) # 0.5배 축소
print(dst2.shape) # 2배 확대
(256, 256, 3) (128, 128, 3) (512, 512, 3)
5.1.3. 회전¶
영상을 회전시키기 위해서는 삼각함수를 활용합니다.
img = cv2.imread('../img/rat.jpg')
rows,cols = img.shape[0:2]
# ---① 라디안 각도 계산(60진법을 호도법으로 변경)
d45 = 45.0 * np.pi / 180 # 45도
d90 = 90.0 * np.pi / 180 # 90도
# ---② 회전을 위한 변환 행렬 생성
m45 = np.float32( [[ np.cos(d45), -1* np.sin(d45), rows//2],
[np.sin(d45), np.cos(d45), -1*cols//4]])
m90 = np.float32( [[ np.cos(d90), -1* np.sin(d90), rows],
[np.sin(d90), np.cos(d90), 0]])
# ---③ 회전 변환 행렬 적용
r45 = cv2.warpAffine(img,m45,(cols,rows))
r90 = cv2.warpAffine(img,m90,(rows,cols))
# ---④ 결과 출력
plt.figure(figsize = (10,6))
imgs = {'original':img, '45':r45, '90':r90}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
- mtrx = cv2.getRotationMatrix2D(center, angle, scale)
- center : 회전 축 중심 좌표 (x, y)
- angle : 회전 각도, 60진법
- scale : 확대/축소 배율
# OpenCV에서 제공하는 함수
img = cv2.imread('../img/rat.jpg')
rows,cols = img.shape[0:2]
#---① 회전을 위한 변환 행렬 구하기
# 회전축:중앙, 각도:45, 배율:0.5
m45 = cv2.getRotationMatrix2D((cols/2,rows/2),45,0.5)
# 회전축:중앙, 각도:90, 배율:1.5
m90 = cv2.getRotationMatrix2D((cols/2,rows/2),90,1.5)
#---② 변환 행렬 적용
img45 = cv2.warpAffine(img, m45,(cols, rows))
img90 = cv2.warpAffine(img, m90,(cols, rows))
#---③ 결과 출력
plt.figure(figsize = (10,6))
imgs = {'original':img, '45':img45, '90':img90}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.2. 뒤틀기¶
5.2.1. 어핀 변환¶
어핀(affine) 변환은 앞서 다룬 이동, 확대/축소, 회전을 포함하는 변환으로 직선, 길이의 비율 평행성을 보존하는 변환을 말합니다. 이러한 어핀 변환의 성질 때문에 변환 전과 후의 3개의 점을 짝 지어 매핑할 수 있다면 변환행렬을 거꾸로 계산할 수 있습니다. OpenCV의 cv2.getAffineTransform() 함수로 이 기능을 제공합니다.
- matrix = cv2.getAffineTransform(pts1, pts2)
- pts1 : 변환 전 영상의 좌표 3개 ( 3 x 2 NumPy 배열 )
- pts2 : 변환 후 영상의 좌표 3개
- matrix : 변환 행렬 반환 ( 2 x 3 행렬 )
import cv2
import numpy as np
from matplotlib import pyplot as plt
file_name = '../img/rat.jpg'
img = cv2.imread(file_name)
rows, cols = img.shape[:2]
# ---① 변환 전, 후 각 3개의 좌표 생성
pts1 = np.float32([[100, 50], [200, 50], [100, 200]])
pts2 = np.float32([[80, 70], [210, 60], [250, 120]])
# ---② 변환 전 좌표를 이미지에 표시
cv2.circle(img, (100,50), 5, (255,0), -1)
cv2.circle(img, (200,50), 5, (0,255,0), -1)
cv2.circle(img, (100,200), 5, (0,0,255), -1)
#---③ 짝지은 3개의 좌표로 변환 행렬 계산
mtrx = cv2.getAffineTransform(pts1, pts2)
#---④ 어핀 변환 적용
dst = cv2.warpAffine(img, mtrx, (int(cols*1.5), rows))
#---⑤ 결과 출력
imgs = {'original':img, 'affine':dst}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.2.2. 원근 변환¶
원근 변환은 보는 사람의 시각에 따라 같은 물체도 먼 것은 작게, 가까운 것은 크게 보이는 현상인 원근감을 주는 변환을 말합니다. 우리가 원근감을 느끼게 되는 이유는 현실이 3차원으로 이루어져있기 때문입니다. 따라서 2차원으로 구성된 영상과 현실간의 차이를 보정해 줄 수 있는 연산과 시스템이 필요한데, 이 때 사용하는 좌표계를 동차 좌표(homogeneous coordinates)라고 하며 이 때문에 원근 변환을 다른 말로 호모그래피(homography)라고도 합니다.
동차 좌표를 간단히 설명하면 2차원 좌표인 (x, y)에 1개의 상수항을 추가하며 (wx, wy, w)로 표현하고 이를 다시 2차원 좌표로 바꿀 때는 다시 상수항 w로 나누어 (x/w, y/w)로 표현합니다. 그래서 원근 변환을 하려면 (x,y,1) 꼴의 좌표계가 필요하고 3x3 변환행렬식이 필요합니다. OpenCV에서는 4개의 매핑 좌표만 지정해주면 원근 변환에 필요한 3x3 변환행렬을 계산해주는 함수를 제공합니다.
- mtrx = cv2.getPerspectiveTransform(pts1, pts2)
- pts1 : 변환 이전 영상의 좌표 4개 ( NumPy 배열 4 x 2)
- pts2 : 변환 이전 영상의 좌표 4개
- mtrx : 변환행렬 반환, 3x3 행렬
또한 원근 변환을 수행하는 함수로 cv2.warpPerspective 함수를 제공합니다. 이 함수는 앞서 소개한 warp Affine() 함수와 기능만 다를 뿐 사용방법은 같습니다.
file_name = "../img/rat.jpg"
img = cv2.imread(file_name)
rows, cols = img.shape[:2]
#---① 원근 변환 전 후 4개 좌표
pts1 = np.float32([[0,0], [0,rows], [cols, 0], [cols,rows]])
pts2 = np.float32([[100,50], [10,rows-50], [cols-100, 50], [cols-10,rows-50]])
#---② 변환 전 좌표를 원본 이미지에 표시
cv2.circle(img, (0,0), 10, (255,0,0), -1)
cv2.circle(img, (0,rows), 10, (0,255,0), -1)
cv2.circle(img, (cols,0), 10, (0,0,255), -1)
cv2.circle(img, (cols,rows), 10, (0,255,255), -1)
#---③ 원근 변환 행렬 계산
mtrx = cv2.getPerspectiveTransform(pts1, pts2)
#---④ 원근 변환 적용
dst = cv2.warpPerspective(img, mtrx, (cols, rows))
imgs = {'original':img, 'perspective':dst}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
실제로는 찍은 사진을 스캔한 것 처럼 표현하는 경우가 더 많기 때문에, 이 때에는 찍은 사진의 원근감을 제거해줍니다.
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 좌상단에서부터 시계방향으로 선택해야 합니다.
win_name = "scanning"
img = cv2.imread("../img/taeyeon.jpg")
rows, cols = img.shape[:2]
draw = img.copy()
pts_cnt = 0
pts = np.zeros((4,2), dtype=np.float32)
def onMouse(event, x, y, flags, param): #마우스 이벤트 콜백 함수 구현 ---①
global pts_cnt # 마우스로 찍은 좌표의 갯수 저장
if event == cv2.EVENT_LBUTTONDOWN:
cv2.circle(draw, (x,y), 10, (0,255,0), -1) # 좌표에 초록색 동그라미 표시
cv2.imshow(win_name, draw)
plt.title('original')
plt.subplot(1,2,1)
plt.imshow(draw[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
pts[pts_cnt] = [x,y] # 마우스 좌표 저장
pts_cnt+=1
if pts_cnt == 4: # 좌표가 4개 수집됨
# 좌표 4개 중 상하좌우 찾기 ---②
sm = pts.sum(axis=1) # 4쌍의 좌표 각각 x+y 계산
diff = np.diff(pts, axis = 1) # 4쌍의 좌표 각각 x-y 계산
topLeft = pts[np.argmin(sm)] # x+y가 가장 값이 좌상단 좌표
bottomRight = pts[np.argmax(sm)] # x+y가 가장 큰 값이 좌상단 좌표
topRight = pts[np.argmin(diff)] # x-y가 가장 작은 것이 우상단 좌표
bottomLeft = pts[np.argmax(diff)] # x-y가 가장 큰 값이 좌하단 좌표
# 변환 전 4개 좌표
pts1 = np.float32([topLeft, topRight, bottomRight , bottomLeft])
# 변환 후 영상에 사용할 서류의 폭과 높이 계산 ---③
w1 = abs(bottomRight[0] - bottomLeft[0]) # 상단 좌우 좌표간의 거리
w2 = abs(topRight[0] - topLeft[0]) # 하당 좌우 좌표간의 거리
h1 = abs(topRight[1] - bottomRight[1]) # 우측 상하 좌표간의 거리
h2 = abs(topLeft[1] - bottomLeft[1]) # 좌측 상하 좌표간의 거리
width = max([w1, w2]) # 두 좌우 거리간의 최대값이 서류의 폭
height = max([h1, h2]) # 두 상하 거리간의 최대값이 서류의 높이
# 변환 후 4개 좌표
pts2 = np.float32([[0,0], [width-1,0],
[width-1,height-1], [0,height-1]])
# 변환 행렬 계산
mtrx = cv2.getPerspectiveTransform(pts1, pts2)
# 원근 변환 적용
result = cv2.warpPerspective(img, mtrx, (int(width), int(height))) # 책의 예시와 달리 int로 수정
cv2.imshow('scanned', result)
plt.subplot(1,2,2)
plt.imshow(result[:,:,(2,1,0)])
plt.title('scanned')
plt.xticks([]),plt.yticks([])
plt.show()
# cv2.namedWindow(win_name, cv2.WINDOW_NORMAL)
cv2.imshow(win_name, img)
# cv2.resizeWindow(win_name, 500, 500) # 창 크기 변경
cv2.setMouseCallback(win_name, onMouse) # 마우스 콜백 함수를 GUI 윈도우에 등록 ---④
cv2.waitKey(0)
cv2.destroyAllWindows()
5.2.3. 삼각형 어핀변환¶
어떤 영역을 여러 개의 삼각형으로 나누는 기법을 들로네 삼각분할(Delaunay triangulation)이라고 하는데, 이렇게 나눈 삼각형들을 흔히 영상 분야에서 입체적 표현이나 모핑 기술에 사용합니다.
모핑(morphing)기술이란 하나의 물체가 다른 물체로 자연스럽게 변하게 하는 것으로 두 영상이 있을 때 한 영상의 삼각형들의 크기와 모양이 나머지 영상에 대응하는 삼각형과 같아질 때까지 조금씩 바꿔서 전체적으로 하나의 영상이 다른 영상으로 자연스레 변하게 만드는 것을 의미합니다.
삼각형 모양의 변환을 하려면 다음의 과정을 거쳐야 합니다.
- 변환 전 삼각형 좌표 3쌍을 정한다
- 변환 후 삼각형 좌표 3쌍을 정한다
- 과정 1의 삼각형 좌표를 완전히 감싸는 외접 사각형 좌표를 구한다
- 과정 3의 사각형 영역을 관심영역으로 지정한다
- 과정 4의 관심영역을 대상으로 과정 1과 과정 2의 좌표로 변환행렬을 구하여 어핀 변환한다
- 과정 5의 변환된 관심영역에서 과정 2의 삼각형 좌표만 마스킹한다
- 과정 6의 마스크를 이용해서 원본 또는 다른 영상에 합성한다
위의 과정 3에서 삼각형 좌표를 완전히 감싸는 사각형의 좌표를 구할 때는 다음의 함수를 이용합니다.
- x, y, w, h = cv2.boundingRect(pts)
- pts : 다각형 좌표
- x, y, w, h = 외접 사각형의 좌표와 폭의 높이
과정 6에서 삼각형 마스크를 생성하기 위해서는 다음의 함수를 이용합니다.
- cv2.fillConvexPoly(img, points, color, [, lineType])
- img : 입력 영상
- points : 다각형 꼭짓점 좌표
- color : 채우기에 사용할 색상
- lineType : 선 그리기 알고리즘 선택 플래그
img = cv2.imread("../img/yungyung.jpg")
img2 = img.copy()
draw = img.copy()
# 변환 전,후 삼각형 좌표 ---①
pts1 = np.float32([[188,14], [85,202], [294,216]])
pts2 = np.float32([[128,40], [85,307], [306,167]])
# 각 삼각형을 완전히 감싸는 사각형 좌표 구하기 ---②
x1,y1,w1,h1 = cv2.boundingRect(pts1)
x2,y2,w2,h2 = cv2.boundingRect(pts2)
# 사각형을 이용한 관심영역 설정 ---③
roi1 = img[y1:y1+h1, x1:x1+w1]
roi2 = img2[y2:y2+h2, x2:x2+w2]
# 관심영역을 기준으로 좌표 계산 ---④
offset1 = np.zeros((3,2), dtype=np.float32)
offset2 = np.zeros((3,2), dtype=np.float32)
for i in range(3):
offset1[i][0], offset1[i][1] = pts1[i][0]-x1, pts1[i][1]-y1
offset2[i][0], offset2[i][1] = pts2[i][0]-x2, pts2[i][1]-y2
# 관심 영역을 주어진 삼각형 좌표로 어핀 변환 ---⑤
mtrx = cv2.getAffineTransform(offset1, offset2)
warped = cv2.warpAffine( roi1, mtrx, (w2, h2), None, \
cv2.INTER_LINEAR, cv2.BORDER_REFLECT_101)
# 어핀 변환 후 삼각형만 골라 내기 위한 마스크 생성 ---⑥
mask = np.zeros((h2, w2), dtype = np.uint8)
cv2.fillConvexPoly(mask, np.int32(offset2), (255))
# 삼각형 영역만 마스킹해서 합성 ---⑦
warped_masked = cv2.bitwise_and(warped, warped, mask=mask)
roi2_masked = cv2.bitwise_and(roi2, roi2, mask=cv2.bitwise_not(mask))
roi2_masked = roi2_masked + warped_masked
img2[y2:y2+h2, x2:x2+w2] = roi2_masked
# 관심 영역과 삼각형에 선 그려서 출력 ---⑧
cv2.rectangle(draw, (x1, y1), (x1+w1, y1+h1), (0,255,0), 1)
cv2.polylines(draw, [pts1.astype(np.int32)], True, (255,0,0), 1)
cv2.rectangle(img2, (x2, y2), (x2+w2, y2+h2), (0,255,0), 1)
imgs = {'original':draw, 'warped triangle':img2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.3. 렌즈 왜곡¶
지금까지의 변환은 행렬식으로 표현하였지만, 행렬식으로 표현할 수 없는 모양의 변환이 필요할 때도 있습니다. 대표적인 예시로는 투명한 물잔을 통해 비친 장면, 물결에 반사된 모습 등이 있습니다.
5.3.1. 리매핑¶
규칙성 없이 마음대로 모양을 변환해 주는 함수로 cv2.remap()을 제공합니다. 이 함수는 기존 픽셀의 위치를 원하는 위치로 재배치합니다.
- dst = cv2.remap(src, mapx, mapy, interpolation[, dst, borderMode, borderValue])
- src : 입력 영상
- mpax, mapy : x축과 y축으로 이동할 좌표(인덱스), src와 동일한 크기
- 나머지 인자는 cv2.warpAffine()과 동일
- dst : 결과 영상
이 때 mapx와 mapy는 초기 값으로 영상의 원래 좌표 값을 가지고 있는 것이 좋은데, 그 이유는 전체 픽셀 중에 옮기고 싶은 몇몇 픽셀에 대해서만 새로운 좌표를 지정하거나 원래 있던 위치에서 얼마만큼 움직이라고 하는 것이 코딩하기 편하기 때문입니다. 새로 옯겨갈 좌표를 세팅하는 작업은 브로드캐스팅 연산을 쓰는 것이 효율적이지만, 경우에 따라 직접 좌표값을 지정해야 할 때는 NumPy 함수를 쓰는 것이 속도가 더 빠릅니다.
import cv2
import numpy as np
import time
img = cv2.imread('../img/girl.jpg')
rows, cols = img.shape[:2]
# 뒤집기 변환 행렬로 구현 ---①
st = time.time()
mflip = np.float32([ [-1, 0, cols-1],[0, -1, rows-1]]) # 변환 행렬 생성
fliped1 = cv2.warpAffine(img, mflip, (cols, rows)) # 변환 적용
print('matrix:', time.time()-st)
# remap 함수로 뒤집기 구현 ---②
st2 = time.time()
mapy, mapx = np.indices((rows, cols),dtype=np.float32) # 매핑 배열 초기화 생성
mapx = cols - mapx -1 # x축 좌표 뒤집기 연산
mapy = rows - mapy -1 # y축 좌표 뒤집기 연산
fliped2 = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR) # remap 적용
print('remap:', time.time()-st2)
# 결과 출력 ---③
plt.figure(figsize = (10,6))
imgs = {'original':img, 'fliped1':fliped1, 'fliped2':fliped2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
matrix: 0.0009951591491699219 remap: 0.0039865970611572266
변환행렬을 이용해서 변환할 수 있는 영상을 remap 함수를 이용해 변환하는 것은 수행 속도에서 많이 차이가 나기 때문에 remap 함수는 변환행렬로 표현할 수 없는 비선형 변환에만 사용하는 것이 좋습니다.
# 삼각함수를 이용한 비선형 리매핑
l = 20 # 파장(wave length)
amp = 15 # 진폭(amplitude)
img = cv2.imread('../img/yungyung.jpg')
rows, cols = img.shape[:2]
# 초기 매핑 배열 생성 ---①
mapy, mapx = np.indices((rows, cols),dtype=np.float32)
# sin, cos 함수를 적용한 변형 매핑 연산 ---②
sinx = mapx + amp * np.sin(mapy/l)
cosy = mapy + amp * np.cos(mapx/l)
# 영상 매핑 ---③
img_sinx=cv2.remap(img, sinx, mapy, cv2.INTER_LINEAR) # x축만 sin 곡선 적용
img_cosy=cv2.remap(img, mapx, cosy, cv2.INTER_LINEAR) # y축만 cos 곡선 적용
# x,y 축 모두 sin, cos 곡선 적용 및 외곽 영역 보정
img_both=cv2.remap(img, sinx, cosy, cv2.INTER_LINEAR, \
None, cv2.BORDER_REPLICATE)
# 결과 출력
plt.figure(figsize=(6,6))
imgs = {'origin':img, 'sin x':img_sinx, 'cos y':img_cosy, 'sin cos':img_both}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(2,2,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.3.2. 오목 렌즈와 볼록 렌즈 왜곡¶
# 극 좌표계 ,직교 좌표계로 왔다갔다하면서 진행합니다.
img = cv2.imread('../img/yungyung.jpg')
print(img.shape)
rows, cols = img.shape[:2]
# ---① 설정 값 셋팅
exp = 2 # 볼록, 오목 지수 (오목 : 0.1 ~ 1, 볼록 : 1.1~)
exp2 = 0.5
scale = 1 # 변환 영역 크기 (0 ~ 1)
# 매핑 배열 생성 ---②
mapy, mapx = np.indices((rows, cols),dtype=np.float32)
# 좌상단 기준좌표에서 -1~1로 정규화된 중심점 기준 좌표로 변경 ---③
mapx = 2*mapx/(cols-1)-1 # 0~1 사이의 값을 2배 -> 0~2 사이의 값 -> -1 ~ 1
mapy = 2*mapy/(rows-1)-1
# 직교좌표를 극 좌표로 변환 ---④
r, theta = cv2.cartToPolar(mapx, mapy)
r2, theta2 = cv2.cartToPolar(mapx, mapy)
# 왜곡 영역만 중심확대/축소 지수 적용 ---⑤
r[r< scale] = r[r<scale] **exp
r2[r2< scale] = r2[r2<scale] **exp2
# 극 좌표를 직교좌표로 변환 ---⑥
mapx, mapy = cv2.polarToCart(r, theta)
mapx2, mapy2 = cv2.polarToCart(r2, theta2)
# 중심점 기준에서 좌상단 기준으로 변경 ---⑦
mapx = ((mapx + 1)*cols-1)/2
mapy = ((mapy + 1)*rows-1)/2
mapx2 = ((mapx2 + 1)*cols-1)/2
mapy2 = ((mapy2 + 1)*rows-1)/2
# 재매핑 변환
distorted = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)
distorted2 = cv2.remap(img,mapx2,mapy2,cv2.INTER_LINEAR)
plt.figure(figsize = (10,6))
imgs = {'original':img, 'distorted':distorted, 'distorted2':distorted2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
(449, 440, 3)
5.3.3. 방사 왜곡¶
대부분의 영상은 카메라로 촬영하여 얻는데, 카메라 렌즈는 동그랗고 영상은 사각형이다 보니 렌즈 가장 자리 영상에는 왜곡이 생기기 마련이고 이러한 왜곡을 배럴 왜곡(Barrel distortion)이라고 합니다. 배럴 왜곡을 할 수 있는 수학식은 다음과 같습니다.
$r_d = r_u(1+k_1r_u^2+k_2r_u^4+k_3r_u^6)$
- $r_d$ : 왜곡 변형 후
- $r_u$ : 왜곡 변형 전
- $k_1, k_2, k_3$ : 왜곡 계수
배럴 왜곡은 왜곡 계수의 값에 따라 밖으로 튀어 나오는 배럴 왜곡이 나타나기도 하고, 안으로 들어 가는 핀쿠션 왜곡이 일어나기도 합니다.
import cv2
import numpy as np
# 왜곡 계수 설정 ---①
k1, k2, k3 = 0.5, 0.2, 0.0 # 배럴 왜곡
kk1, kk2, kk3 = -0.3, 0, 0 # 핀큐션 왜곡
img = cv2.imread('../img/girl.jpg')
rows, cols = img.shape[:2]
# 매핑 배열 생성 ---②
mapy, mapx = np.indices((rows, cols),dtype=np.float32)
# 중앙점 좌표로 -1~1 정규화 및 극좌표 변환 ---③
mapx = 2*mapx/(cols-1)-1
mapy = 2*mapy/(rows-1)-1
r, theta = cv2.cartToPolar(mapx, mapy)
# 방사 왜곡 변영 연산 ---④
ru = r*(1+k1*(r**2) + k2*(r**4) + k3*(r**6))
ru2 = r*(1+kk1*(r**2) + kk2*(r**4) + kk3*(r**6))
# 직교좌표 및 좌상단 기준으로 복원 ---⑤
mapx, mapy = cv2.polarToCart(ru, theta)
mapx2, mapy2 = cv2.polarToCart(ru2, theta)
mapx = ((mapx + 1)*cols-1)/2
mapy = ((mapy + 1)*rows-1)/2
mapx2 = ((mapx2 + 1)*cols-1)/2
mapy2 = ((mapy2 + 1)*rows-1)/2
# 리매핑 ---⑥
distored = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)
distored2 = cv2.remap(img,mapx2,mapy2,cv2.INTER_LINEAR)
plt.figure(figsize = (10,6))
imgs = {'original':img, 'distored':distored, 'distored2':distored2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
OpenCV에서 배럴 왜곡 현상이 일어나는 렌즈 왜곡을 제거할 목적으로 제공하는 함수
- dst = cv2.undistort(src, cameraMtrix, distCoeffs)
- src : 입력 원본 영상
- cameraMatrix : 카메라 매트릭스
- distCoeffs : 왜곡 계수, 최소 4개 or 5, 8, 12, 14
여기서 cameraMatrix는 촬영할 카메라의 내부 파라미터인 중심점과 초점 거리를 입력하며 이 값들은 카메라가 생산될 때 가지는 고유의 특성이므로 개발자가 단순히 값을 입력하거나 계산할 수는 없습니다.
# 방사 왜곡 현상 구현
img = np.full((300,400,3), 255, np.uint8)
img[::10, :, :] = 0
img[:, ::10, :] = 0
width = img.shape[1]
height = img.shape[0]
# 왜곡 계수 설정 ---②
k1, k2, p1, p2 = 0.001, 0, 0, 0 # 배럴 왜곡
kk1, kk2, pp1, pp2 = -0.0005, 0, 0, 0 # 핀쿠션 왜곡
distCoeff = np.float64([k1, k2, p1, p2])
distCoeff2 = np.float64([kk1, kk2, pp1, pp2])
# 임의의 값으로 카메라 매트릭스 설정 ---③
fx, fy = 10, 10
cx, cy = width/2, height/2
camMtx = np.float32([[fx,0, cx],
[0, fy, cy],
[0 ,0 ,1]])
# 왜곡 변형 ---④
dst = cv2.undistort(img,camMtx,distCoeff)
dst2 = cv2.undistort(img,camMtx,distCoeff2)
plt.figure(figsize = (10,6))
imgs = {'original':img, 'dst':dst, 'dst2':dst2}
for i, (k, v) in enumerate(imgs.items()):
plt.subplot(1,3,i+1)
plt.title(k)
plt.imshow(v[:,:,(2,1,0)])
plt.xticks([]),plt.yticks([])
plt.show()
5.4. 실전 워크숍¶
5.4.1. 모자이크 처리 1¶
특정 영역을 작게 축소했다가 다시 확대하면 원래의 픽셀과 비슷하긴 하지만, 보간법에 의해서 연산한 결과기 때문에 선명도가 떨여져 뿌옇게 보입니다. 보간법 알고리즘으로는 cv2.INTER_AREA를 사용하면 저해상도 픽셀처럼 변환됩니다.
import cv2
rate = 15 # 모자이크에 사용할 축소 비율 (1/rate)
win_title = 'mosaic' # 창 제목
img = cv2.imread('../img/yungyung.jpg') # 이미지 읽기
plt.subplot(1,2,1)
plt.imshow(img[:,:,(2,1,0)])
plt.title('original')
plt.xticks([]),plt.yticks([])
while True:
x,y,w,h = cv2.selectROI(win_title, img, False) # 관심영역 선택
if w and h:
roi = img[y:y+h, x:x+w] # 관심영역 지정
roi = cv2.resize(roi, (w//rate, h//rate)) # 1/rate 비율로 축소
# 원래 크기로 확대
roi = cv2.resize(roi, (w,h), interpolation=cv2.INTER_AREA)
img[y:y+h, x:x+w] = roi # 원본 이미지에 적용
cv2.imshow(win_title, img)
else:
break
cv2.destroyAllWindows()
plt.subplot(1,2,2)
plt.imshow(img[:,:,(2,1,0)])
plt.title('mosaic')
plt.xticks([]),plt.yticks([])
plt.show()
5.4.2. 포토샵 리퀴파이 도구¶
일명 "뽀샵"이라고 부르는, 포토샵으로 사진의 원하는 부분만 작게하거나 크게하는 데에는 리퀴파이 도구를 사용합니다. Liquify는 '액체로 만들다'라는 뜻으로 영상 분야에서는 영상의 일부분을 흐물거리게 해서 모양을 바꾸는 효과를 말합니다.
마우스로 드래그를 해서 드래그가 끝나면 새로운 삼각형 좌표 4개를 얻고 새로운 좌표만큼 4개의 삼각형을 어핀 변환하는 방법을 사용합니다.
import cv2
import numpy as np
win_title = 'Liquify' # 창 이름
half = 50 # 관심 영역 절반 크기
isDragging = False # 드래그 여부 플래그
# 리퀴파이 함수
def liquify(img, cx1,cy1, cx2,cy2) :
# 대상 영역 좌표와 크기 설정
x, y, w, h = cx1-half, cy1-half, half*2, half*2
# 관심 영역 설정
roi = img[y:y+h, x:x+w].copy()
out = roi.copy()
# 관심영역 기준으로 좌표 재 설정
offset_cx1,offset_cy1 = cx1-x, cy1-y
offset_cx2,offset_cy2 = cx2-x, cy2-y
# 변환 이전 4개의 삼각형 좌표
tri1 = [[ (0,0), (w, 0), (offset_cx1, offset_cy1)], # 상,top
[ [0,0], [0, h], [offset_cx1, offset_cy1]], # 좌,left
[ [w, 0], [offset_cx1, offset_cy1], [w, h]], # 우, right
[ [0, h], [offset_cx1, offset_cy1], [w, h]]] # 하, bottom
# 변환 이후 4개의 삼각형 좌표
tri2 = [[ [0,0], [w,0], [offset_cx2, offset_cy2]], # 상, top
[ [0,0], [0, h], [offset_cx2, offset_cy2]], # 좌, left
[ [w,0], [offset_cx2, offset_cy2], [w, h]], # 우, right
[ [0,h], [offset_cx2, offset_cy2], [w, h]]] # 하, bottom
for i in range(4):
# 각각의 삼각형 좌표에 대해 어핀 변환 적용
matrix = cv2.getAffineTransform( np.float32(tri1[i]), \
np.float32(tri2[i]))
warped = cv2.warpAffine( roi.copy(), matrix, (w, h), \
None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101)
# 삼각형 모양의 마스크 생성
mask = np.zeros((h, w), dtype = np.uint8)
cv2.fillConvexPoly(mask, np.int32(tri2[i]), (255,255,255))
# 마스킹 후 합성
warped = cv2.bitwise_and(warped, warped, mask=mask)
out = cv2.bitwise_and(out, out, mask=cv2.bitwise_not(mask))
out = out + warped
# 관심 영역을 원본 영상에 합성
img[y:y+h, x:x+w] = out
return img
# 마우스 이벤트 핸들 함수
def onMouse(event,x,y,flags,param):
global cx1, cy1, isDragging, img # 전역변수 참조
# 마우스 중심 점을 기준으로 대상 영역 따라다니기
if event == cv2.EVENT_MOUSEMOVE:
if not isDragging :
img_draw = img.copy()
# 드래그 영역 표시
cv2.rectangle(img_draw, (x-half, y-half), \
(x+half, y+half), (0,255,0))
cv2.imshow(win_title, img_draw) # 사각형 표시된 그림 화면 출력
elif event == cv2.EVENT_LBUTTONDOWN :
isDragging = True # 드래그 시작
cx1, cy1 = x, y # 드래그 시작된 원래의 위치 좌표 저장
elif event == cv2.EVENT_LBUTTONUP :
if isDragging:
isDragging = False # 드래그 끝
# 드래그 시작 좌표와 끝난 좌표로 리퀴파이 적용 함수 호출
liquify(img, cx1, cy1, x, y)
cv2.imshow(win_title, img)
if __name__ == '__main__' :
img = cv2.imread("../img_1/man_face.jpg")
h, w = img.shape[:2]
cv2.namedWindow(win_title)
cv2.setMouseCallback(win_title, onMouse)
cv2.imshow(win_title, img)
while True:
key = cv2.waitKey(1)
if key & 0xFF == 27:
break
cv2.destroyAllWindows()
이를 얼굴 인식과 들로네 삼각분할을 적용하면 얼굴 간의 모핑이나 스와핑 효과를 만들 수 있습니다.
5.4.3. 왜곡 거울 카메라¶
렌즈 왜곡을 사용해 왜곡 카메라를 구현합니다.
import cv2
import numpy as np
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
rows, cols = 240, 320
map_y, map_x = np.indices((rows, cols), dtype=np.float32)
# 거울 왜곡 효과
map_mirrorh_x,map_mirrorh_y = map_x.copy(), map_y.copy()
map_mirrorv_x,map_mirrorv_y = map_x.copy(), map_y.copy()
## 좌우 대칭 거울 좌표 연산
map_mirrorh_x[: , cols//2:] = cols - map_mirrorh_x[:, cols//2:]-1
## 상하 대칭 거울 좌표 연산
map_mirrorv_y[rows//2:, :] = rows - map_mirrorv_y[rows//2:, :]-1
# 물결 효과
map_wave_x, map_wave_y = map_x.copy(), map_y.copy()
map_wave_x = map_wave_x + 15*np.sin(map_y/20)
map_wave_y = map_wave_y + 15*np.sin(map_x/20)
# 렌즈 효과
## 렌즈 효과, 중심점 이동
map_lenz_x = 2*map_x/(cols-1)-1
map_lenz_y = 2*map_y/(rows-1)-1
## 렌즈 효과, 극좌표 변환
r, theta = cv2.cartToPolar(map_lenz_x, map_lenz_y)
r_convex = r.copy()
r_concave = r
## 볼록 렌즈 효과 매핑 좌표 연산
r_convex[r< 1] = r_convex[r<1] **2
print(r.shape, r_convex[r<1].shape)
## 오목 렌즈 효과 매핑 좌표 연산
r_concave[r< 1] = r_concave[r<1] **0.5
## 렌즈 효과, 직교 좌표 복원
map_convex_x, map_convex_y = cv2.polarToCart(r_convex, theta)
map_concave_x, map_concave_y = cv2.polarToCart(r_concave, theta)
## 렌즈 효과, 좌상단 좌표 복원
map_convex_x = ((map_convex_x + 1)*cols-1)/2
map_convex_y = ((map_convex_y + 1)*rows-1)/2
map_concave_x = ((map_concave_x + 1)*cols-1)/2
map_concave_y = ((map_concave_y + 1)*rows-1)/2
while True:
ret, frame = cap.read()
# 준비한 매핑 좌표로 영상 효과 적용
mirrorh=cv2.remap(frame,map_mirrorh_x,map_mirrorh_y,cv2.INTER_LINEAR)
mirrorv=cv2.remap(frame,map_mirrorv_x,map_mirrorv_y,cv2.INTER_LINEAR)
wave = cv2.remap(frame,map_wave_x,map_wave_y,cv2.INTER_LINEAR, \
None, cv2.BORDER_REPLICATE)
convex = cv2.remap(frame,map_convex_x,map_convex_y,cv2.INTER_LINEAR)
concave = cv2.remap(frame,map_concave_x,map_concave_y,cv2.INTER_LINEAR)
# 영상 합치기
r1 = np.hstack(( frame, mirrorh, mirrorv))
r2 = np.hstack(( wave, convex, concave))
merged = np.vstack((r1, r2))
cv2.imshow('distorted',merged)
if cv2.waitKey(1) & 0xFF== 27:
break
cap.release
cv2.destroyAllWindows()
(240, 320) (59868,)
- 지금까지 영상을 변환하는 여러가지 방법에 대하여 배워보았습니다.
- 카메라와 관련된 변환 이미지에 대하여 굉장히 실용성있게 사용할 수 있을 것 같습니다.
감사합니다 :)
'대외활동 > DACrew 2기' 카테고리의 다른 글
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 6장. 영상 필터 (1) | 2022.05.13 |
---|---|
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 4장. 이미지 프로세싱 기초 (1) | 2022.04.28 |
[ 파이썬으로 만드는 OpenCV 프로젝트🔥] 2장. 기본 입출력 (0) | 2022.04.22 |
댓글