33from __future__ import annotations
44
55from dataclasses import dataclass
6- from typing import cast
76
87import numpy as np
98from PIL import Image
@@ -47,48 +46,63 @@ class ErrorDiffusionKernel:
4746 name = "Floyd-Steinberg" ,
4847 divisor = 16 ,
4948 offsets = [
50- (1 , 0 , 7 ), # Right: 7/16
49+ (1 , 0 , 7 ), # Right: 7/16
5150 (- 1 , 1 , 3 ), # Down-left: 3/16
52- (0 , 1 , 5 ), # Down: 5/16
53- (1 , 1 , 1 ), # Down-right: 1/16
51+ (0 , 1 , 5 ), # Down: 5/16
52+ (1 , 1 , 1 ), # Down-right: 1/16
5453 ],
5554)
5655
5756BURKES = ErrorDiffusionKernel (
5857 name = "Burkes" ,
5958 divisor = 32 ,
6059 offsets = [
61- (1 , 0 , 8 ), (2 , 0 , 4 ), # Current row
62- (- 2 , 1 , 2 ), (- 1 , 1 , 4 ), (0 , 1 , 8 ), (1 , 1 , 4 ), (2 , 1 , 2 ), # Next row
60+ (1 , 0 , 8 ),
61+ (2 , 0 , 4 ), # Current row
62+ (- 2 , 1 , 2 ),
63+ (- 1 , 1 , 4 ),
64+ (0 , 1 , 8 ),
65+ (1 , 1 , 4 ),
66+ (2 , 1 , 2 ), # Next row
6367 ],
6468)
6569
6670SIERRA = ErrorDiffusionKernel (
6771 name = "Sierra" ,
6872 divisor = 32 ,
6973 offsets = [
70- (1 , 0 , 5 ), (2 , 0 , 3 ), # Current row
71- (- 2 , 1 , 2 ), (- 1 , 1 , 4 ), (0 , 1 , 5 ), (1 , 1 , 4 ), (2 , 1 , 2 ), # Row +1
72- (- 1 , 2 , 2 ), (0 , 2 , 3 ), (1 , 2 , 2 ), # Row +2
74+ (1 , 0 , 5 ),
75+ (2 , 0 , 3 ), # Current row
76+ (- 2 , 1 , 2 ),
77+ (- 1 , 1 , 4 ),
78+ (0 , 1 , 5 ),
79+ (1 , 1 , 4 ),
80+ (2 , 1 , 2 ), # Row +1
81+ (- 1 , 2 , 2 ),
82+ (0 , 2 , 3 ),
83+ (1 , 2 , 2 ), # Row +2
7384 ],
7485)
7586
7687SIERRA_LITE = ErrorDiffusionKernel (
7788 name = "Sierra Lite" ,
7889 divisor = 4 ,
7990 offsets = [
80- (1 , 0 , 2 ), # Right: 2/4
91+ (1 , 0 , 2 ), # Right: 2/4
8192 (- 1 , 1 , 1 ), # Down-left: 1/4
82- (0 , 1 , 1 ), # Down: 1/4
93+ (0 , 1 , 1 ), # Down: 1/4
8394 ],
8495)
8596
8697ATKINSON = ErrorDiffusionKernel (
8798 name = "Atkinson" ,
8899 divisor = 8 ,
89100 offsets = [
90- (1 , 0 , 1 ), (2 , 0 , 1 ), # Current row
91- (- 1 , 1 , 1 ), (0 , 1 , 1 ), (1 , 1 , 1 ), # Row +1
101+ (1 , 0 , 1 ),
102+ (2 , 0 , 1 ), # Current row
103+ (- 1 , 1 , 1 ),
104+ (0 , 1 , 1 ),
105+ (1 , 1 , 1 ), # Row +1
92106 (0 , 2 , 1 ), # Row +2
93107 ],
94108)
@@ -97,29 +111,42 @@ class ErrorDiffusionKernel:
97111 name = "Stucki" ,
98112 divisor = 42 ,
99113 offsets = [
100- (1 , 0 , 8 ), (2 , 0 , 4 ), # Current row
101- (- 2 , 1 , 2 ), (- 1 , 1 , 4 ), (0 , 1 , 8 ), (1 , 1 , 4 ), (2 , 1 , 2 ), # Row +1
102- (- 2 , 2 , 1 ), (- 1 , 2 , 2 ), (0 , 2 , 4 ), (1 , 2 , 2 ), (2 , 2 , 1 ), # Row +2
114+ (1 , 0 , 8 ),
115+ (2 , 0 , 4 ), # Current row
116+ (- 2 , 1 , 2 ),
117+ (- 1 , 1 , 4 ),
118+ (0 , 1 , 8 ),
119+ (1 , 1 , 4 ),
120+ (2 , 1 , 2 ), # Row +1
121+ (- 2 , 2 , 1 ),
122+ (- 1 , 2 , 2 ),
123+ (0 , 2 , 4 ),
124+ (1 , 2 , 2 ),
125+ (2 , 2 , 1 ), # Row +2
103126 ],
104127)
105128
106129JARVIS_JUDICE_NINKE = ErrorDiffusionKernel (
107130 name = "Jarvis-Judice-Ninke" ,
108131 divisor = 48 ,
109132 offsets = [
110- (1 , 0 , 7 ), (2 , 0 , 5 ), # Current row
111- (- 2 , 1 , 3 ), (- 1 , 1 , 5 ), (0 , 1 , 7 ), (1 , 1 , 5 ), (2 , 1 , 3 ), # Row +1
112- (- 2 , 2 , 1 ), (- 1 , 2 , 3 ), (0 , 2 , 5 ), (1 , 2 , 3 ), (2 , 2 , 1 ), # Row +2
133+ (1 , 0 , 7 ),
134+ (2 , 0 , 5 ), # Current row
135+ (- 2 , 1 , 3 ),
136+ (- 1 , 1 , 5 ),
137+ (0 , 1 , 7 ),
138+ (1 , 1 , 5 ),
139+ (2 , 1 , 3 ), # Row +1
140+ (- 2 , 2 , 1 ),
141+ (- 1 , 2 , 3 ),
142+ (0 , 2 , 5 ),
143+ (1 , 2 , 3 ),
144+ (2 , 2 , 1 ), # Row +2
113145 ],
114146)
115147
116148# Bayer 4x4 matrix normalized to [-0.5, 0.5] (centered around 0)
117- _BAYER_4X4 = (
118- np .array ([[0 , 8 , 2 , 10 ], [12 , 4 , 14 , 6 ], [3 , 11 , 1 , 9 ], [15 , 7 , 13 , 5 ]],
119- dtype = np .float32 )
120- / 16.0
121- - 0.5
122- )
149+ _BAYER_4X4 = np .array ([[0 , 8 , 2 , 10 ], [12 , 4 , 14 , 6 ], [3 , 11 , 1 , 9 ], [15 , 7 , 13 , 5 ]], dtype = np .float32 ) / 16.0 - 0.5
123150
124151
125152def get_palette_colors (color_scheme : ColorScheme | ColorPalette ) -> list [tuple [int , int , int ]]:
@@ -195,8 +222,8 @@ def error_diffusion_dither(
195222 if isinstance (color_scheme , ColorPalette ) and tone_compression != 0 :
196223 if tone_compression == "auto" :
197224 pixels_linear = auto_compress_dynamic_range (pixels_linear , palette_linear )
198- else :
199- pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , cast ( float , tone_compression ) )
225+ elif isinstance ( tone_compression , float ) :
226+ pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , tone_compression )
200227
201228 # Pre-compute palette LAB components for scalar per-pixel matching
202229 palette_L , palette_a , palette_b , palette_C = precompute_palette_lab (palette_linear )
@@ -274,8 +301,10 @@ def error_diffusion_dither(
274301
275302
276303def floyd_steinberg_dither (
277- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
278- serpentine : bool = True , tone_compression : float | str = "auto" ,
304+ image : Image .Image ,
305+ color_scheme : ColorScheme | ColorPalette ,
306+ serpentine : bool = True ,
307+ tone_compression : float | str = "auto" ,
279308) -> Image .Image :
280309 """Apply Floyd-Steinberg error diffusion dithering.
281310
@@ -298,8 +327,10 @@ def floyd_steinberg_dither(
298327
299328
300329def burkes_dither (
301- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
302- serpentine : bool = True , tone_compression : float | str = "auto" ,
330+ image : Image .Image ,
331+ color_scheme : ColorScheme | ColorPalette ,
332+ serpentine : bool = True ,
333+ tone_compression : float | str = "auto" ,
303334) -> Image .Image :
304335 """Apply Burkes error diffusion dithering.
305336
@@ -320,8 +351,10 @@ def burkes_dither(
320351
321352
322353def sierra_dither (
323- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
324- serpentine : bool = True , tone_compression : float | str = "auto" ,
354+ image : Image .Image ,
355+ color_scheme : ColorScheme | ColorPalette ,
356+ serpentine : bool = True ,
357+ tone_compression : float | str = "auto" ,
325358) -> Image .Image :
326359 """Apply Sierra error diffusion dithering.
327360
@@ -345,8 +378,10 @@ def sierra_dither(
345378
346379
347380def sierra_lite_dither (
348- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
349- serpentine : bool = True , tone_compression : float | str = "auto" ,
381+ image : Image .Image ,
382+ color_scheme : ColorScheme | ColorPalette ,
383+ serpentine : bool = True ,
384+ tone_compression : float | str = "auto" ,
350385) -> Image .Image :
351386 """Apply Sierra Lite error diffusion dithering.
352387
@@ -369,8 +404,10 @@ def sierra_lite_dither(
369404
370405
371406def atkinson_dither (
372- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
373- serpentine : bool = True , tone_compression : float | str = "auto" ,
407+ image : Image .Image ,
408+ color_scheme : ColorScheme | ColorPalette ,
409+ serpentine : bool = True ,
410+ tone_compression : float | str = "auto" ,
374411) -> Image .Image :
375412 """Apply Atkinson error diffusion dithering.
376413
@@ -394,8 +431,10 @@ def atkinson_dither(
394431
395432
396433def stucki_dither (
397- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
398- serpentine : bool = True , tone_compression : float | str = "auto" ,
434+ image : Image .Image ,
435+ color_scheme : ColorScheme | ColorPalette ,
436+ serpentine : bool = True ,
437+ tone_compression : float | str = "auto" ,
399438) -> Image .Image :
400439 """Apply Stucki error diffusion dithering.
401440
@@ -419,8 +458,10 @@ def stucki_dither(
419458
420459
421460def jarvis_judice_ninke_dither (
422- image : Image .Image , color_scheme : ColorScheme | ColorPalette ,
423- serpentine : bool = True , tone_compression : float | str = "auto" ,
461+ image : Image .Image ,
462+ color_scheme : ColorScheme | ColorPalette ,
463+ serpentine : bool = True ,
464+ tone_compression : float | str = "auto" ,
424465) -> Image .Image :
425466 """Apply Jarvis-Judice-Ninke error diffusion dithering.
426467
@@ -449,7 +490,9 @@ def jarvis_judice_ninke_dither(
449490
450491
451492def direct_palette_map (
452- image : Image .Image , color_scheme : ColorScheme | ColorPalette , tone_compression : float | str = "auto" ,
493+ image : Image .Image ,
494+ color_scheme : ColorScheme | ColorPalette ,
495+ tone_compression : float | str = "auto" ,
453496) -> Image .Image :
454497 """Map image colors directly to palette without dithering.
455498
@@ -485,8 +528,8 @@ def direct_palette_map(
485528 if isinstance (color_scheme , ColorPalette ) and tone_compression != 0 :
486529 if tone_compression == "auto" :
487530 pixels_linear = auto_compress_dynamic_range (pixels_linear , palette_linear )
488- else :
489- pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , cast ( float , tone_compression ) )
531+ elif isinstance ( tone_compression , float ) :
532+ pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , tone_compression )
490533
491534 # Find closest palette color for ALL pixels at once using LAB
492535 output_pixels = find_closest_palette_color_lab (pixels_linear , palette_linear )
@@ -501,7 +544,9 @@ def direct_palette_map(
501544
502545
503546def ordered_dither (
504- image : Image .Image , color_scheme : ColorScheme | ColorPalette , tone_compression : float | str = "auto" ,
547+ image : Image .Image ,
548+ color_scheme : ColorScheme | ColorPalette ,
549+ tone_compression : float | str = "auto" ,
505550) -> Image .Image :
506551 """Apply ordered (Bayer) dithering with full vectorization.
507552
@@ -528,10 +573,7 @@ def ordered_dither(
528573 """
529574 # Bayer 4x4 matrix normalized to [-0.5, 0.5] (centered around 0)
530575 bayer_matrix = (
531- np .array ([[0 , 8 , 2 , 10 ], [12 , 4 , 14 , 6 ], [3 , 11 , 1 , 9 ], [15 , 7 , 13 , 5 ]],
532- dtype = np .float32 )
533- / 16.0
534- - 0.5
576+ np .array ([[0 , 8 , 2 , 10 ], [12 , 4 , 14 , 6 ], [3 , 11 , 1 , 9 ], [15 , 7 , 13 , 5 ]], dtype = np .float32 ) / 16.0 - 0.5
535577 )
536578
537579 # ===== Image Preprocessing =====
@@ -555,14 +597,14 @@ def ordered_dither(
555597 if isinstance (color_scheme , ColorPalette ) and tone_compression != 0 :
556598 if tone_compression == "auto" :
557599 pixels_linear = auto_compress_dynamic_range (pixels_linear , palette_linear )
558- else :
559- pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , cast ( float , tone_compression ) )
600+ elif isinstance ( tone_compression , float ) :
601+ pixels_linear = compress_dynamic_range (pixels_linear , palette_linear , tone_compression )
560602
561603 # ===== VECTORIZED ORDERED DITHERING =====
562604
563605 # Create threshold matrix for entire image using broadcasting
564606 y_indices = np .arange (height )[:, np .newaxis ] % 4 # Shape: (height, 1)
565- x_indices = np .arange (width )[np .newaxis , :] % 4 # Shape: (1, width)
607+ x_indices = np .arange (width )[np .newaxis , :] % 4 # Shape: (1, width)
566608 threshold_matrix = bayer_matrix [y_indices , x_indices ] # Shape: (height, width)
567609
568610 # Add threshold to all pixels at once
0 commit comments