Документация API
Полное описание всех эндпоинтов AeroVision с примерами
Обзор архитектуры
AeroVision — микросервис для сопоставления 2D-изображений и 3D-поверхностей на базе OpenCV. Архитектура включает 7 контейнеров:
Базовый URL: http://<host>:8888
Жизненный цикл задачи
queued
BRPOPprocessing
done / error
Template Matching (2D-изображения)
/api/v1/match/template
Поиск шаблона на целевом изображении через скользящее окно cv2.matchTemplate. Поддерживает мульти-детекцию с Non-Maximum Suppression и генерацию тепловой карты.
Параметры (multipart/form-data)
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
image | file | ✓ | — | Целевое изображение (PNG, JPEG, BMP, TIFF) |
template | file | ✓ | — | Шаблон для поиска |
method | string | TM_CCOEFF_NORMED | TM_CCOEFF_NORMED | TM_CCORR_NORMED | TM_SQDIFF_NORMED | TM_CCOEFF | TM_CCORR | TM_SQDIFF | |
threshold | float | 0.8 | Порог совпадения (0.1–1.0). Выше = строже | |
max_results | int | 20 | Макс. количество совпадений (1–1000) | |
use_grayscale | bool | true | Конвертировать в градации серого |
Ответ
{
"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
Цветовой поиск
Когда цвет важен — отключите конвертацию в серое.
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}
]
}
}
Feature Matching (2D-изображения)
/api/v1/match/feature
Сопоставление ключевых точек на двух изображениях. Поддерживает детекторы SIFT, ORB, AKAZE, BRISK и матчеры BFMatcher, FLANN.
Параметры (multipart/form-data)
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
query_image | file | ✓ | — | Искомое изображение (PNG, JPEG, BMP, TIFF) |
train_image | file | ✓ | — | Эталонное изображение |
detector | string | SIFT | Детектор: SIFT | ORB | AKAZE | BRISK | |
matcher | string | FLANN | Матчер: FLANN | BFMatcher | |
ratio_threshold | float | 0.75 | Порог Lowe's ratio test (0.1–1.0). Ниже = строже | |
max_matches | int | 200 | Макс. количество совпадений (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": {}
}Как выбрать параметры
Панорамная склейка
Объединение нескольких фото в панораму. Нужна высокая точность совпадений — ложные пары сильно искажают результат.
detector=SIFT matcher=FLANN ratio_threshold=0.7
Поиск объекта на сцене
Найти деталь/объект на фотографии. Можно допустить больше совпадений — они фильтруются гомографией.
detector=ORB matcher=BFMatcher ratio_threshold=0.75
Быстрое грубое сравнение
Когда скорость важнее точности — предварительная фильтрация, batch-обработка.
detector=BRISK matcher=BFMatcher ratio_threshold=0.8
Максимальная точность
Точные измерения, стереозрение, калибровка — строгий порог отсекает все сомнительные пары.
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"
}
}
Surface Matching (3D-поверхности)
/api/v1/match/surface
Сопоставление 3D-облаков точек методом PPF (Point Pair Features) с опциональным уточнением ICP (Iterative Closest Point). Файлы в формате PLY.
Параметры (multipart/form-data)
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
model_file | file | ✓ | — | 3D-модель (.ply) — объект поиска |
scene_file | file | ✓ | — | 3D-сцена (.ply) — где искать |
relative_sampling_step | float | 0.05 | Шаг дискретизации (0.01–0.5). Меньше = точнее | |
relative_distance_step | float | 0.05 | Шаг хеширования PPF (0.01–0.5) | |
num_angles | int | 30 | Дискретизация вращения (1–360) | |
use_icp | bool | true | Уточнение позы через ICP | |
icp_tolerance | float | 0.005 | Порог сходимости ICP | |
icp_iterations | int | 100 | Макс. итераций 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-алгоритм использует их для вычисления фич.
Сценарии использования
Точное сопоставление деталей
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 (цепочка методов)
/api/v1/match/chain
Последовательный запуск нескольких методов сопоставления. Каждый шаг выполняется только если предыдущий не нашёл совпадение. Цепочка прерывается при первом успешном результате.
Логика работы
Воркер проходит шаги по порядку. Для каждого шага проверяется критерий «найдено»:
| Инструмент | Критерий «найдено» | Пояснение |
|---|---|---|
| Template Matching | filtered_matches > 0 | Есть совпадение выше порога после NMS |
| Feature Matching | good_matches > 0 | Есть пара ключевых точек после ratio test |
| Surface Matching | poses_found > 0 | PPF нашёл хотя бы одну позу |
Если критерий выполнен — цепочка останавливается (chain_result: "found"). Если ни один шаг не нашёл — "not_found". Пропущенные шаги имеют status: "skipped".
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
image | file | ✓ | — | Первое изображение (image / query / model) |
template | file | ✓ | — | Второе изображение (template / train / scene) |
steps | string (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 Matching | filtered_matches > 0 |
| Feature Matching | good_matches > 0 |
| Surface Matching | poses_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".
Задачи
/api/v1/tasks
Список всех задач с пагинацией.
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
offset | int | 0 | Смещение (query string) |
limit | int | 50 | Кол-во записей (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']}")
/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']}")
Настройки
Настройки по умолчанию для каждого инструмента. Применяются, когда параметры не переданы при отправке задачи.
/api/v1/settings/template
Получить текущие настройки Template Matching.
/api/v1/settings/template
Обновить настройки Template Matching.
/api/v1/settings/feature
Получить текущие настройки Feature Matching.
/api/v1/settings/feature
Обновить настройки Feature Matching.
/api/v1/settings/surface
Получить текущие настройки Surface Matching.
/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-запрос с результатом.
/api/v1/webhooks
Зарегистрировать новый вебхук. Тело — JSON:
| Поле | Тип | Обязательный | Описание |
|---|---|---|---|
url | string | ✓ | URL для POST-уведомлений |
secret | string | Секрет для подписи HMAC-SHA256. Если указан, запрос содержит заголовок X-Webhook-Signature |
/api/v1/webhooks
Список всех зарегистрированных вебхуков.
/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
Диаграмма потока вебхуков
POST /api/v1/webhooksPOST /api/v1/match/*POST /callbackОбработка ошибок
| Код | Ситуация | Тело ответа |
|---|---|---|
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
}
]
}