Pixdither: 2021

class PixDither: def __init__(self, image_path, output_path=None, bits_per_channel=1, palette_type="monochrome", dither_algorithm="floyd-steinberg"): """ Initialize dithering processor Args: image_path: Path to input image output_path: Path for output image (optional) bits_per_channel: Color depth (1-8 bits per channel) palette_type: "monochrome", "grayscale", "rgb", or custom dither_algorithm: "floyd-steinberg", "atkinson", or "none" """ self.image_path = Path(image_path) self.output_path = output_path self.bits = bits_per_channel self.palette_type = palette_type self.algorithm = dither_algorithm # Load image self.img = Image.open(image_path).convert('RGB') self.pixels = np.array(self.img, dtype=np.float32) self.height, self.width = self.pixels.shape[:2] def quantize_color(self, color): """Quantize a single RGB color based on bits per channel""" if self.palette_type == "monochrome": # Simple black/white based on luminance luminance = 0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2] return np.array([255, 255, 255]) if luminance > 127 else np.array([0, 0, 0]) elif self.palette_type == "grayscale": # Grayscale with 2^bits levels levels = 2 ** self.bits step = 256 / levels luminance = 0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2] gray_level = round(luminance / step) * step return np.array([gray_level, gray_level, gray_level]) else: # RGB quantization levels = 2 ** self.bits step = 256 / levels quantized = np.round(color / step) * step return np.clip(quantized, 0, 255) def floyd_steinberg(self): """Apply Floyd-Steinberg dithering""" result = self.pixels.copy() for y in range(self.height): for x in range(self.width): old_pixel = result[y, x].copy() new_pixel = self.quantize_color(old_pixel) result[y, x] = new_pixel error = old_pixel - new_pixel # Distribute error to neighboring pixels if x + 1 < self.width: result[y, x + 1] += error * 7/16 if y + 1 < self.height: if x > 0: result[y + 1, x - 1] += error * 3/16 result[y + 1, x] += error * 5/16 if x + 1 < self.width: result[y + 1, x + 1] += error * 1/16 return np.clip(result, 0, 255).astype(np.uint8) def atkinson(self): """Apply Atkinson dithering""" result = self.pixels.copy() for y in range(self.height): for x in range(self.width): old_pixel = result[y, x].copy() new_pixel = self.quantize_color(old_pixel) result[y, x] = new_pixel error = old_pixel - new_pixel # Distribute error (all divided by 8) if x + 1 < self.width: result[y, x + 1] += error * 1/8 if x + 2 < self.width: result[y, x + 2] += error * 1/8 if y + 1 < self.height: if x > 0: result[y + 1, x - 1] += error * 1/8 result[y + 1, x] += error * 1/8 if x + 1 < self.width: result[y + 1, x + 1] += error * 1/8 if y + 2 < self.height: result[y + 2, x] += error * 1/8 return np.clip(result, 0, 255).astype(np.uint8) def simple_quantize(self): """Simple quantization without dithering""" result = self.pixels.copy() for y in range(self.height): for x in range(self.width): result[y, x] = self.quantize_color(result[y, x]) return result.astype(np.uint8) def process(self): """Process the image with selected algorithm""" print(f"Processing: {self.image_path.name}") print(f" Size: {self.width}x{self.height}") print(f" Palette: {self.palette_type}") print(f" Bits per channel: {self.bits}") print(f" Algorithm: {self.algorithm}") if self.algorithm == "floyd-steinberg": output_pixels = self.floyd_steinberg() elif self.algorithm == "atkinson": output_pixels = self.atkinson() elif self.algorithm == "none": output_pixels = self.simple_quantize() else: raise ValueError(f"Unknown algorithm: {self.algorithm}") # Create output image output_img = Image.fromarray(output_pixels, 'RGB') # Save or return if self.output_path: output_img.save(self.output_path) print(f" Saved to: {self.output_path}") else: # Auto-generate output filename output_path = self.image_path.stem + f"_dithered{self.image_path.suffix}" output_img.save(output_path) print(f" Saved to: {output_path}") return output_img

def create_gif(input_path, output_path, frames=10, duration=0.1): """Create animated dithering GIF showing progression""" from PIL import ImageDraw, ImageFont images = [] base_img = Image.open(input_path).convert('RGB') for i in range(1, frames + 1): bits = max(1, int(8 * i / frames)) dithered = PixDither(input_path, bits_per_channel=bits, palette_type="rgb", dither_algorithm="floyd-steinberg") img = dithered.process() # Add text overlay draw = ImageDraw.Draw(img) try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20) except: font = ImageFont.load_default() draw.text((10, 10), f"{bits} bits/channel", fill=(255, 255, 255), font=font) images.append(img) images[0].save(output_path, save_all=True, append_images=images[1:], duration=duration*1000, loop=0) print(f"GIF saved to: {output_path}") pixdither

def main(): parser = argparse.ArgumentParser( description='pixdither - Apply dithering to images', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: pixdither input.jpg -o output.png # Default (black/white Floyd-Steinberg) pixdither input.jpg -b 3 -p grayscale # 3-bit grayscale pixdither input.jpg -b 2 -p rgb -a atkinson # 2-bit per channel RGB with Atkinson pixdither input.jpg --no-dither # Simple quantization without dithering pixdither input.jpg --gif output.gif # Create animated dithering GIF """ ) parser.add_argument('input', help='Input image path') parser.add_argument('-o', '--output', help='Output image path') parser.add_argument('-b', '--bits', type=int, default=1, help='Bits per channel (1-8, default: 1)') parser.add_argument('-p', '--palette', choices=['monochrome', 'grayscale', 'rgb'], default='monochrome', help='Color palette type (default: monochrome)') parser.add_argument('-a', '--algorithm', choices=['floyd-steinberg', 'atkinson', 'none'], default='floyd-steinberg', help='Dithering algorithm (default: floyd-steinberg)') parser.add_argument('--gif', help='Create animated dithering GIF (provide output path)') args = parser.parse_args() # Validate bits if args.bits < 1 or args.bits > 8: print("Error: bits must be between 1 and 8") sys.exit(1) # Check if creating GIF if args.gif: create_gif(args.input, args.gif) return # Process single image try: dithered = PixDither( args.input, args.output, bits_per_channel=args.bits, palette_type=args.palette, dither_algorithm=args.algorithm ) dithered.process() except FileNotFoundError: print(f"Error: File '{args.input}' not found") sys.exit(1) except Exception as e: print(f"Error: {e}") sys.exit(1) class PixDither: def __init__(self

import argparse import sys from PIL import Image import numpy as np from pathlib import Path or custom dither_algorithm: "floyd-steinberg"

#!/usr/bin/env python3 """ pixdither - Image dithering tool with Floyd-Steinberg algorithm Converts images to reduced color palettes using error diffusion """

In observance of the recently passed California Assembly Bill #2571, Krieghoff International must cease all advertising and marketing activities directed at individuals 17 years and younger in California. This includes access to Krieghoff.com and the Krieghoff.com Online Store. To comply with California Assembly Bill No. 2571, Krieghoff International, Inc. requires confirmation that you are not a California Resident under the age of 18.

By clicking the confirm button below you confirm that you are not a California Resident under the age of 18.

We appreciate your understanding and cooperation in this matter. If you have any questions, please contact us at [email protected].