[OpenLayers] 현재 지도 화면 영역 얻기

지도를 이동 또는 확대 후에 지도가 표시되는 화면의 영역을 얻어야할 때가 있습니다. 여기서의 화면 영역의 좌표는 지도 좌표입니다. 이 글은 지도가 이동 또는 확대 시 발생되는 이벤트를 통해서.. 변경된 지도 화면에 대한 지도 좌표 영역을 얻어서 지도 상에 사각형으로 그려주는 예를 소개합니다.

먼저 필요한 index.html 파일의 내용입니다.



    
        
        OpenLayers
        
    
                    
        

그리고 index.js 파일에 대한 내용인데, 순차적으로 하나씩 언급하면.. 먼저 필요한 모듈의 추가입니다.

import 'ol/ol.css';
 
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {getBottomLeft, getTopRight, getBottomRight, getTopLeft} from 'ol/extent.js';
import {toLonLat} from 'ol/proj.js';
import OSM from 'ol/source/OSM.js';
import {Vector as VectorSource} from 'ol/source.js';
import Feature from 'ol/Feature.js';
import {LineString} from 'ol/geom.js';
import {Stroke, Style} from 'ol/style.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {get as getProjection} from 'ol/proj.js';

현재 화면 영역에 대한 사각형 도형을 추가하기 위한 벡터 레이어가 필요하므로 이를 위한 레이어 생성과 이 레이어로 구성된 지도를 생성하는 코드입니다.

var vectorsource = new VectorSource();

var vectorlayer = new VectorLayer({
    source: vectorsource,
    style: new Style({
        stroke: new Stroke({
            width: 2,
            color: [0, 0, 255, 1]
        })
    })
});

var map = new Map({
    layers: [
        new TileLayer({
            source: new OSM()
        }),
        vectorlayer
    ],
    target: 'map',
    view: new View({
        center: [0, 0],
        zoom: 18
    })
});

사용자가 지도를 확대 또는 이동하면 지도에 대해서 moveend 이벤트가 발생하는데, 이 이벤트에 대한 콜백 함수를 지정합니다.

map.on('moveend', onMoveEnd);

function onMoveEnd(evt) {
    var map = evt.map;
    var size = map.getSize();
    var extent = map.getView().calculateExtent(size);
    var bottomLeft = toLonLat(getBottomLeft(extent));
    var topRight = toLonLat(getTopRight(extent));
    var bottomRight = toLonLat(getBottomRight(extent));
    var topLeft = toLonLat(getTopLeft(extent));

    var box = new Feature(new LineString(
        [
            [bottomLeft[0], bottomLeft[1]], 
            [bottomRight[0], bottomRight[1]], 
            [topRight[0], topRight[1]],
            [topLeft[0], topLeft[1]],
            [bottomLeft[0], bottomLeft[1]]
        ])
    );

    var current_projection = getProjection('EPSG:4326');
    var new_projection = getProjection('EPSG:3857');
 
    box.getGeometry().transform(current_projection, new_projection);

    vectorsource.addFeatures([box]);
}

이 글의 핵심적인 내용이니 하나씩 파악해보면… 5번 코드의 map에 대한 getSize()를 통해 얻어올 수 있는 것은 현재 지도에 대한 픽셀 단위의 화면 크기입니다. 이 화면 크기를 이용해 map의 view에 대한 calculateExtent 함수의 인자로 전달해 해당 크기만큼의 현재 지도 화면의 지도 좌표의 경계(MinX, MinY, MaxX, MaxY)를 얻는 코드가 6입니다. 이렇게 얻은 지도 좌표 경계는 사각형 형태인데 각 4개의 모서리 좌표를 얻는 것이 7-10번 코드입니다. 그런데 이 코드들을 보면 좌표를 경위도로 변경하고 있는데.. 이는 OSM의 좌표계인 EPSG:4326으로 넘어오기 때문에 이를 WGS84 타원체의 경위도 좌표계(EPSG:3857)로 변환하는 것입니다. 사실 그냥 표현만을 위한 것이라면 OSM 좌표계로 그대로 사용해도 되지만 DBMS에 저장시에는 경위도 좌표계가 필요한 경우가 많아 소개해 봅니다. 이렇게 얻은 좌표를 이용해 Feature를 생성하고, 이를 다시 화면에 표시하기 위해 EPSG:3857에서 EPSG:4326으로 변환합니다. ^^; 역시 좌표계 변환을 방법을 소개 및 정리하기 위해 추가해본 코드입니다. 이처럼 좌표변환까지 완료된 Feature를 소스에 추가해 주면 됩니다.

