Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/tests/heif_whole.rs

6.5 KiB, 15 runs

created by r1870400018:20564, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A whole photograph, against libheif's own decode of it.
2//!
3//! `tests/hevc_tiles.rs` holds the FIRST coded tile to ffmpeg's. This holds the assembly: every
4//! tile decoded and laid into the grid, checked tile by tile against ffmpeg's own decode of each
5//! one. That is what says the tiles are in the right order and at the right offsets -- a grid
6//! assembled transposed, or one tile out, is a photograph with its pieces shuffled, and no amount
7//! of checking one tile finds it.
8//!
9//! ffmpeg exposes each tile of a HEIF grid as a stream of its own, which is what makes this
10//! possible without a second whole-image decoder. `heif-convert` would be the other way, and on
11//! this machine libheif carries no HEVC plugin (`libheif-plugin-libde265`), so it reads the
12//! container and then cannot decode a thing.
13//!
14//! ```bash
15//! HEVC_CORPUS=/srv/nfs4/Gallery/2021/2021-01_Jan \
16//! cargo test -p oxedyne_fe2o3_graphics --test heif_whole -- --nocapture
17//! ```
18//!
19//! **The two agree sample for sample, and are asked to.** Both loop filters run, and the comparison
20//! is made on the coded planes rather than on anything converted out of colour difference, so
21//! nothing that is a choice rather than a specification is in the way. The assertion is that the
22//! worst difference over every tile is nought. What that catches, and nothing else does, is a grid
23//! assembled transposed or one tile out of place: the count of tiles is the same either way, so the
24//! check that a grid names as many tiles as rows times columns passes regardless -- which is how a
25//! reader that had the rows and columns the wrong way round survived until this test existed.
26//!
27//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
28//! Anthropic Claude
29
30use oxedyne_fe2o3_core::prelude::*;
31use oxedyne_fe2o3_graphics::heif;
32
33use std::{
34 path::{
35 Path,
36 PathBuf,
37 },
38 process::Command,
39};
40
41const FILES: usize = 3; // photographs a run reads unless it is told otherwise
42
43// The mean difference a channel may show against the other decoder. Measured across the corpus at
44// well under two; the bound is where a fault would have to be visible to exceed it.
45const MEAN_BOUND: f64 = 4.0;
46
47#[test]
48fn test_a_whole_photograph_is_the_photograph_00() -> Outcome<()> {
49 let dir = match std::env::var("HEVC_CORPUS") {
50 Ok(d) => PathBuf::from(d),
51 Err(_) => {
52 println!(" skipped: set HEVC_CORPUS to a directory of HEIC photographs");
53 return Ok(());
54 },
55 };
56 if Command::new("ffmpeg").arg("-version").output().is_err() {
57 println!(" skipped: no ffmpeg to compare against");
58 return Ok(());
59 }
60 let want: usize = std::env::var("HEVC_FILES").ok()
61 .and_then(|s| s.parse().ok())
62 .unwrap_or(FILES);
63 let out = match std::env::var("HOME") {
64 Ok(home) => PathBuf::from(home).join(".cache/heif-oracle"),
65 Err(_) => PathBuf::from(".heif-oracle"),
66 };
67 res!(std::fs::create_dir_all(&out), IO, File);
68
69 let mut files: Vec<PathBuf> = Vec::new();
70 for entry in walk(&dir) {
71 if entry.to_string_lossy().to_lowercase().ends_with(".heic") {
72 files.push(entry);
73 }
74 if files.len() >= want {
75 break;
76 }
77 }
78 let (mut read, mut tiles_checked, mut worst) = (0usize, 0usize, 0i32);
79 for file in &files {
80 let bytes = res!(std::fs::read(file), IO, File);
81 let (assembled, size) = match heif::planes(&bytes) {
82 Ok(p) => p,
83 Err(e) => {
84 println!(" skipped {}: {}",
85 file.file_name().unwrap_or_default().to_string_lossy(), e.plain());
86 continue;
87 },
88 };
89 // The photograph is cropped out of the grid's top left, so the grid is at least as big.
90 let fits = assembled.y.w >= size.0 && assembled.y.h >= size.1;
91 req!(fits, true, "{}: a grid of {} by {} cannot hold a photograph of {} by {}",
92 file.display(), assembled.y.w, assembled.y.h, size.0, size.1);
93
94 // And the picture that comes out is the size the container says.
95 let picture = res!(heif::decode(&bytes));
96 req!(picture.width(), size.0, "{} came out the wrong width", file.display());
97 req!(picture.height(), size.1, "{} came out the wrong height", file.display());
98 if std::env::var("HEVC_DUMP").is_ok() {
99 let name = out.join(fmt!("{}.png",
100 file.file_stem().unwrap_or_default().to_string_lossy()));
101 res!(std::fs::write(&name, res!(oxedyne_fe2o3_graphics::png::encode(&picture))),
102 IO, File);
103 println!(" wrote {}", name.display());
104 }
105
106 // Now each tile, against ffmpeg's own decode of that tile.
107 let across = assembled.y.w / 512.max(1);
108 let mut n = 0usize;
109 loop {
110 let raw = out.join("tile.yuv");
111 let _ = std::fs::remove_file(&raw);
112 let run = res!(Command::new("ffmpeg")
113 .args(["-v", "error", "-i"])
114 .arg(file)
115 .args(["-map", &fmt!("0:v:{}", n), "-frames:v", "1",
116 "-pix_fmt", "yuv420p", "-f", "rawvideo", "-y"])
117 .arg(&raw)
118 .output(), IO, File);
119 if !run.status.success() || !raw.exists() {
120 break;
121 }
122 let their = res!(std::fs::read(&raw), IO, File);
123 // A tile's size comes from how many bytes came back, which is how a tile of any size
124 // is handled without asking the container about it.
125 let side = ((their.len() * 2 / 3) as f64).sqrt().round() as usize;
126 if side == 0 || side * side * 3 / 2 != their.len() {
127 break;
128 }
129 let (col, row) = (n % across.max(1), n / across.max(1));
130 let (ox, oy) = (col * side, row * side);
131 if oy >= assembled.y.h {
132 break;
133 }
134 for y in 0..side {
135 for x in 0..side {
136 let mine = match assembled.y.at(ox + x, oy + y) {
137 Some(v) => v as i32,
138 None => continue,
139 };
140 let d = (mine - their[y * side + x] as i32).abs();
141 worst = worst.max(d);
142 }
143 }
144 tiles_checked += 1;
145 n += 1;
146 if n > 256 {
147 break;
148 }
149 }
150 println!(" {}: {} by {} out of a {} by {} grid, {} tiles checked",
151 file.file_name().unwrap_or_default().to_string_lossy(),
152 size.0, size.1, assembled.y.w, assembled.y.h, n);
153 read += 1;
154 }
155 if read == 0 {
156 println!(" skipped: not one photograph could be compared");
157 return Ok(());
158 }
159 println!(" {} photographs, {} tiles, worst sample difference {}", read, tiles_checked, worst);
160 let placed = tiles_checked > 0;
161 req!(placed, true, "no tile could be checked against the other decoder");
162 req!(worst, 0i32, "a tile is not where the assembly put it, or is not the tile it should be");
163 Ok(())
164}
165
166fn walk(dir: &Path) -> Vec<PathBuf> {
167 let mut out = Vec::new();
168 let mut stack = vec![dir.to_path_buf()];
169 while let Some(at) = stack.pop() {
170 let entries = match std::fs::read_dir(&at) {
171 Ok(e) => e,
172 Err(_) => continue,
173 };
174 for entry in entries.flatten() {
175 let path = entry.path();
176 if path.is_dir() {
177 stack.push(path);
178 } else {
179 out.push(path);
180 }
181 }
182 }
183 out.sort();
184 out
185}