| | from PIL import Image, ImageFilter
|
| | from collections import defaultdict
|
| | from skimage import color as sk_color
|
| | from PIL import Image
|
| | from tqdm import tqdm
|
| | from skimage.color import deltaE_ciede2000, rgb2lab
|
| | import cv2
|
| | import numpy as np
|
| |
|
| |
|
| | def replace_color(image, color_1, color_2, alpha_np):
|
| |
|
| | data = np.array(image)
|
| |
|
| |
|
| | original_shape = data.shape
|
| | data = data.reshape(-1, 4)
|
| |
|
| |
|
| | matches = np.all(data[:, :3] == color_1, axis=1)
|
| |
|
| |
|
| | nochange_count = 0
|
| | idx = 0
|
| |
|
| | while np.any(matches):
|
| | idx += 1
|
| | new_matches = np.zeros_like(matches)
|
| | match_num = np.sum(matches)
|
| | for i in tqdm(range(len(data))):
|
| | if matches[i]:
|
| | x, y = divmod(i, original_shape[1])
|
| | neighbors = [
|
| | (x-1, y), (x+1, y), (x, y-1), (x, y+1)
|
| | ]
|
| | replacement_found = False
|
| | for nx, ny in neighbors:
|
| | if 0 <= nx < original_shape[0] and 0 <= ny < original_shape[1]:
|
| | ni = nx * original_shape[1] + ny
|
| |
|
| | if not np.all(data[ni, :3] == color_1, axis=0) and not np.all(data[ni, :3] == color_2, axis=0):
|
| | data[i, :3] = data[ni, :3]
|
| | replacement_found = True
|
| | continue
|
| | if not replacement_found:
|
| | new_matches[i] = True
|
| | matches = new_matches
|
| | if match_num == np.sum(matches):
|
| | nochange_count += 1
|
| | if nochange_count > 5:
|
| | break
|
| |
|
| |
|
| | data = data.reshape(original_shape)
|
| | data[:, :, 3] = 255 - alpha_np
|
| | return Image.fromarray(data, 'RGBA')
|
| |
|
| | def recolor_lineart_and_composite(lineart_image, base_image, new_color, alpha_th):
|
| | """
|
| | Recolor an RGBA lineart image to a single new color while preserving alpha, and composite it over a base image.
|
| |
|
| | Args:
|
| | lineart_image (PIL.Image): The lineart image with RGBA channels.
|
| | base_image (PIL.Image): The base image to composite onto.
|
| | new_color (tuple): The new RGB color for the lineart (e.g., (255, 0, 0) for red).
|
| |
|
| | Returns:
|
| | PIL.Image: The composited image with the recolored lineart on top.
|
| | """
|
| |
|
| | if lineart_image.mode != 'RGBA':
|
| | lineart_image = lineart_image.convert('RGBA')
|
| | if base_image.mode != 'RGBA':
|
| | base_image = base_image.convert('RGBA')
|
| |
|
| |
|
| | r, g, b, alpha = lineart_image.split()
|
| |
|
| | alpha_np = np.array(alpha)
|
| | alpha_np[alpha_np < alpha_th] = 0
|
| | alpha_np[alpha_np >= alpha_th] = 255
|
| |
|
| | new_alpha = Image.fromarray(alpha_np)
|
| |
|
| |
|
| | new_lineart_image = Image.merge('RGBA', (
|
| | Image.new('L', lineart_image.size, int(new_color[0])),
|
| | Image.new('L', lineart_image.size, int(new_color[1])),
|
| | Image.new('L', lineart_image.size, int(new_color[2])),
|
| | new_alpha
|
| | ))
|
| |
|
| |
|
| | composite_image = Image.alpha_composite(base_image, new_lineart_image)
|
| |
|
| | return composite_image, alpha_np
|
| |
|
| |
|
| | def thicken_and_recolor_lines(base_image, lineart, thickness=3, new_color=(0, 0, 0)):
|
| | """
|
| | Thicken the lines of a lineart image, recolor them, and composite onto another image,
|
| | while preserving the transparency of the original lineart.
|
| |
|
| | Args:
|
| | base_image (PIL.Image): The base image to composite onto.
|
| | lineart (PIL.Image): The lineart image with transparent background.
|
| | thickness (int): The desired thickness of the lines.
|
| | new_color (tuple): The new color to apply to the lines (R, G, B).
|
| |
|
| | Returns:
|
| | PIL.Image: The image with the recolored and thickened lineart composited on top.
|
| | """
|
| |
|
| | if base_image.mode != 'RGBA':
|
| | base_image = base_image.convert('RGBA')
|
| | if lineart.mode != 'RGB':
|
| | lineart = lineart.convert('RGBA')
|
| |
|
| |
|
| | lineart_cv = np.array(lineart)
|
| |
|
| | white_pixels = np.sum(lineart_cv == 255)
|
| | black_pixels = np.sum(lineart_cv == 0)
|
| |
|
| |
|
| | lineart_gray = cv2.cvtColor(lineart_cv, cv2.COLOR_RGBA2GRAY)
|
| |
|
| | if white_pixels > black_pixels:
|
| | lineart_gray = cv2.bitwise_not(lineart_gray)
|
| |
|
| |
|
| |
|
| | kernel = np.ones((thickness, thickness), np.uint8)
|
| | lineart_thickened = cv2.dilate(lineart_gray, kernel, iterations=1)
|
| | lineart_thickened = cv2.bitwise_not(lineart_thickened)
|
| |
|
| | lineart_recolored = np.zeros_like(lineart_cv)
|
| | lineart_recolored[:, :, :3] = new_color
|
| |
|
| | lineart_recolored[:, :, 3] = np.where(lineart_thickened < 250, 255, 0)
|
| |
|
| |
|
| | lineart_recolored_pil = Image.fromarray(lineart_recolored, 'RGBA')
|
| |
|
| |
|
| | combined_image = Image.alpha_composite(base_image, lineart_recolored_pil)
|
| |
|
| |
|
| | return combined_image
|
| |
|
| |
|
| |
|
| | def generate_distant_colors(consolidated_colors, distance_threshold):
|
| | """
|
| | Generate new RGB colors that are at least 'distance_threshold' CIEDE2000 units away from given colors.
|
| |
|
| | Args:
|
| | consolidated_colors (list of tuples): List of ((R, G, B), count) tuples.
|
| | distance_threshold (float): The minimum CIEDE2000 distance from the given colors.
|
| |
|
| | Returns:
|
| | list of tuples: List of new RGB colors that meet the distance requirement.
|
| | """
|
| |
|
| |
|
| | consolidated_lab = [rgb2lab(np.array([color], dtype=np.float32) / 255.0).reshape(3) for color, _ in consolidated_colors]
|
| |
|
| |
|
| | max_attempts = 10000
|
| | for _ in range(max_attempts):
|
| |
|
| | random_rgb = np.random.randint(0, 256, size=3)
|
| | random_lab = rgb2lab(np.array([random_rgb], dtype=np.float32) / 255.0).reshape(3)
|
| | for base_color_lab in consolidated_lab:
|
| |
|
| | distance = deltaE_ciede2000(base_color_lab, random_lab)
|
| | if distance <= distance_threshold:
|
| | break
|
| | new_color = tuple(random_rgb)
|
| | break
|
| | return new_color
|
| |
|
| |
|
| |
|
| | def consolidate_colors(major_colors, threshold):
|
| | """
|
| | Consolidate similar colors in the major_colors list based on the CIEDE2000 metric.
|
| |
|
| | Args:
|
| | major_colors (list of tuples): List of ((R, G, B), count) tuples.
|
| | threshold (float): Threshold for CIEDE2000 color difference.
|
| |
|
| | Returns:
|
| | list of tuples: Consolidated list of ((R, G, B), count) tuples.
|
| | """
|
| |
|
| | colors_lab = [rgb2lab(np.array([[color]], dtype=np.float32)/255.0).reshape(3) for color, _ in major_colors]
|
| | n = len(colors_lab)
|
| |
|
| |
|
| | i = 0
|
| | while i < n:
|
| | j = i + 1
|
| | while j < n:
|
| | delta_e = deltaE_ciede2000(colors_lab[i], colors_lab[j])
|
| | if delta_e < threshold:
|
| |
|
| | if major_colors[i][1] >= major_colors[j][1]:
|
| | major_colors[i] = (major_colors[i][0], major_colors[i][1] + major_colors[j][1])
|
| | major_colors.pop(j)
|
| | colors_lab.pop(j)
|
| | else:
|
| | major_colors[j] = (major_colors[j][0], major_colors[j][1] + major_colors[i][1])
|
| | major_colors.pop(i)
|
| | colors_lab.pop(i)
|
| | n -= 1
|
| | continue
|
| | j += 1
|
| | i += 1
|
| |
|
| | return major_colors
|
| |
|
| |
|
| |
|
| |
|
| | def get_major_colors(image, threshold_percentage=0.01):
|
| | """
|
| | Analyze an image to find the major RGB values based on a threshold percentage.
|
| |
|
| | Args:
|
| | image (PIL.Image): The image to analyze.
|
| | threshold_percentage (float): The percentage threshold to consider a color as major.
|
| |
|
| | Returns:
|
| | list of tuples: A list of (color, count) tuples for colors that are more frequent than the threshold.
|
| | """
|
| |
|
| | if image.mode != 'RGB':
|
| | image = image.convert('RGB')
|
| |
|
| |
|
| | color_count = defaultdict(int)
|
| | for pixel in image.getdata():
|
| | color_count[pixel] += 1
|
| |
|
| |
|
| | total_pixels = image.width * image.height
|
| |
|
| |
|
| | major_colors = [(color, count) for color, count in color_count.items()
|
| | if (count / total_pixels) >= threshold_percentage]
|
| |
|
| | return major_colors
|
| |
|
| |
|
| | def process(image, lineart, alpha_th):
|
| | org = image
|
| |
|
| | major_colors = get_major_colors(image, threshold_percentage=0.05)
|
| | major_colors = consolidate_colors(major_colors, 10)
|
| | new_color_1 = generate_distant_colors(major_colors, 100)
|
| | image = thicken_and_recolor_lines(org, lineart, thickness=5, new_color=new_color_1)
|
| | major_colors.append((new_color_1, 0))
|
| | new_color_2 = generate_distant_colors(major_colors, 100)
|
| | image, alpha_np = recolor_lineart_and_composite(lineart, image, new_color_2, alpha_th)
|
| | image = replace_color(image, new_color_1, new_color_2, alpha_np)
|
| |
|
| | return image |