资讯详情 用Python自制艺术签名工具:从手写轨迹到透明PNG
📅 2026/10/10 5:35:14
1. 这个签名工具的目标两种写法的取舍有一次朋友让我帮忙设计一个电子签名我翻遍在线生成器不是下载附件带水印就是模板风格土到不行。后来干脆自己动手用 Python 写了一个艺术签名小工具输入名字自动生成或者用鼠标直接在画布上写几秒钟导出一张透明背景的 PNG。这篇就把它拆开讲一遍。先说清楚这个小工具到底解决什么问题。日常需要签名的地方远比想象中多PDF 合同、图片水印、拜年贺卡、视频片尾甚至公众号文章的排版点缀。大多数人去网上找“艺术签名生成器”会发现几个通病风格千篇一律、字体文件被强制付费、导出的图片还带平台水印。自己写一个工具最大的价值不是炫技而是能完全控制签名的每个像素。我定的目标很明确输入姓名就能生成“印刷体艺术签名”能用鼠标在画布上手写自己的签名并把它变成透明背景图片支持调整颜色、笔画粗细、倾斜角度、描边和背景透明度最终导出可直接叠加到任何照片或文档里的 PNG。功能定下来以后技术路线反而清晰了。Python 里能处理图像和鼠标事件的库不少但我只挑了三个Pillow 做最终渲染和合成OpenCV 做画布与轨迹捕获numpy 做轨迹插值和平滑。它们各有专长组合起来刚好覆盖从“写”到“出图”的全流程。1.1 核心工作流整个程序可以拆成两条并行的路线路线 A输入文字 → 加载签名风格字体 → 在透明画布上绘制文字 → 对文字像素做扭曲/旋转/描边 → 导出 PNG。路线 B打开 OpenCV 窗口 → 捕获鼠标左键拖动的坐标 → 对坐标序列进行平滑插值 → 绘制成笔画 → 通过 alpha 通道转透明 PNG。两条路线最后都输出一张带透明通道的 RGBA 图像。所以我在代码里把“导出”这个动作抽成了公共函数方便两种模式复用。这种方式比一开始就写复杂的界面要简单得多也更适合作为脚本快速跑通。1.2 为什么不用重量级框架可能有人会问用 PyQt 或者 Tkinter 做界面不是更顺手吗对于“签名小工具”这种场景界面反而是累赘。Tkinter 的 Canvas 能画图形但要把画布内容转成透明 PNG还得先保存再读取效率很低。PyQt 确实强大但为了一个签名工具引入几百兆的运行库没必要。OpenCV 的cv2.imshow加上鼠标回调三行代码就能生成一个可绘制画布而且回调里直接拿到像素坐标天然适合轨迹采集。Pillow 则承担了字体渲染和透明合成这两件事是 OpenCV 不擅长的。这也解释了为什么我们不用“单一库解决所有问题”的思维——让每一个库做它最核心的事代码反而简单。2. 环境搭建选对依赖库比写代码更省心如果电脑里还没装 Python先去官网下载安装包。Windows 用户特别留意安装界面上的 “Add Python to PATH”这一步一定要勾选否则后面在命令行执行pip的时候会提示找不到命令。macOS 用户则建议直接用brew install python3省去环境变量配置。装好 Python 后打开终端或命令提示符执行下面三条命令pip install pillow pip install numpy pip install opencv-python如果你的网络环境下载速度很慢可以临时用清华镜像源pip install pillow numpy opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple这三个库的版本兼容性其实很省心。Pillow 主流版本都能跑OpenCV 用opencv-python这个包名就是预编译好的不用自己折腾 CMake。唯一要提醒的是别同时安装opencv-python和opencv-contrib-python二者会互相覆盖文件导致cv2.imshow在部分机器上异常。2.1 三库分工背后的逻辑Pillow 负责文字渲染和透明合成。它读取 TTF/OTF 字体文件的能力非常成熟ImageDraw.text可以直接指定字体大小、颜色、锚点。OpenCV 负责画布和鼠标捕获。它的cv2.setMouseCallback能绑定一个全局回调函数每次鼠标移动都会触发坐标精度足够签名使用。numpy 则负责轨迹数据的数学处理。为什么要单独引入 numpy因为手写轨迹的坐标点往往是不均匀的快速划过时两帧之间可能隔了几十像素直接连线会产生折角。用 numpy 做插值和平滑可以把手写轨迹变成流畅的曲线这一步后续会详细讲。2.2 一个容易忽略的细节字体文件模式一的核心不是代码而是字体。如果电脑里没有像样的签名风格字体生成的签名再“艺术”也只像普通宋体加粗。Windows 自带的字体里ScriptMT Bold是常见的西文手写体但没法写中文。中文字体推荐两类楷体simkai.ttf和行书STXINGKA.TTF在 Windows 的C:/Windows/Fonts目录下能找到。macOS 用户可以用STKaiti。后面写代码时我会把字体路径做成列表逐个检测避免因为某台电脑缺字体直接崩溃。更稳妥的做法是让用户传入一个字体文件路径或者把需要的字体附件放在同目录下。3. 模式一字体自动生成优雅签名的实现细节模式一的思路是“用字体代替手写”。先加载一个看起来像手写的字体再在透明背景上绘制文字最后加一些细微的扭转和描边让文字显得不那么死板。这样生成的签名虽然不能完全替代真人手写但胜在一键生成、风格统一。下面是核心代码我会一边写一边解释每个参数的意义。建议你在自己的环境里原样跑一遍再根据自己的喜好调字体和参数。from PIL import Image, ImageDraw, ImageFont, ImageFilter import numpy as np def generate_font_signature( name: str, font_path: str, font_size: int 96, color(0, 0, 0, 255), bg_color(255, 255, 255, 0), outline_width: int 1, output_pathsignature.png, ): # 1. 确定画布大小 canvas_w, canvas_h 800, 320 canvas Image.new(RGBA, (canvas_w, canvas_h), bg_color) draw ImageDraw.Draw(canvas) # 2. 加载字体并测量文字尺寸 font ImageFont.truetype(font_path, font_size) bbox draw.textbbox((0, 0), name, fontfont) text_w bbox[2] - bbox[0] text_h bbox[3] - bbox[1] x0 (canvas_w - text_w) // 2 y0 (canvas_h - text_h) // 2 - bbox[1] # 3. 绘制描边在四个方向偏移1/2像素 if outline_width 0: for dx in range(-outline_width, outline_width 1): for dy in range(-outline_width, outline_width 1): if dx * dx dy * dy outline_width * outline_width: draw.text((x0 dx, y0 dy), name, fontfont, fillcolor) # 4. 绘制主文字 draw.text((x0, y0), name, fontfont, fillcolor) # 5. 对画布做轻微的像素位移模拟手写波动 arr np.array(canvas) height, width arr.shape[:2] shift_map np.sin(np.linspace(0, 4 * np.pi, width)) * 2 new_arr arr.copy() for y in range(height): offset int(shift_map[y % width]) if offset ! 0: new_arr[y, :] np.roll(arr[y, :], offset, axis0) result Image.fromarray(new_arr, RGBA) # 6. 保存 result result.filter(ImageFilter.SMOOTH_MORE) result.save(output_path) print(签名已保存到:, output_path)3.1 画布尺寸与居中公式代码里画布固定为 800x320字体 96 号。这个尺寸足够容纳四五个字的名字。如果你要写很长的英文名建议按文字实际宽高动态设置画布否则容易贴边。居中的关键是把textbbox返回的左上角坐标也考虑进去因为某些字体的左边界不是零直接忽略会导致文字偏左。3.2 描边为什么要循环偏移Pillow 没有直接提供描边参数传统做法是把文字画 8 次上下左右和四个对角各偏移一次。我们这里用循环覆盖 3x3 邻域并且通过dx*dxdy*dy判断距离只取半径内的点这样描边更接近圆形笔触。描边颜色我特意用的是跟文字同色如果想要白字黑边把fill换成一个边框颜色主文字再画一次另一个颜色即可。3.3 像素位移的“手写感”np.roll对每一行像素做水平循环平移平移量由正弦函数决定于是文字边缘会产生类似行书那种“摇摇摆摆”的波动。幅度只有 2 像素肉眼看不出来明显错位但整体气质会和死板的打印体明显不同。这个技巧比用Image.transform做整体波浪扭曲更轻量也更容易控制幅度。4. 模式二手写轨迹捕获与美化处理如果字体图标不能满足你那就用鼠标亲手写。这一步我用 OpenCV 作为画布因为它自带窗口、键盘事件还不需要额外安装图形界面库。4.1 鼠标回调捕获坐标import cv2 import numpy as np points [] drawing False def on_mouse(event, x, y, flags, param): global drawing, points if event cv2.EVENT_LBUTTONDOWN: drawing True points [(x, y)] elif event cv2.EVENT_MOUSEMOVE and drawing: points.append((x, y)) elif event cv2.EVENT_LBUTTONUP: drawing False canvas np.zeros((480, 640, 4), dtypenp.uint8) cv2.namedWindow(Sign Here, cv2.WINDOW_NORMAL) cv2.setMouseCallback(Sign Here, on_mouse) print(按下左键写字按 ESC 结束) while True: # 显示当前画的点 display canvas.copy() if len(points) 1: pts np.array(points, dtypenp.int32) cv2.polylines(display, [pts], False, (255, 255, 255, 255), 3, cv2.LINE_AA) cv2.imshow(Sign Here, display) key cv2.waitKey(10) 0xFF if key 27: # ESC break cv2.destroyAllWindows()这里有个最容易犯的错误直接在鼠标回调里调用cv2.imshow会导致窗口刷新被阻塞所以我把绘制逻辑放在while循环里每次根据最新的points重新绘制画面。虽然重复画线性能有点浪费但签名轨迹最多几百个点完全够用。4.2 轨迹平滑与插值用手写板或者鼠标写字快速划过时相邻两个点之间距离可能超过 20 像素直接连线会看到明显的折线。解决方案是对坐标序列做插值。def smooth_points(points, step6): if len(points) 3: return points arr np.array(points, dtypenp.float32) # 计算相邻点之间的距离按累计长度重新采样 diff np.diff(arr, axis0) dist np.sqrt((diff ** 2).sum(axis1)) cumulative np.concatenate(([0], np.cumsum(dist))) # 在累计长度上生成固定步长的采样点 new_dist np.arange(0, cumulative[-1], step) new_x np.interp(new_dist, cumulative, arr[:, 0]) new_y np.interp(new_dist, cumulative, arr[:, 1]) return np.stack([new_x, new_y], axis1).astype(np.int32)这个函数的核心是利用累计距离重新采样而不是在索引上平均。想象一条笔画你慢速写的时候采集点密集快速勾尾时采集点稀疏如果在索引上做滑动平均密集区域的点会被稀释稀疏区域的点又得不到补偿。累计距离采样把所有点按物理路程等间距重排相当于给笔画重新“描”了一遍平滑效果自然得多。step参数控制插值间距我经验值是 6 到 10 像素太小会导致点太多太大则丢失细节。4.3 从白底画布转透明 PNGOpenCV 默认窗口背景是黑色但最终输出我们想要透明背景。做法是先把画布背景改成纯黑表示透明笔画画成白色然后在 Pillow 里把白色通道提取成 alpha。def cv_to_transparent(image): # 输入是 BGRA 四通道 b, g, r, a cv2.split(image) # 将背景全黑部分的 alpha 设为 0白色笔画 alpha 设为 255 gray cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) alpha cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY)[1] bgra cv2.merge([b, g, r, alpha]) # 转换成 PIL RGBA rgba cv2.cvtColor(bgra, cv2.COLOR_BGRA2RGBA) return Image.fromarray(rgba)这里阈值 10 是关键纯黑背景的灰度值接近 0笔画如果用了白色或彩色灰度值明显高于 10所以可以把笔画保留。如果你的画布是白色背景阈值需要改成 245 以上作为背景剔除但那样处理纯黑笔画时会出问题。所以我建议直接用黑底白笔画画最后再把白色变成 alpha这是最简单的路径。5. 把签名贴到任意图片PNG 合成与导出技巧有了透明签名的 PNG下一步就是把它用到真实场景。最常见的是把签名贴到照片、扫描件或者 PDF 页面上。Pillow 的alpha_composite可以完美处理但步骤上有几个细节。5.1 加载透明签名并缩放def paste_signature(base_image_path, signature_image_path, position(0, 0), scale1.0, rotation0): base Image.open(base_image_path).convert(RGBA) sig Image.open(signature_image_path).convert(RGBA) # 等比缩放 w, h sig.size new_w int(w * scale) new_h int(h * scale) sig sig.resize((new_w, new_h), Image.LANCZOS) # 旋转注意扩展画布避免裁边 if rotation ! 0: sig sig.rotate(rotation, expandTrue, resampleImage.BICUBIC) # 将签名粘贴到底图上 base.paste(sig, position, sig) return base粘贴的关键是第三个参数mask一定要传sig本身。因为签名图片是 RGBA 模式Pillow 会把 alpha 通道当作蒙版只有不透明部分才会粘到底图上透明部分自然透出底图。如果你不小心省略了这个参数签名会变成一个带黑底或白底的矩形块非常难看。5.2 摆放位置的经验值签名在文档里通常放在右下角。我一般把position设置为(base_width - sig_width - 50, base_height - sig_height - 30)留出页边距。如果是照片可以放在角落或者肩部空白区域。scale建议 0.8 到 1.0不要为了显眼刻意放大艺术签名讲究克制。5.3 导出 DPI 与常见误用如果用save直接存成 PNG新闻出版只需要分辨率足够。如果是打印建议设置 DPIbase.save(signed_doc.png, dpi(300, 300))PNG 的 DPI 信息会记录在文件头打印时才能按物理尺寸缩放。如果你把它贴在 Word 或 PDF 里实际上 Pillow 只负责像素输出最终排版由文档软件控制所以 DPI 只是一个辅助信息。另外不要把签名保存成 JPEG。JPEG 不支持透明通道就算硬转透明区域会自动填黑底或白底叠加到深色背景时会很突兀。这也是我最后统一输出 PNG 的原因。6. 踩坑记录与调参心得最后这部分是实际运行过程中的血泪经验。按照常见问题排查能帮你节省大量时间。6.1 字体识别失败导致方块最常见的问题是中文字体路径选错或者 Pillow 加载字体失败时报OSError。解决方法是写一个小函数从候选列表里找第一个存在的字体import os FONT_CANDIDATES [ C:/Windows/Fonts/STXINGKA.TTF, C:/Windows/Fonts/simkai.ttf, /System/Library/Fonts/STKaiti.ttf, /System/Library/Fonts/PingFang.ttc, ] def find_font(): for path in FONT_CANDIDATES: if os.path.exists(path): return path raise FileNotFoundError(请安装一个可用的中文字体文件)强烈建议把字体文件放在项目目录下比如fonts/然后使用绝对路径这样无论在哪个机器上跑都不会遇到系统字体差异问题。6.2 手写笔画断裂与漂移鼠标在屏幕上写字时OpenCV 回调的坐标是用户屏幕坐标不是你画布的像素坐标。如果你的窗口被系统缩放鼠标回调拿到的坐标和cv2.imshow显示的位置可能不一致导致笔画漂移。解决方法是关闭 Windows 的高 DPI 缩放或者直接使用cv2.namedWindow并指定窗口大小为实际画布尺寸。还有一种情况鼠标快速甩动时两点之间的距离过大插值后还是直线。可以把距离阈值设为 40 像素超过阈值就在两点之间做三次贝塞尔插值避免折角。6.3 边缘白边与羽化手写轨迹转透明后笔画边缘往往有一圈很淡的白边。这是因为 OpenCV 绘制线条时带有抗锯齿半透明像素被threshold变成了全透明导致边缘变脆。解决办法是给 alpha 通道做一次轻度高斯模糊然后重新阈值效果会柔和很多alpha cv2.GaussianBlur(alpha, (5, 5), 0) alpha cv2.threshold(alpha, 128, 255, cv2.THRESH_BINARY)[1]这一步之后笔画边缘会有几个像素的渐隐效果看起来更像真实笔墨。如果追求更柔和的边缘可以不阈值化直接保留模糊后的 alpha但要小心整体会变淡。6.4 参数调整实验我调试时最常用的参数组合字体模式用 96 号字号描边宽度 1正弦波动幅度 2。手写模式用 3 像素线宽插值步长 8阈值 10。你可以把scale从 1.0 降到 0.7让签名缩进文档后更精致。另外签名颜色我习惯从黑色改为(30, 30, 30, 220)看起来不像纯黑那么生硬。最后再分享一个小技巧如果你只想要一个自己的签名当水印不要每次都启动程序手写。把轨迹数据保存成 JSON 文件下次直接加载用比绘制更小的笔画渲染出来就能得到一枚永远不变的专属电子签名。这套思路稍微扩展一下还能做成批量给文件加签名水印的小工具实用性相当高。