Skip to content

Stabilo — Detailed Usage Guide

Command-line interface: installing stabilo provides a stabilo console command with video, tracks, and config subcommands. See 15. Command-Line Interface and the scripts/README.md for the threshold-analysis tooling.

CUDA GPU acceleration (gpu=True) requires a CUDA-enabled OpenCV build; see docs/cuda.md for build instructions. Learning-based detectors instead run on any torch device via the device option.


Table of Contents


1. Core Functionality

Stabilo aligns a current frame to a reference frame using a feature-point pipeline:

  1. Pre-processing — optionally apply CLAHE for contrast enhancement and downsample the frame.
  2. Masking — build a binary mask from user-supplied exclusion regions so that dynamic objects (e.g., vehicles) are excluded from feature extraction.
  3. Feature detection and description — detect and describe keypoints using a chosen classical detector (ORB, SIFT, rSIFT, BRISK, KAZE, AKAZE) or learning-based detector (XFeat, DISK, DeDoDe, KeyNet), or match densely with the detector-free LoFTR.
  4. Matching — match descriptors between the current and reference frame using a brute-force (BF), FLANN, or learned LightGlue matcher.
  5. Match filtering — filter matches by cross-check, Lowe's ratio test, or distance threshold.
  6. Transformation estimation — robustly estimate a 3x3 homography (projective) or 2x3 affine matrix using a RANSAC-type algorithm (MAGSAC++ by default).
  7. Stabilisation — warp the current frame, or transform tracked bounding boxes, into the reference coordinate system.

This pipeline makes Stabilo suitable for:

  • video stabilisation (align all frames to a chosen anchor frame),
  • trajectory stabilisation (transform per-frame detections/tracks to a fixed reference),
  • any downstream analysis requiring geometric frame-to-frame consistency.

2. Installation

It is recommended to create and activate a Python virtual environment (Python >= 3.11 and <= 3.13) first:

python3.11 -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
Alternatives: conda or uv

Miniconda:

conda create -n stabilo python=3.11 -y
conda activate stabilo

uv (fastest; use uv pip install in the step below):

uv venv --python 3.11
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

Then, install the stabilo package using one of the following options:

Option 1 — from PyPI (recommended):

pip install stabilo

Option 2 — from local source (for development):

git clone https://github.com/rfonod/stabilo.git
cd stabilo
pip install -e '.[dev]'  # editable install with dev dependencies


3. Main Workflow

import numpy as np
from stabilo import Stabilizer

# 1. Create a stabilizer (all arguments are optional; defaults shown below)
stabilizer = Stabilizer(
    detector_name='orb',
    matcher_name='bf',
    filter_type='ratio',
    transformation_type='projective',
    max_features=2000,
)

# 2. Set the reference frame (once)
#    Optionally supply bounding-box masks to exclude dynamic regions.
stabilizer.set_ref_frame(ref_frame, ref_boxes, box_format='xywh')

# 3. Stabilize each subsequent frame
stabilizer.stabilize(cur_frame, cur_boxes, box_format='xywh')

# 4. Retrieve results
stabilized_frame  = stabilizer.warp_cur_frame()              # warped frame
stabilized_boxes  = stabilizer.transform_cur_boxes()         # transformed boxes (xywh)
trans_matrix      = stabilizer.get_cur_trans_matrix()        # 3x3 or 2x3 matrix

Key constraints

  • set_ref_frame(...) must be called before stabilize(...).
  • All frames passed to stabilize(...) are aligned to the single reference frame; call set_ref_frame(...) again to change the anchor.
  • If a transformation cannot be estimated (too few matches), Stabilo falls back to the last known transformation matrix (or None if no previous matrix exists).

4. Supported Mask and Box Formats

The box_format argument is accepted by set_ref_frame, stabilize, and create_binary_mask. It controls how the boxes array is interpreted. The transform_boxes(...) method uses in_box_format and out_box_format to specify the input and output box formats.

xywh (default)

Axis-aligned bounding box, one row per box:

