GeoContourMaker
Сервис извлечения векторных контуров из растровых GPKG-файлов геосъёмки. Принимает имя GPKG → строит bitmap покрытия тайлами → трассирует контур алгоритмом Potrace → возвращает GeoJSON в EPSG:4326.
Архитектура
GeoContourMaker состоит из 4 компонентов:
- 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
Жизненный цикл задачи
POST /api/v1/tasksgpkg_name + параметры
status:
queuedstatus:
runningstatus:
successКлючевые принципы
- Воркеры не имеют доступа к 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).
Не зависит от содержимого растра — трассируется именно область покрытия тайлами.
Пайплайн
- Чтение индекса тайлов — SQLite запрос к GPKG: позиции тайлов (zoom, col, row)
- Bitmap покрытия — 1 пиксель = 1 тайл (есть/нет). Размер: grid_w × grid_h
- Upscale — бикубическая интерполяция до
upscale_sizeпикселей (cv2.INTER_CUBIC) + пороговое значение 128 - Blur (опц.) — Gaussian blur для сглаживания bitmap (
blur_sigma) - Potrace — трассировка контура Безье-кривыми (CLI:
potrace --svg) - Гео-трансформация — пересчёт координат из pixel space в CRS тайловой сетки
- Simplify / Buffer — опциональное упрощение и сглаживание геометрии
- Репроекция — перевод в 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}
После запуска доступны:
Создать задачу
/api/v1/tasksСоздать задачу извлечения контура из GPKG-файла. Задача ставится в очередь Redis и обрабатывается воркером.
Параметры запроса (JSON body)
| Параметр | Тип | Обяз. | По умолч. | Описание |
|---|---|---|---|---|
gpkg_name | string | ✓ | — | Имя GPKG-файла (без расширения). Ищется в GPKG_DIR |
turdsize | int | 2 | Подавление мелких пятен до N тайлов | |
alphamax | float | 1.0 | Округление углов: 0 = острые углы, 1.334 = максимальное сглаживание | |
opttolerance | float | 0.2 | Допуск оптимизации кривых Potrace | |
blur_sigma | float | 0.0 | Sigma Gaussian blur на bitmap (0 = выкл.) | |
threshold | int | 128 | Порог бинаризации после blur (0-255) | |
simplify_tolerance | float | 0.0 | Упрощение геометрии в единицах CRS (0 = выкл.) | |
buffer_smooth | float | 0.0 | Buffer-сглаживание: buffer(+d).buffer(-d) в ед. CRS | |
upscale_size | int | 8000 | Размер bitmap после upscale (0 = без upscale) | |
zoom_level | int | 0 | Zoom 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}')
Получить задачу
/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")
Список задач
/api/v1/tasks?page=1&per_page=50Пагинированный список задач (newest first).
| Параметр | Тип | По умолч. | Описание |
|---|---|---|---|
page | int | 1 | Номер страницы (от 1) |
per_page | int | 50 | Элементов на странице (1-200) |
{
"tasks": [...],
"total": 142,
"page": 1,
"per_page": 50,
"pages": 3
}Удалить ошибочные / завершённые
/api/v1/tasks/erroredУдалить все задачи со статусом error.
/api/v1/tasks/completedУдалить все задачи со статусом success.
{"deleted": 15}Глубина очереди
/api/v1/queueТекущее количество задач в очереди.
{"depth": 3}Список GPKG файлов
/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
/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"
}| Поле | Тип | Описание |
|---|---|---|
name | string | Имя GPKG |
min_zoom | int | Минимальный zoom level |
max_zoom | int | Максимальный zoom level |
zoom_levels | int[] | Все доступные zoom levels |
tile_path | string | Путь к файлу для tiles.service.levkona.ru |
Tile-метаданные (прокси)
/api/v1/tile-metadata?file={path}Прокси к tiles.service.levkona.ru/metadata для обхода CORS.
Возвращает полные метаданные файла: CRS, экстент, center, zoom levels.
| Параметр | Тип | Описание |
|---|---|---|
file | string (query) | Абсолютный путь к GPKG на tile-сервере |
API — Воркеры
/api/v1/workersСписок всех зарегистрированных воркеров с текущим статусом.
/api/v1/workers/{worker_id}Состояние конкретного воркера.
/api/v1/workers/{worker_id}Обновить конфиг воркера. Body: {"enabled": false} — отключить воркер.
/api/v1/workers/{worker_id}Удалить воркер из реестра. 204 No Content.
Логи воркеров
/api/v1/workers/{worker_id}/logs?lines=200Последние N строк лога конкретного воркера (фильтр по hostname).
| Параметр | Тип | По умолч. | Описание |
|---|---|---|---|
lines | int | 200 | Количество строк (1-5000) |
/api/v1/workers/{worker_id}/logs/streamSSE-стрим новых строк лога в реальном времени (text/event-stream).
Настройки
/api/v1/settingsТекущие настройки (merged с defaults).
/api/v1/settingsDeep merge update настроек.
Вебхуки
/api/v1/webhooksСписок вебхуков (секреты скрыты).
/api/v1/webhooksСоздать вебхук. Body: {"url": "https://...", "secret": "optional"}
/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_size | turdsize | alphamax | opttolerance | Характеристика |
|---|---|---|---|---|---|
| ⚡ Черновик | 0 | 10 | 1.334 | 1.0 | Максимальная скорость, грубый контур |
| 🚀 Быстро | 2000 | 5 | 1.0 | 0.5 | Быстрая обработка, приемлемое качество |
| ⚖ Стандарт | 4000 | 2 | 1.0 | 0.2 | Баланс скорости и качества |
| 🎯 Точный | 8000 | 2 | 0.8 | 0.15 | Высокая точность, больше вершин |
| 📐 1 в 1 | 8000 | 0 | 0 | 0 | Максимальная точность, следование контуру |
Параметры 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/tasksLive-события задач. При создании или обновлении задачи сервер пушит 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/workersLive-состояние воркеров. При подключении отправляет текущий список всех воркеров, затем пушит обновления при 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_app | Dockerfile | FastAPI сервер (:8000) |
gcm_nginx | nginx/Dockerfile | Reverse proxy (:80 → :8080) |
gcm_redis | redis:7-alpine | Очередь + состояние |
gcm-worker-N | Dockerfile.worker | Воркер с GDAL + OpenCV + Potrace |
Переменные окружения
| Переменная | По умолч. | Описание |
|---|---|---|
SERVICE_NAME | GeoContourMaker | Название сервиса (UI header, footer) |
SERVICE_LOGO | 🗺 | Символ/эмодзи в header |
SERVICE_DESCRIPTION | Извлечение векторных... | Описание в footer |
SERVICE_VERSION | 1.0.0 | Версия сервиса |
REDIS_URL | redis://localhost:6379/0 | Подключение к Redis |
REDIS_PREFIX | gcm | Префикс Redis keys |
DATA_DIR | /data | Корневая директория данных |
GPKG_DIR | /mnt/data/GPKG | Директория с GPKG файлами |
SERVER_URL | http://app:8000 | URL сервера для воркеров |
HOST | 0.0.0.0 | Bind адрес |
PORT | 8000 | Порт FastAPI |
LOG_LEVEL | info | Уровень логирования |
CLEANUP_INTERVAL_MINUTES | 30 | Интервал очистки (мин) |
CLEANUP_TTL_HOURS | 24 | TTL завершённых задач (часы) |
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.yaml | Namespace geocontour |
configmap.yaml | Переменные окружения |
pvc.yaml | PVC shared-data (10Gi, ReadWriteMany) |
redis.yaml | Redis Deployment + Service |
app.yaml | FastAPI Deployment + Service |
worker.yaml | Worker Deployment |
nginx.yaml | Nginx Deployment + Service |
ingress.yaml | Ingress для contours.service.levkona.ru |
hpa.yaml | HPA: 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/dequeue | Long-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-документация:
- Swagger UI —
/docs - ReDoc —
/redoc - OpenAPI JSON —
/openapi.json
Health Check
/healthПроверка работоспособности. Используется Docker healthcheck и Kubernetes probes.
{"status": "ok"}