loftr 0.1.1

Native Rust/tch implementation of LoFTR feature matching
Documentation
#[path = "support/geometry.rs"]
mod geometry;
mod support;

use std::error::Error;

use geometry::{
    GeometryRansacConfig, fit_homography_ransac, mat3_from_tensor, points_from_tensor,
    project_homography, sampson_distance,
};
use loftr::LoftrConfig;
use support::{ReferenceFixture, load_model};

#[test]
fn outdoor_matches_support_homography_estimation() -> Result<(), Box<dyn Error>> {
    let fixture = ReferenceFixture::load("loftr_outdoor_reference")?;
    let mut model = load_model(
        "loftr_outdoor_state_dict.safetensors",
        LoftrConfig::outdoor(),
    )?;

    let image0 = fixture.tensor("image0")?;
    let image1 = fixture.tensor("image1")?;
    let matches = model.forward(&image0, &image1)?;
    let points0 = points_from_tensor(&matches.keypoints0)?;
    let points1 = points_from_tensor(&matches.keypoints1)?;
    let fit = fit_homography_ransac(
        &points0,
        &points1,
        GeometryRansacConfig {
            max_iterations: 400,
            threshold: 3.0,
            min_inliers: 30,
            seed: 1,
        },
    )?;
    assert!(fit.inlier_count >= 30);

    let clean0 = points_from_tensor(&fixture.tensor("pts0")?)?;
    let clean1 = points_from_tensor(&fixture.tensor("pts1")?)?;
    for (&point0, &point1) in clean0.iter().zip(clean1.iter()) {
        let projected = project_homography(&fit.model, point0);
        let tolerance_x = 5.0 + 0.15 * point1[0].abs();
        let tolerance_y = 5.0 + 0.15 * point1[1].abs();
        assert!((projected[0] - point1[0]).abs() <= tolerance_x);
        assert!((projected[1] - point1[1]).abs() <= tolerance_y);
    }
    Ok(())
}

#[test]
fn indoor_matches_support_reference_epipolar_geometry() -> Result<(), Box<dyn Error>> {
    let fixture = ReferenceFixture::load("loftr_indoor_reference")?;
    let mut model = load_model("loftr_indoor_state_dict.safetensors", LoftrConfig::indoor())?;

    let image0 = fixture.tensor("image0")?;
    let image1 = fixture.tensor("image1")?;
    let matches = model.forward(&image0, &image1)?;
    let points0 = points_from_tensor(&matches.keypoints0)?;
    let points1 = points_from_tensor(&matches.keypoints1)?;
    let fundamental = mat3_from_tensor(&fixture.tensor("F_gt")?);

    let inliers = points0
        .iter()
        .zip(points1.iter())
        .filter(|(point0, point1)| {
            sampson_distance(&fundamental, **point0, **point1).sqrt() <= 10.0
        })
        .count();
    assert!(inliers >= 480);
    Ok(())
}

#[test]
fn indoor_clean_correspondences_match_reference_fundamental() -> Result<(), Box<dyn Error>> {
    let fixture = ReferenceFixture::load("loftr_indoor_reference")?;
    let fundamental = mat3_from_tensor(&fixture.tensor("F_gt")?);
    let clean0 = points_from_tensor(&fixture.tensor("pts0")?)?;
    let clean1 = points_from_tensor(&fixture.tensor("pts1")?)?;

    let gross_errors = clean0
        .iter()
        .zip(clean1.iter())
        .filter(|(point0, point1)| sampson_distance(&fundamental, **point0, **point1).sqrt() > 1.0)
        .count();
    assert_eq!(gross_errors, 0);
    Ok(())
}