Files
flycarl-ComfyUI-Pixelate/utils.py
T
2024-11-14 15:34:26 +08:00

154 lines
5.7 KiB
Python

import numpy as np
import cv2
from PIL import Image
import colorsys
def scale_down(image, scale_factor):
"""
Downscale image by integer factor using area interpolation
Args:
image: numpy array (H, W, C)
scale_factor: int, factor to reduce image by
"""
h, w = image.shape[:2]
small_h, small_w = h // scale_factor, w // scale_factor
return cv2.resize(image, (small_w, small_h), interpolation=cv2.INTER_AREA)
def scale_up(image, scale_factor):
"""
Upscale image by integer factor using nearest neighbor interpolation
Args:
image: numpy array (H, W, C)
scale_factor: int, factor to enlarge image by
"""
h, w = image.shape[:2]
large_h, large_w = h * scale_factor, w * scale_factor
return cv2.resize(image, (large_w, large_h), interpolation=cv2.INTER_NEAREST)
def resize_pixel_art(image, scale_factor):
"""Resize image using pixel art scaling"""
return scale_up(scale_down(image, scale_factor), scale_factor)
def convert_to_grayscale(image):
"""Convert image to grayscale while preserving dimensions"""
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
return cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
def convert_to_bw(image, threshold=127):
"""Convert image to black and white"""
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
_, bw = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)
return cv2.cvtColor(bw, cv2.COLOR_GRAY2RGB)
class PaletteGenerator:
@staticmethod
def kmeans_palette(image, n_colors):
"""Generate palette using k-means clustering"""
pixels = image.reshape(-1, 3).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 0.1)
_, _, palette = cv2.kmeans(pixels, n_colors, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
return palette
@staticmethod
def median_cut_palette(image, n_colors):
"""Generate palette using median cut algorithm"""
pixels = image.reshape(-1, 3)
def cut_box(box_pixels):
max_range = np.ptp(box_pixels, axis=0)
cut_dim = np.argmax(max_range)
box_pixels = box_pixels[box_pixels[:, cut_dim].argsort()]
mid = len(box_pixels) // 2
return box_pixels[:mid], box_pixels[mid:]
boxes = [pixels]
while len(boxes) < n_colors:
box = boxes.pop(0)
if len(box) < 2:
break
box1, box2 = cut_box(box)
boxes.extend([box1, box2])
return np.array([np.mean(box, axis=0) for box in boxes])
@staticmethod
def parse_custom_palette(palette_str):
"""Parse custom palette string in format 'R,G,B;R,G,B;...'"""
try:
return np.array([list(map(int, color.split(',')))
for color in palette_str.split(';')])
except:
return None
class Dithering:
@staticmethod
def extract_palette_from_image(image, palette_size=16):
"""Extract palette from reference image using k-means clustering"""
pixels = image.reshape(-1, 3).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, 0.1)
_, _, palette = cv2.kmeans(pixels, palette_size, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
return palette
@staticmethod
def find_closest_palette_color(pixel, palette):
"""Find the closest color in palette"""
distances = np.sum((palette - pixel) ** 2, axis=1)
return palette[np.argmin(distances)]
@staticmethod
def floyd_steinberg(image, palette=None, reference_image=None, palette_size=16):
"""
Apply Floyd-Steinberg dithering
Args:
image: Image to dither
palette: Optional pre-defined color palette
reference_image: Optional reference image to extract palette from
palette_size: Number of colors to extract from reference image
"""
# Determine palette
if palette is None and reference_image is not None:
palette = Dithering.extract_palette_from_image(reference_image, palette_size)
elif palette is None:
# Fallback to simple palette if neither is provided
palette = np.array([[0, 0, 0], [255, 255, 255]])
height, width = image.shape[:2]
result = image.copy().astype(np.float32)
for y in range(height):
for x in range(width):
old_pixel = result[y, x].copy()
new_pixel = Dithering.find_closest_palette_color(old_pixel, palette)
result[y, x] = new_pixel
quant_error = old_pixel - new_pixel
if x + 1 < width:
result[y, x + 1] += quant_error * 7/16
if y + 1 < height:
if x > 0:
result[y + 1, x - 1] += quant_error * 3/16
result[y + 1, x] += quant_error * 5/16
if x + 1 < width:
result[y + 1, x + 1] += quant_error * 1/16
return np.clip(result, 0, 255).astype(np.uint8)
@staticmethod
def simple_quantize(image, palette):
"""Simple color quantization using nearest neighbor"""
height, width = image.shape[:2]
result = np.zeros_like(image)
color_cache = {}
for y in range(height):
for x in range(width):
pixel = tuple(image[y, x])
if pixel not in color_cache:
color_cache[pixel] = Dithering.find_closest_palette_color(image[y, x], palette)
result[y, x] = color_cache[pixel]
return result