Документация API

Полное описание всех эндпоинтов AeroVision с примерами

Обзор архитектуры

AeroVision — микросервис для сопоставления 2D-изображений и 3D-поверхностей на базе OpenCV. Архитектура включает 7 контейнеров:

🌐
Client
HTTP
nginx
:8888
reverse proxy
proxy
FastAPI
:8000
REST API + Web UI
enqueue задачу
Redis
:6379
очереди + состояния
/data/
shared volume
BRPOP
worker-template
cv2.matchTemplate
worker-feature
SIFT / ORB / AKAZE
worker-surface
PPF + ICP (3D)
worker-chain
orchestrator

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

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

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

Template Matching (2D-изображения)

POST /api/v1/match/template

Поиск шаблона на целевом изображении через скользящее окно cv2.matchTemplate. Поддерживает мульти-детекцию с Non-Maximum Suppression и генерацию тепловой карты.

Параметры (multipart/form-data)

ПараметрТипОбязательныйПо умолчаниюОписание
imagefileЦелевое изображение (PNG, JPEG, BMP, TIFF)
templatefileШаблон для поиска
methodstringTM_CCOEFF_NORMEDTM_CCOEFF_NORMED | TM_CCORR_NORMED | TM_SQDIFF_NORMED | TM_CCOEFF | TM_CCORR | TM_SQDIFF
thresholdfloat0.8Порог совпадения (0.1–1.0). Выше = строже
max_resultsint20Макс. количество совпадений (1–1000)
use_grayscalebooltrueКонвертировать в градации серого

Ответ

{
  "uid": "d83ae21c-94bc-4a11-b3f7-1a2b3c4d5e6f",
  "status": "queued",
  "tool": "template",
  "params": {
    "method": "TM_CCOEFF_NORMED",
    "threshold": 0.8,
    "max_results": 20,
    "use_grayscale": true
  },
  "created_at": "2026-03-25T12:00:00",
  "input_files": ["uploads/d83ae.../screenshot.png", "uploads/d83ae.../icon.png"],
  "thumbnails": ["thumbnails/d83ae.../screenshot.png", "thumbnails/d83ae.../icon.png"],
  "result_files": [],
  "result_data": {}
}

Как выбрать параметры

Шаблон

Поиск иконки на скриншоте

Точный пиксельный поиск. Высокий порог для единственного совпадения.

method=TM_CCOEFF_NORMED threshold=0.9 use_grayscale=true
Матчи

Мульти-детекция одинаковых объектов

Поиск всех вхождений шаблона. Средний порог + NMS фильтрация.

method=TM_CCOEFF_NORMED threshold=0.8 max_results=50
Heatmap

Цветовой поиск

Когда цвет важен — отключите конвертацию в серое.

method=TM_CCOEFF_NORMED threshold=0.8 use_grayscale=false
Сцена

Визуальный контроль качества

Проверка наличия элемента на изображении. Строгий порог.

method=TM_CCOEFF_NORMED threshold=0.95

Примеры кода

# ── Запрос: изображение + шаблон ──
curl -X POST http://localhost:8888/api/v1/match/template \
  -F "image=@screenshot.png" \
  -F "template=@icon.png"

# ── Ответ (201): ──
# {"uid": "d83ae21c-...", "status": "queued", "tool": "template",
#  "params": {"method":"TM_CCOEFF_NORMED","threshold":0.8,"max_results":20,"use_grayscale":true},
#  "input_files": ["uploads/d83ae.../screenshot.png","uploads/d83ae.../icon.png"],
#  "result_files": [], "result_data": {}}

# ── Запрос: с параметрами ──
curl -X POST http://localhost:8888/api/v1/match/template \
  -F "image=@screenshot.png" \
  -F "template=@icon.png" \
  -F "method=TM_CCORR_NORMED" \
  -F "threshold=0.9" \
  -F "max_results=10" \
  -F "use_grayscale=true"

# ── Запрос: проверить статус ──
curl http://localhost:8888/api/v1/tasks/d83ae21c-94bc-4a11-b3f7-1a2b3c4d5e6f

# ── Ответ (200), когда задача завершена: ──
# {"uid": "d83ae21c-...", "status": "done", "tool": "template",
#  "result_files": ["results/d83ae.../matches.png","results/d83ae.../heatmap.png"],
#  "result_data": {
#    "method": "TM_CCOEFF_NORMED", "matches_found": 3, "best_score": 0.97,
#    "matches": [{"x":120,"y":45,"w":32,"h":32,"score":0.97},
#               {"x":340,"y":180,"w":32,"h":32,"score":0.92},
#               {"x":560,"y":310,"w":32,"h":32,"score":0.85}]}}
// ── Запрос: загрузка через FormData ──
const form = new FormData();
form.append("image", imageInput.files[0]);
form.append("template", templateInput.files[0]);
form.append("method", "TM_CCOEFF_NORMED");
form.append("threshold", "0.8");

const res = await fetch("/api/v1/match/template", {
  method: "POST",
  body: form,
});
const task = await res.json();

