scena 1.7.2

A Rust-native scene-graph renderer with typed scene state, glTF assets, and explicit prepare/render lifecycles.
Documentation
use std::collections::BTreeMap;

use crate::scene::recipe::SceneRecipeQualityDepthOfFieldV1;

use super::metrics::frame_metrics;
use super::types::{
    self, RenderQualityCheckV1, RenderQualityDepthOfFieldMetrics, RenderQualityRegion,
    RenderQualityStatusV1,
};

pub struct DepthOfFieldQualityInput<'a> {
    pub focused_rgba8: &'a [u8],
    pub source_rgba8: &'a [u8],
    pub width: u32,
    pub height: u32,
    pub focal_region: RenderQualityRegion,
    pub background_region: RenderQualityRegion,
    pub expectation: &'a SceneRecipeQualityDepthOfFieldV1,
}

pub fn depth_of_field_metrics(
    focused_rgba8: &[u8],
    source_rgba8: &[u8],
    width: u32,
    height: u32,
    focal_region: RenderQualityRegion,
    background_region: RenderQualityRegion,
) -> RenderQualityDepthOfFieldMetrics {
    let source_background = frame_metrics(source_rgba8, width, height, background_region);
    let focused_background = frame_metrics(focused_rgba8, width, height, background_region);
    let source_background_sobel = source_background.sobel_energy;
    let focused_background_sobel = focused_background.sobel_energy;
    let background_sobel_drop = (source_background_sobel - focused_background_sobel).max(0.0);
    let background_sobel_drop_fraction = if source_background_sobel <= f32::EPSILON {
        0.0
    } else {
        background_sobel_drop / source_background_sobel
    };
    RenderQualityDepthOfFieldMetrics {
        source_background_sobel: types::round3(source_background_sobel),
        focused_background_sobel: types::round3(focused_background_sobel),
        background_sobel_drop: types::round3(background_sobel_drop),
        background_sobel_drop_fraction: types::round3(background_sobel_drop_fraction),
        focal_mean_delta: types::round3(mean_rgb_delta(
            focused_rgba8,
            source_rgba8,
            width,
            height,
            focal_region,
        )),
    }
}

pub fn evaluate_depth_of_field_region_quality(
    id: &str,
    input: DepthOfFieldQualityInput<'_>,
) -> Vec<RenderQualityCheckV1> {
    let focused_rgba8 = input.focused_rgba8;
    let source_rgba8 = input.source_rgba8;
    let width = input.width;
    let height = input.height;
    let focal_region = input.focal_region;
    let background_region = input.background_region;
    let expectation = input.expectation;
    let metrics = depth_of_field_metrics(
        focused_rgba8,
        source_rgba8,
        width,
        height,
        focal_region,
        background_region,
    );
    let min_source_background_sobel =
        expectation.min_source_background_sobel.unwrap_or(0.035) as f32;
    let min_background_sobel_drop = expectation.min_background_sobel_drop.unwrap_or(0.018) as f32;
    let min_background_sobel_drop_fraction = expectation
        .min_background_sobel_drop_fraction
        .unwrap_or(0.18) as f32;
    let max_focal_mean_delta = expectation.max_focal_mean_delta.unwrap_or(0.08) as f32;

    let mut code = "depth_of_field_checked";
    let mut severity = "info";
    let mut fix_hint = "no action needed";
    if metrics.source_background_sobel < min_source_background_sobel {
        code = "depth_of_field_background_detail_missing";
        severity = "error";
        fix_hint = "use a textured or high-frequency background target when proving depth of field, otherwise blur cannot be measured";
    } else if metrics.background_sobel_drop < min_background_sobel_drop
        || metrics.background_sobel_drop_fraction < min_background_sobel_drop_fraction
    {
        code = "depth_of_field_blur_insufficient";
        severity = "error";
        fix_hint = "enable render.depth_of_field, increase radius_px or lower aperture_f_stop, and ensure focus_distance targets the focal subject instead of the background";
    } else if metrics.focal_mean_delta > max_focal_mean_delta {
        code = "depth_of_field_focal_softened";
        severity = "error";
        fix_hint = "set focus_distance to the focal subject distance or reduce depth-of-field radius/aperture so the subject remains sharp";
    }

    vec![RenderQualityCheckV1 {
        id: id.to_owned(),
        code: code.to_owned(),
        status: if severity == "error" {
            RenderQualityStatusV1::Failed
        } else {
            RenderQualityStatusV1::Checked
        },
        severity: severity.to_owned(),
        region: background_region.to_report(),
        observed: BTreeMap::from([
            (
                "background_sobel_drop".to_owned(),
                metrics.background_sobel_drop,
            ),
            (
                "background_sobel_drop_fraction".to_owned(),
                metrics.background_sobel_drop_fraction,
            ),
            ("focal_mean_delta".to_owned(), metrics.focal_mean_delta),
            (
                "focused_background_sobel".to_owned(),
                metrics.focused_background_sobel,
            ),
            (
                "source_background_sobel".to_owned(),
                metrics.source_background_sobel,
            ),
        ]),
        threshold: BTreeMap::from([
            (
                "max_focal_mean_delta".to_owned(),
                types::round3(max_focal_mean_delta),
            ),
            (
                "min_background_sobel_drop".to_owned(),
                types::round3(min_background_sobel_drop),
            ),
            (
                "min_background_sobel_drop_fraction".to_owned(),
                types::round3(min_background_sobel_drop_fraction),
            ),
            (
                "min_source_background_sobel".to_owned(),
                types::round3(min_source_background_sobel),
            ),
        ]),
        fix_hint: fix_hint.to_owned(),
    }]
}

fn mean_rgb_delta(
    left: &[u8],
    right: &[u8],
    width: u32,
    height: u32,
    region: RenderQualityRegion,
) -> f32 {
    if left.len() != right.len() {
        return 1.0;
    }
    let max_x = region.x.saturating_add(region.width).min(width);
    let max_y = region.y.saturating_add(region.height).min(height);
    let mut total = 0.0_f32;
    let mut count = 0_u32;
    for y in region.y..max_y {
        for x in region.x..max_x {
            let offset = ((y as usize) * (width as usize) + x as usize) * 4;
            let Some(left_pixel) = left.get(offset..offset + 3) else {
                continue;
            };
            let Some(right_pixel) = right.get(offset..offset + 3) else {
                continue;
            };
            total += left_pixel
                .iter()
                .zip(right_pixel)
                .map(|(left, right)| f32::from(left.abs_diff(*right)) / 255.0)
                .sum::<f32>()
                / 3.0;
            count = count.saturating_add(1);
        }
    }
    if count == 0 {
        1.0
    } else {
        total / count as f32
    }
}