From b430b02a31e5452c61c11ccfdb8a14941ee2f405 Mon Sep 17 00:00:00 2001 From: drhead Date: Tue, 11 Mar 2025 15:27:14 -0400 Subject: [PATCH 1/7] Add support for Magic Kernel Sharp 2021 resampling --- src/PIL/Image.py | 7 ++++++- src/libImaging/Imaging.h | 1 + src/libImaging/Resample.c | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 884659882..1a43e723d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -169,6 +169,7 @@ class Resampling(IntEnum): HAMMING = 5 BICUBIC = 3 LANCZOS = 1 + MKS2021 = 6 _filters_support = { @@ -177,6 +178,7 @@ _filters_support = { Resampling.HAMMING: 1.0, Resampling.BICUBIC: 2.0, Resampling.LANCZOS: 3.0, + Resampling.MKS2021: 4.5, } @@ -2242,6 +2244,7 @@ class Image: Resampling.LANCZOS, Resampling.BOX, Resampling.HAMMING, + Resampling.MKS2021, ): msg = f"Unknown resampling filter ({resample})." @@ -2254,6 +2257,7 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), (Resampling.BOX, "Image.Resampling.BOX"), (Resampling.HAMMING, "Image.Resampling.HAMMING"), + (Resampling.MKS2021, "Image.Resampling.MKS2021"), ) ] msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}" @@ -2904,11 +2908,12 @@ class Image: Resampling.BILINEAR, Resampling.BICUBIC, ): - if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): + if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS, Resampling.MKS2021): unusable: dict[int, str] = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", + Resampling.MKS2021: "Image.Resampling.MKS2021", } msg = unusable[resample] + f" ({resample}) cannot be used." else: diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 0c2d3fc2e..573a08f0a 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -269,6 +269,7 @@ ImagingError_Clear(void); #define IMAGING_TRANSFORM_HAMMING 5 #define IMAGING_TRANSFORM_BICUBIC 3 #define IMAGING_TRANSFORM_LANCZOS 1 +#define IMAGING_TRANSFORM_MKS2021 6 typedef int (*ImagingTransformMap)(double *X, double *Y, int x, int y, void *data); typedef int (*ImagingTransformFilter)(void *out, Imaging im, double x, double y); diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index f5e386dc2..b723f6f7b 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -79,11 +79,30 @@ lanczos_filter(double x) { return 0.0; } +static inline double +mks_2021_filter(double x) { + /* https://johncostella.com/magic/ */ + if (x < 0.0) + x = -x; + if (x < 0.5) + return 577.0/576.0 - 239.0/144.0 * pow(x, 2.0); + if (x < 1.5) + return 35.0/36.0 * (x - 1.0) * (x - 239.0/140.0); + if (x < 2.5) + return 1.0/6.0 * (x - 2.0) * (65.0/24.0 - x); + if (x < 3.5) + return 1.0/36.0 * (x - 3.0) * (x - 15.0/4.0); + if (x < 4.5) + return -1.0/288.0 * pow(x - 9.0/2.0, 2.0); + return(0.0); +} + static struct filter BOX = {box_filter, 0.5}; static struct filter BILINEAR = {bilinear_filter, 1.0}; static struct filter HAMMING = {hamming_filter, 1.0}; static struct filter BICUBIC = {bicubic_filter, 2.0}; static struct filter LANCZOS = {lanczos_filter, 3.0}; +static struct filter MKS2021 = {mks_2021_filter, 4.5}; /* 8 bits for result. Filter can have negative areas. In one cases the sum of the coefficients will be negative, @@ -693,6 +712,9 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { case IMAGING_TRANSFORM_LANCZOS: filterp = &LANCZOS; break; + case IMAGING_TRANSFORM_MKS2021: + filterp = &MKS2021; + break; default: return (Imaging)ImagingError_ValueError("unsupported resampling filter"); } From acde784811428263cef38564486bfaed9cd6b690 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:31:18 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 7 ++++++- src/libImaging/Resample.c | 12 ++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1a43e723d..f94d307f6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2908,7 +2908,12 @@ class Image: Resampling.BILINEAR, Resampling.BICUBIC, ): - if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS, Resampling.MKS2021): + if resample in ( + Resampling.BOX, + Resampling.HAMMING, + Resampling.LANCZOS, + Resampling.MKS2021, + ): unusable: dict[int, str] = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index b723f6f7b..7b8e289bd 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -85,16 +85,16 @@ mks_2021_filter(double x) { if (x < 0.0) x = -x; if (x < 0.5) - return 577.0/576.0 - 239.0/144.0 * pow(x, 2.0); + return 577.0 / 576.0 - 239.0 / 144.0 * pow(x, 2.0); if (x < 1.5) - return 35.0/36.0 * (x - 1.0) * (x - 239.0/140.0); + return 35.0 / 36.0 * (x - 1.0) * (x - 239.0 / 140.0); if (x < 2.5) - return 1.0/6.0 * (x - 2.0) * (65.0/24.0 - x); + return 1.0 / 6.0 * (x - 2.0) * (65.0 / 24.0 - x); if (x < 3.5) - return 1.0/36.0 * (x - 3.0) * (x - 15.0/4.0); + return 1.0 / 36.0 * (x - 3.0) * (x - 15.0 / 4.0); if (x < 4.5) - return -1.0/288.0 * pow(x - 9.0/2.0, 2.0); - return(0.0); + return -1.0 / 288.0 * pow(x - 9.0 / 2.0, 2.0); + return (0.0); } static struct filter BOX = {box_filter, 0.5}; From 81c115825b10c12dd95643cf587531ad893f475d Mon Sep 17 00:00:00 2001 From: drhead Date: Fri, 4 Apr 2025 16:11:20 -0400 Subject: [PATCH 3/7] update tests and docs --- Tests/test_image_resample.py | 14 ++++++++++++++ Tests/test_image_resize.py | 8 ++++++++ docs/handbook/concepts.rst | 21 +++++++++++++++++++++ src/PIL/Image.py | 14 +++++++++++--- src/libImaging/Imaging.h | 3 ++- src/libImaging/Resample.c | 18 ++++++++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index ce6209c0d..6acb9de23 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -309,6 +309,8 @@ class TestCoreResampleAlphaCorrect: self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.MKS2013)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.MKS2021)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self) -> None: @@ -318,6 +320,8 @@ class TestCoreResampleAlphaCorrect: self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.MKS2013)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.MKS2021)) def make_dirty_case( self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] @@ -360,6 +364,12 @@ class TestCoreResampleAlphaCorrect: self.run_dirty_case( case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.MKS2013), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.MKS2021), (255, 255, 0) + ) def test_dirty_pixels_la(self) -> None: case = self.make_dirty_case("LA", (255, 128), (0, 0)) @@ -368,6 +378,8 @@ class TestCoreResampleAlphaCorrect: self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.MKS2013), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.MKS2021), (255,)) class TestCoreResamplePasses: @@ -453,6 +465,8 @@ class TestCoreResampleBox: Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, + Image.Resampling.MKS2013, + Image.Resampling.MKS2021, ), ) def test_wrong_arguments(self, resample: Image.Resampling) -> None: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 1166371b8..59393552d 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -72,6 +72,8 @@ class TestImagingCoreResize: Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, + Image.Resampling.MKS2013, + Image.Resampling.MKS2021, ), ) def test_reduce_filters(self, resample: Image.Resampling) -> None: @@ -88,6 +90,8 @@ class TestImagingCoreResize: Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, + Image.Resampling.MKS2013, + Image.Resampling.MKS2021, ), ) def test_enlarge_filters(self, resample: Image.Resampling) -> None: @@ -104,6 +108,8 @@ class TestImagingCoreResize: Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, + Image.Resampling.MKS2013, + Image.Resampling.MKS2021, ), ) @pytest.mark.parametrize( @@ -154,6 +160,8 @@ class TestImagingCoreResize: Image.Resampling.HAMMING, Image.Resampling.BICUBIC, Image.Resampling.LANCZOS, + Image.Resampling.MKS2013, + Image.Resampling.MKS2021, ), ) def test_enlarge_zero(self, resample: Image.Resampling) -> None: diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 7da1078c1..a9cef3847 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -195,6 +195,23 @@ pixel, the Python Imaging Library provides different resampling *filters*. .. versionadded:: 1.1.3 +.. data:: Resampling.MKS2013 + :noindex: + + Calculate the output pixel value using the Magic Kernel Sharp 2013 filter + (a quadratic B-spline composed with a sharpening kernel) on all pixels that + may contribute to the output value. This filter can only be used with the + :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` + methods. + +.. data:: Resampling.MKS2021 + :noindex: + + Calculate the output pixel value using the Magic Kernel Sharp 2021 filter + (a quadratic B-spline composed with a sharpening kernel) on all pixels that + may contribute to the output value. This filter can only be used with the + :py:meth:`~PIL.Image.Image.resize` and :py:meth:`~PIL.Image.Image.thumbnail` + methods. Filters comparison table ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -215,3 +232,7 @@ Filters comparison table +---------------------------+-------------+-----------+-------------+ |:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | +---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.MKS2013` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ++---------------------------+-------------+-----------+-------------+ +|:data:`Resampling.MKS2021` | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ++---------------------------+-------------+-----------+-------------+ \ No newline at end of file diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f94d307f6..85adf954e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -169,7 +169,8 @@ class Resampling(IntEnum): HAMMING = 5 BICUBIC = 3 LANCZOS = 1 - MKS2021 = 6 + MKS2013 = 6 + MKS2021 = 7 _filters_support = { @@ -178,6 +179,7 @@ _filters_support = { Resampling.HAMMING: 1.0, Resampling.BICUBIC: 2.0, Resampling.LANCZOS: 3.0, + Resampling.MKS2013: 2.5, Resampling.MKS2021: 4.5, } @@ -2209,7 +2211,8 @@ class Image: :param resample: An optional resampling filter. This can be one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + :py:data:`Resampling.BICUBIC`, :py:data:`Resampling.LANCZOS`, + :py:data:`Resampling.MKS2013`, or :py:data:`Resampling.MKS2021`. If the image has mode "1" or "P", it is always set to :py:data:`Resampling.NEAREST`. If the image mode is "BGR;15", "BGR;16" or "BGR;24", then the default filter is @@ -2244,6 +2247,7 @@ class Image: Resampling.LANCZOS, Resampling.BOX, Resampling.HAMMING, + Resampling.MKS2013, Resampling.MKS2021, ): msg = f"Unknown resampling filter ({resample})." @@ -2257,6 +2261,7 @@ class Image: (Resampling.BICUBIC, "Image.Resampling.BICUBIC"), (Resampling.BOX, "Image.Resampling.BOX"), (Resampling.HAMMING, "Image.Resampling.HAMMING"), + (Resampling.MKS2013, "Image.Resampling.MKS2013"), (Resampling.MKS2021, "Image.Resampling.MKS2021"), ) ] @@ -2694,7 +2699,8 @@ class Image: :param resample: Optional resampling filter. This can be one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, - :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. + :py:data:`Resampling.BICUBIC`, :py:data:`Resampling.LANCZOS`, + :py:data:`Resampling.MKS2013`, or :py:data:`Resampling.MKS2021`. If omitted, it defaults to :py:data:`Resampling.BICUBIC`. (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). See: :ref:`concept-filters`. @@ -2912,12 +2918,14 @@ class Image: Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS, + Resampling.MKS2013, Resampling.MKS2021, ): unusable: dict[int, str] = { Resampling.BOX: "Image.Resampling.BOX", Resampling.HAMMING: "Image.Resampling.HAMMING", Resampling.LANCZOS: "Image.Resampling.LANCZOS", + Resampling.MKS2013: "Image.Resampling.MKS2013", Resampling.MKS2021: "Image.Resampling.MKS2021", } msg = unusable[resample] + f" ({resample}) cannot be used." diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 573a08f0a..5c9d94e44 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -269,7 +269,8 @@ ImagingError_Clear(void); #define IMAGING_TRANSFORM_HAMMING 5 #define IMAGING_TRANSFORM_BICUBIC 3 #define IMAGING_TRANSFORM_LANCZOS 1 -#define IMAGING_TRANSFORM_MKS2021 6 +#define IMAGING_TRANSFORM_MKS2013 6 +#define IMAGING_TRANSFORM_MKS2021 7 typedef int (*ImagingTransformMap)(double *X, double *Y, int x, int y, void *data); typedef int (*ImagingTransformFilter)(void *out, Imaging im, double x, double y); diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 7b8e289bd..aab377888 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -79,6 +79,20 @@ lanczos_filter(double x) { return 0.0; } +static inline double +mks_2013_filter(double x) { + /* https://johncostella.com/magic/ */ + if (x < 0.0) + x = -x; + if (x < 0.5) + return 17.0 / 16.0 - 7.0 / 4.0 * pow(x, 2.0); + if (x < 1.5) + return (1.0 - x) * (7.0/4.0 - x); + if (x < 2.5) + return -1.0 / 8.0 * pow(x - 5.0 / 2.0, 2.0); + return (0.0); +} + static inline double mks_2021_filter(double x) { /* https://johncostella.com/magic/ */ @@ -102,6 +116,7 @@ static struct filter BILINEAR = {bilinear_filter, 1.0}; static struct filter HAMMING = {hamming_filter, 1.0}; static struct filter BICUBIC = {bicubic_filter, 2.0}; static struct filter LANCZOS = {lanczos_filter, 3.0}; +static struct filter MKS2013 = {mks_2013_filter, 2.5}; static struct filter MKS2021 = {mks_2021_filter, 4.5}; /* 8 bits for result. Filter can have negative areas. @@ -712,6 +727,9 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) { case IMAGING_TRANSFORM_LANCZOS: filterp = &LANCZOS; break; + case IMAGING_TRANSFORM_MKS2013: + filterp = &MKS2013; + break; case IMAGING_TRANSFORM_MKS2021: filterp = &MKS2021; break; From 76b4dfb50001b870aaedabf85eba8cf1c99d3edc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 20:11:49 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/handbook/concepts.rst | 2 +- src/libImaging/Resample.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index a9cef3847..b0183a34a 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -235,4 +235,4 @@ Filters comparison table |:data:`Resampling.MKS2013` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | +---------------------------+-------------+-----------+-------------+ |:data:`Resampling.MKS2021` | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | -+---------------------------+-------------+-----------+-------------+ \ No newline at end of file ++---------------------------+-------------+-----------+-------------+ diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index aab377888..89deef5ec 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -87,7 +87,7 @@ mks_2013_filter(double x) { if (x < 0.5) return 17.0 / 16.0 - 7.0 / 4.0 * pow(x, 2.0); if (x < 1.5) - return (1.0 - x) * (7.0/4.0 - x); + return (1.0 - x) * (7.0 / 4.0 - x); if (x < 2.5) return -1.0 / 8.0 * pow(x - 5.0 / 2.0, 2.0); return (0.0); From 2f0458258f48e0108f5d45475425a4a477f18250 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Apr 2025 14:50:35 +1100 Subject: [PATCH 5/7] Corrected table syntax --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index b0183a34a..070913e62 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -234,5 +234,5 @@ Filters comparison table +---------------------------+-------------+-----------+-------------+ |:data:`Resampling.MKS2013` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | +---------------------------+-------------+-----------+-------------+ -|:data:`Resampling.MKS2021` | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | +|:data:`Resampling.MKS2021` | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐| ⭐ | +---------------------------+-------------+-----------+-------------+ From 91c19be6c8f78a3879104a8e788985b05f550ff0 Mon Sep 17 00:00:00 2001 From: drhead <1313496+drhead@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:12:00 -0400 Subject: [PATCH 6/7] add braces Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/libImaging/Resample.c | 42 ++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 89deef5ec..0f399cdc6 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -82,33 +82,43 @@ lanczos_filter(double x) { static inline double mks_2013_filter(double x) { /* https://johncostella.com/magic/ */ - if (x < 0.0) + if (x < 0) { x = -x; - if (x < 0.5) - return 17.0 / 16.0 - 7.0 / 4.0 * pow(x, 2.0); - if (x < 1.5) + } + if (x < 0.5) { + return 17.0 / 16.0 - 7.0 / 4.0 * pow(x, 2); + } + if (x < 1.5) { return (1.0 - x) * (7.0 / 4.0 - x); - if (x < 2.5) - return -1.0 / 8.0 * pow(x - 5.0 / 2.0, 2.0); - return (0.0); + } + if (x < 2.5) { + return -1.0 / 8.0 * pow(x - 5.0 / 2.0, 2); + } + return 0; } static inline double mks_2021_filter(double x) { /* https://johncostella.com/magic/ */ - if (x < 0.0) + if (x < 0) { x = -x; - if (x < 0.5) - return 577.0 / 576.0 - 239.0 / 144.0 * pow(x, 2.0); - if (x < 1.5) + } + if (x < 0.5) { + return 577.0 / 576.0 - 239.0 / 144.0 * pow(x, 2); + } + if (x < 1.5) { return 35.0 / 36.0 * (x - 1.0) * (x - 239.0 / 140.0); - if (x < 2.5) + } + if (x < 2.5) { return 1.0 / 6.0 * (x - 2.0) * (65.0 / 24.0 - x); - if (x < 3.5) + } + if (x < 3.5) { return 1.0 / 36.0 * (x - 3.0) * (x - 15.0 / 4.0); - if (x < 4.5) - return -1.0 / 288.0 * pow(x - 9.0 / 2.0, 2.0); - return (0.0); + } + if (x < 4.5) { + return -1.0 / 288.0 * pow(x - 9.0 / 2.0, 2); + } + return 0; } static struct filter BOX = {box_filter, 0.5}; From 6530c9c05537200d0948a35d40a5eded7c920ac7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 14 Apr 2025 19:20:06 +1000 Subject: [PATCH 7/7] Added additional tests --- Tests/test_image_resample.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 6acb9de23..fc22991f7 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -165,6 +165,32 @@ class TestImagingCoreResampleAccuracy: for channel in case.split(): self.check_case(channel, self.make_sample(data, (8, 8))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_mks2013(self, mode: str) -> None: + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.MKS2013) + # fmt: off + data = ("e1 e1 e9 dc" + "e1 e1 e9 dc" + "e9 e9 f1 e3" + "dc dc e4 d8") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_reduce_mks2021(self, mode: str) -> None: + case = self.make_case(mode, (16, 16), 0xE1) + case = case.resize((8, 8), Image.Resampling.MKS2021) + # fmt: off + data = ("e1 e1 e3 d7" + "e1 e1 e3 d7" + "e3 e3 e5 d9" + "d7 d7 d9 ce") + # fmt: on + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (8, 8))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) @@ -226,6 +252,36 @@ class TestImagingCoreResampleAccuracy: for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_mks2013(self, mode: str) -> None: + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.MKS2013) + data = ( + "e1 e1 e2 ef fb be" + "e1 e1 e2 ef fb be" + "e2 e2 e3 f1 fd bf" + "ef ef f0 ff ff c7" + "fb fb fc ff ff cf" + "be be bf c7 cf a8" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) + + @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) + def test_enlarge_mks2021(self, mode: str) -> None: + case = self.make_case(mode, (6, 6), 0xE1) + case = case.resize((12, 12), Image.Resampling.MKS2021) + data = ( + "e3 e1 df e9 f5 bb" + "e1 df dd e7 f3 b9" + "df dd db e5 f1 b8" + "e9 e7 e5 ef fc be" + "f5 f3 f0 fc ff c5" + "bb ba b8 bf c6 a3" + ) + for channel in case.split(): + self.check_case(channel, self.make_sample(data, (12, 12))) + def test_box_filter_correct_range(self) -> None: im = Image.new("RGB", (8, 8), "#1688ff").resize( (100, 100), Image.Resampling.BOX