|
1 | | -from PIL import Image |
| 1 | +import argparse |
| 2 | + |
2 | 3 | import numpy as np |
| 4 | +from PIL import Image, ImageFilter |
3 | 5 | from sklearn.cluster import KMeans |
4 | 6 |
|
5 | | -def reduce_colors(input_path, output_path, num_colors): |
| 7 | + |
| 8 | +def reduce_colors(input_path, output_path, num_colors, mode="hsv", smooth=0): |
6 | 9 | """ |
7 | | - 减少图片颜色数量 |
8 | | - |
| 10 | + 减少图片颜色数量并进行平滑处理 |
| 11 | +
|
9 | 12 | :param input_path: 输入文件路径 |
10 | 13 | :param output_path: 输出文件路径 |
11 | 14 | :param num_colors: 目标颜色数量 |
| 15 | + :param mode: 颜色模式 'rgb' 或 'hsv' |
| 16 | + :param smooth: 平滑窗口大小 (0表示不平滑,建议3或5) |
12 | 17 | """ |
13 | | - # 打开图片并保留原始模式(RGB/RGBA) |
14 | 18 | img = Image.open(input_path) |
15 | 19 | original_mode = img.mode |
16 | | - has_alpha = 'A' in original_mode |
17 | | - |
18 | | - # 转换到RGB/RGBA并提取像素数据 |
19 | | - if original_mode not in ('RGB', 'RGBA'): |
20 | | - img = img.convert('RGBA' if has_alpha else 'RGB') |
21 | | - |
22 | | - pixels = np.array(img) |
23 | | - height, width = img.size[1], img.size[0] |
24 | | - |
25 | | - # 分离颜色通道和透明度通道 |
26 | | - if has_alpha: |
27 | | - alpha_channel = pixels[:, :, 3] |
28 | | - color_pixels = pixels[:, :, :3] |
| 20 | + |
| 21 | + if original_mode != "RGBA": |
| 22 | + img = img.convert("RGBA") |
| 23 | + |
| 24 | + # 分离通道 |
| 25 | + r, g, b, alpha = img.split() |
| 26 | + rgb_img = Image.merge("RGB", (r, g, b)) |
| 27 | + |
| 28 | + # 颜色空间转换 |
| 29 | + process_mode = mode.upper() |
| 30 | + if process_mode == "HSV": |
| 31 | + work_img = rgb_img.convert("HSV") |
29 | 32 | else: |
30 | | - color_pixels = pixels |
31 | | - |
32 | | - # 准备K-Means输入数据 |
33 | | - color_data = color_pixels.reshape(-1, 3) |
34 | | - |
35 | | - # 执行K-Means聚类 |
| 33 | + work_img = rgb_img |
| 34 | + |
| 35 | + pixels = np.array(work_img) |
| 36 | + height, width = pixels.shape[:2] |
| 37 | + color_data = pixels.reshape(-1, 3) |
| 38 | + |
| 39 | + # K-Means聚类 |
36 | 40 | kmeans = KMeans(n_clusters=num_colors, random_state=0, n_init=10) |
37 | 41 | kmeans.fit(color_data) |
38 | | - |
39 | | - # 生成新颜色数据 |
40 | | - new_colors = kmeans.cluster_centers_[kmeans.labels_] |
41 | | - new_colors = new_colors.reshape(color_pixels.shape).astype(np.uint8) |
42 | | - |
43 | | - # 合并透明度通道 |
44 | | - if has_alpha: |
45 | | - output_pixels = np.dstack((new_colors, alpha_channel)) |
| 42 | + |
| 43 | + # 获取原始标签矩阵 |
| 44 | + labels = kmeans.labels_.reshape(height, width) |
| 45 | + |
| 46 | + # --- 核心修改:对标签进行平滑处理 --- |
| 47 | + if smooth > 0: |
| 48 | + # 将标签转为图像以便使用PIL的滤波器 |
| 49 | + # 标签范围是0 ~ num_colors-1,可以直接存为L模式(8-bit) |
| 50 | + label_img = Image.fromarray(labels.astype(np.uint8), mode="L") |
| 51 | + |
| 52 | + # 使用 ModeFilter (众数滤波) |
| 53 | + # 它会将像素替换为邻域内出现频率最高的标签,非常适合去除噪点且不引入新颜色 |
| 54 | + label_img = label_img.filter(ImageFilter.ModeFilter(size=smooth)) |
| 55 | + |
| 56 | + # 转回numpy数组 |
| 57 | + labels = np.array(label_img) |
| 58 | + # --------------------------------- |
| 59 | + |
| 60 | + # 根据(可能平滑过的)标签重构图像 |
| 61 | + # labels.flatten() 将二维标签展平以索引 cluster_centers_ |
| 62 | + new_colors = kmeans.cluster_centers_[labels.flatten()] |
| 63 | + new_colors = new_colors.reshape(pixels.shape).astype(np.uint8) |
| 64 | + |
| 65 | + # 重建图像 |
| 66 | + output_img_no_alpha = Image.fromarray(new_colors, mode=process_mode) |
| 67 | + |
| 68 | + if process_mode == "HSV": |
| 69 | + output_img_no_alpha = output_img_no_alpha.convert("RGB") |
| 70 | + |
| 71 | + # 合并Alpha通道 |
| 72 | + r_new, g_new, b_new = output_img_no_alpha.split() |
| 73 | + if "A" in original_mode: |
| 74 | + final_output = Image.merge("RGBA", (r_new, g_new, b_new, alpha)) |
46 | 75 | else: |
47 | | - output_pixels = new_colors |
48 | | - |
49 | | - # 创建并保存图片 |
50 | | - output_img = Image.fromarray(output_pixels, mode=original_mode) |
51 | | - output_img.save(output_path) |
52 | | - |
53 | | -if __name__ == '__main__': |
54 | | - import argparse |
55 | | - |
56 | | - parser = argparse.ArgumentParser(description='图片颜色量化工具') |
57 | | - parser.add_argument('input', help='输入图片路径') |
58 | | - parser.add_argument('output', help='输出图片路径') |
59 | | - parser.add_argument('-n', '--num-colors', type=int, required=True, |
60 | | - help='目标颜色数量(2-256)') |
61 | | - |
| 76 | + final_output = Image.merge("RGB", (r_new, g_new, b_new)) |
| 77 | + |
| 78 | + final_output.save(output_path) |
| 79 | + |
| 80 | + |
| 81 | +if __name__ == "__main__": |
| 82 | + parser = argparse.ArgumentParser(description="图片颜色聚类工具") |
| 83 | + parser.add_argument("input", help="输入图片路径") |
| 84 | + parser.add_argument("-o", "--output", help="输出图片路径") |
| 85 | + parser.add_argument( |
| 86 | + "-n", "--num-colors", type=int, required=True, help="目标颜色数量(2-256)" |
| 87 | + ) |
| 88 | + parser.add_argument( |
| 89 | + "-m", |
| 90 | + "--mode", |
| 91 | + choices=["rgb", "hsv"], |
| 92 | + default="hsv", |
| 93 | + help="颜色聚类模式:rgb 或 hsv (默认: hsv)", |
| 94 | + ) |
| 95 | + parser.add_argument('-s', '--smooth', type=int, default=0, |
| 96 | + help='平滑窗口大小,推荐3或5,0为不平滑 (默认: 0)') |
| 97 | + |
62 | 98 | args = parser.parse_args() |
63 | | - |
| 99 | + |
| 100 | + if args.output is None: |
| 101 | + from pathlib import Path |
| 102 | + |
| 103 | + smooth_str = '' |
| 104 | + if args.smooth is not None: |
| 105 | + smooth_str = f"_smooth_{args.smooth}" |
| 106 | + |
| 107 | + |
| 108 | + input_path = Path(args.input) |
| 109 | + # 在文件名末尾添加 color_num_{颜色数量}_{模式} |
| 110 | + new_filename = f"{input_path.stem}_color_num_{args.num_colors}_{args.mode}{smooth_str}{input_path.suffix}" |
| 111 | + args.output = str(input_path.parent / new_filename) |
| 112 | + |
64 | 113 | if not 2 <= args.num_colors <= 256: |
65 | 114 | raise ValueError("颜色数量必须在2到256之间") |
66 | | - |
67 | | - reduce_colors(args.input, args.output, args.num_colors) |
| 115 | + |
| 116 | + reduce_colors(args.input, args.output, args.num_colors, args.mode, args.smooth) |
0 commit comments