-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Premultiply alpha for transparent images to prevent dark edges #213
Conversation
Argh, I see the file hashing based comparison doesn’t pass on CI. Will look into it on Friday. |
Thanks Daniel, as always, for all your work on improving sharp. To quote George Bernard Shaw, "England and America are two nations unpremultiplied by a common language." I don't think
As for the functional tests, perhaps you could use |
Thanks to John's sterling work, this improvement should now be able to take advantage of the new #if (VIPS_MAJOR_VERSION >= 9 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 1))
...
#endif Are you able to make this update, which I guess should also apply to the Composite logic? |
Now that the cat is out of the bag—we launched Think Kit—I will dedicate today to polishing up this addition to
The team and I had a good laugh about that one 🇬🇧 🇺🇸 😜
Is that because it doesn’t do any interpolation? Either way, I am happy to give it a shot and see what happens. Is your main motivation for avoiding premultiplication where possible for performance reasons? If so, what’s the best way to capture a condition that says we will require it, i.e. bool shouldApplyAffineTransform = xresidual != 0.0 || yresidual != 0.0;
bool shouldComposite = !baton->overlayPath.empty();
// …
bool shouldPremultiplyImageAlpha = HasAlpha(image) && image->Bands == 4 &&
(shouldApplyAffineTransform || shouldComposite /* || … */);`
Thanks for confirming.
Thanks, I’ll give this a shot. This sounds like *Magick’s
Yes, I can do that too. Thanks for the snippet with the preprocessing statement 👍 |
Status update: I made the changes you suggested in
|
printf("xmin: %f\n", (double) stats->data[6]); | ||
printf("ymin: %f\n", (double) stats->data[7]); | ||
printf("xmax: %f\n", (double) stats->data[8]); | ||
printf("ymax: %f\n", (double) stats->data[9]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What’s the right way to read the values from vips_stats
? If this is correct, why I am only getting 0
for sum2
when running it with two different images?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn’t cast stats
to double *
correctly.
@lovell Alright, this is ready for another review. Will squash the commits once you give me a 👍 |
|
||
VipsImage *actualPremultiplied; | ||
VipsImage *expectedPremultiplied; | ||
if (Premultiply(context, actual, &actualPremultiplied) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the use of Premultiply
here need wrapping in the shouldPremultiplyAlpha
logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made Premultiply
a noop (strictly: a copy) if the image has no transparency. I found an example in John’s code.
Thanks again for all your work on this Daniel. I've added a few comments inline. Sorry I wasn't able to make time to review this yesterday. |
Thanks for your feedback and no need to apologise. I pushed some more improvements, but I haven’t checked if that was all since we had a little party for our Think Kit launch. A main thing I’d love your 👀 on is, if there are any obvious or not so obvious memory leaks. We are seeing some in production and it’s likely from my |
|
||
* `err` contains an error message, if any. | ||
* `info` contains the info about the difference between the two images such as `isEqual` (Boolean), `meanSquaredError` (Number; present iff `status='success'`, otherwise `undefined`), and `status` (String; one of `success`, `mismatchedDimensions`, `mismatchedBands`, `mismatchedType`). | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added status
to not report certain errors as fatal, e.g. mismatched dimensions, bands, etc.
// Main | ||
|
||
// Constants | ||
var MAX_ALLOWED_IMAGE_MAGICK_MEAN_SQUARED_ERROR = 0.3; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⬇️ Turns out sharp
’s results are really close to ImageMagick, as one would expect 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if this fails again on other platforms or versions. We can adjust it upwards then.
Running
Here's Rather than increase the threshold, perhaps either extracting a region known to suffer regression/problems or using smaller test images will help. |
Thanks for running the test with the latest VIPS version. Agreed, it doesn’t show black fringing.
I thought about that, but wouldn’t we want to ensure output images have a low error against a visually verified reference image as a whole? We might not want to only catch black fringes but any kind of errors. In this case, we could allow an MSE of More importantly, it might be interesting to investigate what causes the discrepancy. I am sure John was much more finessed in his code for the native |
return -1; | ||
|
||
vips_object_local(context, t2); | ||
vips_object_local(context, outRGBPremultiplied); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lovell: Isn’t this another memory leak: When vips_multiply
succeeds t2
is allocated, but then vips_add
fails and the function returns without ever calling vips_object_local(context, t2)
? I originally combined multiple operations into one—if (vips_* || vips_* || vips_*)
—based on @jcupitt’s pattern using vips_object_local_array
: https://gist.github.com/jcupitt/abacc012e2991f332e8b#file-composite2-c-L16-L22. However in his case, all t[…]
seemed to be pre-hooked onto context
right away. No more extra vips_object_local
calls afterwards. Then we switched back to using individual variables for better readability but I kept the if (… || … || …)
pattern. Your code uses individual variables but only single operations per if (…)
. Am I right in identifying this as a potential leak (in error cases) or did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in the event of an error occurring within vips_add
, t2
would never be unreferenced.
I still believe using explicit references is safer even when it means having to spread multiple operations over multiple conditionals, although I may be suffering from a bout of "once bitten, twice shy".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for confirming. I agree, I’d rather play it safe and will try to rewrite it with multiple conditionals. Can’t wait for #152 ❤️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, in ’Murica 🇺🇸 they say ‘Fool me once…’: https://www.youtube.com/watch?v=eKgPY1adc0A 😜
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
libvips uses this style, as a possible alternative:
VipsImage **t = vips_object_local_array(..);
VipsImage *in;
in = ..->input;
if( vips_thing(in, &t[0], NULL ) ||
vips_thing2(t[0], &t[1], NULL )
return -1;
in = t[2];
if( vips_thing3(in, &t[2], NULL ) ||
vips_thing4(t[2], &t[3], NULL )
return -1;
in = t[3];
if (vips_image_write(in, out))
return -1;
return 0;
This has some nice properties:
- you can use
.. || ..
to chain operations together, so it's concise - guaranteed leak-free
- you only need to keep track of the annoying
t[]
variables with a.. || ..
block -- the blocks are independent and joined with properly named variables - because blocks are independent, you can reorder them safely: you could swap these two over and it would all work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks John, I'm hoping this is relatively short-lived as sharp's move to the new vips8 C++ bindings will remove all the reference counting, taking advantage of internal calls to vips_object_local_array
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converted all code to use individual error conditionals 👍
Yes, how about we test the image as a whole using a "general" threshold and test the region known to cause problems at a specific, lower threshold? |
I suspect we're probably looking at difference due to cumulative rounding errors. John's approach avoids the band splitting/joining and seems to be a bit faster as a result, although I've yet to benchmark this. |
The vips test suite looks for |
Add `Sharp.compare(file1, file2, callback)` function for comparing images using mean squared error (MSE). This is useful for unit tests. See: - https://github.com/jcupitt/libvips/issues/291 - http://entropymine.com/imageworsener/resizealpha/
Both might be worth pursuing as well and the second shouldn’t be hard to add to Thanks again to both of you for your help 😄 |
Not sure why this is failing. I didn’t make any changes to it. |
Great job Daniel, I'm happy to take it from here and will use John's Thanks again for you for all your work on this improvement. |
Premultiply alpha for transparent images to prevent dark edges
Thanks for merging this and my pleasure ❤️ |
This change premultiplies the alpha channel into the RGB channels for RGBA images to prevent dark edges when transforming (scaling, blurring, etc.) images. I opened this against
knife
as it had significant changes toComposite
as we want to avoid double premultiplication.On a less serious note: I am happy to take superior naming suggestions for
Unpremultiply
, but it might be actually pretty standard: https://msdn.microsoft.com/en-us/library/windows/desktop/hh780397%28v=vs.85%29.aspx Oh, ’muhrica 🇺🇸Supercedes #211.
I’ve done testing of this against ImageMagick with our own images output from Paper, but haven’t had a chance to do general regression testing with other kinds of inputs. This is a starting point and I’d be happy to polish it for inclusion in
knife
in the coming days. I placed the premultiplication logic before any image transformations and the unpremultiplication (sic!) before output. Please let me know if the placement can be optimized. The reference articles talk about having to watch out for zero alpha values (division by zero) and clamping, but I haven’t had a chance to explore that yet. I wonder ifvips_divided
, etc. take care of this internally, but maybe you know.I did add theAddedfile-compare
module as I needed really strict regression testing, although I realize it might fail due to different versions ofvips
creating the output. Happy to take suggestions for a better approach, although I think it needs to be closer to ImageMagickcompare
than the more loose perceptual hash. At some point, even withtolerance: 0
it couldn’t distinguish two reference images with similar shape but different colors.sharp.compare
to compute mean squared error (though it doesn’t match the same numbers I get fromcompare -metric mse
😢). At least it lets us detect whether images are identical at a pixel level.References:
/cc @julianwa