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
Add automatic image sharpening #22
Comments
Also, since this is tightly related to resizing, I think this should be done automatically after one of the Also, as @mokolabs suggested in https://github.com/janko-m/image_processing/issues/16#issuecomment-375020142, we should probably do this automatically only for JPEG images. |
@janko-m For the libvips implementation, I'd recommend sticking with the mask-style sharpening method using The libvips By contrast, the mask-style sharpening method using That said, we should definitely strive for a great, out-of-the-box sharpen on images resized with either imagemagick and libvips -- so that no configuration is needed. I'm sooooooooo tired of thinking about these things. Let's just pick some awesome defaults and move on. 👍 (I'm happy to do a bit more testing and research on the image matrix settings, so that we're using the best practice thinking on those). |
This is what the documentation suggests:
But I agree the parameters are not intuitive. In contrast, ImageMagick's
Agreed. I'm just a bit confused, the matrix that carrierwave-vips and vips-process use doesn't match any of the provided matrices in the Wikipedia article. Do you know how it originated? I'm asking because I would like "magic constants" to have an explanation in the comments.
Agreed!
I was looking now at some other projects I know that use libvips. Sharp applies this mask using From this information I would suggest that we use the same mask as the mask = Vips::Image.new_from_array [
[-1, -1, -1],
[-1, 32, -1],
[-1, -1, -1]], 24
image.conv(mask) and carrierwave-vips' & vips-process' mask = Vips::Image.new_from_array [
[-1, -1, -1],
[-1, 24, -1],
[-1, -1, -1]], 16
image.conv(mask) If not, I would go with sharp's mask, as those folks really seem to know what they're doing 😃, and there is a large userbase that tested that mask. |
Okay, I did some more testing and research.
32/24 is probably a better default choice since you're getting a reasonable amount of sharpening, but still preserving the contrast of the original image. 24/16 is sharper and shows more detail, but at a cost of increasing image contrast. So, yeah, I think going with Sharp's mask is a totally reasonable choice. ... FYI, here's an interesting graphic from this paper that shows how an unsharp matrix mask works: So, in our case, if we replace 9 with 24 and set α to 1, the center number will be 32 (or 24 + (8 * 1)). |
Also... this site has an interactive image matrix where you can play with the numbers in an image matrix and see how they change an image. If you increase the center number to be higher than the divisor, it will lighten the overall image. If you then add negative numbers to the edges of the matrix, you will see that the negative numbers are darkening edges (and increasing contrast) and then the center number is lightening the image to make up for the increased darkness. And that's basically how the unsharp matrix works. 😄 |
@mokolabs Thank you so much for all this awesome research and information! ❤️ I'll go ahead and start working on the sharpening implementation. |
No problem, @janko-m. It's not exactly fun, but I've learned sooooo much about image processing... and I'm excited to see improvements to this gem. BTW, I think we might want to use a different default for Imagemagick sharpening. Imagemagick has two basic ways of sharpening:
In my testing, using the exact commands and values above, the -sharpen option looks way better. Resize with no sharpening Resize with -sharpen set to 0x1 Resize with -unsharp set to 0.25x0.5+8+0.065 Notice how edges are very jaggy and have more noise than the original. Resize with -unsharp set to 0x1+1+0.065 This version is a lot better. By reducing the radius and sigma to match the 2nd test image, and also reducing the gain to 1, we get very close to the 2nd test image. But the 2nd image brings out a bit more detail in highlights. Summary FWIW, I'd probably still go with |
@mokolabs Awesome, totally agreed, we'll go with |
@janko-m, is there any config option for disabling automatic sharpening for entire pipeline? For example: |
@volodymyr-shevchuk No, there currently isn't one. Since this would only concern the You can always DRY up passing VERSION_DEFINITIONS = {
large: [:resize_to_limit, 800, 800],
medium: [:resize_to_limit, 500, 500],
small: [:resize_to_limit, 300, 300],
square: [:resize_to_fill, 150, 150],
}
versions = {}
VERSION_DEFINITIONS.each do |name, (operation, *args)|
versions[name] = pipeline.send("#{operation}!", *args, sharpen: false)
end |
@mokolabs I just saw a conference talk where the presenter mentioned that without auto-sharpening 30% of their images were blurry (they had migrated from Shrine to Active Storage 5.2, which didn't yet use ImageProcessing). So, thanks again for all your help on adding it! |
Work in janko#22 added automatic image sharpening for Vips. However, discovered in janko#55 `conv()` defaults to float precision and can leave to visual artifacts in some scenarios. Per @jcupitt recommendation, we can add `precision: :int` to conv and this should also give us a speed-up, since int convolutions will use SIMD. janko#55 Fixes janko#55
Work in janko#22 added automatic image sharpening for Vips. However, discovered in janko#55 `conv()` defaults to float precision and can leave to visual artifacts in some scenarios. Per @jcupitt recommendation, we can add `precision: :int` to conv and this should also give us a speed-up, since int convolutions will use SIMD. janko#55 Fixes janko#55
Work in janko#22 added automatic image sharpening for Vips. However, discovered in janko#55 `conv()` defaults to float precision and can leave to visual artifacts in some scenarios. Per @jcupitt recommendation, we can add `precision: :int` to conv and this should also give us a speed-up, since int convolutions will use SIMD. janko#55 Fixes janko#55
Work in #22 added automatic image sharpening for Vips. However, discovered in #55 `conv()` defaults to float precision and can leave to visual artifacts in some scenarios. Per @jcupitt recommendation, we can add `precision: :integer` to conv and this should also give us a speed-up, since int convolutions will use SIMD. #55 Fixes #55
Hello @janko @mokolabs , I have transitionned to VIPS and was trying to add sharpening that would mimick ImageMagick default sharpening which gave good results. I was going with the default matrix: But it didn't add enough sharpening, especially to small images. Thanks to @mokolabs link : http://beej.us/blog/data/convolution-image-processing/ I have kinda understood how to add strength to the image sharpening Let's say we know : And want to know : The matrix looks like : [-s, -s, -s], The missing value r is then equal to Σs + d Regarding the d value, the link recommends to use the number of dots in the matrix, then switched the divider to 9. Also I have noticed a gain in performance by computing the resulting pixel from only a subset of neighbours (and result is still consistent eventhough slightly less sharpened in high contrast zones) [0, -s, 0], (Also Vips behavior was really strange: performance spiked to 25 seconds per image after a few matrix trials. Had to kill Puma, Sidekiq, and close the Chrome tab and reopen localhost in a new tab to see better performance again. Not sure where the problem cam from..) I am really noob in Ruby (I do Rails Frankenstein code) and can't really help but that would be great to be able to supply the s value only, and image_processing to rebuild the matrix ? And try to find equivalent result for ImageMagick so that we don't have to tweak sharpening settings when switching from a processor to the other. I am will still try to read image_processing code and see if I understand and maybe do a pull request .. but Janko will have to proofread me 😄 |
Thank you for researching this, it sounds like you had interesting findings. ImageProcessing currently allows you to override the convolution mask: def sharpen_mask(s)
d = 9
r = 8 * s
matrix = [[0, -s, 0],
[-s, r, -s],
[0, -s, 0]]
Vips::Image.new_from_array matrix, d
end
ImageProcessing::Vips
.source(image)
.resize_to_limit!(600, 600, sharpen: sharpen_mask(1)) I would be open to supporting options for automatically building convolution masks based on the knowledge you posted, to help people adjust sharpening without having to understand convolution masks. I guess something like this would make sense: .resize_to_limit!(600, 600, sharpen: { strength: 2 })
Did that happen only with the convolution masks with zeroes in it, or even with the default one?
Yeah, that'd be really nice. |
Hi Janko, Then if ImageMagick allows convolution masks conversion I guess we have a very similar way of sharpening for both IM and Vips. I have not tried the convolution mask in my Ubuntu console with IM yet but I don't see a reason why it should be different. I will try to do a few tests by Sunday with IM and VIPS and see if we get consistent results between the 2 processes, for a similar sharpening matrix. Your sharpen_mask function seems perfect. (Though with a matrix with 4 s, I guess r is rather something like What I have noticed also is that a matrix with 8 s (well, I mean 8 cells like Yet a sharpening strength of 2, compared to 1, with a matrix with 4 s still makes a big difference.
And it looks perfect. I am not sure if this is possible to modifiy the even refined further this incremental step in sharpness.. I tried to tweak the divisor also :
=>I arbitrarily doubled up the divisor from 9 to 18 ..and VIPS processed 4 Shrine variants of a 13 MO jpg image in about 2 seconds which is consistent with what I used to have with the previous matrixes with a divisor of 9. Even if we don't really have a way to refine the strength, setting s value with an integer (pull request 56) seems quite fulfilling. We can't get a more refined result by tweaking the matrix directly IMO.. Regarding the decrease of performance I got originally am not too sure. Using the above matrixes don't really change performance over time on my side. |
Hi Janko, I made a quick test with a single image sharpening with both IM and VIPS and it gave exactly the same results:
And locally with IM : I used a divisor of 9 and a strength of 4. The resulting images were exactly the same. A sharpening with a strength of 4 was applied to both with no visible difference. So using matrixes for sharpening should give exact same results whether we use VIPS or Im as processor. |
Nice that you found an ImageMagick alternative 👍 Does ImageMagick's According to this article, the Based on the current discussion and some research, I think it would make sense to have
I don't know, that's just an idea. I feel that convolution matrices are complex for reading and seeing them in MiniMagick logs, but they seem to offer more control. I'm just wondering again whether we can use the simpler sharpening functions that ImageMagick/libvips already provide as default. |
I think having the two options is great. Regarding IM sharpening, there seem to be 2 functions:
As per the comments it seems Radius is the size of the matrice and Sigma the strength. Also the sharpen function allows fractional numbers for Sigma. As image_processing matrices is only using integers if I am not mistaken. Regarding Radius, it can be set to 0. Though our smallest matrix is 3x3 which translates into a radius of 1 I guess. (1 pixel around the center pixel
This function is even more complicate and explanation on this thread https://www.even.li/imagemagick-sharp-web-sized-photographs/ is not quite matching what we can read here http://www.imagemagick.org/Usage/blur/. Need to investigate on this. Well I guess it is difficult to translate anything that is used at the moment : function for IM and matrice for Vips into something unified. The best would be to have an inhouse higher-level single digit as sharpness strength that would overwrite the default sharpening. (well if it doesn't create problems elsewhere or in logs). That would be using matrices only, as we now know how to sharpen from matrices from both VIPS (we know that already) and for IM too (with And still allow a different, tweakable, sharpening method depending for each of the processors (function or matrice). Then the user using image_processing for its "higher level" ability can add sharpening very easily. |
FYI, due to #67 (comment) and carrierwaveuploader/carrierwave#2481, I disabled default sharpening in MiniMagick backend in version 1.11.0. |
Thanks Janko. Actually I realise my comments deviated to discussing a higher level sharpening method which could both fit Vips and ImageMagick, with the help of the matrices. |
Images pretty often get a little blurry when resized, so programs such as Photoshop will often apply some sharpening afterwards to make the images a little crisper. I think we should do the same here (continuing the discussion from https://github.com/janko-m/image_processing/issues/16#issuecomment-374677906).
ImageMagick
From this article it seems that for ImageMagick we should use the following:
libvips
For libvips, the
vips_sharpen()
documentation recommends the following defaults:The carrierwave-vips and vips-process gems both use a "sharpen mask" which they apply using
vips_conv()
. I'm copy-pasting @mokolabs' port to the newer libvips version from https://github.com/janko-m/image_processing/issues/16#issuecomment-375037420:@mokolabs Any ideas which ones of these two we should use? I'm leaning towards using
vips_sharpen()
as that's what it was designed for. But if it produces worse results than thevips_conv()
approach then we might need to tweak the parameters. Hopefully the parameters should be fine if it's recommended in the documentation.The text was updated successfully, but these errors were encountered: