FEATURED · 精选文章

用Python和Pillow实现古文字拼图:从概念到实战

发布时间 / 2026/9/1 11:26:44
来源 / 创域科博编辑部
栏目 / 资讯中心
用Python和Pillow实现古文字拼图:从概念到实战 古籍数字化项目里经常会出现一个很直观的需求如何把甲骨文、金文、篆书这类古老文字用更有视觉冲击力的方式呈现出来传统做法是把每个字截图后放到 PS 里手动排版但一旦文字数量超过几十个手动调整位置、大小、旋转角度会变得非常繁琐后期修改成本也很高。本文尝试用 Python 实现一套“古文字拼图”的生成方案把文字按照预设轮廓图排列成一张整体图形用技术手段完成另一种意义上的“图像化解读”。这个主题很适合对 Python 图像处理、数字人文、文字可视化感兴趣的开发者。读完本文后你能掌握 Pillow 的基本绘图流程理解“遮罩采样 随机画字”的核心思路并拿到一套可以直接运行的完整代码。文章会从概念、环境、原理、实战、进阶、排错一直讲到工程建议尽量把每个环节说清楚。1. 项目背景与核心概念1.1 什么是“古文字拼图”古文字拼图简单说就是把一批单字按照某种形状排列使整体看起来像一幅画、一个字或一个文化符号。它和常见的“词云/字云”有相似之处都是通过文字密度和空间分布来形成视觉图形但区别也很明显词云通常按词频决定字号大小而古文字拼图更强调文字本身的文化含义、字形美感以及字符之间的空间关系。比如把“山、水、人、木、火、土、日、月、风、雨”这些字拼成一个“山”字的轮廓视觉上既能看到“山”的外形又能通过细节发觉组成这个外形的细节文字。这种“远看是一个图形近看是一批文字”的双层阅读体验就是标题里所说的“另一种解读”。从技术角度看古文字拼图本质上是一个二维空间的离散排列问题我们有一个目标轮廓有一批待放置的文字需要在轮廓内部找到合适的位置以适当的大小和角度摆放文字。轮廓决定空间范围文字决定内部纹理两者叠加就是最终结果。1.2 这类项目能用在哪些场景古文字拼图的适用范围其实挺广并不只是“好看”。文化展览与海报设计把展品相关的古文字拼成器物轮廓作为展览主视觉。古籍内容可视化从古籍中提取高频字然后拼成书籍主题图形帮助读者快速感知文本气质。字体设计辅助用同一字体家族里的不同字符验证字形的统一感和空间密度。教学演示在汉字文化课堂上展示“文字本身就是图象”的特征。文创产品将拼图结果输出为明信片、书签、帆布袋图案。这些场景的共同点是“文字数量多、希望整体呈现有意义的外形、但手动排版成本太高”。这正是编程能发挥价值的地方。1.3 为什么用 Python Pillow 来实现Python 在图像处理领域生态成熟Pillow 是最常用的基础图像库提供了图片创建、绘制文字、旋转图层、像素读写等能力非常适合做这种中小规模的可视化项目。如果你只做一张图用 PS 或在线工具也能完成。但一旦涉及批量生成、参数调整、字体替换、不同主题组合脚本方案的优势就出来了你可以改一个参数重新跑一遍几分钟内得到几十个版本。当然也可以用专门的 wordcloud 库来生成字云但它对中文和古文字字体的控制不如直接基于 Pillow 灵活。本文选择 Pillow 作为主实现把底层逻辑讲清楚之后你再看 wordcloud 的源码或文档理解起来会容易很多。2. 环境准备与版本说明2.1 运行环境本文示例基于 Python 3.8 及以上版本开发测试。Pillow 的 API 在不同版本之间有一些细微变化尤其是anchor参数和文字包围盒计算建议使用较新的 Pillow 版本。示例代码在 Pillow 9.x、10.x 环境下均可运行。操作系统不限Windows、macOS、Linux 都可以。唯一需要注意的是字体文件路径写法Windows 路径通常用反斜杠macOS/Linux 用斜杠建议在代码里统一使用绝对路径或相对路径并在运行时检查字体文件是否存在。2.2 依赖安装创建一个虚拟环境然后安装依赖。推荐使用requirements.txt管理依赖Pillow9.0.0 fonttools4.30.0 numpy1.21.0其中 Pillow 用于图像绘制fonttools 用于检查字体字符集numpy 在后续做遮罩像素批量处理时可以用到。如果不做字体检查和性能优化numpy 和 fonttools 可以暂不安装。安装命令pip install -r requirements.txt2.3 字体文件准备古文字拼图的核心资源是字体文件。你至少需要准备两个字体一个用于绘制遮罩图比如普通黑体或宋体作用是生成“山”字形轮廓。一个用于拼图文字本身比如某种篆书、甲骨文风格字体作用是让拼出来的图形内部充满古文字。需要注意的是古文字字体文件不一定覆盖所有常用汉字。甲骨文、金文字体往往只包含少量已考释的字符。所以在准备文本素材前最好先确认字体能渲染哪些字符否则运行时会显示成“豆腐块”或空白。字体版权也要留意。很多古文字字体是个人或机构基于出土材料开发的使用前需要确认授权范围。如果只是本地学习演示问题不大如果要对外发布或商业使用请务必检查字体授权协议。2.4 字体字符集检查脚本用 fonttools 可以快速检查某个字体是否支持目标字符from fontTools.ttLib import TTFont font TTFont(fonts/ancient_font.ttf) cmap font.getBestCmap() chars 山水人木火土日月风雨 for ch in chars: print(ch, hex(ord(ch)), ord(ch) in cmap)如果输出中某个字符对应的结果是False说明字体不包含这个字形需要换字体或去掉该字符。这一步虽然简单但能避免后面生成一堆“空字”。3. 核心原理拆解3.1 “拼成图”的本质是什么把文字拼成图形的过程可以拆成两个核心问题哪些位置可以放文字每个位置放什么文字、多大、什么角度第一个问题由遮罩图决定。遮罩图是一张黑白图片白色区域表示“允许放文字”黑色区域表示“不允许放文字”。程序按固定步长扫描遮罩图把白色像素对应的坐标收集起来形成候选点集合。第二个问题是排列策略。最简单的方式是从候选点里随机取点依次放置文字。每次放置时随机选一个字符、随机选字体大小、随机给一个旋转角度。这样生成的图形有手工拼贴的感觉也比较自然。所以整个算法的核心就三步设计遮罩、采样候选点、循环画字。理解了这三步你就能自由扩展出更多效果。3.2 候选点采样采样可以用两层循环遍历像素完成。为了提高效率不必要对所有像素逐点扫描而是按步长step跳跃采样。step越小候选点越多拼接越紧密但生成速度越慢step越大候选点越少文字之间空隙越大图形轮廓也会更模糊。伪代码如下for y in range(0, height, step): for x in range(0, width, step): if 遮罩图在 (x, y) 位置是白色: 把 (x, y) 加入候选点列表这里的关键点是“判断像素是否在轮廓内部”。转换成灰度图后通常把像素值大于 128 视为白色小于等于 128 视为黑色。3.3 在画布上绘制文字Pillow 绘制文字最常用的是ImageDraw.text方法。方法本身不复杂但有几个参数需要理解。font通过ImageFont.truetype(path, size)加载字体size是字号。anchor文字锚点mm表示文字中心点对齐到给定坐标。这样方便把文字精准放到候选点中心。fill文字颜色。绘制旋转文字时不能直接对ImageDraw画布整体旋转而是先创建一个透明图层在图层上画好单个字符再旋转图层最后粘贴到大画布上。from PIL import Image, ImageDraw, ImageFont # 创建一个透明背景的小画布 layer Image.new(RGBA, (80, 80), (0, 0, 0, 0)) draw ImageDraw.Draw(layer) font ImageFont.truetype(fonts/ancient_font.ttf, 40) draw.text((40, 40), 山, fontfont, fill(30, 30, 30, 255), anchormm) # 旋转图层 layer layer.rotate(20, expandTrue, resampleImage.BICUBIC)旋转时expandTrue会自动调整画布尺寸避免文字被裁掉。粘贴到主画布时用layer.width // 2和layer.height // 2计算左上角坐标确保文字中心落在目标位置。3.4 遮罩图的两种来源遮罩图可以直接用图片素材也可以用代码生成。直接画一个“山”字作为遮罩比较简单适合快速验证。如果需要非常精确的品牌 Logo 或图案轮廓则用设计工具导出黑白 PNG再交给程序采样。在实际项目中遮罩图的分辨率也会影响最终效果。遮罩图越大可采样的候选点越多文字拼接越自然。但如果原图太小放大后边缘会锯齿明显。建议遮罩图宽度保持在 600 像素以上。4. 完整实战把古文字拼成“山”字下面我们实现一个完整的案例把一批古文字拼成一个“山”字的轮廓。这个案例麻雀虽小五脏俱全覆盖了遮罩生成、文本加载、坐标采样、随机画字、结果保存的完整流程。4.1 创建项目结构先创建如下目录结构ancient_text_collage/ ├── fonts/ │ ├── ancient_font.ttf │ └── simple_font.ttf ├── masks/ ├── text_source/ │ └── chars.txt ├── output/ ├── make_mask.py └── main.pyfonts存放字体文件。masks存放遮罩图。text_source存放参与拼图的文字。output存放生成结果。make_mask.py用于生成遮罩图。main.py是主程序。4.2 生成遮罩图这里我们直接用 Pillow 在代码里绘制一个“山”字作为遮罩。把“山”字画到灰度图上白色字形就是允许放文字的区域。# 文件路径make_mask.py from PIL import Image, ImageDraw, ImageFont MASK_WIDTH 900 MASK_HEIGHT 700 FONT_PATH fonts/simple_font.ttf OUTPUT_PATH masks/mountain.png mask Image.new(L, (MASK_WIDTH, MASK_HEIGHT), 0) draw ImageDraw.Draw(mask) font ImageFont.truetype(FONT_PATH, 500) draw.text((MASK_WIDTH // 2, MASK_HEIGHT // 2), 山, fontfont, fill255, anchormm) mask.save(OUTPUT_PATH) print(遮罩图已生成:, OUTPUT_PATH)这段代码里Image.new(L, ...)创建了一张灰度图背景为 0黑色。fill255表示文字为白色。anchormm让文字居中。运行后masks/mountain.png就是一张白字黑底的图片。如果你使用的 Pillow 版本较老不支持anchor参数可以先用draw.textbbox计算文字包围盒再手动居中。bbox draw.textbbox((0, 0), 山, fontfont) w bbox[2] - bbox[0] h bbox[3] - bbox[1] draw.text(((MASK_WIDTH - w) // 2, (MASK_HEIGHT - h) // 2), 山, fontfont, fill255)4.3 准备文本素材在text_source/chars.txt中放入参与拼图的字符建议与主题相关重复字符也可以。例如山水人木火土日月 风雨云川河海鸟如果你使用的古文字字体字符集有限可以先通过前面提到的 fonttools 脚本过滤一次只保留字体支持的字符。4.4 编写主程序main.py负责读取文本、读取遮罩、采样候选点、绘制文字并保存结果。代码如下# 文件路径main.py import os import random from PIL import Image, ImageDraw, ImageFont FONT_PATH fonts/ancient_font.ttf MASK_PATH masks/mountain.png TEXT_PATH text_source/chars.txt OUTPUT_PATH output/collage.png # 字体大小候选项随机选择可以让拼图更自然 FONT_SIZES [28, 32, 36] ROTATION_RANGE (-30, 30) STEP 14 def load_text(path): 读取文本文件去除空白字符返回单个字符列表。 with open(path, r, encodingutf-8) as f: content f.read() chars [ch for ch in content if ch.strip()] if not chars: raise ValueError(字符列表为空请检查文本文件内容。) return chars def load_mask_points(mask_path, step): 读取遮罩图返回白色区域的候选点坐标列表和图片尺寸。 mask Image.open(mask_path).convert(L) w, h mask.size points [] for y in range(0, h, step): for x in range(0, w, step): if mask.getpixel((x, y)) 128: points.append((x, y)) return points, (w, h) def draw_char_layer(char, font, base_size, rotation): 在透明图层上绘制一个字符可选旋转返回图层。 layer_size base_size * 2 layer Image.new(RGBA, (layer_size, layer_size), (0, 0, 0, 0)) draw ImageDraw.Draw(layer) draw.text((layer_size // 2, layer_size // 2), char, fontfont, fill(30, 30, 30, 255), anchormm) if rotation: layer layer.rotate(rotation, expandTrue, resampleImage.BICUBIC) return layer def main(): if not os.path.exists(FONT_PATH): raise FileNotFoundError(f字体文件不存在: {FONT_PATH}) if not os.path.exists(MASK_PATH): raise FileNotFoundError(f遮罩文件不存在: {MASK_PATH}) # 固定随机种子方便复现结果 random.seed(42) chars load_text(TEXT_PATH) points, (w, h) load_mask_points(MASK_PATH, STEP) if not points: raise ValueError(遮罩图中没有白色区域请检查遮罩图。) print(候选点数:, len(points)) canvas Image.new(RGBA, (w, h), (255, 255, 255, 255)) fonts [ImageFont.truetype(FONT_PATH, size) for size in FONT_SIZES] idx 0 for x, y in points: char chars[idx % len(chars)] idx 1 font random.choice(fonts) rotation random.randint(ROTATION_RANGE[0], ROTATION_RANGE[1]) layer draw_char_layer(char, font, base_sizef.font.size, rotationrotation) paste_x x - layer.width // 2 paste_y y - layer.height // 2 canvas.paste(layer, (paste_x, paste_y), layer) os.makedirs(output, exist_okTrue) canvas.save(OUTPUT_PATH) print(拼图已生成:, OUTPUT_PATH) if __name__ __main__: main()代码里有几个细节值得说明。draw_char_layer中layer_size base_size * 2是为了给旋转留出空间避免文字旋转后被画布边缘裁切。实际字号由font决定base_size可以理解为文字图层的基础尺寸二者并不完全相同。在这里我直接使用font.size作为base_size保证图层足够大。canvas.paste(layer, (paste_x, paste_y), layer)的第三个参数layer是透明度掩码表示只有不透明的像素才会被粘贴。这一步很重要否则图层透明部分会覆盖掉底下已经画好的文字。random.seed(42)不是必须的但它能保证每次运行结果一致方便调试和复现。如果你希望每次生成的拼图都不一样注释掉这一行即可。4.5 运行与验证在项目根目录依次执行python make_mask.py python main.py正常情况下make_mask.py会输出遮罩图已生成: masks/mountain.pngmain.py会输出类似候选点数: 1562 拼图已生成: output/collage.png打开output/collage.png你会看到一张白底深灰色文字的图片文字密密麻麻地排列成“山”字轮廓。由于字号和旋转角度是随机的整体效果会有手工拼贴的错落感。如果发现轮廓不明显可以缩小STEP比如从 14 改成 8如果发现文字之间重叠太多可以调大STEP或减小字号。5. 进阶让拼图更精致5.1 按权重控制字号基础版本里所有字符字号随机这不适合表达“重点内容”。实际项目中你可能希望某些核心文字更大比如“山”字本身就比其他自然元素更突出。可以提前准备一个权重字典weight_map { 山: 48, 水: 40, 人: 36, 木: 32, }在选择字号时优先查表查不到时再用默认字号列表。这样生成出来的拼图不仅轮廓清晰还带有语义上的层次感。5.2 按语义分配颜色如果拼图用于展览或海报颜色可以按字符类别区分。比如自然类用绿色系人文类用棕色系方位类用灰色系。实现方式是在绘制文字时根据字符类别生成不同的fill颜色。颜色选择建议控制在低饱和度范围避免整体画面过于花哨。古文字本身偏古朴颜色太重会破坏氛围。5.3 支持多语言古文字如果你想把藏文、西夏文、契丹文等文字也做成拼图思路完全一样只需要替换字体和文本数据。主要注意两点字体必须覆盖对应语言的码位范围。文本读取必须使用encodingutf-8避免编码问题。有些古文字字体不是标准的 TTF/OTF 格式可能需要先转换格式或做子集化处理才能被 Pillow 正常加载。这个环节建议先做一个小样本测试不要直接在完整数据集上跑。5.4 结合 OCR 提取文字如果手里只有扫描版的古籍图片无法直接拿到电子文本可以先做 OCR 识别再进入拼图流程。Tesseract 对现代中文的识别效果已经不错但甲骨文、金文等字形和现代汉字差异较大通用 OCR 模型效果有限。如果对准确率要求高需要基于自己的标注数据训练模型。这一块属于独立的方向本文不展开。你可以把“古文字拼图”看作下游呈现环节先解决文字数据问题再考虑如何排列。6. 常见问题与排查思路6.1 问题汇总表问题现象常见原因解决思路文字显示成方块/豆腐块字体不包含目标字符用 fonttools 检查字符集换字体或去掉该字符程序报OSError: cannot open resource字体路径错误或文件不存在检查FONT_PATH改为绝对路径生成图片一片空白遮罩图没有白色区域或文本为空打印候选点数和字符数量检查遮罩图和chars.txt轮廓不明显STEP太大或字号太小调小STEP增大字号文字重叠严重采样点太密、字号太大调大STEP减小字号或实现碰撞检测旋转后文字被裁剪图层尺寸不够增大layer_size确保旋转后留白足够中文字体看起来像宋体字体没有真正加载成功确认使用的是支持古文字的字体文件而不是系统默认字体6.2 字形缺失的进阶排查如果某个字符在画布上显示成方框但开发环境里看起来又是正常的大概率不是字体文件的问题而是该字符超出了当前字体的cmap覆盖范围。不要盲目用“加粗、换颜色”的方式掩盖正确做法是先用 fonttools 脚本逐个检查。检查时要注意getBestCmap()返回的键是 Unicode 码点也就是十进制的整数。直接拿字符的ord(ch)去判断即可。判断结果可能出现“字体能显示但形状和预想不一样”的情况这通常是字体选错了字型风格需要换字体。6.3 性能问题当遮罩图很大、STEP很小时候选点可能达到上万甚至几十万循环画字会变慢。优先策略是降低STEP或缩小遮罩图而不是升级硬件或引入多线程。如果确实需要高性能可以把像素判断部分用 numpy 向量化再从中随机采样。示例思路import numpy as np from PIL import Image mask np.array(Image.open(MASK_PATH).convert(L)) ys, xs np.where(mask[::STEP, ::STEP] 128) points list(zip(xs * STEP, ys * STEP))这种方式比双层for循环快很多。注意xs和ys的顺序避免坐标颠倒。7. 最佳实践与工程建议7.1 字符清洗与编码规范文本数据必须统一使用 UTF-8 编码。加载后要过滤空白字符、标点和控制字符只保留需要参与拼图的字符。如果文本来源复杂建议先做一次清洗避免把换行符、空格当成文字画到图上。清洗逻辑可以放在独立函数里方便复用def clean_chars(text): return [ch for ch in text if ch.strip() and not ch.isspace()]ch.isspace()能过滤空格、换行、制表符等比单纯判断ch ! 更可靠。7.2 参数配置与管理不要把字体路径、字号、旋转范围、采样步长写死在业务代码深处。建议统一放在配置区或配置文件里。最简单的方式是在main.py顶部集中定义常量。如果项目规模扩大可以改用config.py或 YAML 配置。参数集中后调参与代码逻辑分离任何协作同事都能快速修改输出效果。7.3 输出结果可复现生成型项目最怕“这次跑出 A 结果下次跑出 B 结果”。固定随机种子是一种有效手段。random.seed(42)只能控制 Pythonrandom模块。如果你后续引入 numpy 的随机采样还需要单独设置np.random.seed(42)。建议在代码开头统一设置import random import numpy as np random.seed(42) np.random.seed(42)同时可以把参与拼图的字符列表、参数配置输出为日志文件方便后续追溯任何一张图的生成来源。7.4 字体版权与内容安全使用古文字字体前务必确认授权范围。很多数字化字形来自文物保护机构或字体厂商可能只允许学术研究使用。如果要商用请先获得授权。另外不要把古文字拼图用于虚构文化含义、拼接敏感内容。项目本身是技术演示和艺术表达工具输出内容的使用场景需要自己把握边界。7.5 从脚本到服务的演进如果这个拼图能力要提供给内部同学使用可以封装成函数def generate_collage(chars, mask_path, output_path, **kwargs): ...对外只暴露必要参数内部处理点采样、字体加载、绘制保存等细节。这样方便接入命令行工具、Web 接口或批量生成流程。8. 总结与学习路线本文围绕“古老文字拼成图”完成了从思路到实现的完整闭环。核心收获有三点一是理解了遮罩图、候选点采样、文字图层旋转粘贴这套通用流程二是拿到了一套可运行的 Python 代码可以改成任意轮廓、任意文字素材三是掌握了字体字符集检查、随机种子固定、性能优化等工程经验。下一步可以继续深入研究的方向包括wordcloud 库的源码实现、基于碰撞检测的排版算法、文字密度的方向场控制、不同古文字字体的字形对比分析。建议你动手做一个自己的版本准备一段《诗经》或《千字文》文本选一种古文字字体把“风”“雅”“颂”等字作为遮罩生成一张属于你自己的古文字拼图。过程中如果遇到字形缺失、轮廓模糊、布局不均再回到本文对应章节排查会比只看不练理解得更深。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