#![allow(clippy::expect_used, clippy::unwrap_used)]
use rand_distr::{Distribution, Normal};
#[must_use]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::missing_panics_doc
)]
pub fn generate_synthetic_test_image(
family: crate::config::TagFamily,
id: u16,
tag_size: usize,
canvas_size: usize,
noise_sigma: f32,
) -> (Vec<u8>, [[f64; 2]; 4]) {
let mut data = vec![255u8; canvas_size * canvas_size];
let margin = (canvas_size - tag_size) / 2;
let quiet_zone = tag_size / 5;
for y in margin.saturating_sub(quiet_zone)..(margin + tag_size + quiet_zone).min(canvas_size) {
for x in
margin.saturating_sub(quiet_zone)..(margin + tag_size + quiet_zone).min(canvas_size)
{
data[y * canvas_size + x] = 255;
}
}
let decoder = crate::decoder::family_to_decoder(family);
let code = decoder.get_code(id).expect("Invalid tag ID for family");
let dim = decoder.dimension();
let cell_size = tag_size / (dim + 2); let actual_tag_size = cell_size * (dim + 2);
let start_x = margin + (tag_size - actual_tag_size) / 2;
let start_y = margin + (tag_size - actual_tag_size) / 2;
for y in 0..(dim + 2) {
for x in 0..(dim + 2) {
if x == 0 || x == dim + 1 || y == 0 || y == dim + 1 {
draw_cell(&mut data, canvas_size, start_x, start_y, x, y, cell_size, 0);
}
}
}
let points = decoder.sample_points();
let d_f = (dim + 2) as f64;
for (i, p) in points.iter().enumerate() {
let gx = ((p.0 + 1.0) * d_f / 2.0 - 0.5).round() as usize;
let gy = ((p.1 + 1.0) * d_f / 2.0 - 0.5).round() as usize;
let bit = (code >> i) & 1;
let val = if bit != 0 { 255 } else { 0 };
draw_cell(
&mut data,
canvas_size,
start_x,
start_y,
gx,
gy,
cell_size,
val,
);
}
if noise_sigma > 0.0 {
let mut rng = rand::rng();
let normal = Normal::new(0.0, f64::from(noise_sigma)).expect("Invalid noise params");
for pixel in &mut data {
let noise = normal.sample(&mut rng) as i32;
let val = (i32::from(*pixel) + noise).clamp(0, 255);
*pixel = val as u8;
}
}
let gt_corners = [
[start_x as f64, start_y as f64],
[(start_x + actual_tag_size) as f64, start_y as f64],
[
(start_x + actual_tag_size) as f64,
(start_y + actual_tag_size) as f64,
],
[start_x as f64, (start_y + actual_tag_size) as f64],
];
(data, gt_corners)
}
#[allow(clippy::too_many_arguments)]
fn draw_cell(
data: &mut [u8],
stride: usize,
start_x: usize,
start_y: usize,
cx: usize,
cy: usize,
size: usize,
val: u8,
) {
let px = start_x + cx * size;
let py = start_y + cy * size;
for y in py..(py + size) {
for x in px..(px + size) {
data[y * stride + x] = val;
}
}
}
#[must_use]
pub fn compute_corner_error(detected: &[[f64; 2]; 4], ground_truth: &[[f64; 2]; 4]) -> f64 {
compute_rmse(detected, ground_truth)
}
#[must_use]
pub fn compute_rmse(detected: &[[f64; 2]; 4], ground_truth: &[[f64; 2]; 4]) -> f64 {
let mut sum_sq = 0.0;
for i in 0..4 {
let dx = detected[i][0] - ground_truth[i][0];
let dy = detected[i][1] - ground_truth[i][1];
sum_sq += dx * dx + dy * dy;
}
(sum_sq / 4.0).sqrt()
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct TestImageParams {
pub family: crate::config::TagFamily,
pub id: u16,
pub tag_size: usize,
pub canvas_size: usize,
pub noise_sigma: f32,
pub brightness_offset: i16,
pub contrast_scale: f32,
}
impl Default for TestImageParams {
fn default() -> Self {
Self {
family: crate::config::TagFamily::AprilTag36h11,
id: 0,
tag_size: 100,
canvas_size: 320,
noise_sigma: 0.0,
brightness_offset: 0,
contrast_scale: 1.0,
}
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
#[allow(dead_code)]
pub(crate) fn generate_test_image_with_params(
params: &TestImageParams,
) -> (Vec<u8>, [[f64; 2]; 4]) {
let (mut data, corners) = generate_synthetic_test_image(
params.family,
params.id,
params.tag_size,
params.canvas_size,
params.noise_sigma,
);
if params.brightness_offset != 0 || (params.contrast_scale - 1.0).abs() > 0.001 {
apply_brightness_contrast(
&mut data,
i32::from(params.brightness_offset),
params.contrast_scale,
);
}
(data, corners)
}
#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)]
#[allow(dead_code)]
pub(crate) fn apply_brightness_contrast(image: &mut [u8], brightness: i32, contrast: f32) {
for pixel in image.iter_mut() {
let b = f32::from(*pixel);
let with_contrast = (b - 128.0) * contrast + 128.0;
let with_brightness = with_contrast as i32 + brightness;
*pixel = with_brightness.clamp(0, 255) as u8;
}
}
#[must_use]
#[allow(clippy::naive_bytecount)]
#[allow(dead_code)]
pub(crate) fn count_black_pixels(data: &[u8]) -> usize {
data.iter().filter(|&&p| p == 0).count()
}
#[must_use]
#[allow(clippy::cast_sign_loss)]
#[allow(dead_code)]
pub(crate) fn measure_border_integrity(
binary: &[u8],
width: usize,
corners: &[[f64; 2]; 4],
) -> f64 {
let min_x = corners
.iter()
.map(|c| c[0])
.fold(f64::MAX, f64::min)
.max(0.0) as usize;
let max_x = corners
.iter()
.map(|c| c[0])
.fold(f64::MIN, f64::max)
.max(0.0) as usize;
let min_y = corners
.iter()
.map(|c| c[1])
.fold(f64::MAX, f64::min)
.max(0.0) as usize;
let max_y = corners
.iter()
.map(|c| c[1])
.fold(f64::MIN, f64::max)
.max(0.0) as usize;
let height = binary.len() / width;
let min_x = min_x.min(width.saturating_sub(1));
let max_x = max_x.min(width.saturating_sub(1));
let min_y = min_y.min(height.saturating_sub(1));
let max_y = max_y.min(height.saturating_sub(1));
if max_x <= min_x || max_y <= min_y {
return 0.0;
}
let tag_width = max_x - min_x;
let tag_height = max_y - min_y;
let cell_size_x = tag_width / 8;
let cell_size_y = tag_height / 8;
if cell_size_x == 0 || cell_size_y == 0 {
return 0.0;
}
let mut black_count = 0usize;
let mut total_count = 0usize;
for y in min_y..(min_y + cell_size_y).min(max_y) {
for x in min_x..=max_x {
if y < height && x < width {
total_count += 1;
if binary[y * width + x] == 0 {
black_count += 1;
}
}
}
}
let bottom_start = max_y.saturating_sub(cell_size_y);
for y in bottom_start..=max_y {
for x in min_x..=max_x {
if y < height && x < width {
total_count += 1;
if binary[y * width + x] == 0 {
black_count += 1;
}
}
}
}
for y in (min_y + cell_size_y)..(max_y.saturating_sub(cell_size_y)) {
for x in min_x..(min_x + cell_size_x).min(max_x) {
if y < height && x < width {
total_count += 1;
if binary[y * width + x] == 0 {
black_count += 1;
}
}
}
}
let right_start = max_x.saturating_sub(cell_size_x);
for y in (min_y + cell_size_y)..(max_y.saturating_sub(cell_size_y)) {
for x in right_start..=max_x {
if y < height && x < width {
total_count += 1;
if binary[y * width + x] == 0 {
black_count += 1;
}
}
}
}
if total_count == 0 {
0.0
} else {
black_count as f64 / total_count as f64
}
}
#[must_use]
#[cfg(feature = "bench-internals")]
pub fn generate_checkered(width: usize, height: usize) -> Vec<u8> {
let mut data = vec![200u8; width * height];
for y in (0..height).step_by(16) {
for x in (0..width).step_by(16) {
if ((x / 16) + (y / 16)) % 2 == 0 {
for dy in 0..16 {
if y + dy < height {
let row_off = (y + dy) * width;
for dx in 0..16 {
if x + dx < width {
data[row_off + x + dx] = 50;
}
}
}
}
}
}
}
data
}
pub mod subpixel;
#[cfg(any(feature = "extended-tests", feature = "extended-bench"))]
pub mod scene;
#[cfg(any(feature = "extended-tests", feature = "extended-bench"))]
pub use scene::{SceneBuilder, TagPlacement};