GeoContourMaker

Сервис извлечения векторных контуров из растровых GPKG-файлов геосъёмки. Принимает имя GPKG → строит bitmap покрытия тайлами → трассирует контур алгоритмом Potrace → возвращает GeoJSON в EPSG:4326.

Полностью автономный сервис. Единственные зависимости — Redis (очередь + состояние) и файлы GPKG на диске. Не требует внешних БД, Celery или брокеров.

Архитектура

GeoContourMaker состоит из 4 компонентов:

🌐
Клиент
HTTP / WebSocket
HTTP
Nginx
:80
reverse proxy + WS upgrade
proxy
🚀
FastAPI (app)
:8000
REST API + Web UI
queue / state
Redis
:6379
Queue + State + PubSub
HTTP long-poll
🔧
Worker ×N
:9100
GDAL + OpenCV + Potrace
read
📁
/mnt/data/GPKG
GPKG файлы
  • Nginx — reverse proxy, WebSocket upgrade, кэш статики
  • FastAPI (app) — REST API, Web UI (Jinja2), WebSocket live-feed, очередь задач
  • Redis — очередь (LPUSH/BRPOP), состояние задач (Sorted Set), PubSub уведомлений
  • Worker ×N — воркеры с GDAL, OpenCV, Potrace. Общение с сервером только через HTTP

Жизненный цикл задачи

1
Запрос
POST /api/v1/tasks
gpkg_name + параметры
2
Очередь
Redis LPUSH
status: queued
3
Worker
Dequeue + потрейс
status: running
4
Контур
Bitmap → Upscale → Potrace → GeoJSON
5
Результат
GeoJSON файл
status: success
6
Уведомление
WebSocket + Webhook

Ключевые принципы

  • Воркеры не имеют доступа к Redis — вся коммуникация через HTTP API сервера
  • Sorted Set для индекса задач — O(log N) пагинация, без SCAN
  • Heartbeat каждые 10 сек, TTL 30 сек — воркер считается offline после 20 сек тишины
  • Stale recovery — задачи давнее 5 мин в running автоматически → error
  • Горизонтальное масштабирование: docker compose up -d --scale worker=5

Алгоритм извлечения контура

Контур строится по индексу тайлов в GPKG-файле (таблица tiles). Не зависит от содержимого растра — трассируется именно область покрытия тайлами.

Пайплайн

  1. Чтение индекса тайлов — SQLite запрос к GPKG: позиции тайлов (zoom, col, row)
  2. Bitmap покрытия — 1 пиксель = 1 тайл (есть/нет). Размер: grid_w × grid_h
  3. Upscale — бикубическая интерполяция до upscale_size пикселей (cv2.INTER_CUBIC) + пороговое значение 128
  4. Blur (опц.) — Gaussian blur для сглаживания bitmap (blur_sigma)
  5. Potrace — трассировка контура Безье-кривыми (CLI: potrace --svg)
  6. Гео-трансформация — пересчёт координат из pixel space в CRS тайловой сетки
  7. Simplify / Buffer — опциональное упрощение и сглаживание геометрии
  8. Репроекция — перевод в EPSG:4326 (WGS 84)

Upscale-механизм

Bitmap покрытия имеет разрешение 1 пиксель = 1 тайл. При прямой трассировке контур повторяет границы тайлов (ступеньки). Upscale с бикубической интерполяцией создаёт сглаженные антиалиасные края, которые Potrace трассирует как плавные кривые — даже при alphamax=0.

upscale_size задаёт максимальную сторону bitmap после масштабирования. Больше = точнее контур, больше вершин, дольше обработка.

Быстрый старт

# Запустить сервис
docker compose up -d --build

# Проверить
curl http://localhost:8080/health

# Список доступных GPKG
curl http://localhost:8080/api/v1/gpkg

# Создать задачу извлечения контура
curl -X POST http://localhost:8080/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{"gpkg_name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm"}'

# Получить результат
curl http://localhost:8080/api/v1/tasks/{task_id}

После запуска доступны:

  • Задачи — создание и мониторинг задач
  • Воркеры — состояние воркеров в реальном времени
  • Настройки — конфигурация параметров по умолчанию
  • Swagger — интерактивная API-документация

Создать задачу

POST /api/v1/tasks

Создать задачу извлечения контура из GPKG-файла. Задача ставится в очередь Redis и обрабатывается воркером.

Параметры запроса (JSON body)

