REST API reference¶
All paths are prefixed with /v1 unless noted. The canonical,
always-accurate reference is the live OpenAPI spec rendered as
Swagger UI on the OpenAPI page — this document gives
the resource-shape overview and the most-used endpoint groups for
quick orientation.
Resource model¶
flowchart LR
Tenant["Tenant"] --> Project["Project"]
Project --> Dataset["Dataset"]
Project --> Job["Job"]
Project --> Reconstruction["Reconstruction"]
Dataset --> Image["Image"]
Reconstruction --> SubModel["SubModel"]
Reconstruction --> SealedSnapshot["Sealed snapshot"]
Image -.-> Blob
Upload["Upload"] --> Blob["Blob"]
Job --> Task["Task"]
Resource |
Lifetime |
Owns |
Created via |
Notes |
|---|---|---|---|---|
|
persistent |
Datasets / Reconstructions / Jobs |
|
Top-level workspace |
|
persistent |
Images, ImageSource |
|
Discriminated |
|
persistent |
(links a |
|
Per-image EXIF / thumbnail / bytes endpoints |
|
TTL (24h default) |
one |
|
Chunked: |
|
content-addressed |
none (deduplicated) |
implicit from upload finalize |
sha256-keyed; reference-counted |
|
persistent |
Tasks, ProgressEvents |
every job-submitting |
LRO; |
|
persistent |
(worker result via |
created by orchestrator from DAG |
Status enum: `pending |
|
persistent |
SubModels + SealedSnapshots |
implicit from |
Status mirrors driving Job |
|
persistent |
one sealed dir per model |
implicit (one per pycolmap component) |
Indexed by |
|
append-only |
files (cameras, images, points.bin, …) |
implicit on phase boundary |
Reads via |
|
persistent |
tenant binding |
|
Raw key returned once at issue time |
Conventions¶
Auth: see authentication — default
auth_mode=nonefor dev;auth_mode=api_keyenablesAuthorization: Bearer <key>.Errors:
application/problem+jsonper RFC 7807 — see errors. Typederrors[]array on 422.IDs: 26-char ULIDs (Crockford base32, sortable, timestamp-prefixed).
Timestamps: ISO-8601 / RFC 3339 in UTC.
Pagination (AIP-158): keyset via
?page_token=+?page_size=. Responses includenext_page_token(nullends the cursor). Page tokens are opaque — clients MUST NOT parse them.Long-running ops:
POSTreturns202 AcceptedwithJobAcceptedResponse{job_id, task_ids[], …}and aLocation: /v1/jobs/{id}header. Cancel viaPOST /v1/jobs/{jid}:cancel(AIP-136 colon verb), notDELETE. Poll/v1/jobs/{jid}for lifecycle state,/v1/jobs/{jid}/progressfor dashboard-friendly progress, or/v1/jobs/{jid}/eventsfor the full event stream.Capabilities vs actions:
/v1/capabilitiesadvertises portable sfmapi feature flags only. Backend-native commands live in/v1/backend/actions; action ids are stable dot-namespaced strings. Treat action ids as opaque and URL-encode them when building path requests.Portable vs backend-specific options: stage specs keep portable knobs at the top level. Provider-specific knobs go in
backend_optionsand are discoverable from/v1/backend/config-schemas.Plugin hub: install and enable backend plugins with the
sfmapi plugins ...CLI or/v1/admin/plugins...operator routes. Public SfM job APIs never install plugins implicitly; HTTP execution requiresallow_unsafe_execution=true.Idempotency:
Idempotency-Keyheader onPOST /v1/uploads(replay-safe).Caching: sealed snapshot files emit strong
ETag+Cache-Control: public, max-age=31536000, immutable.SSE / WebSocket:
GET /v1/jobs/{id}/events(SSE) orGET /ws/v1/jobs/{id}(WS). Both honorLast-Event-IDresume.
Discovery & meta¶
Method |
Path |
Purpose |
|---|---|---|
GET |
|
Liveness probe |
GET |
|
Readiness — DB + queue checks |
GET |
|
sfmapi + backend SHAs |
GET |
|
Spec discovery envelope |
GET |
|
Backend feature flags (dot-notated names) |
GET |
|
Active backend identity, runtime versions, action links, and config-schema links |
GET |
|
Enabled providers discovered from installed plugins |
GET |
|
Provider priority and default routing-profile state |
GET |
|
Portable camera model parameter layouts |
GET |
|
OpenAPI 3.1 document |
GET |
|
Prometheus exposition |
Backend actions¶
Backend actions expose backend-native tools, such as COLMAP or OpenMVG commands, without turning those tool names into portable sfmapi capabilities.
Method |
Path |
Body / Query |
Returns |
|---|---|---|---|
GET |
|
- |
|
GET |
|
|
|
GET |
|
- |
|
POST |
|
|
|
POST |
|
|
202 + |
GET |
|
|
|
GET |
|
- |
|
GET |
|
|
|
GET |
|
- |
|
GET |
|
|
|
GET |
|
- |
|
GET /v1/backend/actions omits input_schema and output_schema by
default so catalog reads stay small. Pass include_schemas=true, or
read one action, when a UI needs form fields. :run enqueues a normal
backend_action job, returns Location: /v1/jobs/{id}, and includes
optional action_id and backend fields in the accepted-job body.
When SFMAPI_MCP_MODE=local or SFMAPI_MCP_ENABLED=true mounts MCP
into the API process, GET /v1/backend also advertises _links.mcp
and _links.mcp_status.
Backend config schemas describe valid keys for backend_options on
portable stage specs. They are scoped by stage, optional
capability, and optional provider. sfmapi validates unknown keys
and basic JSON types before queuing a job when the active backend
publishes a matching schema; otherwise the options pass through and
the backend validates them.
Backend artifact contracts describe the portable artifact kinds and
format ids a stage accepts and emits. Core sfmapi.*.v1 formats are
stable interchange contracts; backend-native formats remain
namespaced extensions such as colmap.features.database.v1 or
hloc.features.h5.v1. Conversions are explicit metadata on the
contract and should declare whether they are lossless.
Provider discovery is driven by sfm_hub plugin manifests plus local
install state. A clean sfmapi install can return an empty provider
page and still run the configured backend directly. Once multiple
enabled providers expose the same portable capability, sfmapi requires
either a request-level provider or a project, workspace, default, or
priority routing rule.
Example feature extraction request:
{
"spec": {
"type": "sift",
"max_num_features": 8192,
"backend_options": {
"SiftExtraction.peak_threshold": 0.01
}
}
}
Projects¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
|
GET |
|
|
|
GET |
|
— |
|
PATCH |
|
|
|
DELETE |
|
— |
204 |
POST |
|
|
202 + |
POST |
|
|
202 + |
PATCH accepts an optional AIP-161 update_mask query parameter. Mask
paths are comma-separated, body-relative, and must also be present in
the JSON body; without a mask, sfmapi applies the body fields that are
present.
Uploads (chunked)¶
Method |
Path |
Body / Headers |
Returns |
|---|---|---|---|
POST |
|
|
|
GET |
|
— |
|
PATCH |
|
raw chunk + |
|
POST |
|
|
|
Datasets & images¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
|
GET |
|
|
|
GET |
|
— |
|
PATCH |
|
|
|
DELETE |
|
— |
204 |
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
|
|
POST |
|
|
|
GET |
|
|
|
DELETE |
|
— |
204 |
DELETE |
|
legacy label-addressed delete |
204 |
GET |
|
— |
|
GET |
|
|
image bytes |
GET |
|
|
JPEG; requires the optional image-processing extra |
GET |
|
— |
|
GET / PUT / DELETE |
|
|
|
GET / PUT |
|
|
|
Projection jobs produce a projection.images.v1 stage artifact and a
projection_manifest.json. The manifest includes generic SfM metadata:
source_images, output_images, face geometry when applicable, and an
optional derived_dataset block. When output.create_dataset=true
(default), sfmapi registers the generated image directory as a normal
dataset so later feature, match, and mapping stages can consume it.
If the requested derived dataset name already exists in the project,
sfmapi appends a deterministic task suffix instead of failing the job;
task retries reuse the already-registered derived dataset.
The built-in projection engine is deliberately narrow. With the
projection extra installed it handles
projection.equirectangular_to_cubemap using vectorized NumPy plus
OpenCV image I/O, with nearest and linear sampling. cubic,
lanczos, projection.cubemap_to_equirectangular, and
projection.equirectangular_to_perspective are contract-only in core;
a backend must advertise those capabilities to serve them.
SfM stages (single-task jobs)¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
|
202 + |
PairsSpec and MatcherSpec are independent. A deployment can select
pairs with hloc-style retrieval and still match with a COLMAP or learned
matcher. Use optional provider only when two installed providers expose
the same portable capability.
Explicit pair lists are portable:
{
"pairs": {
"strategy": "explicit",
"image_pairs": [{"image_name1": "a.jpg", "image_name2": "b.jpg"}]
},
"matcher": {"type": "superglue", "provider": "hloc"}
}
For large hloc/COLMAP pair files, upload the text file through
/v1/uploads, finalize it, then pass
{"strategy": "explicit", "pairs_blob_sha": "<sha256>"}. The file
format is one image1 image2 pair per line.
Use input_artifacts to select a previous stage output, for example
{"features": {"artifact_id": "...", "kind": "features.local.v1"}}
when matching against a specific feature artifact.
Pipelines (recipe sugar)¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
202 + |
recipe ∈ {incremental, global, hierarchical, spherical} and
spec.kind MUST match.
Reconstructions / submodels / snapshots¶
Method |
Path |
Query / Body |
Returns |
|---|---|---|---|
GET |
|
- |
|
GET |
|
|
|
GET |
|
|
|
GET |
|
- |
|
GET |
|
- |
|
GET |
|
|
file bytes |
GET |
|
|
component file bytes |
GET |
|
- |
JSON |
GET |
|
- |
JSON |
POST |
|
|
202 + |
Mapping jobs persist one SubModel per disconnected component. The
root snapshot file routes expose the largest/default model; use the
submodel snapshot route for a specific component.
Stage artifacts are typed outputs produced by worker stages. They are the selection surface for ambiguous pipelines, for example when a job produces both COLMAP SIFT matches and hloc/LightGlue matches, or when verification emits several candidate pair sets.
Artifacts¶
Method |
Path |
Query |
Returns |
|---|---|---|---|
GET |
|
- |
|
GET |
|
- |
|
POST |
|
|
|
GET |
|
- |
|
GET |
|
|
file bytes |
POST |
|
|
|
POST |
|
|
202 + |
POST |
|
- |
|
StageArtifact.uri is metadata, not a portability contract. Use
/content for local server-managed file artifacts. Remote URIs and
paths outside sfmapi-managed storage are not dereferenced by the API.
Stage submissions can pass selected artifacts through
input_artifacts, for example:
{
"input_artifacts": {
"verified_matches": {
"artifact_id": "01HZ...",
"kind": "matches.verified.v1"
}
}
}
Core roles validate expected kinds before job creation. Unknown
backend-specific roles are allowed if they use the same dot-key syntax.
Use /v1/artifacts:import to register an existing server-local or
remote artifact URI without copying bytes. sfmapi creates a completed
import job/task so the artifact has the same ownership, listing, and
validation behavior as worker-produced artifacts.
Core portable artifact kinds include features.local.v1,
features.global.v1, pairs.image_names.v1, matches.indexed.v1,
matches.coordinates.v1, matches.dense.v1, matches.verified.v1,
and reconstruction.sparse.v1. Each kind has a default canonical
format id such as sfmapi.features.local.v1 or
sfmapi.matches.verified.v1. Backend-native artifacts should use a
namespaced same-family kind such as features.hloc_h5 or
matches.database.colmap, plus a backend-owned artifact_format
such as hloc.features.h5.v1 or colmap.matches.database.v1.
/v1/artifacts/{artifact_id}:conversionPlan chooses the shortest
conversion path from the active backend’s artifact contracts. Pass
accepted_formats in preference order to let sfmapi negotiate the
target format; pass to_format for an exact target. :convert
submits the selected conversion as a normal job and requires the
backend to implement convert_artifact(...). Multi-step paths are
executed inside that conversion task by calling the backend once per
step. :validate checks the artifact descriptor, core kind/format
compatibility, local managed files, declared byte sizes, SHA-256
digests, and JSON manifests when bytes are available.
{name} is one of cameras.json | images.json | rigs.json | frames.json | pose_graph.json | summary.json | points.bin | points_preview.bin | tiles/index.json | dense/index.json | dense/fused.bin.
Reconstruction-level stages (LRO)¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
|
202 + |
POST |
|
— |
202 + |
POST |
|
— |
202 + |
Similarity¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
GET |
|
|
|
POST |
|
|
200 ( |
One-shot (bytes-in / typed-result-out)¶
For “right now” use cases that don’t need a Project/Dataset row.
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
image bytes ( |
|
POST |
|
image bytes + |
|
Bytes are tempfile’d then deleted; no DB row is created. Capped at
SFMAPI_ONESHOT_MAX_REQUEST_BYTES (50 MiB default).
Jobs¶
Method |
Path |
Body |
Returns |
|---|---|---|---|
GET |
|
|
|
GET |
|
|
|
GET |
|
— |
|
GET |
|
— |
|
POST |
|
|
|
POST |
|
— |
202 + |
GET |
|
|
SSE stream of |
GET |
|
WebSocket upgrade |
bidirectional events |
?status= filters on the closed JobStatus set: pending | running | succeeded | failed | cancelled | cancelled_dirty.
Progress snapshots¶
GET /v1/jobs/{jid}/progress is a compact polling view over durable
task rows plus the latest persisted ProgressEvent records. It is
intended for CLIs and dashboards that do not want to hold an SSE
connection open.
curl -s "$BASE/v1/jobs/$JOB_ID/progress" \
| jq '{status, progress, current_task_kind, current_phase}'
Important fields:
Field |
Meaning |
|---|---|
|
Best-effort fraction from |
|
Count of tasks by lifecycle status |
|
Running task when present, otherwise next pending task |
|
Latest reported phase, such as |
|
Durable cursor shared with the SSE stream |
|
Per-task status, progress, latest event kind, elapsed time, and optional |
Progress is telemetry. Clients should not use exact percentages for
scheduling decisions; use status to decide whether work is terminal.
Admin¶
/v1/admin/api-keys are operator routes. They are not tenant-scoped
and are not protected by sfmapi’s tenant API-key dependency, so
production deployments must restrict them with an external admin-only
control-plane layer.
Method |
Path |
Body |
Returns |
|---|---|---|---|
POST |
|
|
|
GET |
|
— |
|
DELETE |
|
— |
|
GET |
|
|
|
GET |
|
— |
local external-tool detection |
GET |
|
|
installed Python entry points |
GET |
|
— |
plugin manifest + local state |
POST |
|
|
install plan or result |
POST |
|
— |
plugin state |
POST |
|
— |
plugin state |
POST |
|
— |
diagnostics |
POST |
|
|
routing state |
POST |
|
|
routing state |
POST |
|
|
routing state |
POST |
|
|
routing state |
Plugin installation is an operator action. method="uv" creates a
direct-reference command such as
uv pip install "sfmapi-colmap-cli @ git+https://github.com/SFMAPI/sfmapi_colmap_cli.git@main";
mutable refs produce warnings. docker and external_tool modes are
planned or recorded as runtime choices and are never invoked by normal
job submission. Backend packages can expose
[project.entry-points."sfmapi.backends"]; sfmapi plugins entry-points --load and sfmapi check-backend --load-entry-points
validate those contracts.
ProgressEvent¶
ProgressEvent v1 — discriminated union for the SSE stream.
Workers emit events to events.jsonl (one JSON line per event); the API serves them via SSE with Last-Event-ID resume.
Schema is versioned: a schema_version literal is pinned per kind. New kinds extend the union; old kinds never change shape.
ProgressEvent kinds
ProgressEvent is a tagged union discriminated on kind:
phase_started/phase_progress/phase_completed— per-phase lifecycle (phaseis one ofPhase).metric— scalar telemetry sample (key+value).snapshot_available— a sealed snapshot is now readable (snapshot_seq+ summary).log_line/warning/error— opaque message channels.
Forward-compatibility: SDKs MUST treat unknown kind values as
unknown and surface them through a generic catch-all rather than
crashing the stream. New kinds are additive (new kind literal
plus new arm of the union); existing kinds never change shape.
schema_version is pinned to 1 across every variant — bump it
in lock-step if the wire shape ever has to break, never per-kind.
- class app.schemas.progress_event.ErrorEvent(*, schema_version=1, ts, job_id, task_id=None, seq, kind='error', error_class, message, detail=None)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['error'])
error_class (str)
message (str)
detail (dict[str, Any] | None)
- kind: Literal['error']
- error_class: str
- message: str
- detail: dict[str, Any] | None
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.LogLine(*, schema_version=1, ts, job_id, task_id=None, seq, kind='log_line', level, message)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['log_line'])
level (Literal['DEBUG', 'INFO', 'WARNING', 'ERROR'])
message (str)
- kind: Literal['log_line']
- level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR']
- message: str
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.Metric(*, schema_version=1, ts, job_id, task_id=None, seq, kind='metric', key, value)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['metric'])
key (str)
value (float)
- kind: Literal['metric']
- key: str
- value: float
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.PhaseCompleted(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_completed', phase)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['phase_completed'])
phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])
- kind: Literal['phase_completed']
- phase: Phase
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.PhaseProgress(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_progress', phase, current, total=None, rate=None)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['phase_progress'])
phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])
current (int)
total (int | None)
rate (float | None)
- kind: Literal['phase_progress']
- phase: Phase
- current: int
- total: int | None
- rate: float | None
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.PhaseStarted(*, schema_version=1, ts, job_id, task_id=None, seq, kind='phase_started', phase)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['phase_started'])
phase (Literal['feature_extraction', 'matching', 'geometric_verification', 'incremental_init', 'incremental_register', 'incremental_ba', 'global_rotation_avg', 'global_positioning', 'global_ba', 'hierarchical_cluster', 'hierarchical_merge', 'panorama', 'spherical', 'bundle_adjust', 'triangulate', 'relocalize', 'pose_graph_optimize', 'segment', 'export', 'vlad_index', 'backend_action', 'artifact_conversion'])
- kind: Literal['phase_started']
- phase: Phase
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.SnapshotAvailable(*, schema_version=1, ts, job_id, task_id=None, seq, kind='snapshot_available', snapshot_seq, summary)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['snapshot_available'])
snapshot_seq (int)
summary (dict[str, Any])
- kind: Literal['snapshot_available']
- snapshot_seq: int
- summary: dict[str, Any]
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.progress_event.Warning_(*, schema_version=1, ts, job_id, task_id=None, seq, kind='warning', message)[source]
Bases:
_Base- Parameters:
schema_version (Literal[1])
ts (datetime)
job_id (str)
task_id (str | None)
seq (int)
kind (Literal['warning'])
message (str)
- kind: Literal['warning']
- message: str
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Pipeline specs¶
PipelineSpec — discriminated union of mapping pipelines.
Web layer accepts these. Workers translate them to engine-specific option classes inside the backend implementation — never out here.
PipelineSpec kinds
PipelineSpec is a tagged union discriminated on the kind
field:
incremental->IncrementalSpecglobal->GlobalSpechierarchical->HierarchicalSpecspherical->SphericalSpec
Forward-compatibility: SDKs MUST treat unknown kind values as
unsupported rather than failing the whole response. Add new
variants in additive form (new kind literal + new arm of the
union); never repurpose an existing kind. Backends advertise the
recipes they implement via the pipelines.{kind} capability flags
(see GET /v1/capabilities); requests against an unsupported
kind return 501 capability_unavailable.
- app.schemas.pipeline_spec.BackendOptions
Backend-specific option bag.
Portable fields on the stage specs are the stable sfmapi contract.
backend_optionsis reserved for options discovered fromGET /v1/backend/config-schemasand interpreted by the selected backend provider.alias of
dict[str,Any]
- class app.schemas.pipeline_spec.BundleAdjustmentSpec(*, version=1, mode='standard', provider=None, refine_focal_length=True, refine_principal_point=False, refine_extra_params=True, max_num_iterations=100, loss_kernel='squared', loss_threshold=1.0, backend_options=<factory>)[source]
Bases:
BaseModelStandalone bundle-adjustment spec.
modeselects the algorithm:standard: a single ceres / baxx solve over all registered cameras + 3D points (capabilityba.standard).two_stage: a two-pass refinement (capabilityba.two_stage).featuremetric: Pixel-Perfect SfM-style refinement that minimizes a CNN-feature error, not raw reprojection (capabilityba.featuremetric). Requires a backend with learned-feature support.
loss_kernelchooses the robust loss applied to per-residual cost:squared(no robustification),huber,cauchy,soft_l1,tukey.loss_thresholdis the kernel scale (in pixels for reprojection loss).- Parameters:
version (Literal[1])
mode (Literal['standard', 'two_stage', 'featuremetric'])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
refine_focal_length (bool)
refine_principal_point (bool)
refine_extra_params (bool)
max_num_iterations (int)
loss_kernel (Literal['squared', 'huber', 'cauchy', 'soft_l1', 'tukey'])
loss_threshold (float)
backend_options (dict[str, Any])
- version: Literal[1]
- mode: Literal['standard', 'two_stage', 'featuremetric']
- provider: str | None
- refine_focal_length: bool
- refine_principal_point: bool
- refine_extra_params: bool
- max_num_iterations: int
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- loss_kernel: Literal['squared', 'huber', 'cauchy', 'soft_l1', 'tukey']
- loss_threshold: float
- backend_options: BackendOptions
- app.schemas.pipeline_spec.FeatureType
Canonical names for local feature extractors. The capability flag for an extractor is
features.extract.{type}; the colmap_mod backend advertisesfeatures.extract.siftand (when pycolmap exposes it)features.extract.aliked.alias of
Literal[‘sift’, ‘superpoint’, ‘aliked’, ‘disk’, ‘r2d2’, ‘d2net’]
- class app.schemas.pipeline_spec.FeaturesSpec(*, version=1, type='sift', provider=None, max_num_features=8192, use_gpu=True, seed=0, backend_options=<factory>, input_artifacts=<factory>)[source]
Bases:
BaseModelType-tagged feature extractor request.
Backends report which
typevalues they support via thefeatures.extract.{type}capability flags. Unsupported types return 501 with the canonical capability name.Backend-specific extractor controls belong in
backend_options.- Parameters:
version (Literal[1])
type (Literal['sift', 'superpoint', 'aliked', 'disk', 'r2d2', 'd2net'])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
max_num_features (int)
use_gpu (bool)
seed (int)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
- version: Literal[1]
- type: FeatureType
- provider: str | None
- max_num_features: int
- use_gpu: bool
- seed: int
- backend_options: BackendOptions
- input_artifacts: ArtifactInputMap
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.GlobalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='global', backend='AUTO', formulation='AUTO', use_incremental_quality_fallback=True)[source]
Bases:
_SpecBase- Parameters:
version (Literal[1])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
seed (int)
max_runtime_seconds (int | None)
snapshot_frames_freq (int | None)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
kind (Literal['global'])
backend (Literal['AUTO', 'BAXX', 'CERES'])
formulation (Literal['AUTO', 'EXPLICIT_SCALE', 'ELIMINATED_SCALE'])
use_incremental_quality_fallback (bool)
- kind: Literal['global']
- backend: Literal['AUTO', 'BAXX', 'CERES']
- formulation: Literal['AUTO', 'EXPLICIT_SCALE', 'ELIMINATED_SCALE']
- use_incremental_quality_fallback: bool
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.HierarchicalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='hierarchical', cluster_max_size=100, cluster_overlap=25)[source]
Bases:
_SpecBase- Parameters:
version (Literal[1])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
seed (int)
max_runtime_seconds (int | None)
snapshot_frames_freq (int | None)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
kind (Literal['hierarchical'])
cluster_max_size (int)
cluster_overlap (int)
- kind: Literal['hierarchical']
- cluster_max_size: int
- cluster_overlap: int
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.ImagePairRef(*, image_name1, image_name2)[source]
Bases:
BaseModelOne explicit pair of dataset image names.
- Parameters:
image_name1 (Annotated[str, MinLen(min_length=1), MaxLen(max_length=2048)])
image_name2 (Annotated[str, MinLen(min_length=1), MaxLen(max_length=2048)])
- image_name1: str
- image_name2: str
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.IncrementalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='incremental', init_image_pair=None, multiple_models=True, max_num_models=50, min_num_matches=15, ba_global_use_pba=True, extract_colors=True)[source]
Bases:
_SpecBase- Parameters:
version (Literal[1])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
seed (int)
max_runtime_seconds (int | None)
snapshot_frames_freq (int | None)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
kind (Literal['incremental'])
init_image_pair (tuple[str, str] | None)
multiple_models (bool)
max_num_models (int)
min_num_matches (int)
ba_global_use_pba (bool)
extract_colors (bool)
- kind: Literal['incremental']
- init_image_pair: tuple[str, str] | None
- multiple_models: bool
- max_num_models: int
- min_num_matches: int
- ba_global_use_pba: bool
- extract_colors: bool
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.MatcherSpec(*, version=1, type='nn-mutual', provider=None, use_gpu=True, cross_check=True, max_ratio=0.8, max_distance=0.7, backend_options=<factory>, input_artifacts=<factory>)[source]
Bases:
BaseModelPer-pair feature matcher.
nn-mutualis the COLMAP default (mutual nearest-neighbor).nn-ratioadds Lowe’s ratio test.superglue/lightglueare learned matchers.loftris semi-dense (no separate extractor — setFeaturesSpec.typeto a placeholder).- Parameters:
version (Literal[1])
type (Literal['nn-mutual', 'nn-ratio', 'superglue', 'lightglue', 'loftr', 'mast3r'])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
use_gpu (bool)
cross_check (bool)
max_ratio (float)
max_distance (float)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
- version: Literal[1]
- type: MatcherType
- provider: str | None
- use_gpu: bool
- cross_check: bool
- max_ratio: float
- max_distance: float
- backend_options: BackendOptions
- input_artifacts: ArtifactInputMap
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- app.schemas.pipeline_spec.MatcherType
Per-pair matching algorithm. The capability flag is
matchers.{type}. Backends pick the subset they implement.alias of
Literal[‘nn-mutual’, ‘nn-ratio’, ‘superglue’, ‘lightglue’, ‘loftr’, ‘mast3r’]
- app.schemas.pipeline_spec.PairStrategy
How to pick which image pairs to match.
exhaustive: every pair (O(N²)).sequential: consecutive pairs withinoverlapwindow (video / time-ordered).spatial: pairs within a metric distance (needs GPS or pose priors).vocabtree: COLMAP vocabulary tree retrieval.retrieval: pairs from a similarity index (VLAD / NetVLAD / custom). Requires the dataset to have a built similarity index.from_poses: pairs whose camera centers are withinoverlap_distance_mAND whose principal axes are withinmax_angle_deg.explicit: use exactly the image-name pairs supplied inline or through an uploaded pair-list blob.
alias of
Literal[‘exhaustive’, ‘sequential’, ‘spatial’, ‘vocabtree’, ‘retrieval’, ‘from_poses’, ‘explicit’]
- class app.schemas.pipeline_spec.PairsSpec(*, version=1, strategy='exhaustive', provider=None, overlap=10, vocab_tree_path=None, retrieval_strategy='vlad', retrieval_k=20, overlap_distance_m=None, max_angle_deg=None, image_pairs=None, pairs_blob_sha=None, pairs_blob_format='image_name_pairs_txt', backend_options=<factory>, input_artifacts=<factory>)[source]
Bases:
BaseModelPair-selection strategy. Decoupled from the matcher so the standard supports “select pairs with hloc-style retrieval, then match with any local-feature matcher” workflows.
Capability flag is
pairs.{strategy}.- Parameters:
version (Literal[1])
strategy (Literal['exhaustive', 'sequential', 'spatial', 'vocabtree', 'retrieval', 'from_poses', 'explicit'])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
overlap (int)
vocab_tree_path (str | None)
retrieval_strategy (Literal['dhash', 'vlad', 'netvlad'])
retrieval_k (int)
overlap_distance_m (float | None)
max_angle_deg (float | None)
image_pairs (list[ImagePairRef] | None)
pairs_blob_sha (Annotated[str | None, MinLen(min_length=64), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[0-9a-f]{64}$')])
pairs_blob_format (Literal['image_name_pairs_txt'])
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
- version: Literal[1]
- strategy: PairStrategy
- provider: str | None
- overlap: int
- vocab_tree_path: str | None
- retrieval_strategy: Literal['dhash', 'vlad', 'netvlad']
- retrieval_k: int
- overlap_distance_m: float | None
- max_angle_deg: float | None
- image_pairs: list[ImagePairRef] | None
- pairs_blob_sha: str | None
- pairs_blob_format: Literal['image_name_pairs_txt']
- backend_options: BackendOptions
- input_artifacts: ArtifactInputMap
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.SphericalSpec(*, version=1, provider=None, seed=0, max_runtime_seconds=None, snapshot_frames_freq=50, backend_options=<factory>, input_artifacts=<factory>, kind='spherical', panorama=True)[source]
Bases:
_SpecBase- Parameters:
version (Literal[1])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
seed (int)
max_runtime_seconds (int | None)
snapshot_frames_freq (int | None)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
kind (Literal['spherical'])
panorama (bool)
- kind: Literal['spherical']
- panorama: bool
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class app.schemas.pipeline_spec.VerifySpec(*, version=1, provider=None, use_gpu=True, min_inlier_ratio=0.25, backend_options=<factory>, input_artifacts=<factory>)[source]
Bases:
BaseModel- Parameters:
version (Literal[1])
provider (Annotated[str | None, MinLen(min_length=1), MaxLen(max_length=64), _PydanticGeneralMetadata(pattern='^[A-Za-z0-9][A-Za-z0-9_.-]*$')])
use_gpu (bool)
min_inlier_ratio (float)
backend_options (dict[str, Any])
input_artifacts (dict[str, ArtifactRef])
- version: Literal[1]
- provider: str | None
- use_gpu: bool
- min_inlier_ratio: float
- backend_options: BackendOptions
- input_artifacts: ArtifactInputMap
- model_config = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
Binary points format¶
Content-Type: application/x-sfm-points-v1. Fixed 32-byte header,
26 bytes per point.
Binary points wire format — application/x-sfm-points-v1.
- Header (44 bytes, little-endian):
magic: 8 bytes = b”SFMP3Dx00x00” version: uint32 = 1 count: uint64 bbox_min: 3 x float32 (12 B) bbox_max: 3 x float32 (12 B)
- Each record (26 bytes, little-endian):
xyz: 3 x float32 (12) rgb: 3 x uint8 (3) _pad: 1 x uint8 (1) track_len: uint16 (2) point3d_id: uint64 (8)
Records are written in ascending point3d_id order so that HTTP Range requests can treat the file as a fixed-stride array.
- class app.schemas.points_binary.Point3DRecord(point3d_id: 'int', xyz: 'tuple[float, float, float]', rgb: 'tuple[int, int, int]', track_len: 'int')[source]
Bases:
object- Parameters:
point3d_id (int)
xyz (tuple[float, float, float])
rgb (tuple[int, int, int])
track_len (int)
- point3d_id: int
- xyz: tuple[float, float, float]
- rgb: tuple[int, int, int]
- track_len: int
- app.schemas.points_binary.write_header(fh, *, count, bbox_min, bbox_max)[source]
- Parameters:
fh (BinaryIO)
count (int)
bbox_min (tuple[float, float, float])
bbox_max (tuple[float, float, float])
- Return type:
None
- app.schemas.points_binary.write_record(fh, rec)[source]
- Parameters:
fh (BinaryIO)
rec (Point3DRecord)
- Return type:
None
- app.schemas.points_binary.read_header(fh)[source]
- Parameters:
fh (BinaryIO)
- Return type:
tuple[int, tuple[float, float, float], tuple[float, float, float]]
- app.schemas.points_binary.read_record(buf, offset=0)[source]
- Parameters:
buf (bytes)
offset (int)
- Return type:
Point3DRecord
- app.schemas.points_binary.encode_all(records, *, bbox_min, bbox_max)[source]
- Parameters:
records (Iterable[Point3DRecord])
bbox_min (tuple[float, float, float])
bbox_max (tuple[float, float, float])
- Return type:
bytes
- app.schemas.points_binary.decode_records(buf)[source]
- Parameters:
buf (bytes)
- Return type:
tuple[list[Point3DRecord], tuple[float, float, float], tuple[float, float, float]]
Records are written in ascending point3d_id order so HTTP Range
requests treat the file as a fixed-stride array. points_preview.bin
is the same format, decimated.