[x_center, y_center, width, height]

xywha

Oriented bounding box (OBB) with an explicit rotation angle:

[x_center, y_center, width, height, angle_degrees]

angle_degrees is the counter-clockwise rotation from the positive x-axis.

four

Four corner points (clockwise or arbitrary order), one row per box:

[x1, y1, x2, y2, x3, y3, x4, y4]

Stabilo automatically detects whether the box is axis-aligned or rotated. For axis-aligned boxes fast rectangular masking is used; rotated boxes use polygon filling.

polygon

Arbitrary convex or non-convex polygons with N >= 3 vertices. Two input shapes are accepted:

  • Flattened row — one polygon per row: [x1, y1, x2, y2, ..., xN, yN] (all rows must have the same even length >= 6)
  • Vertex-pair array — shape (N, 2) for a single polygon, or shape (P, N, 2) for P polygons
# Flattened rows (two quadrilaterals)
polygon_masks = np.array([
    [60, 60, 120, 60, 120, 120, 60, 120],
    [200, 200, 260, 210, 240, 270, 190, 260],
])
stabilizer.set_ref_frame(ref_frame, polygon_masks, box_format='polygon')

circle

Circular exclusion regions, one row per circle:

[x_center, y_center, radius]
circle_masks = np.array([[120, 120, 25], [360, 240, 40]])
stabilizer.stabilize(cur_frame, circle_masks, box_format='circle')

Note — polygon and circle formats are supported only for masking (i.e., feature-extraction exclusion). They cannot be passed to transform_cur_boxes() / transform_boxes(), which require xywh, xywha, or four.


5. Masking Behaviour

When boxes are provided, create_binary_mask builds a single-channel uint8 image (same height x width as the reference frame) where:

Pixel value Meaning
255 Included in feature extraction
0 Excluded from feature extraction

The mask_margin_ratio parameter (default 0.15) expands each exclusion region by 15% to add a safety margin:

Format How margin is applied
xywh Width and height each multiplied by (1 + margin_ratio)
xywha Width and height expanded, then the OBB is converted to four corner points
four — aligned Same as xywh after bounding-box extraction
four — rotated Corner points scaled away from their centroid
polygon Vertices scaled away from their centroid
circle Radius multiplied by (1 + margin_ratio)

Masks for the reference and current frame are independent — each frame may have its own set of exclusion boxes.

Regions are clipped to the frame: one that is partly outside excludes only its visible part, and one wholly outside the frame excludes nothing. A region with a NaN or infinite coordinate is skipped with a warning instead of stopping the run.

If mask_use=False, no mask is created even if boxes are supplied.


6. Frame and Box Transformation

Warp a frame

stabilized = stabilizer.warp_cur_frame()
# or warp an arbitrary frame with the current matrix:
stabilized = stabilizer.warp_frame(some_frame)

For projective transformations cv2.warpPerspective is used; for affine cv2.warpAffine is used.

Transform bounding boxes

# transform boxes that were passed to stabilize(...)
out_boxes = stabilizer.transform_cur_boxes(out_box_format='xywh')

# transform arbitrary boxes
out_boxes = stabilizer.transform_boxes(
    boxes,
    trans_matrix,
    in_box_format='xywha',
    out_box_format='four',
)

Internally, every box format is first converted to four corner points (four), those points are transformed with the matrix, then converted back to the requested output format.

Supported in_box_format / out_box_format values for transformation: 'xywh', 'xywha', 'four'.


7. Point Transformation

To transform an arbitrary pixel coordinate from the current frame to the reference frame:

# projective (homography)
cur_point = np.array([x, y, 1.0])
ref_point_h = stabilizer.get_cur_trans_matrix() @ cur_point
ref_point = ref_point_h[:2] / ref_point_h[2]   # divide by homogeneous coordinate

# affine (no perspective division needed — last row is [0, 0, 1])

8. Configuration Parameters

All parameters can be passed as keyword arguments to Stabilizer(...) or set via a YAML file (see stabilo/cfg/default.yaml). Parameters not supplied fall back to their defaults.

stabilo/cfg/default.yaml is the complete list. A keyword argument whose name is not one of its keys is ignored, so the parameter it was meant to set keeps its default; stabilo logs a warning naming the unrecognized arguments and, where there is one, the nearest valid name. Stabilizer.configurable_keys() returns the valid names, for a caller that wants to check or filter a configuration itself.

Parameter Default Valid values Description
detector_name 'orb' orb, sift, rsift, brisk, kaze, akaze, xfeat, disk, dedode, keynet, loftr Feature detector (see section 9)
matcher_name 'bf' bf, flann, lightglue Feature matcher (see section 10)
filter_type 'ratio' none, ratio, distance Match filtering strategy
filter_ratio 0.9 (0, 1] Lowe's ratio (for ratio) or distance threshold ratio (for distance)
match_query_frame 'reference' reference, current Which frame's descriptors form the knnMatch query (matching is asymmetric)
transformation_type 'projective' projective, affine Geometric model
clahe false true, false Apply CLAHE contrast enhancement
downsample_ratio 0.5 (0, 1] Resize factor before feature extraction
max_features 2000 > 0 (int) Maximum keypoints kept in the current frame, the strongest by response
ref_multiplier 2.0 >= 1.0 Scale factor for keypoints in reference frame (ref_multiplier x max_features)
sift_enable_precise_upscale false true, false SIFT/rSIFT only: enable precise sub-pixel upscaling at octave -1
rsift_eps 1e-8 > 0 RootSIFT (rsift) L1-normalization epsilon
mask_use true true, false Enable exclusion masking
mask_margin_ratio 0.15 [0, 1] Fractional margin added to exclusion regions
ransac_method 38 see section 11 RANSAC algorithm (affine supports only 4 and 8)
ransac_epipolar_threshold 2.0 > 0 Reprojection-error threshold, in pixels of ransac_threshold_space
ransac_threshold_space 'full' full, processed Pixels the threshold is measured in: full-resolution, or the downsampled image (see section 11)
ransac_max_iter 5000 > 0 (int) Maximum RANSAC iterations
ransac_confidence 0.999999 (0, 1] Required confidence level
brisk_threshold 30 (0, 255] BRISK detection threshold (AGAST score)
kaze_threshold 0.001 > 0 KAZE detection threshold (detector response)
akaze_threshold 0.001 > 0 AKAZE detection threshold (detector response)
gpu false true, false Use CUDA GPU acceleration for detection/matching/warping; requires a CUDA-enabled OpenCV build, see docs/cuda.md
gpu_device_id 0 >= 0 (int) CUDA device index to use when gpu is true
device 'auto' auto, cpu, cuda, mps Torch device for learning-based detectors/matchers only (auto picks cuda > mps > cpu)
loftr_weights 'outdoor' outdoor, indoor LoFTR pretrained weights
loftr_confidence 0.0 [0, 1] Minimum LoFTR correspondence confidence to keep (0.0 keeps all)
disk_weights 'depth' depth, epipolar DISK pretrained weights
dedode_detector_weights 'L-C4-v2' L-upright, L-C4, L-SO2, L-C4-v2 DeDoDe detector weights
dedode_descriptor_weights 'B-upright' B-upright, G-upright, B-C4, B-SO2, G-C4, G-SO2 DeDoDe descriptor weights. The G variants pull a 1.2 GB DINOv2 backbone and roughly double the memory for no rotation benefit, so B is the default
logger None logging.Logger Optional external logger (constructor-only; not a YAML key)
viz false true, false Retain intermediate data for visualisation
benchmark false true, false Benchmarking mode (see section 13)
min_good_match_count_warning 20 >= 0 Warn if fewer than N good matches found
min_inliers_match_count_warning 10 >= 0 Warn if fewer than N inliers found

9. Feature Detectors

Classical detectors (OpenCV):

