Skip to content

Commit bd6e80c

Browse files
committed
Process image band by band
1 parent 4790936 commit bd6e80c

File tree

2 files changed

+41
-66
lines changed

2 files changed

+41
-66
lines changed

Tests/test_imageops.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -606,35 +606,27 @@ def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None:
606606
assert_image_equal(img, out)
607607

608608

609-
def test_dither_primary_returns_image() -> None:
610-
im = Image.new("RGB", (4, 4), (128, 128, 128))
609+
@pytest.mark.parametrize("size", (2, 4))
610+
def test_dither_primary(size: int) -> None:
611+
im = Image.new("RGB", (size, size), (200, 100, 50))
611612
out = ImageOps.dither_primary(im)
612613

613614
assert isinstance(out, Image.Image)
614-
assert out.size == im.size
615615
assert out.mode == "RGB"
616+
assert out.size == (size, size)
616617

617-
618-
def test_dither_primary_uses_only_primary_colors() -> None:
619-
im = Image.new("RGB", (4, 4), (200, 100, 50))
620-
out = ImageOps.dither_primary(im)
621-
622-
px = out.load()
623-
assert px is not None
624-
625-
for x in range(out.width):
626-
for y in range(out.height):
627-
value = px[x, y]
618+
for x in range(size):
619+
for y in range(size):
620+
value = out.getpixel((x, y))
628621
assert isinstance(value, tuple)
629622

630-
r, g, b = value
631-
assert r in (0, 255)
632-
assert g in (0, 255)
633-
assert b in (0, 255)
623+
assert all(v in (0, 255) for v in value)
634624

635625

636-
def test_dither_primary_small_image() -> None:
637-
im = Image.new("RGB", (2, 2), (255, 0, 0))
626+
def test_dither_primary_non_rgb() -> None:
627+
im = Image.new("L", (2, 2))
638628
out = ImageOps.dither_primary(im)
639629

640-
assert out.size == (2, 2)
630+
for x in range(2):
631+
for y in range(2):
632+
assert out.getpixel((x, y)) in (0, 255)

src/PIL/ImageOps.py

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -669,52 +669,35 @@ def dither_primary(image: Image.Image) -> Image.Image:
669669
"""
670670
if image.mode != "RGB":
671671
image = image.convert("RGB")
672-
width, height = image.size
673-
674-
src = image.load()
675-
out = Image.new("RGB", (width, height))
676-
dst = out.load()
677-
678-
# Step 1: primary color reduction
679-
assert src is not None
680-
for x in range(width):
681-
for y in range(height):
682-
px = src[x, y]
683-
assert isinstance(px, tuple)
684-
r, g, b = px
685-
src[x, y] = (
686-
255 if r > 127 else 0,
687-
255 if g > 127 else 0,
688-
255 if b > 127 else 0,
689-
)
690-
691-
# Step 2: ordered dithering (2x2 blocks)
692-
assert dst is not None
693-
for x in range(0, width - 1, 2):
694-
for y in range(0, height - 1, 2):
695-
p1 = src[x, y]
696-
p2 = src[x, y + 1]
697-
p3 = src[x + 1, y]
698-
p4 = src[x + 1, y + 1]
699-
700-
assert isinstance(p1, tuple)
701-
assert isinstance(p2, tuple)
702-
assert isinstance(p3, tuple)
703-
assert isinstance(p4, tuple)
704-
red = (p1[0] + p2[0] + p3[0] + p4[0]) / 4
705-
green = (p1[1] + p2[1] + p3[1] + p4[1]) / 4
706-
blue = (p1[2] + p2[2] + p3[2] + p4[2]) / 4
707-
708-
r1 = [_dither_saturation(red, q) for q in range(4)]
709-
g1 = [_dither_saturation(green, q) for q in range(4)]
710-
b1 = [_dither_saturation(blue, q) for q in range(4)]
711-
712-
dst[x, y] = (r1[0], g1[0], b1[0])
713-
dst[x, y + 1] = (r1[1], g1[1], b1[1])
714-
dst[x + 1, y] = (r1[2], g1[2], b1[2])
715-
dst[x + 1, y + 1] = (r1[3], g1[3], b1[3])
716672

717-
return out
673+
bands = []
674+
for band in image.split():
675+
# Step 1: primary color reduction
676+
band = band.point(lambda x: 255 if x > 127 else 0)
677+
bands.append(band)
678+
679+
# Step 2: ordered dithering (2x2 blocks)
680+
px = band.load()
681+
assert px is not None
682+
for x in range(0, band.width - 1, 2):
683+
for y in range(0, band.height - 1, 2):
684+
p1 = px[x, y]
685+
p2 = px[x, y + 1]
686+
p3 = px[x + 1, y]
687+
p4 = px[x + 1, y + 1]
688+
689+
assert isinstance(p1, (int, float))
690+
assert isinstance(p2, (int, float))
691+
assert isinstance(p3, (int, float))
692+
assert isinstance(p4, (int, float))
693+
694+
value = (p1 + p2 + p3 + p4) / 4
695+
696+
px[x, y] = _dither_saturation(value, 0)
697+
px[x, y + 1] = _dither_saturation(value, 1)
698+
px[x + 1, y] = _dither_saturation(value, 2)
699+
px[x + 1, y + 1] = _dither_saturation(value, 3)
700+
return Image.merge("RGB", bands)
718701

719702

720703
def posterize(image: Image.Image, bits: int) -> Image.Image:

0 commit comments

Comments
 (0)