
국가데이터처 100m 격자 인구
고작 1.7MB에 전국 100m 격자 인구 데이터를 몽땅 넣어보자.
국가데이터처(통계청)은 1년 단위로 100m 격자 인구를 공개한다. sgis에서 신청하면 받을 수 있다.
2021년의 데이터를 받으면 아래와 같다. "다바000277"은 EPSG:5179 기준 100m 격자 코드. 곧 좌표가 되는 값이다.

to_in_001, 007, 008은 각 셀의 총인구, 남성, 여성 인구값이다. 5인 미만을 0으로 표시하기 때문에 남성과 여성을 합하여 총인구값이 나오지 않는다. 마침 받았던 데이터가 저렇게 3개의 값을 지니므로 일단 전부 보전하기로 했다.

2021년의 격자 데이터는 총 91MB, 압축하면 11.7MB가 된다.
그럼 이제 본격적으로 시작해볼까.
우선 지도에 찍어야 하니 "다바000277"과 같은 격자 번호를 정해진 규칙들에 의해 함수를 통해 일괄적으로 변환한다. (코드는 github에 공개해 놓았다) 그렇게 x좌표, y좌표, 남, 여, 총인구 이렇게 5개의 cell로 변환한다. 한해 전국의 인구를 모두 합치면 931,495 행. 전국에 그만큼의 인구 격자가 존재한다는 의미다. 중간 중간에 사람이 살지 않는 100m 격자는 데이터에 포함되어 있지 않다.

물론, 이런 데이터를 압축적으로 저장하는데는 parquet이라는 훌륭한 열방향 압축 포맷이 존재한다.
parquet으로 저장해보니 3.6MB가 나왔다. 많이 줄어들었다. parquet를 다시 zip으로 압축하니 3.2MB가 되었다. 개방하는 압축 원본이 11.7MB라면 27%로 줄어들었다.
이 데이터를 1.7MB, 즉 압축한 parquet에서 다시 절반 수준까지 줄여보자.
1.7MB을 93만행으로 나누면 한 행은 1.97Byte(=2Byte)다. 2Byte라고 하면 의문이 생기겠지. 아니 도대체 어떻게... 왜냐하면, 좌표값 x,y만 하더라도 벌써 4+4=8 Byte가 되기 때문이다.. 물론 parquet만 해도 벌써 한 행을 4Byte 수준으로 줄여놓았다. 그러고 보니 왠지 할 수 있을 것 같다. 압축의 세계란 신비롭고도 놀라운 것이라서 비슷한 값들의 연속, 명목값처럼 정해진 값들의 반복들을 여러가지 알고리즘으로 효율적으로 줄여버린다. 그렇게 나온 포맷이 바로 parquet이다.
그리고 나는 이걸 좀 더 극단적으로 줄여보기로 했다.
quadkey를 표현하는 Bitmask의 나열
우선 quadkey에서부터 출발해보자.

bingmap의 xyz 타일에서 사용하는 인덱싱 방식인데, 전세계 EPSG3857 지도를 정사각형으로 놓고, 4등분한다.
위에서 가장 오른쪽 그림에서 보듯, 제일 위의 zoom레벨에서부터 해당하는 타일의 번호를 연속적으로 써내려가면 일종의 좌표가 정의된다.
예를 들어 Level 3 에서 131번 타일은,
전체를 4등분 했을 때 1번 타일,
다시 그 1번 타일을 4등분 했을 때 3번 타일,
다시 그 3번 타일을 4등분 했을 때 1번 타일이라는 의미다.
그리고 이걸 계속해서 100m 수준까지 잘게 쪼개나가면 좌표값을 대신할 수 있다. 물론 위의 그림과는 좌표계가 다르므로 EPSG5179 에서 적절한 정사각형을 정의하고 출발해야 한다.
EPSG5179 좌표계에서 (700000, 1300000)을 좌하단 원점으로 잡고 한 변 819200m의 정사각형을 정의하면 전 국토를 포함하는 정사각형의 좌표계 위에 정의되고, 13레벨까지 쪼개나갈 때 100m 격자가 정의된다. 그렇다면 그 격자는 0~3으로 이루어진 13자리 숫자로 표현할 수 있다. 예를 들면, 3 3120 1122 3022 과 같은 식이다.
그런데 0~3은 두 자리의 이진수로 표현가능하다. 그렇다면 위의 좌표는 다시,
11 11011000 01011010 11001010 으로 표현 가능하다. 십진수로 변환하면 64,510,666
그런데 00321처럼 0으로 시작하는 번호는 321로 취급되기 때문에 전체 자리수를 알 수가 없다. 따라서 제일 처음에 일종의 매직 넘버 11을 붙여 자리수를 보전해 준다.

