Improve best-filter-per-line heuristic used in filter mode 5

Fixes #16
This commit is contained in:
Joshua Holmer 2016-03-10 14:22:14 -05:00
parent 3f42eae01c
commit f1e65067f6
3 changed files with 182 additions and 232 deletions

View file

@ -1,5 +1,6 @@
**Version 0.2.1**
- Add rustdoc for public methods and structs
- Improve filter mode 5 heuristic ([#16](https://github.com/shssoichiro/oxipng/issues/16))
**Version 0.2.0**
- Fix program version that is displayed when running `oxipng -V`

View file

@ -272,7 +272,7 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
const PRESERVED_HEADERS: [&'static str; 9] = ["cHRM", "gAMA", "iCCP", "sBIT", "sRGB",
"bKGD", "hIST", "pHYs", "sPLT"];
let mut preserved = HashMap::new();
for (hdr, contents) in png.aux_headers.iter() {
for (hdr, contents) in &png.aux_headers {
if PRESERVED_HEADERS.contains(&hdr.as_ref()) {
preserved.insert(hdr.clone(), contents.clone());
}

View file

@ -396,95 +396,15 @@ impl PngData {
/// Reverse all filters applied on the image, returning an unfiltered IDAT bytestream
pub fn unfilter_image(&self) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(self.raw_data.len());
let tmp = self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel();
let bpp = ((tmp as f32) / 8f32).ceil() as usize;
let mut last_line: Vec<u8> = vec![];
let bpp = (((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel()) as f32) /
8f32)
.ceil() as usize;
let mut last_line: Vec<u8> = Vec::new();
for line in self.scan_lines() {
let unfiltered_line = unfilter_line(line.filter, bpp, &line.data, &last_line);
unfiltered.push(0);
match line.filter {
0 => {
unfiltered.extend_from_slice(&line.data);
}
1 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
match i.checked_sub(bpp) {
Some(x) => {
let b = data[x];
data.push(byte.wrapping_add(b))
}
None => data.push(*byte),
}
}
last_line = data.clone();
unfiltered.append(&mut data);
}
2 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
data.push(*byte);
} else {
data.push(byte.wrapping_add(last_line[i]));
};
}
last_line = data.clone();
unfiltered.append(&mut data);
}
3 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = data[x];
data.push(byte.wrapping_add(b >> 1))
}
None => data.push(*byte),
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = data[x];
data.push(byte.wrapping_add(
((b as u16 + last_line[i] as u16) >> 1) as u8
))
}
None => data.push(byte.wrapping_add(last_line[i] >> 1)),
};
};
}
last_line = data.clone();
unfiltered.append(&mut data);
}
4 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = data[x];
data.push(byte.wrapping_add(b))
}
None => data.push(*byte),
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = data[x];
data.push(byte.wrapping_add(paeth_predictor(b,
last_line[i],
last_line[x])))
}
None => data.push(byte.wrapping_add(last_line[i])),
};
};
}
last_line = data.clone();
unfiltered.append(&mut data);
}
_ => unreachable!(),
}
unfiltered.extend_from_slice(&unfiltered_line);
last_line = unfiltered_line;
}
unfiltered
}
@ -497,156 +417,38 @@ impl PngData {
/// 5: All (heuristically pick the best filter for each line)
pub fn filter_image(&self, filter: u8) -> Vec<u8> {
let mut filtered = Vec::with_capacity(self.raw_data.len());
let tmp = self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel();
let bpp = ((tmp as f32) / 8f32).ceil() as usize;
let mut last_line: Vec<u8> = vec![];
// We could try a different filter method for each line
// But that would be prohibitively slow and probably not provide much benefit
// So we just use one filter method for the whole image
let bpp = (((self.ihdr_data.bit_depth.as_u8() * self.channels_per_pixel()) as f32) /
8f32)
.ceil() as usize;
let mut last_line: Vec<u8> = Vec::new();
for line in self.scan_lines() {
if filter != 5 {
filtered.push(filter);
}
match filter {
0 => {
filtered.extend_from_slice(&line.data);
}
1 => {
for (i, byte) in line.data.iter().enumerate() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(line.data[x]),
None => *byte,
});
}
}
2 => {
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(*byte);
} else {
filtered.push(byte.wrapping_sub(last_line[i]));
};
}
}
3 => {
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(line.data[x] >> 1),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(
((line.data[x] as u16 + last_line[i] as u16) >> 1) as u8
),
None => byte.wrapping_sub(last_line[i] >> 1),
});
};
}
}
4 => {
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(line.data[x]),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(line.data[x],
last_line[i],
last_line[x]))
}
None => byte.wrapping_sub(last_line[i]),
});
};
}
0 | 1 | 2 | 3 | 4 => {
filtered.push(filter);
filtered.extend(filter_line(filter, bpp, &line.data, &last_line));
}
5 => {
// Heuristically guess best filter per line
// Really simple algorithm, maybe we could replace this with something better
// libpng's heuristic no longer exists so I can't reference it
// Yes I know this code is ugly, but I didn't want to mess with mutable
// references from a HashMap that return options
// FIXME: Regardless of that, this is not very memory efficient
// Someone who's better at Rust can clean this up if they want
let line_0 = line.data.clone();
let mut line_1 = Vec::with_capacity(line.data.len());
let mut line_2 = Vec::with_capacity(line.data.len());
let mut line_3 = Vec::with_capacity(line.data.len());
let mut line_4 = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
line_1.push(byte.wrapping_sub(line.data[x]));
line_2.push(*byte);
line_3.push(byte.wrapping_sub(line.data[x] >> 1));
line_4.push(byte.wrapping_sub(line.data[x]));
}
None => {
line_1.push(*byte);
line_2.push(*byte);
line_3.push(*byte);
line_4.push(*byte);
}
}
} else {
match i.checked_sub(bpp) {
Some(x) => {
line_1.push(byte.wrapping_sub(line.data[x]));
line_2.push(byte.wrapping_sub(last_line[i]));
line_3.push(byte.wrapping_sub(
((line.data[x] as u16 + last_line[i] as u16) >> 1) as u8)
);
line_4.push(byte.wrapping_sub(paeth_predictor(line.data[x],
last_line[i],
last_line[x])));
}
None => {
line_1.push(*byte);
line_2.push(byte.wrapping_sub(last_line[i]));
line_3.push(byte.wrapping_sub(last_line[i] >> 1));
line_4.push(byte.wrapping_sub(last_line[i]));
}
}
};
// Uses MSAD algorithm mentioned in libpng reference docs
// http://www.libpng.org/pub/png/book/chapter09.html
let mut trials: HashMap<u8, Vec<u8>> = HashMap::with_capacity(5);
for filter in 0..5 {
trials.insert(filter, filter_line(filter, bpp, &line.data, &last_line));
}
// Count the number of unique bytes and take the lowest
let mut uniq_0 = line_0.clone();
uniq_0.sort();
uniq_0.dedup();
let mut uniq_1 = line_1.clone();
uniq_1.sort();
uniq_1.dedup();
let mut uniq_2 = line_2.clone();
uniq_2.sort();
uniq_2.dedup();
let mut uniq_3 = line_3.clone();
uniq_3.sort();
uniq_3.dedup();
let mut uniq_4 = line_4.clone();
uniq_4.sort();
uniq_4.dedup();
let mut best: (u8, &[u8], usize) = (0, &line_0, uniq_0.len());
if uniq_1.len() < best.2 {
best = (1, &line_1, uniq_1.len());
}
if uniq_2.len() < best.2 {
best = (2, &line_2, uniq_2.len());
}
if uniq_3.len() < best.2 {
best = (3, &line_3, uniq_3.len());
}
if uniq_4.len() < best.2 {
best = (4, &line_4, uniq_4.len());
}
filtered.push(best.0);
filtered.extend_from_slice(best.1);
let (best_filter, best_line) = trials.iter()
.min_by_key(|x| {
x.1.iter().fold(0u64, |acc, &x| {
let signed: i16 = if x > 127 {
(x as i16) - 256
} else {
x as i16
};
acc + signed.abs() as u64
})
})
.unwrap();
filtered.push(*best_filter);
filtered.extend_from_slice(best_line);
}
_ => unreachable!(),
}
@ -803,6 +605,153 @@ impl PngData {
}
}
fn filter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
let mut filtered = Vec::with_capacity(data.len());
match filter {
0 => {
filtered.extend_from_slice(data);
}
1 => {
for (i, byte) in data.iter().enumerate() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(data[x]),
None => *byte,
});
}
}
2 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(*byte);
} else {
filtered.push(byte.wrapping_sub(last_line[i]));
};
}
}
3 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(data[x] >> 1),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(((data[x] as u16 + last_line[i] as u16) >> 1) as u8)
}
None => byte.wrapping_sub(last_line[i] >> 1),
});
};
}
}
4 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
filtered.push(match i.checked_sub(bpp) {
Some(x) => byte.wrapping_sub(data[x]),
None => *byte,
});
} else {
filtered.push(match i.checked_sub(bpp) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(data[x], last_line[i], last_line[x]))
}
None => byte.wrapping_sub(last_line[i]),
});
};
}
}
_ => unreachable!(),
}
filtered
}
fn unfilter_line(filter: u8, bpp: usize, data: &[u8], last_line: &[u8]) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(data.len());
match filter {
0 => {
unfiltered.extend_from_slice(data);
}
1 => {
for (i, byte) in data.iter().enumerate() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b));
}
None => {
unfiltered.push(*byte);
}
};
}
}
2 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
unfiltered.push(*byte);
} else {
unfiltered.push(byte.wrapping_add(last_line[i]));
};
}
}
3 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b >> 1));
}
None => {
unfiltered.push(*byte);
}
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(((b as u16 + last_line[i] as u16) >> 1) as u8));
}
None => {
unfiltered.push(byte.wrapping_add(last_line[i] >> 1));
}
};
};
}
}
4 => {
for (i, byte) in data.iter().enumerate() {
if last_line.is_empty() {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(b));
}
None => {
unfiltered.push(*byte);
}
};
} else {
match i.checked_sub(bpp) {
Some(x) => {
let b = unfiltered[x];
unfiltered.push(byte.wrapping_add(paeth_predictor(b,
last_line[i],
last_line[x])));
}
None => {
unfiltered.push(byte.wrapping_add(last_line[i]));
}
};
};
}
}
_ => unreachable!(),
}
unfiltered
}
fn reduce_bit_depth_8_or_less(png: &PngData) -> Option<(Vec<u8>, u8)> {
let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8);
let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize;