리매핑(Remapping)
리매핑(Remapping)을 이용해 이미지를 직선이 아닌 곡선으로 표현할 수 있는데, 입력 이미지에 기하학적(Geometric) 변환을 적용하는 방법이라고 볼 수 있다. 이를 통해 좀 더 자유도 있는 변환을 수행할 수 있다. 리매핑은 이미지에 변환 행렬 연산을 적용하는 것이 아닌, 비선형 변환을 적용할 수 있다. 즉, 리매핑이란 규칙성 없이 마음대로 이미지의 모양을 변환하는 것을 말한다.
어파인 변환, 투시 변환을 포함한 다양한 변환을 리매핑으로 표현할 수 있다. 오른쪽 수식은 대칭 변환을 리매핑으로 표현한 것이고, 대칭 변환은 x좌표를 (가로크기 -1 -x)로 매핑하였고 y좌표는 그대로 가져온다. x가 0이면 w-1이 되어 가로 끝 좌표를 참조합니다. 따라서 좌우 대칭이 된다. 왼쪽은 이동 변환을 리매핑으로 표현한 것이다. 단, (x-200)은 -200 이동하는 것이 아니라 x방향으로 200만큼 이동한다는 의미이다.
OpenCV에서 remap()이라는 함수를 이용하여 리매핑을 수행할 수 있다.
cv2.remap(src, mapx, mapy, interpolation, dst, borderMode, borderValue)
- src: input image
- mapx, mapy: x축과 y축으로 이동할 좌표, src와 동일한 크기, dtype=float32
- interpolation: 보간법
- borderMode: 가장자리 픽셀 확장 방식. default = cv2.BORDER_CONSTANT
- borderValue: cv2.BORDER_CONSTANT일 때 사용할 상수 값. default=0
이를 이용하여 이미지 뒤집기(Flip), 파도 모양 왜곡, 오목렌즈와 볼록 렌즈 왜곡(Distortion)을 수행할 수 있다.
Flip
변환 행렬을 이용하여 뒤집은 이미지와 cv2.remap() 함수로 리매핑하여 뒤집은 이미지의 결과는 똑같지만 cv2.remap() 함수로 변환하는 것은 변환 행렬로 변환하는 것보다 수행 속도가 더 느리다. 따라서 변환 행렬로 표현할 수 있는 것은 변환행렬로 변환을 하는 것이 좋다고 한다. 단, 변환행렬로 표현할 수 없는 비선형 변환에만 cv2.remap() 함수를 사용하는 것이 더 효율적이다.
import cv2
import numpy as np
def flip(img):
rows, cols = img.shape[:2]
# 뒤집기 변환 행렬
mflip = np.float32([[-1, 0, cols - 1], [0, -1, rows - 1]]) # 변환 행렬 생성
fliped1 = cv2.warpAffine(img, mflip, (cols, rows)) # 변환 적용
# remap 함수로 뒤집기
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 적용
return fliped1, fliped2
img = cv2.imread('brokenEgg.jpeg')
fliped1, fliped2 = flip(img)
result = np.hstack((img, fliped1, fliped2))
cv2.imshow('result', result)
cv2.imwrite('result.png', result)
cv2.waitKey()
Waveform Distortion
이번엔 삼각함수를 이용한 비선형(Nonlinear) 리매핑을 수행한다. 이 또한 변환 행렬로 표현할 수 없는 비선형 변환을 수행한다. 마땅한 이름이 생각이 안나 삼각함수 파형 모양인 Waveform이라고 표기하였다.
import cv2
import numpy as np
def waveDistortion(img, amp=20, waveFreq=32):
h, w = img.shape[:2] # 입력 영상의 높이와 넓이 정보 추출
# np.indice는 행렬의 인덱스값 x좌표값 y좌표값을 따로따로 행렬의 형태로 변환해줌
mapy, mapx = np.indices((h, w), dtype=np.float32)
# borderMode는 근방의 색깔로 대칭되게 해서 채워줌, 기본값은 빈 공간을 검은색으로 표현
# sin, cos 함수를 적용한 변형 매핑 연산
sinx = mapx + amp * np.sin(mapy / waveFreq)
cosy = mapy + amp * np.cos(mapx / waveFreq)
img_x = cv2.remap(img, sinx, mapy, cv2.INTER_LINEAR) # x축만 sin 곡선 적용
img_y = cv2.remap(img, mapx, cosy, cv2.INTER_LINEAR) # y축만 cos 곡선 적용
img_both = cv2.remap(img, sinx, cosy, cv2.INTER_LINEAR, borderMode=cv2.BORDER_DEFAULT)
return img_x, img_y, img_both
img = cv2.imread('brokenEgg.jpeg')
img_x, img_y, img_both = waveDistortion(img)
result = np.hstack((img, img_x, img_y, img_both))
cv2.imshow('result', result)
cv2.imwrite('result.png', result)
cv2.waitKey()
만약 x축만 곡선을 적용하면 x축을 기준으로 흔들릴 것이고, y축에만 적용하면 y축 기준으로만 흔들릴 것이다. 위 예제는 BORDER_DEFAULT를 이용해 파라미터로 외곽 보정을 수행하여 외곽에 사라졌어야 할 영역을 보정하였다.
Lens Distortion(볼록 렌즈/오목 렌즈 왜곡)
해당 변환을 위해 직교 좌표계와 극 좌표계에 대해 기본적인 이해가 필요하다.
직교좌표(cartesian coodinate)는 우리가 잘 알고 있는 x, y축으로 이루어진 데카르트가 생각한 좌표계이다. 이를 원점에서 떨어진 거리 r과 동경이 x축 양의 방향과 이루는 각 θ로 바꾼 좌표계가 극좌표(polar coodinate)이다. 그림과 같이 두 좌표계 사이에 일대일 대응이 있다. 직교좌표에서는 복잡한 방정식으로 나타나는 도형이 극좌표로 나타내면 매우 간단한 방정식으로 표현되기도 한다. 예를 들어 면 반지름이 1인 원 위에 있는 점은 각에 관계없이 거리가 항상 1이므로 \( r(\theta)=1\)로 나타낼 수 있다. OpenCV에서는 두 좌표계를 쉽게 변환할 수 있는 함수를 제공한다.
cv2.cartToPolar(x, y): 직교 좌표 → 극좌표 변환
cv2.polarToCart(r, theta): 극좌표 → 직교 좌표 변환
좌표의 변환뿐만 아니라 좌표의 기준점 변환도 중요하다. 일반적으로 직교 좌표계를 사용할 때는 좌측 상단을 원점(0, 0)으로 정하지만, 극좌표에서는 이미지의 중앙을 원점으로 해야 한다. 이미지의 중앙을 (0, 0)으로 두기 위해서 좌표의 값을 -1 ~ 1로 정규화해야 한다.
import cv2
import numpy as np
def LensDistortionImage(img, exp=2, scale=1):
'''
:param img: image
:param exp: 오목, 볼록 지수 (오목 : 0.1 ~ 1, 볼록 : 1.1~) => 1보다 작으면 오목 렌즈 효과를 내고, 1보다 크면 볼록 렌즈 효과
:param scale: 변환 영역 크기 (0 ~ 1)
'''
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)
# 왜곡 영역만 중심확대/축소 지수 적용
r[r < scale] = r[r < scale] ** exp
# 극 좌표를 직교좌표로 변환
mapx, mapy = cv2.polarToCart(r, theta)
# 중심점 기준에서 좌상단 기준으로 변경
mapx = ((mapx + 1) * cols - 1) / 2
mapy = ((mapy + 1) * rows - 1) / 2
# 재매핑 변환
result = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
return result
img = cv2.imread('brokenEgg.jpeg')
concave = LensDistortionImage(img, exp=0.5)
convex = LensDistortionImage(img, exp=2)
result = np.hstack((img, concave, convex))
cv2.imshow('result', result)
cv2.imwrite('result.png', result)
cv2.waitKey()
아래는 전체 python 소스 코드이다.
import cv2
import numpy as np
def flip(img):
rows, cols = img.shape[:2]
# 뒤집기 변환 행렬
mflip = np.float32([[-1, 0, cols - 1], [0, -1, rows - 1]]) # 변환 행렬 생성
fliped1 = cv2.warpAffine(img, mflip, (cols, rows)) # 변환 적용
# remap 함수로 뒤집기
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 적용
return fliped1, fliped2
def waveDistortion(img, amp=20, waveFreq=32):
h, w = img.shape[:2] # 입력 영상의 높이와 넓이 정보 추출
# np.indice는 행렬의 인덱스값 x좌표값 y좌표값을 따로따로 행렬의 형태로 변환해줌
mapy, mapx = np.indices((h, w), dtype=np.float32)
# borderMode는 근방의 색깔로 대칭되게 해서 채워줌, 기본값은 빈 공간을 검은색으로 표현
# sin, cos 함수를 적용한 변형 매핑 연산
sinx = mapx + amp * np.sin(mapy / waveFreq)
cosy = mapy + amp * np.cos(mapx / waveFreq)
img_x = cv2.remap(img, sinx, mapy, cv2.INTER_LINEAR) # x축만 sin 곡선 적용
img_y = cv2.remap(img, mapx, cosy, cv2.INTER_LINEAR) # y축만 cos 곡선 적용
img_both = cv2.remap(img, sinx, cosy, cv2.INTER_LINEAR, borderMode=cv2.BORDER_DEFAULT)
return img_x, img_y, img_both
def LensDistortionImage(img, exp=2, scale=1):
'''
:param img: image
:param exp: 오목, 볼록 지수 (오목 : 0.1 ~ 1, 볼록 : 1.1~) => 1보다 작으면 오목 렌즈 효과를 내고, 1보다 크면 볼록 렌즈 효과
:param scale: 변환 영역 크기 (0 ~ 1)
'''
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)
# 왜곡 영역만 중심확대/축소 지수 적용
r[r < scale] = r[r < scale] ** exp
# 극 좌표를 직교좌표로 변환
mapx, mapy = cv2.polarToCart(r, theta)
# 중심점 기준에서 좌상단 기준으로 변경
mapx = ((mapx + 1) * cols - 1) / 2
mapy = ((mapy + 1) * rows - 1) / 2
# 재매핑 변환
result = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
return result
img = cv2.imread('brokenEgg.jpeg')
concave = LensDistortionImage(img, exp=0.5)
convex = LensDistortionImage(img, exp=2)
result = np.hstack((img, concave, convex))
cv2.imshow('result', result)
cv2.imwrite('result.png', result)
cv2.waitKey()
관련 포스트
2022.05.03 - [Data Science/CV (Computer Vision)] - [OpenCV] [python] 이미지 Flip
참고 자료
https://076923.github.io/posts/Python-opencv-40/
https://deep-learning-study.tistory.com/201
소스 코드
https://github.com/sehoon787/Personal_myBlog/blob/main/Data%20Science/CV/Remapping.py