Skip to content

Add the ability to scale down images during decode (IDCT scaling) #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 7, 2019

Conversation

kevinmehall
Copy link
Contributor

When loading a large image to generate a small thumbnail, jpeg-decoder currently requires allocating a large buffer and decoding the full image. It's possible to use a smaller IDCT to directly decode a JPEG image at a fraction of the full size, saving decode time and memory. This adds IDCT implementations to handle 1/8, 1/4, and 1/2 size decoding.

The interface is a new Decoder constructor that accepts a requested image size. The implementation rounds this up to the nearest scale for which an IDCT implementation exists. This seems nicer than libjpeg's scaling API, which represents the size as a fraction and requires the user to know which scale factors are available.

Currently only adds 1/8 scale because the 1x1 IDCT is trivial, but this
adds the infrastructure to easily support the others.
The reference images were generated with djpeg:
djpeg -scale 1/8 tests/reftest/images/rgb.jpg | convert -
tests/reftest/images/rgb_63x42.png
djpeg -scale 2/8 tests/reftest/images/rgb.jpg | convert -
tests/reftest/images/rgb_125x84.png
djpeg -scale 4/8 tests/reftest/images/rgb.jpg | convert -
tests/reftest/images/rgb_250x167.png
@lovasoa
Copy link
Contributor

lovasoa commented Nov 21, 2019

👍 on this, this is a great feature to have !
Can someone review this ? @fintelia maybe ?

@kevinmehall
Copy link
Contributor Author

Failing tests are due to the use of the use crate:: syntax not available in 1.28, but all PRs seem to be failing on 1.28 because of dependencies updating to the 2018 edition. I've opened #118 to increase the minimum Rust version to 1.34.

@fintelia
Copy link
Contributor

I like the idea of exposing this functionality.

Would it be possible to control the scale factor via a separate modifier function on Decoder rather than making a new constructor? That way the user would be able to see the metadata of the image before deciding whether to try rescaling, and would also be compatible with adding other similar sorts of functionality without an exponential blowup in the number of constructors required.

I'm also not completely sold on taking minimum output dimensions as a argument. If the only possible downscale factors are 2, 4 and 8, why can't we just have the user pass one of those directly? Another point is that I'd expect users would often either want to decode to exactly the target size or to something substantially larger so that they can get a nicer downsampled result?

@kevinmehall
Copy link
Contributor Author

Would it be possible to control the scale factor via a separate modifier function on Decoder rather than making a new constructor?

Sure, I'll try that. It will take some refactoring of parser.rs because right now it computes values while parsing the headers that would need to be recomputed after changing the scale factor.

If the only possible downscale factors are 2, 4 and 8, why can't we just have the user pass one of those directly?

It's possible to add other scale factors N/8 by adding a NxN IDCT. libjpeg supports all values of N between 1 and 16. It accepts a fraction N/M, but rounds to the next supported scale factor. Do you think that interface is preferable? Since the goal is usually a particular size thumbnail rather than a fraction of the original size, I imagine most users of that interface would implement the same math I added here to compute that scale factor, except they'd either have to hard-code the supported scale factors, or we'd have to add another API to list them. If a library user would like to perform more of the scaling with an external resampling algorithm, they can always double the requested size or similar.

@lovasoa
Copy link
Contributor

lovasoa commented Nov 22, 2019

I also think the interface that exposes Dimensions will both be easier to use and less likely to require a breaking change in the future.

src/decoder.rs Outdated
Comment on lines 77 to 80
/// Creates a new `Decoder` using the reader `reader` that returns a
/// scaled image that is equal to or larger than the requested size in at
/// least one axis, or the full size of the image if the requested size is
/// larger.
Copy link
Contributor

@fintelia fintelia Nov 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this description says we can always just return the original image size and only hints that it sometimes won't. Perhaps it could instead say something along the lines of "scales by the smallest supported scale factor that produces an image larger or equal to (min_width, min_height) if possible. Otherwise scales by the largest supported factor".

@fintelia
Copy link
Contributor

It's possible to add other scale factors N/8 by adding a NxN IDCT. libjpeg supports all values of N between 1 and 16.

This is the piece I was missing, your design is cleaner if a bunch more scale factors could be added.

One more question I have is the "rounding mode". The current strategy is to always round up to a large image size when picking between two scale factors. Other options would be round towards closest and round towards the original size (so decreasing the size rounds up, but increasing image size rounds down). The current choice seems reasonable to me, but I want to double check there isn't a reason we might later want to prefer a different strategy

@kevinmehall
Copy link
Contributor Author

I chose to round up for the use case you mentioned previously: you want the IDCT to scale to larger than the desired size, then follow it with a resample to the final size.

If we were to later add a N>8 IDCT for upscaling, I think the desired behavior would be to round towards the original image size. That is, round up when downscaling, and round down when upscaling. Since this does not add upscaling support, the difference would only be how it is documented. (You could see the "or the full size of the image if the requested size is larger" as the degenerate case of this. Since there is no N>8 IDCT, a larger size always rounds down to 8/8 scaling).

Copy link
Contributor

@fintelia fintelia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you could update the doc comments to say how the scaling factor selection works, I think this would be ready to merge

@kevinmehall
Copy link
Contributor Author

Updated to make the API be decoder.scale(requested_width, requested_height) instead of Decoder::scaled(requested_width, requested_height), with clearer documentation on scaling factor selection.

@kevinmehall
Copy link
Contributor Author

@fintelia Is there anything else you'd like changed before merging? We're using this in production and have run about 100k photos through it so far.

@fintelia fintelia merged commit 5607279 into image-rs:master Dec 7, 2019
buffer[y * component.size.width as usize + x] = data[0][y * line_stride + x];
for y in 0 .. width {
for x in 0 .. height {
buffer[y * width + x] = data[0][y * line_stride + x];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#125 seems to have been introduced by this line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I swapped width and height in the for loop ranges when extracting those expressions as variables. 🤦‍♂

Also, this presumably means there are no single-channel JPEG images in the test suite?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I missed that ! I fixed that too in #126

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite could clearly be more extensive !

let height = component.size.height as usize;

let mut buffer = vec![0u8; width * height];
let line_stride = width * component.dct_scale;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was inadvertently changed from component.block_size.width to component.size.width, causing #125

wartmanm pushed a commit to wartmanm/jpeg-decoder that referenced this pull request Oct 4, 2021
Add the ability to scale down images during decode (IDCT scaling)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants