Segmentantor
Универсальный микросервис сегментации изображений. Подключай любую ONNX-модель (SAM, YOLO, SegFormer…) — получай контуры объектов через единый HTTP API.
Архитектура
Segmentantor включает 4 контейнера:
Базовый URL: http://<host>:8090
Поток данных
- Клиент отправляет
POST /segmentс URL тайла и координатами клика - Segmentantor загружает изображение (по URL или из base64)
- Выбранный процессор (SAM по умолчанию) выполняет инференс
- Бинарная маска конвертируется в полигон (контур)
- Контур возвращается клиенту в пиксельных координатах
Жизненный цикл задачи
queued
BRPOPprocessing
success / error
Статусы задачи
| Статус | Описание |
|---|---|
queued | В очереди, ожидает обработки |
processing | Воркер обрабатывает |
success | Успешно, результат доступен |
error | Ошибка при обработке |
Быстрый старт
1. Скачать модели
bash scripts/download_sam.sh
2. Запустить
docker compose up -d
3. Проверить
curl -X POST http://localhost:8090/segment \
-H "Content-Type: application/json" \
-d '{"image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "click_x": 128, "click_y": 100}'POST /segment
/segment
Основной эндпоинт сегментации. Принимает изображение (URL или base64) и координаты, возвращает контур объекта.
Параметры (JSON body)
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
image_url | string | * | URL изображения. Абсолютный или относительный (резолвится через UPSTREAM_BASE_URL). |
image_b64 | string | * | Изображение в формате base64 (PNG/JPEG). Альтернатива image_url. |
click_x | float | ** | X-координата клика в пикселях (legacy, используйте points). |
click_y | float | ** | Y-координата клика в пикселях (legacy, используйте points). |
points | array | ** | Массив точек [{x, y, label}]. label: 1 = foreground, 0 = background. |
box | object | ** | Ограничивающий прямоугольник {x1, y1, x2, y2}. Можно совмещать с points. |
processor | string | Имя процессора: "sam", "yolo", … По умолчанию из конфига. |
|
cache_key | string | Ключ кэша эмбеддингов. Ускоряет повторные клики по одному тайлу. | |
simplify_tolerance | float | Допуск упрощения контура (Douglas-Peucker), пиксели. По умолчанию: 2.0. |
|
callback_url | string | URL для POST-уведомления при завершении задачи. Разовый — только для этой задачи. |
image_url или image_b64.** Обязателен хотя бы один промпт:
click_x+click_y, points или box.
Ответ (202 Accepted)
{
"task_id": "a1b2c3d4e5f6",
"status": "queued",
"queue_depth": 0
}Параметры результата (после обработки)
| Поле | Тип | Описание |
|---|---|---|
contour | float[][] | Массив точек [[x, y], ...] в пиксельных координатах. |
result_score | float | Уверенность модели (IoU score). |
mask_width | int | Ширина исходного изображения. |
mask_height | int | Высота исходного изображения. |
result_processor | string | Имя процессора. |
duration_ms | float | Время обработки в миллисекундах. |
Коды ответов
| Код | Описание |
|---|---|
202 | Задача поставлена в очередь. |
422 | Невалидные параметры (Pydantic validation). |
Примеры кода
# Одна точка (legacy) curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{ "image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "click_x": 128, "click_y": 100 }' # Несколько точек + box + callback curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{ "image_url": "/api/gpkg/file.gpkg/tiles/17/1234/5678/", "points": [ {"x": 128, "y": 100, "label": 1}, {"x": 50, "y": 50, "label": 0} ], "box": {"x1": 30, "y1": 30, "x2": 200, "y2": 200}, "callback_url": "https://my-server.com/webhook" }'
const resp = await fetch('/segment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_url: '/api/gpkg/map.gpkg/tiles/17/1234/5678/', points: [{ x: 128, y: 100, label: 1 }], cache_key: '17/1234/5678', callback_url: 'https://my-server.com/webhook', }), }); const { task_id, status, queue_depth } = await resp.json();
import requests resp = requests.post('http://localhost:8090/segment', json={ 'image_url': '/api/gpkg/file.gpkg/tiles/17/1234/5678/', 'points': [{'x': 128, 'y': 100, 'label': 1}], 'processor': 'sam', 'cache_key': 'tile-17-1234-5678', 'callback_url': 'https://my-server.com/webhook', }) print(resp.json()) # {"task_id": "...", "status": "queued", "queue_depth": 0}
GET /models
/models
Список зарегистрированных процессоров и их статус.
{
"processors": [
{ "name": "sam", "ready": true, "description": "Segment Anything Model (ViT-B, ONNX quantized)" }
],
"default": "sam"
}GET /health
/health
Healthcheck. Возвращает {"status": "ok"}.
WS /ws/tasks
/ws/tasks
WebSocket стрим событий задач. Каждое сообщение — JSON задачи при смене статуса.
Формат сообщения
{
"id": "a1b2c3d4e5f6",
"created_at_iso": "2026-03-27 18:00:00",
"processor": "sam",
"status": "success",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952
}Пример подключения
const ws = new WebSocket('ws://localhost:8090/ws/tasks'); ws.onmessage = (e) => { const task = JSON.parse(e.data); console.log(`[${task.status}] ${task.id} — ${task.duration_ms?.toFixed(0)}ms`); };
GET /api/tasks
/api/tasks
Список задач с пагинацией (новейшие первыми).
Параметры (query string)
| Поле | Тип | По умолч. | Описание |
|---|---|---|---|
page | int | 1 | Номер страницы (1-based) |
per_page | int | 50 | Кол-во записей (1–200) |
Ответ
{
"tasks": [
{
"id": "a1b2c3d4e5f6",
"created_at_iso": "2026-03-28 12:00:00",
"processor": "sam",
"status": "success",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952
}
],
"total": 128,
"page": 1,
"per_page": 50,
"pages": 3
}Примеры кода
curl "http://localhost:8090/api/tasks?page=1&per_page=20"
import requests resp = requests.get('http://localhost:8090/api/tasks', params={'page': 1, 'per_page': 20}) data = resp.json() print(f"Всего задач: {data['total']}, страниц: {data['pages']}") for task in data['tasks']: print(f" [{task['status']}] {task['id']} — {task.get('duration_ms', '?')}ms")
GET /api/tasks/{'{task_id}'}
/api/tasks/{'{task_id}'}
Статус и результат конкретной задачи.
Ответ (задача завершена)
{
"id": "a1b2c3d4e5f6",
"status": "success",
"processor": "sam",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952,
"result_processor": "sam",
"contour": [[102.0, 85.0], [115.0, 82.0], ...],
"mask_width": 256,
"mask_height": 256,
"preview_input": "/data/previews/a1b2c3d4e5f6_input.png",
"preview_result": "/data/previews/a1b2c3d4e5f6_result.png"
}Примеры кода
curl http://localhost:8090/api/tasks/a1b2c3d4e5f6
import requests resp = requests.get('http://localhost:8090/api/tasks/a1b2c3d4e5f6') if resp.status_code == 404: print('Задача не найдена') else: task = resp.json() print(f"Статус: {task['status']}, вершин: {task.get('result_vertices')}")
Настройки
Настройки по умолчанию для процессоров. Применяются, когда параметры не переданы в запросе.
/api/settings
Получить текущие настройки.
/api/settings
Обновить настройки (deep-merge). Передайте только изменённые поля.
Схема настроек
{
"enabled_processors": ["sam", "yolo"],
"use_gpu": false,
"default_processor": "sam",
"sam": {
"embedding_cache_size": 16,
"simplify_tolerance": 2.0,
"ort_inter_threads": 2,
"ort_intra_threads": 4
},
"yolo": {
"conf_threshold": 0.25,
"iou_threshold": 0.7
}
}Примеры кода
# Получить curl http://localhost:8090/api/settings # Обновить SAM tolerance curl -X PUT http://localhost:8090/api/settings \ -H "Content-Type: application/json" \ -d '{"sam": {"simplify_tolerance": 1.5}}'
import requests # Получить settings = requests.get('http://localhost:8090/api/settings').json() # Обновить updated = requests.put('http://localhost:8090/api/settings', json={ 'sam': {'simplify_tolerance': 1.5} }).json()
Вебхуки
Вебхуки позволяют получать уведомления о завершении задач без поллинга. При регистрации указывается URL, на который будет отправлен POST-запрос с результатом.
Два механизма уведомлений:
- Persistent webhooks — зарегистрированные через API, получают ВСЕ результаты
callback_url— разовый, передаётся в запросеPOST /segment, только для этой задачи
CRUD
/api/webhooks
Зарегистрировать вебхук. Тело — JSON.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
url | string | ✓ | URL для POST-уведомлений |
secret | string | Секрет для HMAC-SHA256 подписи |
/api/webhooks
Список всех зарегистрированных вебхуков (секреты замаскированы).
/api/webhooks/{'{webhook_id}'}
Удалить вебхук по ID.
Payload вебхука
При завершении задачи (статус success или error) на все зарегистрированные URL отправляется POST:
POST https://your-server.com/callback
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4...
{
"id": "a1b2c3d4e5f6",
"status": "success",
"processor": "sam",
"duration_ms": 1523.4,
"result_vertices": 42,
"result_score": 0.952,
"contour": [[102.0, 85.0], [115.0, 82.0], ...],
"mask_width": 256,
"mask_height": 256
}Подпись HMAC-SHA256
Если при регистрации указан secret, заголовок X-Webhook-Signature содержит HMAC-SHA256 хеш тела.
import hmac, hashlib def verify_signature(body: bytes, secret: str, signature: str) -> bool: expected = "sha256=" + hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)
const crypto = require('crypto'); function verifySignature(body, secret, signature) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }
callback_url (per-request)
Разовый вебхук — передаётся в POST /segment. Вызывается один раз при завершении только этой задачи.
Формат POST-запроса идентичен persistent-вебхукам, но без X-Webhook-Signature.
Полный пример
# 1. Зарегистрировать вебхук curl -X POST http://localhost:8090/api/webhooks \ -H "Content-Type: application/json" \ -d '{"url": "https://my-server.com/callback", "secret": "my-key"}' # Ответ: {"id": "wh_abc123...", "url": "...", "secret": "***", "created_at": "..."} # 2. Отправить задачу curl -X POST http://localhost:8090/segment \ -H "Content-Type: application/json" \ -d '{"image_url": "/tiles/17/1234/5678/", "click_x": 128, "click_y": 100}' # Результат придёт POST-запросом на https://my-server.com/callback # 3. Посмотреть вебхуки curl http://localhost:8090/api/webhooks # 4. Удалить curl -X DELETE http://localhost:8090/api/webhooks/wh_abc123
import requests BASE = 'http://localhost:8090' # 1. Зарегистрировать hook = requests.post(f'{BASE}/api/webhooks', json={ 'url': 'https://my-server.com/callback', 'secret': 'my-key', }).json() print(hook) # {"id": "wh_...", "url": "...", ...} # 2. Отправить задачу task = requests.post(f'{BASE}/segment', json={ 'image_url': '/tiles/17/1234/5678/', 'click_x': 128, 'click_y': 100, }).json() print(task['task_id']) # 3. Список hooks = requests.get(f'{BASE}/api/webhooks').json() print(hooks) # 4. Удалить requests.delete(f'{BASE}/api/webhooks/{hook["id"]}')
Обработка ошибок
| Код | Описание | Пример |
|---|---|---|
202 | Задача принята в очередь | {"task_id": "...", "status": "queued"} |
404 | Задача или вебхук не найден | {"detail": "Task not found."} |
422 | Невалидные параметры | Pydantic validation error (см. ниже) |
Пример ошибки валидации (422)
{
"detail": [
{
"type": "value_error",
"loc": ["body"],
"msg": "Provide at least one prompt: click_x+click_y, points, or box.",
"input": {}
}
]
}Процессор: SAM (ViT-B)
Segment Anything Model от Meta AI Research. Квантованная ONNX-версия (Xenova/sam-vit-base).
Файлы модели
models/sam/ ├── vision_encoder_quantized.onnx # ~97 MB └── prompt_encoder_mask_decoder_quantized.onnx # ~5 MB
Как работает
- Предобработка: resize longest → 1024px, нормализация, pad до 1024×1024
- Encoder: pixel_values → image_embeddings. Кэшируется по
cache_key. - Decoder: embeddings + point → 3 маски + IoU. Берётся лучшая.
- Постобработка: resize mask, largest component, Moore tracing, Douglas-Peucker.
cache_key повторные клики мгновенные.
Размер кэша: EMBEDDING_CACHE_SIZE (default 64).
Добавление своего процессора
1. Создать файл
# app/processors/yolo.py from .base import BaseProcessor, SegmentResult class YOLOProcessor(BaseProcessor): name = 'yolo' description = 'YOLOv8 instance segmentation' def load(self): self._ready = True def segment(self, image, points, labels=None, *, cache_key=None): return SegmentResult( mask=binary_mask, score=confidence, orig_width=image.shape[1], orig_height=image.shape[0], )
2. Зарегистрировать
# app/processors/__init__.py PROCESSORS = { 'sam': SAMProcessor(), 'yolo': YOLOProcessor(), }
Docker
# Собрать и запустить docker compose up -d # Или вручную docker build -t geo_segmentantor . docker run -d -p 8090:80 geo_segmentantor
Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
MODELS_DIR | /app/models | Директория с моделями |
DEFAULT_PROCESSOR | sam | Процессор по умолчанию |
UPSTREAM_BASE_URL | http://host.docker.internal:8080 | URL для резолва относительных image_url |
LOG_LEVEL | info | Уровень логирования |
ORT_INTRA_THREADS | 4 | Потоки ONNX внутри оператора |
ORT_INTER_THREADS | 2 | Потоки ONNX между операторами |
EMBEDDING_CACHE_SIZE | 64 | Размер кэша эмбеддингов |
SEGMENTANTOR_PORT | 8090 | Внешний порт nginx |
Структура моделей
models/
├── sam/
│ ├── vision_encoder_quantized.onnx
│ └── prompt_encoder_mask_decoder_quantized.onnx
└── yolo/
└── model.onnxbash scripts/download_sam.sh.