mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-09-24 13:07:00 +03:00
Add the magic kernel sharp 2021 resampling filter
This commit is contained in:
parent
97a4d1f593
commit
6216187fb6
|
@ -165,6 +165,21 @@ 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_magic_kernel_sharp_2021(self, mode: str) -> None:
|
||||
case = self.make_case(mode, (20, 20), 0xE1)
|
||||
case = case.resize((10, 10), Image.Resampling.MAGIC_KERNEL_SHARP_2021)
|
||||
# fmt: off
|
||||
data = ("e1 e1 e1 e3 d7"
|
||||
"e1 e1 e1 e3 d7"
|
||||
"e1 e1 e1 e3 d7"
|
||||
"e3 e3 e3 e5 d9"
|
||||
"d7 d7 d7 d9 ce")
|
||||
# fmt: on
|
||||
for channel in case.split():
|
||||
self.check_case(channel, self.make_sample(data, (10, 10)))
|
||||
|
||||
|
||||
@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 +241,23 @@ 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_magic_kernel_sharp_2021(self, mode: str) -> None:
|
||||
case = self.make_case(mode, (8, 8), 0xE1)
|
||||
case = case.resize((16, 16), Image.Resampling.MAGIC_KERNEL_SHARP_2021)
|
||||
# fmt: off
|
||||
data = ("e1 e1 e2 e0 de e8 f4 ba"
|
||||
"e1 e1 e2 e0 de e8 f4 ba"
|
||||
"e2 e2 e3 e1 df e9 f5 ba"
|
||||
"e0 e0 e1 df dd e7 f3 b9"
|
||||
"de de df dd db e5 f0 b8"
|
||||
"e8 e8 e9 e7 e5 ef fc be"
|
||||
"f4 f4 f5 f2 f0 fc ff c5"
|
||||
"ba ba bb ba b9 bf c6 a3")
|
||||
# fmt: on
|
||||
for channel in case.split():
|
||||
self.check_case(channel, self.make_sample(data, (16, 16)))
|
||||
|
||||
def test_box_filter_correct_range(self) -> None:
|
||||
im = Image.new("RGB", (8, 8), "#1688ff").resize(
|
||||
(100, 100), Image.Resampling.BOX
|
||||
|
@ -309,6 +341,7 @@ 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.MAGIC_KERNEL_SHARP_2021))
|
||||
|
||||
@pytest.mark.xfail(reason="Current implementation isn't precise enough")
|
||||
def test_levels_la(self) -> None:
|
||||
|
@ -318,6 +351,7 @@ 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.MAGIC_KERNEL_SHARP_2021))
|
||||
|
||||
def make_dirty_case(
|
||||
self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...]
|
||||
|
@ -360,6 +394,9 @@ 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.MAGIC_KERNEL_SHARP_2021), (255, 255, 0)
|
||||
)
|
||||
|
||||
def test_dirty_pixels_la(self) -> None:
|
||||
case = self.make_dirty_case("LA", (255, 128), (0, 0))
|
||||
|
@ -368,6 +405,7 @@ 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.MAGIC_KERNEL_SHARP_2021), (255,))
|
||||
|
||||
|
||||
class TestCoreResamplePasses:
|
||||
|
@ -453,6 +491,7 @@ class TestCoreResampleBox:
|
|||
Image.Resampling.HAMMING,
|
||||
Image.Resampling.BICUBIC,
|
||||
Image.Resampling.LANCZOS,
|
||||
Image.Resampling.MAGIC_KERNEL_SHARP_2021,
|
||||
),
|
||||
)
|
||||
def test_wrong_arguments(self, resample: Image.Resampling) -> None:
|
||||
|
|
|
@ -72,6 +72,7 @@ class TestImagingCoreResize:
|
|||
Image.Resampling.HAMMING,
|
||||
Image.Resampling.BICUBIC,
|
||||
Image.Resampling.LANCZOS,
|
||||
Image.Resampling.MAGIC_KERNEL_SHARP_2021,
|
||||
),
|
||||
)
|
||||
def test_reduce_filters(self, resample: Image.Resampling) -> None:
|
||||
|
@ -88,6 +89,7 @@ class TestImagingCoreResize:
|
|||
Image.Resampling.HAMMING,
|
||||
Image.Resampling.BICUBIC,
|
||||
Image.Resampling.LANCZOS,
|
||||
Image.Resampling.MAGIC_KERNEL_SHARP_2021,
|
||||
),
|
||||
)
|
||||
def test_enlarge_filters(self, resample: Image.Resampling) -> None:
|
||||
|
@ -104,6 +106,7 @@ class TestImagingCoreResize:
|
|||
Image.Resampling.HAMMING,
|
||||
Image.Resampling.BICUBIC,
|
||||
Image.Resampling.LANCZOS,
|
||||
Image.Resampling.MAGIC_KERNEL_SHARP_2021,
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -217,23 +217,36 @@ pixel, the Python Imaging Library provides different resampling *filters*.
|
|||
|
||||
.. versionadded:: 1.1.3
|
||||
|
||||
.. data:: Resampling.MAGIC_KERNEL_SHARP_2021
|
||||
:noindex:
|
||||
|
||||
A high-quality sharpening filter designed by John Costella, known as the
|
||||
'Magic Kernel'. It is engineered to produce sharp results with minimal
|
||||
resampling artifacts like ringing and aliasing.
|
||||
This filter can only be used with the :py:meth:`~PIL.Image.Image.resize`
|
||||
and :py:meth:`~PIL.Image.Image.thumbnail` methods.
|
||||
|
||||
.. versionadded:: 11.4.0
|
||||
|
||||
|
||||
Filters comparison table
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
| Filter | Downscaling | Upscaling | Performance |
|
||||
| | quality | quality | |
|
||||
+===========================+=============+===========+=============+
|
||||
|:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
|:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
|:data:`Resampling.BILINEAR`| ⭐ | ⭐ | ⭐⭐⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
|:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
|:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
|:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
|
||||
+---------------------------+-------------+-----------+-------------+
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
| Filter | Downscaling | Upscaling | Performance |
|
||||
| | quality | quality | |
|
||||
+===========================================+=============+============+=============+
|
||||
|:data:`Resampling.NEAREST` | | | ⭐⭐⭐⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.BOX` | ⭐ | | ⭐⭐⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.BILINEAR` | ⭐ | ⭐ | ⭐⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.HAMMING` | ⭐⭐ | | ⭐⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.BICUBIC` | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.LANCZOS` | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|:data:`Resampling.MAGIC_KERNEL_SHARP_2021` | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
|
||||
+-------------------------------------------+-------------+------------+-------------+
|
||||
|
|
|
@ -151,6 +151,7 @@ class Resampling(IntEnum):
|
|||
HAMMING = 5
|
||||
BICUBIC = 3
|
||||
LANCZOS = 1
|
||||
MAGIC_KERNEL_SHARP_2021 = 6
|
||||
|
||||
|
||||
_filters_support = {
|
||||
|
@ -159,6 +160,7 @@ _filters_support = {
|
|||
Resampling.HAMMING: 1.0,
|
||||
Resampling.BICUBIC: 2.0,
|
||||
Resampling.LANCZOS: 3.0,
|
||||
Resampling.MAGIC_KERNEL_SHARP_2021: 4.5,
|
||||
}
|
||||
|
||||
|
||||
|
@ -2215,10 +2217,11 @@ 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`.
|
||||
If the image has mode "1" or "P", it is always set to
|
||||
:py:data:`Resampling.NEAREST`. Otherwise, the default filter is
|
||||
:py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`.
|
||||
:py:data:`Resampling.BICUBIC`, :py:data:`Resampling.LANCZOS` or
|
||||
:py:data:`Resampling.MAGIC_KERNEL_SHARP_2021`. If the image has mode
|
||||
"1" or "P", it is always set to :py:data:`Resampling.NEAREST`.
|
||||
Otherwise, the default filter is :py:data:`Resampling.BICUBIC`. See:
|
||||
:ref:`concept-filters`.
|
||||
:param box: An optional 4-tuple of floats providing
|
||||
the source image region to be scaled.
|
||||
The values must be within (0, 0, width, height) rectangle.
|
||||
|
@ -2245,6 +2248,7 @@ class Image:
|
|||
Resampling.BILINEAR,
|
||||
Resampling.BICUBIC,
|
||||
Resampling.LANCZOS,
|
||||
Resampling.MAGIC_KERNEL_SHARP_2021,
|
||||
Resampling.BOX,
|
||||
Resampling.HAMMING,
|
||||
):
|
||||
|
@ -2255,6 +2259,7 @@ class Image:
|
|||
for filter in (
|
||||
(Resampling.NEAREST, "Image.Resampling.NEAREST"),
|
||||
(Resampling.LANCZOS, "Image.Resampling.LANCZOS"),
|
||||
(Resampling.MAGIC_KERNEL_SHARP_2021, "Image.Resampling.MAGIC_KERNEL_SHARP_2021"),
|
||||
(Resampling.BILINEAR, "Image.Resampling.BILINEAR"),
|
||||
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
|
||||
(Resampling.BOX, "Image.Resampling.BOX"),
|
||||
|
@ -2710,10 +2715,11 @@ 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`.
|
||||
If omitted, it defaults to :py:data:`Resampling.BICUBIC`.
|
||||
(was :py:data:`Resampling.NEAREST` prior to version 2.5.0).
|
||||
See: :ref:`concept-filters`.
|
||||
:py:data:`Resampling.BICUBIC`, :py:data:`Resampling.LANCZOS` or
|
||||
:py:data:`Resampling.MAGIC_KERNEL_SHARP_2021`. If omitted, it
|
||||
defaults to :py:data:`Resampling.BICUBIC`. (was
|
||||
:py:data:`Resampling.NEAREST` prior to version 2.5.0). See:
|
||||
:ref:`concept-filters`.
|
||||
:param reducing_gap: Apply optimization by resizing the image
|
||||
in two steps. First, reducing the image by integer times
|
||||
using :py:meth:`~PIL.Image.Image.reduce` or
|
||||
|
@ -2924,11 +2930,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.MAGIC_KERNEL_SHARP_2021):
|
||||
unusable: dict[int, str] = {
|
||||
Resampling.BOX: "Image.Resampling.BOX",
|
||||
Resampling.HAMMING: "Image.Resampling.HAMMING",
|
||||
Resampling.LANCZOS: "Image.Resampling.LANCZOS",
|
||||
Resampling.MAGIC_KERNEL_SHARP_2021: "Image.Resampling.MAGIC_KERNEL_SHARP_2021",
|
||||
}
|
||||
msg = unusable[resample] + f" ({resample}) cannot be used."
|
||||
else:
|
||||
|
|
|
@ -294,6 +294,7 @@ ImagingError_ValueError(const char *message);
|
|||
#define IMAGING_TRANSFORM_HAMMING 5
|
||||
#define IMAGING_TRANSFORM_BICUBIC 3
|
||||
#define IMAGING_TRANSFORM_LANCZOS 1
|
||||
#define IMAGING_TRANSFORM_MAGIC_KERNEL_SHARP_2021 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);
|
||||
|
|
|
@ -79,11 +79,33 @@ lanczos_filter(double x) {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
static inline double
|
||||
magic_kernel_sharp_2021_filter(double x) {
|
||||
x = fabs(x);
|
||||
if (x < 0.5) {
|
||||
return 577.0 / 576.0 - 239.0 / 144.0 * x *x;
|
||||
}
|
||||
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 - 3.75);
|
||||
}
|
||||
if (x < 4.5) {
|
||||
return -1.0 / 288.0 * (x - 4.5) * (x - 4.5);
|
||||
}
|
||||
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 MAGIC_KERNEL_SHARP_2021 = {magic_kernel_sharp_2021_filter, 4.5};
|
||||
|
||||
/* 8 bits for result. Filter can have negative areas.
|
||||
In one cases the sum of the coefficients will be negative,
|
||||
|
@ -695,6 +717,9 @@ ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]) {
|
|||
case IMAGING_TRANSFORM_LANCZOS:
|
||||
filterp = &LANCZOS;
|
||||
break;
|
||||
case IMAGING_TRANSFORM_MAGIC_KERNEL_SHARP_2021:
|
||||
filterp = &MAGIC_KERNEL_SHARP_2021;
|
||||
break;
|
||||
default:
|
||||
return (Imaging)ImagingError_ValueError("unsupported resampling filter");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user