// ── Ответ task: ──
// {uid: "d83ae21c-...", status: "queued", tool: "template",
//  params: {method:"TM_CCOEFF_NORMED", threshold:0.8, max_results:20, use_grayscale:true},
//  input_files: ["uploads/d83ae.../screenshot.png","uploads/d83ae.../icon.png"]}

// ── Запрос: поллинг статуса до завершения ──
async function waitForResult(uid) {
  while (true) {
    const r = await fetch(`/api/v1/tasks/${uid}`);
    const data = await r.json();
    if (data.status === "done" || data.status === "error")
      return data;
    await new Promise(r => setTimeout(r, 1000));
  }
}
const result = await waitForResult(task.uid);

// ── Ответ result (когда done): ──
// {uid: "d83ae21c-...", status: "done",
//  result_files: ["results/d83ae.../matches.png","results/d83ae.../heatmap.png"],
//  result_data: {method:"TM_CCOEFF_NORMED", matches_found:3, best_score:0.97,
//    matches: [{x:120,y:45,w:32,h:32,score:0.97}, ...]}}
import requests
import time

# ── Запрос: отправить изображения ──
files = {
    "image": open("screenshot.png", "rb"),
    "template": open("icon.png", "rb"),
}
data = {
    "method": "TM_CCOEFF_NORMED",
    "threshold": "0.8",
    "max_results": "20",
}
resp = requests.post(
    "http://localhost:8888/api/v1/match/template",
    files=files, data=data,
)
task = resp.json()

# ── Ответ task: ──
# {"uid": "d83ae21c-...", "status": "queued", "tool": "template",
#  "params": {"method":"TM_CCOEFF_NORMED", "threshold":0.8,
#             "max_results":20, "use_grayscale":true},
#  "input_files": ["uploads/d83ae.../screenshot.png", "uploads/d83ae.../icon.png"]}

# ── Запрос: дождаться результата ──
while True:
    r = requests.get(f"http://localhost:8888/api/v1/tasks/{task['uid']}")
    status = r.json()
    if status["status"] in ("done", "error"):
        break
    time.sleep(1)

# ── Ответ status (когда done): ──
# {"uid": "d83ae21c-...", "status": "done",
#  "result_files": ["results/d83ae.../matches.png", "results/d83ae.../heatmap.png"],
#  "result_data": {"method": "TM_CCOEFF_NORMED", "matches_found": 3, "best_score": 0.97,
#    "matches": [{"x":120,"y":45,"w":32,"h":32,"score":0.97}, ...]}}

Результат обработки

После завершения задачи в result_files появятся два изображения, а в result_data:

{
  "result_data": {
    "method": "TM_CCOEFF_NORMED",
    "matches_found": 3,
    "best_score": 0.97,
    "matches": [
      {"x": 120, "y": 45, "w": 32, "h": 32, "score": 0.97},
      {"x": 340, "y": 180, "w": 32, "h": 32, "score": 0.92},
      {"x": 560, "y": 310, "w": 32, "h": 32, "score": 0.85}
    ]
  }
}
matches.png
matches.png — совпадения
heatmap.png
heatmap.png — тепловая карта

Feature Matching (2D-изображения)

POST /api/v1/match/feature

Сопоставление ключевых точек на двух изображениях. Поддерживает детекторы SIFT, ORB, AKAZE, BRISK и матчеры BFMatcher, FLANN.

Параметры (multipart/form-data)

ПараметрТипОбязательныйПо умолчаниюОписание
query_imagefileИскомое изображение (PNG, JPEG, BMP, TIFF)
train_imagefileЭталонное изображение
detectorstringSIFTДетектор: SIFT | ORB | AKAZE | BRISK
matcherstringFLANNМатчер: FLANN | BFMatcher
ratio_thresholdfloat0.75Порог Lowe's ratio test (0.1–1.0). Ниже = строже
max_matchesint200Макс. количество совпадений (1–5000)

Ответ

{
  "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "queued",
  "tool": "feature",
  "params": {
    "detector": "SIFT",
    "matcher": "FLANN",
    "ratio_threshold": 0.75,
    "max_matches": 200
  },
  "created_at": "2026-03-25T12:00:00",
  "input_files": ["uploads/a1b2.../query.jpg", "uploads/a1b2.../train.jpg"],
  "thumbnails": ["thumbnails/a1b2.../query.jpg", "thumbnails/a1b2.../train.jpg"],
  "result_files": [],
  "result_data": {}
}

Как выбрать параметры

Feature

Панорамная склейка

Объединение нескольких фото в панораму. Нужна высокая точность совпадений — ложные пары сильно искажают результат.

detector=SIFT matcher=FLANN ratio_threshold=0.7
Query

Поиск объекта на сцене

Найти деталь/объект на фотографии. Можно допустить больше совпадений — они фильтруются гомографией.

detector=ORB matcher=BFMatcher ratio_threshold=0.75
Сцена

Быстрое грубое сравнение

Когда скорость важнее точности — предварительная фильтрация, batch-обработка.

detector=BRISK matcher=BFMatcher ratio_threshold=0.8
Fallback

Максимальная точность

Точные измерения, стереозрение, калибровка — строгий порог отсекает все сомнительные пары.

detector=SIFT matcher=FLANN ratio_threshold=0.6

Примеры кода

# ── Запрос: два изображения ──
curl -X POST http://localhost:8888/api/v1/match/feature \
  -F "query_image=@photo1.jpg" \
  -F "train_image=@photo2.jpg"

# ── Ответ (200): ──
# {
#   "uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
#   "status": "queued",
#   "tool": "feature",
#   "params": {"detector":"SIFT","matcher":"FLANN","ratio_threshold":0.75,"max_matches":200},
#   "created_at": "2026-03-25T12:00:00",
#   "input_files": ["uploads/f47ac.../photo1.jpg","uploads/f47ac.../photo2.jpg"],
#   "thumbnails": ["thumbnails/f47ac.../photo1.jpg","thumbnails/f47ac.../photo2.jpg"],
#   "result_files": [], "result_data": {}
# }

# ── Запрос: с параметрами ──
curl -X POST http://localhost:8888/api/v1/match/feature \
  -F "query_image=@photo1.jpg" \
  -F "train_image=@photo2.jpg" \
  -F "detector=ORB" \
  -F "matcher=BFMatcher" \
  -F "ratio_threshold=0.8" \
  -F "max_matches=300"

# ── Ответ (200): ──
# { "uid": "...", "status": "queued", "params": {"detector":"ORB","matcher":"BFMatcher",...} }

# ── Запрос: проверить статус ──
curl http://localhost:8888/api/v1/tasks/f47ac10b-58cc-4372-a567-0e02b2c3d479

# ── Ответ (200), когда задача завершена: ──
# {
#   "uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
#   "status": "done",
#   "tool": "feature",
#   "result_files": ["results/f47ac.../matches.png"],
#   "result_data": {
#     "query_keypoints": 1523, "train_keypoints": 1847,
#     "good_matches": 342, "detector": "SIFT", "matcher": "FLANN"
#   }
# }
// ── Запрос: загрузка через FormData ──
const form = new FormData();
form.append("query_image", fileInput1.files[0]);
form.append("train_image", fileInput2.files[0]);
form.append("detector", "SIFT");
form.append("ratio_threshold", "0.75");

const res = await fetch("/api/v1/match/feature", {
  method: "POST",
  body: form,
});
const task = await res.json();

// ── Ответ task: ──
// {uid: "f47ac10b-...", status: "queued", tool: "feature",
//  params: {detector:"SIFT", matcher:"FLANN", ratio_threshold:0.75, max_matches:200},
//  input_files: ["uploads/f47ac.../photo1.jpg","uploads/f47ac.../photo2.jpg"],
//  result_files: [], result_data: {}}

// ── Запрос: поллинг статуса до завершения ──
async function waitForResult(uid) {
  while (true) {
    const r = await fetch(`/api/v1/tasks/${uid}`);
    const data = await r.json();
    if (data.status === "done" || data.status === "error")
      return data;
    await new Promise(r => setTimeout(r, 1000));
  }
}
const result = await waitForResult(task.uid);

// ── Ответ result (когда done): ──
// {uid: "f47ac10b-...", status: "done",
//  result_files: ["results/f47ac.../matches.png"],
//  result_data: {query_keypoints:1523, train_keypoints:1847,
//    good_matches:342, detector:"SIFT", matcher:"FLANN"}}
import requests
import time

# ── Запрос: отправить изображения ──
files = {
    "query_image": open("photo1.jpg", "rb"),
    "train_image": open("photo2.jpg", "rb"),
}
data = {
    "detector": "SIFT",
    "matcher": "FLANN",
    "ratio_threshold": "0.75",
    "max_matches": "200",
}
resp = requests.post(
    "http://localhost:8888/api/v1/match/feature",
    files=files, data=data,
)
task = resp.json()

# ── Ответ task: ──
# {"uid": "f47ac10b-...", "status": "queued", "tool": "feature",
#  "params": {"detector":"SIFT", "matcher":"FLANN",
#             "ratio_threshold":0.75, "max_matches":200},
#  "input_files": ["uploads/f47ac.../photo1.jpg", "uploads/f47ac.../photo2.jpg"],
#  "result_files": [], "result_data": {}}

# ── Запрос: дождаться результата ──
while True:
    r = requests.get(f"http://localhost:8888/api/v1/tasks/{task['uid']}")
    status = r.json()
    if status["status"] in ("done", "error"):
        break
    time.sleep(1)

# ── Ответ status (когда done): ──
# {"uid": "f47ac10b-...", "status": "done",
#  "result_files": ["results/f47ac.../matches.png"],
#  "result_data": {"query_keypoints": 1523, "train_keypoints": 1847,
#    "good_matches": 342, "detector": "SIFT", "matcher": "FLANN"}}

Результат обработки

После завершения задачи в result_files появится изображение-визуализация совпавших точек, а в result_data:

{
  "result_data": {
    "query_keypoints": 1523,
    "train_keypoints": 1847,
    "good_matches": 342,
    "detector": "SIFT",
    "matcher": "FLANN"
  }
}
Feature matches
matches.png — совпадения ключевых точек (SIFT + FLANN)

