Compare commits
6 Commits
f7212d6f67
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
27670920f6
|
|||
| e0268ec86b | |||
| 575cd43538 | |||
| cd010afc24 | |||
| c2161635a8 | |||
| f21b7067df |
BIN
assets/img/meme/vr.jpg
Executable file
BIN
assets/img/meme/vr.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@ -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 存入图像 <目标名称>```
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -65,6 +65,8 @@ class ImageFilterManager:
|
||||
"覆盖图像": ImageFilterImplement.apply_overlay,
|
||||
# 生成式
|
||||
"覆加颜色": ImageFilterImplement.generate_solid,
|
||||
# Pixel Sort
|
||||
"像素排序": ImageFilterImplement.apply_pixel_sort,
|
||||
}
|
||||
|
||||
generate_filter_map = {
|
||||
|
||||
@ -6,7 +6,7 @@ import PIL
|
||||
import PIL.Image
|
||||
import cv2
|
||||
import imageio.v3 as iio
|
||||
from nonebot_plugin_alconna import Alconna, Args, Image, Option, UniMessage, on_alconna
|
||||
from nonebot_plugin_alconna import Alconna, Args, Image, Option, Query, UniMessage, on_alconna
|
||||
import numpy
|
||||
|
||||
from konabot.common.nb.exc import BotExceptionMessage
|
||||
@ -34,7 +34,7 @@ cmd_giftool = on_alconna(
|
||||
Option("-t", Args["length", str]),
|
||||
Option("-to", Args["end_point", str]),
|
||||
Option("--speed", Args["speed_factor", float], default=1.0, alias=["-s"]),
|
||||
Option("--pingpong", default=False),
|
||||
Option("--pingpong"),
|
||||
)
|
||||
)
|
||||
|
||||
@ -47,7 +47,7 @@ async def _(
|
||||
length: str | None = None,
|
||||
speed_factor: float = 1.0,
|
||||
end_point: str | None = None,
|
||||
pingpong: bool = False,
|
||||
pingpong: Query[bool] = Query("pingpong"),
|
||||
):
|
||||
ss: None | float = None
|
||||
if start_point:
|
||||
@ -165,7 +165,7 @@ async def _(
|
||||
rdur_ms = rdur_ms[::-1]
|
||||
|
||||
# 处理 pingpong 模式
|
||||
if pingpong:
|
||||
if pingpong.available:
|
||||
# 复制一份反转的帧序列(去掉第一帧避免重复)
|
||||
pingpong_frames = rframes[1:][::-1] if len(rframes) > 1 else rframes[::-1]
|
||||
pingpong_durations = rdur_ms[1:][::-1] if len(rdur_ms) > 1 else rdur_ms[::-1]
|
||||
|
||||
@ -34,6 +34,7 @@ from konabot.plugins.memepack.drawing.saying import (
|
||||
draw_mnk,
|
||||
draw_pt,
|
||||
draw_suan,
|
||||
draw_vr,
|
||||
)
|
||||
from konabot.plugins.memepack.drawing.watermark import draw_doubao_watermark
|
||||
|
||||
@ -334,3 +335,29 @@ async def _(img: DepPILImage):
|
||||
result_bytes = BytesIO()
|
||||
result.save(result_bytes, format="PNG")
|
||||
await doubao_cmd.send(await UniMessage().image(raw=result_bytes).export())
|
||||
|
||||
|
||||
vrsay = on_alconna(
|
||||
Alconna(
|
||||
"vr说",
|
||||
Args[
|
||||
"saying",
|
||||
MultiVar(str, "+"),
|
||||
Field(missing_tips=lambda: "你没有写vr说了什么"),
|
||||
],
|
||||
),
|
||||
use_cmd_start=True,
|
||||
use_cmd_sep=False,
|
||||
skip_for_unmatch=False,
|
||||
aliases=set(),
|
||||
)
|
||||
|
||||
|
||||
@vrsay.handle()
|
||||
async def _(saying: list[str]):
|
||||
img = await draw_vr("\n".join(saying))
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format="PNG")
|
||||
|
||||
await vrsay.send(await UniMessage().image(raw=img_bytes).export())
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@ dasuan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "dss.png").convert(
|
||||
suan_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "suanleba.png").convert("RGBA")
|
||||
cute_ten_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "tententen.png").convert("RGBA")
|
||||
kio_image = PIL.Image.open(ASSETS_PATH / "img" / "meme" / "kiosay.jpg").convert("RGBA")
|
||||
vr_image = PIL.Image.open(ASSETS_PATH / 'img' / 'meme' / 'vr.jpg').convert("RGBA")
|
||||
|
||||
|
||||
def _draw_geimao(saying: str):
|
||||
@ -123,3 +124,24 @@ def draw_kiosay(saying: str):
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
@make_async
|
||||
def draw_vr(saying: str):
|
||||
img = vr_image.copy()
|
||||
w, h = img.size
|
||||
hw = 300
|
||||
|
||||
img2 = PIL.Image.new("RGBA", (w, h + hw), 'white')
|
||||
img2.paste(img, (0, hw))
|
||||
|
||||
with imagetext_py.Writer(img2) as iw:
|
||||
iw.draw_text_wrapped(
|
||||
saying, w // 2, hw // 2 + 15, 0.5, 0.5, w, 64, LXGWWENKAI_REGULAR,
|
||||
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
|
||||
1.0,
|
||||
imagetext_py.TextAlign.Center,
|
||||
draw_emojis=True,
|
||||
)
|
||||
|
||||
return img2
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
Reference in New Issue
Block a user