Segmentantor

Универсальный микросервис сегментации изображений. Подключай любую ONNX-модель (SAM, YOLO, SegFormer…) — получай контуры объектов через единый HTTP API.

Сервис полностью автономный. Не зависит от Django, БД или внешних сервисов. Единственный вход — HTTP-запрос с изображением и координатами клика.

Архитектура

Segmentantor включает 4 контейнера:

🌐
Браузер
HTTP
nginx
:8090
reverse proxy
proxy
FastAPI
:8000
REST API + Web UI
enqueue задачу
Redis
:6379
очереди + состояния
/data/
shared volume
BRPOP
worker
SAM (ViT-B, ONNX)
worker
YOLO (v8, ONNX)

Базовый URL: http://<host>:8090

Поток данных

  1. Клиент отправляет POST /segment с URL тайла и координатами клика
  2. Segmentantor загружает изображение (по URL или из base64)
  3. Выбранный процессор (SAM по умолчанию) выполняет инференс
  4. Бинарная маска конвертируется в полигон (контур)
  5. Контур возвращается клиенту в пиксельных координатах

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

1
Отправка
Клиент POST-ом отправляет изображение + промпты
2
Очередь
API создаёт задачу в Redis
queued
3
Обработка
Воркер забирает через BRPOP
processing
4
Результат
Воркер возвращает контур
success / error
5
Уведомление
POST на зарегистрированные вебхуки

Статусы задачи

СтатусОписание
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

POST /segment

Основной эндпоинт сегментации. Принимает изображение (URL или base64) и координаты, возвращает контур объекта.

Параметры (JSON body)

ПараметрТипОбяз.Описание
image_urlstring* URL изображения. Абсолютный или относительный (резолвится через UPSTREAM_BASE_URL).
image_b64string* Изображение в формате base64 (PNG/JPEG). Альтернатива image_url.
click_xfloat** X-координата клика в пикселях (legacy, используйте points).
click_yfloat** Y-координата клика в пикселях (legacy, используйте points).
pointsarray** Массив точек [{x, y, label}]. label: 1 = foreground, 0 = background.
boxobject** Ограничивающий прямоугольник {x1, y1, x2, y2}. Можно совмещать с points.
processorstring Имя процессора: "sam", "yolo", … По умолчанию из конфига.
cache_keystring Ключ кэша эмбеддингов. Ускоряет повторные клики по одному тайлу.
simplify_tolerancefloat Допуск упрощения контура (Douglas-Peucker), пиксели. По умолчанию: 2.0.
callback_urlstring URL для POST-уведомления при завершении задачи. Разовый — только для этой задачи.
* Обязательно одно из: image_url или image_b64.
** Обязателен хотя бы один промпт: click_x+click_y, points или box.

Ответ (202 Accepted)

{
  "task_id": "a1b2c3d4e5f6",
  "status": "queued",
  "queue_depth": 0
}

Параметры результата (после обработки)

ПолеТипОписание
contourfloat[][]Массив точек [[x, y], ...] в пиксельных координатах.
result_scorefloatУверенность модели (IoU score).
mask_widthintШирина исходного изображения.
mask_heightintВысота исходного изображения.
result_processorstringИмя процессора.
duration_msfloatВремя обработки в миллисекундах.

Коды ответов

КодОписание
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

GET /models

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

{
  "processors": [
    { "name": "sam", "ready": true, "description": "Segment Anything Model (ViT-B, ONNX quantized)" }
  ],
  "default": "sam"
}

GET /health

GET /health

Healthcheck. Возвращает {"status": "ok"}.

WS /ws/tasks

WS /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

GET /api/tasks

Список задач с пагинацией (новейшие первыми).

Параметры (query string)

ПолеТипПо умолч.Описание
pageint1Номер страницы (1-based)
per_pageint50Кол-во записей (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}'}

GET /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')}")

Настройки

Настройки по умолчанию для процессоров. Применяются, когда параметры не переданы в запросе.

GET /api/settings

Получить текущие настройки.

PUT /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

POST /api/webhooks

Зарегистрировать вебхук. Тело — JSON.

ПолеТипОбяз.Описание
urlstringURL для POST-уведомлений
secretstringСекрет для HMAC-SHA256 подписи
GET /api/webhooks

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

DELETE /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

Как работает

  1. Предобработка: resize longest → 1024px, нормализация, pad до 1024×1024
  2. Encoder: pixel_values → image_embeddings. Кэшируется по cache_key.
  3. Decoder: embeddings + point → 3 маски + IoU. Берётся лучшая.
  4. Постобработка: resize mask, largest component, Moore tracing, Douglas-Peucker.
Кэш: encoder ~1-3 сек, decoder ~50мс. С 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_PROCESSORsamПроцессор по умолчанию
UPSTREAM_BASE_URLhttp://host.docker.internal:8080URL для резолва относительных image_url
LOG_LEVELinfoУровень логирования
ORT_INTRA_THREADS4Потоки ONNX внутри оператора
ORT_INTER_THREADS2Потоки ONNX между операторами
EMBEDDING_CACHE_SIZE64Размер кэша эмбеддингов
SEGMENTANTOR_PORT8090Внешний порт nginx

Структура моделей

models/
├── sam/
│   ├── vision_encoder_quantized.onnx
│   └── prompt_encoder_mask_decoder_quantized.onnx
└── yolo/
    └── model.onnx
Модели скачиваются при сборке Docker-образа. Для локальной разработки: bash scripts/download_sam.sh.