SHA256
Funktioning rewrite, that is able to use gpu on a more variety of
systems
This commit is contained in:
+326
@@ -0,0 +1,326 @@
|
||||
//! Port of `pipeline_360.py rig --self-test`: validates stage-4 geometry &
|
||||
//! schema without any video or GPU. Pure `std` so it can run offline.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::json;
|
||||
use crate::math;
|
||||
use crate::stages::rig::{self, Layout, LinkMode};
|
||||
|
||||
/// Write a tiny valid (stored/deflate) single-colour PNG so rig's pure-std
|
||||
/// dimension reader and copy-linker can operate on it without a codec.
|
||||
fn write_dummy_png(path: &Path, width: u32, height: u32) -> std::io::Result<()> {
|
||||
// Build an uncompressed-all-zero RGB image: each scanline prefixed with a
|
||||
// filter byte 0, then width RGB triples.
|
||||
let mut raw: Vec<u8> = Vec::with_capacity((height as usize) * (1 + 3 * width as usize));
|
||||
for _ in 0..height {
|
||||
raw.push(0);
|
||||
raw.extend(std::iter::repeat(0u8).take((3 * width) as usize));
|
||||
}
|
||||
// zlib stream: a single "stored" deflate block (BTYPE=00) with the full
|
||||
// uncompressed length, no compression. Valid zlib with header 0x78 0x01.
|
||||
let len = raw.len() as u32;
|
||||
let mut deflate = Vec::new();
|
||||
deflate.push(0x01); // BFINAL=1, BTYPE=00
|
||||
deflate.extend_from_slice(&len.to_le_bytes());
|
||||
deflate.extend_from_slice(&(!len).to_le_bytes());
|
||||
deflate.extend_from_slice(&raw);
|
||||
let mut idat = vec![0x78, 0x01];
|
||||
// zlib adler32 of `raw`.
|
||||
let adler = adler32(&raw);
|
||||
idat.extend_from_slice(&deflate);
|
||||
idat.extend_from_slice(&adler.to_be_bytes());
|
||||
|
||||
let ihdr_data = make_ihdr(width, height);
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(b"\x89PNG\r\n\x1a\n");
|
||||
png_chunk(&mut out, b"IHDR", &ihdr_data);
|
||||
png_chunk(&mut out, b"IDAT", &idat);
|
||||
png_chunk(&mut out, b"IEND", &[]);
|
||||
std::fs::create_dir_all(path.parent().unwrap_or(Path::new(".")))?;
|
||||
std::fs::write(path, out)
|
||||
}
|
||||
|
||||
fn make_ihdr(width: u32, height: u32) -> Vec<u8> {
|
||||
let mut d = Vec::new();
|
||||
d.extend_from_slice(&width.to_be_bytes());
|
||||
d.extend_from_slice(&height.to_be_bytes());
|
||||
d.extend_from_slice(&[8, 2, 0, 0, 0]); // bit depth 8, colour type 2 (RGB)
|
||||
d
|
||||
}
|
||||
|
||||
fn png_chunk(out: &mut Vec<u8>, tag: &[u8; 4], data: &[u8]) {
|
||||
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(tag);
|
||||
out.extend_from_slice(data);
|
||||
// CRC32 of (tag + data).
|
||||
let mut crc_input = Vec::new();
|
||||
crc_input.extend_from_slice(tag);
|
||||
crc_input.extend_from_slice(data);
|
||||
out.extend_from_slice(&crc32(&crc_input).to_be_bytes());
|
||||
}
|
||||
|
||||
fn crc32(data: &[u8]) -> u32 {
|
||||
let mut table = [0u32; 256];
|
||||
for (n, item) in table.iter_mut().enumerate() {
|
||||
let mut c = n as u32;
|
||||
for _ in 0..8 {
|
||||
c = if c & 1 != 0 {
|
||||
0xEDB8_8320 ^ (c >> 1)
|
||||
} else {
|
||||
c >> 1
|
||||
};
|
||||
}
|
||||
*item = c;
|
||||
}
|
||||
let mut c = 0xFFFF_FFFFu32;
|
||||
for &b in data {
|
||||
c = table[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
|
||||
}
|
||||
c ^ 0xFFFF_FFFF
|
||||
}
|
||||
|
||||
fn adler32(data: &[u8]) -> u32 {
|
||||
let mut a = 1u32;
|
||||
let mut b = 0u32;
|
||||
for &byte in data {
|
||||
a = (a + byte as u32) % 65521;
|
||||
b = (b + a) % 65521;
|
||||
}
|
||||
(b << 16) | a
|
||||
}
|
||||
|
||||
fn approx_eq_vec(a: &[f64], b: &[f64], tol: f64) -> bool {
|
||||
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tol)
|
||||
}
|
||||
|
||||
/// Run the six check groups. Returns `(ok_count, first_error)`.
|
||||
pub fn run_selftest() -> Result<usize, String> {
|
||||
let n = 64u32;
|
||||
let mut ok = 0usize;
|
||||
|
||||
// Group 1: every FACE_AXES rotation is valid.
|
||||
for face in math::FACES {
|
||||
let m = math::face_rotation_matrix(face);
|
||||
math::assert_valid_rotation(&m, &format!("FACE_AXES[{face}]"), 1e-9)
|
||||
.map_err(|e| format!("group 1: {e}"))?;
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
// Group 2: R<-F quaternion and 90° angle.
|
||||
let q_r = math::cam_from_rig_rotation("R", "F");
|
||||
let sqrt2_2 = std::f64::consts::SQRT_2 / 2.0;
|
||||
let expected_r = [sqrt2_2, 0.0, -sqrt2_2, 0.0];
|
||||
if !approx_eq_vec(&[q_r.0, q_r.1, q_r.2, q_r.3], &expected_r, 1e-12) {
|
||||
return Err(format!("group 2: R quaternion wrong: {q_r:?}"));
|
||||
}
|
||||
if (math::rotation_angle_deg(q_r) - 90.0).abs() >= 1e-9 {
|
||||
return Err("group 2: rotation angle not 90°".into());
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
// Group 3: forward directions from each face's transpose mapping +Z.
|
||||
let r_mat = math::face_rotation_matrix("R");
|
||||
let fwd_f = math::apply(&math::mat_transpose(&r_mat), &[0.0, 0.0, 1.0]);
|
||||
if !approx_eq_vec(&fwd_f, &[1.0, 0.0, 0.0], 1e-12) {
|
||||
return Err(format!("group 3: R forward wrong: {fwd_f:?}"));
|
||||
}
|
||||
for (face, expected) in [
|
||||
("B", [0.0, 0.0, -1.0]),
|
||||
("L", [-1.0, 0.0, 0.0]),
|
||||
("U", [0.0, -1.0, 0.0]),
|
||||
("D", [0.0, 1.0, 0.0]),
|
||||
] {
|
||||
let got = math::apply(
|
||||
&math::mat_transpose(&math::face_rotation_matrix(face)),
|
||||
&[0.0, 0.0, 1.0],
|
||||
);
|
||||
if !approx_eq_vec(&got, &expected, 1e-12) {
|
||||
return Err(format!("group 3: face {face} forward wrong: {got:?}"));
|
||||
}
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
// Group 4: intrinsics.
|
||||
let (fx, fy, cx, cy) = math::compute_intrinsics(n, "linspace");
|
||||
if !approx_eq_vec(
|
||||
&[fx, fy, cx, cy],
|
||||
&[
|
||||
(n - 1) as f64 / 2.0,
|
||||
(n - 1) as f64 / 2.0,
|
||||
n as f64 / 2.0,
|
||||
n as f64 / 2.0,
|
||||
],
|
||||
0.0,
|
||||
) {
|
||||
return Err(format!("group 4: intrinsics wrong: {fx},{fy},{cx},{cy}"));
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
// Group 5: full rig run on a built-up dataset.
|
||||
let tmp = std::env::temp_dir().join(format!("rig_selftest_{}", std::process::id()));
|
||||
let root = tmp.as_path();
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
let flat = root.join("images");
|
||||
let masks_flat = root.join("masks");
|
||||
|
||||
for i in 1..=2u32 {
|
||||
for face in math::FACES {
|
||||
write_dummy_png(&flat.join(format!("frame_{i:05}_{face}.png")), n, n)
|
||||
.map_err(|e| format!("group 5: write image: {e}"))?;
|
||||
}
|
||||
}
|
||||
for face in ["F", "R", "B", "L"] {
|
||||
write_dummy_png(&flat.join(format!("frame_00003_{face}.png")), n, n)
|
||||
.map_err(|e| format!("group 5: write partial image: {e}"))?;
|
||||
}
|
||||
for face in math::FACES {
|
||||
write_dummy_png(
|
||||
&masks_flat.join(format!("frame_00001_{face}.png.png")),
|
||||
n,
|
||||
n,
|
||||
)
|
||||
.map_err(|e| format!("group 5: write mask: {e}"))?;
|
||||
}
|
||||
for face in ["F", "R"] {
|
||||
write_dummy_png(
|
||||
&masks_flat.join(format!("frame_00003_{face}.png.png")),
|
||||
n,
|
||||
n,
|
||||
)
|
||||
.map_err(|e| format!("group 5: write partial mask: {e}"))?;
|
||||
}
|
||||
|
||||
let out = root.join("rig.json");
|
||||
let link_tree = root.join("nested");
|
||||
let masks_link_tree = root.join("masks_nested");
|
||||
let _res = rig::run_rig_stage(
|
||||
&flat,
|
||||
&out,
|
||||
Some(&masks_flat),
|
||||
None,
|
||||
Layout::Auto,
|
||||
"F",
|
||||
"linspace",
|
||||
Some(&link_tree),
|
||||
Some(&masks_link_tree),
|
||||
LinkMode::Copy,
|
||||
)
|
||||
.map_err(|e| format!("group 5: run_rig_stage: {e}"))?;
|
||||
|
||||
let text = std::fs::read_to_string(&out).map_err(|e| format!("group 5: read rig: {e}"))?;
|
||||
let config = json::parse(&text).map_err(|e| format!("group 5: parse rig json: {e}"))?;
|
||||
let cams = config
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|v| v.find("cameras"))
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or("group 5: no cameras array")?;
|
||||
if cams.len() != 6 {
|
||||
return Err(format!("group 5: expected 6 cameras, got {}", cams.len()));
|
||||
}
|
||||
let refs: Vec<&json::Value> = cams
|
||||
.iter()
|
||||
.filter(|c| matches!(c.find("ref_sensor"), Some(crate::json::Value::Bool(true))))
|
||||
.collect();
|
||||
if refs.len() != 1 {
|
||||
return Err(format!(
|
||||
"group 5: expected exactly 1 ref_sensor, got {}",
|
||||
refs.len()
|
||||
));
|
||||
}
|
||||
if refs[0].find("image_prefix").and_then(|v| v.as_str()) != Some("F/") {
|
||||
return Err("group 5: ref prefix not F/".into());
|
||||
}
|
||||
let mut by_prefix: std::collections::HashMap<&str, &json::Value> =
|
||||
std::collections::HashMap::new();
|
||||
for c in cams {
|
||||
if let Some(p) = c.find("image_prefix").and_then(|v| v.as_str()) {
|
||||
by_prefix.insert(p, c);
|
||||
}
|
||||
}
|
||||
let got_r: Vec<f64> = by_prefix
|
||||
.get("R/")
|
||||
.and_then(|c| c.find("cam_from_rig_rotation"))
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|x| x.as_f64()).collect())
|
||||
.ok_or("group 5: no R rotation")?;
|
||||
if !approx_eq_vec(&got_r, &expected_r, 1e-12) {
|
||||
return Err(format!("group 5: R rotation wrong: {got_r:?}"));
|
||||
}
|
||||
if !link_tree.join("F").join("frame_00001.png").is_file() {
|
||||
return Err("group 5: linked image missing".into());
|
||||
}
|
||||
if !masks_link_tree
|
||||
.join("F")
|
||||
.join("frame_00001.png.png")
|
||||
.is_file()
|
||||
{
|
||||
return Err("group 5: linked mask missing".into());
|
||||
}
|
||||
if masks_link_tree
|
||||
.join("F")
|
||||
.join("frame_00002.png.png")
|
||||
.exists()
|
||||
{
|
||||
return Err("group 5: unexpected frame_00002 mask".into());
|
||||
}
|
||||
if !masks_link_tree
|
||||
.join("R")
|
||||
.join("frame_00003.png.png")
|
||||
.is_file()
|
||||
{
|
||||
return Err("group 5: partial frame_00003 mask missing".into());
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
// Group 6: partial rig (only 4 faces).
|
||||
let partial_dir = root.join("partial");
|
||||
for i in 1..=2u32 {
|
||||
for face in ["F", "R", "B", "L"] {
|
||||
write_dummy_png(&partial_dir.join(format!("frame_{i:05}_{face}.png")), n, n)
|
||||
.map_err(|e| format!("group 6: write partial: {e}"))?;
|
||||
}
|
||||
}
|
||||
let out2 = root.join("rig_partial.json");
|
||||
let _ = rig::run_rig_stage(
|
||||
&partial_dir,
|
||||
&out2,
|
||||
None,
|
||||
None,
|
||||
Layout::Auto,
|
||||
"F",
|
||||
"linspace",
|
||||
None,
|
||||
None,
|
||||
LinkMode::Copy,
|
||||
)
|
||||
.map_err(|e| format!("group 6: run_rig_stage: {e}"))?;
|
||||
let text2 = std::fs::read_to_string(&out2).map_err(|e| format!("group 6: read rig: {e}"))?;
|
||||
let cfg2 = json::parse(&text2).map_err(|e| format!("group 6: parse: {e}"))?;
|
||||
let cams2 = cfg2
|
||||
.as_array()
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|v| v.find("cameras"))
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or("group 6: no cameras")?;
|
||||
if cams2.len() != 4 {
|
||||
return Err(format!("group 6: expected 4 cameras, got {}", cams2.len()));
|
||||
}
|
||||
ok += 1;
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
Ok(ok)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::run_selftest;
|
||||
|
||||
#[test]
|
||||
fn rig_selftest_passes() {
|
||||
let ok = run_selftest().expect("self-test must pass");
|
||||
assert_eq!(ok, 6, "expected 6 check groups");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user