Feature Matching OpenCV Python: Read the Indices Before Trusting the Lines

Query 3 can prefer train 0 while train 0 prefers a different query. Inspect descriptor indices and both directions before treating a match line as evidence.

Calculate a complete synthetic matrix and inspect retained and rejected pairs. Original descriptor identities stay visible; ties remain unresolved instead of guessing native behavior.

Feature Matching OpenCV Python: Read the Indices Before Trusting the Lines
Feature Matching OpenCV Python: Read the Indices Before Trusting the Lines
Open the full example ↗

State the distance and mutual rule

OpenCV's official matching tutorial describes Hamming distance for binary descriptors and mutual-best filtering with crossCheck. For the later ORB specification here, WTA_K is explicitly 2; a change to 3 or 4 requires a different norm review. OpenCV's binary matching guidance.

The synthetic exercise uses ordinary Hamming distance: count the differing bits. The decimal values below each represent one byte, not a Euclidean coordinate.

Mutual-best means that a query row selects a train row and that train row selects the same query row. Being nearest in only one direction is insufficient for that rule.

Prepare the synthetic arrays with fixed row identities

Query row ID Decimal byte Binary byte
Q0 0 00000000
Q1 3 00000011
Q2 255 11111111
Q3 1 00000001
Train row ID Decimal byte Binary byte
T0 0 00000000
T1 7 00000111
T2 254 11111110

The authored arrays have shape 4 × 1 and 3 × 1, respectively, with uint8 elements. Row IDs encode the intended test order. They are not image keypoints.

Do not attach fabricated pixel coordinates or a claimed ORB descriptor length to this one-byte fixture. It tests the binary matcher mechanism independently.

Check the full distance matrix

Hamming distance T0 = 0 T1 = 7 T2 = 254
Q0 = 0 0 3 7
Q1 = 3 2 1 7
Q2 = 255 8 5 1
Q3 = 1 1 2 8

For example, Q1 and T1 differ by one bit: 00000011 versus 00000111. Q3 and T0 also differ by one bit, but that does not automatically make Q3/T0 mutual.

Each query row's unique nearest train row is:

queryIdx trainIdx Expected distance
0 0 0
1 1 1
2 2 1
3 0 1

That is four one-direction nearest matches. Now inspect the reverse preferences: T0's best query is Q0 at distance 0, T1's best is Q1 at distance 1, and T2's best is Q2 at distance 1.

Mutual filtering therefore keeps 0/0, 1/1, and 2/2. It rejects 3/0, because T0 prefers Q0. Preserve the rejected pair and its reason in the expected-case ledger rather than pretending the fourth query had no candidate at all.

The fixture has unique nearest choices, so it does not establish how a particular runtime handles tied distances. Keep that as a separate test if ties enter a revised dataset.

Prepare the matcher source specification

import numpy as np
import cv2 as cv

query = np.array([[0], [3], [255], [1]], dtype=np.uint8)
train = np.array([[0], [7], [254]], dtype=np.uint8)

nearest_matcher = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=False)
mutual_matcher = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True)

nearest = nearest_matcher.match(query, train)
mutual = mutual_matcher.match(query, train)

def match_ledger(matches):
    rows = [
        (m.queryIdx, m.trainIdx, m.distance)
        for m in matches
    ]
    return sorted(rows, key=lambda row: (row[0], row[1]))

nearest_rows = match_ledger(nearest)
mutual_rows = match_ledger(mutual)

The ledger sorts evidence by descriptor identity for comparison. It does not rewrite descriptor IDs. When you run the matcher, record Python, NumPy, and OpenCV versions, array shape/type checks, and the returned match rows.

Keep expected and actual rows separate. The tutorial documents DMatch's query/train indices and distance; this fixture's numerical expectations are independently authored and manually traced. OpenCV's DMatch fields.

Keep distance sorting separate from correspondence identity

For a later overlay, sorting matches by distance may change display order. It does not turn the first displayed line into queryIdx 0 or trainIdx 0.

Prepare image extraction as a separate native case

orb = cv.ORB_create(WTA_K=2)
query_keypoints, query_descriptors = orb.detectAndCompute(
    query_image, None
)
train_keypoints, train_descriptors = orb.detectAndCompute(
    train_image, None
)

Write the expected-case matrix

Case Input Expected disposition Actual observation
S1 Synthetic arrays, Hamming, no cross-check Four query-nearest pairs
S2 Same arrays, mutual cross-check Three retained pairs; Q3/T0 rejected
S3 Same matches sorted for display Original indices/distance preserved
I1 Later original images, ORB WTA_K 2 Extract/match observations required
I2 None or empty descriptors Held; no fabricated pairs
I3 Changed images or matching setup Fresh native acceptance required

Try the worked example

Calculate the default arrays. Change the last query byte from 1 to 254: Q3/T2 becomes exact and Q2/T2 is rejected. No image coordinates are invented for these descriptors. Distance sorting never changes descriptor identity.

The calculator executes synthetic Hamming arithmetic, not OpenCV, ORB extraction, or geometric image validation.

Recreate this example

Build one complete self-contained HTML page for this task: calculate binary descriptor distances and mutual-best pairs.

Use these exact original practice data and worked expectations: Compare query bytes [0,3,255,1] with train bytes [0,7,254]. The distance-matrix rows are [0,3,7], [2,1,7], [8,5,1], and [1,2,8]. Mutual pairs are Q0/T0 at distance 0, Q1/T1 at distance 1, and Q2/T2 at distance 1. Q3/T0 is rejected because T0 prefers Q0.

Implement the following useful local interactions: Allow 1–16 integer bytes in 0–255 per array. Compute XOR popcounts and unique minima in both directions. Show matrix, nearest, mutual, and rejected pair IDs. Explicitly hold ties. Preserve the supplied source text and distinguish an uncomputed state, a successful local result, and an invalid or incomplete input. Update output only from the current input snapshot. An input edit must retire stale feedback until the reader runs the relevant action again. Do not silently invent missing values or replace a failing case with a success.

Design the page as a compact source-and-result developer workbench with readable code, labeled inputs, and a separate explanation area. Put a compact task heading, actual sample data, the main result, and usable controls early enough to be useful on a narrow screen. Use readable text of at least 16px, generous paragraph spacing, strong contrast, native labels, accessible buttons, and visible keyboard focus. Stack long records at 360px and wrap code without page-level horizontal overflow.

Use type=button and explicit querySelector-based click handlers rather than relying on form submission. If Enter is useful, handle it only on the relevant input and ignore isComposing. Keep effects within this open page; no remote dependencies, network calls, persistence, fake downloads, or automated sending. The calculator executes synthetic Hamming arithmetic, not OpenCV, ORB extraction, or geometric image validation. Place worked explanations beside their data and identify teacher or synthetic examples clearly. Output only the complete HTML document, with inline styles and script and LF line endings.

Review the Hamming-distance and mutual-best calculator in NOS

Use the complete pair set when preparing a native matcher test. Review Q3/T0's rejection beside the full distance matrix in NOS, and revise eligible static matcher notes in visual editing mode if one-way and mutual choices are confused.

Keep reading