Skip to content
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

Feature/webp target size #3950

Merged
merged 1 commit into from
May 20, 2024

Conversation

john-parton
Copy link
Contributor

This is the initial work to close #3824

It works, but there's one "gotcha." The target_size parameter doesn't do anything unless pass is 2 or greater. The more passes, the closer it will get to target_size. In my non-scientific tests, 4 to 6 seem to be reasonable numbers.

I did a basic 'smoke test' with the CLI tool:

vips rot myfile.jpg out.webp[Q=100,target_size=2500000,pass=4] d90

and python

with open("myfile.jpg", "rb") as f:
    buffer = f.read()

image = pyvips.Image.new_from_buffer(buffer, "")

buff = image.webpsave_buffer(target_size=1_000_000, **{"pass": 4})

with open("out.webp", "wb") as f:
    f.write(buff)

Note that 'pass' is a keyword in python, so I have to pass it by unpacking a dictionary. Very not fun.

Does this general approach seem reasonable?

Copy link
Member

@jcupitt jcupitt left a comment

Choose a reason for hiding this comment

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

Looks good!

You'll need to also add a note to ChangeLog and a test to test_webp in test/test_suite/test_foreign.py.

libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
@john-parton
Copy link
Contributor Author

john-parton commented May 3, 2024

An additional note: I'll have to verify, but I believe the target_size parameter doesn't limit the size, it just tries to make it "close" depending on the number of passes. I'll do some smoke tests.

Edit: For my case, I think that's fine. Cloudflare Images has a hard limit of 10MB, but honestly a 10MB webp has way more fidelity that I really need. I'll likely set target_size to some lower value like 5MB.

@john-parton
Copy link
Contributor Author

I wanted to make sure that I pick a sane default for passes

Here's some python code I wrote to exercise this branch:

TARGET_SIZE = 1_000_000

with open("myfile.jpg", "rb") as f:
    buffer = f.read()

image = pyvips.Image.new_from_buffer(buffer, "")

print("Default encoding")
with Bench("webpsave_buffer"):
    buff = image.webpsave_buffer()
print(f"Size: {len(buff)}")
print()

print()
for i in range(1, 11):
    with Bench(f"target_size={TARGET_SIZE} & passes={i}"):
        buff = image.webpsave_buffer(target_size=TARGET_SIZE, passes=i)
    print(
        f"Size with passes={i}: {len(buff)} ({100 * len(buff) / TARGET_SIZE:.2f}%)"
    )
    print()

And here's the output

Default encoding
Starting webpsave_buffer
webpsave_buffer completed, elapsed 0:00:01.005219
Size: 656040


Starting target_size=1000000 & passes=1
target_size=1000000 & passes=1 completed, elapsed 0:00:00.891990
Size with passes=1: 656040 (65.60%)

Starting target_size=1000000 & passes=2
target_size=1000000 & passes=2 completed, elapsed 0:00:01.725788
Size with passes=2: 1165504 (116.55%)

Starting target_size=1000000 & passes=3
target_size=1000000 & passes=3 completed, elapsed 0:00:02.498880
Size with passes=3: 1011412 (101.14%)

Starting target_size=1000000 & passes=4
target_size=1000000 & passes=4 completed, elapsed 0:00:03.260787
Size with passes=4: 1011438 (101.14%)

Starting target_size=1000000 & passes=5
target_size=1000000 & passes=5 completed, elapsed 0:00:03.260441
Size with passes=5: 1011438 (101.14%)

Starting target_size=1000000 & passes=6
target_size=1000000 & passes=6 completed, elapsed 0:00:03.263195
Size with passes=6: 1011438 (101.14%)

Starting target_size=1000000 & passes=7
target_size=1000000 & passes=7 completed, elapsed 0:00:03.270455
Size with passes=7: 1011438 (101.14%)

Starting target_size=1000000 & passes=8
target_size=1000000 & passes=8 completed, elapsed 0:00:03.263690
Size with passes=8: 1011438 (101.14%)

Starting target_size=1000000 & passes=9
target_size=1000000 & passes=9 completed, elapsed 0:00:03.262610
Size with passes=9: 1011438 (101.14%)

Starting target_size=1000000 & passes=10
target_size=1000000 & passes=10 completed, elapsed 0:00:03.261657
Size with passes=10: 1011438 (101.14%)

A few conclusions:

  • In general, it doesn't limit the size to target_size, and it looks like even when it's "as close as possible", it's still a bit larger. I suspect that has to do with headers/container overhead.
  • passes == 1 makes the target_size parameter have no effect.
  • passes == 2 is off by over 15%
  • passes == 3 is off by only 1% and doesn't dramatically change encoding time.
  • passes == 4 is only 24 bytes different from passes == 3, but the encoding time is significantly longer.
  • passes > 4 has literally no effect at all with the test image I have.

@john-parton
Copy link
Contributor Author

john-parton commented May 3, 2024

I think this is ready for another review.

  • Added a message in the ChangeLog
  • Added description of parameters in doc comment
  • Define a variable for default number of passes. Let me know if there's a preferred way of doing this. I didn't want to have a magic number just hard coded.
  • Added a python test
  • use the isset helper function

I think I need help with code style. Is there an auto-formatter / linter that I can run locally?

Copy link
Member

@jcupitt jcupitt left a comment

Choose a reason for hiding this comment

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

Great!

ChangeLog Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
libvips/foreign/webpsave.c Outdated Show resolved Hide resolved
@jcupitt
Copy link
Member

jcupitt commented May 3, 2024

Ooop, "approved when changes are made", I meant.

Thank you for doing this work!

@jcupitt
Copy link
Member

