leibniz 0.2.0

The package provides a differentiable vector graphics rasterization loss.
Documentation
//! Box reconstruction filter shared by the forward raster and the boundary
//! gather.
//!
//! Mirrors DiffVG's pixel filter: the forward render and `gather_d_color` use
//! the same box filter, and the per-pixel `weight_image` normalizes both. The
//! filter is separable, so each axis contributes a selection matrix mapping
//! sub-sample positions to pixels and a per-pixel sub-sample count.

use ::burn::tensor::{Int, Tensor, backend::Backend};

/// Selection matrix with shape `[extent, extent * samples]`.
///
/// Entry `[pixel, sub]` is one when sub-sample `sub` lies within `radius` of the
/// pixel center, mirroring DiffVG's box filter `|d| > radius` cutoff.
pub type Selection<B> = Tensor<B, 2>;

/// Per-pixel sub-sample counts with shape `[extent]`.
pub type Counts<B> = Tensor<B, 1>;

/// Per-pixel forward filter weights with shape `[height, width]`.
pub type Weights<B> = Tensor<B, 2>;

/// Build the selection matrix and per-pixel sub-sample counts for one axis.
///
/// Pixel centers sit at `pixel + 0.5`; sub-sample centers sit at
/// `(sub + 0.5) / samples`. A sub-sample contributes to a pixel when their
/// distance is at most `radius`.
pub fn axis<B: Backend>(
    extent: usize,
    sample_count: usize,
    radius: f32,
    device: &B::Device,
) -> (Selection<B>, Counts<B>) {
    let centers =
        (Tensor::<B, 1, Int>::arange(0..extent as i64, device).float() + 0.5).unsqueeze_dim::<2>(1);
    let fine = ((Tensor::<B, 1, Int>::arange(0..(extent * sample_count) as i64, device).float()
        + 0.5)
        / sample_count as f32)
        .unsqueeze_dim::<2>(0);
    let selection = (centers - fine).abs().lower_equal_elem(radius).float();
    let counts = selection.clone().sum_dim(1).squeeze_dim::<1>(1);

    (selection, counts)
}

/// Build the per-pixel forward filter weight.
///
/// The weight is the number of antialiasing sub-samples whose position lies
/// within `radius` of the pixel center, matching DiffVG's `weight_image`. The
/// box filter is separable, so the weight is the outer product of the per-axis
/// counts.
pub fn weight_image<B: Backend>(
    height: usize,
    width: usize,
    x_sample_count: usize,
    y_sample_count: usize,
    radius: f32,
    device: &B::Device,
) -> Weights<B> {
    let (_, column_counts) = axis::<B>(width, x_sample_count, radius, device);
    let (_, row_counts) = axis::<B>(height, y_sample_count, radius, device);

    row_counts.unsqueeze_dim::<2>(1) * column_counts.unsqueeze_dim::<2>(0)
}

#[cfg(test)]
mod tests {
    use super::weight_image;
    use crate::burn::tests::Backend;

    #[test]
    fn counts_subsamples_within_radius() {
        let weight = weight_image::<Backend>(2, 2, 2, 2, 0.5, &Default::default());

        assert_eq!(weight.into_data().to_vec::<f32>().unwrap(), [4.0; 4]);
    }

    #[test]
    fn widens_with_radius_and_clips_at_borders() {
        let weight = weight_image::<Backend>(1, 3, 1, 1, 1.0, &Default::default());

        // Pixel centers at 0.5, 1.5, 2.5; one sub-sample per pixel at the same
        // positions. Radius 1.0 reaches one neighbor on each interior side.
        assert_eq!(weight.into_data().to_vec::<f32>().unwrap(), [2.0, 3.0, 2.0]);
    }
}