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}")
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) pixdither
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 frames + 1): bits = max(1
View this post on InstagramA post shared by Tamilmovie.ch (@tamilmovie_ch) on
Company of THAMILAR.CH
Bahnhofstrasse 1
CH-5436 Wünrenlos
Tel. 076 220 22 12
We can help! All you need to do is enter your email ID and follow the instructions!
We Send You Back Again Vefification Link On you E-mail Account