Motion-vector based registration
The VideoIO.VideoRegistration module estimates global (and residual local) frame-to-frame motion from the codec motion vectors exposed by openvideo(...; export_mvs = true) (see Codec motion vectors). This can be used to register video frames — or to decide cheaply which frames actually require a full image-domain registration pass.
VideoRegistration is experimental. Its API may change in breaking ways between minor VideoIO releases, and the module may be moved out of VideoIO into a separate package in the future (it is self-contained — stdlib dependencies only — precisely to allow that). The motion-vector extraction API in VideoIO itself (export_mvs, motion_vectors, MotionVector, correspondences) is not affected and would remain in VideoIO.
Codec motion vectors are compression decisions, not measured optical flow. They may be coarse, reference non-adjacent frames, and be misleading around scene cuts and intra-coded regions. The module therefore fits models robustly (RANSAC) and reports quality metrics so that unreliable estimates can be detected and a conventional registration fallback used.
VideoIO.VideoRegistration — Module
VideoRegistrationRobust 2D point-set registration: estimation of a global transform (and residual local motion) from point correspondences, such as those derived from codec block motion vectors via openvideo(...; export_mvs = true), VideoIO.motion_vectors, and VideoIO.correspondences.
This module is experimental: its API may change in breaking ways between minor VideoIO releases, and it may be moved out of VideoIO into a separate package in the future. It is deliberately self-contained — stdlib dependencies only, and a purely point-based API (n×2 matrices) — so that such a move can happen with the code unchanged. All codec-specific knowledge (motion vector semantics, filtering) lives in VideoIO and would stay there.
Codec motion vectors are compression decisions, not measured optical flow. This module therefore emphasises robust fitting (RANSAC + closed-form model fits) and quality metrics, so that a caller can decide when to trust the codec-derived estimate, when to use it only as an initialization for conventional registration, and when to fall back entirely.
Typical usage
using VideoIO
using VideoIO.VideoRegistration
video = openvideo("video.mp4", export_mvs = true)
sz = out_frame_size(video) # (width, height)
for frame in video
mvs = motion_vectors(video)
gm = estimate_global_motion(mvs; model = :similarity, frame_size = sz)
conf = classify_confidence(gm)
if conf === :high || conf === :moderate
# use `gm.A` (a 2×3 matrix mapping current-frame points to
# reference-frame points) directly, or as an initialization
dx, dy = translation(gm)
else
# fall back to full image-domain registration
end
end(The estimate_global_motion(mvs; ...) motion-vector convenience method is VideoIO-side glue — see src/registration_glue.jl; the module itself only takes point matrices.)
Conventions
A fitted transform is stored as a 2×3 matrix A such that
[x_src, y_src] ≈ A * [x_dst, y_dst, 1]i.e. it maps a point in the current frame (dst) to the corresponding point in the reference frame (src), matching FFmpeg's motion vector convention src = dst + motion / motion_scale. Use invert_transform for the reference→current direction.
Point sets are n×2 matrices with columns (x, y) in pixels.
util/motion_vector_demo.jl in the VideoIO repository renders a triple-pane video (original | annotated | globally corrected): per-block dots colored by RANSAC inlier status, an arrow showing the fitted global translation, and a third pane stabilized by warping each frame with the chained global-motion estimates:
julia --project=. util/motion_vector_demo.jl [input.mp4] [output.mp4]With no arguments it generates a synthetic panning clip containing an independently moving patch, which shows up as a cluster of outlier dots.
Estimating global motion
VideoIO.VideoRegistration.estimate_global_motion — Function
estimate_global_motion(dst::AbstractMatrix, src::AbstractMatrix; kwargs...)Estimate the global transform mapping the n×2 point set dst to src (e.g. current-frame → reference-frame correspondences from VideoIO.correspondences).
Returns a GlobalMotion, or nothing when there are fewer than min_points correspondences (e.g. I-frames) or RANSAC fails — callers should treat nothing as "no estimate available" and fall back to conventional registration.
Keyword arguments
model = :similarity::translation,:similarity, or:affinemin_points = 8: minimum correspondences requiredthreshold = 2.0,max_iterations = 2000,confidence = 0.99,rng: RANSAC parameters, seeransac_fitframe_size = nothing:(width, height); enables thecoveragequality metric
VideoRegistration.estimate_global_motion(mvs::AbstractVector{MotionVector}; kwargs...)Estimate the global frame-to-reference transform directly from decoder motion vectors: converts them to filtered point correspondences with correspondences and fits with VideoRegistration.estimate_global_motion(dst, src; ...).
Accepts the filter keyword arguments of correspondences (past_only, min_block_size, max_displacement, border, and frame_size, which also enables the coverage quality metric) in addition to the fitting keyword arguments of the point-matrix method.
VideoIO.VideoRegistration.GlobalMotion — Type
GlobalMotionResult of estimate_global_motion.
Fields
model::Symbol: the fitted model (:translation,:similarity,:affine)A::Matrix{Float64}: 2×3 transform mapping current-frame (dst) points to reference-frame (src) pointsnpoints::Int: number of correspondences after filteringinliers::BitVector: RANSAC inlier mask over those correspondencesinlier_fraction::Float64median_residual::Float64: median reprojection error of the inliers, pixelscoverage::Float64: fraction of the frame area spanned by the inlier points' bounding box (NaNifframe_sizewas not provided)
VideoIO.VideoRegistration.translation — Function
translation(gm::GlobalMotion) -> (tx, ty)Translation component of the fitted transform.
VideoIO.VideoRegistration.rotation — Function
rotation(gm::GlobalMotion) -> θIn-plane rotation angle (radians) of the fitted transform. Meaningful for :similarity fits (and :affine fits with little shear).
VideoIO.VideoRegistration.scale_factor — Function
scale_factor(gm::GlobalMotion) -> sUniform scale of the fitted transform. Meaningful for :similarity fits.
VideoIO.VideoRegistration.classify_confidence — Function
classify_confidence(gm; min_points = 30, min_inlier_fraction = 0.5,
max_median_residual = 1.0, min_coverage = 0.25)
-> :high | :moderate | :low | :noneClassify a GlobalMotion estimate for decision making:
| Result | Suggested action |
|---|---|
:high | Use the codec-derived transform directly |
:moderate | Use it as an initialization for registration refinement |
:low | Run full image-domain registration |
:none | No usable estimate (gm === nothing); run full registration |
:high requires all thresholds to pass (coverage is only checked when it was computed, i.e. frame_size was provided); :moderate requires the inlier-fraction and residual checks; anything else is :low.
Residual local motion
Point correspondences are extracted from motion vectors with VideoIO.correspondences (see Codec motion vectors); the module itself operates purely on n×2 point matrices.
VideoIO.VideoRegistration.local_residuals — Function
local_residuals(gm::GlobalMotion, dst, src) -> Matrix{Float64}Residual local motion after removing the fitted global motion: for each correspondence, observed src - predicted src. Returns an n×2 matrix row-aligned with dst/src. Rows with large residuals indicate blocks moving inconsistently with the global model (independent motion, deformation, or bad codec vectors).
Note this is a sparse, block-based signal, not a dense optical-flow field.
Fitting primitives
VideoIO.VideoRegistration.fit_translation — Function
fit_translation(dst, src) -> Matrix{Float64}Robust (coordinate-wise median) translation fit. Returns a 2×3 transform.
VideoIO.VideoRegistration.fit_similarity — Function
fit_similarity(dst, src) -> Matrix{Float64}Least-squares similarity (partial affine) fit: translation + rotation + uniform scale (Umeyama's method). Returns a 2×3 transform mapping dst points to src points.
VideoIO.VideoRegistration.fit_affine — Function
fit_affine(dst, src) -> Matrix{Float64}Least-squares full affine fit (translation, rotation, anisotropic scale, shear). Returns a 2×3 transform mapping dst points to src points.
VideoIO.VideoRegistration.ransac_fit — Function
ransac_fit(dst, src; model = :similarity, threshold = 2.0,
max_iterations = 2000, confidence = 0.99,
rng = Random.default_rng()) -> (A, inliers::BitVector)Robustly fit a 2×3 transform mapping dst points to src points using RANSAC, then refine by refitting the model on all inliers.
model::translation,:similarity(translation + rotation + uniform scale), or:affine.threshold: inlier reprojection threshold in pixels.max_iterations: RANSAC iteration cap; iterations also stop early once the observed inlier ratio makes further sampling unnecessary at the requestedconfidence.
Returns the refined transform and the inlier mask (recomputed after refinement). Throws ArgumentError if there are fewer correspondences than the model's minimal sample size.
VideoIO.VideoRegistration.residuals — Function
residuals(A, dst, src) -> Vector{Float64}Euclidean residual per correspondence: ‖A*[dstᵢ,1] - srcᵢ‖.
VideoIO.VideoRegistration.apply_transform — Function
apply_transform(A, p) -> (x, y)Apply a 2×3 transform to the point p = (x, y).
VideoIO.VideoRegistration.transform_points — Function
transform_points(A, pts::AbstractMatrix) -> Matrix{Float64}Apply a 2×3 transform to each row of an n×2 point matrix.
VideoIO.VideoRegistration.invert_transform — Function
invert_transform(A) -> Matrix{Float64}Invert a 2×3 affine transform.
VideoIO.VideoRegistration.compose_transform — Function
compose_transform(B, A) -> Matrix{Float64}Compose two 2×3 affine transforms: the result applies A first, then B, so apply_transform(compose_transform(B, A), p) == apply_transform(B, apply_transform(A, p)).
Useful for chaining per-frame global-motion estimates into a cumulative transform relative to an anchor frame (e.g. for stabilization): C_n = compose_transform(C_{n-1}, A_n).