이미지 필터링은 이미지의 외곽선 추출이나 이미지의 잡음을 제거하기 위해 수행되는 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); }
위의 코드의 핵심을 짚어보면 입력 데이터의 각 화소에 접근하여 커널 행렬을 적용하고, 그 결과 화소로 구성된 이미지를 다시 그려준다는 것입니다. 실행 결과는 아래와 같습니다.
위의 지역은 어딜까요...? 외곽선을 좀더 두드러지게 추출할 수 있는 다른 커널 행렬을 적용해 볼 필요가 있을듯합니다.