ПараметрТипОбяз.По умолч.Описание
gpkg_namestringИмя GPKG-файла (без расширения). Ищется в GPKG_DIR
turdsizeint2Подавление мелких пятен до N тайлов
alphamaxfloat1.0Округление углов: 0 = острые углы, 1.334 = максимальное сглаживание
opttolerancefloat0.2Допуск оптимизации кривых Potrace
blur_sigmafloat0.0Sigma Gaussian blur на bitmap (0 = выкл.)
thresholdint128Порог бинаризации после blur (0-255)
simplify_tolerancefloat0.0Упрощение геометрии в единицах CRS (0 = выкл.)
buffer_smoothfloat0.0Buffer-сглаживание: buffer(+d).buffer(-d) в ед. CRS
upscale_sizeint8000Размер bitmap после upscale (0 = без upscale)
zoom_levelint0Zoom level тайловой пирамиды (0 = максимальный доступный)

Ответ — 202 Accepted

{
  "task_id": "a1b2c3d4-...",
  "status": "queued",
  "gpkg_name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
  "queue_depth": 0
}

Примеры

curl -X POST https://contours.service.levkona.ru/api/v1/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "gpkg_name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
    "upscale_size": 8000,
    "turdsize": 2,
    "alphamax": 1.0,
    "opttolerance": 0.2
  }'
const resp = await fetch('https://contours.service.levkona.ru/api/v1/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    gpkg_name: 'GTSAR_F_2023.06.20_16_1_6_SA6000_20mm',
    upscale_size: 8000,
    turdsize: 2,
    alphamax: 1.0,
    opttolerance: 0.2
  })
});
const { task_id } = await resp.json();
console.log('Task created:', task_id);
import httpx

resp = httpx.post(
    'https://contours.service.levkona.ru/api/v1/tasks',
    json={
        'gpkg_name': 'GTSAR_F_2023.06.20_16_1_6_SA6000_20mm',
        'upscale_size': 8000,
        'turdsize': 2,
        'alphamax': 1.0,
        'opttolerance': 0.2,
    },
)
task_id = resp.json()['task_id']
print(f'Task created: {task_id}')

Получить задачу

GET /api/v1/tasks/{task_id}

Возвращает текущее состояние задачи.

Ответ — задача в обработке

{
  "id": "a1b2c3d4-...",
  "status": "running",
  "gpkg_name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
  "created_at": 1719900000,
  "params": {
    "turdsize": 2,
    "alphamax": 1.0,
    "upscale_size": 8000
  }
}

Ответ — успешное завершение

{
  "id": "a1b2c3d4-...",
  "status": "success",
  "gpkg_name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
  "created_at": 1719900000,
  "duration_ms": 1200,
  "result_file": "/data/results/a1b2c3d4-.../contour.geojson",
  "feature_count": 1,
  "vertex_count": 9971,
  "params": {"turdsize": 2, "alphamax": 1.0, "upscale_size": 8000}
}

Ответ — ошибка

{
  "id": "a1b2c3d4-...",
  "status": "error",
  "gpkg_name": "INVALID_NAME",
  "error": "GPKG not found: INVALID_NAME in /mnt/data/GPKG",
  "duration_ms": 15
}

Примеры

curl https://contours.service.levkona.ru/api/v1/tasks/a1b2c3d4-...
const resp = await fetch('https://contours.service.levkona.ru/api/v1/tasks/a1b2c3d4-...');
const task = await resp.json();
if (task.status === 'success') {
  console.log(`Contour: ${task.vertex_count} vertices, ${task.duration_ms}ms`);
}
resp = httpx.get('https://contours.service.levkona.ru/api/v1/tasks/a1b2c3d4-...')
task = resp.json()
if task['status'] == 'success':
    print(f"Contour: {task['vertex_count']} vertices")

Список задач

GET /api/v1/tasks?page=1&per_page=50

Пагинированный список задач (newest first).

ПараметрТипПо умолч.Описание
pageint1Номер страницы (от 1)
per_pageint50Элементов на странице (1-200)
{
  "tasks": [...],
  "total": 142,
  "page": 1,
  "per_page": 50,
  "pages": 3
}

Удалить ошибочные / завершённые

DELETE /api/v1/tasks/errored

Удалить все задачи со статусом error.

DELETE /api/v1/tasks/completed

Удалить все задачи со статусом success.

{"deleted": 15}

Глубина очереди

GET /api/v1/queue

Текущее количество задач в очереди.

{"depth": 3}

Список GPKG файлов

GET /api/v1/gpkg

Возвращает список доступных GPKG-файлов из директории GPKG_DIR.

Находит как файлы в поддиректориях (name/name.gpkg), так и «голые» файлы (name.gpkg).

[
  "GTMSK_F_2023.01.21_1.6-1.9_1_2_SA6000_20mm",
  "GTMSK_F_2023.02.22_1.6-1.9_1_2_SA6000_20mm",
  "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
  "GTVOL_F_2023.05.20_2.6-2.7_1_8_SA6000_20mm"
]

Примеры

curl https://contours.service.levkona.ru/api/v1/gpkg
const files = await fetch('https://contours.service.levkona.ru/api/v1/gpkg')
  .then(r => r.json());
