Spaces:
Runtime error
Runtime error
from collections import defaultdict, deque | |
import cv2 | |
import numpy as np | |
from PIL import Image | |
from skimage.color import deltaE_ciede2000, rgb2lab | |
from tqdm import tqdm | |
def modify_transparency(img, target_rgb): | |
# 画像を読み込む | |
copy_img = img.copy() | |
data = copy_img.getdata() | |
# 新しいピクセルデータを作成 | |
new_data = [] | |
for item in data: | |
# 指定されたRGB値のピクセルの場合、透明度を255に設定 | |
if item[:3] == target_rgb: | |
new_data.append((item[0], item[1], item[2], 255)) | |
else: | |
# それ以外の場合、透明度を0に設定 | |
new_data.append((item[0], item[1], item[2], 0)) | |
# 新しいデータを画像に設定し直す | |
copy_img.putdata(new_data) | |
return copy_img | |
def replace_color(image, color_1, color_2, alpha_np): | |
# 画像データを配列に変換 | |
data = np.array(image) | |
# RGBAモードの画像であるため、形状変更時に4チャネルを考慮 | |
original_shape = data.shape | |
color_1 = np.array(color_1, dtype=np.uint8) | |
color_2 = np.array(color_2, dtype=np.uint8) | |
# 幅優先探索で color_1 の領域を外側から塗りつぶす | |
# color_2 で保護されたオリジナルの線画 | |
protected = np.all(data[:, :, :3] == color_2, axis=2) | |
# color_1 で塗られた塗りつぶしたい領域 | |
fill_target = np.all(data[:, :, :3] == color_1, axis=2) | |
# すでに塗られている領域 | |
colored = (protected | fill_target) == False | |
# bfs の始点を列挙 | |
# colored をそのまま使ってもいいが、pythonは遅いのでnumpy経由のこの方が速い | |
# 上下左右にシフトした fill_target & colored == True になるやつ | |
adj_r = colored & np.roll(fill_target, -1, axis=0) | |
adj_r[:, -1] = False | |
adj_l = colored & np.roll(fill_target, 1, axis=0) | |
adj_l[:, 0] = False | |
adj_u = colored & np.roll(fill_target, 1, axis=1) | |
adj_u[:, 0] = False | |
adj_d = colored & np.roll(fill_target, -1, axis=1) | |
adj_d[:, -1] = False | |
# そのピクセルはすでに塗られていて、上下左右いずれかのピクセルが color_1 であるもの | |
bfs_start = adj_r | adj_l | adj_u | adj_d | |
que = deque( | |
zip(*np.where(bfs_start)), | |
maxlen=original_shape[0] * original_shape[1] * 2, | |
) | |
with tqdm(total=original_shape[0] * original_shape[1]) as pbar: | |
pbar.update(np.sum(colored) - np.sum(bfs_start) + np.sum(protected)) | |
while len(que) > 0: | |
y, x = que.popleft() | |
neighbors = [ | |
(x - 1, y), | |
(x + 1, y), | |
(x, y - 1), | |
(x, y + 1), # 上下左右 | |
] | |
pbar.update(1) | |
# assert not fill_target[y, x] and not protected[y, x] | |
# assert colored[y, x] | |
color = data[y, x, :3] | |
for nx, ny in neighbors: | |
if ( | |
nx < 0 | |
or nx >= original_shape[1] | |
or ny < 0 | |
or ny >= original_shape[0] | |
): | |
continue | |
if fill_target[ny, nx]: | |
fill_target[ny, nx] = False | |
# colored[ny, nx] = True | |
data[ny, nx, :3] = color | |
que.append((ny, nx)) | |
pbar.update(pbar.total - pbar.n) | |
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. | |
""" | |
# Ensure images are in RGBA mode | |
if lineart_image.mode != "RGBA": | |
lineart_image = lineart_image.convert("RGBA") | |
if base_image.mode != "RGBA": | |
base_image = base_image.convert("RGBA") | |
# Extract the alpha channel from the lineart image | |
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) | |
# Create a new image using the new color and the alpha channel from the original lineart | |
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 the new lineart image over the base image | |
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. | |
""" | |
# Ensure both images are in RGBA format | |
if base_image.mode != "RGBA": | |
base_image = base_image.convert("RGBA") | |
if lineart.mode != "RGB": | |
lineart = lineart.convert("RGBA") | |
# Convert the lineart image to OpenCV format | |
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) | |
# Thicken the lines using OpenCV | |
kernel = np.ones((thickness, thickness), np.uint8) | |
lineart_thickened = cv2.dilate(lineart_gray, kernel, iterations=1) | |
lineart_thickened = cv2.bitwise_not(lineart_thickened) | |
# Create a new RGBA image for the recolored lineart | |
lineart_recolored = np.zeros_like(lineart_cv) | |
lineart_recolored[:, :, :3] = new_color # Set new RGB color | |
lineart_recolored[:, :, 3] = np.where( | |
lineart_thickened < 250, 255, 0 | |
) # Blend alpha with thickened lines | |
# Convert back to PIL Image | |
lineart_recolored_pil = Image.fromarray(lineart_recolored, "RGBA") | |
# Composite the thickened and recolored lineart onto the base image | |
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. | |
""" | |
# new_colors = [] | |
# Convert the consolidated colors to LAB | |
consolidated_lab = [ | |
rgb2lab(np.array([color], dtype=np.float32) / 255.0).reshape(3) | |
for color, _ in consolidated_colors | |
] | |
# Try to find a distant color | |
max_attempts = 1000 | |
best_dist = 0.0 | |
best_color = (0, 0, 0) | |
# np.random.seed(42) | |
for _ in range(max_attempts): | |
# Generate a random color in RGB and convert to LAB | |
random_rgb = np.random.randint(0, 256, size=3) | |
random_lab = rgb2lab(np.array([random_rgb], dtype=np.float32) / 255.0).reshape( | |
3 | |
) | |
# consolidated_lab にある色からできるだけ遠い色を選びたい | |
min_distance = min( | |
map( | |
lambda base_color_lab: deltaE_ciede2000(base_color_lab, random_lab), | |
consolidated_lab, | |
) | |
) | |
if min_distance > distance_threshold: | |
return tuple(random_rgb) | |
# 閾値以上のものが見つからなかった場合に備えて一番良かったものを覚えておく | |
if best_dist < min_distance: | |
best_dist = min_distance | |
best_color = tuple(random_rgb) | |
return best_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. | |
""" | |
# Convert RGB to LAB | |
colors_lab = [ | |
rgb2lab(np.array([[color]], dtype=np.float32) / 255.0).reshape(3) | |
for color, _ in major_colors | |
] | |
n = len(colors_lab) | |
# Find similar colors and consolidate | |
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: | |
# Compare counts and consolidate to the color with the higher count | |
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. | |
""" | |
# Convert image to RGB if it's not | |
if image.mode != "RGB": | |
image = image.convert("RGB") | |
# Count each color | |
color_count = defaultdict(int) | |
for pixel in image.getdata(): | |
color_count[pixel] += 1 | |
# Total number of pixels | |
total_pixels = image.width * image.height | |
# Filter colors to find those above the threshold | |
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, thickness): | |
org = image | |
image.save("tmp.png") | |
major_colors = get_major_colors(image, threshold_percentage=0.05) | |
major_colors = consolidate_colors(major_colors, 10) | |
th = 10 | |
threshold_percentage = 0.05 | |
while len(major_colors) < 1: | |
threshold_percentage = threshold_percentage - 0.001 | |
major_colors = get_major_colors(image, threshold_percentage=threshold_percentage) | |
while len(major_colors) < 1: | |
th = th + 1 | |
major_colors = consolidate_colors(major_colors, th) | |
new_color_1 = generate_distant_colors(major_colors, 50) | |
image = thicken_and_recolor_lines( | |
org, lineart, thickness=thickness, new_color=new_color_1 | |
) | |
major_colors.append((new_color_1, 0)) | |
new_color_2 = generate_distant_colors(major_colors, 40) | |
image, alpha_np = recolor_lineart_and_composite( | |
lineart, image, new_color_2, alpha_th | |
) | |
# import time | |
# start = time.time() | |
image = replace_color(image, new_color_1, new_color_2, alpha_np) | |
# end = time.time() | |
# print(f"{end-start} sec") | |
unfinished = modify_transparency(image, new_color_1) | |
return image, unfinished | |
def main(): | |
import os | |
import sys | |
from argparse import ArgumentParser | |
from PIL import Image | |
from utils import randomname | |
args = ArgumentParser( | |
prog="starline", | |
description="Starline", | |
epilog="Starline", | |
) | |
args.add_argument("-c", "--colored_image", help="colored image", required=True) | |
args.add_argument("-l", "--lineart_image", help="lineart image", required=True) | |
args.add_argument("-o", "--output_dir", help="output directory", default="output") | |
args.add_argument("-a", "--alpha_th", help="alpha threshold", default=100, type=int) | |
args.add_argument("-t", "--thickness", help="line thickness", default=5, type=int) | |
args = args.parse_args(sys.argv[1:]) | |
colored_image_path = args.colored_image | |
lineart_image_path = args.lineart_image | |
alpha = args.alpha_th | |
thickness = args.thickness | |
output_dir = args.output_dir | |
colored_image = Image.open(colored_image_path) | |
lineart_image = Image.open(lineart_image_path) | |
if lineart_image.mode == "P" or lineart_image.mode == "L": | |
# 線画が 1-channel 画像のときの処理 | |
# alpha-channel の情報が入力されたと仮定して (透明 -> 0, 不透明 -> 255) | |
# RGB channel はこれを反転させたものにする (透明 -> 白 -> 255, 不透明 -> 黒 -> 0) | |
lineart_image = lineart_image.convert("RGBA") | |
lineart_image = np.array(lineart_image) | |
lineart_image[:, :, 0] = 255 - lineart_image[:, :, 3] | |
lineart_image[:, :, 1] = 255 - lineart_image[:, :, 3] | |
lineart_image[:, :, 2] = 255 - lineart_image[:, :, 3] | |
lineart_image = Image.fromarray(lineart_image) | |
lineart_image = lineart_image.convert("RGBA") | |
result_image, unfinished = process(colored_image, lineart_image, alpha, thickness) | |
output_image = Image.alpha_composite(result_image, lineart_image) | |
name = randomname(10) | |
os.makedirs(f"{output_dir}/{name}") | |
output_image.save(f"{output_dir}/{name}/output_image.png") | |
result_image.save(f"{output_dir}/{name}/color_image.png") | |
unfinished.save(f"{output_dir}/{name}/unfinished_image.png") | |
if __name__ == "__main__": | |
main() | |