Files
wehub-resource-sync 3a2c66702c
Tests on CPU (scheduled) / check-skip (push) Has been cancelled
Tests on CPU (scheduled) / pre-tests (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-ubuntu (float32) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-ubuntu (float64) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float64, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.11, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float64, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.12, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.13, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-windows (3.13, float64, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.11, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.11, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.12, float32, 2.5.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.12, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / tests-cpu-mac (3.13, float32, 2.9.1) (push) Has been cancelled
Tests on CPU (scheduled) / coverage (push) Has been cancelled
Tests on CPU (scheduled) / typing (push) Has been cancelled
Tests on CPU (scheduled) / tutorials (push) Has been cancelled
Tests on CPU (scheduled) / docs (push) Has been cancelled
Lint / TOML Format (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:49:27 +08:00

247 lines
10 KiB
Python

# LICENSE HEADER MANAGED BY add-license-header
#
# Copyright 2018 Kornia Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from unittest.mock import MagicMock
import pytest
import torch
from kornia.core._compat import torch_version_le
from kornia.feature import DescriptorMatcher, GFTTAffNetHardNet, LocalFeatureMatcher, SIFTFeature
from kornia.geometry import rescale, transform_points
from kornia.tracking import HomographyTracker
from testing.base import BaseTester
def _make_tracker(minimum_inliers_num: int = 5) -> HomographyTracker:
"""Return a HomographyTracker whose heavy sub-modules are replaced with lightweight mocks."""
initial_matcher = MagicMock()
fast_matcher = MagicMock()
ransac = MagicMock()
# Set extract_features to None so isinstance(..., nn.Module) guard skips feature pre-extraction
initial_matcher.extract_features = None
fast_matcher.extract_features = None
return HomographyTracker(
initial_matcher=initial_matcher,
fast_matcher=fast_matcher,
ransac=ransac,
minimum_inliers_num=minimum_inliers_num,
)
def _match_dict(n_keypoints: int, device: torch.device, dtype: torch.dtype) -> dict:
"""Produce a fake match dict with n_keypoints matches for batch 0."""
return {
"keypoints0": torch.rand(n_keypoints, 2, device=device, dtype=dtype),
"keypoints1": torch.rand(n_keypoints, 2, device=device, dtype=dtype),
"batch_indexes": torch.zeros(n_keypoints, dtype=torch.long, device=device),
}
class TestHomographyTrackerUnit:
"""Fast unit tests for HomographyTracker using mocked sub-modules."""
def test_reset_tracking_clears_homography(self):
tracker = _make_tracker()
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.previous_homography = torch.eye(3)
tracker.reset_tracking()
assert tracker.previous_homography is None
def test_set_target_without_extract_features(self):
tracker = _make_tracker()
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
assert torch.equal(tracker.target, image)
# Representations stay empty when extract_features not present
assert tracker.target_initial_representation == {}
assert tracker.target_fast_representation == {}
def test_set_target_with_extract_features(self):
from torch import nn
fake_feats = {"desc": torch.rand(1, 4)}
class _FakeExtract(nn.Module):
def forward(self, x):
return fake_feats
initial_matcher = MagicMock()
fast_matcher = MagicMock()
initial_matcher.extract_features = _FakeExtract()
fast_matcher.extract_features = _FakeExtract()
tracker = HomographyTracker(
initial_matcher=initial_matcher,
fast_matcher=fast_matcher,
ransac=MagicMock(),
)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
assert tracker.target_initial_representation == fake_feats
assert tracker.target_fast_representation == fake_feats
def test_no_match_returns_empty_tensor_and_false(self):
tracker = _make_tracker()
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
H, success = tracker.no_match()
assert not success
assert H.shape == (3, 3)
assert tracker.inliers_num == 0
def test_match_initial_too_few_keypoints(self):
# Fewer than minimum_inliers_num keypoints → no_match
tracker = _make_tracker(minimum_inliers_num=10)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.initial_matcher.return_value = _match_dict(3, torch.device("cpu"), torch.float32)
_, success = tracker.match_initial(torch.rand(1, 1, 8, 8))
assert not success
def test_match_initial_too_few_inliers(self):
# Enough keypoints but RANSAC reports few inliers → no_match
tracker = _make_tracker(minimum_inliers_num=5)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.initial_matcher.return_value = _match_dict(20, torch.device("cpu"), torch.float32)
# RANSAC returns homography + inlier mask with only 2 inliers
inliers = torch.zeros(20, dtype=torch.bool)
inliers[:2] = True
tracker.ransac.return_value = (torch.eye(3), inliers)
_, success = tracker.match_initial(torch.rand(1, 1, 8, 8))
assert not success
def test_match_initial_success(self):
tracker = _make_tracker(minimum_inliers_num=5)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.initial_matcher.return_value = _match_dict(20, torch.device("cpu"), torch.float32)
inliers = torch.ones(20, dtype=torch.bool)
H_expected = torch.eye(3) * 2
tracker.ransac.return_value = (H_expected, inliers)
H, success = tracker.match_initial(torch.rand(1, 1, 8, 8))
assert success
assert tracker.previous_homography is not None
assert torch.allclose(H, H_expected)
def test_forward_routes_to_match_initial_when_no_previous(self):
tracker = _make_tracker(minimum_inliers_num=5)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.initial_matcher.return_value = _match_dict(20, torch.device("cpu"), torch.float32)
inliers = torch.ones(20, dtype=torch.bool)
tracker.ransac.return_value = (torch.eye(3), inliers)
assert tracker.previous_homography is None
_, success = tracker(torch.rand(1, 1, 8, 8))
assert success
def test_forward_routes_to_track_next_frame_when_previous_set(self):
tracker = _make_tracker(minimum_inliers_num=5)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.previous_homography = torch.eye(3)
tracker.fast_matcher.return_value = _match_dict(20, torch.device("cpu"), torch.float32)
inliers = torch.ones(20, dtype=torch.bool)
tracker.ransac.return_value = (torch.eye(3), inliers)
_, success = tracker(torch.rand(1, 1, 8, 8))
assert success
def test_track_next_frame_too_few_keypoints_resets(self):
tracker = _make_tracker(minimum_inliers_num=10)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.previous_homography = torch.eye(3)
tracker.fast_matcher.return_value = _match_dict(3, torch.device("cpu"), torch.float32)
_, success = tracker.track_next_frame(torch.rand(1, 1, 8, 8))
assert not success
assert tracker.previous_homography is None # reset_tracking was called
def test_track_next_frame_too_few_inliers_resets(self):
tracker = _make_tracker(minimum_inliers_num=5)
image = torch.rand(1, 1, 8, 8)
tracker.set_target(image)
tracker.previous_homography = torch.eye(3)
tracker.fast_matcher.return_value = _match_dict(20, torch.device("cpu"), torch.float32)
inliers = torch.zeros(20, dtype=torch.bool)
inliers[:2] = True
tracker.ransac.return_value = (torch.eye(3), inliers)
_, success = tracker.track_next_frame(torch.rand(1, 1, 8, 8))
assert not success
assert tracker.previous_homography is None # reset_tracking was called
@pytest.fixture()
def data_url():
url = "https://github.com/kornia/data_test/blob/main/loftr_outdoor_and_homography_data.pt?raw=true"
return url
class TestHomographyTracker(BaseTester):
@pytest.mark.slow
def test_smoke(self, device):
tracker = HomographyTracker().to(device)
assert tracker is not None
@pytest.mark.slow
def test_nomatch(self, device, dtype, data_url):
data = torch.hub.load_state_dict_from_url(data_url)
# This is not unit test, but that is quite good integration test
matcher = LocalFeatureMatcher(SIFTFeature(100), DescriptorMatcher("smnn", 0.95)).to(device, dtype)
tracker = HomographyTracker(matcher, matcher, minimum_inliers_num=100)
for k in data.keys():
if isinstance(data[k], torch.Tensor):
data[k] = data[k].to(device, dtype)
tracker.set_target(data["image0"])
torch.random.manual_seed(0)
_, success = tracker(torch.zeros_like(data["image0"]))
assert not success
@pytest.mark.slow
@pytest.mark.skipif(torch_version_le(1, 9, 1), reason="Fails for bached torch.linalg.solve")
def test_real(self, device, dtype, data_url):
data = torch.hub.load_state_dict_from_url(data_url)
# This is not unit test, but that is quite good integration test
for k in data.keys():
if isinstance(data[k], torch.Tensor):
data[k] = data[k].to(device, dtype)
data["image0"] = rescale(data["image0"], 0.5, interpolation="bilinear", align_corners=False)
data["image1"] = rescale(data["image1"], 0.5, interpolation="bilinear", align_corners=False)
matcher = LocalFeatureMatcher(GFTTAffNetHardNet(1000), DescriptorMatcher("snn", 0.8)).to(device, dtype)
torch.manual_seed(8) # issue kornia#2027
tracker = HomographyTracker(matcher, matcher).to(device, dtype)
with torch.no_grad():
tracker.set_target(data["image0"])
torch.manual_seed(8) # issue kornia#2027
homography, success = tracker(data["image1"])
assert success
pts_src = data["pts0"].to(device, dtype) / 2.0
pts_dst = data["pts1"].to(device, dtype) / 2.0
# Reprojection error of 5px is OK
self.assert_close(transform_points(homography[None], pts_src[None]), pts_dst[None], rtol=5e-2, atol=5)
with torch.no_grad():
torch.manual_seed(6)
homography, success = tracker(data["image1"])
assert success
self.assert_close(transform_points(homography[None], pts_src[None]), pts_dst[None], rtol=5e-2, atol=5)