222 lines
9.3 KiB
Rust
222 lines
9.3 KiB
Rust
//! 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
|
||
}
|