Python과 OpenCV – 25 : Fourier Transform

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_transforms/py_fourier_transform/py_fourier_transform.html#fourier-transform 입니다.

퓨리에 변환이란 어떤 입력 신호를 다수의 sin과 cos 함수의 합으로 변환한 것을 의미합니다. sin과 cos 함수의 합이므로 그래프를 그려보면 어떤 반복적인 주기를 가지고 있을 것입니다. 이러한 퓨리에 변환을 이미지에 적용할 수 있는데, 이미지에 적용한 퓨리에 변환의 결과를 얻는 것을 2D DFT(Discrete Fourier Transform)이라고 합니다. 또한 이렇게 얻은 퓨리에 변환 결과를 이용해 다시 역으로 이미지를 얻는 것을 역 퓨리에 변환이라고 합니다. 퓨리에 변환은 Numpy와 OpenCV 모두에서 함수로 제공합니다. 먼저 Numpy에서 제공하는 퓨리에 변환에 대해서 살펴보면..

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg',0)
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)
magnitude_spectrum = 20*np.log(np.abs(fshift))

plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

실행 결과는 다음과 같습니다.

왼쪽은 입력 이미지이고 오른쪽은 이미지에 대한 푸리에 변환의 결과를 시각화한 것입니다. 6번 코드의 np.fft.fft2 함수가 이미지에 대한 푸리에 변환을 실행하는 것이고, 7번 코드의 np.fft.fftshift 함수는 푸리에 변환의 결과를 중심으로 이동시킵니다. 푸리에 변환은 반복적인 주기를 갖으므로 이처럼 그 결과를 중심으로 이동시키는 것이 시각화 관점에서 좋습니다. 그리고 8번 코드는 푸리에 변환의 결과의 값을 증폭시킵니다. 워낙 푸리에 변환의 값이 작기때문에 이를 시각화하면 두드러진 특징을 살펴볼 수 없어 값을 증폭시킵니다.

이제 푸리에 변환의 결과를 변경해서, 변경된 푸리에 변환을 통해 이미지로 역 변환시키는 코드를 살펴보겠습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg',0)
f = np.fft.fft2(img)
fshift = np.fft.fftshift(f)

rows,cols = img.shape
crow,ccol = (int)(rows/2),(int)(cols/2)
fshift[crow-30:crow+30, ccol-30:ccol+30] = 0
f_ishift = np.fft.ifftshift(fshift)
img_back = np.fft.ifft2(f_ishift)
img_back = np.abs(img_back)

plt.subplot(131),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(132),plt.imshow(img_back, cmap = 'gray')
plt.title('Image after HPF'), plt.xticks([]), plt.yticks([])
plt.subplot(133),plt.imshow(img_back)
plt.title('Result in JET'), plt.xticks([]), plt.yticks([])

plt.show()

푸리에 변환 결과에 어떤 변경을 가했는지 언급하면, 11번 코드에서 푸리에 변환 결과 이미지의 중심을 기준으로 좌우로 30픽셀 범위 영역의 값을 0으로 변경하고 있습니다. 이렇게 변경된 푸리에 변환 결과를 통해 역 변환시키는 코드가 12번과 13번 코드입니다. 결과는 다음과 같습니다.

이미지의 Edge를 추출한 결과가 도출되었습니다. 다음으로 OpenCV를 통한 푸리에 변환에 대해 살펴보겠습니다.

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg',0)

dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

magnitude_spectrum = 20*np.log(cv2.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))

plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(magnitude_spectrum, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

실행 결과는 아래와 같고 Numpy를 통한 푸리에 변환과 동일합니다.

이제 여기서 푸리에 변환 결과에 어떤 변경을 가하고 난뒤 푸리에 역변환을 통해 이미지를 얻어 보겠습니다. 이번 변경은 앞서서 가운데 영역을 0으로 설정했는데, 이번에는 1로 설정합니다.

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg',0)

dft = cv2.dft(np.float32(img),flags = cv2.DFT_COMPLEX_OUTPUT)
dft_shift = np.fft.fftshift(dft)

rows, cols = img.shape
crow,ccol = int(rows/2), int(cols/2)

# create a mask first, center square is 1, remaining all zeros
mask = np.zeros((rows,cols,2),np.uint8)
mask[crow-30:crow+30, ccol-30:ccol+30] = 1

# apply mask and inverse DFT
fshift = dft_shift*mask
f_ishift = np.fft.ifftshift(fshift)
img_back = cv2.idft(f_ishift)
img_back = cv2.magnitude(img_back[:,:,0],img_back[:,:,1])

plt.subplot(121),plt.imshow(img, cmap = 'gray')
plt.title('Input Image'), plt.xticks([]), plt.yticks([])
plt.subplot(122),plt.imshow(img_back, cmap = 'gray')
plt.title('Magnitude Spectrum'), plt.xticks([]), plt.yticks([])
plt.show()

실행 결과는 다음과 같습니다.

이미지에 블러링 필터가 적용된 것과 같습니다. 그렇다면 왜 푸리에 변환과 그 역변환을 통해 원래의 이미지가 Edge 추출이나 Blur 효과가 적용되는지 직관적으로 이해하기 위해 다음 예제를 살펴보겠습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

# simple averaging filter without scaling parameter
mean_filter = np.ones((3,3))

# creating a guassian filter
x = cv2.getGaussianKernel(5,10)
gaussian = x*x.T

# different edge detecting filters
# scharr in x-direction
scharr = np.array([[-3, 0, 3],
                   [-10,0,10],
                   [-3, 0, 3]])
# sobel in x direction
sobel_x= np.array([[-1, 0, 1],
                   [-2, 0, 2],
                   [-1, 0, 1]])
# sobel in y direction
sobel_y= np.array([[-1,-2,-1],
                   [0, 0, 0],
                   [1, 2, 1]])
# laplacian
laplacian=np.array([[0, 1, 0],
                    [1,-4, 1],
                    [0, 1, 0]])

filters = [mean_filter, gaussian, laplacian, sobel_x, sobel_y, scharr]
filter_name = ['mean_filter', 'gaussian','laplacian', 'sobel_x', \
                'sobel_y', 'scharr_x']
fft_filters = [np.fft.fft2(x) for x in filters]
fft_shift = [np.fft.fftshift(y) for y in fft_filters]
mag_spectrum = [np.log(np.abs(z)+1) for z in fft_shift]

for i in range(6):
    plt.subplot(2,3,i+1),plt.imshow(mag_spectrum[i],cmap = 'gray')
    plt.title(filter_name[i]), plt.xticks([]), plt.yticks([])

plt.show()

위의 코드를 실행해보면 아래와 같은데.. 각 행렬에 대해 푸리에 변환을 수행해 그 결과를 살펴볼 수 있습니다.

처음 Numpy 방식에서는 푸리에 변환에 위의 예제 결과에서의 laplacian으로 변경했고, 두번째 OpenCV 방식에서는 mean_filter나 gaussian으로 변경하여 푸리에 역변환을 수행했다는 것을 상기할 수 있습니다.

Python과 OpenCV – 24 : Template Matching

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html#template-matching 입니다.

템플릿 매칭이란 어떤 큰 이미지에 존재하는 다른 작은 조각 이미지(템플릿 이미지)가 어디에 존재하는지를 찾아 내는 것입니다. 예를들어 아래의 이미지 중 왼쪽 이미지에서 오른쪽의 조각 이미지를 찾아내는 것을 말합니다.

위의 입력 이미지를 활용하여 템플릿 매칭에 대한 OpenCV 예제를 살펴보면..

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('./data/messi5.jpg',0)
img2 = img.copy()
template = cv2.imread('./data/messi_face.jpg',0)
w, h = template.shape[::-1]

# All the 6 methods for comparison in a list
methods = ['cv2.TM_CCOEFF', 'cv2.TM_CCOEFF_NORMED', 'cv2.TM_CCORR',
            'cv2.TM_CCORR_NORMED', 'cv2.TM_SQDIFF', 'cv2.TM_SQDIFF_NORMED']

for meth in methods:
    img = img2.copy()
    method = eval(meth)

    # Apply template Matching
    res = cv2.matchTemplate(img,template,method)
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

    # If the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
    if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
        top_left = min_loc
    else:
        top_left = max_loc
    bottom_right = (top_left[0] + w, top_left[1] + h)

    cv2.rectangle(img,top_left, bottom_right, 255, 2)

    plt.subplot(121),plt.imshow(res,cmap = 'gray')
    plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
    plt.subplot(122),plt.imshow(img,cmap = 'gray')
    plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
    plt.suptitle(meth)

    plt.show()

OpenCV에서 제공하는 템플릿 매칭에 대한 함수는 cv2.matchTemplate입니다. 총 6가지 종류의 탬플릿 매칭 알고리즘을 제공하는데 11번 코드의 배열에 언급되어 있습니다. 총 6개의 결과가 표시되는데 이해를 돕기 위해 하나만 언급하면 다음과 같습니다.

위의 예제는 단 하나의 매칭 결과만을 반환하는데, 만약 이미지에 동일한 템플릿 이미지가 여러개 존재할 경우에 대한 예제를 살펴보겠습니다. 먼저 입력 이미지인데요. 아래의 왼쪽 이미지에서 오른쪽의 조각 이미지를 찾아내고자 합니다.

코드는 다음과 같습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

img_rgb = cv2.imread('./data/mario.png')
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)
template = cv2.imread('./data/coin.png',0)
w, h = template.shape[::-1]

res = cv2.matchTemplate(img_gray,template,cv2.TM_CCOEFF_NORMED)
threshold = 0.8
loc = np.where(res >= threshold)
for pt in zip(*loc[::-1]):
    cv2.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 2)

cv2.imshow('result', img_rgb)
cv2.waitKey()
cv2.destroyAllWindows()

결과는 다음과 같습니다.

일치하는 것을 1개만 찾을때는 최대, 최소값에 기반하지만 여러개를 찾을 경우에는 임계치 조건(위의 예제의 경우 0.8)을 사용하여 임계치 값보다 큰 모든 매칭 영역을 표시하고 있습니다.

[OpenLayers] Canvas를 활용한 ImageLayer

ol에서 제공하는 레이어중 개발자가 직접 Canavs를 생성하고, 이 Canvas에 도형을 그려 넣을 수 있는 기능을 가진 ImageLayer가 있습니다. 이 레이어는 지도 좌표를 가진 도형을 원하는 스타일로 자유롭게 다른 레이어와 어색하지 않게 표현해 줍니다. 이 글은 ImageLayer를 이용해 지도 좌표를 갖는 간단한 사각형을 표현하는 예제를 소개합니다.

먼저 index.html은 다음과 같습니다.



    
        
        OpenLayers
        
    
                    
        

그리고 위에서 언급된 index.js 파일인데, 하나씩 살펴보면.. 먼저 필요한 모듈에 대한 코드입니다.

import 'ol/ol.css';

import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js';
import {ImageCanvas as ImageCanvasSource, Stamen} from 'ol/source.js';

그리고 지도 객체와 배경으로 사용할 레이어를 생성합니다.

var map = new Map({
    layers: [
        new TileLayer({
            source: new Stamen({
                layer: 'watercolor'
            })
        })
    ],
    target: 'map',
    view: new View({
        center: [50000, -50000],
        zoom: 9
    })
});

다음은 ImageLayer 객체를 생성하고 이 객체를 지도 객체에 추가하는 코드입니다.

var layer = new ImageLayer({
        source: new ImageCanvasSource({
        canvasFunction: canvasFunction,
        projection: 'EPSG:3857'
    })
});

map.addLayer(layer);

위의 코드 중 canvasFunction이 보이는데, 이 함수는 ImageLayer에서 Canvas 요소에 어떤 도형을 표현할 것인지에 대한 정의가 있습니다. 예를들어 다음과 같습니다.

