init
This commit is contained in:
Executable
+8
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "panel-tuner"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
struct Config {
|
||||
target: PathBuf,
|
||||
panel: String,
|
||||
output_dir: PathBuf,
|
||||
gui_binary: PathBuf,
|
||||
/// Each parameter is a list of values to try.
|
||||
parameters: Vec<(String, Vec<f32>)>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: panel-tuner <config.json>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let config: Config = serde_json::from_reader(
|
||||
std::fs::File::open(&args[1]).expect("Failed to open config"),
|
||||
).expect("Failed to parse config");
|
||||
|
||||
std::fs::create_dir_all(&config.output_dir).ok();
|
||||
|
||||
let mut best: Option<(Vec<f32>, f64, PathBuf)> = None;
|
||||
let combinations = cartesian_product(&config.parameters.iter().map(|p| p.1.clone()).collect::<Vec<_>>());
|
||||
|
||||
for (idx, values) in combinations.iter().enumerate() {
|
||||
let out = config.output_dir.join(format!("capture_{}.png", idx));
|
||||
let mut cmd = Command::new(&config.gui_binary);
|
||||
for (name, value) in config.parameters.iter().map(|p| p.0.clone()).zip(values.iter()) {
|
||||
cmd.env(format!("HCIE_TUNER_{}", name.to_uppercase()), value.to_string());
|
||||
}
|
||||
cmd.arg("--screenshot-panel")
|
||||
.arg(&config.panel)
|
||||
.arg(&out);
|
||||
|
||||
let status = cmd.status().expect("Failed to run GUI binary");
|
||||
if !status.success() {
|
||||
eprintln!("Run {} failed: {:?}", idx, status);
|
||||
continue;
|
||||
}
|
||||
|
||||
let diff_out = config.output_dir.join(format!("diff_{}.png", idx));
|
||||
let mae = run_diff(&config.target, &out, &diff_out);
|
||||
eprintln!(
|
||||
"Run {}: params={:?} MAE={:.6}",
|
||||
idx, values, mae
|
||||
);
|
||||
if best.as_ref().map_or(true, |b| mae < b.1) {
|
||||
best = Some((values.clone(), mae, out.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((values, mae, out)) = best {
|
||||
println!("Best params: {:?}", values);
|
||||
println!("Best MAE: {:.6}", mae);
|
||||
println!("Best capture: {}", out.display());
|
||||
} else {
|
||||
eprintln!("No successful run");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn cartesian_product(lists: &[Vec<f32>]) -> Vec<Vec<f32>> {
|
||||
let mut result = vec![vec![]];
|
||||
for list in lists {
|
||||
let mut new_result = Vec::new();
|
||||
for item in list {
|
||||
for prefix in &result {
|
||||
let mut v = prefix.clone();
|
||||
v.push(*item);
|
||||
new_result.push(v);
|
||||
}
|
||||
}
|
||||
result = new_result;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn run_diff(target: &Path, actual: &Path, diff: &Path) -> f64 {
|
||||
let out = Command::new("target/debug/screenshot-diff")
|
||||
.arg(target)
|
||||
.arg(actual)
|
||||
.arg("--save-diff")
|
||||
.arg(diff)
|
||||
.output()
|
||||
.expect("Failed to run screenshot-diff");
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
stdout.trim().parse().expect("Invalid MAE output")
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "screenshot-diff"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
image = { workspace = true }
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: screenshot-diff <target.png> <actual.png> [--save-diff <diff.png>]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let target_path = PathBuf::from(&args[1]);
|
||||
let actual_path = PathBuf::from(&args[2]);
|
||||
let save_diff = args.iter().position(|a| a == "--save-diff").and_then(|i| args.get(i + 1)).map(PathBuf::from);
|
||||
|
||||
let target = image::open(&target_path).expect("Failed to open target").to_rgba8();
|
||||
let actual = image::open(&actual_path).expect("Failed to open actual").to_rgba8();
|
||||
|
||||
let (w, h) = (target.width().min(actual.width()), target.height().min(actual.height()));
|
||||
if w == 0 || h == 0 {
|
||||
eprintln!("One of the images has zero size");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut total_err: f64 = 0.0;
|
||||
let mut max_err: u32 = 0;
|
||||
let mut diff = image::RgbaImage::new(w, h);
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let t = target.get_pixel(x, y);
|
||||
let a = actual.get_pixel(x, y);
|
||||
let err = (0..4)
|
||||
.map(|c| (t[c] as i32 - a[c] as i32).unsigned_abs() as u32)
|
||||
.sum::<u32>();
|
||||
total_err += err as f64;
|
||||
max_err = max_err.max(err);
|
||||
let e = (err / 4).min(255) as u8;
|
||||
diff.put_pixel(x, y, image::Rgba([e, e, e, 255]));
|
||||
}
|
||||
}
|
||||
|
||||
let pixel_count = (w * h) as f64;
|
||||
let mae_per_pixel = total_err / (pixel_count * 4.0); // per channel per pixel
|
||||
let mae_total = total_err / pixel_count;
|
||||
|
||||
println!("{:.6}", mae_per_pixel);
|
||||
eprintln!("Resolution: {}x{}", w, h);
|
||||
eprintln!("MAE per channel per pixel: {:.6}", mae_per_pixel);
|
||||
eprintln!("MAE per pixel (all channels): {:.2}", mae_total);
|
||||
eprintln!("Max channel error: {}", max_err);
|
||||
|
||||
if let Some(diff_path) = save_diff {
|
||||
diff.save(&diff_path).expect("Failed to save diff image");
|
||||
eprintln!("Diff saved: {}", diff_path.display());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user