Merge f58097755e into 877527cefc
This commit is contained in:
commit
d44cab3871
@ -611,3 +611,20 @@ def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
|
||||
img, cutoff=10, preserve_tone=True
|
||||
) # single color 10 cutoff
|
||||
assert_image_equal(img, out)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", (2, 4))
|
||||
def test_dither_primary(size: int) -> None:
|
||||
im = Image.new("RGB", (size, size), (200, 100, 50))
|
||||
out = ImageOps.dither_primary(im)
|
||||
|
||||
expected = Image.new("RGB", (size, size), (255, 0, 0))
|
||||
assert_image_equal(out, expected)
|
||||
|
||||
|
||||
def test_dither_primary_non_rgb() -> None:
|
||||
im = Image.new("L", (2, 2), 100)
|
||||
out = ImageOps.dither_primary(im)
|
||||
|
||||
expected = Image.new("RGB", (2, 2))
|
||||
assert_image_equal(out, expected)
|
||||
|
||||
@ -13,6 +13,7 @@ only work on L and RGB images.
|
||||
.. autofunction:: autocontrast
|
||||
.. autofunction:: colorize
|
||||
.. autofunction:: crop
|
||||
.. autofunction:: dither_primary
|
||||
.. autofunction:: scale
|
||||
.. autoclass:: SupportsGetMesh
|
||||
:show-inheritance:
|
||||
|
||||
@ -647,6 +647,62 @@ def mirror(image: Image.Image) -> Image.Image:
|
||||
return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
|
||||
|
||||
def _dither_saturation(value: float, quadrant: int) -> int:
|
||||
if value > 233:
|
||||
return 255
|
||||
if value > 159:
|
||||
return 255 if quadrant != 1 else 0
|
||||
if value > 95:
|
||||
return 255 if quadrant in (0, 3) else 0
|
||||
if value > 32:
|
||||
return 255 if quadrant == 1 else 0
|
||||
return 0
|
||||
|
||||
|
||||
def dither_primary(image: Image.Image) -> Image.Image:
|
||||
"""
|
||||
Reduce the image to primary colors and apply ordered dithering.
|
||||
|
||||
This operation first reduces each RGB channel to its primary values
|
||||
(0 or 255), then applies a 2x2 ordered dithering pattern based on the
|
||||
average color intensity.
|
||||
|
||||
:param image: The image to process.
|
||||
:return: An image.
|
||||
"""
|
||||
if image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
|
||||
bands = []
|
||||
for band in image.split():
|
||||
# Step 1: primary color reduction
|
||||
band = band.point(lambda x: 255 if x > 127 else 0)
|
||||
bands.append(band)
|
||||
|
||||
# Step 2: ordered dithering (2x2 blocks)
|
||||
px = band.load()
|
||||
assert px is not None
|
||||
for x in range(0, band.width - 1, 2):
|
||||
for y in range(0, band.height - 1, 2):
|
||||
p1 = px[x, y]
|
||||
p2 = px[x, y + 1]
|
||||
p3 = px[x + 1, y]
|
||||
p4 = px[x + 1, y + 1]
|
||||
|
||||
assert isinstance(p1, (int, float))
|
||||
assert isinstance(p2, (int, float))
|
||||
assert isinstance(p3, (int, float))
|
||||
assert isinstance(p4, (int, float))
|
||||
|
||||
value = (p1 + p2 + p3 + p4) / 4
|
||||
|
||||
px[x, y] = _dither_saturation(value, 0)
|
||||
px[x, y + 1] = _dither_saturation(value, 1)
|
||||
px[x + 1, y] = _dither_saturation(value, 2)
|
||||
px[x + 1, y + 1] = _dither_saturation(value, 3)
|
||||
return Image.merge("RGB", bands)
|
||||
|
||||
|
||||
def posterize(image: Image.Image, bits: int) -> Image.Image:
|
||||
"""
|
||||
Reduce the number of bits for each color channel.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user