function canvasFunction(extent, resolution, pixelRatio, size, projection) {
    var canvasWidth = size[0];
    var canvasHeight = size[1];

    var canvas = document.createElement('canvas');
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    var ctx = canvas.getContext('2d');

    var canvasOrigin = map.getPixelFromCoordinate([extent[0], extent[3]]);
    var mapExtent = map.getView().calculateExtent(map.getSize())
    var mapOrigin = map.getPixelFromCoordinate([mapExtent[0], mapExtent[3]]);
    var delta = [mapOrigin[0] - canvasOrigin[0], mapOrigin[1] - canvasOrigin[1]]

    var a1 = map.getPixelFromCoordinate([0, 0]); // [0, 0] -> EPSG:3857
    var a2 = map.getPixelFromCoordinate([100000, 100000]); // [100000, 100000] -> EPSG:3857
    
    ctx.fillStyle = "rgba(0,0,255,0.4)";
    ctx.fillRect(
        a1[0] + delta[0], a1[1] + delta[1], 
        Math.abs(a2[0]-a1[0]), Math.abs(a1[1]-a2[1])
    );

    return canvas;
};        

인자를 5개를 갖는 함수로 extent는 현재 지도의 extent, resolution은 현재 지도의 DPI, PixelRatio는 픽셀 비율값, 지도에 대한 DOM 요소의 크기, projection은 좌표계 정보인데 앞서 ImageLayer 생성시 projection을 ‘EPSG:3857’로 지정했으므로 이에 대한 제원정보를 갖는 투영객체가 할당됩니다. 코드를 보면 Canvas를 생성하기 위한 크기를 계산하여 지정하는 코드가 1-7번까지의 코드이고 11-14번은 도형의 좌표를 화면 좌표로 변환했을 시에 Canvas에 적용해야할 Offset값을 계산합니다. 이 값은 14번 코드의 delta에 저장됩니다. 16-17은 지도 좌표 (0,0)과 (100000, 100000) 좌표를 화면 좌표로 계산하여 a1, a2에 저장합니다. 이렇게 저장된 a1, a2를 이용해 Canvas에 사각형으로 그리는 코드가 19-23번 코드입니다. 최종적으로 처음 생성한 Canvas 객체를 반환해 줍니다. 실행해 보면 아래처럼 지도에 좌상단 좌표가 (0,0)이고 너비와 높이가 100000미터인 사각형이 그려진 것을 볼 수 있습니다.

Python과 OpenCV – 23 : 히스토그램(Histogram) 4/4

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_histograms/py_histogram_backprojection/py_histogram_backprojection.html#histogram-backprojection 입니다.

2차원 히스토그램을 응용하여 이미지에서 원하는 객체만을 추출해 내는 방법인 Backprojection에 대한 코드를 살펴보겠습니다. 바로 예제 나갑니다. 상세한 설명은 원문을 참고하시기 바랍니다.

import cv2
import numpy as np

roi = cv2.imread('./data/messi5_g.jpg')
hsv = cv2.cvtColor(roi,cv2.COLOR_BGR2HSV)

target = cv2.imread('./data/messi5.jpg')
hsvt = cv2.cvtColor(target,cv2.COLOR_BGR2HSV)

# calculating object histogram
roihist = cv2.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )

# normalize histogram and apply backprojection
cv2.normalize(roihist,roihist,0,255,cv2.NORM_MINMAX)
dst = cv2.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)

# Now convolute with circular disc
disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
cv2.filter2D(dst,-1,disc,dst)

# threshold
ret,thresh = cv2.threshold(dst,50,255,0)

# threshold and binary AND
thresh = cv2.merge((thresh,thresh,thresh))
res = cv2.bitwise_and(target,thresh)

res = np.vstack((thresh,res))

cv2.imshow('result', res)
cv2.waitKey()
cv2.destroyAllWindows()

위에서 messi5.jpg는 어떤 객체를 추출할 대상이고 messi5_g.jpg는 추출할 대상입니다. messi5.jpg는 다음과 같습니다.

messi5_g.jpg는 다음과 같은데, 이 이미지는 messi5.jpg에서 잘라내기로 추출한 이미지입니다.

즉, 이미지에서 잔디에 해당되는 영역을 추출하겠다는 것입니다. 위의 코드의 실행 결과는 다음과 같습니다.

결과로 생성된 이미지가 2개이고, 이를 하나로 합쳐서 표시한 것인데.. 위에는 잔디에 해당되는 영역에 대한 결과 마스크 이미지이고 두번째는 이 마스크 이미지와 원본 이미지에 대한 bitwise 연산을 통해 실제 잔디 영상만을 추출한 결과입니다.