Python과 OpenCV – 9 : Image Thresholding (1/2)

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

이 글에서는 Simple thresholding, Adaptive thresholding, Otsu’s thresholding 중 처음 2가지에 대해 언급합니다. 그리고 글을 나눠 Otsu’s thresholding을 설명하겠습니다.

Image Thresholding 즉, 이미지의 임계값 처리란 픽셀값(대부분은 Grayscale 값)이 어떤 조건(임계 조건)을 만족할때 해당 픽셀의 값을 어떤 값으로 치환할 것인가를 결정하는 것인데, 먼저 Simple thresholding에 대한 예제를 살펴보면..

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

img = cv2.imread('./data/gray-gradient.jpg', cv2.IMREAD_GRAYSCALE)

ret,thresh1 = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
ret,thresh2 = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY_INV)
ret,thresh3 = cv2.threshold(img, 180, 255, cv2.THRESH_TRUNC)
ret,thresh4 = cv2.threshold(img, 180, 255, cv2.THRESH_TOZERO)
ret,thresh5 = cv2.threshold(img, 180, 255, cv2.THRESH_TOZERO_INV)

titles = ['Original Image', 'BINARY', 'BINARY_INV', 'TRUNC', 'TOZERO', 'TOZERO_INV']
images = [img, thresh1, thresh2, thresh3, thresh4, thresh5]

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

plt.show()

결과를 먼저 보고 코드에서 중요한 부분을 언급하겠습니다.

Original Image가 입력 이미지인 원본인데, 5개에 대한 Simple thresholding에 대한 결과가 표시되고 있습니다. Simple thresholding 처리에 대한 함수는 cv.threshold 인데, 첫번째 인자는 입력 이미지, 두번째는 조건값에 대한 픽셀값의 최소값, 세번째는 조건값에 대한 픽셀값의 최대값, 네번째는 thresholding 조건에 대한 결과값 처리 방식인데 살펴보면..

  • cv2.THRESH_BINARY – 조건을 만족하는 픽셀값을 255로 만족하지 않는 픽셀값을 0으로 설정
  • cv2.THRESH_BINARY_INV – 조건을 만족하는 픽셀값을 0로 만족하지 않는 픽셀값을 255으로 설정
  • cv2.THRESH_TRUNC – 조건을 만족하는 픽셀값을 255로 만족하지 않는 픽셀값을 원본값으로 설정
  • cv2.THRESH_TOZERO – 조건을 만족하는 픽셀값을 원본값으로 만족하지 않는 픽셀값을 0으로 설정
    cv2.THRESH_TOZERO_INV – 조건을 만족하는 픽셀값을 0로 만족하지 않는 픽셀값을 원본값으로 설정

이제 다음은 Adaptive thresholding인데요. Simple thresholding의 문제점은 이미지가 전체적으로 동일한 밝기를 가져야 한다는 것인데.. 예를 들어서 하나의 이미지에 어느 부분은 그림자가 들어가 있을 경우 이 그림자 부분에 대한 처리가 제대로 되지 않습니다. 이를 개선하기 위한 방식이 Adaptive thresholding이며 예제 코드는 다음과 같습니다.

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

img = cv2.imread('./data/srcThreshold.png', 0)
img = cv2.medianBlur(img, 5) # 잡음제거

ret,th1 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)

th2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)

th3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)

titles = ['Original Image', 'Simple Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding']
images = [img, th1, th2, th3]

for i in range(4):
    plt.subplot(2, 2, i+1), plt.imshow(images[i], 'gray')
    plt.title(titles[i])
    plt.xticks([]), plt.yticks([])
plt.show()

결과를 보면..

총 4개의 영상이 표시되는데 Original Image는 입력 원본 영상, Simple Threshold는 앞서 살펴본 cv2.threshold에 의한 처리 영상입니다. 원본 이미지에서 다른 부분보다 상대적으로 어두운 부분에 대해도 원하는 처리가 된것을 비교해 볼 수 있습니다. 나머지 2개가 Adaptive thresholding인데 첫번째는 단순 평균값, 두번째는 가중치에 의해 처리된 값이 합입니다. 이는 cv.adaptiveThreshold 함수의 세번째 인자에 의해 지정되며 아래와 같습니다.

  • cv2.ADAPTIVE_THRESH_MEAN_C – 대상 픽셀값 주위의 픽셀값들의 평균값에 C 값을 더함
  • cv2.ADAPTIVE_THRESH_GAUSSIAN_C – Gaussian Window에 의해 가중치 값이 적용된 픽셀값들의 합에 C 값을 더함

cv.adaptiveThreshold의 전체 인자를 살펴보면 첫번째는 입력 이미지, 두번째는 픽셀값의 최대값, 세번째와 네번째는 앞서 설명한 바가 같으며, 다섯번째는 주의값의 픽셀을 얻을 Window의 크기, 여섯번째는 C 값입니다.

[OpenLayers] 지도 화면을 PDF 파일로 저장하기

웹에서 PDF 파일을 생성하는 것은 클라이언트가 서버에 PDF 파일 생성을 요청하고 서버측에서 생성한 PDF 파일을 클라이언트에서는 다운로드 하는 방식으로 이루어졌다. 하지만 서버의 기술 없이도 클라이언트에서 PDF 파일을 생성해 내려받을 수 있는데, 이를 ol의 지도를 PDF 파일로 생성하는 것을 토대로 정리해 본다.

웹에서 JS만으로 PDF를 생성하는 API로 많이 사용되는 jspdf.js 라이브러리가 있는데 이를 이용한다.

먼저 index.html에 UI 요소에 대한 코드를 다음처럼 작성한다.



    
        
        OpenLayers
        

        
    
                    
        

jspdf.js 스크립트에 대한 추가와 DOM 요소로써 PDF로 출력할 지도에 대한 DIV 및 PDF 출력을 위한 버튼이 보인다. index.js에 JS 코드를 작성할 것인데, 하나씩 살펴보자.

먼저 필요한 모듈을 추가한다.

import 'ol/ol.css';
 
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {OSM, Vector as VectorSource} from 'ol/source.js';

다음은 출력할 지도 객체에 대한 코드이다.

var map = new Map({
        layers: [
        new TileLayer({
            source: new OSM()
        })
    ],
    target: 'map',
    view: new View({
        center: [0, 0],
        zoom: 2
    })
});

이제 버튼을 클릭하면 해당 지도의 내용이 PDF로 출력되는데, 이를 위해 필요한 변수이다.

var format = 'a4';

/*  Unit : mm
    A0: [1189, 841]
    A1:  [841, 594]
    A2:  [594, 420]
    A3:  [420, 297]
    A4:  [297, 210]
    A5:  [210, 148]
*/
var dim = [297, 210];

var resolution = 150; //dpi (Unit: inch)

지도를 A4 용지 크기로 PDF 출력할 것이고, A4 용지의 크기는 397mm x 210mm 라는 점. 그리고 출력 DPI 해상도는 150으로 지정했는데, 이를 높이면 그만큼 출력 품질은 올라가지만 더 많은 CPU와 메모리 자원을 사용하고 생성하는 시간도 상당이 길어진다. 다음은 출력 버튼의 클릭 이벤트에 대한 코드이다.

