import gradio as gr from PIL import Image, ImageFilter, ImageOps import cv2 import numpy as np import os from collections import defaultdict from skimage.color import deltaE_ciede2000, rgb2lab import zipfile def DoG_filter(image, kernel_size=0, sigma=1.0, k_sigma=2.0, gamma=1.5): g1 = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma) g2 = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma * k_sigma) return g1 - gamma * g2 def XDoG_filter(image, kernel_size=0, sigma=1.4, k_sigma=1.6, epsilon=0, phi=10, gamma=0.98): epsilon /= 255 dog = DoG_filter(image, kernel_size, sigma, k_sigma, gamma) dog /= dog.max() e = 1 + np.tanh(phi * (dog - epsilon)) e[e >= 1] = 1 return (e * 255).astype('uint8') def binarize_image(image): _, binarized = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return binarized def process_XDoG(image_path): kernel_size=0 sigma=1.4 k_sigma=1.6 epsilon=0 phi=10 gamma=0.98 image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) xdog_image = XDoG_filter(image, kernel_size, sigma, k_sigma, epsilon, phi, gamma) binarized_image = binarize_image(xdog_image) final_image = Image.fromarray(binarized_image) return final_image def replace_color(image, color_1, blur_radius=2): data = np.array(image) original_shape = data.shape channels = original_shape[2] if len(original_shape) > 2 else 1 # チャンネル数を確認 data = data.reshape(-1, channels) color_1 = np.array(color_1) matches = np.all(data[:, :3] == color_1, axis=1) nochange_count = 0 mask = np.zeros(data.shape[0], dtype=bool) while np.any(matches): new_matches = np.zeros_like(matches) match_num = np.sum(matches) for i in range(len(data)): # Removed tqdm if matches[i]: x, y = divmod(i, original_shape[1]) neighbors = [ (x, y-1), (x, y+1), (x-1, y), (x+1, y) ] valid_neighbors = [] 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): valid_neighbors.append(data[ni, :3]) if valid_neighbors: new_color = np.mean(valid_neighbors, axis=0).astype(np.uint8) data[i, :3] = new_color data[i, 3] = 255 mask[i] = True else: 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) mask = mask.reshape(original_shape[:2]) result_image = Image.fromarray(data, 'RGBA') blurred_image = result_image.filter(ImageFilter.GaussianBlur(radius=blur_radius)) blurred_data = np.array(blurred_image) np.copyto(data, blurred_data, where=mask[..., None]) return Image.fromarray(data, 'RGBA') def generate_distant_colors(consolidated_colors, distance_threshold): 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) if all(deltaE_ciede2000(base_color_lab, random_lab) > distance_threshold for base_color_lab in consolidated_lab): return tuple(random_rgb) return (128, 128, 128) def consolidate_colors(major_colors, threshold): colors_lab = [rgb2lab(np.array([[color]], dtype=np.float32)/255.0).reshape(3) for color, _ in major_colors] i = 0 while i < len(colors_lab): j = i + 1 while j < len(colors_lab): if deltaE_ciede2000(colors_lab[i], colors_lab[j]) < 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) continue j += 1 i += 1 return major_colors def get_major_colors(image, threshold_percentage=0.01): 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 line_color(image, mask, new_color): data = np.array(image) data[mask, :3] = new_color return Image.fromarray(data) def process_image(image, lineart): if image.mode != 'RGBA': image = image.convert('RGBA') lineart = lineart.point(lambda x: 0 if x < 200 else 255) lineart = ImageOps.invert(lineart) kernel = np.ones((3, 3), np.uint8) lineart = cv2.dilate(np.array(lineart), kernel, iterations=1) lineart = Image.fromarray(lineart) mask = np.array(lineart) == 255 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) filled_image = line_color(image, mask, new_color_1) replace_color_image = replace_color(filled_image, new_color_1, 2).convert('RGB') return replace_color_image def zip_files(zip_files, zip_path): with zipfile.ZipFile(zip_path, 'w') as zipf: for file_path in zip_files: zipf.write(file_path, arcname=os.path.basename(file_path)) class webui: def __init__(self): self.demo = gr.Blocks() def main(self, image_path, kernel_size, sigma, k_sigma, gamma): image = Image.open(image_path).convert('RGBA') image_name = os.path.splitext(image_path)[0] alpha = image.getchannel('A') if image.mode == 'RGBA' else None image.save(image_path) image = Image.open(image_path).convert('RGBA') rgb_image = image.convert('RGB') lineart = process_XDoG(image_path, kernel_size, sigma, k_sigma, gamma).convert('L') replace_color_image = process_image(rgb_image, lineart).convert('RGBA') if alpha: replace_color_image.putalpha(alpha) replace_color_image_path = f"{image_name}_noline.png" replace_color_image.save(replace_color_image_path) lineart_image = lineart.convert('RGBA') lineart_alpha = 255 - np.array(lineart) lineart_image.putalpha(Image.fromarray(lineart_alpha)) lineart_image_path = f"{image_name}_lineart.png" lineart_image.save(lineart_image_path) zip_files_list = [replace_color_image_path, lineart_image_path] zip_path = f"{image_name}.zip" zip_files(zip_files_list, zip_path) outputs = [replace_color_image, lineart_image] return outputs, zip_path def launch(self, share): with self.demo: with gr.Row(): with gr.Column(): input_image = gr.Image(type='filepath', image_mode="RGBA", label="Original Image") kernel_size_input = gr.Number(value=0, label="Kernel Size", precision=0) sigma_input = gr.Number(value=1.0, label="Sigma") k_sigma_input = gr.Number(value=2.0, label="K Sigma") gamma_input = gr.Number(value=1.5, label="Gamma") submit = gr.Button(value="Start") with gr.Row(): with gr.Column(): with gr.Tab("output"): output_0 = gr.Gallery(format="png") output_file = gr.File() submit.click( self.main, inputs=[input_image, kernel_size_input, sigma_input, k_sigma_input, gamma_input], outputs=[output_0, output_file] ) self.demo.queue() self.demo.launch(share=share) if __name__ == "__main__": ui = webui() ui.launch(share=True)