Efficient decoding 16 bits PNG into a matrix?

#1

Hi, I’m wondering how to efficiently decode a 16 bit gray PNG into nalgebra matrices. I currently have the following (unoptimized) solution, based on the png crate.

pub fn read_png_16bits<P: AsRef<Path>>(
    file_path: P,
) -> Result<(usize, usize, Vec<u16>), png::DecodingError> {
    // Load 16 bits PNG depth image.
    let img_file = File::open(file_path)?;
    let mut decoder = png::Decoder::new(img_file);
    // Use the IDENTITY transformation because by default
    // it will use STRIP_16 which only keep 8 bits.
    decoder.set(png::Transformations::IDENTITY);
    let (info, mut reader) = decoder.read_info()?;
    let mut buffer = vec![0; info.buffer_size()];
    reader.next_frame(&mut buffer)?;

    // Transform buffer into 16 bits Vec.
    let buffer_u16 = vec![0; (info.width * info.height) as usize];
    let mut buffer_cursor = Cursor::new(buffer);
    buffer_cursor.read_u16_into::<BigEndian>(&mut buffer_u16)?;

    // Return image size and u16 buffer.
    Ok((info.width as usize, info.height as usize, buffer_u16))
}

// Then the u16 buffer is transposed into nalgebra DMatrix (column major):
let (w, h, buffer) = read_png_16bits("icl-depth/0.png")?;
let depth_map = DMatrix::from_row_slice(h, w, buffer.as_slice());

I have a few questions about potential optimizations that I don’t know how to do:

  1. Is it possible to check system endianness, and in case it is big endian (like png spec), just define buffer_u16 as a cast of buffer into a Vec of half size? Otherwise (little endian) we must convert/copy anyway.

  2. In the otherwise case (little endian) can we just allocate the Vec somehow (with Vec::with_capacity()?) without setting its components to 0, and push into from buffer_cursor instead of initializing with vec![0; ...]?

#2

You can use #[cfg(target_endian = "big")], for example https://docs.rs/byteorder/1.3.1/src/byteorder/lib.rs.html#1864

you can use unsafe Vec::set_len, this is safe for trivial types like u16.

1 Like
#3

No, set_len is not safe to increase the size of a Vec beyond its capacity, as that would allow writing past the Vec’s allocation, which is ub.

1 Like
#4

Yes, you can use Vec:: with capacity to allocate space, then push into it to fill it up.

#5

I obviously mean code like this:

let n = 17;
let mut v = Vec::<u16>::with_capacity(n);
unsafe { v.set_len(n) };

to prevent memory initialization, if the next step will be give some C library this chunk of memory for filling.

#6

That was not at all clear from your reply, it sounded like you meant to use set_len as an alternative to with_capacity.

#7

I would wait on set_len until after you’ve passed the raw pointer to C and gotten the indication that it has been initialized.

1 Like
#8

Thanks a lot for the answers.

Turns out I’m not going to gain any performance however ^^. The copy saving in case of big endian system is likely not happening (at least not on my machine) since most computers use little endian natively it seems. The unsafe Vec::set_len following a with_capacity also works. However, after benchmarking the read_png_16bits function with Criterion, it does not seem to have any significant impact (less than 2% improvement on function speed).

Thanks for the help anyway!