Skip to content

Make FLANN matching reproducible by seeding OpenCV's RNG - #1123

Closed
birgerbr wants to merge 2 commits into
mapillary:mainfrom
Imerso3D:flann-deterministic-seed
Closed

Make FLANN matching reproducible by seeding OpenCV's RNG#1123
birgerbr wants to merge 2 commits into
mapillary:mainfrom
Imerso3D:flann-deterministic-seed

Conversation

@birgerbr

@birgerbr birgerbr commented Sep 9, 2026

Copy link
Copy Markdown

Make FLANN matching reproducible by seeding OpenCV's RNG

build_flann_index builds a FLANN index without fixing OpenCV's RNG. FLANN's
index construction draws from cv::theRNG(), which is thread-local and advances
between builds, so two builds from identical descriptors produce different
indexes
— and with processes > 1 it also depends on which worker builds
which index and in what order. Matching is therefore not reproducible.

This pins the RNG immediately before the index is built, making the index a pure
function of (descriptors, seed), with the seed exposed as
flann_random_seed.

Reproducing it without any dataset

import hashlib
import cv2, numpy as np

PARAMS = {"algorithm": 2, "branching": 8, "iterations": 10, "tree": 8}  # KMEANS

def build_and_search(d, q, seed=None):
    if seed is not None:
        cv2.setRNGSeed(seed)
    idx = cv2.flann_Index(d, PARAMS)
    res, _ = idx.knnSearch(q, 2, params={"checks": 20})
    return hashlib.md5(np.ascontiguousarray(res).tobytes()).hexdigest()[:12]

rng = np.random.default_rng(0)
d = rng.random((6000, 128), dtype=np.float32)
q = rng.random((1500, 128), dtype=np.float32)

print(build_and_search(d, q),     build_and_search(d, q))      # differ
print(build_and_search(d, q, 42), build_and_search(d, q, 42))  # agree
print(build_and_search(d, q, 7))                               # differs from 42

On OpenCV 4.11.0:

unseeded : e8d3429884ac / 478f3270661b  -> agree: False
seed 42  : ea2000ba0207 / ea2000ba0207  -> agree: True
seed 7   : 8d6ed5cbb8b8                 -> differs from seed 42: True

The third line is the control: the seed is what determines the index, so the
agreement on line two is not simply everything being constant.

Effect on the matching stage

On a 151-image dataset with matcher_type: FLANN, flann_algorithm: KMEANS,
processes: 8, running only match_features twice and comparing the
unpickled contents of matches/*.pkl.gz:

files identical files differing
before 14 / 151 137
after 151 / 151 0

The "before" row is one pair of runs; a second pair of the same configuration
gave 11 / 151 identical. How much differs varies, which is itself the point.
The "after" row reproduced in two independent pairs of runs.

Feature files were bit-identical across both runs and the pair graph was
identical (same pair counts by source), so matching was the only stage
diverging. Match totals are unchanged in aggregate (~0.1%), as expected from a
differently-seeded approximate search rather than a behavioural change.

Note that matches/*.pkl.gz differ byte-wise between any two runs
regardless, because gzip records an mtime in its header — comparing the
compressed bytes will "confirm" non-determinism whatever the state of the code.
The numbers above come from comparing unpickled contents.

Scope, and what this does not fix

This addresses matching only. Reconstruction has separate, well-known
non-determinism from Ceres and pointer-based ordering, discussed in #213 and
#566; this change does not touch it, and a run can still differ downstream of
identical tracks.

Relation to existing reports:

  • Different mathcing results on two machines #792 identifies FLANN's randomized k-means as a cause of differing
    matching results across machines and suggests flann_tree: 1 as a partial
    mitigation. Seeding addresses it directly and keeps the default index
    structure.
  • How to set a random seed? #566 answers "there is no exposed way of setting an initial seed for
    this"
    . cv2.setRNGSeed is such a way for the FLANN path.

Trade-off worth flagging

cv2.setRNGSeed pins OpenCV's thread-local RNG rather than only FLANN's,
so it also fixes the starting state of any later OpenCV randomness on the same
thread — notably cv2.findFundamentalMat(..., FM_RANSAC) in
robust_match_fundamental on the perspective path. That moves in the same
direction (more reproducibility), but it is wider than the title suggests, and
OpenCV exposes no getRNGSeed, so the previous state cannot be saved and
restored around the call.

Seeding by default also means existing users get a different index than they
would have got by chance. There is no stable baseline being broken — any two
runs already differed — but it is a change in output and worth a reviewer's
attention. If you would rather it be opt-in, a sentinel value could leave the
RNG untouched; say which you prefer and I will adjust.

@meta-cla

meta-cla Bot commented Sep 9, 2026

Copy link
Copy Markdown

Hi @birgerbr!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@birgerbr

birgerbr commented Sep 9, 2026

Copy link
Copy Markdown
Author

Closing in favour of OpenSfM/OpenSfM#42, which carries the same change to the community-maintained repo. Opened here first by mistake; this repo's Meta CLA requirement is not one we need to clear for a fix that belongs upstream of the community fork.

@birgerbr birgerbr closed this Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant