Skip to content

Align two overlapping images

CookbookMotion and matching

Match ORB features, reject inconsistent correspondences with RANSAC, then warp the second image into the first image’s coordinates.

Try the recipe ↓

The pipeline

VISUAL WALKTHROUGHMotion and matching
Follow the images, then read what passes to the next algorithm.
STARTING IMAGEFirst frame448 × 320
The unchanged source image. All steps use this same example.

The unchanged source image. All steps use this same example.

STEP 01 · 1 / 1Detected ORB features896 × 320
Detected ORB features: Circles mark detected features in each frame. Each feature also has a binary descriptor; correspondence has not been established yet.

Circles mark detected features in each frame. Each feature also has a binary descriptor; correspondence has not been established yet.

STEP 01 / 03

Describe both images

Explore the algorithm →
ORB.createdetectAndCompute

ORB produces oriented keypoints and binary descriptors.

Receives
Two overlapping grayscale views.
Passes to the next step
Keypoint coordinates and a descriptor matrix for each image. Descriptor rows correspond to keypoints.

Why this step? ORB finds multiscale, oriented keypoints and describes each neighbourhood as a binary pattern. This gives matching evidence even when a plain pixel comparison fails because the images are translated, rotated or moderately rescaled.

Actual OpenCV 5.0.0 results on the illustrated sample, using the lab’s default algorithm settings. Masks, overlays and normalized fields are labelled previews; the data contracts above describe what the algorithms really exchange. Try this chain with your images ↓

Why this chain works

Alignment needs corresponding locations before it can decide how to warp pixels. Descriptors propose those locations; a geometric model tests whether they can belong to the same view change.

  1. 01

    Describe both images

    ORB.createdetectAndCompute

    ORB finds multiscale, oriented keypoints and describes each neighbourhood as a binary pattern. This gives matching evidence even when a plain pixel comparison fails because the images are translated, rotated or moderately rescaled.

    Receives
    Two overlapping grayscale views.
    Passes on
    Keypoint coordinates and a descriptor matrix for each image. Descriptor rows correspond to keypoints.
  2. 02

    Keep consistent matches

    BFMatcherNORM_HAMMINGfindHomography

    Hamming distance counts differing descriptor bits. Cross-checking keeps mutual best matches; the lab retains up to 200 lowest-distance candidates. Similar-looking patches can still be wrong, so RANSAC keeps matches consistent with a single projective transform rather than trusting descriptor distance alone.

    Receives
    Binary descriptor pairs, then their matched 2D coordinates.
    Passes on
    A first-to-second 3×3 homography and an inlier mask.
  3. 03

    Warp to the reference

    warpPerspectiveWARP_INVERSE_MAPaddWeighted

    The output should use first-frame coordinates. WARP_INVERSE_MAP lets the warp use the fitted first-to-second map to look up source pixels in the second frame for each output position. An equal-weight overlay makes residual misalignment visible as doubled edges.

    Receives
    The second colour frame and the fitted first-to-second homography.
    Passes on
    The second image resampled onto the first image grid, ready for comparison or compositing.

Tune and diagnose

Choose the parameters

Increase ORB features when overlap is small or features are unevenly distributed. RANSAC tolerance is a reprojection distance in processed pixels: too tight rejects useful noisy matches; too loose accepts the wrong geometry.

Read the result

Look at both the inlier distribution and Alignment overlay. A correct fit should align structures across the shared plane, not just in one corner. A planar scene or camera rotation can fit a homography; depth-dependent parallax and independently moving objects often cannot.

Try it with your images

Choose an input image and a second image. Without uploads, the lab uses a labelled synthetic pair. Run the recipe, then use the stage buttons to inspect intermediate results without rerunning it.

YOUR IMAGE · REAL OPENCV

Experiment at pixel level

Open full lab ↗

Match ORB features, reject inconsistent correspondences with RANSAC, then warp the second image into the first image’s coordinates.

The engine loads on your first run. Your images stay in this browser.

Input448 × 320
OutputWaiting for a result

Scroll over either image to zoom at the pointer. Use the scrollbars to pan both views over the same relative area. Zoom is relative to the input; pixel coordinates belong to each image. Warps can change scene correspondence.

Pixel inspector RGBA · native values · matched scale · 9 × 9 output pixels
Hover to inspect. Click to pin a pixel.
Input
Select a pixel

Output
Select a pixel

Sample models and licenses

Assumptions and limits

A homography assumes a planar scene or mostly rotational camera motion. Parallax and moving objects can stay misaligned.

TypeScript core chain

Initialize the shared engine once with await initOpenCV(), then use these named imports. In a bundled browser app, pass the WASM URL as shown in the quickstart. image is an 8-bit BGR Mat from that same engine; paired recipes receive an equally sized nextImage. Region recipes use an in-bounds pixel rect. Read outputs before the using scope ends. See matrix ownership.

import {
  BFMatcher,
  CV_32FC2,
  DMatchVector,
  INTER_LINEAR,
  KeyPointVector,
  Mat,
  NORM_HAMMING,
  ORB_create,
  RANSAC,
  WARP_INVERSE_MAP,
  countNonZero,
  findHomography,
  warpPerspective
} from '@banou/opencv-wasm'

// The engine is already initialized; image is an 8-bit BGR Mat.
// nextImage is the equally sized second frame from the same engine.
// "using" releases native handles at scope exit; inspect or copy outputs before then.

// 1. ORB detects up to 800 oriented features and binary descriptors.
// Hamming distance compares descriptor bits; true enables mutual-best cross-checking.
using orb = ORB_create(800), matcher = new BFMatcher(NORM_HAMMING, true)
// Factories can return null, so check before using the detector.
if (!orb) throw new Error('ORB factory failed')
// Store each image's feature coordinates separately from its descriptor rows.
using first = new KeyPointVector(), second = new KeyPointVector()
// a/b will hold descriptors; an empty mask allows features anywhere in the image.
using a = new Mat(), b = new Mat(), mask = new Mat()
// Detect keypoints in the first image and write their descriptors into a.
orb.detectAndCompute(image, mask, first, a)
// Do the same for the second image; descriptor row i belongs to keypoint i.
orb.detectAndCompute(nextImage, mask, second, b)
// Matching requires at least some detectable features in both views.
if (a.empty() || b.empty()) throw new Error('Both images need features')
// 2. Allocate a native vector of descriptor-match records.
using matches = new DMatchVector()
// Match descriptors by Hamming distance with the cross-check selected above.
matcher.match(a, b, matches)
// Copy the match records into JavaScript, rank by distance (smaller is better),
// and retain the best 200 candidates for the geometric fit.
const ordered = Array.from({ length: matches.size() }, (_, i) => matches.get(i)!)
  .sort((a, b) => a.distance - b.distance).slice(0, 200)
// A homography needs at least four point correspondences; geometry must also be suitable.
if (ordered.length < 4) throw new Error('Fewer than four matches')
// queryIdx indexes first-image keypoints; pack their x,y coordinates.
const from = ordered.flatMap(m => { const p = first.get(m.queryIdx)!.pt; return [p.x, p.y] })
// trainIdx indexes second-image keypoints; preserve the same match order.
const to = ordered.flatMap(m => { const p = second.get(m.trainIdx)!.pt; return [p.x, p.y] })
// Pack the coordinate lists as N rows of two-channel float32 points.
// inliers will mark the correspondences consistent with one transform.
using sourcePoints = new Mat(ordered.length, 1, CV_32FC2)
using targetPoints = new Mat(ordered.length, 1, CV_32FC2), inliers = new Mat()
// Copy the first-image coordinates into the source-point matrix.
sourcePoints.data32F.set(from)
// Copy the corresponding second-image coordinates into the target matrix.
targetPoints.data32F.set(to)
// 3. RANSAC fits a first-to-second projective map while rejecting outliers.
// The 3-pixel threshold controls acceptable reprojection error in processed pixels.
using homography = findHomography(sourcePoints, targetPoints, RANSAC, 3, inliers)
// Reject failed fits and fits supported by fewer than four inlier pairs.
if (homography.empty() || countNonZero(inliers) < 4) throw new Error('No stable homography')
// Allocate the destination for the aligned second image.
using output = new Mat()
// For each first-frame output position, look up the corresponding second-frame
// source position with the fitted map. WARP_INVERSE_MAP selects that lookup direction;
// INTER_LINEAR interpolates fractional positions. Output uses the first frame's size.
warpPerspective(nextImage, output, homography, { width: image.cols, height: image.rows },
  INTER_LINEAR | WARP_INVERSE_MAP)

The snippets isolate the core operations. The complete runnable recipes also include validation, filtering, overlays, intermediate previews and resource cleanup.