Name Type Notes
orb Binary Fast; uses Hamming distance. Default.
sift Float Scale and rotation invariant; uses L2 distance.
rsift Float RootSIFT variant of SIFT with improved matching quality.
brisk Binary Detection threshold brisk_threshold.
kaze Float Non-linear scale-space detector; detection threshold kaze_threshold.
akaze Binary Accelerated KAZE; detection threshold akaze_threshold.

Every classical detector keeps at most max_features keypoints in the current frame and ref_multiplier x max_features in the reference frame, the strongest by detector response, counted in the processed (downsampled) image. ORB and SIFT do this natively. BRISK, KAZE, and AKAZE detect every keypoint whose response exceeds their threshold (brisk_threshold, kaze_threshold, akaze_threshold, OpenCV's defaults) and stabilo keeps the strongest. The cap therefore holds at any resolution and downsample_ratio, but it is a maximum, not a target: a small or weakly textured frame can yield fewer keypoints, in which case lowering the threshold yields more. Before v1.4.5, BRISK, KAZE, and AKAZE converted max_features into a threshold with linear models fitted at native resolution, which ignored downsample_ratio and, for large feature counts, went negative and kept every keypoint.

Learning-based detectors (kornia, core dependency; pretrained weights download on first use and run on the device):

Name Type Rotation invariant Notes
xfeat Float (64-d) ❌ Fast learned sparse detector/descriptor, and by far the lightest of the learned models. Works with bf/flann.
disk Float (128-d) ❌ Robust learned sparse features. Works with bf/flann/lightglue.
dedode Float (256-d) ❌ Decoupled detect-and-describe; strong but slower. Works with bf/flann/lightglue. Weights via dedode_detector_weights / dedode_descriptor_weights.
keynet Float (128-d) ✅ KeyNet detector + HardNet descriptor, with an OriNet orientation estimator. The only rotation-invariant learned option. Works with bf/flann/lightglue.
loftr Detector-free ❌ Semi-dense image-pair matching; produces correspondences directly, so filter_type and the bf/flann choice of matcher_name are ignored (lightglue is rejected). Weights via loftr_weights; filter correspondences with loftr_confidence.

Learning-based detectors receive the same CLAHE, downsampling, and exclusion-mask handling as the classical ones: masks are applied by discarding keypoints (or LoFTR correspondences) that fall in excluded regions, and keypoint coordinates are rescaled back to full resolution after downsampled detection. max_features caps the number of detected keypoints; the brisk_threshold/kaze_threshold/akaze_threshold options do not apply. LoFTR matches on a coarse grid of stride 8, so its input is zero-padded at the bottom and right to a multiple of 8 and correspondences falling in the padding are dropped. Before v1.4.5 it was not padded, and a processed frame whose sides are not multiples of 8 (for example 691×389, a 4K frame at downsample_ratio: 0.18) was registered with a systematic error that grows toward the far edges, several to tens of pixels at 4K. Select the compute device with device (auto picks cuda > mps > cpu). Note that gpu=True (OpenCV CUDA) is incompatible with learning-based detectors; use device='cuda' instead.

Pretrained weights are downloaded once, on first use, into torch's hub cache (~/.cache/torch/hub/checkpoints on Linux and macOS, %USERPROFILE%\.cache\torch\hub\checkpoints on Windows). Set the TORCH_HOME environment variable to relocate it. Stabilo logs whether the weights were downloaded or reused from that cache, so a run that stalls on a first-use download is easy to recognise.

Rotation invariance

xfeat, disk, dedode, and loftr are trained on upright imagery and do not estimate a keypoint orientation. Matching against the reference frame degrades as the current frame rotates, and collapses beyond roughly 30°. Measured on a synthetically rotated frame pair, the mean reprojection error of the recovered homography is:

Detector 0° 10° 30° 90°
orb, sift 0.0 px 0.4 px 0.6 px 1.2 px
keynet 0.0 px 0.7 px 1.3 px 2.7 px
xfeat 0.0 px 0.9 px 1.8 px fails
disk 0.0 px 0.4 px 11.8 px fails
dedode 0.0 px 0.5 px 1.1 px fails
loftr 0.1 px 0.5 px 1.3 px fails

For footage with large in-plane rotation, such as a drone orbiting or yawing about its own axis, use a classical detector or keynet. Note that DeDoDe's C4/SO2 weight variants are not a workaround: they require a steerer to be applied at match time, which kornia does not do.

Choosing a downsample ratio

The learned models allocate memory in proportion to the processed frame size, which is downsample_ratio² times the input resolution. They are much hungrier than the classical detectors: at roughly 0.5 MP, disk, dedode, keynet, and loftr each peak at several GB of RAM on CPU, while xfeat stays a few hundred MB. loftr is the extreme case because its coarse matching builds an N×N confidence matrix over N = (H/8)·(W/8) tokens, so its cost grows quadratically: a 4K frame at the default downsample_ratio: 0.5 yields a 3.9 GB matrix (before intermediates), and at full resolution over 60 GB.

Stabilo emits a warning, but does not cap the resolution, when the processed frame exceeds the guideline below:

Detector Guideline (processed frame) Suggested for a 4K input (3840×2160)
xfeat 2.0 MP --downsample-ratio 0.49
disk, dedode, keynet 0.5 MP --downsample-ratio 0.24
loftr 0.3 MP --downsample-ratio 0.19

These are guidelines for a machine with a few GB of free RAM, not hard limits; a large GPU tolerates more. Lower downsample_ratio further if the process is killed or the machine begins to swap.

Running out of device memory is raised, not treated as an unregistrable frame. A torch.OutOfMemoryError, or a RuntimeError whose message says the device or host allocator ran out of memory (including MPS's Invalid buffer size), propagates out of set_ref_frame(...) and stabilize(...) so the caller can tell an infeasible configuration apart from an estimation failure. Before v1.4.3, the sparse detectors (xfeat, disk, dedode, keynet) caught it, logged that no features were found, and fell back like any other failed frame; loftr and lightglue always raised it.


10. Feature Matching and Filtering

Three matchers are supported:

  • bf — OpenCV BFMatcher. Uses crossCheck=True for filter_type='none' and 'distance'; crossCheck=False for filter_type='ratio'.
  • flann — OpenCV FlannBasedMatcher. Uses LSH index for binary descriptors and KD-Tree for float descriptors.
  • lightglue — learned kornia LightGlueMatcher, compatible with the disk, dedode, and keynet detectors only. It performs its own filtering, so filter_type is ignored. (The detector-free loftr needs no matcher.)

Three filtering strategies:

filter_type Behaviour
none Keep all matches returned by the matcher
ratio Lowe's ratio test: keep match m where m.distance < filter_ratio x n.distance
distance Distance threshold: keep matches below min_dist + (max_dist - min_dist) x filter_ratio

knnMatch is asymmetric, so the set of surviving matches depends on which descriptors are used as the query. By default (match_query_frame='reference') the reference frame's descriptors are the query; set match_query_frame='current' to query with the current frame's descriptors instead. The estimated transform still maps current → reference in both cases.


11. Transformation Types and RANSAC Methods

Transformation types

transformation_type Matrix shape Function
projective 3 x 3 cv2.findHomography
affine 2 x 3 cv2.estimateAffinePartial2D (4-DOF similarity)

Use projective (default) when the camera undergoes any motion (pan, tilt, zoom, rotation). Use affine to restrict the estimate to a 4-DOF similarity transform: rotation, uniform scale, and translation. Despite the name, cv2.estimateAffinePartial2D does not estimate shear or non-uniform scale; a general 6-DOF affine would need cv2.estimateAffine2D, which stabilo does not wire up.

RANSAC methods (integer codes)

Code Method projective affine
4 LMEDS (cv2.LMEDS) yes yes
8 RANSAC (cv2.RANSAC) yes yes
16 RHO (cv2.RHO) yes no
32 USAC-Default, LO-RANSAC (cv2.USAC_DEFAULT) yes no
33 USAC-Parallel, LO-RANSAC run in parallel (cv2.USAC_PARALLEL) yes no
35 USAC-Fast, LO-RANSAC with fewer local-optimization iterations (cv2.USAC_FAST) yes no
36 USAC-Accurate, GC-RANSAC (cv2.USAC_ACCURATE) yes no
37 PROSAC (cv2.USAC_PROSAC) yes no
38 MAGSAC++ (cv2.USAC_MAGSAC), default yes no

Codes 32, 33, and 35 are three LO-RANSAC configurations that differ in how local optimization is scheduled, per OpenCV's USAC tutorial. None of them is DEGENSAC: that is a degeneracy check used for fundamental-matrix estimation, which OpenCV does not apply to homographies.

The epipolar threshold and the downsample ratio

Keypoints detected on a downsampled frame are rescaled to full resolution before the estimator sees them, so ransac_epipolar_threshold is a full-resolution pixel distance by default (ransac_threshold_space: 'full'). The keypoint localization error it has to separate from real outliers is not: that arises in the processed image and is magnified by 1 / downsample_ratio when the points are rescaled. A fixed threshold is therefore a stricter filter at a low ratio than at a high one, by exactly the ratio between them, and a value tuned at one downsample_ratio does not transfer to another.

Set ransac_threshold_space: 'processed' to express the threshold in processed-image pixels instead, which keeps it fixed in the units the noise is generated in; stabilo then passes ransac_epipolar_threshold / downsample_ratio to the estimator.

Stabilizer(downsample_ratio=0.25, ransac_epipolar_threshold=2.0)                                  # 2.0 px at full resolution
Stabilizer(downsample_ratio=0.25, ransac_epipolar_threshold=2.0, ransac_threshold_space='processed')  # 8.0 px at full resolution

Neither setting is right in general: the residuals a threshold must separate are part localization noise (which scales with 1 / downsample_ratio) and part model error (which does not). Which to use, and at what value, is a tuning question; get_ransac_reproj_threshold() reports the value that is actually handed to the estimator.

Method and transformation compatibility

affine maps to cv2.estimateAffinePartial2D, which implements only LMEDS and RANSAC; every other code raises inside OpenCV. Stabilo rejects the combination when the Stabilizer is constructed, rather than failing on every frame:

Stabilizer(transformation_type='affine')                    # ValueError: the default ransac_method 38 is projective-only
Stabilizer(transformation_type='affine', ransac_method=8)   # OK

12. Visualisation Mode

Set viz=True to have Stabilo retain intermediate data on the instance after each stabilize(...) call:

Attribute Contents
ref_mask Binary mask used for the reference frame
cur_mask Binary mask used for the current frame
ref_frame_gray Grayscale reference frame
cur_frame_gray Grayscale current frame
ref_kpts Reference keypoints
cur_kpts Current keypoints
ref_pts Matched reference keypoint coordinates
cur_pts Matched current keypoint coordinates
cur_inliers Boolean inlier mask for matched points
cur_inliers_count Number of inlier matches

These attributes are used by the stabilo video --viz command to render side-by-side stabilisation visualisations.

Match-quality statistics are also exposed through dedicated getters: - get_cur_inliers_count() -> int | None — number of inliers (or None if estimation failed or has not run yet). - get_cur_num_matches() -> int | None — total number of good matches fed to the estimator (denominator of inliers/total), or None if stabilize() has not yet been called. - get_cur_num_keypoints() -> tuple — (num_reference_keypoints, num_current_keypoints).


13. Benchmarking Mode

Set benchmark=True when running systematic parameter evaluation:

  • If transformation estimation fails or produces None, the identity matrix (np.eye(3)) is used instead of the last known matrix.
  • Log warnings about missing matches and fallback matrices are suppressed for cleaner batch output.

See Stabilo-Optimize for a dedicated benchmarking and hyperparameter-tuning framework built on top of Stabilo.


14. Testing and Development

# run the full test suite
pytest

# run lint checks
ruff check .

# install in editable mode with development dependencies
pip install -e '.[dev]'

Run pytest from the repository root: the fixtures load images through relative paths such as tests/ND_before.jpg, so the suite fails from anywhere else.

Tests that are skipped by default

Three groups of tests stay skipped unless their prerequisites are met:

Group Runs when Why it is gated
Learning-based end-to-end tests (tests/test_dl.py) kornia is importable and STABILO_DL_TESTS=1 is set Each detector downloads pretrained weights on first run (hundreds of MB into torch's hub cache) and needs far more time and memory than the classical path. Gating them keeps a plain pytest offline, fast, and safe to run in CI.
CUDA tests (tests/test_stabilizer.py) OpenCV reports a CUDA-enabled device Stock opencv-python wheels ship no CUDA algorithms, see docs/cuda.md.
Codec-dependent CLI tests (tests/test_cli.py) The OpenCV build can actually encode a video file The end-to-end render tests skip themselves when no VideoWriter codec is available, or when the writer produces no file, which happens on minimal or headless OpenCV builds.

The learning-based gate is an explicit environment variable rather than an "enable if kornia is installed" check because kornia and torch became core dependencies in v1.4.0: they are always present, so an importability check would download weights for everyone running the suite. Opt in with:

STABILO_DL_TESTS=1 pytest tests/test_dl.py

Once the weights are cached, later runs work offline. The validation-only tests in tests/test_dl.py (invalid device, incompatible matcher, and so on) always run, because argument validation raises before kornia is ever imported.

Core-dependency smoke test

.github/scripts/smoke_core.py synthesizes a small video plus a matching bounding-box file and drives both stabilo video and stabilo tracks over them. It needs no development dependency, so it can validate an install made with a plain pip install .:

pip install .
python .github/scripts/smoke_core.py --core-only

--core-only additionally asserts that the extras dependency (matplotlib) is absent. CI runs this as the core-install job on every push and pull request.

Documentation site

The documentation at rfonod.github.io/stabilo is built with MkDocs and the Material theme from mkdocs.yml and the docs/ folder. The CLI reference is generated from the installed stabilo parsers and the API reference from the Stabilizer docstrings, so both follow the code. Build it locally with:

pip install -e '.[docs]'
mkdocs serve            # live preview at http://127.0.0.1:8000
mkdocs build --strict   # the check CI runs; fails on any warning

The docs workflow builds the site on every pull request and deploys it to GitHub Pages on every push to main.


15. Command-Line Interface

Installing stabilo provides a stabilo console command:

stabilo video <input> [options]    # stabilize a video relative to a reference frame
stabilo tracks <input> [options]   # stabilize per-frame object annotations (tracks)
stabilo config show                # print the default configuration
stabilo config copy [--output PATH]  # write an editable copy (default: ./custom.yaml)

Run stabilo video --help or stabilo tracks --help for the full list of options; both expose every stabilo parameter (detector, matcher, RANSAC, masking, device, etc.).

To customize parameters, run stabilo config copy, edit the generated custom.yaml, and pass it with --custom-config custom.yaml:

stabilo config copy
# edit custom.yaml ...
stabilo video data/video.mp4 --save --output data/out/ --custom-config custom.yaml

Resolution order is explicit CLI flags > custom config file > built-in defaults: a key set in the file passed to --custom-config fills in anything not explicitly passed as a CLI flag, and both override stabilo/cfg/default.yaml.

Each CLI invocation performs a best-effort check for a newer stabilo release on PyPI and prints a one-line notice if one is available. Set STABILO_DISABLE_UPDATE_CHECK=1 to disable it.

Using an external logger

Stabilizer accepts an optional logger so an embedding application can route stabilo's log records through its own logging setup:

import logging
from stabilo import Stabilizer

my_logger = logging.getLogger("my_app.stabilo")
stabilizer = Stabilizer(logger=my_logger)

If omitted, stabilo uses its own module logger.