use blazediff::DiffOptions;
use blazediff_interpret::{interpret, interpret_diff, ChangeSource, InterpretResult};
use blazediff_shared::Image;
use blazediff_ssim::{
hitchhikers_ssim, ms_ssim, ssim, HitchhikersOptions, MsSsimOptions, Plane, Rgba8, SsimOptions,
};
use clap::Parser;
use std::path::Path;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(name = "blazediff-interpret")]
#[command(author = "Teimur Gasanov")]
#[command(about = "Describe what changed between two images, not just where")]
struct Args {
image1: String,
image2: String,
output: Option<String>,
#[arg(long, default_value = "pixel")]
source: String,
#[arg(short, long, default_value_t = 0.1)]
threshold: f64,
#[arg(long)]
antialiasing: bool,
#[arg(long, default_value_t = 0.99)]
region_floor: f64,
#[arg(long)]
json: bool,
}
fn run(args: &Args) -> Result<InterpretResult, String> {
let (image1, image2) = blazediff_shared::load_image_pair(&args.image1, &args.image2)
.map_err(|e| format!("Failed to load images: {e}"))?;
if args.source == "pixel" {
let options = DiffOptions {
threshold: args.threshold,
include_aa: !args.antialiasing,
..Default::default()
};
let mut output = args
.output
.as_ref()
.map(|_| Image::new_uninit(image1.width, image1.height));
let result = interpret_diff(&image1, &image2, output.as_mut(), &options)
.map_err(|e| e.to_string())?;
if let (Some(path), Some(image)) = (&args.output, &output) {
if result.diff_count > 0 {
blazediff_shared::save_image(image, Path::new(path), options.compression, 90)
.map_err(|e| format!("Failed to save {path}: {e}"))?;
}
}
return Ok(result);
}
let plane = |image: &Image| {
Plane::from_rgba8(Rgba8::new(
&image.data,
image.width as usize,
image.height as usize,
))
.map_err(|e| e.to_string())
};
let (plane1, plane2) = (plane(&image1)?, plane(&image2)?);
let shared = SsimOptions::default();
let outcome = match args.source.as_str() {
"ssim" => ssim(&plane1, &plane2, &shared),
"ms-ssim" => ms_ssim(&plane1, &plane2, &shared, &MsSsimOptions::default()),
"hitchhikers-ssim" => {
hitchhikers_ssim(&plane1, &plane2, &shared, &HitchhikersOptions::default())
}
other => {
return Err(format!(
"Unknown source '{other}'. Expected one of: pixel, ssim, ms-ssim, hitchhikers-ssim"
))
}
}
.map_err(|e| e.to_string())?;
interpret(
&image1,
&image2,
ChangeSource::Ssim {
outcome: &outcome,
floor: args.region_floor as f32,
},
)
.map_err(|e| e.to_string())
}
fn main() -> ExitCode {
let args = Args::parse();
let result = match run(&args) {
Ok(result) => result,
Err(message) => {
eprintln!("{message}");
return ExitCode::from(2);
}
};
if args.json {
match serde_json::to_string_pretty(&result) {
Ok(json) => println!("{json}"),
Err(e) => {
eprintln!("Failed to serialize: {e}");
return ExitCode::from(2);
}
}
} else {
println!("{}", result.summary);
for region in &result.regions {
println!(
" {:?} {:?} at {} ({}px, {:.2}%)",
region.change_type,
region.shape,
region.position,
region.pixel_count,
region.percentage
);
}
}
if result.total_regions == 0 {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}