그렇게 해서 나온 수는 258,042,667
이 9자리의 수로 전 국토 범위의 100m 격자 x,y좌표를 담을 수 있다.
아래 표 처럼 특정 행은 244021529, 28, 18, 13 으로 표현가능하다.
그리고 아직 2Byte와는 거리가 좀 있어보인다.

그럼 이제 좌표값에 할당하는 공간을 좀 줄여볼까.
흥미로운 점은 특정 레벨의 어떤 셀의 네 사분면은 그 상위 사분면의 좌표들이 모두 같다는 점이다. 그러므로 존재하는 좌표들을 규칙적으로 잘 나열할 수 있다면 좌표값을 표시하는 일부 자릿수들은 생략하고 그룹별로 공유할 수 있다.
예를 들어,
0010, 1010, 1111, 0010
이 연속적인 16개의 비트 안에는 레벨 3 깊이 5개의 좌표값들이 담겨 있다.
0010은 레벨 1의 좌표다. 레벨 1 을 4개로 쪼갰을 때 쿼드키 2번 셀 아래에만 값들이 존재한다는 의미다.
즉, [특정 레벨에 존재하는 1의 개수] 에 4를 곱하면 바로 한 단계 아래 레벨의 총 비트 길이가 된다.
그래서, 레벨 2는 1010이 전부다. 연속적인 배열에서 굳이 어디까지가 레벨1이고, 어디까지가 레벨2인지 별도로 설명할 필요가 없다. 그리고 1010은 레벨 3의 경우, 레벨 2의 0번 2번 쿼드키 공간에만 좌표값들이 존재한다는 의미다.
1010에서 1은 2개이므로 2 x 4 = 8. 레벨 3의 총 길이는 8비트, 즉 1111, 0010으로 끝난다.
1111은 레벨 3의 모든 셀에 좌표값이 존재한다는 것을 의미
0010은 레벨 3의 2번 셀에만 좌표값이 존재한다는 것을 의미한다.
따라서 0010, 1010, 1111, 0010은 아래와 같은 5개의 좌표로 디코딩할 수 있다.
10 00 00
10 00 01
10 00 10
10 00 11
10 10 10
(쓰다보니 자리수 순서가 좀 뒤바뀐 듯 한데 여튼 의미는 전달되었으리라 생각한다)
즉, 전체 공간을 가로 8x8 = 64개로 쪼갠 공간에서 5개의 좌표를 2바이트(0010 1010 1111 0010)안에 모두 담을 수 있었다.
앞에서 설명한 원리에 따르면 모든 좌표는 아래 그림과 같이 한꺼번에 표현할 수 있다.

