
在古文字文创、字体设计、文化科普类项目里有一个出现频率很高的需求把甲骨文、金文、篆书等古文字当作视觉素材排版成图案而不是简单地排成一句话。常规做法是把文字拉成一行或者做成文字云但视觉冲击力总差一点。真正有意思的做法是把古文字当作“画笔单元”让它们按照目标图像的明暗、轮廓、色块分布去排列最终远看是一张图近看全是古文字。这篇文章将完整拆解这套方案基于 Python Pillow NumPy从零实现一个“古文字拼图生成器”。内容包括文字渲染、字形复杂度评估、亮度网格映射、边缘轮廓模式、旋转与彩色增强以及工程落地时容易踩的坑。新手可以照着跑通流程有图像处理基础的开发者也可以直接拿去改造。1. 什么是“古文字拼图”1.1 核心概念古文字拼图本质上属于“字符画”或“文字拼贴画”的一种变体。它的基本思路是先把目标图片压缩成一个小尺寸的网格然后读取每个网格的亮度或边缘信息再根据这些信息从预先准备好的古文字候选集中挑选合适的文字最后把文字逐个渲染到画布上。举个例子你想用“日、月、山、水、木、火、土、金”这些篆书文字拼出一张山的剪影。山体阴影较暗的区域就放入笔画较多、字形饱满的文字天空较亮的区域就放入笔画少、留白多的文字。最终得到的效果是整体轮廓是山局部细节全是古文字。这里要区分几个容易混淆的概念古文字拼图不是 OCR不做文字识别也不还原碑帖原文。它不是字体识别不需要判断某个字属于哪种古文字体系。它更接近“以字形为像素”的生成式视觉排版核心是图像处理与文字渲染的组合。1.2 应用场景这类技术可以用在很多方向文创海报设计把《诗经》里的字拼成山水、祥云、瑞兽轮廓。文化科普内容配图把甲骨文常用字拼成对应器物形状帮助读者建立字形与实物的联想。字体展示用一套古文字体做“字形密度热力图”展示不同文字的笔画复杂程度。新媒体配图把品牌名或核心图案用古文字拼出来兼顾文化感与识别度。教学演示在文字学课程中用拼图展示“象形字”的图画属性。1.3 实现思路总览整个实现可以拆成 5 步准备支持古文字的字体文件以及一组候选古文字字符。用 Pillow 把每个字符渲染成小图并计算该字符的“笔画密度”。读取目标图片灰度化后缩放到网格尺寸得到每个格子的亮度值。根据亮度从候选字符池中选取字符让暗区域对应笔画密的字亮区域对应笔画疏的字。把所有字符渲染到一张高清大图上输出最终拼图。这套思路不依赖深度学习模型不要求 GPU普通办公电脑即可运行核心依赖只有 Pillow 和 NumPy。2. 技术选型与整体流程2.1 为什么选择 PythonPython 在图像处理领域有天然优势Pillow 提供完善的字体渲染与图像合成能力NumPy 可以高效处理二维亮度矩阵生态成熟代码量少。相比前端 Canvas 方案Python 脚本更适合批量生成、命令行调用和后续接入自动化流程。2.2 核心依赖本文主要用到以下库依赖库用途是否必需Pillow字体渲染、图像读取、图像合成必需NumPy网格矩阵运算、密度统计必需tqdm进度条显示可选argparse命令行参数解析可选如果你本机还没安装可以在终端执行pip install pillow numpy如果需要运行带进度条的版本再补一个pip install tqdm2.3 整体处理流程用文字描述流程如下输入目标图片路径、古文字字体路径、候选字符集。预处理阶段目标图片转为灰度图缩放到grid_size * grid_size。采样阶段计算每个网格的亮度值亮度范围归一化到 0 到 10 表示纯黑1 表示纯白。候选字池构建对每个古文字渲染后再采样计算黑色像素占比作为该字的“视觉密度”。映射阶段暗网格选择视觉密度高的字亮网格选择视觉密度低的字。合成阶段按网格坐标把选中的字渲染到一张高清画布上。输出保存最终 PNG 图片。3. 环境准备3.1 安装依赖建议使用 Python 3.9 及以上版本。低版本 Python 也可以运行但 Pillow 的字体接口在高版本中更稳定。安装命令pip install pillow numpy如果希望代码里有进度提示可以再安装 tqdmpip install tqdm3.2 准备古文字字体这是整个项目里最关键的一步。古文字拼图的效果高度依赖字体你需要一个支持目标古文字的 TTF 或 OTF 字体文件。需要注意几点普通黑体、宋体不支持甲骨文、金文等古文字字符直接使用会出现方框。需要安装带有对应 Unicode 编码的古文字字体或支持 PUA 私用区的字体。字体文件可能涉及版权非商业项目请优先选择开源字体商业项目务必确认授权范围。本文示例代码以“支持篆书风格字形”的字体为例。你可以在字体官网或开源字体库中寻找合适的 TTF 文件放到项目的fonts目录下。3.3 项目目录结构建议按下面的结构组织文件glyph_art/ ├── fonts/ │ └── custom_zhuan.ttf # 古文字字体按实际文件名调整 ├── input/ │ └── target.png # 目标图片 ├── output/ │ └── result.png # 输出图片 └── glyph_art.py # 主脚本如果你的目标图片不是现成的也可以先用脚本生成一张简单的测试图后面第 5 节会给出测试方案。4. 核心代码实现4.1 单字渲染与居中处理先实现一个最基础的工具函数把单个古文字渲染成正方形小图并且尽量让文字在图片中居中。from PIL import Image, ImageDraw, ImageFont def render_char(char: str, font: ImageFont.FreeTypeFont, size: int): 渲染单个字符到正方形画布。 :param char: 单个字符 :param font: Pillow 字体对象 :param size: 画布边长 :return: mode 为 L 的 PIL Image白底黑字 img Image.new(L, (size, size), 255) draw ImageDraw.Draw(img) # 获取文字实际占用的包围盒 bbox draw.textbbox((0, 0), char, fontfont) text_w bbox[2] - bbox[0] text_h bbox[3] - bbox[1] # 根据包围盒计算居中偏移 x (size - text_w) // 2 - bbox[0] y (size - text_h) // 2 - bbox[1] draw.text((x, y), char, fontfont, fill0) return img这里用textbbox而不是旧的draw.textsize是因为textsize在较新的 Pillow 版本中已经标记为过时。textbbox需要 Pillow 8.0 以上版本如果你的环境报错AttributeError: ImageDraw object has no attribute textbbox请升级 Pillowpip install --upgrade pillow4.2 字形密度评估不同古文字的视觉重量差异很大。比如“一”字形简单留白多“龍”字形复杂墨迹占比高。我们需要一种自动评估方式把候选字符按“视觉密度”排序。实现思路把渲染好的字符图转为 NumPy 数组统计颜色小于 128 的像素占比。import numpy as np def glyph_density(glyph_img: Image.Image) - float: 计算一个字符图像中黑色像素占比用于衡量字形复杂程度。 arr np.asarray(glyph_img, dtypenp.float32) return float((arr 128).mean())接着遍历候选字符集合构建一个新的候选池def build_glyph_pool(font: ImageFont.FreeTypeFont, chars: str, cell_size: int): 根据字符的视觉密度建立候选池并从小到大排序。 :return: 列表每个元素是 (density, char, glyph_img) pool [] for ch in chars: if ch.strip() : continue img render_char(ch, font, cell_size) density glyph_density(img) pool.append((density, ch, img)) # 按密度升序排列密度小表示笔画少、留白多 pool.sort(keylambda x: x[0]) return pool为什么要按密度排序因为后续映射时目标图片的亮暗区域需要对应到不同密度的文字。暗的区域要选“墨迹密”的字亮的区域要选“留白多”的字。通过排序我们可以把字符池想象成一把“密度标尺”。4.3 图片网格化与亮度提取目标图片需要先转换为灰度图再缩小到grid_size * grid_size的尺寸这样每个像素就对应一个网格单元。def load_brightness_grid(image_path: str, grid_size: int) - np.ndarray: 读取目标图片并返回网格化后的亮度矩阵。 :return: shape 为 (grid_size, grid_size)取值 0 到 1 0 表示该区域接近黑色1 表示接近白色 img Image.open(image_path).convert(L) img_small img.resize((grid_size, grid_size)) arr np.asarray(img_small, dtypenp.float32) return arr / 255.0这里使用了 Pillow 默认的重采样算法对于大多数形状明显的图片已经足够。如果你希望轮廓更锐利可以在resize中传入Image.Resampling.LANCZOSimg_small img.resize((grid_size, grid_size), Image.Resampling.LANCZOS)4.4 亮度映射到古文字现在进入核心映射逻辑。基本规则是目标网格越暗选择 Density 越大的文字目标网格越亮选择 Density 越小的文字。为了避免相邻区域重复使用同一个字可以在索引计算中加一点随机扰动。def pick_char_index(density_list, brightness: float, random_jitter: bool True): 根据亮度值从密度列表中选取字符索引。 brightness 接近 0 时选择密度大的字符 brightness 接近 1 时选择密度小的字符。 n len(density_list) raw_index int((1.0 - brightness) * (n - 1)) if random_jitter: # 随机扰动范围控制在 ±3 个索引避免大面积重复 jitter np.random.randint(-3, 4) raw_index jitter return max(0, min(n - 1, raw_index))这里n是候选字符数量。如果候选字符只有十几个扰动范围可以减小如果字符很多扰动可以适当加大。4.5 基础矩阵拼图有了前面的工具函数就可以实现核心的矩阵拼图函数。def build_matrix_art( image_path: str, font_path: str, chars: str, output_path: str, grid_size: int 64, cell_size: int 20, random_seed: int 42, ): np.random.seed(random_seed) # 1. 加载字体 font ImageFont.truetype(font_path, cell_size) # 2. 构建密度候选池 pool build_glyph_pool(font, chars, cell_size) if len(pool) 0: raise ValueError(候选字符集为空请检查字符输入) densities [p[0] for p in pool] # 3. 读取目标图片亮度矩阵 brightness load_brightness_grid(image_path, grid_size) # 4. 创建最终画布 canvas_w grid_size * cell_size canvas_h grid_size * cell_size canvas Image.new(RGB, (canvas_w, canvas_h), white) draw ImageDraw.Draw(canvas) # 5. 逐格渲染 for row in range(grid_size): for col in range(grid_size): b brightness[row, col] idx pick_char_index(densities, b) _, char, glyph_img pool[idx] x col * cell_size y row * cell_size # glyph_img 是 L 模式需要转换为 RGB 才能粘贴到画布 glyph_rgb glyph_img.convert(RGB) canvas.paste(glyph_rgb, (x, y)) canvas.save(output_path) print(f保存成功: {output_path})这个版本已经能跑通全流程但视觉效果比较朴素所有文字都是黑色排列规则没有旋转。实际项目中通常还需要加入轮廓模式和装饰增强。4.6 边缘轮廓模式矩阵拼图适合面积感强的图案如果目标图片的轮廓更重要例如想用古文字拼出一只鹿的侧面轮廓那么可以加入边缘检测模式。Pillow 自带FIND_EDGES滤镜不需要额外安装 OpenCVfrom PIL import ImageFilter def load_edge_grid(image_path: str, grid_size: int, threshold: float 0.1) - np.ndarray: 提取目标图片边缘并返回网格化后的边缘强度矩阵。 返回值越大表示该区域越接近边缘。 img Image.open(image_path).convert(L) edges img.filter(ImageFilter.FIND_EDGES) img_small edges.resize((grid_size, grid_size)) arr np.asarray(img_small, dtypenp.float32) arr arr / 255.0 # 小于阈值的弱边缘直接归零减少噪点 arr[arr threshold] 0.0 return arr在边缘模式下映射逻辑要反过来边缘强度大的区域使用视觉密度较大的文字让轮廓“重”起来非边缘区域留白或者使用密度很小的文字作为底纹。def build_outline_art( image_path: str, font_path: str, chars: str, output_path: str, grid_size: int 64, cell_size: int 20, edge_threshold: float 0.1, random_seed: int 7, ): np.random.seed(random_seed) font ImageFont.truetype(font_path, cell_size) pool build_glyph_pool(font, chars, cell_size) densities [p[0] for p in pool] edge load_edge_grid(image_path, grid_size, edge_threshold) canvas_w grid_size * cell_size canvas_h grid_size * cell_size canvas Image.new(RGB, (canvas_w, canvas_h), white) for row in range(grid_size): for col in range(grid_size): e edge[row, col] # 边缘强度为 0 的格子直接跳过保留白底 if e 0: continue # 边缘越强选密度越大的文字 n len(densities) raw_index int(e * (n - 1)) jitter np.random.randint(-2, 3) idx max(0, min(n - 1, raw_index jitter)) _, char, glyph_img pool[idx] x col * cell_size y row * cell_size canvas.paste(glyph_img.convert(RGB), (x, y)) canvas.save(output_path) print(f保存成功: {output_path})4.7 旋转与彩色增强为了让拼图更有手作感可以给文字增加随机旋转和颜色变化。旋转需要先把文字渲染到透明背景上再用rotate旋转最后粘贴到画布。def render_char_with_rotation( char: str, font: ImageFont.FreeTypeFont, size: int, angle: int 0, ) - Image.Image: 渲染单字到透明背景并支持旋转。 # RGBA 透明背景 img Image.new(RGBA, (size, size), (0, 0, 0, 0)) draw ImageDraw.Draw(img) bbox draw.textbbox((0, 0), char, fontfont) text_w bbox[2] - bbox[0] text_h bbox[3] - bbox[1] x (size - text_w) // 2 - bbox[0] y (size - text_h) // 2 - bbox[1] # 文字填充为黑色但 alpha 保持不透明 draw.text((x, y), char, fontfont, fill(0, 0, 0, 255)) if angle ! 0: img img.rotate(angle, resampleImage.Resampling.BICUBIC, expandTrue) return img粘贴时可以先把目标网格区域的颜色采样出来然后给文字着色def colorize_glyph(glyph_rgba: Image.Image, color: tuple) - Image.Image: 把黑色文字替换成指定颜色。 r, g, b, a glyph_rgba.split() # 创建纯色图层用于替换黑色文字 color_layer Image.new(RGBA, glyph_rgba.size, color (255,)) result Image.composite(color_layer, Image.new(RGBA, glyph_rgba.size, (0, 0, 0, 0)), a) return result这样可以在合成时把目标图片对应位置的颜色采样出来让文字融入原图色调。5. 完整示例用古文字拼出圆形徽章在没有现成目标图片的情况下可以先用 Pillow 生成一张简单的黑白测试图验证流程是否跑通。5.1 先生成目标图片def create_demo_target(path: str input/target.png): 生成一个简单的圆形目标图方便测试。 img Image.new(L, (300, 300), 255) draw ImageDraw.Draw(img) draw.ellipse((50, 50, 250, 250), fill0) img.save(path)这段代码会在白色背景上画一个黑色圆形。5.2 运行拼图假设你准备的字体文件为fonts/custom_zhuan.ttf候选字符集包含常用的篆书风格字符if __name__ __main__: create_demo_target() chars 一山山水木火土金日月龍鳳龜麟天地玄黃宇宙洪荒 build_matrix_art( image_pathinput/target.png, font_pathfonts/custom_zhuan.ttf, charschars, output_pathoutput/result_matrix.png, grid_size64, cell_size20, random_seed42, ) build_outline_art( image_pathinput/target.png, font_pathfonts/custom_zhuan.ttf, charschars, output_pathoutput/result_outline.png, grid_size64, cell_size20, edge_threshold0.1, random_seed7, )5.3 预期效果result_matrix.png圆形内部由大量笔画复杂的古文字填充圆形外部基本被笔画简单的字覆盖形成明暗对比。result_outline.png白色画布上只有圆形边缘区域有古文字轮廓感明显内部是空白。如果圆形边缘的文字排列稀疏可以通过增大cell_size或降低edge_threshold来增强效果。6. 常见问题与排查思路问题现象常见原因解决思路所有字都变成方框字体文件不支持这些古文字字符更换支持对应 Unicode 段的古文字字体OSError: cannot open resource字体路径错误或字体文件损坏检查路径确认文件是有效 TTF/OTF输出图片全是黑色块字体笔画过粗或字符密度区分度不够换细笔画字体增加候选字数量调小cell_size运行速度很慢grid_size过大渲染字符数过多先小尺寸调试再逐步提高分辨率文字重复率太高候选字符集太少或扰动范围太小增加字符集适当加大 jitter 范围轮廓模式没有效果边缘阈值设置过高弱边缘被过滤调低edge_threshold或先保存边缘图检查Pillow 报textbbox不存在Pillow 版本低于 8.0升级 Pillow6.1 古文字字体出现方框这是最常见的问题。普通宋体、黑体的 Unicode 字形覆盖范围有限遇到甲骨文、金文、篆书扩展字符时无法显示。解决方案确认字体文件确实包含这些字符。使用专门的古文字字体。如果字符集来自私用区需要确保字体和字符编码一致。6.2 输出图案没有层次感层次感来自候选字符池的密度差异。如果所有字符都是笔画复杂的大字那么亮暗区域映射后区别不大。建议优先准备一组“笔画数跨度大”的字符例如一、人、山、水、木、火、土、金、龍、鳳、龜、麟、靈、龘这样亮度映射才有足够的中间梯度。6.3 图片分辨率与性能取舍假设grid_size 128cell_size 24最终画布是3072 * 3072像素需要渲染 16384 个字符。这个量级在普通电脑上可能需要几秒到几十秒视字体复杂度而定。调试阶段建议先使用grid_size 48确认构图满意后再生成高清版本。6.4 如何保留原图色彩想要生成彩色拼图可以在粘贴前对每个网格采样原图颜色def sample_color(image: Image.Image, col: int, row: int, grid_size: int): 从原图中采样某个网格的代表颜色。 small image.resize((grid_size, grid_size)) return small.getpixel((col, row))再把采样到的 RGB 颜色传给colorize_glyph。7. 最佳实践与工程建议7.1 字符集设计原则候选字符集决定了拼图的“语言质感”。建议按主题分组山水主题山、水、云、雨、川、谷、泉、石。祥瑞主题龍、鳳、龜、麟、鶴、鹿、象。时间主题日、月、星、辰、春、秋、寒、暑。字符数量最好在 30 个以上密度梯度才够丰富。7.2 自动密度排序不要手动调整字符密度程序自动计算黑色像素占比更可靠。换字体以后同样的字符在不同字体中的笔画粗细可能差异很大自动排序可以避免重新人工标记。7.3 字体版权古文字字体通常有明确的授权协议。开源字体可以放心用于个人项目但商业项目需要逐字确认授权范围。尤其注意免费版可能限制商业用途。部分字体禁止嵌入网页或打包发布。图片来源也要考虑版权最好使用自绘图或可商用素材。7.4 输出高清大图生成文创海报时建议把最终画布设置为目标打印尺寸的整数倍。例如需要打印 A4 大小分辨率 300 DPI那么画布宽度应为 2480 像素左右。可以先小尺寸确认构图再调大参数输出。7.5 脚本参数化为了让工具可复用建议把配置项抽成函数参数或配置文件。至少包含字体路径。候选字符集。输入输出路径。网格大小。单元格大小。边缘阈值。随机种子。随机种子固定后相同参数可以复现效果方便微调时对比。7.6 保留文字语义既然是“做另一种解读”生成结果最好附带一张“字符与密度对照表”避免拼图失去文字本来的语义价值。可以在输出图片下方留白加入图例或伴随生成一个legend.txt文件记录每个网格坐标对应的字符。8. 总结与下一步本文从零实现了一套古文字拼图工具核心能力包括使用 Pillow 渲染古文字识别字形密度。将目标图片网格化并提取亮度矩阵。根据亮度、边缘强度映射到不同密度的古文字。支持矩阵模式和轮廓模式。支持旋转、彩色、随机扰动等视觉增强。你可以把grid_size从 64 改成 128或把字体换成金文风格效果会完全不同。下一步可以考虑加入语义分区不同区域使用不同主题的古文字比如“天空区域用日月星辰相关文字山川区域用山水草木相关文字”这样拼图在视觉之外又多了一层可读性。如果你正在做古文字相关的视觉探索建议先拿一张轮廓简单的图跑通全流程再逐步提升复杂度。