Surface Matching (3D-поверхности)

POST /api/v1/match/surface

Сопоставление 3D-облаков точек методом PPF (Point Pair Features) с опциональным уточнением ICP (Iterative Closest Point). Файлы в формате PLY.

Параметры (multipart/form-data)

ПараметрТипОбязательныйПо умолчаниюОписание
model_filefile3D-модель (.ply) — объект поиска
scene_filefile3D-сцена (.ply) — где искать
relative_sampling_stepfloat0.05Шаг дискретизации (0.01–0.5). Меньше = точнее
relative_distance_stepfloat0.05Шаг хеширования PPF (0.01–0.5)
num_anglesint30Дискретизация вращения (1–360)
use_icpbooltrueУточнение позы через ICP
icp_tolerancefloat0.005Порог сходимости ICP
icp_iterationsint100Макс. итераций ICP (1–1000)

Формат PLY

ply
format ascii 1.0
element vertex 4
property float x
property float y
property float z
property float nx
property float ny
property float nz
end_header
0.0 0.0 0.0 0.0 0.0 1.0
1.0 0.0 0.0 0.0 0.0 1.0
1.0 1.0 0.0 0.0 0.0 1.0
0.0 1.0 0.0 0.0 0.0 1.0

⚠ Обязательны нормали (nx, ny, nz) — PPF-алгоритм использует их для вычисления фич.

Сценарии использования

3D

Точное сопоставление деталей

CAD-модель vs скан. Мелкий шаг + ICP с большим числом итераций.

sampling_step=0.025 use_icp=true icp_iterations=200
Быстрый

Быстрый поиск позы

Предварительная оценка, реалтайм — крупный шаг, без ICP.

sampling_step=0.1 use_icp=false
Шумные

Зашумлённые сканы

Данные с Kinect/LiDAR — средний шаг + много итераций ICP для компенсации шума.

sampling_step=0.05 use_icp=true icp_iterations=300

Примеры кода

# ── Запрос: базовый ──
curl -X POST http://localhost:8888/api/v1/match/surface \
  -F "model_file=@model.ply" \
  -F "scene_file=@scene.ply"

# ── Ответ: ──
# {"uid": "b82f4c...", "status": "queued", "tool": "surface",
#  "params": {"relative_sampling_step":0.05, "relative_distance_step":0.05,
#    "num_angles":30, "use_icp":false, "icp_tolerance":0.005, "icp_iterations":100},
#  "input_files": ["uploads/b82f4c.../model.ply","uploads/b82f4c.../scene.ply"],
#  "result_files": [], "result_data": {}}

# ── Запрос: точное сопоставление с ICP ──
curl -X POST http://localhost:8888/api/v1/match/surface \
  -F "model_file=@model.ply" \
  -F "scene_file=@scene.ply" \
  -F "relative_sampling_step=0.025" \
  -F "num_angles=60" \
  -F "use_icp=true" \
  -F "icp_iterations=200"

# ── Получить результат: ──
curl http://localhost:8888/api/v1/tasks/b82f4c...

# ── Ответ (когда done): ──
# {"uid": "b82f4c...", "status": "done",
#  "result_data": {
#    "pose": [[0.99,-0.01,0.03,12.5],[0.01,0.99,-0.02,-3.1],
#             [-0.03,0.02,0.99,0.8],[0.0,0.0,0.0,1.0]],
#    "num_model_points":4521, "num_scene_points":18230, "icp_residual":0.0023}}
// ── Запрос: отправка PLY файлов ──
const form = new FormData();
form.append("model_file", modelInput.files[0]);
form.append("scene_file", sceneInput.files[0]);
form.append("relative_sampling_step", "0.025");
form.append("use_icp", "true");

const res = await fetch("/api/v1/match/surface", {
  method: "POST",
  body: form,
});
const task = await res.json();

// ── Ответ task: ──
// {uid: "b82f4c...", status: "queued", tool: "surface",
//  params: {relative_sampling_step:0.025, use_icp:true, icp_iterations:100, ...},
//  input_files: ["uploads/b82f4c.../model.ply","uploads/b82f4c.../scene.ply"]}

// ── Поллинг и ответ: ──
const r = await fetch(`/api/v1/tasks/${task.uid}`);
const result = await r.json();
// {uid: "b82f4c...", status: "done",
//  result_data: {pose: [[0.99,-0.01,0.03,12.5],...],
//    num_model_points:4521, num_scene_points:18230, icp_residual:0.0023}}
import requests, time

# ── Запрос: отправить PLY файлы ──
files = {
    "model_file": open("model.ply", "rb"),
    "scene_file": open("scene.ply", "rb"),
}
data = {
    "relative_sampling_step": "0.025",
    "use_icp": "true",
    "icp_iterations": "200",
}
resp = requests.post(
    "http://localhost:8888/api/v1/match/surface",
    files=files, data=data,
)
task = resp.json()