다시 말해서,
레벨별로 존재하는 좌표값들을 그저 일렬로 늘어놓기만 해도 완전 복원하는 디코딩이 가능하는 의미다. 이 얼마나 매력적인가!
그리고 여기서 좌표값 저장 공간 절약의 비밀이 드러난다. 하나의 좌표값 표현은 상위 레벨부터 공유되므로 내려오므로 그만큼 저장공간이 절약된다. 그리고 최종 하위 레벨의 타일 유무는 단지 1비트(!) 만으로 표현된다.
사실 이런 알고리즘은 이미 여러 응용 형태로 존재한다.
3D 공간에서 SVO(Sparse Voxel Octrees)가 바로 그러한 것 중 하나. 가로세로높이를 2개씩 쪼개서 8개의 정육면체들로 공간을 계속 분할해 나간다. (https://eisenwave.github.io/voxel-compression-docs/svo/optimization.html)
다만, 찾아보니 2차원 지리정보에서 아직 많이 쓰는 포맷으로 나와 있지 않았다.
그래서 존재하는 이런저런 효율적 좌표 저장 방식들을 조합해서 데이터 포맷을 하나 만들어보기로 했다.
이름은 QBTiles (Quadkey Bitmask Tiles)로 붙였다.
QBTiles : Quadkey Bitmask Tiles
사실 처음부터 포맷을 만들려고 계획한 것은 아니었다. 작년에 의뢰받아서 작업하던 프로젝트에서 저장공간의 분할이 똑같은 시계열 데이터를 다루어야 했다. 똑같은 길이의 데이터가 수 천번 이상 반복되는 상황이었다.
그때 쯤에는 PMTiles라는, 아는 사람들은 다 아는 훌륭한 Cloud Optimized 타일 포맷을 잘 사용하고 있었다.
PMTiles는 xyz 타일맵을 하나의 파일에 연속적으로 이어붙인 포맷이다.앞 부분의 인덱스(좌표ID+뒷부분의 각 데이터 시작 주소 및 길이 등)와 뒷 부분에 데이터가 순서대로 패킹되어 들어간다.물론 인덱스는 중간중간에 트리구조처럼 잘게 쪼개져 있다. 몇백GB의 전세계 OSM의 인덱스를 다 긁어모으면 300MB정도 되는데, 이것을 중간중간에 잘 흩어놓아서, 사용자는 탐색하는 위치에 따라 100KB 정도의 인덱스들만 중간중간에 받아가면서 거의 끊김없이 xyz 타일 공간을 탐색할 수 있다.
인덱스를 통해 데이터의 위치를 계산할 수 있으므로 클라이언트에서 HTTP range request로 서버에 요청해서 필요한 부분만 받아갈 수 있기 때문이다.
그런데 이 포맷은 당시 해결해야했던 문제들과 여러 측면에서 부딪혔다.
일단, 내가 담아야 했던 데이터 포맷을 PMTiles가 지원하지 않았다. 물론 도형이므로 mvt에 넣을 수는 있지만 데이터 크기가 너무 커져버렸다. 공개된 PMTiles 코드를 분해해서 적용할 수도 있었겠지만, 또 다른 문제가 있었다. PMTiles는 데이터들을 최대한 압축해버렸던 까닭에 형식이 같더라도 데이터 좌표값들이 조금씩 달라지면 인덱스가 달라졌다. 시계열 데이터가 1분 단위로 있는데 1시간을 탐색하려면 인덱스를 60번 내려받아서 디코딩해야한다. 그 잠깐의 시간들이 화면의 play에서 끊김을 만들어냈다.
결국 존재하는 포맷들을 찾다가 딱 내가 원하는 것은 없었으므로 필요한 특징들을 담아 새로 만들기로 했다. 우연히 그 때 쯤 Roaring Bitmap 같은 비트 단위의 압축과 탐색에 꽂혀 있었는데, 그래서 알고 있던 지식과 찾아본 여러가지들을 바탕으로 만든게 QBTiles였다.
QBTiles는 PMTiles와 비슷한 이름으로 지었는데, PMTiles의 구조에서 많은 아이디어를 차용했기 때문이다. 단, 힐베르트 곡선 ID 를 사용한 PMTiles와는 달리 quadkey를 표현한 bitmask의 나열로 좌표값을 압축적으로 표현했다.
PMTiles처럼, 전국 행정구역 다단계 zxy 타일맵을 30MB의 단일 파일 안에 담았다. 당연히 잘 돌아간다.
아래 site에서 확인해 볼 수있다.
QBTiles Demo - Tiles
vuski.github.io
그리고, 압축된 인덱스를 동일 조건에서 테스트해보니 크기가 비슷하거나 50% 정도까지도 줄어들었다. 만들어서 쓰고 있던 전국 행정구역은 PMTiles의 경우 인덱스가 81KB였는데 QBTiles 방식으로 압축하니 60KB로 약 24% 줄어들었다. 사실 보통의 인터넷 환경에서 느낄 수 없는 차이긴 하지만. ㅋ
아래 표처럼 전세계 OSM 역시 인덱스만 비교하면 80% 정도의 크기로 줄어든다.(맨 마지막 행)
인덱스 외의 데이터 크기는 물론 똑같다.

Benchmark - QBTiles
Benchmark Results Comparing PMTiles and QBTiles index sizes using the same tile entries, serialized with gzip compression. Both formats sort entries by their respective key (tile_id for PMTiles, quadkey for QBTiles) with data arranged in that order. Index
vuski.github.io
PMTiles처럼 인덱스를 곳곳에 심어놓는 섬세한 작업은 따라하지 못하여 1:1로 대체가능하다고 말할 수는 없지만, 일단 bitmask의 나열이 좌표값이 차지해야 하는 저장공간을 효과적으로 압축할 수 있다는 점을 확인할 수 있었다.
다시, 전국 인구 1.7MB 로
알고리즘 얘기는 이쯤 해 두고 다시 전국 인구로 돌아오자.
QBTiles의 경우 항상 다음의 순서들로 데이터가 저장된다.
-------------------------------
+ [header] → 128B
-------------------------------
+ [bitmask] → 트리 구조 (어떤 노드가 존재하는지)
+ [run_lengths[]] → 각 노드의 run length. 바다 처럼 비어있는 동일 타일이 등장할 경우 반복되는 회수
+ [lengths[]] → 각 노드의 타일 데이터 크기
+ [offsets[]] → 각 노드의 byte offset (delta encoding)
---- 여기까지 index --------
+ [본 데이터] → 비트마스크 배열 순서로 이어 붙임
--------------------------------
그런데 만약 모든 타일의 저장공간의 크기가 동일하다면, 헤더에 저장공간 크기를 넣고 bitmask 뒤에 곧바로 본 데이터를 배치할 수 있었다. 인덱스 공간도 삭제해버릴 수 있었다. 1.7MB로 만들어 버린 전국 인구가 바로 그 경우다.
그래서 QBTiles는 3가지 모드를 수용한다. 하나는 PMTiles과 대응하는 타일맵 형식, 두 번째가 바로 여기서 적용한 불규칙 그리드 데이터, 세 번째는 뒤에서 언급할 GeoTIFF 대응 형식이다.
전국 인구는 총인구, 남성, 여성의 3개 컬럼이 필요하다. 그래서 각각의 컬럼을 열 방향으로 varInt를 써서 저장했다.
쿼드키 값을 기준으로 데이터를 정렬하면 인접한 위치의 셀들이 연속된다. 인접한 셀들끼리는 인구 값도 비슷한 경우가 많으므로 열 방향으로 차이만 담게되는 varInt의 압축 효율이 올라간다.
그렇게 해서,
[bitmask] + [총인구(varInt)] + [남성(varInt)] + [여성(varInt)] 이렇게 데이터를 담고 압축했다.
그랬더니 놀랍게도 parquet보다 용량이 절반 가까이 줄어들었다.
그게 바로 한 행 평균 1.9Byte에 전국의 인구, 그것도 3개 속성을 담아낼 수 있던 비결이다.
변환한 과정은 아래 노트에 담았다.
Bitmask as a Data Container - QBTiles
import numpy as np import matplotlib.pyplot as plt # Sort by quadkey for binary search df_sorted = df_pivot.sort_values("quadkey").reset_index(drop=True) qk_arr = np.array(df_sorted["quadkey"].values, dtype=np.int64) val_arr = np.array(df_sorted[["total",
vuski.github.io
아래 데모에서 확인할 수 있다. 데이터는 1초 안에 디코딩되어 deck.gl 레이어에 올라간다.
QBTiles Demo - Population
vuski.github.io

전국 인구의 경우 희소 분포에 해당하므로 QBTiles의 압축 효율이 극대화된다.
여기까지 읽은 사람들이 있다면, 1.7MB에 전국 인구를 넣는다고 해 놓고 정작 다른 설명들을 한참 들었다는 생각이 들 것 같다.
맞다. 사실 이 글의 제목은 낚시다. (그렇다고 거짓도 아니지만)
그리고, 다음부터 나오는 내용이 더 중요하다.
COG (Cloud Optimized GeoTIFF)를 대신하는 포맷으로 확장
여기서 QBTiles 포맷이 담는 범위를 좀 더 넓혀보기로 했다.
COG는 Cloud Optimized GeoTIFF의 약자로, 위성 영상이나 전세계 인구 격자 등에 쓰이는 래스터 이미지 포맷인 GeoTIFF를, 서버 없이 클라우드에 올려놓고 접근하기 위해 만들어진 포맷이다.
예를 들어 전 세계 1km 격자 인구 같은 경우 300MB정도가 되는데, COG 포맷의 경우 전체를 512x512 pixel 로 분할해놓고 사용자가 요청하는 지역만 range request로 받아올 수 있다. 즉, 액티브 서버 없이 클라우드 저장공간에 파일만 올려놓고 부분적으로 접근해서 쓸 수 있다.
전 세계를 격자로 분할했다는 특징은 QBTiles와도 잘 맞아 떨어졌다. 그리고 COG 같은 경우 래스터 이미지인지라 인구가 한 명도 없는 바다와 같은 빈 공간도 빠짐없이 저장할 수 밖에 없는데, QBTiles 같은 경우 존재하는 셀만 저장할 수 있다.
QBTiles는 비트마스크 + 인덱스 + 데이터로 이루어지는데, 이 경우 역시 각 셀에 들어가는 데이터의 크기가 동일하므로 인덱스를 생략할 수 있었다.
아래 이미지는 WorldPop에서 배포하는 전세계 약1km 격자 인구수다. 위경도를 동일한 0.008333도의 크기로 분할하여 사람이 거주한다고 추정되는 격자에 인구를 배분했다.

해상도는 43200 x 17820 = 769,824,000 개의 픽셀 정보를 담고 있다. COG는 이를 512 x 512 pixel 크기로 분할하여 총 84x34개의 타일로 구성하여 각각을 압축한다. 바다처럼 아무것도 없는 공간은 압축 효율이 올라간다. 각 격자 저장 위치에 대한 인덱스를 갖고 있으므로 클라우드 저장공간만으로 서비스가 가능하다. 클라이언트는 처음 인덱스를 요청하고 두 번째 인덱스에서 알게 된 타일의 위치를 부분적으로 요청해서 받아갈 수 있다.
장점 1 : 크기가 작다
그런데 이 전체 공간 중 인구가 있는 셀은 6.9%에 불과했다. 빈 공간이 많으므로 QBTiles의 포맷이 적합하다고 생각했다. 적용해본 결과 276MB는 204MB로 줄어들었다. 물론 데이터 손실은 없다. QBTiles는 range request를 위해 저장 공간을 압축하지 않고 동일한 크기로 유지해야 했으므로 압축할 수 없었다. 따라서 유효한 셀만 저장했지만 512x512 청크 단위로 압축한 COG에 비해서 생각보다 많이 줄어들지는 않았다.
물론 그렇다고 COG의 74%가 결코 적은 양은 아니다. 알고 있는 포맷들로 변환해서 비교한 결과는 아래와 같다.
QBTiles가 가장 작다!

변환한 결과를 뷰어에 얹어봤다. 200MB 파일을 Drag&Drop 해도 순식간에 열린다. 파일을 모두 읽지 않고 줌 레벨과 화면 범위에 따라서만 부분적으로 range request 하듯 읽기 때문이다.

초기에 인덱싱을 위해 추출하는 비트마스크 8.7MB를 통해 줌 레벨 별로 존재하는 모든 셀을 알 수 있으므로, 줌을 해가면서 어디에 데이터가 있는지 효과적으로 파악할 수 있다. 투명한 하늘색 원들이 바로 비트마스크의 위치들을 곧바로 렌더링한 결과다.
장점 2 : 셀 단위로 접근 가능하다.
그리고 QBTiles는 크기만 작은 것이 아니라 셀 단위 접근이 가능하다는 장점이 있다.
통상적인 경우 접근 해상도와 전체 파일 크기는 트레이드 오프 관계지만, QBTiles는 초기 8.7MB의 인덱스를 받는다는 전제로, 1픽셀 단위 접근이 가능하다.
여러 차례의 요청 과정을 COG와 비교해봤다.

우측의 COG는 512 픽셀 단위로 내려받을 수 밖에 없기 때문에 작은 공간을 요청해도 큰 용량의 데이터가 날아온다. 그에 비해 QBTiles는 필요한 데이터만 받으므로 불필요한 트래픽을 발생시키지 않는다. 따라서 대략 5~10회 정도의 요청을 반복하고 나면 QBTiles이 초기에 받은 8.7MB의 인덱스 용량이 상쇄된다.
단, 너무 정직하게 셀 단위로 받으면 quadkey 배열의 z 커브 형상으로 인해 불연속 구간이 많이 생겨난다. 위의 영상에서도 한 셀에 50회 이상의 동시 요청을 보내는 것을 볼 수 있다. 그래서 5000셀 정도를 건너 뛸 때까지는 그냥 한 번에 요청함으로써 100KB 정도의 버리는 부분(물론 클라이언트 캐시됨)이 생겨난 대신 분할 요청수를 1/5 정도 이하로 줄일 수 있었다.
초기에 8.7MB의 비트마스크 인덱스를 받아야 한다는 부분은 단점일 수도 있지만 앞에서 본 것처럼 데이터의 분포를 한 눈에 볼 수 있다는 장점도 된다. 그리고 만약 AWS에서 Lambda + S3의 조합으로 사용하게 된다면 워커 역할을 하는 람다가 인덱스를 쥐게 되므로 사용자는 인덱스 조차 받지 않으면서 단 1회 요청으로 셀 단위로 데이터를 받을 수 있게 된다.
물론 COG도 중간에 워커가 개입하면 1회 요청으로 셀 단위 전송이 가능하다. 그렇지만 람다와 S3사이의 내부 트래픽이 줄어든다는 점을 생각해보면 QBTiles는 서버 인프라 운영 측면에서 이득을 준다.
장점 3 : 쿼드키 비트마스크를 이용한 효율적 탐색
QBTiles의 공간 탐색은 쿼드트리 bitmask를 직접 순회하는 방식이다. 각 노드의 4비트 마스크가 네 자식의 존재 여부를 나타내므로, 요청 범위와 겹치지 않는 사분면은 하위 트리 전체를 한 번의 판단으로 건너뛸 수 있다. 탐색 비용은 전체 데이터 크기가 아니라 실제 방문하는 노드 수에 비례하며, 이는 요청 범위의 크기와 경계에 걸치는 트리 노드에 의해 결정된다.
각 노드에는 하위 트리의 leaf 수가 미리 계산되어 있어, 건너뛴 영역의 셀 수를 즉시 알 수 있다. 이를 통해 leaf의 순번(leaf index)을 누적 계산하고, 이 순번이 곧 파일 내 바이트 오프셋으로 직결된다. 별도의 오프셋 테이블 없이 트리 구조 자체가 인덱스 역할을 하므로, bitmask 하나로 공간 탐색과 데이터 접근이 모두 해결된다.
파이썬의 경우 아래와 같이 이진 탐색을 한다.
shift = 2 * (leaf_zoom - parent_zoom)
qk_min = parent_qk << shift
qk_max = qk_min | ((1 << shift) - 1)
i_start = np.searchsorted(qk_arr, qk_min)
i_end = np.searchsorted(qk_arr, qk_max, side='right')
# → qk_arr[i_start:i_end] is the contiguous subregion
자바스크립트에서는 쿼드트리 탐색을 한다
// 4비트 마스크에서 자식 존재 확인
const mask = nibbles[nodeIdx];
// bbox와 안 겹치면 subtree 통째로 스킵
if (crMax < rowMin || crMin > rowMax || ccMax < colMin || ccMin > colMax) {
leafOffset += subtreeLeaves[ci];
continue;
}
// zoom 도달하면 leaf index 수집
if (childZoom === zoom) {
leafIndices.push(leafOffset);
}
// 아니면 자식으로 내려감
stack.push([ci, childRow, childCol, childZoom, leafOffset, 0]);
극단적 압축 : 99KB 비트마스크만으로 표현하는 전 세계 강수 지도
마지막으로 갈 데 까지 가보자.
NASA GPM IMERG (Integrated Multi-satellite Estimates for Rainfall) 는 강수량에 대한 데이터인데 그 중, [3B-HHR-L.MS.MRG.3IMERG.20260301-S133000-E135959.0810.V07B.30min.numPrecipHalfHour_cog.tif]는 30분동안의 강수 여부 (T/F)를 담고 있다.

즉 한 셀은 0/1으로 표현되어 있으므로, QBTiles에서는 인덱스나 데이터가 필요 없다. 단지 비트마스크만으로 표현가능하다. 게다가 0인 셀(검정색)이 무척 많다.

해상도는 3600x1800. 정직하게 1비트로 배열하면 790KB의 용량이 된다. 우선 이 데이터를 압축된 cog로 변환하면 276KB다.
그리고 비트마스크 배열로만 표현된 QBTiles는.... 99KB에 불과하다.
아래에 이 데이터를 뷰어에 올린 영상이 있다.

Github, NPM, PyPl
파일을 만드는 파이썬 라이브러리와 파일을 읽는 타입스크립트 라이브러리로 만들어서 공개했다.
아래 링크에는 좀 더 상세한 스펙과 벤치마크 및 직접 해볼 수 있는 4개의 데모도 포함되어 있다.
모든 코드는 공개되어 있다.
GitHub - vuski/qbtiles
Contribute to vuski/qbtiles development by creating an account on GitHub.
github.com
QBTiles는 PMTiles의 대안으로 만들었지만, 구현의 디테일이 아직 PMTiles만큼은 안된다. 대용량 파일일 경우 인덱스 분할을 구현하지 못했기 때문이다.
그렇지만 격자 데이터를 표현하는 경우 GeoParquet이나 GeoTIFF의 대안으로는 충분히 쓰일 수 있다고 생각한다. 읽기 쓰기에 필요한 기본적인 함수들은 라이브러리에 구현해놓았다.
그리고 GeoTIFF를 QBTiles로 변환하는 컨버터를 두었다. 파일을 변환한 후 네 번째 데모인 viewer에 drag&drop 해서 확인할 수 있다.
희소 분포일수록 효율이 올라가고, 위성영상처럼 모든 공간이 채워진 경우에는 QBTiles의 용량이 오히려 증가하게 된다. 비트마스크가 모두 1111111... 이기 때문이다.
좌표값 없이 비트마스크의 연속적 배열만으로 격자 좌표를 담아낸다는 점이 QBTiles의 매력이라고 생각한다. 물론 쿼드트리 탐색은 덤이다.
GeoTIFF 처럼 여러 툴과 커뮤니티에서 받쳐주는 포맷이 되는 것은 매우 어려운 일이지만, 분명한 장점이 있는 만큼 한 번 쯤 사용해보시길 권한다.
'Function' 카테고리의 다른 글
| Roaring Bitmap을 사용한 로컬데이터(localdata.kr) 브라우징 최적화 (1) | 2026.03.23 |
|---|---|
| OpenStreetMap과 GTFS 데이터를 이용한 도시 자원 접근성 분석 방법 (1) | 2025.03.27 |
| 전세계 기온 시각화 (2) | 2024.12.02 |
| OD 시각화 6 : OD 시각화 프로토타입 (1) | 2024.10.31 |
| OD 시각화 5 : deck.gl.TripsLayer 의 커스터마이징 (0) | 2024.10.30 |