init
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
/// Grid-search Bevel & Emboss MAE tuning against Krita ground-truth PNGs.
|
||||
///
|
||||
/// Expects `_tmp/bevel_emboss_png_kra_set/` with transparent PNGs exported
|
||||
/// from `_tmp/bevel_emboss_kra_set/`.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
const GT_DIR: &str = "_tmp/bevel_emboss_png_kra_set";
|
||||
static mut CANVAS: u32 = 800;
|
||||
const RESULTS_FILE: &str = "bevel_emboss_kra_grid_results.txt";
|
||||
|
||||
struct Sample {
|
||||
_name: String,
|
||||
depth: f32,
|
||||
size: f32,
|
||||
soften: f32,
|
||||
angle: f32,
|
||||
direction: String,
|
||||
ref_pixels: Vec<u8>,
|
||||
content_mask: Vec<bool>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let entries: Vec<_> = std::fs::read_dir(GT_DIR)
|
||||
.unwrap_or_else(|e| panic!("cannot read {}: {}", GT_DIR, e))
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
let s = e.file_name();
|
||||
let s = s.to_string_lossy();
|
||||
s.starts_with("be_") && s.ends_with(".png")
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("Found {} PNGs in {}", entries.len(), GT_DIR);
|
||||
|
||||
let mut samples: Vec<Sample> = Vec::new();
|
||||
|
||||
for entry in &entries {
|
||||
let path = entry.path();
|
||||
let stem = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
|
||||
let params = match parse_filename(&stem) {
|
||||
Some(p) => p,
|
||||
None => { eprintln!(" SKIP: {}", stem); continue; }
|
||||
};
|
||||
|
||||
let (size, soften, depth, angle, direction) = params;
|
||||
|
||||
let img = match image::open(&path) {
|
||||
Ok(i) => i,
|
||||
Err(e) => { eprintln!(" SKIP {}: {}", stem, e); continue; }
|
||||
};
|
||||
|
||||
let rgba = img.to_rgba8();
|
||||
let (src_w, src_h) = rgba.dimensions();
|
||||
if src_w != src_h {
|
||||
eprintln!(" SKIP {}: non-square dimensions {}x{}", stem, src_w, src_h);
|
||||
continue;
|
||||
}
|
||||
unsafe {
|
||||
CANVAS = src_w;
|
||||
}
|
||||
let src_raw = rgba.as_raw();
|
||||
let mut cropped_raw = vec![0u8; unsafe { (CANVAS * CANVAS * 4) as usize }];
|
||||
cropped_raw.copy_from_slice(src_raw);
|
||||
|
||||
let npix = unsafe { (CANVAS * CANVAS) as usize };
|
||||
let mut content_mask = vec![false; npix];
|
||||
for i in 0..npix {
|
||||
content_mask[i] = cropped_raw[i * 4 + 3] > 0;
|
||||
}
|
||||
|
||||
samples.push(Sample {
|
||||
_name: stem,
|
||||
depth, size, soften, angle, direction,
|
||||
ref_pixels: cropped_raw,
|
||||
content_mask,
|
||||
});
|
||||
}
|
||||
|
||||
println!("Loaded {} samples", samples.len());
|
||||
if samples.is_empty() {
|
||||
eprintln!("No valid samples found.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let content_count = samples[0].content_mask.iter().filter(|&&b| b).count();
|
||||
println!("Content pixels per image: {} / {} ({:.1}%)",
|
||||
content_count, npix(), content_count as f64 / npix() as f64 * 100.0);
|
||||
|
||||
let rect_alpha: Vec<u8> = {
|
||||
let n = npix();
|
||||
let mut a = vec![0u8; n];
|
||||
for i in 0..n {
|
||||
a[i] = samples[0].ref_pixels[i * 4 + 3];
|
||||
}
|
||||
a
|
||||
};
|
||||
|
||||
let lighting_blurs: &[i32] = &[0, 1, 2, 3, 5];
|
||||
let hl_scales: &[f32] = &[0.3, 0.5, 0.75, 1.0, 1.5, 2.0];
|
||||
let sh_scales: &[f32] = &[0.3, 0.5, 0.75, 1.0, 1.5, 2.0];
|
||||
let profile_exponents: &[f32] = &[0.3, 0.5, 0.7, 1.0, 1.5, 2.0, 3.0];
|
||||
|
||||
let total_combos = profile_exponents.len() * lighting_blurs.len() * hl_scales.len() * sh_scales.len();
|
||||
println!("Grid: {} combos x {} samples = {} evals", total_combos, samples.len(), total_combos * samples.len());
|
||||
std::io::stdout().flush().ok();
|
||||
|
||||
let mut best_avg_mae = f64::MAX;
|
||||
let mut best_params = (0i32, 0.0f32, 0.0f32, 1.0f32);
|
||||
let mut eval_count = 0usize;
|
||||
|
||||
for &profile_exp in profile_exponents {
|
||||
for &lighting_blur in lighting_blurs {
|
||||
for &hl_scale in hl_scales {
|
||||
for &sh_scale in sh_scales {
|
||||
let mut total_mae = 0.0f64;
|
||||
|
||||
for s in &samples {
|
||||
let style_enum = hcie_fx::types::BevelStyle::InnerBevel;
|
||||
let dir_enum = match s.direction.as_str() {
|
||||
"down" => hcie_fx::types::Direction::Down,
|
||||
_ => hcie_fx::types::Direction::Up,
|
||||
};
|
||||
|
||||
let (highlight, shadow) = hcie_fx::tuned::generate_bevel_tuned(
|
||||
&rect_alpha, unsafe { CANVAS }, unsafe { CANVAS },
|
||||
s.depth, s.size, s.soften, s.angle, 30.0,
|
||||
&dir_enum, &hcie_fx::types::Technique::Smooth, &style_enum,
|
||||
lighting_blur, hl_scale, sh_scale, profile_exp,
|
||||
);
|
||||
|
||||
let px = npix();
|
||||
let mut out = vec![0u8; px * 4];
|
||||
|
||||
// Detect actual shape color from ground truth center pixel
|
||||
let c = unsafe { CANVAS };
|
||||
let center = ((c / 2) * c + (c / 2)) as usize;
|
||||
let shape_r = s.ref_pixels[center * 4];
|
||||
let shape_g = s.ref_pixels[center * 4 + 1];
|
||||
let shape_b = s.ref_pixels[center * 4 + 2];
|
||||
|
||||
for i in 0..px {
|
||||
if rect_alpha[i] > 0 {
|
||||
out[i * 4] = shape_r;
|
||||
out[i * 4 + 1] = shape_g;
|
||||
out[i * 4 + 2] = shape_b;
|
||||
out[i * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Composite highlight (screen) and shadow (multiply) over the shape
|
||||
for i in 0..px {
|
||||
let sa = highlight[i * 4 + 3];
|
||||
if sa > 0 {
|
||||
let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]];
|
||||
let src = [highlight[i * 4], highlight[i * 4 + 1], highlight[i * 4 + 2], sa];
|
||||
let blended = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Screen, 1.0);
|
||||
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
||||
}
|
||||
}
|
||||
for i in 0..px {
|
||||
let sa = shadow[i * 4 + 3];
|
||||
if sa > 0 {
|
||||
let dst = [out[i * 4], out[i * 4 + 1], out[i * 4 + 2], out[i * 4 + 3]];
|
||||
let src = [shadow[i * 4], shadow[i * 4 + 1], shadow[i * 4 + 2], sa];
|
||||
let blended = hcie_blend::blend_pixels(dst, src, hcie_blend::BlendMode::Multiply, 1.0);
|
||||
out[i * 4..i * 4 + 4].copy_from_slice(&blended);
|
||||
}
|
||||
}
|
||||
|
||||
let mut diff_sum = 0.0f64;
|
||||
let mut content_pixels = 0u64;
|
||||
for i in 0..px {
|
||||
if !s.content_mask[i] { continue; }
|
||||
content_pixels += 1;
|
||||
for c in 0..4 {
|
||||
diff_sum += (out[i * 4 + c] as i32 - s.ref_pixels[i * 4 + c] as i32).abs() as f64;
|
||||
}
|
||||
}
|
||||
if content_pixels > 0 {
|
||||
total_mae += diff_sum / content_pixels as f64 / 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
eval_count += 1;
|
||||
let avg_mae = total_mae / samples.len() as f64;
|
||||
|
||||
if avg_mae < best_avg_mae {
|
||||
best_avg_mae = avg_mae;
|
||||
best_params = (lighting_blur, hl_scale, sh_scale, profile_exp);
|
||||
println!("*** [{}/{}] NEW BEST avg MAE = {:.4} (lb={}, hl={:.2}, sh={:.2}, pe={:.2})",
|
||||
eval_count, total_combos, best_avg_mae, lighting_blur, hl_scale, sh_scale, profile_exp);
|
||||
std::io::stdout().flush().ok();
|
||||
} else if eval_count % 100 == 0 {
|
||||
println!(" [{}/{}] avg={:.4} best={:.4}", eval_count, total_combos, avg_mae, best_avg_mae);
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (lb, hl, sh, pe) = best_params;
|
||||
println!("\n=== RESULT ===");
|
||||
println!("Best avg MAE = {:.4}", best_avg_mae);
|
||||
println!(" lighting_blur = {}", lb);
|
||||
println!(" hl_scale = {:.2}", hl);
|
||||
println!(" sh_scale = {:.2}", sh);
|
||||
println!(" profile_exp = {:.2}", pe);
|
||||
|
||||
if let Ok(mut file) = std::fs::OpenOptions::new()
|
||||
.create(true).write(true).truncate(true).open(RESULTS_FILE)
|
||||
{
|
||||
let _ = writeln!(file,
|
||||
"lighting_blur={} hl_scale={:.2} sh_scale={:.2} profile_exp={:.2}\n\
|
||||
avg_mae={:.4} samples={} combos={}",
|
||||
lb, hl, sh, pe, best_avg_mae, samples.len(), total_combos);
|
||||
}
|
||||
}
|
||||
|
||||
fn npix() -> usize { unsafe { (CANVAS * CANVAS) as usize } }
|
||||
|
||||
fn parse_filename(name: &str) -> Option<(f32, f32, f32, f32, String)> {
|
||||
// be_s{size}_sf{soften}_d{depth}_a{angle}_{dir_name}
|
||||
let parts: Vec<&str> = name.split('_').collect();
|
||||
if parts.len() < 6 || parts[0] != "be" { return None; }
|
||||
let size = parts[1].strip_prefix('s')?.parse::<f32>().ok()?;
|
||||
let soften = parts[2].strip_prefix("sf")?.parse::<f32>().ok()?;
|
||||
let depth = parts[3].strip_prefix('d')?.parse::<f32>().ok()?;
|
||||
let angle = parts[4].strip_prefix('a')?.parse::<f32>().ok()?;
|
||||
let direction = parts[5].to_string();
|
||||
Some((size, soften, depth, angle, direction))
|
||||
}
|
||||
Reference in New Issue
Block a user