# ── Ответ task: ──
# {"uid": "b82f4c...", "status": "queued", "tool": "surface",
#  "params": {"relative_sampling_step":0.025, "use_icp":true, "icp_iterations":200, ...},
#  "input_files": ["uploads/b82f4c.../model.ply","uploads/b82f4c.../scene.ply"]}

# ── Запрос: дождаться результата ──
while True:
    r = requests.get(f"http://localhost:8888/api/v1/tasks/{task['uid']}")
    result = r.json()
    if result["status"] in ("done", "error"):
        break
    time.sleep(1)

# ── Ответ result (когда done): ──
# {"uid": "b82f4c...", "status": "done",
#  "result_data": {
#    "pose": [[0.99,-0.01,0.03,12.5],[0.01,0.99,-0.02,-3.1],
#             [-0.03,0.02,0.99,0.8],[0.0,0.0,0.0,1.0]],
#    "num_model_points": 4521, "num_scene_points": 18230,
#    "icp_residual": 0.0023}}

Результат обработки

{
  "result_data": {
    "pose": [
      [0.99, -0.01,  0.03,  12.5],
      [0.01,  0.99, -0.02,  -3.1],
      [-0.03, 0.02,  0.99,   0.8],
      [0.0,   0.0,   0.0,    1.0]
    ],
    "num_model_points": 4521,
    "num_scene_points": 18230,
    "icp_residual": 0.0023
  }
}

pose — матрица трансформации 4×4, переводящая координаты модели в координаты сцены.

Chain Matching (цепочка методов)

POST /api/v1/match/chain

Последовательный запуск нескольких методов сопоставления. Каждый шаг выполняется только если предыдущий не нашёл совпадение. Цепочка прерывается при первом успешном результате.

Template result
Шаг 1 — Template Matching
Feature fallback
Шаг 2 — Feature Matching

Логика работы

Воркер проходит шаги по порядку. Для каждого шага проверяется критерий «найдено»:

ИнструментКритерий «найдено»Пояснение
Template Matchingfiltered_matches > 0Есть совпадение выше порога после NMS
Feature Matchinggood_matches > 0Есть пара ключевых точек после ratio test
Surface Matchingposes_found > 0PPF нашёл хотя бы одну позу

Если критерий выполнен — цепочка останавливается (chain_result: "found"). Если ни один шаг не нашёл — "not_found". Пропущенные шаги имеют status: "skipped".

ПараметрТипОбязательныйПо умолчаниюОписание
imagefileПервое изображение (image / query / model)
templatefileВторое изображение (template / train / scene)
stepsstring (JSON)JSON-массив шагов цепочки (макс. 10)

Формат steps

Каждый элемент массива — объект с полями tool и params:

[
  {"tool": "template", "params": {"method": "TM_CCOEFF_NORMED", "threshold": 0.95}},
  {"tool": "template", "params": {"method": "TM_CCOEFF_NORMED", "threshold": 0.8}},
  {"tool": "feature",  "params": {"detector": "SIFT", "matcher": "FLANN", "ratio_threshold": 0.7}}
]

Критерии «найдено»

ИнструментКритерий
Template Matchingfiltered_matches > 0
Feature Matchinggood_matches > 0
Surface Matchingposes_found > 0

Примеры

curl -X POST http://localhost:8888/api/v1/match/chain \
  -F image=@photo.png \
  -F template=@icon.png \
  -F 'steps=[{"tool":"template","params":{"method":"TM_CCOEFF_NORMED","threshold":0.95}},{"tool":"template","params":{"threshold":0.7}},{"tool":"feature","params":{"detector":"SIFT","ratio_threshold":0.7}}]'

# ── Ответ: ──
# {"uid": "abc123", "status": "queued", "tool": "chain", ...}
const form = new FormData();
form.append('image', imageFile);
form.append('template', templateFile);
form.append('steps', JSON.stringify([
  {tool: 'template', params: {method: 'TM_CCOEFF_NORMED', threshold: 0.95}},
  {tool: 'template', params: {threshold: 0.7}},
  {tool: 'feature',  params: {detector: 'SIFT', ratio_threshold: 0.7}},
]));

const res = await fetch('http://localhost:8888/api/v1/match/chain', {
  method: 'POST',
  body: form,
});
const data = await res.json();

// ── Ответ: ──
// {uid: 'abc123', status: 'queued', tool: 'chain', ...}
import requests, json

steps = [
    {"tool": "template", "params": {"method": "TM_CCOEFF_NORMED", "threshold": 0.95}},
    {"tool": "template", "params": {"threshold": 0.7}},
    {"tool": "feature",  "params": {"detector": "SIFT", "ratio_threshold": 0.7}},
]

resp = requests.post(
    "http://localhost:8888/api/v1/match/chain",
    files={
        "image": open("photo.png", "rb"),
        "template": open("icon.png", "rb"),
    },
    data={"steps": json.dumps(steps)},
)
print(resp.json())

# ── Ответ: ──
# {"uid": "abc123", "status": "queued", "tool": "chain", ...}

Формат результата (result_data)

{
  "chain_result": "found",
  "completed_step": 2,
  "total_steps": 3,
  "steps": [
    {
      "step": 1, "tool": "template",
      "params": {"method": "TM_CCOEFF_NORMED", "threshold": 0.95},
      "status": "done", "found": false,
      "result_data": {"filtered_matches": 0, ...},
      "result_files": ["results/abc123/matches.png", "results/abc123/heatmap.png"]
    },
    {
      "step": 2, "tool": "template",
      "params": {"threshold": 0.7},
      "status": "done", "found": true,
      "result_data": {"filtered_matches": 3, ...},
      "result_files": ["results/abc123/matches.png", "results/abc123/heatmap.png"]
    }
  ]
}

chain_result: "found" если хотя бы один шаг нашёл совпадение, иначе "not_found".

Задачи

GET /api/v1/tasks

Список всех задач с пагинацией.

ПараметрТипПо умолчаниюОписание
offsetint0Смещение (query string)
limitint50Кол-во записей (1–200)
# ── Запрос: ──
curl "http://localhost:8888/api/v1/tasks?offset=0&limit=20"

# ── Ответ: ──
# {"total": 42, "tasks": [
#   {"uid": "f47ac10b-...", "status": "done", "tool": "feature",
#    "created_at": "2024-01-15T10:30:00", "updated_at": "2024-01-15T10:30:12",
#    "input_files": [...], "result_files": [...], "result_data": {...}},
#   {"uid": "b82f4c...", "status": "queued", "tool": "surface", ...},
#   ...]}
import requests

# ── Запрос: ──
resp = requests.get(
    "http://localhost:8888/api/v1/tasks",
    params={"offset": 0, "limit": 20},
)
data = resp.json()

# ── Ответ data: ──
# {"total": 42, "tasks": [
#   {"uid": "f47ac10b-...", "status": "done", "tool": "feature",
#    "created_at": "2024-01-15T10:30:00", "updated_at": "2024-01-15T10:30:12",
#    "input_files": [...], "result_files": [...], "result_data": {...}},
#   ...]}

print(f"Всего задач: {data['total']}")
for t in data["tasks"]:
    print(f"  {t['uid'][:12]}  {t['status']:12s}  {t['tool']}")

GET /api/v1/tasks/{uid}

Статус и результат задачи по UID.

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

СтатусОписание
queuedЗадача в очереди, ожидает воркера
processingВоркер обрабатывает задачу
doneЗавершена, результаты доступны в result_files и result_data
errorОшибка, подробности в поле error
# ── Запрос: ──
curl http://localhost:8888/api/v1/tasks/a1b2c3d4-e5f6-7890-abcd-ef1234567890

# ── Ответ (задача завершена): ──
# {"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
#  "status": "done", "tool": "feature",
#  "params": {"detector":"SIFT", "matcher":"FLANN", "ratio_threshold":0.75, "max_matches":200},
#  "created_at": "2024-01-15T10:30:00", "updated_at": "2024-01-15T10:30:12",
#  "input_files": ["uploads/a1b2c3d4.../photo1.jpg","uploads/a1b2c3d4.../photo2.jpg"],
#  "result_files": ["results/a1b2c3d4.../matches.png"],
#  "result_data": {"query_keypoints":1523, "train_keypoints":1847, "good_matches":342},
#  "error": null}

# ── Ответ (задача не найдена, HTTP 404): ──
# {"detail": "Task not found"}
import requests

# ── Запрос: ──
uid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
resp = requests.get(f"http://localhost:8888/api/v1/tasks/{uid}")

if resp.status_code == 404:
    print("Задача не найдена")
    # Ответ: {"detail": "Task not found"}
else:
    task = resp.json()
    # ── Ответ task: ──
    # {"uid": "a1b2c3d4-...", "status": "done", "tool": "feature",
    #  "result_files": ["results/a1b2c3d4.../matches.png"],
    #  "result_data": {"query_keypoints":1523, ...}, "error": null}
    print(f"Статус: {task['status']}")
    if task["status"] == "done":
        print(f"Файлы результатов: {task['result_files']}")
    elif task["status"] == "error":
        print(f"Ошибка: {task['error']}")

Настройки

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

GET /api/v1/settings/template

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

PUT /api/v1/settings/template

Обновить настройки Template Matching.

GET /api/v1/settings/feature

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

PUT /api/v1/settings/feature

Обновить настройки Feature Matching.

GET /api/v1/settings/surface

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

PUT /api/v1/settings/surface

Обновить настройки Surface Matching.

Примеры

# ── Запрос: получить настройки Template Matching ──
curl http://localhost:8888/api/v1/settings/template

# ── Ответ: ──
# {"tool": "template", "params": {
#   "method": "TM_CCOEFF_NORMED", "threshold": 0.8,
#   "max_results": 20, "use_grayscale": true}}

# ── Запрос: обновить настройки Template Matching ──
curl -X PUT http://localhost:8888/api/v1/settings/template \
  -H "Content-Type: application/json" \
  -d '{
    "method": "TM_CCOEFF_NORMED",
    "threshold": 0.9,
    "max_results": 10,
    "use_grayscale": true
  }'

