-
-
Notifications
You must be signed in to change notification settings - Fork 700
cgifsave file size larger than expected for some gifs #2576
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
Comments
Thanks!
The extra colour tables are of course extra data in the GIF. Would be interesting to know whether the number of colours actually increases from |
Hi @jmreid, thanks for these interesting examples. Yes, resizing will make new in-between colours, since it effectively averages pixels. Something we discussed a few months ago is getting the GIF decoder to attach the colour tables from the source image. The encoder could then just reuse that rather than finding a new table -- at least the result would be no worse. The difficulty comes if processing is more than a simple resize. For example, a watermark might be drawn on a few frames. In this case, you probably would want to recompute the colour tables. How about:
Does that sound enough? I've probably missed something. We'd need two new functions: something to compute maximum encoding error given an RGB image and a colour table, and something to find the maximum inter-colour distance in a table. |
Another possibility would be to add a GIF save option for The change would be:
On the command line, it'd be something like:
That sounds really simple and safe, and not much extra work for the application level. There would be no change for any of the bindings either. Is that better? @kleisauke, do you have any thoughts on this? |
I like the Perhaps it would make sense to overload |
I thought about doing in libvips, but it would add a lot of special cases everywhere and extra code we'd have to maintain. The application level already knows if tables can be reused, so it's a lot simpler to just get them to pass the information down to the writer. |
I'd forgotten about the INDEXED idea. Yes, that's another possibility, though I think it wouldn't help with resize, since images would need to be converted back to RGB for that. We could implement both: INDEXED would make things like flip, crop, rot90 automatically reuse the colour table, and |
I was bored and did a quick table reuse prototype: https://github.com/libvips/libvips/tree/add-gif-tables But it doesn't work! It seems libimagequant has no API to reuse an old colour table. there's I think we'd need to get changes upstream. |
How about doing a low-pass filter before looking for similar pixels? That would remove the slight noise and let you find similar areas, not just similar points. |
I did another quick hack adding set-palette to @lovell's libimagequant: https://github.com/jcupitt/libimagequant/tree/add-set-palette I now see:
ie. the bad behaviour you saw before, but with
Much saner. The colours look a bit messed up though, I'm not sure why. No doubt I've done something dumb. edit perhaps quantizr would be a better place to add API, since it's not a fork? |
... I had a quick look at INDEXED mode, but that would need libnsgif changes -- the current API always gives you a fully decoded RGBA frame. |
Thanks for the suggestion, it's a good idea to remove the noise when doing the transparency optimization. Maybe there is a bit of a misunderstanding in this issue: |
Sure. I was thinking the same thing -- really you need to take control of the whole process (palette selection, dither, encode) if you want to optimise it significantly. Perhaps there's a possible collaboration with @DarthSim (the very skilled quantizr person)? |
Hey everyone! I think the core problem here is the usage of local color tables while the originals use only a global one. And here're two parts of the problem I see:
It increases the quality though. So I think what we need here is a compromise. If the original uses the global palette only, the result should use only the global palette, too. However, as an author of an app, I'd like to have control over this behavior. Ideally, the decoder should save the info about global/local palettes usage and their sizes so an app could decide how the encoder should behave. Since animated images are represented as a column of frames inside of Vips, we can generate a single palette for all the frames in one pass. The problem here is that both LIQ and Quantizr change the palette while remapping the image. This most probably will lead to the color changes in all frames but the last one. Not good. Alternatively, in the case of using only the global palette, we could remap all the frames in one pass. This will remove the palette change issue but will all an unwanted dithering on the top edges of frames. To fix this, I can add an option like I don't think the palette reusage is a good option. It's good in terms of speed, but if the image was resized, blurred, sharpened, color-adjusted, etc, the original's palette won't fit the image anymore. Anyway, I'm going to spend some time on experiments on it on holidays. I'll let you know if I achieve something. |
The problem for libvips is that global palette optimisation needs a lot of memory and prevents streaming, which (in turn) limits parellelism, We'd really like a GIF saver which can work a frame at a time. That's the motivation behind the use of local palettes: make one for the first frame, and switch to local palettes if encoding error goes over some threshold (eg. perhaps there's a cut in the video). The current change detector is really bad (just the sum of index pixels), so replacing that would be a simple improvement, Palette reuse can work well. For example, for resize, colours will mostly be the average of existing colours, so peak encode error is constrained. Imagine the original palette as 256 points scattered in a 256 x 256 x 256 RGB cube, and imagine a sphere at each point. Now expand each sphere equally until it just touches the nearest sphere. The radius of the largest sphere is the peak encode error on resize, blur, rotate, etc. |
I agree that replacing the changes detector may improve things. However, this new detector should be really good. For example, if some new colors appeared in a new frame, the detector should notice this, otherwise, we're risking losing ones. I doubt it's possible to generate a good palette for multiple frames without having all these frames' data. On the other hand, having this good changes detector that detects small yet noticeable changes, we will get redundant local palettes and less effective frames optimizations. Palette reuse can work in case we don't change the colors of the image. If we apply a watermark, extend an image with some color outside of the palette, or apply color adjustments, the original palette will simply become invalid. |
How about a function like libvips could measure the quant error for the first frame, then for subsequent frames, generate a new palette if |
It sounds like remapping without actual remapping :( If I got your idea right, we'll need to find the best color from the palette for each pixel and get the error (actual_color - palette_color). This is basically what happens during the remapping process. And this is not fast at all :( |
My brother and I had another look at the resizing problem. We made a branch gifresize where we do a very simple GIF-to-GIF resizing which avoids the size explosion and makes a reasonable resize. Regarding image quality, the resizing is not ideal yet as no color smoothing is done. But it's hopefully a first step. For the proof-of-principle purpose, we used gifdec as the decoder (included as a git submodule). As libvips uses libnsgif, we will switch. Important is that this decoder outputs also just RGB data. But knowing that it came from a GIF, we can reconstruct the color table and color indices. Since we just take RGB data as the input to the encoder, we used for simplicity only local color tables, but no size explosion happens. So local tables are not the main issue here, what matters is that identical areas of subsequent frames stay identical -- then the GIF transparency optimization in combination with LZW leads to really good compression. Our resize tool works but it is more a draft right now. We can include further functionalities like this simple resize in cgif_tools and extend cgif to more than a bare encoder. With your help (@jcupitt, @DarthSim) we can make the quality better as well (color smoothing/quantization, etc.). Results:
|
I agree, local palettes themselves are not a big problem. The problem is that the colors of a local palette most probably will differ from the colors of the global one so will indexes. I'm not sure how exactly frames optimizations are implemented in CGIG (didn't have a chance to check), but I bet this will lead to what we see in #2576 (comment). |
Hmm then perhaps the remapper could compute peak error? Perhaps a public field in the struct has the peak error number after the remapper finishes. Then after calling remap, libvips could decide whether to reoptimise the palette (and call the remapper again for that frame). |
I rewrote the code of I used the G-Shock gif for the test and set the result's bitsize to 6 as in the original image: Current implementation:
My implementation:
Well, this may do the job. LIQ calculates MSE, but not at high speeds. I can add the same to Quantizr. |
Is it safe to assume that Quantizr will be the quantizer of choice in libvips? If we're working to add these improvements there, then LIQ will end up being the inferior package to use. Which is fine by me! We're happy to use Quantizr if it becomes the golden path for the best vips experience. |
I noticed that you could also use the So instead of quantizing all frames in one pass (which effectively reverts PR #2445), or generating a palette for each frame (current behavior), you can just call I had a quick attempt to implement this with commit kleisauke@86e499b and I see (with the second example image): $ /usr/bin/time -f %M:%e vipsthumbnail test.gif[n=-1] --size 1400 -o x.gif
105504:11.86
$ du -h x.gif
24M x.gif $ /usr/bin/time -f %M:%e vipsthumbnail test.gif[n=-1] --size 1400 -o x.gif
821900:6.56
$ du -h x.gif
660K x.gif So, much higher memory usage, but faster and a better file size. |
The results look good! Unfortunately, I see a few problems here:
|
Agreed, it's GPL-licensed so we should not spend much time on it. But it's worth investigating this API to add something similar to Quantizr. I had another idea, instead of keeping those $ /usr/bin/time -f %M:%e vipsthumbnail test.gif[n=-1] --size 1400 -o x.gif
124264:8.90
$ du -h x.gif
660K x.gif So, the memory usage is more or less the same, but the execution time is faster and the file size is much better. |
Nice! Had the same idea about two I implemented histogram API in Quantizr and tried your code - works great! |
Is there any progress on this? Is there any way I can help? Having this improved in libvips would be very beneficial |
There's a fix to the GIF change detector discussed at #2622 might help a little bit here, as it should reduce the number of frame local palettes for a typical GIF. |
Thanks a lot @dloebl for your effort! @joshuamsager and I built both libvips and cgif from your branches to see what's the impact of this fix on GIF sizes with real GIFs uploaded by our users that are resized by us. We detailed our analysis process below. Please let us know if you need more details around anything. We're also happy to help testing / comparing multiple configuration options with real world GIFs (i.e. testing multiple "quality" settings, to see how it impacts file-size and quality on real world images). Impact on previously mentioned problematic GIFsThis table shows the file size after resizing the original image with libvips
We're seeing an outstanding improvement for the last image and a significant improvement on the first image (the resized file size is still more than twice the original). The quality of the resized images also seems fine. For example, this is a comparison between the original image and the one resized to a 1400 px wide GIF with the version of libvips that includes the file size fixes: Holistic impactWe compared the file size of thousands of resized GIFs with libvips 8.12.1, libvips with #2628 and gifsicle. The results are encouraging, but we're still observing that GIFs tends to be significantly bigger w/ libvips. For instance, without the libvips fix, 3.25% of all resized gifs were at least 5 times bigger than the gifsicle equivalent. With the fix, that went down to ~1%. About 28% of the resized GIFs are at least 1.5 times bigger than gifsicle, and this didn't change significantly with the fix. Only ~2% of the resized GIFs are more than 10% smaller than gifsicle. Sample GIFsTo help further debugging, here are some additional problematic GIFs. All of those images were resized with the following commands:
|
Thanks @maleblond and @joshuamsager for this detailed analysis! In my tests, this method reduces the file size while keeping the high-quality resize of libvips. Your feedback on quality/file size would be very helpful. Can you also see an improvement with your data set? Just keep in mind that the method is lossy, so one should not increase the Furthermore: |
Hi @dloebl, thanks a lot for your work on this ! I took a deep look at our image size + quality metrics and there is no doubt your work has made a huge impact. We plan on setting Do you have a rough idea of when your PRs could make it into the next libvips / cgif releases? Is there any way we can help? Detailed analysisImage size
As expected, the higher The image quality metrics we use are SSIM and DeltaE . It basically tell us how different the output GIF is different from the original ones, from a scale of 0 to 100 (0 being identical, 100 being opposite). Even if those metrics are not really meant to be used for animated images / videos, we were able to observe that increasing Delta E, lower is better
The metrics for SSIM are similar. We also did side by side comparison of selected GIFs. We observed that our old image processing server (gifsicle) was serving GIFs that were lower quality than when using libvips with |
These results look great!
From the libvips POV, we run on roughly a six-month cycle, so the next version (8.13) is due in perhaps May or early June. |
@jcupitt Got it, thanks! Is there any change it's considered a "bug fix" and it's included in the next patch release (which appears to be done more frequently)? Definitely not a big deal; we can fallback to building from source if we want to @dloebl I noticed #2628 is in "draft mode"; is the latest commit production-ready as is? You mentioned adding support for lossy compression for GIFs w/ active alpha channel, but we were wondering if you were planning to do it in the same PR or a follow-up one? From our POV, we'd be good using your PR as is for now, since we saw really good improvements even without the active alpha channel support 😄 |
This feels like a large change, so I think it's better to keep it for 8.13. I'd build from master (as you say). |
@maleblond Thanks a lot for these systematic benchmarks! I'm very happy to see that the improvement is so significant. The initial value of Regarding the next cgif release: |
I finally had some time to incorporate that, see commit kleisauke@e0be9a6. This avoids having the libimagequant symbol aliases in Quantizr. This implementation also removes the 2000x2000 pixel limit, which helps kleisauke/net-vips#157. Here's the file size comparison table from above with the extra added column built from this commit. Details
(Tested on Fedora 35 with libimagequant v2.15.1 and cgif v0.2.0) |
I've just tweaked the It's not the best way to do this. We are testing if the input image image changes when we want to know is "is the current palette good enough?" How about:
Then libvips GIF write would do this:
So we now only recompute the palette if the rendering error gets bad, not every time the image changes. It should make smaller images for video clips (a very common case). |
Note that we could get rid of the internal change detector if we switch back to global palettes. The histogram API of Quantizr and libimagequant to generate a global palette in a separate I've just rebased this branch, see: I now see: $ vipsthumbnail test1.gif[n=-1] -s 200x -o tn_%s.gif
$ vipsthumbnail test2.gif[n=-1] -s 1400x -o tn_%s.gif
$ vipsthumbnail test3.gif[n=-1] -s 200x -o tn_%s.gif
$ vipsthumbnail test4.gif[n=-1] -s 200x -o tn_%s.gif
$ vipsthumbnail test5.gif[n=-1] -s 1024x1024 -o tn_%s.gif
$ du -h --apparent-size tn_*.gif
2.1M tn_test1.gif
734K tn_test2.gif
1.4M tn_test3.gif
4.5M tn_test4.gif
18M tn_test5.gif It's surprisingly fast without having to compromise on memory usage and file size (as noted in #2576 (comment)). Looking at the commits that added the histogram API to libimagequant (commit ImageOptim/libimagequant@bdb17d2, ImageOptim/libimagequant@1b858a3, ImageOptim/libimagequant@cfc15ec and ImageOptim/libimagequant@e5f45cb), it looks like it's just making some internal features publicly available. I think this is doable in Lovell's BSD-2-Clause licensed fork without having to look at those commits (clean room design), but I'm not a lawyer. Another option is to migrate the pre-built binaries to using Quantizr. However, that would imply that the |
That's true, but you can't make two passes if the upstream image is sequential, like the output of I tried to make a list of the strategies that have been suggested:
By far the most common case must be GIF -> GIF, so maybe 4. is the best strategy? Plus an option to generate a global palette with 1., or a local palette with 3. |
I thought running two passes over a sequential image was fine (modifying the test suite to test sequential access in libvips/libvips/foreign/vipspng.c Lines 1035 to 1043 in 5f7c12e
I think this way (i.e. without doing
Thanks for listing the pros and cons! Global palette optimization (1.) was faster in my tests, without affecting memory usage, than its current behaviour with local palettes (2.). However, I only tested it with the GIF images mentioned in this issue. You're right about the lower image quality when generating global palettes, especially when there are more than 256 colours in the image. Gifsicle resolves this neatly by generating global colour palettes by default and switching back to local palettes when it discovers that there are more than 256 colours in the image. Reusing encoding palette (4.) sounds like an interesting strategy that would accelerate GIF to GIF processing, which is the most common case. I'm not sure if we should support GIF images with local palettes in that case, since it requires adding each palette of the frame as a separate metadata item. |
Using the 3rd approach may be better than using the 2nd in terms of a better trigger to generate a new palette. Yet I see come cons here:
Here's what I'd offer:
Pros:
Cons:
|
And yeah, I still think that the global palette approach is the best one. GIF -> GIF conversion is the most common case, and most GIFs have global palettes. So I think that by trying to generate the best palette we are trying to solve a VERY rare case. |
I was looking at hacking up a test implementation for (4) and realized that libnsgif (our GIF loader) can't tell us if an image has local colour tables, so that's blocked, sigh. I'll ask the libnsgif maintainers if they'd be willing to add this feature (it should be simple, I think). |
... libnsgif are adding this feature. Once it's done, I'll hack up a quick test of strategy (4) for us to evaluate. Slight tangent: how about end of May for a release of libvips 8.13? It'd be good if we could resolve this issue by then. |
... if there are no local colour tables. See #2576
Here's a libvips branch which attaches the input GIF palette, if possible: https://github.com/libvips/libvips/tree/gif-palette Eg.:
Each int is actually a int32 of RGBA bytes, hence the -ves. Would anyone like to try getting GIF save to use these palettes if they are available? |
* save GIF palette as metadata ... if there are no local colour tables. See #2576 * cgifsave: reuse global palette, if possible * add reuse_palette parameter * add reoptimise parameter and reuse palette by default * attach global palette even if local palettes are present * add check for presence of use-lct * Revert "add check for presence of use-lct" This reverts commit cd0f14e. * Revert "attach global palette even if local palettes are present" This reverts commit 4085b9e. * move global palette quantization to cgif_build * rename member variable gct * update comments * improve error handling * update documentation Co-authored-by: John Cupitt <jcupitt@gmail.com>
Master now has GIF palette reuse! Good job everyone.
There's no option to generate a new global palette on write, perhaps there should be. This will be in 8.13. |
... let's make a new issue for any further improvements to GIF handling, this one has become very large, |
Uh oh!
There was an error while loading. Please reload this page.
I have a few example gifs that seem to really increase in filesize when run through libvips. This doesn't seem to be the case for all gifs, but i haven't been able to figure out why it's seemingly much worse for some files.
Given this example input gif (~897 KB):

vipsthumbnail
(using cgif) returns a 3.5MB image:vipsthumbnail "example.gif[n=-1]" --size 200 -o example_vips_out.gif
Gifsicle returns a less nicer-looking image, but it's ~1MB:

gifsicle --resize-fit 200x_ -o example_gifsicle_out.gif example.gif
Here's another example I found online:

This is a 570KB file, and if I run it through vips to resize to 1400px wide:
Using gifsicle:
Again, Gifsicle's image is not as nice looking. But that filesize difference is quite a bit, especially because the source is only 570KB.
I'm running:
@dloebl @MCLoebl Found you a couple examples! 😄
The text was updated successfully, but these errors were encountered: