use burn_core as burn;
use burn::config::Config;
use burn::module::{Content, DisplaySettings, Module, ModuleDisplay};
use burn::tensor::Device;
use burn::tensor::Tensor;
use super::calibrators::{
AfineAdapter, AfineAdapterConfig, FrCalibratorWithLimit, FrCalibratorWithLimitConfig,
NrCalibrator, NrCalibratorConfig, scale_finalscore,
};
use super::clip_vit::{ClipVisualEncoder, ClipVisualEncoderConfig};
use super::heads::{AfineDHead, AfineDHeadConfig, AfineQHead, AfineQHeadConfig};
const CLIP_MEAN: [f32; 3] = [0.481_454_66, 0.457_827_5, 0.408_210_73];
const CLIP_STD: [f32; 3] = [0.268_629_54, 0.261_302_58, 0.275_777_11];
#[derive(Config, Debug)]
pub struct AfineConfig {
#[config(default = "256")]
pub image_size: usize,
#[config(default = "true")]
pub normalize_input: bool,
}
impl AfineConfig {
pub fn init(&self, device: &Device) -> Afine {
assert_eq!(
self.image_size % 32,
0,
"A-FINE image_size must be a multiple of 32, got {}",
self.image_size
);
Afine {
clip_visual: ClipVisualEncoderConfig::new()
.with_image_size(self.image_size)
.init(device),
qhead: AfineQHeadConfig::new().init(device),
dhead: AfineDHeadConfig::new().init(device),
nr_calibrator: NrCalibratorConfig::new().init(device),
fr_calibrator: FrCalibratorWithLimitConfig::new().init(device),
adapter: AfineAdapterConfig::new().init(device),
image_size: self.image_size,
normalize_input: self.normalize_input,
}
}
pub fn init_pretrained(&self, device: &Device) -> Afine {
let afine = self.init(device);
super::weights::load_pretrained_weights(afine)
}
}
#[derive(Module, Debug)]
#[module(custom_display)]
pub struct Afine {
pub(crate) clip_visual: ClipVisualEncoder,
pub(crate) qhead: AfineQHead,
pub(crate) dhead: AfineDHead,
pub(crate) nr_calibrator: NrCalibrator,
pub(crate) fr_calibrator: FrCalibratorWithLimit,
pub(crate) adapter: AfineAdapter,
pub(crate) image_size: usize,
pub(crate) normalize_input: bool,
}
impl Afine {
pub fn forward(&self, distorted: Tensor<4>, reference: Tensor<4>) -> Tensor<1> {
let [_, _, height, width] = distorted.dims();
assert_eq!(
height % 32,
0,
"A-FINE input height ({height}) must be a multiple of 32"
);
assert_eq!(
width % 32,
0,
"A-FINE input width ({width}) must be a multiple of 32"
);
let device = distorted.device();
let (dis_norm, ref_norm) = if self.normalize_input {
let mean = Tensor::<4>::from_floats(
[[[[CLIP_MEAN[0]]], [[CLIP_MEAN[1]]], [[CLIP_MEAN[2]]]]],
&device,
);
let std = Tensor::<4>::from_floats(
[[[[CLIP_STD[0]]], [[CLIP_STD[1]]], [[CLIP_STD[2]]]]],
&device,
);
(
(distorted - mean.clone()) / std.clone(),
(reference - mean) / std,
)
} else {
(distorted, reference)
};
let feat_dis = self
.clip_visual
.forward_with_features(dis_norm.clone(), false)
.features;
let feat_ref = self
.clip_visual
.forward_with_features(ref_norm.clone(), false)
.features;
let natural_dis = self.qhead.forward(dis_norm.clone(), &feat_dis);
let natural_ref = self.qhead.forward(ref_norm.clone(), &feat_ref);
let natural_dis_scaled = self.nr_calibrator.forward(natural_dis);
let natural_ref_scaled = self.nr_calibrator.forward(natural_ref);
let fidelity = self.dhead.forward(dis_norm, ref_norm, &feat_dis, &feat_ref);
let fidelity_scaled = self.fr_calibrator.forward(fidelity);
let d = self
.adapter
.forward(natural_dis_scaled, natural_ref_scaled, fidelity_scaled);
let score = scale_finalscore(d); score.squeeze_dim::<1>(1) }
}
impl ModuleDisplay for Afine {
fn custom_settings(&self) -> Option<DisplaySettings> {
DisplaySettings::new()
.with_new_line_after_attribute(false)
.optional()
}
fn custom_content(&self, content: Content) -> Option<Content> {
content
.add("backbone", &"CLIP ViT-B/32".to_string())
.add("image_size", &self.image_size.to_string())
.add("normalize_input", &self.normalize_input.to_string())
.optional()
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::tensor::Distribution;
fn small_metric() -> Afine {
let device = Default::default();
AfineConfig::new().with_image_size(64).init(&device)
}
#[test]
fn afine_forward_shape() {
let device = Default::default();
let metric = small_metric();
let dis = Tensor::<4>::random([2, 3, 64, 64], Distribution::Default, &device);
let reference = Tensor::<4>::random([2, 3, 64, 64], Distribution::Default, &device);
let score = metric.forward(dis, reference);
assert_eq!(score.dims(), [2]);
}
#[test]
fn afine_batch_processing() {
let device = Default::default();
let metric = small_metric();
let dis = Tensor::<4>::random([4, 3, 64, 64], Distribution::Default, &device);
let reference = Tensor::<4>::random([4, 3, 64, 64], Distribution::Default, &device);
let score = metric.forward(dis, reference);
let values = score.into_data().try_to_vec::<f32>().unwrap();
assert_eq!(values.len(), 4);
for v in values {
assert!(v.is_finite(), "A-FINE produced non-finite value: {v}");
}
}
#[test]
fn afine_finite_on_constant_inputs() {
let device = Default::default();
let metric = small_metric();
let zeros = Tensor::<4>::zeros([1, 3, 64, 64], &device);
let ones = Tensor::<4>::ones([1, 3, 64, 64], &device);
let score = metric.forward(zeros, ones);
let value = score.into_data().try_to_vec::<f32>().unwrap()[0];
assert!(value.is_finite(), "got non-finite value {value}");
}
#[test]
fn afine_asymmetry() {
let device = Default::default();
let metric = small_metric();
let a = Tensor::<4>::random([1, 3, 64, 64], Distribution::Default, &device);
let b = Tensor::<4>::random([1, 3, 64, 64], Distribution::Default, &device);
let forward = metric
.forward(a.clone(), b.clone())
.into_data()
.try_to_vec::<f32>()
.unwrap()[0];
let reverse = metric
.forward(b, a)
.into_data()
.try_to_vec::<f32>()
.unwrap()[0];
assert!(
(forward - reverse).abs() > 1e-6,
"expected asymmetric output, got fwd={forward}, rev={reverse}"
);
}
#[test]
fn afine_image_size_must_be_multiple_of_32() {
let result = std::panic::catch_unwind(|| {
AfineConfig::new()
.with_image_size(33)
.init(&Default::default());
});
assert!(result.is_err(), "expected init to panic on bad image_size");
}
#[test]
fn display_afine() {
let metric = small_metric();
let formatted = format!("{metric}");
assert!(
formatted.contains("CLIP ViT-B/32"),
"expected backbone name in display output: {formatted}"
);
}
#[test]
#[ignore = "downloads pre-trained weights"]
fn test_afine_pretrained_parity() {
const D_EXPECTED: f32 = 43.15711594_f32;
let device = Default::default();
let metric = AfineConfig::new()
.with_image_size(224)
.init_pretrained(&device);
let total: i64 = 1 * 3 * 224 * 224;
let dis = Tensor::<1, burn::tensor::Int>::arange(0..total, &device)
.float()
.div_scalar((total - 1) as f64)
.reshape([1, 3, 224, 224]);
let reference = Tensor::<1, burn::tensor::Int>::arange(0..total, &device)
.float()
.div_scalar(total as f64)
.reshape([1, 3, 224, 224]);
let value = metric
.forward(dis, reference)
.into_data()
.try_to_vec::<f32>()
.unwrap()[0];
let tolerance = D_EXPECTED.abs() * 5e-3 + 5e-3;
assert!(
(value - D_EXPECTED).abs() < tolerance,
"expected {D_EXPECTED}, got {value} (tolerance {tolerance})"
);
}
#[test]
#[ignore = "downloads pre-trained weights"]
fn test_afine_pretrained() {
let device = Default::default();
let dis = Tensor::<4>::random([1, 3, 256, 256], Distribution::Default, &device);
let reference = Tensor::<4>::random([1, 3, 256, 256], Distribution::Default, &device);
let random_metric = AfineConfig::new().init(&device);
let random_score = random_metric
.forward(dis.clone(), reference.clone())
.into_data()
.try_to_vec::<f32>()
.unwrap()[0];
let pretrained_metric = AfineConfig::new().init_pretrained(&device);
let pretrained_score = pretrained_metric
.forward(dis, reference)
.into_data()
.try_to_vec::<f32>()
.unwrap()[0];
assert!(
pretrained_score.is_finite(),
"pretrained A-FINE produced non-finite output: {pretrained_score}"
);
assert!(
(pretrained_score - random_score).abs() > 1e-3,
"pretrained output ({pretrained_score}) too close to random-init ({random_score}) — \
a shard may have failed to load silently"
);
}
}