# ── Ответ: ──
# {"tool": "template", "params": {
#   "method": "TM_CCOEFF_NORMED", "threshold": 0.9,
#   "max_results": 10, "use_grayscale": true}}

# ── Запрос: получить настройки Feature Matching ──
curl http://localhost:8888/api/v1/settings/feature

# ── Ответ: ──
# {"tool": "feature", "params": {
#   "detector": "SIFT", "matcher": "FLANN",
#   "ratio_threshold": 0.75, "max_matches": 200}}

# ── Запрос: обновить настройки Feature Matching ──
curl -X PUT http://localhost:8888/api/v1/settings/feature \
  -H "Content-Type: application/json" \
  -d '{
    "detector": "SIFT",
    "matcher": "FLANN",
    "ratio_threshold": 0.7,
    "max_matches": 200
  }'

# ── Ответ: ──
# {"tool": "feature", "params": {
#   "detector": "SIFT", "matcher": "FLANN",
#   "ratio_threshold": 0.7, "max_matches": 200}}

# ── Запрос: обновить настройки Surface Matching ──
curl -X PUT http://localhost:8888/api/v1/settings/surface \
  -H "Content-Type: application/json" \
  -d '{
    "relative_sampling_step": 0.05,
    "relative_distance_step": 0.05,
    "num_angles": 30,
    "use_icp": true,
    "icp_tolerance": 0.005,
    "icp_iterations": 100
  }'

# ── Ответ: ──
# {"tool": "surface", "params": {
#   "relative_sampling_step": 0.05, "relative_distance_step": 0.05,
#   "num_angles": 30, "use_icp": true,
#   "icp_tolerance": 0.005, "icp_iterations": 100}}
import requests

# ── Запрос: получить настройки Template Matching ──
tmpl = requests.get(
    "http://localhost:8888/api/v1/settings/template"
).json()

# ── Ответ tmpl: ──
# {"tool": "template", "params": {
#   "method": "TM_CCOEFF_NORMED", "threshold": 0.8,
#   "max_results": 20, "use_grayscale": true}}

# ── Запрос: обновить настройки Template Matching ──
resp = requests.put(
    "http://localhost:8888/api/v1/settings/template",
    json={
        "method": "TM_CCOEFF_NORMED",
        "threshold": 0.9,
        "max_results": 10,
        "use_grayscale": True,
    },
)

# ── Ответ: ──
# {"tool": "template", "params": {
#   "method": "TM_CCOEFF_NORMED", "threshold": 0.9,
#   "max_results": 10, "use_grayscale": true}}

# ── Запрос: получить текущие настройки Feature Matching ──
current = requests.get(
    "http://localhost:8888/api/v1/settings/feature"
).json()

# ── Ответ current: ──
# {"tool": "feature", "params": {
#   "detector": "SIFT", "matcher": "FLANN",
#   "ratio_threshold": 0.75, "max_matches": 200}}

# ── Запрос: обновить ──
resp = requests.put(
    "http://localhost:8888/api/v1/settings/feature",
    json={
        "detector": "ORB",
        "matcher": "BFMatcher",
        "ratio_threshold": 0.8,
        "max_matches": 500,
    },
)

# ── Ответ: ──
# {"tool": "feature", "params": {
#   "detector": "ORB", "matcher": "BFMatcher",
#   "ratio_threshold": 0.8, "max_matches": 500}}

Вебхуки

Вебхуки позволяют получать уведомления о завершении задач без поллинга. При регистрации указывается URL, на который будет отправлен POST-запрос с результатом.

POST /api/v1/webhooks

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

ПолеТипОбязательныйОписание
urlstringURL для POST-уведомлений
secretstringСекрет для подписи HMAC-SHA256. Если указан, запрос содержит заголовок X-Webhook-Signature
GET /api/v1/webhooks

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

DELETE /api/v1/webhooks/{webhook_id}

Удалить вебхук по ID.

Payload вебхука

При завершении задачи (статус done или error) на все зарегистрированные URL отправляется POST-запрос:

POST https://your-server.com/callback
Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4...  ← только если указан secret

{
  "uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "done",
  "tool": "feature",
  "result_files": [
    "results/a1b2c3d4.../matches.png"
  ],
  "result_data": {
    "query_keypoints": 1523,
    "train_keypoints": 1847,
    "good_matches": 342
  }
}

Подпись HMAC-SHA256

Если при регистрации был указан secret, заголовок X-Webhook-Signature содержит HMAC-SHA256 хеш тела запроса. Проверка подписи на стороне получателя:

import hmac
import 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)

# В обработчике Flask / FastAPI:
from fastapi import Request, HTTPException

async def webhook_handler(request: Request):
    body = await request.body()
    sig = request.headers.get("X-Webhook-Signature", "")
    if not verify_signature(body, "my-secret", sig):
        raise HTTPException(status_code=401, detail="Invalid signature")
    data = await request.json()
    print(f"Задача {data['uid']} завершена: {data['status']}")
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),
  );
}

