|
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 |