jcupitt commented May 3, 2024

I think I need help with code style. Is there an auto-formatter / linter that I can run locally?

Yes, run git clang-format after a change and it'll reformat the code you just entered. Or clang-format -i xxxxx.c to reformat one file.

@john-parton
Copy link
Contributor Author

I think this is ready for review again. I have a comment above, but I'll re-iterate it here.

It's important that passes not be set to 3 if target_size isn't set. Doing so will result in the encoder taking significantly longer for no gain.

@john-parton
Copy link
Contributor Author

I might need a little help documenting "passes".

So basically, it seems to only do something positive/beneficial when target_size or target_PSNR is set.

If you turn on passes without either target_size or target_PSNR, it just dramatically increases encoding time, while leaving both the size and PSNR of the resulting image essentially untouched.

Output:

Starting passes=1,Q=90
passes=1,Q=90 completed, elapsed 0:00:01.216998
Size with passes=1: 1776302

Starting passes=2,Q=90
passes=2,Q=90 completed, elapsed 0:00:01.913680
Size with passes=2: 1773404

Starting passes=3,Q=90
passes=3,Q=90 completed, elapsed 0:00:02.734768
Size with passes=3: 1773730

Starting passes=4,Q=90
passes=4,Q=90 completed, elapsed 0:00:03.536159
Size with passes=4: 1773784

Starting passes=5,Q=90
passes=5,Q=90 completed, elapsed 0:00:04.361937
Size with passes=5: 1773346

Starting passes=6,Q=90
passes=6,Q=90 completed, elapsed 0:00:05.169635
Size with passes=6: 1773392

Starting passes=7,Q=90
passes=7,Q=90 completed, elapsed 0:00:05.994716
Size with passes=7: 1773680

Starting passes=8,Q=90
passes=8,Q=90 completed, elapsed 0:00:06.808321
Size with passes=8: 1773834

Starting passes=9,Q=90
passes=9,Q=90 completed, elapsed 0:00:07.636810
Size with passes=9: 1773820

Starting passes=10,Q=90
passes=10,Q=90 completed, elapsed 0:00:08.462970
Size with passes=10: 1773620

PSNR with passes=1: 41.95693621828301

PSNR with passes=2: 41.95630049312027

PSNR with passes=3: 41.95636007882095

PSNR with passes=4: 41.95786021891513

PSNR with passes=5: 41.968248519712894

PSNR with passes=6: 41.956590374928

PSNR with passes=7: 41.96897815022609

PSNR with passes=8: 41.955997198410685

PSNR with passes=9: 41.956692915390896

PSNR with passes=10: 41.95625941346408

Exercise script:

with open("test.jpg", "rb") as f:
    buffer = f.read()

image = pyvips.Image.new_from_buffer(buffer, "")

for i in range(1, 11):
    with Bench(f"passes={i},Q={Q}"):
        buff = image.webpsave_buffer(passes=i, Q=Q)
    with open(f"out_{i}.webp", "wb") as f:
        f.write(buff)
    print(f"Size with passes={i}: {len(buff)}")
    print()

img_og = cv2.imread("test.jpg")

for i in range(1, 11):
    img = cv2.imread(f"out_{i}.webp")
    psnr = cv2.PSNR(img_og, img)
    print(f"PSNR with passes={i}: {psnr}")
    print()

I didn't implement target_PSNR because I didn't need it for my project, and it seems even more esoteric than target_size.

Not sure how to document this to basically say "this is a foot-gun if you don't have target_size set."

@john-parton
Copy link
Contributor Author

I think this if ready for a final review. I updated the docs to caution users against setting passes without setting target_size

@john-parton
Copy link
Contributor Author

@kleisauke Would you mind taking another look? I think this is pretty close.

@jcupitt
Copy link
Member

jcupitt commented May 19, 2024

Ah I jumped the gun, there seem to be some settings issues.

With this branch, I see:

$ vips black x.webp 1000 1000
webpsave: invalid configuration

The tests above are failing with:

webpsave: unable to encode
webpsave: mux error

@jcupitt
Copy link
Member

jcupitt commented May 19, 2024

Use ninja test to run those tests locally.

@kleisauke
Copy link
Member

Commit 8de3704 should fix that.

@kleisauke
Copy link
Member

Ah, sorry, that only resolves meson test, the Python test suite seems to fail with:

        # target_size should reasonably work, +/- 2% is fine
        x = pyvips.Image.new_from_file(WEBP_FILE)
        buf_size = len(x.webpsave_buffer(target_size=10_000))
>       assert 9800 < buf_size < 10200
E       assert 18684 < 10200

@kleisauke
Copy link
Member

kleisauke commented May 19, 2024

Commit bedd618 should fix the Python test suite by increasing the target_size argument to 20000, since the original ~30 KiB image is already heavily optimized.

I also passed keep='none' to strip all metadata, since that size is not included as part of the target_size calculations in libwebp.

Change default number of passes when target size is set to 3.

Exercise target_size.

Add note in changelog.

Manually cherry-picked fixes by kleisauke.
@john-parton
Copy link
Contributor Author

Thanks for being so helpful and patient. This is my first time using meson/ninja and the first time I've touched C/C++ seriously in a while.

I merged in the fixes you wrote, squashed it down to one commit, and rebased on master to fix a merge conflict with the changelog.

Ran both meson test and ninja test this time.

@jcupitt jcupitt merged commit 3adc62b into libvips:master May 20, 2024
6 checks passed
@jcupitt
Copy link
Member

jcupitt commented May 20, 2024

Great! This will be in 8.16, due in a few months.

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.

Add support for libwebp 'target_size' parameter
3 participants