주요 개념
- 모아레(Moire)
- 퓨리에 변환(Fast Fourier Transform, FFT)
- LPF(Low Pass Filter)
모아레(Moire) 현상은 선이나 점이 반복되는 물체(보통 의류)를 촬영할 때 생기는 현상을 말한다.
모아레 현상은 ‘맥놀이 현상’에 빗대어 설명하기도 하는데 이는 빛 또한 소리와 같이 파동의 성질을 갖고 있기 때문이다. 맥놀이 현상이란 진동수가 비슷한 두 개의 파동이 서로 영향을 미쳐 진동수의 폭이 일정한 주기로 변하는 현상이다.
예를 들어 큰 종을 치면 처음에는 큰 소리가 나지만 시간이 지날수록 소리가 커지고 작아지기를 반복하는 현상이 있다. 범종은 두꺼워 종을 칠 때 2개 이상의 진동수를 가진 소리가 만들어지는데, 이 소리들은 서로 간섭한다. 위상이 같을 때는 소리가 더 커지고, 위상이 반대일 때는 소리가 줄어든다. 이러한 현상은 진동수는 비슷하지만 일치하지 않을 때 나타나는 현상이다.
앞서 언급한 것처럼 빛 역시 파동의 성질을 갖고 있어 소리와 같이 간섭 현상이 일어난다. 즉, 비슷한 파장의 빛이 겹치면 위상이 같은 방향일 때는 더 커지고, 위상이 반대 방향일 때는 줄어든다. 물결무늬가 생기기도 하고, 또 무지갯빛이 나타나기도 한다. 심지어 반복 패턴이 아주 짧으면 단 1개의 무늬로도 모아레 무늬가 나타나기도 한다. 이는 우리 눈의 잔상 때문인데 우리가 한 곳을 응시해도 정확히 한 점을 계속 볼 수는 없고 매 순간 보는 방향과 위치가 아주 조금씩 달라지기 때문이다. 그러므로 먼저 봤던 상이 완전히 사라지지 않고 남아 있게 되고, 먼저 본 상(잔상)과 지금 보고 있는 상이 겹치면서 모아레 무늬를 보게 된다.
예를 들어 햇빛이 비칠 때 모기장이 겹쳐 있는 부위에 생긴 물결무늬를 볼 수 있는데 가까운 모기장을 상하좌우로 움직이거나 각도를 바꾸면 물결무늬 또한 움직인다. 모기장뿐만 아니라 일정한 간격을 갖는 무늬가 반복해 겹쳐지면 어디서든 이런 무늬가 생길 수 있다. 다른 예시로 스마트폰 카메라를 이용하여 모니터를 촬영하였을 때도 모아레 현상을 발견할 수 있다.
위 사진에서 모아레 현상이 생기는 이유는 반복되는 패턴의 크기가 너무 조밀해서 이미지 센서가 이를 구분 해낼 수 없기 때문이다. 결과적으로, 카메라는 위 사진과 같이 이상한 형태의 무늬가 들어간 이미지를 만들게 된다.
추가(2022.08.18)
써니님 댓글 내용 중 "모아레 현상은 카메라의 센서 픽셀 배열구조와 디스플레이 픽셀 배열구조 간의 공간주파수가 겹쳐서 생기는 현상으로 노출시간과 관계없다."고 추가 설명해주셨습니다. (라이블리 연동 해제 후 댓글이 지워졌다.)
모아레 현상은 LPF(Low Pass Filter)를 이용해 어느정도 지울 수 있다. 퓨리에 변환(Fast Fourier Transform, FFT)을 이용해 사진이 가지고 있는 주파수를 파악하고 LPF를 이용해 특정 고주파 영역을 날려주면 된다. 영상 처리에서는 2차원 푸리에 변환을 사용한다. 이는 영상을 x축 또는 y축 방향으로 따라가면서 픽셀의 밝기 변화를 파형 또는 신호로 보고 주파수 분석을 적용한다. 푸리에 변환을 통해 얻은 각 주파수 성분의 강도를 스펙트럼(spectrum)이라고 부르는데 스펙트럼도 이미지처럼 표현이 가능하다.
퓨리에 변환을 모두 다루기에는 내용이 너무 많아 간략하게 아래 python 소스 코드를 구현한 것으로 대체한다.
import cv2
import numpy as np
from matplotlib import pyplot as plt
ori = cv2.imread('brokenEgg.jpeg', cv2.IMREAD_GRAYSCALE)
img = cv2.imread('moireEgg.jpeg', cv2.IMREAD_GRAYSCALE)
f_ori = np.fft.fft2(ori)
shift_ori = np.fft.fftshift(f_ori)
magnitude_spectrum_ori = 20*np.log(np.abs(shift_ori))
f = np.fft.fft2(img)
shift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(shift))
fig = plt.figure(figsize=(12, 8))
plt.subplot(221), plt.imshow(ori, cmap='gray')
plt.title('Original', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(222), plt.imshow(magnitude_spectrum_ori, cmap='gray')
plt.title('Original Magnitude Spectrum', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(223), plt.imshow(img, cmap='gray')
plt.title('Moire', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(224), plt.imshow(magnitude_spectrum, cmap='gray')
plt.title('Moire Magnitude Spectrum', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.savefig('result', dpi=300, bbox_inches='tight')
plt.show()
13번 째 라인의 shift를 통해 원점을 왼쪽 상단에서 중앙 부분으로 이동시켰다. 이를 통해 가운데로 갈수록 저주파이고 가장자리로 향할수록 고주파가 되도록 설정할 수 있다.
위 코드를 통해 아래의 퓨리에 변환을 통해 스펙트럼을 표현한 결과를 볼 수 있다. 원본 사진과 모아레 현상이 생긴 사진의 스펙트럼을 비교하면 확연하게 노이즈가 생겨있는 것을 볼 수 있다. 사진 사이즈는 따로 맞추지 않아 대략적인 스펙트럼만 주목하면 된다.
모아레 제거를 할 때 스펙트럼 영상의 중심에 있는 밝은 부분은 건드리면 안 된다. 이 부분은 영상의 전체적인 밝기 및 원본 영상의 주요 패턴을 나타내는 부분이기 때문이다. 따라서 모아레 패턴은 비교적 고주파 반복 패턴이기 때문에 아래와 같이 테두리 부근에서 밝게 빛나는 부분들을 제거해 주면 된다.
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('moireEgg.jpeg', cv2.IMREAD_GRAYSCALE)
f = np.fft.fft2(img)
shift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(shift))
magnitude_spectrum[600:800, :420] = 0
magnitude_spectrum[600:800, 680:] = 0
magnitude_spectrum[:550, 465:570] = 0
magnitude_spectrum[940:, 465:570] = 0
plt.subplot(111), plt.imshow(magnitude_spectrum, cmap='gray')
plt.savefig('result2', dpi=300, bbox_inches='tight')
위와 같이 고주파 부분을 날려주면 모아레 현상을 어느정도 제거할 수 있다. 일단 임의로 특정 고주파를 강제로 제거했다. 원하는 주파수 대역을 제거한 후 푸리에 역변환을 하면 아래 그림과 같이 특정 주파수 패턴이 제거된 결과를 얻을 수 있다. 아래 python 소스 코드에서 ifft를 수행하여 모아레 현상이 제거되었는지 확인해본다.
import cv2
import copy
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('moireEgg.jpeg', cv2.IMREAD_GRAYSCALE)
f = np.fft.fft2(img)
shift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(shift))
magnitude_spectrum_moire = copy.deepcopy(magnitude_spectrum)
magnitude_spectrum[600:800, :420] = 0
magnitude_spectrum[600:800, 680:] = 0
magnitude_spectrum[:550, 465:570] = 0
magnitude_spectrum[940:, 465:570] = 0
shift[600:800, :420] = 0
shift[600:800, 680:] = 0
shift[:550, 465:570] = 0
shift[940:, 465:570] = 0
f_ishift = np.fft.ifftshift(shift)
img_back = np.fft.ifft2(f_ishift)
img_back = np.abs(img_back)
plt.subplot(221), plt.imshow(img, cmap='gray')
plt.title('Moire', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(222), plt.imshow(magnitude_spectrum_moire, cmap='gray')
plt.title('Moire Magnitude Spectrum', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(223), plt.imshow(img_back, cmap='gray')
plt.title('Self LPF', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.subplot(224), plt.imshow(magnitude_spectrum, cmap='gray')
plt.title('LPF Magnitude Spectrum', fontsize=14, fontweight='bold'), plt.xticks([]), plt.yticks([])
plt.savefig('result', dpi=300, bbox_inches='tight')
plt.show()
잘 보면 어느정도 제거된 것을 확인할 수 있다.
참고 자료
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jkhan012&logNo=220397863065
https://blog.daum.net/usstory/5264466
https://darkpgmr.tistory.com/14