// Express handler
app.post("/callback", (req, res) => {
  const sig = req.headers["x-webhook-signature"];
  if (!verifySignature(JSON.stringify(req.body), "my-secret", sig)) {
    return res.status(401).send("Invalid signature");
  }
  console.log("Task", req.body.uid, req.body.status);
  res.sendStatus(200);
});

Полный пример: регистрация → отправка задачи → получение результата через вебхук

# ── 1. Запрос: зарегистрировать вебхук ──
curl -X POST http://localhost:8888/api/v1/webhooks \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/callback",
    "secret": "my-secret-key"
  }'

# ── Ответ: ──
# {"id": "wh_abc123", "url": "https://your-server.com/callback",
#  "created_at": "2024-01-15T10:30:00"}

# ── 2. Запрос: отправить задачу ──
curl -X POST http://localhost:8888/api/v1/match/feature \
  -F "query_image=@photo1.jpg" \
  -F "train_image=@photo2.jpg"

# ── Ответ: ──
# {"uid": "f47ac10b-...", "status": "queued", "tool": "feature", ...}
# Результат придёт POST-запросом на https://your-server.com/callback

# ── 3. Запрос: посмотреть вебхуки ──
curl http://localhost:8888/api/v1/webhooks

# ── Ответ: ──
# [{"id": "wh_abc123", "url": "https://your-server.com/callback",
#   "created_at": "2024-01-15T10:30:00"}]

# ── 4. Запрос: удалить вебхук ──
curl -X DELETE http://localhost:8888/api/v1/webhooks/wh_abc123

# ── Ответ: HTTP 204 No Content ──
import requests

BASE = "http://localhost:8888"

# ── 1. Запрос: зарегистрировать вебхук ──
wh = requests.post(f"{BASE}/api/v1/webhooks", json={
    "url": "https://your-server.com/callback",
    "secret": "my-secret-key",  # опционально
}).json()

# ── Ответ wh: ──
# {"id": "wh_abc123", "url": "https://your-server.com/callback",
#  "created_at": "2024-01-15T10:30:00"}

# ── 2. Запрос: отправить задачу ──
task = requests.post(f"{BASE}/api/v1/match/feature", files={
    "query_image": open("photo1.jpg", "rb"),
    "train_image": open("photo2.jpg", "rb"),
}).json()

# ── Ответ task: ──
# {"uid": "f47ac10b-...", "status": "queued", "tool": "feature", ...}
# Результат придёт POST-запросом на вебхук — поллинг не нужен!

# ── 3. Запрос: посмотреть все вебхуки ──
hooks = requests.get(f"{BASE}/api/v1/webhooks").json()

# ── Ответ hooks: ──
# [{"id": "wh_abc123", "url": "https://your-server.com/callback",
#   "created_at": "2024-01-15T10:30:00"}]

for h in hooks:
    print(f"  {h['id']}  →  {h['url']}")

# ── 4. Запрос: удалить вебхук ──
requests.delete(f"{BASE}/api/v1/webhooks/{wh['id']}")
# Ответ: HTTP 204 No Content
const BASE = "http://localhost:8888";

// ── 1. Запрос: зарегистрировать вебхук ──
const whRes = await fetch(`${BASE}/api/v1/webhooks`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://your-server.com/callback",
    secret: "my-secret-key",
  }),
});
const wh = await whRes.json();

// ── Ответ wh: ──
// {id: "wh_abc123", url: "https://your-server.com/callback",
//  created_at: "2024-01-15T10:30:00"}

// ── 2. Запрос: отправить задачу ──
const form = new FormData();
form.append("query_image", fileInput1.files[0]);
form.append("train_image", fileInput2.files[0]);
const taskRes = await fetch(`${BASE}/api/v1/match/feature`, {
  method: "POST", body: form,
});
const task = await taskRes.json();

// ── Ответ task: ──
// {uid: "f47ac10b-...", status: "queued", tool: "feature", ...}

// ── 3. Запрос: список вебхуков ──
const hooks = await (await fetch(`${BASE}/api/v1/webhooks`)).json();

// ── Ответ hooks: ──
// [{id: "wh_abc123", url: "https://your-server.com/callback",
//   created_at: "2024-01-15T10:30:00"}]

hooks.forEach(h => console.log(h.id, "→", h.url));

// ── 4. Запрос: удалить ──
await fetch(`${BASE}/api/v1/webhooks/${wh.id}`, { method: "DELETE" });
// Ответ: HTTP 204 No Content

Диаграмма потока вебхуков

Client
AeroVision
Your Server
POST /api/v1/webhooks
{"id": "wh_abc"}
POST /api/v1/match/*
{"uid": "task_xyz"}
worker обрабатывает задачу
POST /callback
X-Webhook-Signature: hmac-sha256 {"uid":"task_xyz", ...}
200 OK

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

КодСитуацияТело ответа
200Успешный запросJSON с данными
404Задача или вебхук не найден{"detail": "Задача не найдена"}
422Невалидные параметрыPydantic validation error

Пример ошибки валидации (422)

{
  "detail": [
    {
      "type": "value_error",
      "loc": ["body", "ratio_threshold"],
      "msg": "Input should be less than or equal to 1.0",
      "input": 1.5
    }
  ]
}