open-standards/wai/wai-rs/src/synthesis_mark.rs

222 lines
9.3 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! What the second marking layer's registered scheme survives, measured
//! (extensions/synthesis.md §7.5.7 to §7.5.9), and the scheme's conformance corpus
//! (`live-conformance/synthesis-mark/`).
//!
//! The scheme itself, `wai-mark:tent64`, is [`crate::synthesis::mark`]: its embedding and its
//! key-free detector are part of the synthesis verifier, which recomputes a marking and runs a
//! detection again. This module holds what is not needed to verify:
//!
//! - [`battery`]: the attacks, each an integer function of the frames (re-encoding with the
//! crate's own integer codecs, rescaling, cropping, rotation, noise from a fixed stream,
//! brightness and contrast, a local blur, the quarter turns and mirrors), so a run of the
//! battery is the same bytes on every machine;
//! - the **measurement sets** ([`spectral_set`], [`leaves_set`], [`clips`]): pictures and
//! clips the crate generates from fixed seeds, the spectral generator of `wai.generate.*`
//! with its committed model and a dead-leaves generator of this module;
//! - the **quality figures** ([`psnr`], [`ssim`]) the measurement states per picture;
//! - [`corpus`]: the conformance corpus, its battery table and its verifier.
pub mod battery;
pub mod corpus;
use crate::synthesis::mark;
use sha2::{Digest, Sha256};
/// One picture or clip: `n` frames of `w` × `h` RGB8, frame after frame.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Frames {
pub data: Vec<u8>,
pub w: usize,
pub h: usize,
pub n: usize,
}
impl Frames {
pub fn frame(&self, i: usize) -> &[u8] {
let fb = self.w * self.h * 3;
&self.data[i * fb..(i + 1) * fb]
}
pub fn one(data: Vec<u8>, w: usize, h: usize) -> Frames {
Frames { data, w, h, n: 1 }
}
}
/// A stream of 32-bit words: SHA-256 of `label`, `seed` as a big-endian `u32` and a
/// big-endian `u32` counter from 0, each digest read as eight big-endian words.
pub fn words(label: &[u8], seed: u32) -> impl Iterator<Item = u32> + use<> {
let label = label.to_vec();
(0u32..).flat_map(move |i| {
let d: [u8; 32] = Sha256::new().chain_update(&label).chain_update(seed.to_be_bytes()).chain_update(i.to_be_bytes()).finalize().into();
(0..8).map(move |o| u32::from_be_bytes(d[o * 4..o * 4 + 4].try_into().expect("four bytes")))
})
}
// ---------------------------------------------------------------------------------------
// The measurement sets
// ---------------------------------------------------------------------------------------
/// The side of a measurement set's pictures.
pub const SET_SIDE: usize = 512;
/// The pictures in each set.
pub const SET_SIZE: usize = 16;
/// The prompts of the spectral set, one picture each, seeded by its index.
pub const PROMPTS: [&str; SET_SIZE] = [
"a calm ocean horizon",
"fire and chaos",
"a tall forest",
"a spiral galaxy",
"a storm cloud",
"a desert dune at sunset",
"a jungle leaf",
"marble stone",
"a city grid",
"lava ember",
"an ice glacier",
"a nebula in the cosmos",
"a garden in blossom",
"a maze lattice",
"a whirlpool of water",
"a mountain in snow",
];
fn spectral_model() -> &'static crate::generate::SpectralModel {
static M: std::sync::OnceLock<crate::generate::SpectralModel> = std::sync::OnceLock::new();
M.get_or_init(|| {
let v: serde_json::Value = serde_json::from_str(include_str!("../tests/vectors/generate/generate_spectral_model.json")).expect("the committed model");
crate::generate::SpectralModel::from_value(&v)
})
}
/// A spectral picture: `wai.generate.spectral`'s generator with its committed model.
pub fn spectral(prompt: &str, seed: u64, w: usize, h: usize) -> Vec<u8> {
crate::generate::generate(spectral_model(), prompt, seed, 4, h, w)
}
/// The spectral set: [`PROMPTS`], each at [`SET_SIDE`] square, seed its index.
pub fn spectral_set() -> Vec<(String, Frames)> {
PROMPTS.iter().enumerate().map(|(i, p)| (format!("spectral-{i:02}"), Frames::one(spectral(p, i as u64, SET_SIDE, SET_SIDE), SET_SIDE, SET_SIDE))).collect()
}
/// A dead-leaves picture: 600 discs painted back to front over grey, each with a radius
/// `⌊r_max·16 / (16 + u)⌋` (at least 3) for `u` uniform in 0..240, a colour each channel of
/// which is uniform in 16..240, and a linear shading of slope up to 3/8 a pixel; then noise
/// uniform in −2..2 on every sample. Every draw is a word of [`words`] with the label
/// `wai-mark:battery leaves` and the seed.
pub fn leaves(seed: u32, w: usize, h: usize) -> Vec<u8> {
let mut r = words(b"wai-mark:battery leaves", seed);
let mut next = move || r.next().expect("an endless stream") as i64;
let mut px = vec![128u8; w * h * 3];
let rmax = (w.min(h) / 4) as i64;
for _ in 0..600 {
let (cx, cy) = (next() % w as i64, next() % h as i64);
let rad = (rmax * 16 / (16 + next() % 240)).max(3);
let base = [next() % 224 + 16, next() % 224 + 16, next() % 224 + 16];
let (gx, gy) = (next() % 7 - 3, next() % 7 - 3);
for y in (cy - rad).max(0)..(cy + rad + 1).min(h as i64) {
for x in (cx - rad).max(0)..(cx + rad + 1).min(w as i64) {
let (dx, dy) = (x - cx, y - cy);
if dx * dx + dy * dy > rad * rad {
continue;
}
let shade = (gx * dx + gy * dy).div_euclid(8);
let p = (y as usize * w + x as usize) * 3;
for c in 0..3 {
px[p + c] = (base[c] + shade).clamp(0, 255) as u8;
}
}
}
}
for v in px.iter_mut() {
*v = (*v as i64 + next() % 5 - 2).clamp(0, 255) as u8;
}
px
}
/// The dead-leaves set: seeds 0 to 15, each at [`SET_SIDE`] square.
pub fn leaves_set() -> Vec<(String, Frames)> {
(0..SET_SIZE as u32).map(|i| (format!("leaves-{i:02}"), Frames::one(leaves(i, SET_SIDE, SET_SIDE), SET_SIDE, SET_SIDE))).collect()
}
/// The side and length of the measurement clips.
pub const CLIP_SIDE: usize = 256;
pub const CLIP_FRAMES: usize = 8;
/// A clip: a [`CLIP_SIDE`] square window panning over a picture of [`SET_SIDE`], `(4, 6)`
/// pixels a frame from `(64, 32)`.
pub fn pan(src: &[u8]) -> Frames {
let mut data = Vec::with_capacity(CLIP_SIDE * CLIP_SIDE * 3 * CLIP_FRAMES);
for f in 0..CLIP_FRAMES {
let (x0, y0) = (64 + 6 * f, 32 + 4 * f);
for y in y0..y0 + CLIP_SIDE {
data.extend_from_slice(&src[(y * SET_SIDE + x0) * 3..(y * SET_SIDE + x0 + CLIP_SIDE) * 3]);
}
}
Frames { data, w: CLIP_SIDE, h: CLIP_SIDE, n: CLIP_FRAMES }
}
/// The clips: panning over two dead-leaves and two spectral pictures.
pub fn clips() -> Vec<(String, Frames)> {
vec![
("clip-leaves-a".into(), pan(&leaves(100, SET_SIDE, SET_SIDE))),
("clip-leaves-b".into(), pan(&leaves(101, SET_SIDE, SET_SIDE))),
("clip-spectral-a".into(), pan(&spectral("a river in the valley", 100, SET_SIDE, SET_SIDE))),
("clip-spectral-b".into(), pan(&spectral("a swirl of smoke", 101, SET_SIDE, SET_SIDE))),
]
}
// ---------------------------------------------------------------------------------------
// Quality
// ---------------------------------------------------------------------------------------
/// The sum of squared differences of two buffers.
pub fn sse(a: &[u8], b: &[u8]) -> u64 {
a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).pow(2) as u64).sum()
}
/// PSNR in decibels of `samples` samples with this SSE: `10·log10(255²·samples / sse)`.
pub fn psnr(sse: u64, samples: usize) -> f64 {
10.0 * ((255.0 * 255.0 * samples as f64) / sse.max(1) as f64).log10()
}
/// The PSNR floor of a frame of `w` × `h` (§7.5.6): the PSNR of the full-amplitude
/// embedding.
pub fn floor(w: usize, h: usize) -> f64 {
psnr(mark::floor_sse(w, h), w * h * 3)
}
/// SSIM of two frames' luma: the mean over 8 × 8 windows at a stride of 4 of
/// `(2μxμy + C1)(2σxy + C2) / ((μx² + μy² + C1)(σx² + σy² + C2))`, `C1 = (0.01·255)²`,
/// `C2 = (0.03·255)²`, population moments. Only additions, multiplications and divisions,
/// in a fixed order, so the same bits on every machine.
pub fn ssim(a: &[u8], b: &[u8], w: usize, h: usize) -> f64 {
let ya: Vec<f64> = a.as_chunks::<3>().0.iter().map(|p| mark::luma(p[0], p[1], p[2]) as f64).collect();
let yb: Vec<f64> = b.as_chunks::<3>().0.iter().map(|p| mark::luma(p[0], p[1], p[2]) as f64).collect();
let (c1, c2) = ((0.01f64 * 255.0).powi(2), (0.03f64 * 255.0).powi(2));
let (mut total, mut n) = (0.0f64, 0u64);
let mut y0 = 0;
while y0 + 8 <= h {
let mut x0 = 0;
while x0 + 8 <= w {
let (mut sa, mut sb, mut saa, mut sbb, mut sab) = (0.0f64, 0.0f64, 0.0f64, 0.0f64, 0.0f64);
for y in y0..y0 + 8 {
for x in x0..x0 + 8 {
let (p, q) = (ya[y * w + x], yb[y * w + x]);
sa += p;
sb += q;
saa += p * p;
sbb += q * q;
sab += p * q;
}
}
let (ma, mb) = (sa / 64.0, sb / 64.0);
let (va, vb, cab) = (saa / 64.0 - ma * ma, sbb / 64.0 - mb * mb, sab / 64.0 - ma * mb);
total += ((2.0 * ma * mb + c1) * (2.0 * cab + c2)) / ((ma * ma + mb * mb + c1) * (va + vb + c2));
n += 1;
x0 += 4;
}
y0 += 4;
}
total / n as f64
}