前段时间在社区一个AI主题的创造营里,我做了一件很“朋友圈风”的小工具:用Wechaty搭一个机器人,用户把一张照片发给它,它自动把照片处理成爱心形状的九宫图,然后按顺序把九张图发回来。发朋友圈的时候九张拼一起,就是一颗完整的爱心。听起来不算复杂,但真正从“能跑”到“能用”,中间踩了不少坑。这篇就把整个思路、代码、坑和调整过程一起写出来,给打算用Wechaty做图片类小工具的朋友做个参考。
如果你只是想找个现成方案,那其实没必要读下去;但如果你打算自己动手做一个类似的机器人,或者已经在用Wechaty做图像处理相关的功能,这篇文章应该能帮你少走不少弯路。我会从需求拆解讲起,把图片收发、蒙版切图、发送顺序这几个核心环节掰开揉碎,最后再聊一些上线部署时容易忽略的细节。
1. 从朋友圈九宫格玩法聊起:需求拆解与方案选型
1.1 这个机器人到底做了什么
先明确一下“爱心九宫格生成器”到底是干什么的。用户在微信里给机器人发一张图,机器人后台拿到这张图,把图片裁成正方形,套上一个爱心形状的蒙版,让图片中间区域变成一颗爱心、四周变成透明背景。然后再把这张带透明通道的大图平均切成九小块,按特定顺序通过聊天窗口发回给用户。用户保存这九张图,按顺序发朋友圈,九宫格拼起来就是一颗完整的爱心照片。
这里有几个技术点其实是分离的:微信消息的接入和图片收发,由Wechaty负责;爱心蒙版、裁切、九宫格切分,由图像处理库负责;最后的顺序发送,又要回到Wechaty的消息发送接口。所以整个项目本质上是一个“消息流 + 图像流水线”的组合,难度不高,但对细节要求不少。
我做这个工具的初衷很简单,就是想在创造营里做一个和微信生态结合、又能在几周内跑通的小项目。朋友圈九宫格是大家都熟悉的东西,爱心形状又是最容易出效果的视觉元素,天然适合演示。
1.2 为什么选Wechaty而不是小程序或网页工具
需求确定之后,我其实先纠结了一阵子:这个功能用小程序做行不行?用网页工具做行不行?后来还是选了Wechaty。原因主要在于“使用路径”。
如果做成小程序,用户得搜索、打开、授权、上传图片、再下载九张图,路径太长了。如果做成网页工具,用户得先保存照片、再打开浏览器上传,同样麻烦。而微信机器人是完全相反的体验:用户就在聊天窗口里,发一张图出去,收到九张图回来,全程不需要离开微信。这个体验对朋友圈场景来说几乎是零成本的。
Wechaty本身是开源的聊天机器人框架,支持多种消息协议,也提供了统一的Message、Contact、FileBox等抽象。对于图片类小工具来说,它的“图片接收”和“文件发送”能力刚好够用,不需要自己处理底层的微信协议细节。
我不建议一上来就自己搞协议解析和消息模拟,那是另一个深坑。老老实实用框架,把精力放在图像处理和流程稳定性上,才是做这种小工具的正确姿势。
| 方案 | 用户操作路径 | 开发成本 | 适合场景 |
|---|---|---|---|
| 微信小程序 | 搜索、授权、上传、下载 | 需要注册开发者账号,审核周期长 | 长期商业化产品 |
| 网页H5工具 | 存图、打开浏览器、上传、下载 | 需要部署Web服务 | PC端或活动运营 |
| Wechaty机器人 | 直接发图、收图 | 较低,本地/服务器即可运行 | 熟人社交、朋友圈工具、尝鲜体验 |
从表里能看出来,如果只想快速验证一个小创意,Wechaty的路径最短。当然,个人号接入自动化有合规风险,做这个项目建议只在测试环境和小范围试用,不要大规模对外使用。
2. 机器人如何收到图片并完成落盘:Wechaty消息接入的几个关键点
2.1 接收图片消息的必要判断
Wechaty的接入套路很简单,监听message事件,判断消息类型,如果是图片就走处理流程。但这里有几个初学者容易忽略的点。
第一,必须判断msg.type()是不是图片类型。微信里消息类型很多,文本、图片、语音、视频、链接、表情,如果不做类型判断,后面的FileBox调用会直接报错或者拿到一个空对象。
第二,图片消息里拿到的文件有两种,缩略图和大图。如果直接取toFileBox(),得到的是当前消息对应的原始文件对象,但实际传输可能被微信压缩过。一般来说,通过msg.toFileBox()拿到的文件,再调用fileBox.toFile()落地,即可得到一个可处理的图片文件。
第三,下载过程可能是异步的,文件没落地之前不要急着做图像处理。很多第一次写这种工具的朋友,会忘记等待下载完成,结果Pillow打开文件时拿到的还是一个空文件或者损坏文件。
下面是一段极简的消息监听核心逻辑,用的Wechaty TypeScript接口来示意:
import { WechatyBuilder, Message } from 'wechaty' const bot = WechatyBuilder.build({ name: 'heart-grid-bot', }) bot.on('message', async (msg: Message) => { if (msg.self()) return if (msg.type() !== MessageType.Image) { await msg.say('发一张照片给我,我来帮你做爱心九宫图') return } const fileBox = await msg.toFileBox() const savedPath = `/tmp/heart-grid/${Date.now()}.jpg` await fileBox.toFile(savedPath, true) // 这里再调用图像处理函数,处理完再逐张发送 await processAndSend(msg, savedPath) }) bot.start()具体到微信不同类型的图片,落地之后的格式可能是jpg也可能是gif。我们做蒙版处理时,GIF动图要先取第一帧或者只处理静态部分,否则后面Pillow打开时可能报错或者得到多帧对象。这个我在后面踩坑部分会展开。
2.2 下载超时、缩略图与缓存目录
图片下载有很多“看似很小、实际很伤”的细节,我逐个说一下。
第一,下载超时。如果图片比较大,或者网络不稳定,toFile()可能迟迟不返回,甚至超时。我一开始没有设置超时,结果有用户在弱网环境发图,整个流程卡在那里,后续消息全部堆积。后来我在外层包了一个超时函数,超过15秒就主动放弃并给用户提示“图片下载失败,请重试”。
第二,缓存目录清理。因为每次都要写到临时目录,时间久了会堆积大量图片文件。我建议每次处理完就把源文件删除,只保留最终发送出去的九张图的临时文件,等消息发送成功后再批量清理。如果不清理,跑几天就能攒下上百MB的垃圾。
第三,文件名不能重复。很多机器人项目用固定文件名落盘,比如temp.jpg,如果同时收到两个人的图片,后来的会覆盖之前的,导致处理结果错乱。我后来统一用时间戳 + 随机后缀,避免并发时的文件冲突。
说到并发,这里先提一个很重要的设计:微信消息本身是顺序进入监听回调的,但如果你的处理函数是异步的,两条消息可能几乎同时处理。此时如果两个人同时发图,临时文件就可能交叉读写。最简单的办法是加一个简单的“处理中”锁,同一时间只处理一张图。虽然牺牲了一点并发能力,但对这种个人小工具来说,稳定性远比吞吐量重要。
3. 把照片变成一颗“爱心”:蒙版、裁切与九宫格切图
3.1 爱心蒙版的生成思路
整个工具的核心其实是图像处理,而图像处理的核心是蒙版。我们的目标是把照片变成爱心形状,四周完全透明,所以必须用带Alpha通道的图像来存储结果,也就是PNG格式。
爱心蒙版本质上是一张黑白位图:白色区域代表“保留原图”,黑色区域代表“透明”。把照片和这张蒙版做一个合成运算,就能得到带爱心形状的透明图。这里有一个关键点:蒙版的边缘要做得平滑,否则切成九格之后,边缘会看到明显的锯齿。比如在蒙版生成后做一次高斯模糊,再根据阈值重新二值化,能得到更柔和的边缘;或者在绘制爱心时使用超采样,也就是把蒙版做大到2048甚至4096,再缩小到1024,这样抗锯齿效果非常好。
爱心形状怎么来?最简单的做法是直接用绘制函数生成。网上常见的爱心曲线公式是用参数方程:
x = 16 * sin(t)^3 y = 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t)在t从0到2π的范围内取足够多离散点,就能画出一个标准爱心轮廓。然后把这个轮廓填充成白色,背景设为黑色,就得到了一张爱心蒙版。
用Pillow实现的时候,我建议先用ImageDraw在“L”模式的图上画轮廓,再填充。不要直接画在RGB图上,否则后面合成的时候还要转灰度,多一步反而容易出错。
from PIL import Image, ImageDraw import math def create_heart_mask(size=1024): im = Image.new('L', (size, size), 0) draw = ImageDraw.Draw(im) points = [] steps = 2000 for i in range(steps): t = 2 * math.pi * i / steps x = 16 * math.sin(t) ** 3 y = 13 * math.cos(t) - 5 * math.cos(2*t) - 2 * math.cos(3*t) - math.cos(4*t) # 映射到画布坐标,同时放大并留边距 px = int((x * size / 34) + size / 2) py = int((-y * size / 34) + size / 2 + size * 0.08) points.append((px, py)) draw.polygon(points, fill=255) return im代码里的几个系数不是随手拍的,34是爱心函数在x和y方向的最大跨度,用来把爱心映射到整张画布上;size * 0.08是Y轴向下偏移,保证爱心在九宫格里看起来居中。这三个参数是调试裁切效果时花费时间最多的地方,后面实测部分会细说。
3.2 裁切到正方形后的合成与九格切分
蒙版有了,原图处理分成三步:缩放裁切、合成、切九格。
第一步,把原图先缩放到一个尽量小的尺寸,比如宽或高至少等于1024,然后取中间区域裁成正方形。注意,如果你不裁成正方形,爱心蒙版里会有大片空白区域来自动补成透明,切出来构图会很怪。裁切时可以稍微多留一点上半部分空间,因为大头照通常在上面,后面调取景时我还会再说。
第二步,合成。Pillow里最稳妥的方式是:
rgba = Image.new('RGBA', (1024, 1024), (0, 0, 0, 0)) heart = Image.composite(square_photo.convert('RGBA'), rgba, mask)这里square_photo是先处理好尺寸的方图,rgba是全透明底图,mask就是上面生成的爱心蒙版。Image.composite会依据蒙版的灰度值决定显示上层还是底层,白色区域显示照片,黑色区域显示透明。
第三步,切九格。把合成好的1024x1024大图按行列切成3x3个格子,每格大约341x341像素。切图本身不复杂,用crop即可,真正的坑在于“格子的边缘像素”和“发送顺序”是绑定的,这一点我在下一章单独说。
3.3 为什么要存成PNG而不是JPG
很多人切完图后顺手存成jpg,结果朋友圈发出来背景一片白色或者黑色,完全没有透明效果。原因是JPG格式根本不支持透明通道,透明部分会被填充成默认底色,通常是白色或黑色。
所以切完的九张图必须导出为PNG。同时要注意,有些聊天工具发送PNG时会自动压缩或者转码,实测下来微信PC端和手机端对PNG的容忍度比较高,但超大PNG偶尔也会被拒收,所以单张图片文件大小最好控制在1MB到2MB以内。一张1024x1024的PNG如果内容不复杂,通常只有几百KB,比较安全。
下面是切九格的核心代码,配合发送顺序一起看会更清楚:
def split_into_grid(heart_image, grid=3): width, height = heart_image.size cell_w = width // grid cell_h = height // grid tiles = [] for row in range(grid): for col in range(grid): left = col * cell_w upper = row * cell_h right = left + cell_w lower = upper + cell_h tile = heart_image.crop((left, upper, right, lower)) tiles.append(tile) return tiles这段代码返回的tiles列表顺序是“从左到右、从上到下”。但实际发送时,我强烈建议你反着发,原因看下一章。
4. 发回顺序、尺寸比例和发送频率:这三个坑我全踩了一遍
4.1 朋友圈“反序”的实测结果
九张图切好后,逻辑上是按左上、上中、右上、左中、中心……一直到右下这个顺序发给用户,朋友围发出来从上到下再从左到右,刚好是一张完整图。但我第一次实测时发现,朋友圈渲染出来的九宫格顺序完全反了。
我知道很多人看到这里会说“你直接倒序发送不就行了”。对,理论上就这么简单,但问题在于:微信在不同端、不同版本的排序规则可能不完全一致,不能只看一次结果就下结论。我当时在手机端和客户端分别测试,发现两种端里“接收到的九张图”在朋友圈里的排列顺序是相同的,都是第一条消息排在最后一位。这就意味着,代码里最后发送的那张图,会出现在朋友圈九宫格的左上角。
为了验证这个规则是稳定的,我连续用三个微信号各发了一轮测试图,确认是一致的。于是最终的做法很简单:代码里先发送右下角的格子,再发送下中、左下……最后发送左上角的格子。这样朋友圈里显示出来的顺序就是正常的九宫格顺序。你如果自己实现,建议先发一张测试图确认当前环境下的排序规则,再决定是否倒序。
这里有一个更稳妥的办法:不要直接在代码里写死顺序,而是把“顺序策略”抽成一个配置项,比如send_order字段。如果未来某个客户端版本改了规则,改配置即可,不用重新部署整套逻辑。
4.2 尺寸比例与透明区域的细节
九张图拼起来的时候,理想情况下每张图片之间是没有缝隙的。但实际保存图片时,如果切图尺寸不是严格的1:1正方形,或者发送过程中被聊天工具二次压缩,拼起来就会出现细缝或者边缘错位。
我实测下来的几个数据供参考:
- 原图处理尺寸建议用1024x1024,这样可以兼顾清晰度和文件大小;
- 切图后每张尽量保持正方形,微信对图片会做居中裁切展示,如果比例不正,边缘会被裁掉;
- 发送时不要给图片加白边、圆角或者阴影效果,任何额外修饰都会在拼接时产生视觉缝隙。
另外,透明背景的图片发到聊天窗口里,用户在对话框看到的是“灰色或白色格子”。很多人会误以为图片有问题,所以我在收到图片后的自动回复文案里加了一句“请选择九张图时,注意保留透明部分,不要套用滤镜或编辑”。这个小细节能省掉大量“图片坏了”的用户反馈。
4.3 频率控制与稳定性
连续发送九张图,其实是在很短的时间内发九条消息。微信对高频消息是有风控的,连续发图尤其容易触发限制。第一次上线时,我在循环里直接发送,没有做间隔,结果发到第五张的时候被系统拦了,用户只收到五张图,体验非常奇怪。
后来我在每次发送之间增加了一个随机延迟,基础延迟800毫秒,再加一个0到500毫秒的随机抖动。这样九张图发送完大概需要10秒左右,用户体验上稍微慢一点,但稳定性好了很多,没有再出现发送中断。
延迟代码大概是这样:
import time, random for tile in ordered_tiles: await send_image(tile) time.sleep(0.8 + random.random() * 0.5)注意,如果你的机器人是单进程处理消息,这个延迟会阻塞后续其他用户的处理请求。所以更合理的做法是,把发送任务放到队列后台执行,收到消息后立刻回复“收到,正在生成,请稍后”,然后异步去处理、再异步发送。这样用户不会觉得机器人卡住了,同时也能避免并发时延迟互相影响。
5. 实测效果与调整:从“能用”到“好用”的参数打磨
5.1 三个真实用户带来的问题
第一版在小范围群里放出来后,大概有三类反馈,每一类都指向一个具体问题。
第一类反馈是“图片主体位置不对”。有的人发的照片里人物偏左,爱心形状虽然正确,但九宫格拼出来后人物被爱心边缘切掉了一半脸。这个问题出在“无脑居中裁切”上:我们的裁切策略是取正方形中心区域,但很多竖构图照片的人物本来就在中间偏下或偏上位置,直接取中心就会砍掉关键信息。
第二类反馈是“图片内容太杂,爱心形状不明显”。有些用户发的是带有大量背景的照片,爱心区域的轮廓被淹没。这个倒不完全是问题,毕竟爱心是形状上的玩法,但如果照片太亮,边缘过渡就不明显,导致看起来像一张普通方格图。
第三类反馈最意外:有用户把九张图保存后,在“最近编辑”里直接点了滤镜,结果透明背景全部变成了黑色,九宫格拼出来是一颗黑心。本质上还是很多用户不理解透明通道,这一点只能靠文案提示了。
5.2 取景优化:帮用户自动找主体
为了解决“位置不对”的问题,我给裁切环节加了一个简单的主体检测。不是上深度学习模型,而是用了最基础的“显著性区域”判断:把原图缩到很小,比如64x64,转成灰度图,然后分成横三竖三的九块区域,计算每一块的像素方差。方差最大的一块,往往就是纹理最丰富、最可能包含主体的区域。
根据这个区域的位置,再决定裁切框向上、向下还是居中偏移。实现思路不复杂,效果却出乎意料地好,因为大多数用户照片的主体就是人脸或物体,纹理复杂度天然高于天空、墙壁这类纯色背景。
如果你对准确率要求更高,可以用现成的轻量级目标检测模型来定位人脸或物体,但为了一个练手项目,我建议先上“方差法”。够用、没有额外依赖、运行也快,一张图处理下来不到50毫秒。
5.3 异常兜底与用户提示
还有一类用户会上传一些奇怪的内容,比如截图、表情包、文档扫描件。这些图片在缩放裁切后,爱心效果往往不理想,但也不会让程序报错。更需要注意的是上传超长图、GIF动图、或者那种分辨率巨大的全景图。
我在处理前增加了一个参数检查步骤:图片格式不支持时,直接catch异常并回复“图片有点特殊,我没法处理,换一张试试”;图片文件过大时,先尝试压缩到长边不超过2000px再处理;图片是GIF时,只取第一帧。这些边界处理写起来不复杂,全部做完之后,工具的使用体验才真正让人觉得“稳了”。
6. 上线部署与扩展思路:把它从“练手作品”变成“常驻服务”
6.1 运行环境与挂机注意事项
做到这一步,工具在本地跑已经没问题了。但如果你想让它常驻运行,有几个部署层面的问题必须考虑。
第一,运行环境。微信机器人需要长期在线,所以你得有一台24小时开机的机器,树莓派、云主机或者旧手机都行。我建议不要放在Windows桌面机器上,因为休眠、断电都会让机器人掉线,而且重启后还要处理扫码重新登录的问题。
第二,自动重启。进程崩溃是不可避免的,用进程守护工具把机器人拉起,崩溃后自动重启。否则哪天晚上挂了,第二天白天用户消息全部丢失。
第三,日志与监控。多打日志,尤其是图片下载、处理耗时、发送耗时这三步。出问题时能快速定位是网络问题、图像处理问题还是发送被限制。
6.2 并发处理、隐私清理与内容安全
并发处理是这类机器人从小白走向可用的一道坎。最开始没有锁,两三个人同时发图,处理结果互相串了。后来我给每个会话加了串行锁,同一时间只处理一张图,其余消息排队等待。
隐私方面,因为这个工具会接收用户的相册照片,属于比较敏感的个人信息。我建议处理完图片后立即删除源文件和临时文件,不要长期留存。至少也要做到“每天定时清理临时目录”,并在日志中隐藏文件完整路径和用户名。
内容安全也要考虑。如果这个机器人开放给不认识的人使用,理论上别人可以发任何内容,你实际上是在帮他们处理和传播内容。对于个人练手项目,建议加白名单机制,只允许指定的联系人或测试群使用,别一上来就面向全量用户开放。
6.3 值得继续做的扩展方向
做完爱心九宫格,我手里掌握的核心能力其实是“接收图片 → 图像处理 → 异步发回多张图”,这套流程可以不变,只替换图片处理模块,就能扩展出很多玩法。
比如文字签名图,用户发一段文字,机器人生成一张好看的文字背景图再发回来。再比如纯色头像九宫格,用户选择喜欢的颜色,机器人自动生成一组渐变色的九宫格头像。要是再进一步,还可以做模板市场:爱心、兔子、圣诞、雪花,每个模板对应一套蒙版,用户发“爱心”或“圣诞”关键词来选择。
这次做完之后我最大的感受是:看起来很不起眼的小功能,真正要把细节全部处理好,工作量比想象中大得多。尤其是发送顺序和透明通道这两个问题,不实际发一轮朋友圈,光靠看代码是发现不了的。如果你也在做类似的Wechaty图片工具,建议先把“收图—处理—发送”三个环节各自独立测试,再串起来联调,最后再做小范围真实用户测试。这个顺序能帮你省掉大量排错时间。