Fudge factor for reductions to prefer better reductions even if gzip estimation says otherwise

This commit is contained in:
Kornel Lesiński 2019-01-23 16:50:25 +00:00 committed by Kornel
parent 1f9ed834bd
commit c5959d3a53
3 changed files with 21 additions and 18 deletions

View file

@ -19,7 +19,7 @@ use std::thread;
/// Collect image versions and pick one that compresses best /// Collect image versions and pick one that compresses best
pub struct Evaluator { pub struct Evaluator {
/// images are sent to the thread for evaluation /// images are sent to the thread for evaluation
eval_send: Option<SyncSender<(Arc<PngImage>, bool)>>, eval_send: Option<SyncSender<(Arc<PngImage>, f32, bool)>>,
// the thread helps evaluate images asynchronously // the thread helps evaluate images asynchronously
eval_thread: thread::JoinHandle<Option<PngData>>, eval_thread: thread::JoinHandle<Option<PngData>>,
} }
@ -43,24 +43,26 @@ impl Evaluator {
/// Set baseline image. It will be used only to measure minimum compression level required /// Set baseline image. It will be used only to measure minimum compression level required
pub fn set_baseline(&self, image: Arc<PngImage>) { pub fn set_baseline(&self, image: Arc<PngImage>) {
self.try_image_inner(image, false) self.try_image_inner(image, 1.0, false)
} }
/// Check if the image is smaller than others /// Check if the image is smaller than others
pub fn try_image(&self, image: Arc<PngImage>) { /// Bias is a value in 0..=1 range. Compressed size is multiplied by
self.try_image_inner(image, true) /// this fraction when comparing to the best, so 0.95 allows 5% larger size.
pub fn try_image(&self, image: Arc<PngImage>, bias: f32) {
self.try_image_inner(image, bias, true)
} }
fn try_image_inner(&self, image: Arc<PngImage>, is_reduction: bool) { fn try_image_inner(&self, image: Arc<PngImage>, bias: f32, is_reduction: bool) {
self.eval_send.as_ref().expect("not finished yet").send((image, is_reduction)).expect("send") self.eval_send.as_ref().expect("not finished yet").send((image, bias, is_reduction)).expect("send")
} }
/// Main loop of evaluation thread /// Main loop of evaluation thread
fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, bool)>) -> Option<PngData> { fn evaluate_images(from_channel: Receiver<(Arc<PngImage>, f32, bool)>) -> Option<PngData> {
let best_candidate_size = AtomicMin::new(None); let best_candidate_size = AtomicMin::new(None);
let best_result: Mutex<Option<(PngData, _, _)>> = Mutex::new(None); let best_result: Mutex<Option<(PngData, _, _)>> = Mutex::new(None);
// ends when sender is dropped // ends when sender is dropped
for (nth, (image, is_reduction)) in from_channel.iter().enumerate() { for (nth, (image, bias, is_reduction)) in from_channel.iter().enumerate() {
#[cfg(feature = "parallel")] #[cfg(feature = "parallel")]
let filters_iter = STD_FILTERS.par_iter().with_max_len(1); let filters_iter = STD_FILTERS.par_iter().with_max_len(1);
#[cfg(not(feature = "parallel"))] #[cfg(not(feature = "parallel"))]
@ -75,15 +77,16 @@ impl Evaluator {
&best_candidate_size, &best_candidate_size,
) { ) {
let mut res = best_result.lock().unwrap(); let mut res = best_result.lock().unwrap();
if best_candidate_size.get().map_or(true, |best_len| { if best_candidate_size.get().map_or(true, |old_best_len| {
let new_len = (idat_data.len() as f64 * bias as f64) as usize;
// a tie-breaker is required to make evaluation deterministic // a tie-breaker is required to make evaluation deterministic
if let Some(res) = res.as_ref() { if let Some(res) = res.as_ref() {
// choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter // choose smallest compressed, or if compresses the same, smallest uncompressed, or cheaper filter
let old_img = &res.0.raw; let old_img = &res.0.raw;
let new = (idat_data.len(), image.data.len(), image.ihdr.bit_depth, f, nth); let new = (new_len, image.data.len(), image.ihdr.bit_depth, f, nth);
let old = (best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2); let old = (old_best_len, old_img.data.len(), old_img.ihdr.bit_depth, res.1, res.2);
new < old new < old
} else if best_len > idat_data.len() { } else if new_len < old_best_len {
true true
} else { } else {
false false

View file

@ -747,7 +747,7 @@ fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadlin
if let Some(interlacing) = opts.interlace { if let Some(interlacing) = opts.interlace {
if let Some(reduced) = png.change_interlacing(interlacing) { if let Some(reduced) = png.change_interlacing(interlacing) {
png = Arc::new(reduced); png = Arc::new(reduced);
eval.try_image(png.clone()); eval.try_image(png.clone(), 0.);
} }
if deadline.passed() { if deadline.passed() {
return; return;
@ -757,7 +757,7 @@ fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadlin
if opts.palette_reduction { if opts.palette_reduction {
if let Some(reduced) = reduced_palette(&png) { if let Some(reduced) = reduced_palette(&png) {
png = Arc::new(reduced); png = Arc::new(reduced);
eval.try_image(png.clone()); eval.try_image(png.clone(), 0.95);
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(&png); report_reduction(&png);
} }
@ -772,11 +772,11 @@ fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadlin
let previous = png.clone(); let previous = png.clone();
let bits = reduced.ihdr.bit_depth; let bits = reduced.ihdr.bit_depth;
png = Arc::new(reduced); png = Arc::new(reduced);
eval.try_image(png.clone()); eval.try_image(png.clone(), 1.0);
if (bits == BitDepth::One || bits == BitDepth::Two) && previous.ihdr.bit_depth != BitDepth::Four { if (bits == BitDepth::One || bits == BitDepth::Two) && previous.ihdr.bit_depth != BitDepth::Four {
// Also try 16-color mode for all lower bits images, since that may compress better // Also try 16-color mode for all lower bits images, since that may compress better
if let Some(reduced) = reduce_bit_depth(&previous, 4) { if let Some(reduced) = reduce_bit_depth(&previous, 4) {
eval.try_image(Arc::new(reduced)); eval.try_image(Arc::new(reduced), 0.98);
} }
} }
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
@ -791,7 +791,7 @@ fn perform_reductions(mut png: Arc<PngImage>, opts: &Options, deadline: &Deadlin
if opts.color_type_reduction { if opts.color_type_reduction {
if let Some(reduced) = reduce_color_type(&png) { if let Some(reduced) = reduce_color_type(&png) {
png = Arc::new(reduced); png = Arc::new(reduced);
eval.try_image(png.clone()); eval.try_image(png.clone(), 0.96);
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(&png); report_reduction(&png);
} }

View file

@ -19,7 +19,7 @@ pub fn try_alpha_reductions(png: Arc<PngImage>, alphas: &HashSet<AlphaOptim>, ev
let alphas_iter = alphas.iter(); let alphas_iter = alphas.iter();
alphas_iter alphas_iter
.filter_map(|&alpha| filtered_alpha_channel(&png, *alpha)) .filter_map(|&alpha| filtered_alpha_channel(&png, *alpha))
.for_each(|image| eval.try_image(Arc::new(image))); .for_each(|image| eval.try_image(Arc::new(image), 0.99));
} }
pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> { pub fn filtered_alpha_channel(png: &PngImage, optim: AlphaOptim) -> Option<PngImage> {