Fast evaluate filters

This commit is contained in:
Andrew 2022-11-18 08:57:39 +13:00 committed by Josh Holmer
parent 301b413dc5
commit 7a5df33027
3 changed files with 137 additions and 88 deletions

View file

@ -27,7 +27,8 @@ impl AtomicMin {
&self.val &self.val
} }
pub fn set_min(&self, new_val: usize) { /// Try a new value, returning true if it is the new minimum
self.val.fetch_min(new_val, SeqCst); pub fn set_min(&self, new_val: usize) -> bool {
new_val < self.val.fetch_min(new_val, SeqCst)
} }
} }

View file

@ -90,6 +90,11 @@ impl Evaluator {
self.try_image_inner(image, false) self.try_image_inner(image, false)
} }
/// Set best size, if known in advance
pub fn set_best_size(&self, size: usize) {
self.best_candidate_size.set_min(size);
}
/// Check if the image is smaller than others /// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) { pub fn try_image(&self, image: Arc<PngImage>) {
self.try_image_inner(image, true) self.try_image_inner(image, true)

View file

@ -192,12 +192,12 @@ pub struct Options {
/// ///
/// Default: `Libdeflater` /// Default: `Libdeflater`
pub deflate: Deflaters, pub deflate: Deflaters,
/// Whether to use heuristics to pick the best filter and compression /// Whether to use fast evaluation to pick the best filter
/// ///
/// Intended for use with `-o 1` from the CLI interface /// Intended for use with `-o 1` from the CLI interface
/// ///
/// Default: `false` /// Default: `false`
pub use_heuristics: bool, pub fast_evaluation: bool,
/// Maximum amount of time to spend on optimizations. /// Maximum amount of time to spend on optimizations.
/// Further potential optimizations are skipped if the timeout is exceeded. /// Further potential optimizations are skipped if the timeout is exceeded.
@ -234,7 +234,7 @@ impl Options {
if let Deflaters::Libdeflater { compression } = &mut self.deflate { if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 5; *compression = 5;
} }
self.use_heuristics = true; self.fast_evaluation = true;
self self
} }
@ -243,7 +243,7 @@ impl Options {
if let Deflaters::Libdeflater { compression } = &mut self.deflate { if let Deflaters::Libdeflater { compression } = &mut self.deflate {
*compression = 10; *compression = 10;
} }
self.use_heuristics = true; self.fast_evaluation = true;
self self
} }
@ -295,7 +295,7 @@ impl Default for Options {
idat_recoding: true, idat_recoding: true,
strip: Headers::None, strip: Headers::None,
deflate: Deflaters::Libdeflater { compression: 11 }, deflate: Deflaters::Libdeflater { compression: 11 },
use_heuristics: false, fast_evaluation: false,
timeout: None, timeout: None,
} }
} }
@ -446,6 +446,7 @@ struct TrialOptions {
pub filter: RowFilter, pub filter: RowFilter,
pub compression: u8, pub compression: u8,
} }
type TrialWithData = (TrialOptions, Vec<u8>);
/// Perform optimization on the input PNG object using the options provided /// Perform optimization on the input PNG object using the options provided
fn optimize_png( fn optimize_png(
@ -454,8 +455,6 @@ fn optimize_png(
opts: &Options, opts: &Options,
deadline: Arc<Deadline>, deadline: Arc<Deadline>,
) -> PngResult<Vec<u8>> { ) -> PngResult<Vec<u8>> {
type TrialWithData = (TrialOptions, Vec<u8>);
let original_png = png.clone(); let original_png = png.clone();
// Print png info // Print png info
@ -482,102 +481,106 @@ fn optimize_png(
info!(" IDAT size = {} bytes", idat_original_size); info!(" IDAT size = {} bytes", idat_original_size);
info!(" File size = {} bytes", file_original_size); info!(" File size = {} bytes", file_original_size);
let mut filter = opts.filter.clone();
if filter.is_empty() {
// Heuristically determine which set of options to use
if png.raw.ihdr.bit_depth.as_u8() >= 8
&& png.raw.ihdr.color_type != colors::ColorType::Indexed
{
filter.insert(RowFilter::MinSum);
} else {
filter.insert(RowFilter::None);
};
}
// Must use normal (lazy) compression, as faster ones (greedy) are not representative // Must use normal (lazy) compression, as faster ones (greedy) are not representative
let eval_compression = 5; let eval_compression = 5;
let eval_filters = indexset! {RowFilter::None, RowFilter::MinSum}; let eval_filters = indexset! {RowFilter::None, RowFilter::MinSum};
// This will collect all versions of images and pick one that compresses best // This will collect all versions of images and pick one that compresses best
let eval = Evaluator::new(deadline.clone(), eval_filters, eval_compression); let eval = Evaluator::new(deadline.clone(), eval_filters.clone(), eval_compression);
perform_reductions(png.raw.clone(), opts, &deadline, &eval); perform_reductions(png.raw.clone(), opts, &deadline, &eval);
let reduction_occurred = if let Some(result) = eval.get_best_candidate() { let (reduction_occurred, mut eval_filter) = if let Some(result) = eval.get_best_candidate() {
*png = result.image; *png = result.image;
result.is_reduction (result.is_reduction, Some(result.filter))
} else { } else {
false (false, None)
}; };
if opts.idat_recoding || reduction_occurred { if opts.idat_recoding || reduction_occurred {
// Go through selected permutations and determine the best let mut filters = opts.filter.clone();
let mut results: Vec<TrialOptions> = Vec::with_capacity(filter.len()); let fast_eval = opts.fast_evaluation && (filters.len() > 1 || eval_filter.is_some());
let best: Option<TrialWithData> = if fast_eval {
// Perform a fast evaluation of selected filters followed by a single main compression trial
if eval_filter.is_some() {
// Some filters have already been evaluated, we don't need to try them again
filters = filters.difference(&eval_filters).cloned().collect();
}
for f in &filter { if !filters.is_empty() {
results.push(TrialOptions { debug!("Evaluating: {} filters", filters.len());
filter: *f, let eval = Evaluator::new(deadline, filters, eval_compression);
if eval_filter.is_some() {
eval.set_best_size(png.idat_data.len());
}
eval.try_image(png.raw.clone());
if let Some(result) = eval.get_best_candidate() {
*png = result.image;
eval_filter = Some(result.filter);
}
}
let trial = TrialOptions {
filter: eval_filter.unwrap(),
compression: match opts.deflate { compression: match opts.deflate {
Deflaters::Libdeflater { compression } => compression, Deflaters::Libdeflater { compression } => compression,
_ => 0, _ => 0,
}, },
});
}
info!("Trying: {} combinations", results.len());
let original_len = original_png.idat_data.len();
let added_interlacing = opts.interlace == Some(1) && original_png.raw.ihdr.interlaced == 0;
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
let results_iter = results.into_par_iter().with_max_len(1);
let best = results_iter.filter_map(|trial| {
if deadline.passed() {
return None;
}
let filtered = &png.raw.filter_image(trial.filter);
let new_idat = match opts.deflate {
Deflaters::Libdeflater { .. } => {
deflate::deflate(filtered, trial.compression, &best_size)
}
#[cfg(feature = "zopfli")]
Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations),
}; };
if trial.compression <= eval_compression {
// No further compression required
if png.idat_data.len() < idat_original_size || opts.force {
Some((trial, png.idat_data.clone()))
} else {
None
}
} else {
info!("Trying: {}", trial.filter);
let original_len = idat_original_size;
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
perform_trial(&png.raw, opts, trial, &best_size)
}
} else {
// Perform full compression trials of selected filters and determine the best
if filters.is_empty() {
// Heuristically determine which filter to use
if png.raw.ihdr.bit_depth.as_u8() >= 8
&& png.raw.ihdr.color_type != colors::ColorType::Indexed
{
filters.insert(RowFilter::MinSum);
} else {
filters.insert(RowFilter::None);
}
}
let new_idat = match new_idat { let mut results: Vec<TrialOptions> = Vec::with_capacity(filters.len());
Ok(n) => n,
Err(PngError::DeflatedDataTooLong(max)) => { for f in &filters {
debug!( results.push(TrialOptions {
" zc = {} f = {} >{} bytes", filter: *f,
trial.compression, trial.filter, max, compression: match opts.deflate {
); Deflaters::Libdeflater { compression } => compression,
_ => 0,
},
});
}
info!("Trying: {} filters", results.len());
let original_len = idat_original_size;
let best_size = AtomicMin::new(if opts.force { None } else { Some(original_len) });
let results_iter = results.into_par_iter().with_max_len(1);
let best = results_iter.filter_map(|trial| {
if deadline.passed() {
return None; return None;
} }
Err(_) => return None, perform_trial(&png.raw, opts, trial, &best_size)
}; });
best.reduce_with(|i, j| {
// update best size across all threads if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
let new_size = new_idat.len(); i
best_size.set_min(new_size); } else {
j
debug!( }
" zc = {} f = {} {} bytes", })
trial.compression, };
trial.filter,
new_idat.len()
);
if new_size < original_len || added_interlacing || opts.force {
Some((trial, new_idat))
} else {
None
}
});
let best: Option<TrialWithData> = best.reduce_with(|i, j| {
if i.1.len() < j.1.len() || (i.1.len() == j.1.len() && i.0 < j.0) {
i
} else {
j
}
});
if let Some((opts, idat_data)) = best { if let Some((opts, idat_data)) = best {
png.idat_data = idat_data; png.idat_data = idat_data;
@ -588,10 +591,10 @@ fn optimize_png(
opts.filter, opts.filter,
png.idat_data.len() png.idat_data.len()
); );
} else { } else if eval_filter.is_some() {
*png = original_png; *png = original_png;
} }
} else if png.idat_data.len() > idat_original_size { } else if png.idat_data.len() >= idat_original_size {
*png = original_png; *png = original_png;
} }
@ -732,6 +735,46 @@ fn perform_reductions(
} }
} }
/// Execute a compression trial
fn perform_trial(
png: &PngImage,
opts: &Options,
trial: TrialOptions,
best_size: &AtomicMin,
) -> Option<TrialWithData> {
let filtered = &png.filter_image(trial.filter);
let new_idat = match opts.deflate {
Deflaters::Libdeflater { .. } => deflate::deflate(filtered, trial.compression, best_size),
#[cfg(feature = "zopfli")]
Deflaters::Zopfli { iterations } => deflate::zopfli_deflate(filtered, iterations),
};
// update best size or convert to error if not smaller
let new_idat = match new_idat {
Ok(n) if !best_size.set_min(n.len()) => Err(PngError::DeflatedDataTooLong(n.len())),
_ => new_idat,
};
match new_idat {
Ok(n) => {
let bytes = n.len();
debug!(
" zc = {} f = {} {} bytes",
trial.compression, trial.filter, bytes
);
Some((trial, n))
}
Err(PngError::DeflatedDataTooLong(bytes)) => {
debug!(
" zc = {} f = {} >{} bytes",
trial.compression, trial.filter, bytes,
);
None
}
Err(_) => None,
}
}
#[derive(Debug)] #[derive(Debug)]
struct DeadlineImp { struct DeadlineImp {
start: Instant, start: Instant,