diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..2f83472 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,23 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..5a5a58a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +build/ +source/api/generated/ +source/gallery/ + + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..99c0116 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d849dea --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,12 @@ +matplotlib +opencv-python +Sphinx==5.3.0 +sphinx-gallery==0.11.1 +sphinx-rtd-theme==1.1.1 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-napoleon==0.7 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..1eff688 --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,39 @@ +.. _api: + +homlib API +================= + +The Application Programming Interface (API) includes: + +Module +------ + +.. currentmodule:: homlib + +.. autosummary:: + :toctree: generated/ + + PoseData + LORansacOptions + RansacStatistics + + estimate_fitzgibbon_cvpr_2001_one_sided + estimate_fitzgibbon_cvpr_2001_two_sided_equal + estimate_kukelova_cvpr_2015_two_sided + estimate_kukelova_cvpr_2015_two_sided_6pt + estimate_nakano_icpr_2025_one_sided + estimate_wadenback_3dv_2026_one_sided + estimate_wadenback_3dv_2026_two_sided_equal + estimate_wadenback_3dv_2026_two_sided + + lomsac_fitzgibbon_cvpr_2001_one_sided + lomsac_fitzgibbon_cvpr_2001_two_sided_equal + lomsac_kukelova_cvpr_2015_two_sided + lomsac_kukelova_cvpr_2015_two_sided_equal + lomsac_kukelova_cvpr_2015_two_sided_equal_6pt + lomsac_kukelova_cvpr_2015_two_sided_6pt + lomsac_nakano_icpr_2025_one_sided + lomsac_wadenback_3dv_2026_one_sided + lomsac_wadenback_3dv_2026_two_sided_equal + lomsac_wadenback_3dv_2026_two_sided + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..3345dcc --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +import datetime + +from sphinx_gallery.sorting import ExampleTitleSortKey + + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.coverage', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', + 'sphinx.ext.extlinks', + 'sphinx.ext.napoleon', + 'sphinx_gallery.gen_gallery', +] + + +# Generate autodoc stubs with summaries from code +autosummary_generate = True + +# Include Python objects as they appear in source files +autodoc_member_order = 'bysource' + +# Default flags used by autodoc directives +autodoc_default_flags = ['members'] + +#autodoc_typehints = "description" + +sphinx_gallery_conf = { + # path to your examples scripts + 'examples_dirs': ['../../python/example'], + # path where to save gallery generated examples + 'gallery_dirs': ['gallery'], + 'filename_pattern': '\.py', + # Remove the "Download all examples" button from the top level gallery + 'download_all_examples': False, + # Sort gallery example by file name instead of number of lines (default) + 'within_subsection_order': ExampleTitleSortKey, + # directory where function granular galleries are stored + 'backreferences_dir': 'api/generated/backreferences', + # Modules for which function level galleries are created. + 'doc_module': 'homlib', + # Insert links to documentation of objects in the examples + 'reference_url': {'satoa': None}, + # Allow animations + 'matplotlib_animations': False, +} + +# Always show the source code that generates a plot +plot_include_source = True +plot_formats = ['png'] + +# Sphinx project configuration +templates_path = ['_templates'] +exclude_patterns = ['_build', '**.ipynb_checkpoints'] +source_suffix = '.rst' +# The encoding of source files. +source_encoding = 'utf-8-sig' +master_doc = 'index' + +# General information about the project +year = datetime.date.today().year +project = 'HomLib' +copyright = '{}, Marcus Valtonen Örnhag'.format(year) + +# Version +version = 'latest' + +# These enable substitutions using |variable| in the rst files +rst_epilog = """ +.. |year| replace:: {year} +""".format(year=year) + +html_last_updated_fmt = '%b %d, %Y' +html_title = 'HomLib' +html_short_title = 'HomLib' +#html_logo = '_static/logo.png' +#html_favicon = '_static/favicon.ico' +#html_static_path = ['_static'] +html_extra_path = [] +pygments_style = 'default' +add_function_parentheses = False +html_show_sourcelink = False +html_show_sphinx = True +html_show_copyright = True + +# Theme config +html_theme = "sphinx_rtd_theme" +html_theme_options = { + 'logo_only': True, + 'display_version': True, +} + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..d91982f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,37 @@ +Welcome to homlib's documentation! +========================================================= + +A collection of state-of-the-art homography solvers. + +.. warning:: + This repository is currently under active development. + Please be mindful of potential issues and keep an eye for improvements, + new features and bug fixes in upcoming versions. + +**Maintainer** + +* `Marcus Valtonen Örnhag `_ + + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: Getting started: + + installation/dependencies.rst + installation/installation_users.rst + installation/installation_developers.rst + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: Examples and usage: + + gallery/index.rst + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: Reference documentation: + + api/index.rst diff --git a/docs/source/installation/dependencies.rst b/docs/source/installation/dependencies.rst new file mode 100644 index 0000000..adb293a --- /dev/null +++ b/docs/source/installation/dependencies.rst @@ -0,0 +1,6 @@ +.. _dependencies: + +Dependencies +============ + +To install the the library you will need **Python 3.10 or greater**. diff --git a/docs/source/installation/installation_developers.rst b/docs/source/installation/installation_developers.rst new file mode 100644 index 0000000..7dfc484 --- /dev/null +++ b/docs/source/installation/installation_developers.rst @@ -0,0 +1,28 @@ +.. _installation_developers: + +Step-by-step installation for developers +======================================== + +To build the core C++ library you need Eigen 3.3+. On Ubuntu: + + +.. code-block:: + + apt install libeigen3-dev + +and also `PoseLib `__. Follow +the installation instructions in the repo. + +Furthermore, you need `RansacLib `__, which +is included as a submodule. You may simply pull it recursively, + +.. code-block:: + + git submodule update --init --recursive + +RansacLib is a header-only library and does not need any further installation. + +You may now use the **build.sh** script in the root of the repository to build the +C++ library for HomLib. + + diff --git a/docs/source/installation/installation_users.rst b/docs/source/installation/installation_users.rst new file mode 100644 index 0000000..4eeb11e --- /dev/null +++ b/docs/source/installation/installation_users.rst @@ -0,0 +1,12 @@ +.. _installation_users: + +Step-by-step installation for users +=================================== + +Pre-built python wheels are available on PyPI for latest version of Windows/Mac/Linux. +Simply install them using + +.. code-block:: + + pip install homlib + diff --git a/python/example/README.rst b/python/example/README.rst new file mode 100644 index 0000000..ad2a004 --- /dev/null +++ b/python/example/README.rst @@ -0,0 +1,11 @@ +.. _examples: + +Examples +-------- +Install the necessary extra requirements + +.. code:: bash + + pip install -r requirements.txt + +prior to running the examples. diff --git a/python/example/example_ransac.py b/python/example/example_ransac.py new file mode 100644 index 0000000..fdd42ca --- /dev/null +++ b/python/example/example_ransac.py @@ -0,0 +1,57 @@ +r""" +Synthetic RANSAC experiment +=========================== + +In this example, we show how to robustly estimate a homography +in the presence of outliers using synthetic data and different +distortion profiles. +""" + +import numpy as np + +import homlib + + +############################################################################### +# Configure RANSAC options + +options = homlib.LORansacOptions() +options.squared_inlier_threshold = 0.005 +options.final_least_squares = True + +############################################################################### +# Generate random problem instance (no radial distortion added for simplicity) + +N = 100 +H = np.random.randn(3, 3) +x = np.random.randn(2, N) +y = H @ np.vstack((x, np.ones((1, N)))) +y = y[:2] / y[2] + + +############################################################################### +# Add noise + +noise_std = 1e-4 +for i in range(N): + x[:,i] += np.random.randn(2) * noise_std + y[:,i] += np.random.randn(2) * noise_std + + +############################################################################### +# Add outliers + +nbr_outliers = 20 +for i in range(nbr_outliers): + x[:,i] = np.random.randn(2) * 10 + y[:,i] = np.random.randn(2) * 10 + +############################################################################### +# Estimate homography and distortion coefficient + +estimate, stats = homlib.lomsac_wadenback_3dv_2026_one_sided(x, y, options) + +print(f"H error = {np.linalg.norm(estimate.homography / estimate.homography[2,2] - H / H[2,2]):4e}") +print(f"Dist. error = {np.linalg.norm(estimate.distortion_parameter):4e}") # No distortion added - should be zero +print(stats.inlier_indices) + diff --git a/python/example/test.py b/python/example/example_synthetic.py similarity index 54% rename from python/example/test.py rename to python/example/example_synthetic.py index 12411ea..b912a2d 100644 --- a/python/example/test.py +++ b/python/example/example_synthetic.py @@ -1,12 +1,16 @@ -import numpy as np +r""" +Synthetic experiment +==================== -import homlib +In this example, we show how to estimate a homography from synthetic data. +""" +import numpy as np -H = np.eye(3) -p = homlib.PoseData(H, 1.0, -0.001, -0.002) -print(f"PoseData: {p}") +import homlib +###################################### +# Generate synthetic data N = 5 H = np.random.randn(3, 3) x = np.random.randn(2, N) @@ -15,7 +19,12 @@ print(f"H={H / H[2,2]}") -poses = homlib.estimate_wadenback_2025_one_sided(x, y, True) +###################################### +# Run estimator from homlib on the minimal sample. +poses = homlib.estimate_wadenback_3dv_2026_one_sided(x, y, True) + +###################################### +# Test against ground truth for i, p in enumerate(poses): print(f"Pose {i}: {p} with H =") diff --git a/python/example/requirements.txt b/python/example/requirements.txt new file mode 100644 index 0000000..6ccafc3 --- /dev/null +++ b/python/example/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/python/example/test_distort_image.py b/python/example/test_distort_image.py deleted file mode 100644 index 8131d38..0000000 --- a/python/example/test_distort_image.py +++ /dev/null @@ -1,30 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import cv2 - - - - -img1_name = 'DSC_2519' -img1 = cv2.cvtColor(cv2.imread(f'/home/elramav/Downloads/drive-download-20250816T102202Z-1-001/fisheye_grossmunster/images/{img1_name}.JPG'), cv2.COLOR_BGR2RGB) - -kappa = -1.1302514855113225e-07 - -x = np.arange(0,img1.shape[1]) - img1.shape[1]/2 -y = np.arange(0,img1.shape[0]) - img1.shape[0]/2 -xv, yv = np.meshgrid(x, y) - -for i in range(img1.shape[1]): - for j in range(img1.shape[0]): - r2 = xv[j,i]**2 + yv[j,i]**2 - xv[j, i] /= 1 + kappa*r2 - yv[j, i] /= 1 + kappa * r2 - -fig, ax = plt.subplots() -ax.pcolormesh(xv, yv, img1.astype(float) / 255.0) -ax.yaxis.set_inverted(True) - -plt.axis('off') -plt.savefig(f"{img1_name}.png", bbox_inches='tight') - -plt.show() \ No newline at end of file diff --git a/python/example/test_ransac.py b/python/example/test_ransac.py deleted file mode 100644 index 76f67b1..0000000 --- a/python/example/test_ransac.py +++ /dev/null @@ -1,49 +0,0 @@ -import numpy as np - -import homlib - - -solvers = [ - homlib.lomsac_wadenback_2025_one_sided, - homlib.lomsac_wadenback_2025_two_sided_equal, - homlib.lomsac_wadenback_2025_two_sided, -] - -options = homlib.LORansacOptions() -options.squared_inlier_threshold = 0.005 -options.final_least_squares = True - -N = 100 -H = np.random.randn(3, 3) -x = np.random.randn(2, N) -y = H @ np.vstack((x, np.ones((1, N)))) -y = y[:2] / y[2] - -print(f"H={H / H[2,2]}") - - -# Add noise -noise_std = 1e-4 -for i in range(N): - x[:,i] += np.random.randn(2) * noise_std - y[:,i] += np.random.randn(2) * noise_std - - -# Add outliers -nbr_outliers = 20 -for i in range(nbr_outliers): - x[:,i] = np.random.randn(2) * 10 - y[:,i] = np.random.randn(2) * 10 - - -for solver in solvers: - estimate, stats = solver(x, y, options) - - print(f"Results for {solver.__name__}") - print(estimate) - print(estimate.homography) - print(f"H error = {np.linalg.norm(estimate.homography / estimate.homography[2,2] - H / H[2,2]):4e}") - print(f"Dist. error = {np.linalg.norm(estimate.distortion_parameter):4e}") # No distortion added - should be zero - print(stats) - print(stats.inlier_indices) - diff --git a/python/example/test_real.py b/python/example/test_real.py deleted file mode 100644 index f8cb422..0000000 --- a/python/example/test_real.py +++ /dev/null @@ -1,161 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import cv2 -import homlib -from time import time - -def decolorize(img): - return cv2.cvtColor(cv2.cvtColor(img,cv2.COLOR_RGB2GRAY), cv2.COLOR_GRAY2RGB) - - - - -def draw_matches(kps1, kps2, tentatives, img1, img2, H, mask): - if H is None: - print ("No homography found") - return - matchesMask = mask.ravel().tolist() - h,w,ch = img1.shape - pts = np.float32([ [0,0],[0,h-1],[w-1,h-1],[w-1,0] ]).reshape(-1,1,2) - dst = cv2.perspectiveTransform(pts, H) - #Ground truth transformation - dst_GT = cv2.perspectiveTransform(pts, H_gt) - img2_tr = cv2.polylines(decolorize(img2),[np.int32(dst)],True,(0,0,255),3, cv2.LINE_AA) - img2_tr = cv2.polylines(deepcopy(img2_tr),[np.int32(dst_GT)],True,(0,255,0),3, cv2.LINE_AA) - # Blue is estimated, green is ground truth homography - draw_params = dict(matchColor = (255,255,0), # draw matches in yellow color - singlePointColor = None, - matchesMask = matchesMask, # draw only inliers - flags = 2) - img_out = cv2.drawMatches(img1,kps1,img2_tr,kps2,tentatives,None,**draw_params) - plt.figure(figsize = (12,8)) - plt.imshow(img_out) - return - - -def verify_cv2(kps1, kps2, tentatives): - src_pts = np.float32([ kps1[m.queryIdx].pt for m in tentatives ]).reshape(-1,1,2) - dst_pts = np.float32([ kps2[m.trainIdx].pt for m in tentatives ]).reshape(-1,1,2) - H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 1.0) - print (deepcopy(mask).astype(np.float32).sum(), 'inliers found') - - # print(f"H error = {np.linalg.norm(H / np.linalg.norm(H) - H_gt / np.linalg.norm(H_gt)):4e}") - - return H, mask - -def verify_homlib(kps1, kps2, tentatives, h1, w1, h2, w2): - src_pts = np.float32([kps1[m.queryIdx].pt for m in tentatives]).reshape(-1, 2).T - dst_pts = np.float32([kps2[m.trainIdx].pt for m in tentatives]).reshape(-1, 2).T - - # Shift s.t. (0,0) is in the center - src_pts = src_pts - np.array([[h1, w1]]).T / 2.0 - dst_pts = dst_pts - np.array([[h2, w2]]).T / 2.0 - T1 = np.eye(3) - T1[0, 2] = -h1 / 2.0 - T1[1, 2] = -w1 / 2.0 - T2inv = np.eye(3) - T2inv[0, 2] = h2 / 2.0 - T2inv[1, 2] = w2 / 2.0 - - options = homlib.LORansacOptions() - options.squared_inlier_threshold = 5.0 ** 2 - options.min_num_iterations = 500 - options.final_least_squares = True - estimate, stats = homlib.lomsac_kukelova_cvpr_2015_two_sided_equal(src_pts, dst_pts, options) - - H = T2inv @ estimate.homography @ T1 - - print(estimate) - mask = np.array([x in stats.inlier_indices for x in range(len(tentatives))], dtype=np.uint8) - print(stats) - print(estimate.distortion_parameter) - print(estimate.distortion_parameter2) - - #print(f"H error = {np.linalg.norm(H / np.linalg.norm(H) - H_gt / np.linalg.norm(H_gt)):4e}") - #print(f"Dist. coeff. error = {np.abs(estimate.distortion_parameter-dist_coeff_gt):4e}") - - return H, mask, estimate.distortion_parameter, stats.time - -pose_data = np.load('../posedata.npy',allow_pickle=True).item() - -from itertools import pairwise -from scipy.spatial.transform import Rotation - - - -n_plane = np.array([0.5921775818439035, -0.3666382743010555, -0.7175667825220526]) -n_norm = np.linalg.norm(n_plane) -d = 2.1878781959850406 -n_plane /= n_norm -print(f'Plane normal: {n_plane}') -d /= n_norm - -times = [] -errors = [] - -for i, j in [(2512, 2513), (2513, 2514), (2514, 2515), (2517, 2518), (2518, 2519)]: # pairwise(pose_data) - print(f'Image pair: ({i}, {j})') - p1 = pose_data[i] - p2 = pose_data[j] - - R1 = Rotation.from_quat(p1['q']).as_matrix() - R2 = Rotation.from_quat(p2['q']).as_matrix() - t1 = p1['t'] - t2 = p2['t'] - K = p1['K'] - - # Relative pose - R = R2 @ R1.T - t = t2 - R @ t1 - n = R1 @ n_plane - - H_gt = K @ (R + t @ n.T / d) @ np.linalg.inv(K) - print('H_gt') - print(H_gt/H_gt[2,2]) - img1 = cv2.cvtColor(cv2.imread(f'/home/elramav/Downloads/drive-download-20250816T102202Z-1-001/fisheye_grossmunster/images/DSC_{i}.JPG'), cv2.COLOR_BGR2RGB) - img2 = cv2.cvtColor(cv2.imread(f'/home/elramav/Downloads/drive-download-20250816T102202Z-1-001/fisheye_grossmunster/images/DSC_{j}.JPG'), cv2.COLOR_BGR2RGB) - - #We will detect ORB features and match them with cross-check test - det = cv2.SIFT_create(8000) - kps1, descs1 = det.detectAndCompute(img1,None) - kps2, descs2 = det.detectAndCompute(img2,None) - - bf = cv2.BFMatcher() - - SNN_threshold = 0.8 - matches = bf.knnMatch(descs1, descs2, k=2) - - # Apply ratio test - snn_ratios = [] - tentatives = [] - for m, n in matches: - if m.distance < SNN_threshold * n.distance: - tentatives.append(m) - snn_ratios.append(m.distance / n.distance) - - sorted_indices = np.argsort(snn_ratios) - tentatives = list(np.array(tentatives)[sorted_indices]) - - print(f'Tentative: {len(tentatives)}') - - #Now, some visualization from OpenCV tutorial - #https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_feature_homography/py_feature_homography.html - #We will draw correspondences found and the geometric transformation between the images. - from copy import deepcopy - - H, homlib_mask, kappa, exec_time = verify_homlib(kps1, kps2, tentatives, img1.shape[1], img1.shape[0], img2.shape[1], img2.shape[0]) - print(exec_time / 1e6, ' ms homlib') - print(H / H[2,2]) - - print(f"H error = {np.linalg.norm(H / np.linalg.norm(H) - H_gt / np.linalg.norm(H_gt)):4e}") - - - times.append(exec_time / 1e6) - errors.append(np.linalg.norm(H / np.linalg.norm(H) - H_gt / np.linalg.norm(H_gt))) - - #draw_matches(kps1, kps2, tentatives, img1, img2, H, homlib_mask) - - #plt.show() - -print(f'time: {np.mean(times)}, (median {np.median(times)})') -print(f'error: {np.mean(errors)}, (median {np.median(errors)})') \ No newline at end of file diff --git a/python/example/test_real_affine.py b/python/example/test_real_affine.py deleted file mode 100644 index 67c256d..0000000 --- a/python/example/test_real_affine.py +++ /dev/null @@ -1,216 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import cv2 -import pygcransac -from copy import deepcopy -import kornia as K -import kornia.feature as KF -import torch -from kornia_moons.feature import OpenCVDetectorWithAffNetKornia -from kornia_moons.viz import draw_LAF_matches, visualize_LAF -from time import time - -import homlib - -from enum import Enum - - -class Solver(Enum): - PointBased = 0 - SIFTBased = 1 - AffineBased = 2 - - -class Sampler(Enum): - Uniform = 0 - PROSAC = 1 - PNAPSAC = 2 - NGRANSAC = 3 - ARSampler = 4 - - -# Function to load the image into a pytorch tensor -def load_torch_image(fname): - img = K.image_to_tensor(cv2.imread(fname), False).float() / 255. - img = K.color.bgr_to_rgb(img) - return img - - -# Deciding about the device used. Prefer CUDA if available. -device = "cpu" -if torch.cuda.is_available(): - device = "cuda" - -# The number of keypoints to be detected -desired_kpts = 2000 - -# Loading the images -print("Loading images") - -# img1 = load_torch_image('img/grafA.png') -img1 = load_torch_image('/home/elramav/Downloads/drive-download-20250816T102202Z-1-001/fisheye_grossmunster/images/DSC_2515.JPG') -img2 = load_torch_image('/home/elramav/Downloads/drive-download-20250816T102202Z-1-001/fisheye_grossmunster/images/DSC_2517.JPG') - - -H_gt = np.linalg.inv(np.loadtxt('img/graf_model.txt')) -print(H_gt / H_gt[2, 2]) - -# Sending the images to the device -img1.to(device) -img2.to(device) - -# Detecting DoG + HardNet + AffNet features -print("Computing features") -kornia_cv2dogaffnet = OpenCVDetectorWithAffNetKornia(cv2.SIFT_create(desired_kpts), max_kpts=desired_kpts) -dogaffnethardnet = KF.LocalFeature(kornia_cv2dogaffnet, KF.LAFDescriptor(KF.HardNet(True))).eval() - -# Detecting features in the source image -lafs1, r1, descs1 = dogaffnethardnet(img1) -# Detecting features in the destination image -lafs2, r2, descs2 = dogaffnethardnet(img2) - -# Visualizing the found affine correspondences -print("Visualizing") -visualize_LAF(img1, lafs1, 0, 'y', figsize=(8,6)) -visualize_LAF(img2, lafs2, 0, 'y', figsize=(8,6)) -plt.show() -# Initialize a brute-force matcher to match the descriptors -print("matching") -bf = cv2.BFMatcher() - -# Send the detected keypoints and other variables to the CPU and convert them to numpy array -descs1np = np.squeeze(descs1.cpu().detach().numpy()) -descs2np = np.squeeze(descs2.cpu().detach().numpy()) -lafs1np = np.squeeze(lafs1.cpu().detach().numpy()) -lafs2np = np.squeeze(lafs2.cpu().detach().numpy()) -r1np = np.squeeze(r1.cpu().detach().numpy()) -r2np = np.squeeze(r2.cpu().detach().numpy()) - -# The threshold for the SNN ratio test -SNN_threshold = 0.8 - -# Applying brute-force matcher -matches = bf.knnMatch(descs1np, descs2np, k=2) - -# Apply the SNN ratio test -snn_ratios = [] -tentatives = [] -for m, n in matches: - if m.distance < SNN_threshold * n.distance: - tentatives.append(m) - snn_ratios.append(m.distance / n.distance) - -# Sort the keypoints based on the SNN ratio. -# This is used in many samplers, e.g., PROSAC, NG-RANSAC's, AR-Sampler -sorted_indices = np.argsort(snn_ratios) -tentatives = list(np.array(tentatives)[sorted_indices]) - -print(f"{len(tentatives)} tentative correspondences are found.") - - -# A function to convert to local affine frames (LAFs) to their centroids to obtain simple keypoints -def get_coordinates(lafs1, lafs2): - kps1 = [[lafs1[i, 0, 2], lafs1[i, 1, 2]] for i in range(lafs1.shape[0])] - kps2 = [[lafs2[i, 0, 2], lafs2[i, 1, 2]] for i in range(lafs2.shape[0])] - return kps1, kps2 - - -# A function to run OpenCV's RANSAC on the point correspondences extracted from the affine frames -def verify_cv2_homography(kps1, kps2, tentatives): - # Copy the coordinates in the source image selected by the tentative correspondences - src_pts = np.float32([kps1[m.queryIdx] for m in tentatives]).reshape(-1, 1, 2) - # Copy the coordinates in the destination image selected by the tentative correspondences - dst_pts = np.float32([kps2[m.trainIdx] for m in tentatives]).reshape(-1, 1, 2) - # Apply OpenCV's RANSAC - H, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 1.5) - print(deepcopy(mask).astype(np.float32).sum(), 'inliers found') - return H, mask - - -def verify_homlib_homography(src_pts, dst_pts, h1, w1, h2, w2): - # Run LO-RANSAC with the solver - options = homlib.LORansacOptions() - options.squared_inlier_threshold = 10.0 ** 2 - options.final_least_squares = True - - # Shift s.t. (0,0) is in the center - src_pts = np.array(src_pts).T - np.array([[h1, w1]]).T / 2.0 - dst_pts = np.array(dst_pts).T - np.array([[h2, w2]]).T / 2.0 - T1 = np.eye(3) - T1[0, 2] = -h1 / 2.0 - T1[1, 2] = -w1 / 2.0 - T2inv = np.eye(3) - T2inv[0, 2] = h2 / 2.0 - T2inv[1, 2] = w2 / 2.0 - - estimate, stats = homlib.lomsac_wadenback_2025_two_sided_equal(src_pts, dst_pts, options) - print(stats) - print(estimate) - - mask = np.zeros(x.shape[0], dtype=bool) - mask[stats.inlier_indices] = 1 - print(deepcopy(mask).astype(np.float32).sum(), 'inliers found') - - H = T2inv @ estimate.homography @ T1 - - return H, mask - - -# Extracting the point correspondences to OpenCV RANSAC can run -kps1, kps2 = get_coordinates(lafs1np, lafs2np) - -print(f"H gt =\n{H_gt / H_gt[2, 2]}\n") - -# The time before starting the estimation with OpenCV's RANSAC -t = time() -# Running OpenCV's RANSAC for fundamental matrix estimation -cv2_H, cv2_mask = verify_cv2_homography(kps1, kps2, tentatives) -# Measuring the run-time -print(time() - t, 'sec cv2') - - -# The time before starting the estimation with OpenCV's RANSAC -t = time() -gc_H, gc_mask = verify_homlib_homography(kps1, kps2, - # The matches containing the indices of the matched keypoints - img1.shape[3], # The width of the source image - img1.shape[2], # The height of the source image - img2.shape[3], # The width of the destination image - img2.shape[2], # The height of the destination image -) -# The id of the used sampler. 0 - uniform, 1 - PROSAC, 3 - NG-RANSAC's sampler, 4 - AR-Sampler -# Measuring the run-time -print(time() - t, 'sec pygcransac-ac') -print(f"H pygcransac-ac =\n{gc_H / gc_H[2, 2]}\n") - -tent_idxs = torch.from_numpy(np.array([[m.queryIdx, m.trainIdx] for m in tentatives])) - -inlier_color = (0.2, 1, 0.2) -inlier_color = None -with torch.no_grad(): - draw_LAF_matches(lafs1, lafs2, tent_idxs, - img1, img2, - inlier_mask=cv2_mask, - draw_dict={"inlier_color": inlier_color, - "tentative_color": None, - "feature_color": None, - "vertical": False}, H=cv2_H) - - draw_LAF_matches(lafs1, lafs2, tent_idxs, - img1, img2, - inlier_mask=gc_mask, - draw_dict={"inlier_color": inlier_color, - "tentative_color": None, - "feature_color": None, - "vertical": False}, H=gc_H) - - draw_LAF_matches(lafs1, lafs2, tent_idxs, - img1, img2, - inlier_mask=homlib_mask, - draw_dict={"inlier_color": inlier_color, - "tentative_color": None, - "feature_color": None, - "vertical": False}, H=homlib_H) - -plt.show() - diff --git a/python/src/main.cpp b/python/src/main.cpp index a9e2317..2d378ec 100644 --- a/python/src/main.cpp +++ b/python/src/main.cpp @@ -260,6 +260,8 @@ PYBIND11_MODULE(_core, m) { :toctree: _generate PoseData + LORansacOptions + RansacStatistics estimate_fitzgibbon_cvpr_2001_one_sided estimate_fitzgibbon_cvpr_2001_two_sided_equal @@ -280,9 +282,7 @@ PYBIND11_MODULE(_core, m) { lomsac_wadenback_3dv_2026_one_sided lomsac_wadenback_3dv_2026_two_sided_equal lomsac_wadenback_3dv_2026_two_sided - - LORansacOptions - RansacStatistics + )pbdoc"; @@ -314,7 +314,8 @@ PYBIND11_MODULE(_core, m) { "distortion_parameter2=" + std::to_string(p.distortion_parameter2) + ")"; } - ); + ) + .doc() = "Primary return class for HomLib functions."; py::class_(m, "LORansacOptions") .def( @@ -349,7 +350,8 @@ PYBIND11_MODULE(_core, m) { "final_least_squares=" + std::to_string(opt.final_least_squares_) + ")"; } - ); + ) + .doc() = "Options for LO RANSAC.";; py::class_(m, "RansacStatistics") .def( @@ -372,7 +374,8 @@ PYBIND11_MODULE(_core, m) { "number_lo_iterations=" + std::to_string(s.number_lo_iterations) + ")"; } - ); + ) + .doc() = "Statistics related to the RANSAC estimation process.";; /* * Wrappers for minimal solvers @@ -381,11 +384,13 @@ PYBIND11_MODULE(_core, m) { "estimate_fitzgibbon_cvpr_2001_one_sided", &estimate_fitzgibbon_cvpr_2001_one_sided_wrapper, R"pbdoc( - Solver from [1] as modified by [2]. + One-sided solver by Fitzgibbon. + + Solver from [1]_ as modified by [2]_. + + .. [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion". In Computer Vision and Pattern Recognition (CVPR), 2001. + .. [2] Gaku Nakano, "Inverse DLT Method for One-Sided Radial Distortion Homography". In International Conference on Pattern Recognition (ICPR), 2024. - [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In - Computer Vision and Pattern Recognition (CVPR), 2001 - [2] )pbdoc", "x"_a, "y"_a @@ -394,10 +399,12 @@ PYBIND11_MODULE(_core, m) { "estimate_fitzgibbon_cvpr_2001_two_sided_equal", &estimate_fitzgibbon_cvpr_2001_two_sided_equal_wrapper, R"pbdoc( - Solver from [1]. + Two-sided and equal solver by Fitzgibbon. + + Non-minimal solver from [1]_. + + .. [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion". In Computer Vision and Pattern Recognition (CVPR), 2001. - [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In - Computer Vision and Pattern Recognition (CVPR), 2001 )pbdoc", "x"_a, "y"_a @@ -406,10 +413,14 @@ PYBIND11_MODULE(_core, m) { "estimate_kukelova_cvpr_2015_two_sided", &estimate_kukelova_cvpr_2015_two_sided_wrapper, R"pbdoc( - Solver from [1]. + Two-sided solver by Kukelova et al. + + Minimal solver re-implemented from [1]_ using the automatic Gröbner basis solver proposed + in [2]_. + + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography". In Computer Vision and Pattern Recognition (CVPR), 2015. + .. [2] Larsson et al. "Efficient Solvers for Minimal Problems by Syzygy-based Reduction". In Computer Vision and Pattern Recognition (CVPR), 2017. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision - and Pattern Recognition (CVPR), 2015 )pbdoc", "x"_a, "y"_a, @@ -419,10 +430,12 @@ PYBIND11_MODULE(_core, m) { "estimate_kukelova_cvpr_2015_two_sided_6pt", &estimate_kukelova_cvpr_2015_two_sided_6pt_wrapper, R"pbdoc( - Solver from [1]. + Two-sided solver by Kukelova et al. + + Non-minimal solver re-implemented from [1]_. + + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography". In Computer Vision and Pattern Recognition (CVPR), 2015. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision - and Pattern Recognition (CVPR), 2015 )pbdoc", "x"_a, "y"_a, @@ -432,10 +445,12 @@ PYBIND11_MODULE(_core, m) { "estimate_nakano_icpr_2025_one_sided", &estimate_nakano_icpr_2025_one_sided_wrapper, R"pbdoc( - Solver from [1] + One-sided solver by Nakano. + + Minimal solver re-implemented from [1]_. + + .. [1] Gaku Nakano, "Inverse DLT Method for One-Sided Radial Distortion Homography". In International Conference on Pattern Recognition (ICPR), 2024. - [1] Gaku Nakano. "Inverse DLT Method for One-Sided Radial Distortion Homography", In - International Conference on Pattern Recognition (ICPR), 2024. )pbdoc", "x"_a, "y"_a, @@ -445,9 +460,12 @@ PYBIND11_MODULE(_core, m) { "estimate_wadenback_3dv_2026_one_sided", &estimate_wadenback_3dv_2026_one_sided_wrapper, R"pbdoc( - Solver from [1] + One-sided solver by Wadenbäck et al. + + Minimal solver from [1]_. Original implementation. + + .. [1] M. Wadenbäck, M. Valtonen Örnhag, J. Edstedt, "Radially Distorted Homographies, Revisited" In International Conference on 3D Vision (3DV), 2026. - [1] TODO )pbdoc", "x"_a, "y"_a, @@ -457,9 +475,12 @@ PYBIND11_MODULE(_core, m) { "estimate_wadenback_3dv_2026_two_sided_equal", &estimate_wadenback_3dv_2026_two_sided_equal_wrapper, R"pbdoc( - Solver from [1] + Two-sided and equal solver by Wadenbäck et al. + + Minimal solver from [1]_. Original implementation. + + .. [1] M. Wadenbäck, M. Valtonen Örnhag, J. Edstedt, "Radially Distorted Homographies, Revisited" In International Conference on 3D Vision (3DV), 2026. - [1] TODO )pbdoc", "x"_a, "y"_a, @@ -469,9 +490,12 @@ PYBIND11_MODULE(_core, m) { "estimate_wadenback_3dv_2026_two_sided", &estimate_wadenback_3dv_2026_two_sided_wrapper, R"pbdoc( - Solver from [1] + Two-sided solver by Wadenbäck et al. + + Minimal solver from [1]_. Original implementation. + + .. [1] M. Wadenbäck, M. Valtonen Örnhag, J. Edstedt, "Radially Distorted Homographies, Revisited" In International Conference on 3D Vision (3DV), 2026. - [1] TODO )pbdoc", "x"_a, "y"_a, @@ -486,12 +510,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_fitzgibbon_cvpr_2001_one_sided", &lomsac_fitzgibbon_cvpr_2001_one_sided_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + One-sided solver by Fitzgibbon embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In + .. [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In Conference on Computer Vision and Pattern Recognition (CVPR), 2001 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -501,12 +528,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_fitzgibbon_cvpr_2001_two_sided_equal", &lomsac_fitzgibbon_cvpr_2001_two_sided_equal_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided and equal solver by Fitzgibbon embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In + .. [1] A. W. Fitzgibbon, "Simultaneous linear estimation of multiple view geometry and lens distortion," In Conference on Computer Vision and Pattern Recognition (CVPR), 2001 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -516,12 +546,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_kukelova_cvpr_2015_two_sided", &lomsac_kukelova_cvpr_2015_two_sided_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided solver by Kukelova et al. embedded in a LOMSAC framework. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision + Solver from [1]_ in a LOMSAC framework [2]_. + + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision and Pattern Recognition (CVPR), 2015 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -531,12 +564,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_kukelova_cvpr_2015_two_sided_equal", &lomsac_kukelova_cvpr_2015_two_sided_equal_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided and equal solver by Kukelova et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision and Pattern Recognition (CVPR), 2015 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -546,12 +582,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_kukelova_cvpr_2015_two_sided_equal_6pt", &lomsac_kukelova_cvpr_2015_two_sided_equal_6pt_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided and equal non-minimal solver by Kukelova et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision and Pattern Recognition (CVPR), 2015 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -561,12 +600,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_kukelova_cvpr_2015_two_sided_6pt", &lomsac_kukelova_cvpr_2015_two_sided_6pt_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided non-minimal solver by Kukelova et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision + .. [1] Z. Kukelova, J. Heller, M. Bujnak and T. Pajdla, "Radial distortion homography," In Computer Vision and Pattern Recognition (CVPR), 2015 - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -576,12 +618,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_nakano_icpr_2025_one_sided", &lomsac_nakano_icpr_2025_one_sided_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + One-sided solver by Nakano embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] Gaku Nakano. "Inverse DLT Method for One-Sided Radial Distortion Homography", In + .. [1] Gaku Nakano. "Inverse DLT Method for One-Sided Radial Distortion Homography", In International Conference on Pattern Recognition (ICPR), 2024. - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -591,11 +636,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_wadenback_3dv_2026_one_sided", &lomsac_wadenback_3dv_2026_one_sided_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + One-sided solver by Wadenback et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] TODO - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [1] Marten Wadenback, Marcus Valtonen Ornhag, and Johan Edstedt. "Radially Distorted Homographies, Revisited", + In the Proceedings of the International Conference on 3D Vision (3DV), 2026. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -606,11 +655,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_wadenback_3dv_2026_two_sided_equal", &lomsac_wadenback_3dv_2026_two_sided_equal_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided and equal solver by Wadenback et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] TODO - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [1] Marten Wadenback, Marcus Valtonen Ornhag, and Johan Edstedt. "Radially Distorted Homographies, Revisited", + In the Proceedings of the International Conference on 3D Vision (3DV), 2026. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a, @@ -620,11 +673,15 @@ PYBIND11_MODULE(_core, m) { "lomsac_wadenback_3dv_2026_two_sided", &lomsac_wadenback_3dv_2026_two_sided_wrapper, R"pbdoc( - Solver from [1] in a LOMSAC framework [2]. + Two-sided solver by Wadenback et al. embedded in a LOMSAC framework. + + Solver from [1]_ in a LOMSAC framework [2]_. - [1] TODO - [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the - Proceedings of the British Machine Vision Conference (BMVC), 2012. + .. [1] Marten Wadenback, Marcus Valtonen Ornhag, and Johan Edstedt. "Radially Distorted Homographies, Revisited", + In the Proceedings of the International Conference on 3D Vision (3DV), 2026. + .. [2] Karel Lebeda, Jiri Matas, and Ondrej Chum. "Fixing the Locally Optimized RANSAC", In the + Proceedings of the British Machine Vision Conference (BMVC), 2012. + )pbdoc", "x"_a, "y"_a,