From cd010afc2434237ba282bdd4e8d8f9fb3bb2f77d Mon Sep 17 00:00:00 2001 From: pi-agent Date: Wed, 8 Apr 2026 14:20:46 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(fx):=20=E6=B7=BB=E5=8A=A0=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E6=8E=92=E5=BA=8F=20(Pixel=20Sort)=20=E6=BB=A4?= =?UTF-8?q?=E9=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 '像素排序' 滤镜,实现类似 Photoshop Pixel Sort 效果 - 支持水平/垂直方向排序 - 支持多种排序依据:亮度、色相、红/绿/蓝通道 - 支持自动阈值计算(使用图像亮度中位数) - 支持自定义遮罩阈值 - 支持反向排序 - 支持块大小参数 - 添加相关单元测试 --- konabot/plugins/fx_process/fx_handle.py | 134 +++++++++++++++++++++++ konabot/plugins/fx_process/fx_manager.py | 2 + tests/test_fx_process.py | 64 +++++++++++ 3 files changed, 200 insertions(+) diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index 58a23ad..8ce9129 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -1354,6 +1354,140 @@ class ImageFilterImplement: images.append(text_image) return image + # Pixel Sort - 像素排序效果 + @staticmethod + def apply_pixel_sort( + image: Image.Image, + direction: str = "horizontal", + threshold: float = 0.0, + auto_threshold: bool = True, + sort_by: str = "brightness", + mask_threshold: float = 128.0, + reverse: bool = False, + block_size: int = 1 + ) -> Image.Image: + """ + Pixel Sort 效果 + + 参数: + image: 输入图像 + direction: 排序方向,"horizontal"(水平) 或 "vertical"(垂直) + threshold: 亮度阈值 (0-255),低于此值的像素会被排序(仅在 auto_threshold=False 时生效) + auto_threshold: 是否自动计算阈值(使用图像中位数) + sort_by: 排序依据,"brightness"(亮度)、"hue"(色相)、"red"、"green"、"blue" + mask_threshold: 遮罩阈值 (0-255),决定哪些像素参与排序 + reverse: 是否反向排序 + block_size: 块大小,每 N 行/列作为一个整体排序单位 + """ + if image.mode != 'RGBA': + image = image.convert('RGBA') + + arr = np.array(image) + height, width = arr.shape[:2] + + # 获取排序属性 + def get_sort_value(pixel): + r, g, b = pixel[0], pixel[1], pixel[2] + if sort_by == "brightness": + return 0.299 * r + 0.587 * g + 0.114 * b + elif sort_by == "hue": + max_c = max(r, g, b) + min_c = min(r, g, b) + diff = max_c - min_c + if diff == 0: + return 0 + if max_c == r: + return 60 * (((g - b) / diff) % 6) + elif max_c == g: + return 60 * ((b - r) / diff + 2) + else: + return 60 * ((r - g) / diff + 4) + elif sort_by == "red": + return r + elif sort_by == "green": + return g + elif sort_by == "blue": + return b + return 0.299 * r + 0.587 * g + 0.114 * b + + # 自动计算阈值 + if auto_threshold: + # 使用图像亮度中位数作为阈值 + gray = np.array(image.convert('L')) + mask_threshold = float(np.median(gray)) + + # 创建遮罩:哪些像素需要排序 + mask = np.zeros((height, width), dtype=bool) + for y in range(height): + for x in range(width): + brightness = 0.299 * arr[y, x, 0] + 0.587 * arr[y, x, 1] + 0.114 * arr[y, x, 2] + mask[y, x] = brightness >= mask_threshold + + result = arr.copy() + + if direction.lower() in ["horizontal", "h", "水平"]: + # 水平排序(逐行) + for y in range(height): + # 收集当前行中需要排序的像素 + if block_size > 1: + # 按块处理 + for block_start in range(0, width, block_size): + block_end = min(block_start + block_size, width) + pixels = [] + indices = [] + for x in range(block_start, block_end): + if mask[y, x]: + pixels.append(arr[y, x].copy()) + indices.append(x) + if len(pixels) > 1: + # 按指定属性排序 + sorted_pixels = sorted(pixels, key=get_sort_value, reverse=reverse) + for i, x in enumerate(indices): + result[y, x] = sorted_pixels[i] + else: + # 逐像素处理 + pixels = [] + indices = [] + for x in range(width): + if mask[y, x]: + pixels.append(arr[y, x].copy()) + indices.append(x) + if len(pixels) > 1: + sorted_pixels = sorted(pixels, key=get_sort_value, reverse=reverse) + for i, x in enumerate(indices): + result[y, x] = sorted_pixels[i] + + elif direction.lower() in ["vertical", "v", "垂直"]: + # 垂直排序(逐列) + for x in range(width): + if block_size > 1: + # 按块处理 + for block_start in range(0, height, block_size): + block_end = min(block_start + block_size, height) + pixels = [] + indices = [] + for y in range(block_start, block_end): + if mask[y, x]: + pixels.append(arr[y, x].copy()) + indices.append(y) + if len(pixels) > 1: + sorted_pixels = sorted(pixels, key=get_sort_value, reverse=reverse) + for i, y in enumerate(indices): + result[y, x] = sorted_pixels[i] + else: + pixels = [] + indices = [] + for y in range(height): + if mask[y, x]: + pixels.append(arr[y, x].copy()) + indices.append(y) + if len(pixels) > 1: + sorted_pixels = sorted(pixels, key=get_sort_value, reverse=reverse) + for i, y in enumerate(indices): + result[y, x] = sorted_pixels[i] + + return Image.fromarray(result, 'RGBA') + class ImageFilterEmpty: diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index 2b8fedd..9100fb4 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -65,6 +65,8 @@ class ImageFilterManager: "覆盖图像": ImageFilterImplement.apply_overlay, # 生成式 "覆加颜色": ImageFilterImplement.generate_solid, + # Pixel Sort + "像素排序": ImageFilterImplement.apply_pixel_sort, } generate_filter_map = { diff --git a/tests/test_fx_process.py b/tests/test_fx_process.py index 69b00cc..4495733 100644 --- a/tests/test_fx_process.py +++ b/tests/test_fx_process.py @@ -86,3 +86,67 @@ def test_prase_input_args_parses_resize_second_argument_as_float(): assert len(filters) == 1 assert filters[0].name == "缩放" assert filters[0].args == [2.0, 3.0] + + +def test_apply_pixel_sort_keeps_image_mode_and_size(): + """测试 Pixel Sort 保持图像的 mode 和 size""" + image = Image.new("RGBA", (10, 10), (255, 0, 0, 128)) + + result = ImageFilterImplement.apply_pixel_sort(image) + + assert result.size == image.size + assert result.mode == "RGBA" + + +def test_apply_pixel_sort_horizontal(): + """测试水平方向的 Pixel Sort""" + # 创建一个简单的渐变图像 + image = Image.new("RGB", (5, 3)) + # 第一行:红到蓝渐变 + image.putpixel((0, 0), (255, 0, 0)) + image.putpixel((1, 0), (200, 0, 0)) + image.putpixel((2, 0), (100, 0, 0)) + image.putpixel((3, 0), (50, 0, 0)) + image.putpixel((4, 0), (0, 0, 255)) + # 填充其他行 + for y in range(1, 3): + for x in range(5): + image.putpixel((x, y), (128, 128, 128)) + + result = ImageFilterImplement.apply_pixel_sort( + image, direction="horizontal", auto_threshold=False, mask_threshold=10 + ) + + assert result.size == image.size + assert result.mode == "RGBA" + + +def test_apply_pixel_sort_vertical(): + """测试垂直方向的 Pixel Sort""" + image = Image.new("RGB", (3, 5)) + # 第一列:绿到红渐变 + image.putpixel((0, 0), (0, 255, 0)) + image.putpixel((0, 1), (0, 200, 0)) + image.putpixel((0, 2), (0, 100, 0)) + image.putpixel((0, 3), (0, 50, 0)) + image.putpixel((0, 4), (255, 0, 0)) + # 填充其他列 + for y in range(5): + for x in range(1, 3): + image.putpixel((x, y), (128, 128, 128)) + + result = ImageFilterImplement.apply_pixel_sort( + image, direction="vertical", auto_threshold=False, mask_threshold=10 + ) + + assert result.size == image.size + assert result.mode == "RGBA" + + +def test_prase_input_args_parses_pixel_sort_arguments(): + """测试解析 Pixel Sort 参数""" + filters = prase_input_args("像素排序 horizontal 0 false brightness 128 false 1") + + assert len(filters) == 1 + assert filters[0].name == "像素排序" + assert filters[0].args == ["horizontal", 0.0, False, "brightness", 128.0, False, 1] From 575cd43538af154dfa8d61387002193820c39de1 Mon Sep 17 00:00:00 2001 From: pi-agent Date: Wed, 8 Apr 2026 15:50:31 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=E6=8E=92=E5=BA=8F=E6=BB=A4=E9=95=9C=E7=9A=84=20man=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/fx.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/konabot/docs/user/fx.txt b/konabot/docs/user/fx.txt index c7ea93d..552d0b0 100644 --- a/konabot/docs/user/fx.txt +++ b/konabot/docs/user/fx.txt @@ -79,6 +79,14 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx JPEG损坏 <质量=10>``` * 质量范围建议为 1~95,数值越低,压缩痕迹越重、效果越搞笑。 * ```fx 动图 <帧率=10>``` +* ```fx 像素排序 <方向=horizontal> <阈值=0> <自动阈值=true> <排序依据=brightness> <遮罩阈值=128> <反向=false> <块大小=1>``` + * 对像素按指定属性进行排序,效果类似 Photoshop/GIMP Pixel Sort。 + * **方向**:horizontal(水平)/ vertical(垂直) + * **排序依据**:brightness(亮度)/ hue(色相)/ red / green / blue + * **自动阈值**:true 时使用图像亮度中位数作为遮罩阈值 + * **遮罩阈值**:决定哪些像素参与排序(亮度 >= 阈值) + * **反向**:true 时从亮到暗排序 + * **块大小**:每 N 行/列作为一个整体排序单位 ### 多图像处理器 * ```fx 存入图像 <目标名称>```