Skip to main content

hrx/image/
similarity.rs

1use super::*;
2use std::fmt::Write;
3
4/// Least-squares orientation-preserving 2D similarity, mapping source points
5/// to template points in FP32. No robust outlier rejection. Collinear points
6/// are allowed if the fit has nonzero scale; collapsed/non-finite fits fail.
7pub fn fit_similarity_2d(points: &[[f32; 2]], template: &[[f32; 2]]) -> Result<[[f32; 3]; 2]> {
8    if points.len() < 2
9        || points.len() != template.len()
10        || !points
11            .iter()
12            .chain(template)
13            .flatten()
14            .all(|v| v.is_finite())
15    {
16        return Err(Error::Message(
17            "similarity needs matching finite point sets".into(),
18        ));
19    }
20    let mean = |points: &[[f32; 2]]| {
21        [
22            points.iter().map(|p| p[0]).sum::<f32>() / points.len() as f32,
23            points.iter().map(|p| p[1]).sum::<f32>() / points.len() as f32,
24        ]
25    };
26    let p = mean(points);
27    let q = mean(template);
28    let (mut den, mut a, mut b) = (0., 0., 0.);
29    for (s, d) in points.iter().zip(template) {
30        let (x, y, u, v) = (s[0] - p[0], s[1] - p[1], d[0] - q[0], d[1] - q[1]);
31        den += x * x + y * y;
32        a += x * u + y * v;
33        b += x * v - y * u;
34    }
35    a /= den;
36    b /= den;
37    let result = [
38        [a, -b, q[0] - a * p[0] + b * p[1]],
39        [b, a, q[1] - b * p[0] - a * p[1]],
40    ];
41    invert_affine(result)?;
42    Ok(result)
43}
44
45impl ImageOps {
46    /// Prepare FP32 similarity fitting for `[batch,points,2]`. Positive
47    /// non-overlapping strides allow direct landmark views of detection rows.
48    /// Template coordinates are immutable shape-plan data (2..=64 points).
49    /// Outputs: forward `[batch,2,3]`, inverse `[batch,2,3]`, status I32 `[batch]`.
50    /// Status is zero for success and one for invalid/degenerate geometry; both
51    /// matrices are NaN on failure. Consumers must preserve/check status, not
52    /// treat a black crop from invalid geometry as a successful alignment.
53    pub fn prepare_similarity_2d(
54        &self,
55        points: &TensorDesc,
56        template: &[[f32; 2]],
57    ) -> Result<Arc<PreparedModel>> {
58        let key = (
59            points.clone(),
60            template.iter().flatten().map(|v| v.to_bits()).collect(),
61        );
62        self.similarity_plans.get_or_prepare(key, || {
63            self.similarity_2d_fragment(points, template)?.prepare(3)
64        })
65    }
66
67    /// Recordable fit producing forward matrices, inverse matrices and status.
68    pub fn similarity_2d_fragment(
69        &self,
70        points: &TensorDesc,
71        template: &[[f32; 2]],
72    ) -> Result<ModelFragment> {
73        let shape = points.shape();
74        if points.dtype() != DType::F32
75            || points.layout() != Layout::General
76            || shape.len() != 3
77            || shape[2] != 2
78            || shape[0] == 0
79            || !(2..=64).contains(&template.len())
80            || shape[1] != template.len()
81            || points.bytes() / 4 > i32::MAX as usize
82            || !template.iter().flatten().all(|v| v.is_finite())
83        {
84            return Err(Error::Message(
85                "similarity needs F32 [batch,points,2] and a finite template".into(),
86            ));
87        }
88        let batch = shape[0];
89        let count = template.len();
90        let elements = points.bytes() / 4;
91        let matrices = TensorDesc::new(DType::F32, vec![batch, 2, 3])?;
92        let status = TensorDesc::new(DType::I32, vec![batch])?;
93        if matrices.elements() > i32::MAX as usize {
94            return Err(Error::Message("similarity output too large".into()));
95        }
96        let q = [0, 1].map(|axis| template.iter().map(|p| p[axis]).sum::<f32>() / count as f32);
97        if !q.iter().all(|v| v.is_finite()) {
98            return Err(Error::Message("template mean overflow".into()));
99        }
100        let mut body = String::new();
101        for (i, point) in template.iter().enumerate() {
102            for (axis, letter) in [(0, "x"), (1, "y")] {
103                let offset = i * points.strides()[1] + axis * points.strides()[2];
104                writeln!(
105                    body,
106                    r#"
107    %off{letter}{i} = index.constant {offset} : index
108    %raw{letter}{i} = index.add %inputbase, %off{letter}{i} : index
109    %at{letter}{i} = index.assume %raw{letter}{i} [range(%raw{letter}{i}, 0, {last})] : index
110    %{letter}{i} = view.load %src[%at{letter}{i}] : view<{elements}xf32> -> f32
111    %abs{letter}{i} = scalar.absf %{letter}{i} : f32
112    %finite{letter}{i} = scalar.cmpf ole, %abs{letter}{i}, %max : f32
113    %sum{letter}{next} = scalar.addf %sum{letter}{i}, %{letter}{i} : f32
114"#,
115                    last = elements - 1,
116                    next = i + 1
117                )
118                .unwrap();
119            }
120            writeln!(body, "    %xyfinite{i} = scalar.andi %finitex{i}, %finitey{i} : i1\n    %finite{next} = scalar.andi %finite{i}, %xyfinite{i} : i1",next=i+1).unwrap();
121            for (axis, letter) in [(0, "u"), (1, "v")] {
122                writeln!(
123                    body,
124                    "    %{letter}{i} = scalar.constant {:.9e} : f32",
125                    point[axis] - q[axis]
126                )
127                .unwrap();
128            }
129        }
130        writeln!(body,"    %px = scalar.divf %sumx{count}, %n : f32\n    %py = scalar.divf %sumy{count}, %n : f32").unwrap();
131        for i in 0..count {
132            writeln!(
133                body,
134                r#"
135    %sx{i} = scalar.subf %x{i}, %px : f32
136    %sy{i} = scalar.subf %y{i}, %py : f32
137    %xx{i} = scalar.mulf %sx{i}, %sx{i} : f32
138    %yy{i} = scalar.mulf %sy{i}, %sy{i} : f32
139    %xy{i} = scalar.addf %xx{i}, %yy{i} : f32
140    %den{next} = scalar.addf %den{i}, %xy{i} : f32
141    %xu{i} = scalar.mulf %sx{i}, %u{i} : f32
142    %yv{i} = scalar.mulf %sy{i}, %v{i} : f32
143    %dot{i} = scalar.addf %xu{i}, %yv{i} : f32
144    %dot{next}sum = scalar.addf %dot{i}sum, %dot{i} : f32
145    %xv{i} = scalar.mulf %sx{i}, %v{i} : f32
146    %yu{i} = scalar.mulf %sy{i}, %u{i} : f32
147    %cross{i} = scalar.subf %xv{i}, %yu{i} : f32
148    %cross{next}sum = scalar.addf %cross{i}sum, %cross{i} : f32
149"#,
150                next = i + 1
151            )
152            .unwrap();
153        }
154        let mut source = include_str!("similarity_2d.loom").replace("@BODY@", &body);
155        for (name, value) in [
156            ("BATCH", batch),
157            ("BATCH_LAST", batch - 1),
158            ("GRID", batch.div_ceil(64)),
159            ("INPUT", elements),
160            ("STRIDE", points.strides()[0]),
161            ("COUNT", count),
162            ("OUTPUT", batch * 6),
163            ("OUTPUT_LAST", batch * 6 - 1),
164        ] {
165            source = source.replace(&format!("@{name}@"), &value.to_string());
166        }
167        source = source
168            .replace("@QX@", &format!("{:.9e}", q[0]))
169            .replace("@QY@", &format!("{:.9e}", q[1]));
170        let mut model = ModelSession::in_context(&self.context)?;
171        let input = model.allocate(points.bytes())?;
172        let forward = model.allocate(matrices.bytes())?;
173        let inverse = model.allocate(matrices.bytes())?;
174        let flags = model.allocate_shared(status.bytes())?;
175        let kernel =
176            unsafe { model.compile(&[(&source, Specialization::new("similarity_2d"))])? }[0];
177        unsafe {
178            model.freeze(&self.context)?.fragment(
179                &[Command::Dispatch(Dispatch::indices(
180                    kernel,
181                    [0],
182                    [batch.div_ceil(64) as u32, 1, 1],
183                    vec![
184                        input.read(),
185                        forward.write(),
186                        inverse.write(),
187                        flags.write(),
188                    ],
189                ))],
190                &[(input, points.clone())],
191                &[
192                    (forward, matrices.clone()),
193                    (inverse, matrices),
194                    (flags, status),
195                ],
196            )
197        }
198    }
199
200    /// Fit resident landmarks; no landmark or matrix readback is performed.
201    pub fn similarity_2d(&self, points: &DeviceTensor, template: &[[f32; 2]]) -> Result<Inference> {
202        self.context.validate(points)?;
203        self.prepare_similarity_2d(points.desc(), template)?
204            .submit(std::slice::from_ref(points))
205    }
206}