[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);
}

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

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

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다