Python과 OpenCV – 4 : 마우스 이벤트

원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_mouse_handling/py_mouse_handling.html 입니다.

이미지 처리를 위해 OpenCV에서는 자체적인 마우스 이벤트 체계를 가지고 있습니다. 마우스 이벤트는 cv2.setMouseCallback 함수를 통해 마우스 이벤트를 처리할 Window에 등록됩니다. 먼저 간단한 예를 통해 살펴보겠습니다.

import cv2
import numpy as np

def draw_circle(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONDBLCLK:
        cv2.circle(img,(x,y),100,(255,0,0),-1)

img = np.zeros((512,512,3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle)

while(1):
    cv2.imshow('image',img)
    if cv2.waitKey(20) & 0xFF == 27:
        break

cv2.destroyAllWindows()

위의 프로그램은 Window 위에서 마우스 버튼을 더블클릭하면 해당 지점에서 파란색 큰 원을 그립니다.

좀더 복잡한 예제를 살펴보겠습니다.

import cv2
import numpy as np

drawing = False
mode = True
ix, iy = -1, -1

def draw_circle(event,x,y,flags,param):
    global ix, iy, drawing, mode

    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        ix, iy = x, y

    elif event == cv2.EVENT_MOUSEMOVE:
        if drawing == True:
            if mode == True:
                cv2.rectangle(img, (ix,iy), (x,y), (0,255,0), -1)
            else:
                cv2.circle(img, (x,y), 5, (0,0,255), -1)

    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        if mode == True:
            cv2.rectangle(img, (ix,iy), (x,y), (0,255,0), -1)
        else:
            cv2.circle(img, (x,y), 5, (0,0,255), -1)

img = np.zeros((512,512,3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image', draw_circle)

while(1):
    cv2.imshow('image', img)
    k = cv2.waitKey(1) & 0xFF
    if k == ord('m'):
        mode = not mode
    elif k == 27:
        break

cv2.destroyAllWindows()

위의 프로그램은 마우스 드래그를 이용해 사각형과 원을 그릴 수 있는데, m 키를 눌러 그리고자 하는 원과 사각형을 결정할 수 있습니다.

[OpenLayers] 이미지 필터링

이미지 필터링은 이미지의 외곽선 추출이나 이미지의 잡음을 제거하기 위해 수행되는 NxN 행렬의 커널을 이미지의 각 화소에 연산하여 다시 조합하는 것을 말합니다. 이 글은 ol에서 받은 영상 데이터에 대한 외곽선을 추출하는 필터링 연산을 적용하여 그 결과를 실시간으로 살펴보는 것에 대한 내용을 정리합니다.

먼저 DOM을 아래처럼 구성합니다. 스타일과 함께 언급했고요..



    
        
        OpenLayers
        
    
    
        

지도에 대한 div 요소가 전부입니다. 이 div 요소가 바로 지도가 담길 DOM 입니다. js 코드를 순서대로 하나씩 살펴 보겠습니다. 먼저 필요한 모듈을 추가합니다.

import 'ol/ol.css';

import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {Tile} from 'ol/layer.js';
import {XYZ} from 'ol/source.js';

그리고 필터링 대상이 되는 이미지를 VWorld의 항공영상 레이어로 사용할텐데.. 이에 대한 레이어 객체 imagery를 아래처럼 준비합니다.

let imagery = new Tile({
    source: new XYZ({
        url : 'http://xdworld.vworld.kr:8080/2d/Satellite/service/{z}/{x}/{y}.jpeg',
        crossOrigin: 'anonymous'
    }),
})

4번 코드를 보면 crossOrigin 옵션을 ‘anonymous’로 지정하고 있습니다. 이는 외부에서 가져온 이미지를 처리하여 다시 화면에 그릴때 발생하는 보안상의 오류를 방지합니다. 이제 생서한 레이어를 이용하여 지도 객체를 아래처럼 준비합니다.

let map = new Map({
    target: 'map',
    layers: [ 
        imagery
    ],
    view: new View({
        center:  [14128579.82, 4512570.74],
        zoom: 17,
        minZoom: 6
    })
});

외곽선 추출을 위한 커널로 3×3 행렬을 사용합니다. 가장 일반적인 외곽선 추출을 위한 3×3 행렬을 아래처럼 정의합니다.

let kernel = 
    [
        0,  1,  0,
        1, -4,  1,
        0,  1,  0
    ];

입력 데이터가 되는 항공영상 레이어에 postcompose 이벤트를 추가하여 입력 데이터가 완전이 준비되면 호출할 이벤트를 아래처럼 입력합니다.

imagery.on('postcompose', function(event) {
    convolve(event.context, kernel);
});

convolve 함수는 2개의 인자를 취하는데, 첫번째는 Canvas에 대한 context이고 두번째는 커널 행렬입니다. 첫번째 context의 Canvas에 입력 데이터에 대한 화소(Pixel 값으로써의 RGB)를 가지고 있습니다. 또한 이 Canvas에 다시 우리가 원하는 무언가를 그릴 수 있는데, 그리고자 하는 것은 필터링이 적용된 결과 이미지가 됩니다. convolve 함수는 다음과 같습니다.

function convolve(context, kernel) {
    let canvas = context.canvas;
    let width = canvas.width;
    let height = canvas.height;

    let size = Math.sqrt(kernel.length);
    let half = Math.floor(size / 2);

    let inputData = context.getImageData(0, 0, width, height).data;

    let output = context.createImageData(width, height);
    let outputData = output.data;

    for (let pixelY = 0; pixelY < height; ++pixelY) {
        let pixelsAbove = pixelY * width;
        for (let pixelX = 0; pixelX < width; ++pixelX) {
            let r = 0, g = 0, b = 0, a = 0;
            for (let kernelY = 0; kernelY < size; ++kernelY) {
                for (let kernelX = 0; kernelX < size; ++kernelX) {
                    let weight = kernel[kernelY * size + kernelX];
                    let neighborY = Math.min(height - 1, Math.max(0, pixelY + kernelY - half));
                    let neighborX = Math.min(width - 1, Math.max(0, pixelX + kernelX - half));
                    let inputIndex = (neighborY * width + neighborX) * 4;

                    r += inputData[inputIndex] * weight;
                    g += inputData[inputIndex + 1] * weight;
                    b += inputData[inputIndex + 2] * weight;
                    a += inputData[inputIndex + 3] * weight;
                }
            }

            let outputIndex = (pixelsAbove + pixelX) * 4;
            
            outputData[outputIndex] = r;
            outputData[outputIndex + 1] = g;
            outputData[outputIndex + 2] = b;
            outputData[outputIndex + 3] = 255;
        }
    }

    context.putImageData(output, 0, 0);
}

위의 코드의 핵심을 짚어보면 입력 데이터의 각 화소에 접근하여 커널 행렬을 적용하고, 그 결과 화소로 구성된 이미지를 다시 그려준다는 것입니다. 실행 결과는 아래와 같습니다.

위의 지역은 어딜까요...? 외곽선을 좀더 두드러지게 추출할 수 있는 다른 커널 행렬을 적용해 볼 필요가 있을듯합니다.