Skip to content

Conversation

@lakshayg
Copy link
Contributor

@lakshayg lakshayg commented Oct 18, 2024

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practices as demonstrated in the repository.

Description

Add MagicKernelSharp{2013,2021} resizing kernels. The implementation is based on "Solving the mystery of Magic Kernel Sharp" by John P. Costella (https://johncostella.com/magic/mks.pdf).

Partially resolves #3316

@lakshayg
Copy link
Contributor Author

lakshayg commented Oct 18, 2024

Hi maintainers, this is my first time contributing to ImageMagick. I am not deeply familiar with signal processing but have taken a stab at implementing the Magic Kernel based on the paper https://johncostella.com/magic/mks.pdf and by looking at how other filters were implemented. Please let me know if I overlooked certain details or missed something in this PR.

As an aside, the paper mentions that this filter is essentially (box * box * box * sharp2013). I couldn't figure out a way to chain multiple filters in the resize operation. I wonder if having such an ability will make it easier to implement custom filters.

Add the MagicKernelSharp (2013) resizing kernel. The implementation
is based on "Solving the mystery of Magic Kernel Sharp" by
John P. Costella (https://johncostella.com/magic/mks.pdf)
Copy link
Member

@dlemstra dlemstra left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution and I will take a look at the actual implementation at a later moment but I already have two comments.

LanczosSharpFilter,
Lanczos2Filter,
Lanczos2SharpFilter,
MagicSharp2013Filter,
Copy link
Member

Choose a reason for hiding this comment

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

New enum values should be added at the end to maintain backwards compatibility

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

{ "Lanczos2Sharp", Lanczos2SharpFilter, UndefinedOptionFlag, MagickFalse },
{ "LanczosRadius", LanczosRadiusFilter, UndefinedOptionFlag, MagickFalse },
{ "LanczosSharp", LanczosSharpFilter, UndefinedOptionFlag, MagickFalse },
{ "MagicSharp2013", MagicSharp2013Filter, UndefinedOptionFlag, MagickFalse },
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if we need the year?

Copy link
Contributor Author

@lakshayg lakshayg Oct 18, 2024

Choose a reason for hiding this comment

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

There are two variants of this algorithm (2013 and 2021), with different amounts of sharpening. I wanted to leave the possibility open that we can implement the other one as well.

Copy link
Contributor Author

@lakshayg lakshayg Oct 18, 2024

Choose a reason for hiding this comment

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

I saw that the author has also referred to these algorithms as "MagicKernelSharp" and "MagicKernelSharp+". I have renamed things accordingly.

Copy link
Contributor Author

@lakshayg lakshayg Oct 19, 2024

Choose a reason for hiding this comment

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

I read through the website and the paper again and realized that both my comments above are incorrect. I'm putting a very high-level summary of my current understanding here:

  1. The author discovers the "Magic Kernel"
  2. They notice that it introduces a slight blur so they include a sharpening filter (Sharp 2013 = {-1/4,3/2,-1/4}) to compensate. This is called "Magic Kernel Sharp 2013"
  3. Instagram wants to add an extra "pop" to the image by sharpening it a little more. They change the sharpening filter to {-s,4+2s,-s}/4 for some value of s > 1. It's not explicitly mentioned what value of s Instagram uses. This filter is called "Magic Kernel Sharp+ 2013"
  4. The author notices that "Sharp 2013" sharpens a little too much so they introduce a small blurring term {1,0,34,0,1}/36. Combining this term with "Sharp 2013" gives "Sharp 2021". This combined with the "Magic Kernel" gives "Magic Kernel Sharp 2021"
  5. "Magic Kernel Sharp+ 2021" is a version of the above filter with a little extra sharpening. The exact parameters of this version are also unclear.

I decided to implement "Magic Kernel Sharp 2021" in this PR because it does not blur or sharpen to 9 bits of accuracy and therefore can be considered the latest version of this algorithm.

I am unsure if it is worth implementing:

  • the 2013 version (it is faster but not as accurate) OR
  • the "pop" versions as their parameters are not specified

In addition to these, the author has generalized the idea of the magic filter and produced lower/higher order filters (the lower order ones are "box" and "triangle"). If there's interest, a proper implementation would mean adding an "order" parameter and a "sharpening" parameter for MKS which would let the user create any of the variants of this filter. Like before, I'm unsure if it's worth implementing.

Please let me know your thoughts.

Copy link

Choose a reason for hiding this comment

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

I suggest both 2013 and 2021

Copy link
Contributor Author

@lakshayg lakshayg Oct 19, 2024

Choose a reason for hiding this comment

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

Would you prefer having "2013" and "2021" in the name or do you have other ideas? Done

@lakshayg
Copy link
Contributor Author

lakshayg commented Oct 18, 2024

I am adding a mathematica workbook listing what I did in the hope that it will help with reviewing the implementation. It is identical to what the paper says.

image

@lakshayg lakshayg changed the title Implement "Magic Kernel Sharp (2013)" Implement "Magic Kernel Sharp (2013 and 2021)" Oct 18, 2024
@lakshayg lakshayg changed the title Implement "Magic Kernel Sharp (2013 and 2021)" Implement Magic Kernel Sharp (2013 and 2021) Oct 18, 2024
@lakshayg lakshayg changed the title Implement Magic Kernel Sharp (2013 and 2021) Implement Magic Kernel Sharp 2021 Oct 19, 2024
@lakshayg lakshayg changed the title Implement Magic Kernel Sharp 2021 Implement Magic Kernel Sharp 2013 and 2021 Oct 19, 2024
See: Solving the mystery of Magic Kernel Sharp (https://johncostella.com/magic/mks.pdf)
*/
if (resize_filter->support <= 2.5)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this should be a separate method instead of a check inside this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@urban-warrior urban-warrior merged commit 6e3f8e5 into ImageMagick:main Oct 20, 2024
@j-p-c
Copy link

j-p-c commented Dec 8, 2024

Ah sorry, I didn't even see this work—I was only alerted to it through an email from someone grateful for both of our work today. I could have helped with this discussion, but it looks like it's all been sorted.

I also agree with the decision on 2013 v 2021 v both. 2013 remains popular, particularly for fast, low-level implementations, but 2021 is more accurate. The other high-level algorithms are more mathematically correct, but slower, and I think in many situations actually create unexpected artifacts.

@j-p-c
Copy link

j-p-c commented Dec 11, 2024

[EDIT: I thought that this was wrong, but now I think it might not be; see below]

I've tried it out against my reference implementation and it looks good to me.

@kleisauke
Copy link
Contributor

FWIW, according to ResampleScope, the 2013 variant appears to have been incorrectly implemented:
https://github.com/kleisauke/resize-comparison#imagemagick-magickernelsharp2013-vs-libvips-mks2013

@j-p-c
Copy link

j-p-c commented Dec 16, 2024

FWIW, according to ResampleScope, the 2013 variant appears to have been incorrectly implemented: https://github.com/kleisauke/resize-comparison#imagemagick-magickernelsharp2013-vs-libvips-mks2013

Ah ok, I defer to this analysis and withdraw my comment. I will look into this shortly.

[Edit: see below; it's not clear to me now where the error is]

@j-p-c
Copy link

j-p-c commented Dec 16, 2024

Uh … I'm actually concerned about both implementations now. It should look like this:

https://johncostella.com/magic/mks-2013-graph.png

I will try to debug this further. If applied as a single kernel (I have not checked if it was done that way either here or for libvips) then the formula should be:

https://johncostella.com/magic/mks-2013-formula.png

If, alternatively, it was done in the original (Facebook and Instagram) way, then the MK should look like this:

https://johncostella.com/magic/m-x-graph.png

with formula

https://johncostella.com/magic/m-x-formula.png

with the three-tap Sharp2013 filter applied in the appropriate space.

@j-p-c
Copy link

j-p-c commented Dec 16, 2024

OK, now I'm confused again. resize.c has the code

Screenshot 2024-12-16 at 6 36 17 PM

which appears to differ from what I have (above) on my website

Screenshot 2024-12-16 at 6 39 41 PM

but expanding out the algebra shows that they're both equivalent. And (as with all variants of MK and MKS) they are all piecewise parabolic (and continuous and with continuous first derivative).

It's not clear from the graphs on your comparison whether those are piecewise parabolic—they're obviously wrong, but to the resolution shown they might be parabolas, just the wrong ones. (For libvips it's the flat top that is clearly wrong.)

I don't know whether the problem is with ImageMagick's use of its own resize.c (unlikely), with ResampleScope, or with your use of it.

Given that it would be remarkable if both ImageMagick and libvips were to each wrongly implement MKS 2013 in different ways, but both get MKS 2021 (and every other kernel) correct, I'm leaning towards one of the two latter explanations, but you'll have to test this yourself.

@urban-warrior
Copy link
Member

We corrected the algorithm. Let us know if you still think the algorithm has issues. If so, we can always revert the patch.

@j-p-c
Copy link

j-p-c commented Dec 17, 2024

We corrected the algorithm. Let us know if you still think the algorithm has issues. If so, we can always revert the patch.

Ha, now I'm even more confused. :)

I noted that the output of both 2013 and 2021 appeared to match that of my reference implementation. I discounted small localized deviations, as there are implementation-dependent details that are not expected to match exactly.

@kleisauke found that 2021 was equal to libvips, but that 2013 was not. I then noted that the graphs he got for 2013 were wrong both here and for libvips. The graphs for 2021 look correct, visually.

@urban-warrior just noted that 2021 was just patched tonight. I haven't checked the formulas for 2021, and @kleisauke's test had shown it to be equivalent to libvips.

In all of these, we are being imprecise as to which version each of us is running. I was just using the current Homebrew version on Mac, since (until tonight) these were landed two months ago, and the options agreed with the current codebase.

Let me do some more digging.

@j-p-c
Copy link

j-p-c commented Dec 17, 2024

Confirmed that I have been running 7.1.1-41 which is the current release, and that resize.c for it has what I showed above for 2013. It should be correct. 7.1.1-41 also matches what was in there for 2021 before @urban-warrior's patch tonight.

Let me check 2021 against the formulas on my page.

@j-p-c
Copy link

j-p-c commented Dec 17, 2024

MKS 2013

OK, so I just noticed @urban-warrior's other patch tonight, for 2013. Since I had already established that what was previously there was correct (and I have just double-checked the algebra), this patch to 2013 is wrong and should be reverted.

MKS 2021

For 2021, the reference formulas on my page are

Screenshot 2024-12-16 at 11 02 51 PM

Tonight's patch is

Screenshot 2024-12-16 at 11 04 06 PM

In all cases I find that what was there before tonight's patch to be equivalent to my reference formula. The new patch is wrong for lines 451, 453, and 455, and is a no-op for line 457, and so also should be reverted.

@kleisauke
Copy link
Contributor

I've spent some time researching this and didn't find any obvious issues in the original implementation. However, I can confirm that the recent patches are definitely broken.

I was able to resolve the "flat top"-issue for the Magic Kernel Sharp 2013 algorithm, it seems to be a minor issue in ResampleScope itself, see: jsummers/resamplescope#3.

@j-p-c
Copy link

j-p-c commented Dec 17, 2024

I've spent some time researching this and didn't find any obvious issues in the original implementation. However, I can confirm that the recent patches are definitely broken.

I was able to resolve the "flat top"-issue for the Magic Kernel Sharp 2013 algorithm, it seems to be a minor issue in ResampleScope itself, see: jsummers/resamplescope#3.

So this run still remains anomalous and unexplained? There are both small discontinuities—at positions that are not the half-integer transition points of the parabolas—as well as the strange section around zero. Looking at your diff in that other thread, it seems that changing the colors causes significant changes to the apparent shape of the graph in a number of places of various graphs, not just the "flat top" issue.

I.e., it seems like the bug is in ResampleScope, not here. (Except for the incorrect diffs committed last night.)

Screenshot 2024-12-17 at 1 11 51 PM

@fmw42
Copy link

fmw42 commented Dec 17, 2024

Can you explain how you create these graphs by running Imagemagick -- what command as Imagemagick does 2D resampling not 1D?

Could the IM flat-top be a clipping issue?

@j-p-c
Copy link

j-p-c commented Dec 18, 2024

Can you explain how you create these graphs by running Imagemagick -- what command as Imagemagick does 2D resampling not 1D?

Could the IM flat-top be a clipping issue?

I'll let @kleisauke answer that properly, and I haven't looked at how ResampleScope works, but I imagine that it wouldn't be too difficult to basically do an impulse response—say, an input image with a vertical line down the middle; blow it up using the program under test by a large enough factor to get horizontal resolution for the graphs; and then analyze the resulting image, which should show the impulse response in each row. There would be some interesting dynamic range and clipping issues to deal with, unless you had fully floating point images, which is what I think you're pointing to as a likely culprit.

@kleisauke
Copy link
Contributor

For downscaling, ResampleScope generates two "dots" pattern test images (pd.png and pdr.png), which have dimensions of 557x275 and 275x557, respectively. These images are downscaled to 555x275 to test the horizontal direction and to 275x555 to test the vertical dimension.

pd.png pdr.png
pd pdr

ResampleScope is extremely sensitive to cases where an application performs both horizontal and vertical shrinking, even when it should only shrink in one direction. This appears to be the issue with ImageMagick, for example, when resizing with:

$ magick pd.png -filter magickernelsharp2013 -resize '555x275!' pd_magick_magickernelsharp2013.png

(original source can be found here)

It also appears to call VerticalFilter() despites that y_factor being equal to 1.0. I was able to resolve this using this patch.

With that, it generates the correct graphs (see e.g. commit kleisauke/resize-comparison@931cb55). I confirm that the "flat top"-issue persists in the generated graphs for ImageMagick when the previously linked PR is not applied. I still need to investigate why altering the special colors resolves this.

My apologies for the noise and wasted time. :(

@j-p-c
Copy link

j-p-c commented Dec 22, 2024

Ah, interesting, and thanks for root-causing this. A very elegant way of testing the kernel, but also, as you note, somewhat sensitive to the assumption that the implementation will not resize in both directions when it only needs to do one.

After seeing this, I thought that it might be useful for me to implement the "kernel test" the way that I spitballed above, which is less elegant, but has the advantage of being directly inspectable. So I did. I include it on my page at https://johncostella.com/magic/#test.

Interestingly, I found that ImageMagick got the right results (in agreement with your revised findings) but does more quantizing than I thought it would be doing. It's probably just my misreading of the documentation, and missing out one or more required command-line options to prevent it, but anyway I include these observations in that section as well.

@fmw42
Copy link

fmw42 commented Dec 22, 2024

Suggestion: try using -distort resize rather than -resize. See if that is smoother

magick mks-2013-input.png -colorspace RGB -filter MagicKernelSharp2013 -distort resize 10000% -colorspace sRGB mks-2013-magick.png

@j-p-c
Copy link

j-p-c commented Dec 22, 2024

magick mks-2013-input.png -colorspace RGB -filter MagicKernelSharp2013 -distort resize 10000% -colorspace sRGB mks-2013-magick.png

Not good.
mks-2013-magick-distort

@fmw42
Copy link

fmw42 commented Dec 22, 2024

OK. Thought it might help, but I guess not. Sorry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

6 participants