var btn = document.querySelector('#btn');

btn.addEventListener('click', function() {
    btn.disabled = true;
    document.body.style.cursor = 'progress';

    var width = Math.round(dim[0] * resolution / 25.4); // 1in = 25.4mm
    var height = Math.round(dim[1] * resolution / 25.4);
    var size = /** @type {module:ol/size~Size} */ (map.getSize());
    var extent = map.getView().calculateExtent(size);

    map.once('rendercomplete', function(event) {
        var canvas = event.context.canvas;
        var data = canvas.toDataURL('image/jpeg');
        var pdf = new jsPDF({
            orientation: 'landscape',
            unit: 'mm',
            format: format
        });

        pdf.addImage(data, 'JPEG', 0, 0, dim[0], dim[1]);
        pdf.save('map.pdf');
        
        // Reset original map size
        map.setSize(size);
        map.getView().fit(extent, {size: size});
        
        btn.disabled = false;
        document.body.style.cursor = 'auto';
    });

    // Set print size
    var printSize = [width, height];
    map.setSize(printSize);
    map.getView().fit(extent, {size: printSize});
}, false);

위의 코드를 이해하기 위해서는 먼저 jspdf.js 라이브러리의 사용법을 익히는게 필요하다. 중요한 부분만을 언급하면 7~10번은 앞서 A4 용지 크기만큼 지도를 출력하기 위해서는 A4 용지의 크기만큼 지도를 다시 그려줘야하는데 그려줄 영역의 크기를 계산하고 이 크기를 33~35번 코드처럼 지도 객체에 설정하게 되면 지도가 그려지게 시작하면서 다 그려지면 rendercomplete 이벤트가 발생한다. 이 이벤트에 대한 호출 함수는 12번에서 정의되어 있는데 여기서 jsPDF 라이브러리를 사용하는 코드가 보인다. jsPDF를 통해 PDF 출력이 끝나면 다시 지도를 원래의 크기로 되돌리기 위해 25~26번 코드의 호출이 필요하다.

고해상도 DEM 생성을 위한 자동화 프로그램

GIS 시스템 개발을 위한 다양한 데이터가 필요한데, 그 중 하나가 DEM입니다. DEM은 지형에 대한 3차원 시각화 뿐만 아니라 지형 분석을 위한 필수 데이터입니다.

이 툴의 장점은 다음과 같습니다.

  • 전국 범위에 대한 DEM 생성이 가능합니다.
  • DEM을 생성하고자 하는 지역을 쉽게 지정할 수 있으며 범위 제한이 없습니다.
  • 1미터, 5미터, 10미터 등의 원하는 해상도를 갖는 DEM 생성이 가능합니다.
  • SHP 파일, (X Y Z) 문자열 형식의 파일로 DEM 데이터를 생성할 수 있으며, 사용자가 원하는 구조로도 생성할 수 있습니다.
  • 위치에 대한 다양한 좌표계를 지정할 수 있어, 별도의 좌표계 변환이 필요치 않습니다.
  • 기존 다른 곳에서 제공하는 DEM 보다 최신의 표고값을 제공합니다. (2018년도 중순 데이터 활용)

DEM 데이터를 이용한 다양한 활용예가 있지만, 먼저 DEM 데이터를 이용한 지형의 3차원 시각화입니다. 다양한 지형의 분석 이전에 실제 지형의 형상을 바로 시각화해 살펴볼 수 있습니다.

그리고 아래는 지형에 대한 평균경사도를 측정하는 기능입니다. 태양광 분석이나 인허가 업무에 활용될 수 있습니다.

또한 아래는 지형에 대한 단면도를 측정하는 기능입니다. 지형이 오르막길인지, 내리막길인지를 효과적으로 파악할 수있으며, 지점간의 보다 정확한 거리를 측정할 수 있습니다.