console.log(`Available: ${files.length} GPKG files`);
resp = httpx.get('https://contours.service.levkona.ru/api/v1/gpkg')
files = resp.json()
print(f"Available: {len(files)} GPKG files")

Метаданные GPKG

GET /api/v1/gpkg/{name}/meta

Возвращает метаданные тайловой пирамиды: доступные zoom-уровни, путь к файлу для tile-сервера.

{
  "name": "GTSAR_F_2023.06.20_16_1_6_SA6000_20mm",
  "min_zoom": 8,
  "max_zoom": 21,
  "zoom_levels": [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
  "tile_path": "/data/GTSAR_F_2023.06.20_16_1_6_SA6000_20mm.gpkg"
}
ПолеТипОписание
namestringИмя GPKG
min_zoomintМинимальный zoom level
max_zoomintМаксимальный zoom level
zoom_levelsint[]Все доступные zoom levels
tile_pathstringПуть к файлу для tiles.service.levkona.ru

Tile-метаданные (прокси)

GET /api/v1/tile-metadata?file={path}

Прокси к tiles.service.levkona.ru/metadata для обхода CORS. Возвращает полные метаданные файла: CRS, экстент, center, zoom levels.

ПараметрТипОписание
filestring (query)Абсолютный путь к GPKG на tile-сервере

API — Воркеры

GET /api/v1/workers

Список всех зарегистрированных воркеров с текущим статусом.

GET /api/v1/workers/{worker_id}

Состояние конкретного воркера.

PUT /api/v1/workers/{worker_id}

Обновить конфиг воркера. Body: {"enabled": false} — отключить воркер.

DELETE /api/v1/workers/{worker_id}

Удалить воркер из реестра. 204 No Content.

Логи воркеров

GET /api/v1/workers/{worker_id}/logs?lines=200

Последние N строк лога конкретного воркера (фильтр по hostname).

ПараметрТипПо умолч.Описание
linesint200Количество строк (1-5000)
GET /api/v1/workers/{worker_id}/logs/stream

SSE-стрим новых строк лога в реальном времени (text/event-stream).

Настройки

GET /api/v1/settings

Текущие настройки (merged с defaults).

PUT /api/v1/settings

Deep merge update настроек.

Вебхуки

GET /api/v1/webhooks

Список вебхуков (секреты скрыты).

POST /api/v1/webhooks

Создать вебхук. Body: {"url": "https://...", "secret": "optional"}

DELETE /api/v1/webhooks/{webhook_id}

Удалить вебхук. 204 No Content.

Формат callback

При завершении задачи (success/error) сервер отправляет POST на URL вебхука с телом задачи. Если задан secret — заголовок X-Webhook-Signature: sha256=<hmac_hex>.

# Проверка подписи (Python)
import hmac, hashlib

expected = hmac.new(
    secret.encode(), body_bytes, hashlib.sha256
).hexdigest()
assert signature == f'sha256={expected}'

Пресеты качества

Предустановленные наборы параметров для быстрого выбора в UI. Влияют на детализацию контура и скорость обработки.

Пресетupscale_sizeturdsizealphamaxopttoleranceХарактеристика
⚡ Черновик0101.3341.0Максимальная скорость, грубый контур
🚀 Быстро200051.00.5Быстрая обработка, приемлемое качество
⚖ Стандарт400021.00.2Баланс скорости и качества
🎯 Точный800020.80.15Высокая точность, больше вершин
📐 1 в 18000000Максимальная точность, следование контуру

Параметры Potrace — справочник

ПараметрДиапазонПо умолч.Описание
turdsize 0 — 100 2 Подавление мелких «островков» площадью до N пикселей. Увеличение убирает шум, но может потерять мелкие детали.
alphamax 0.0 — 1.334 1.0 Контроль углов: 0 = только острые углы (прямые линии), 1.334 = максимальное скругление. Для «1 в 1» используйте 0.
opttolerance 0.0 — 5.0 0.2 Допуск оптимизации Безье-кривых. Больше = меньше вершин, грубее кривая. 0 = без оптимизации.
blur_sigma 0.0 — 10.0 0.0 Sigma Gaussian blur на bitmap перед трассировкой. Сглаживает ступеньки тайлов. 0 = без blur.
threshold 0 — 255 128 Порог бинаризации bitmap после blur/upscale. Значения ≥ threshold → белый (тайл есть).
simplify_tolerance 0.0 — ∞ 0.0 Дуглас-Пейкер упрощение в единицах CRS. Уменьшает количество вершин. 0 = без упрощения.
buffer_smooth 0.0 — ∞ 0.0 Buffer-сглаживание: buffer(+d).buffer(-d). Убирает мелкие неровности. Значение в ед. CRS.
upscale_size 0 — 16000 8000 Максимальная сторона bitmap после масштабирования. 0 = без upscale (1px = 1 тайл). Больше = точнее контур.
zoom_level 0 — 25 0 Zoom level тайловой пирамиды. 0 = автовыбор максимального. Меньший zoom = быстрее, грубее.

WebSocket — Задачи

WS /ws/tasks

Live-события задач. При создании или обновлении задачи сервер пушит JSON в WebSocket. Используется для обновления UI в реальном времени без polling.

// JavaScript
const ws = new WebSocket('wss://contours.service.levkona.ru/ws/tasks');
ws.onmessage = (event) => {
    const task = JSON.parse(event.data);
    console.log(`Task ${task.id}: ${task.status}`);
    if (task.status === 'success') {
        console.log(`Vertices: ${task.vertex_count}, Time: ${task.duration_ms}ms`);
    }
};

WebSocket — Воркеры

WS /ws/workers

Live-состояние воркеров. При подключении отправляет текущий список всех воркеров, затем пушит обновления при heartbeat.

// JavaScript
const ws = new WebSocket('wss://contours.service.levkona.ru/ws/workers');
ws.onmessage = (event) => {
    const workers = JSON.parse(event.data);
    console.log(`Online workers: ${workers.length}`);
};

Docker Compose

# Полный запуск
docker compose up -d --build

# Пересборка воркеров (после обновления алгоритма)
docker compose build worker
docker compose up -d worker

# Масштабирование воркеров
docker compose up -d --scale worker=5

# Логи
docker compose logs -f worker

# Остановка
docker compose down

Контейнеры

КонтейнерОбразОписание
gcm_appDockerfileFastAPI сервер (:8000)
gcm_nginxnginx/DockerfileReverse proxy (:80 → :8080)
gcm_redisredis:7-alpineОчередь + состояние
gcm-worker-NDockerfile.workerВоркер с GDAL + OpenCV + Potrace

Переменные окружения

ПеременнаяПо умолч.Описание
SERVICE_NAMEGeoContourMakerНазвание сервиса (UI header, footer)
SERVICE_LOGO🗺Символ/эмодзи в header
SERVICE_DESCRIPTIONИзвлечение векторных...Описание в footer
SERVICE_VERSION1.0.0Версия сервиса
REDIS_URLredis://localhost:6379/0Подключение к Redis
REDIS_PREFIXgcmПрефикс Redis keys
DATA_DIR/dataКорневая директория данных
GPKG_DIR/mnt/data/GPKGДиректория с GPKG файлами
SERVER_URLhttp://app:8000URL сервера для воркеров
HOST0.0.0.0Bind адрес
PORT8000Порт FastAPI
LOG_LEVELinfoУровень логирования
CLEANUP_INTERVAL_MINUTES30Интервал очистки (мин)
CLEANUP_TTL_HOURS24TTL завершённых задач (часы)
WORKER_ID(auto)ID воркера (если не задан — hostname-pid)

Kubernetes

Манифесты в директории k8s/:

# Применить все манифесты
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/

# Проверить
kubectl -n geocontour get pods
kubectl -n geocontour get hpa
ФайлОписание
namespace.yamlNamespace geocontour
configmap.yamlПеременные окружения
pvc.yamlPVC shared-data (10Gi, ReadWriteMany)
redis.yamlRedis Deployment + Service
app.yamlFastAPI Deployment + Service
worker.yamlWorker Deployment
nginx.yamlNginx Deployment + Service
ingress.yamlIngress для contours.service.levkona.ru
hpa.yamlHPA: 1-5 реплик воркеров, CPU 60%

Обработка ошибок

HTTP кодОписание
200Успешный ответ
202Задача принята в очередь
204Удаление выполнено (без тела ответа)
400Отсутствует обязательный параметр (gpkg_name)
404Задача / воркер / GPKG / вебхук не найден
500Внутренняя ошибка сервера

Worker Internal API

Внутренние эндпоинты для коммуникации воркер → сервер. Не предназначены для клиентов.

МетодURLОписание
POST/api/worker/registerРегистрация воркера при старте
POST/api/worker/heartbeatПульс (каждые 10 сек) — статус, текущая задача, счётчики
POST/api/worker/dequeueLong-poll: получить ID задачи из очереди (timeout до 30 сек)
GET/api/worker/payload/{task_id}Получить полное тело задачи (параметры)
POST/api/worker/task/{task_id}Отчёт о выполнении: status, duration_ms, result_file, error

Swagger / ReDoc

Интерактивная API-документация:

Health Check

GET /health

Проверка работоспособности. Используется Docker healthcheck и Kubernetes probes.

{"status": "ok"}
GeoContourMaker v1.0 — Извлечение векторных контуров из растровых GPKG