Lint
This commit is contained in:
parent
9a009f0b4c
commit
eed1567f7f
12 changed files with 463 additions and 243 deletions
|
|
@ -6,22 +6,20 @@ for comparison testing.
|
|||
"""
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from typing import Tuple, List
|
||||
|
||||
from numba import jit
|
||||
from PIL import Image
|
||||
|
||||
# 6-color ACeP palette (RGB format)
|
||||
PALETTE_RGB = [
|
||||
(0, 0, 0), # BLACK
|
||||
(0, 0, 0), # BLACK
|
||||
(255, 255, 255), # WHITE
|
||||
(255, 255, 0), # YELLOW
|
||||
(255, 0, 0), # RED
|
||||
(0, 0, 255), # BLUE
|
||||
(0, 255, 0), # GREEN
|
||||
(255, 255, 0), # YELLOW
|
||||
(255, 0, 0), # RED
|
||||
(0, 0, 255), # BLUE
|
||||
(0, 255, 0), # GREEN
|
||||
]
|
||||
|
||||
PALETTE_NAMES = ['Black', 'White', 'Yellow', 'Red', 'Blue', 'Green']
|
||||
PALETTE_NAMES = ["Black", "White", "Yellow", "Red", "Blue", "Green"]
|
||||
|
||||
|
||||
def create_pil_palette_image() -> Image.Image:
|
||||
|
|
@ -36,20 +34,20 @@ def create_pil_palette_image() -> Image.Image:
|
|||
return pal_image
|
||||
|
||||
|
||||
def find_nearest_color(pixel: np.ndarray, palette: np.ndarray) -> Tuple[int, np.ndarray]:
|
||||
def find_nearest_color(pixel: np.ndarray, palette: np.ndarray) -> tuple[int, np.ndarray]:
|
||||
"""Find the nearest palette color using Euclidean distance."""
|
||||
distances = np.sqrt(np.sum((palette - pixel) ** 2, axis=1))
|
||||
idx = np.argmin(distances)
|
||||
return idx, palette[idx]
|
||||
|
||||
|
||||
def find_nearest_color_weighted(pixel: np.ndarray, palette: np.ndarray) -> Tuple[int, np.ndarray]:
|
||||
def find_nearest_color_weighted(pixel: np.ndarray, palette: np.ndarray) -> tuple[int, np.ndarray]:
|
||||
"""Find nearest color using perceptually-weighted distance (human eye sensitivity)."""
|
||||
# Weights based on human perception: Green > Red > Blue
|
||||
weights = np.array([0.299, 0.587, 0.114])
|
||||
diff = palette - pixel
|
||||
weighted_diff = diff * weights
|
||||
distances = np.sqrt(np.sum(weighted_diff ** 2, axis=1))
|
||||
distances = np.sqrt(np.sum(weighted_diff**2, axis=1))
|
||||
idx = np.argmin(distances)
|
||||
return idx, palette[idx]
|
||||
|
||||
|
|
@ -58,6 +56,7 @@ def find_nearest_color_weighted(pixel: np.ndarray, palette: np.ndarray) -> Tuple
|
|||
# Error Diffusion Dithering Algorithms
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def dither_floyd_steinberg(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
"""
|
||||
Floyd-Steinberg dithering (1976).
|
||||
|
|
@ -66,7 +65,7 @@ def dither_floyd_steinberg(image: Image.Image, weighted: bool = False) -> Image.
|
|||
* 7/16
|
||||
3/16 5/16 1/16
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -88,7 +87,7 @@ def dither_floyd_steinberg(image: Image.Image, weighted: bool = False) -> Image.
|
|||
img[y + 1, x + 1] += error * 1 / 16
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_jarvis_judice_ninke(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -101,7 +100,7 @@ def dither_jarvis_judice_ninke(image: Image.Image, weighted: bool = False) -> Im
|
|||
1 3 5 3 1
|
||||
All divided by 48.
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -132,7 +131,7 @@ def dither_jarvis_judice_ninke(image: Image.Image, weighted: bool = False) -> Im
|
|||
img[y + 2, x + dx] += error * w / 48
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_stucki(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -145,7 +144,7 @@ def dither_stucki(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
1 2 4 2 1
|
||||
All divided by 42.
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -173,7 +172,7 @@ def dither_stucki(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
img[y + 2, x + dx] += error * w / 42
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_atkinson(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -186,7 +185,7 @@ def dither_atkinson(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
1
|
||||
All divided by 8 (but only 6/8 total error diffused).
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -215,7 +214,7 @@ def dither_atkinson(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
img[y + 2, x] += error / 8
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_sierra(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -228,7 +227,7 @@ def dither_sierra(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
2 3 2
|
||||
All divided by 32.
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -256,7 +255,7 @@ def dither_sierra(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
img[y + 2, x + dx] += error * w / 32
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_sierra_lite(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -267,7 +266,7 @@ def dither_sierra_lite(image: Image.Image, weighted: bool = False) -> Image.Imag
|
|||
1 1
|
||||
All divided by 4.
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -288,7 +287,7 @@ def dither_sierra_lite(image: Image.Image, weighted: bool = False) -> Image.Imag
|
|||
img[y + 1, x] += error * 1 / 4
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
def dither_burkes(image: Image.Image, weighted: bool = False) -> Image.Image:
|
||||
|
|
@ -299,7 +298,7 @@ def dither_burkes(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
2 4 8 4 2
|
||||
All divided by 32.
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
find_fn = find_nearest_color_weighted if weighted else find_nearest_color
|
||||
|
|
@ -322,20 +321,21 @@ def dither_burkes(image: Image.Image, weighted: bool = False) -> Image.Image:
|
|||
img[y + 1, x + dx] += error * w / 32
|
||||
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Ordered Dithering Algorithms
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_bayer_matrix(n: int) -> np.ndarray:
|
||||
"""Generate a Bayer matrix of size 2^n x 2^n."""
|
||||
if n == 0:
|
||||
return np.array([[0]])
|
||||
smaller = get_bayer_matrix(n - 1)
|
||||
size = 2 ** (n - 1)
|
||||
result = np.zeros((2 ** n, 2 ** n))
|
||||
result = np.zeros((2**n, 2**n))
|
||||
result[:size, :size] = 4 * smaller
|
||||
result[:size, size:] = 4 * smaller + 2
|
||||
result[size:, :size] = 4 * smaller + 3
|
||||
|
|
@ -343,7 +343,9 @@ def get_bayer_matrix(n: int) -> np.ndarray:
|
|||
return result
|
||||
|
||||
|
||||
def dither_ordered_bayer(image: Image.Image, matrix_size: int = 4, strength: float = 1.0) -> Image.Image:
|
||||
def dither_ordered_bayer(
|
||||
image: Image.Image, matrix_size: int = 4, strength: float = 1.0
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Ordered dithering using Bayer matrix.
|
||||
|
||||
|
|
@ -352,7 +354,7 @@ def dither_ordered_bayer(image: Image.Image, matrix_size: int = 4, strength: flo
|
|||
matrix_size: Size of Bayer matrix (2, 4, 8, or 16)
|
||||
strength: Dithering strength multiplier (0.0-2.0)
|
||||
"""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
height, width = img.shape[:2]
|
||||
palette = np.array(PALETTE_RGB, dtype=np.float64)
|
||||
|
||||
|
|
@ -362,7 +364,7 @@ def dither_ordered_bayer(image: Image.Image, matrix_size: int = 4, strength: flo
|
|||
bayer_size = bayer.shape[0]
|
||||
|
||||
# Normalize Bayer matrix to -0.5 to 0.5 range, then scale
|
||||
bayer_normalized = (bayer / (bayer_size ** 2) - 0.5) * strength * 128
|
||||
bayer_normalized = (bayer / (bayer_size**2) - 0.5) * strength * 128
|
||||
|
||||
result = np.zeros_like(img)
|
||||
|
||||
|
|
@ -374,15 +376,23 @@ def dither_ordered_bayer(image: Image.Image, matrix_size: int = 4, strength: flo
|
|||
_, new_pixel = find_nearest_color(adjusted_pixel, palette)
|
||||
result[y, x] = new_pixel
|
||||
|
||||
return Image.fromarray(result.astype(np.uint8), 'RGB')
|
||||
return Image.fromarray(result.astype(np.uint8), "RGB")
|
||||
|
||||
|
||||
_NUMBA_PALETTE = np.array([
|
||||
[0, 0, 0], [255, 255, 255], [255, 255, 0],
|
||||
[255, 0, 0], [0, 0, 255], [0, 255, 0],
|
||||
], dtype=np.float64)
|
||||
_NUMBA_PALETTE = np.array(
|
||||
[
|
||||
[0, 0, 0],
|
||||
[255, 255, 255],
|
||||
[255, 255, 0],
|
||||
[255, 0, 0],
|
||||
[0, 0, 255],
|
||||
[0, 255, 0],
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
_NUMBA_WEIGHTS = np.array([0.299, 0.587, 0.114], dtype=np.float64)
|
||||
|
||||
|
||||
@jit(nopython=True)
|
||||
def _numba_find_nearest(r, g, b, palette, weights):
|
||||
best_idx = 0
|
||||
|
|
@ -397,6 +407,7 @@ def _numba_find_nearest(r, g, b, palette, weights):
|
|||
best_idx = i
|
||||
return best_idx
|
||||
|
||||
|
||||
@jit(nopython=True)
|
||||
def _numba_atkinson(img, palette, weights):
|
||||
height, width = img.shape[0], img.shape[1]
|
||||
|
|
@ -435,32 +446,34 @@ def _numba_atkinson(img, palette, weights):
|
|||
img[y + 2, x, 2] += err_b
|
||||
return img
|
||||
|
||||
|
||||
def dither_atkinson_numba(image: Image.Image) -> Image.Image:
|
||||
"""Numba-accelerated Atkinson dithering with perceptual weighting (~150x faster)."""
|
||||
img = np.array(image.convert('RGB'), dtype=np.float64)
|
||||
img = np.array(image.convert("RGB"), dtype=np.float64)
|
||||
img = _numba_atkinson(img, _NUMBA_PALETTE, _NUMBA_WEIGHTS)
|
||||
img = np.clip(img, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(img, 'RGB')
|
||||
return Image.fromarray(img, "RGB")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PIL Built-in (for comparison)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def dither_pil_floyd_steinberg(image: Image.Image) -> Image.Image:
|
||||
"""PIL's built-in Floyd-Steinberg dithering for comparison."""
|
||||
pal_image = create_pil_palette_image()
|
||||
img = image.convert('RGB')
|
||||
img = image.convert("RGB")
|
||||
quantized = img.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=pal_image)
|
||||
return quantized.convert('RGB')
|
||||
return quantized.convert("RGB")
|
||||
|
||||
|
||||
def dither_pil_none(image: Image.Image) -> Image.Image:
|
||||
"""PIL quantization with no dithering (nearest color only)."""
|
||||
pal_image = create_pil_palette_image()
|
||||
img = image.convert('RGB')
|
||||
img = image.convert("RGB")
|
||||
quantized = img.quantize(dither=Image.Dither.NONE, palette=pal_image)
|
||||
return quantized.convert('RGB')
|
||||
return quantized.convert("RGB")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -468,90 +481,90 @@ def dither_pil_none(image: Image.Image) -> Image.Image:
|
|||
# =============================================================================
|
||||
|
||||
DITHER_ALGORITHMS = {
|
||||
'none': {
|
||||
'name': 'No Dithering (PIL)',
|
||||
'func': dither_pil_none,
|
||||
'description': 'Simple nearest-color quantization without error diffusion',
|
||||
"none": {
|
||||
"name": "No Dithering (PIL)",
|
||||
"func": dither_pil_none,
|
||||
"description": "Simple nearest-color quantization without error diffusion",
|
||||
},
|
||||
'pil_fs': {
|
||||
'name': 'Floyd-Steinberg (PIL)',
|
||||
'func': dither_pil_floyd_steinberg,
|
||||
'description': 'PIL built-in Floyd-Steinberg implementation',
|
||||
"pil_fs": {
|
||||
"name": "Floyd-Steinberg (PIL)",
|
||||
"func": dither_pil_floyd_steinberg,
|
||||
"description": "PIL built-in Floyd-Steinberg implementation",
|
||||
},
|
||||
'floyd_steinberg': {
|
||||
'name': 'Floyd-Steinberg',
|
||||
'func': dither_floyd_steinberg,
|
||||
'description': 'Classic error diffusion (1976), good balance of speed and quality',
|
||||
"floyd_steinberg": {
|
||||
"name": "Floyd-Steinberg",
|
||||
"func": dither_floyd_steinberg,
|
||||
"description": "Classic error diffusion (1976), good balance of speed and quality",
|
||||
},
|
||||
'floyd_steinberg_weighted': {
|
||||
'name': 'Floyd-Steinberg (Weighted)',
|
||||
'func': lambda img: dither_floyd_steinberg(img, weighted=True),
|
||||
'description': 'Floyd-Steinberg with perceptual color weighting',
|
||||
"floyd_steinberg_weighted": {
|
||||
"name": "Floyd-Steinberg (Weighted)",
|
||||
"func": lambda img: dither_floyd_steinberg(img, weighted=True),
|
||||
"description": "Floyd-Steinberg with perceptual color weighting",
|
||||
},
|
||||
'atkinson': {
|
||||
'name': 'Atkinson',
|
||||
'func': dither_atkinson,
|
||||
'description': 'Bill Atkinson (Apple), diffuses only 75% of error for cleaner results',
|
||||
"atkinson": {
|
||||
"name": "Atkinson",
|
||||
"func": dither_atkinson,
|
||||
"description": "Bill Atkinson (Apple), diffuses only 75% of error for cleaner results",
|
||||
},
|
||||
'atkinson_weighted': {
|
||||
'name': 'Atkinson (Weighted)',
|
||||
'func': lambda img: dither_atkinson(img, weighted=True),
|
||||
'description': 'Atkinson with perceptual color weighting',
|
||||
"atkinson_weighted": {
|
||||
"name": "Atkinson (Weighted)",
|
||||
"func": lambda img: dither_atkinson(img, weighted=True),
|
||||
"description": "Atkinson with perceptual color weighting",
|
||||
},
|
||||
'atkinson_fast': {
|
||||
'name': 'Atkinson (Numba Fast)',
|
||||
'func': dither_atkinson_numba,
|
||||
'description': 'Numba-accelerated Atkinson (~150x faster, requires numba)',
|
||||
"atkinson_fast": {
|
||||
"name": "Atkinson (Numba Fast)",
|
||||
"func": dither_atkinson_numba,
|
||||
"description": "Numba-accelerated Atkinson (~150x faster, requires numba)",
|
||||
},
|
||||
'jarvis': {
|
||||
'name': 'Jarvis-Judice-Ninke',
|
||||
'func': dither_jarvis_judice_ninke,
|
||||
'description': 'Larger diffusion kernel (1976), smoother gradients but slower',
|
||||
"jarvis": {
|
||||
"name": "Jarvis-Judice-Ninke",
|
||||
"func": dither_jarvis_judice_ninke,
|
||||
"description": "Larger diffusion kernel (1976), smoother gradients but slower",
|
||||
},
|
||||
'stucki': {
|
||||
'name': 'Stucki',
|
||||
'func': dither_stucki,
|
||||
'description': 'Similar to JJN with modified weights (1981)',
|
||||
"stucki": {
|
||||
"name": "Stucki",
|
||||
"func": dither_stucki,
|
||||
"description": "Similar to JJN with modified weights (1981)",
|
||||
},
|
||||
'sierra': {
|
||||
'name': 'Sierra',
|
||||
'func': dither_sierra,
|
||||
'description': 'Full Sierra dithering, balanced results',
|
||||
"sierra": {
|
||||
"name": "Sierra",
|
||||
"func": dither_sierra,
|
||||
"description": "Full Sierra dithering, balanced results",
|
||||
},
|
||||
'sierra_lite': {
|
||||
'name': 'Sierra Lite',
|
||||
'func': dither_sierra_lite,
|
||||
'description': 'Faster Sierra variant with smaller kernel',
|
||||
"sierra_lite": {
|
||||
"name": "Sierra Lite",
|
||||
"func": dither_sierra_lite,
|
||||
"description": "Faster Sierra variant with smaller kernel",
|
||||
},
|
||||
'burkes': {
|
||||
'name': 'Burkes',
|
||||
'func': dither_burkes,
|
||||
'description': 'Simplified two-row error diffusion',
|
||||
"burkes": {
|
||||
"name": "Burkes",
|
||||
"func": dither_burkes,
|
||||
"description": "Simplified two-row error diffusion",
|
||||
},
|
||||
'bayer2': {
|
||||
'name': 'Ordered (Bayer 2x2)',
|
||||
'func': lambda img: dither_ordered_bayer(img, matrix_size=2),
|
||||
'description': 'Ordered dithering with 2x2 Bayer matrix',
|
||||
"bayer2": {
|
||||
"name": "Ordered (Bayer 2x2)",
|
||||
"func": lambda img: dither_ordered_bayer(img, matrix_size=2),
|
||||
"description": "Ordered dithering with 2x2 Bayer matrix",
|
||||
},
|
||||
'bayer4': {
|
||||
'name': 'Ordered (Bayer 4x4)',
|
||||
'func': lambda img: dither_ordered_bayer(img, matrix_size=4),
|
||||
'description': 'Ordered dithering with 4x4 Bayer matrix',
|
||||
"bayer4": {
|
||||
"name": "Ordered (Bayer 4x4)",
|
||||
"func": lambda img: dither_ordered_bayer(img, matrix_size=4),
|
||||
"description": "Ordered dithering with 4x4 Bayer matrix",
|
||||
},
|
||||
'bayer8': {
|
||||
'name': 'Ordered (Bayer 8x8)',
|
||||
'func': lambda img: dither_ordered_bayer(img, matrix_size=8),
|
||||
'description': 'Ordered dithering with 8x8 Bayer matrix',
|
||||
"bayer8": {
|
||||
"name": "Ordered (Bayer 8x8)",
|
||||
"func": lambda img: dither_ordered_bayer(img, matrix_size=8),
|
||||
"description": "Ordered dithering with 8x8 Bayer matrix",
|
||||
},
|
||||
'bayer4_strong': {
|
||||
'name': 'Ordered (Bayer 4x4 Strong)',
|
||||
'func': lambda img: dither_ordered_bayer(img, matrix_size=4, strength=1.5),
|
||||
'description': 'Bayer 4x4 with increased dithering strength',
|
||||
"bayer4_strong": {
|
||||
"name": "Ordered (Bayer 4x4 Strong)",
|
||||
"func": lambda img: dither_ordered_bayer(img, matrix_size=4, strength=1.5),
|
||||
"description": "Bayer 4x4 with increased dithering strength",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_algorithm_names() -> List[str]:
|
||||
def get_algorithm_names() -> list[str]:
|
||||
"""Return list of available algorithm names."""
|
||||
return list(DITHER_ALGORITHMS.keys())
|
||||
|
||||
|
|
@ -560,4 +573,4 @@ def apply_dithering(image: Image.Image, algorithm: str) -> Image.Image:
|
|||
"""Apply the specified dithering algorithm to an image."""
|
||||
if algorithm not in DITHER_ALGORITHMS:
|
||||
raise ValueError(f"Unknown algorithm: {algorithm}. Available: {get_algorithm_names()}")
|
||||
return DITHER_ALGORITHMS[algorithm]['func'](image)
|
||||
return DITHER_ALGORITHMS[algorithm]["func"](image)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue