artcraft 实战:从随机种子到可复现的生成艺术
1. 从“artcraft”这个名字说起它到底想解决什么问题第一次看到“artcraft”这个标题我脑子里蹦出来的第一个念头是这大概率不是一个单纯的绘画工具也不是一个纯粹的手工教程合集。它更像是一个把“艺术art”和“手艺craft”捏在一起的复合概念——用代码或数字工具去做那些原本需要双手完成的事或者反过来用传统手艺的思路去理解数字创作。我之所以这么判断是因为在实际项目里凡是名字里同时出现“art”和“craft”的通常都指向一个共同痛点创意和落地之间的断层。做设计的人有想法但不会写代码写代码的人有技术但审美和手工感跟不上。artcraft 这个标题本质上是在试图弥合这条裂缝。它适合谁来参考三类人最值得往下看一是有点编程基础、想用代码生成视觉作品的前端或脚本玩家二是做手工、做实体创作、想把流程数字化记录和复用的人三是纯粹对“创意编程”好奇、想找一个能上手的切入点的爱好者。不管你是哪一类核心逻辑都是一样的——把重复的、机械的部分交给工具把判断和审美留给自己。我接下来要拆的不是某个官方文档的复述而是基于这个标题和常见实践把 artcraft 这类项目从零到跑通、从跑通到用得顺手、从用得顺手到能分享出去的完整链路讲清楚。中间会穿插我自己踩过的坑、参数怎么定、工具怎么选以及那些文档里不会写的经验。2. 动手之前先想清楚artcraft 类项目的技术选型逻辑2.1 为什么“生成式”和“手工式”两条路要分开走artcraft 这类项目最容易犯的一个错误就是一上来就想做一个“全能工具”——既能自动生成图案又能手动微调还能导出各种格式。我试过结果就是每个功能都半吊子。后来我总结出一个原则生成和手工必须分阶段不能混在一个交互循环里。生成阶段的核心是“快”和“多”。你需要用代码批量产出候选方案比如用随机种子生成 50 个配色组合、20 种线条走向。这个阶段用 Python 的random或numpy就够了不需要任何图形界面。手工阶段的核心是“慢”和“准”。你要从候选里挑出有潜力的然后手动调参数、改细节。这个阶段才需要交互界面比如用 HTML Canvas 或者 Processing 的 sketch。为什么这么分因为人的注意力是有限的。如果你一边生成一边手动改很容易陷入“改了一个参数、生成结果全变、又得重新找感觉”的死循环。分开之后生成阶段可以完全自动化手工阶段可以完全专注。2.2 工具选型Processing、p5.js 还是纯 Python这是被问得最多的问题。我直接给结论如果你要做的是静态图像或简单动画p5.js 是首选如果你要做的是数据驱动的复杂图形Processing 的 Java 模式更稳如果你只是想在命令行里快速验证想法纯 Python Pillow 最轻。p5.js 的优势在于它跑在浏览器里分享极其方便——你写一个 sketch发个链接别人就能看。而且它的 API 设计非常“手工友好”beginShape()、vertex()、bezierVertex()这些函数用起来就像在纸上画线。Processing 的优势在于性能尤其是涉及大量像素操作或 3D 渲染时Java 的底层比 JavaScript 稳得多。纯 Python 的优势在于生态你可以直接调用numpy做矩阵运算、用scipy做插值适合做那些“数学味”很重的生成艺术。我个人的习惯是原型用 p5.js因为改一行代码就能看到效果最终输出用 Processing 或 Python因为可以控制分辨率和文件格式。这个组合我用了两年多基本没遇到过卡死的情况。2.3 一个容易被忽略的细节颜色空间的选择绝大多数人做 artcraft 项目时默认用 RGB 或 HSL 来定义颜色。但如果你做的是实体输出比如打印、丝网印刷、激光雕刻RGB 会骗你。屏幕上的亮蓝色印出来可能是灰紫色屏幕上的深红色印出来可能发黑。我的经验是只要涉及实体输出一律用 CMYK 或 Lab 颜色空间来定义最终颜色RGB 只用于屏幕预览。具体操作上在 p5.js 里可以用colorMode()切换到 HSB 做交互但导出时用saveCanvas()之前先把颜色转成 CMYK 值记录在元数据里。Python 这边更直接Pillow支持CMYK模式colorsys库可以做 RGB 到 CMYK 的转换。这个细节看起来小但实际项目中颜色偏差是返工率最高的原因之一。我见过一个做手工书封面的项目屏幕上调好的墨绿色印出来变成了军绿色整批材料报废。后来他们改成 Lab 空间定义颜色问题就解决了。3. 从零跑通第一个 artcraft 原型完整步骤与参数解释3.1 环境准备别急着装一堆库很多人一上来就pip install一堆东西结果版本冲突、依赖打架。我的做法是先建一个干净的虚拟环境只装最核心的两三个库跑通最小闭环之后再按需添加。以 Python 为例最小闭环只需要python -m venv artcraft_env source artcraft_env/bin/activate # Windows 用 artcraft_env\Scripts\activate pip install pillow numpy就这两个。Pillow负责图像读写和基本绘制numpy负责数值计算。不需要matplotlib不需要opencv那些是后面做特定效果时才用的。如果你走 p5.js 路线更简单建一个index.html引入 p5.js 的 CDN 链接再建一个sketch.js就完事了。不需要npm不需要构建工具。我试过用npm管理 p5.js 项目结果光是配置webpack就花了一下午最后发现直接用 CDN 最省事。3.2 第一个生成脚本用随机种子控制“可控的意外”artcraft 的核心魅力在于“可控的意外”——你设定规则但结果不完全可预测。实现这个的关键是随机种子random seed。import numpy as np from PIL import Image, ImageDraw np.random.seed(42) # 固定种子保证每次运行结果一致 width, height 800, 800 img Image.new(RGB, (width, height), white) draw ImageDraw.Draw(img) for i in range(200): x1 np.random.randint(0, width) y1 np.random.randint(0, height) x2 np.random.randint(0, width) y2 np.random.randint(0, height) color tuple(np.random.randint(0, 255, 3)) draw.line([(x1, y1), (x2, y2)], fillcolor, width2) img.save(artcraft_output.png)这段代码跑出来是一堆随机线条。看起来简单但有几个关键点np.random.seed(42)里的42可以是任意整数。换一个数字整个画面就完全不同。这就是“可控的意外”——你控制种子种子控制结果。range(200)里的200是线条数量。我试过 50 到 5000发现 200 到 500 之间最有“手工感”太少显得空太多显得乱。width2是线宽。如果你想要更细腻的效果改成 1想要更粗犷改成 4 或 5。提示固定种子之后如果你改了代码里的任何参数比如线条数量结果会变但仍然是可复现的。所以建议把种子值和所有参数一起记录在一个配置文件里方便回溯。3.3 从随机到“有意图”引入概率分布纯随机线条看久了会腻因为它没有“节奏”。要让画面有节奏就得用非均匀的概率分布。比如让线条的角度更倾向于某个方向angles np.random.normal(locnp.pi/4, scale0.3, size200) lengths np.random.exponential(scale100, size200) for angle, length in zip(angles, lengths): x1 np.random.randint(0, width) y1 np.random.randint(0, height) x2 x1 length * np.cos(angle) y2 y1 length * np.sin(angle) draw.line([(x1, y1), (x2, y2)], fillblack, width1)这里用了两个分布normal让角度集中在 45 度附近exponential让长度集中在短线段、偶尔出现长线段。跑出来的效果会比纯随机“顺眼”很多因为它模拟了手工绘画中“手腕自然摆动”的倾向。参数怎么定loc是中心角度scale是分散程度。scale越小线条越整齐越大越杂乱。我一般从 0.3 开始试觉得太整齐就加到 0.5觉得太乱就降到 0.2。exponential的scale控制平均长度100 像素在 800x800 的画布上大概是八分之一宽度比较适中。3.4 输出格式与分辨率别等到最后才想这件事很多人跑完脚本直接save成 PNG结果要打印时发现分辨率不够。我的建议是从一开始就用 300 DPI 的标准来设定画布尺寸。如果你要打印 A4 大小210mm x 297mm300 DPI 对应的像素是 2480 x 3508。这个尺寸在屏幕上看起来很大但打印出来刚好。如果你只是做屏幕展示72 DPI 就够了A4 对应 595 x 842。在 Pillow 里你可以这样设置dpi 300 width_mm, height_mm 210, 297 width int(width_mm / 25.4 * dpi) height int(height_mm / 25.4 * dpi) img Image.new(RGB, (width, height), white)这样生成的图像保存时带上 DPI 信息img.save(artcraft_print.png, dpi(dpi, dpi))注意PNG 格式对 DPI 的支持不如 TIFF 和 PDF 好。如果你要交给印刷厂建议导出 TIFF 或 PDF。Pillow 支持img.save(output.tiff, dpi(300, 300))但 PDF 需要额外用reportlab或img2pdf库。4. 让作品“活”起来交互与动态化的几个关键决策4.1 什么时候该加交互什么时候不该加不是所有 artcraft 项目都需要交互。我见过太多项目本来静态图像已经很好看了非要加个鼠标跟随结果变得很廉价。判断标准很简单如果交互能让观众更理解你的生成规则就加如果只是“看起来酷”就不加。举个例子如果你做的是一个基于噪声函数的纹理生成器加一个滑块让观众调整噪声频率这是有意义的因为观众能直观感受到频率对纹理的影响。但如果你做的是一个基于固定算法的几何图案加一个鼠标拖拽旋转那就纯粹是装饰不如不加。4.2 p5.js 里的交互实现从mouseX到自定义参数p5.js 的交互非常直接。最基本的function setup() { createCanvas(800, 800); } function draw() { background(255); let freq map(mouseX, 0, width, 0.01, 0.1); let amp map(mouseY, 0, height, 10, 100); for (let i 0; i 200; i) { let x noise(i * freq) * width; let y noise(i * freq 100) * height; let size noise(i * freq 200) * amp; ellipse(x, y, size, size); } }这里mouseX控制噪声频率mouseY控制振幅。观众移动鼠标时能立刻看到纹理从细密变粗犷、从平坦变起伏。这种交互是有信息量的。但要注意noise()函数在 p5.js 里是 Perlin 噪声它的输出范围是 0 到 1。如果你直接用它乘width结果会集中在中间区域。更好的做法是map(noise(...), 0, 1, -0.5, 0.5) * width width/2这样分布更均匀。4.3 动态化的性能陷阱别让帧率掉到 30 以下动态 artcraft 项目最容易踩的坑是性能。我试过一个项目每帧要画 5000 个粒子结果在笔记本上只有 15 帧。后来发现瓶颈不在绘制而在每帧重新计算所有粒子的位置。解决方案是把计算和绘制分开。粒子的位置更新可以用numpy或typed array批量算算完再一次性传给绘制函数。在 p5.js 里可以用p5.Vector数组但更高效的是直接用Float32Array存坐标然后用beginShape(POINTS)批量绘制。另一个技巧是限制重绘区域。如果只有局部在变化不要每帧background(255)全清。可以用一个半透明的矩形覆盖制造拖尾效果同时减少全屏重绘的开销。5. 从作品到产品导出、分享与版本管理的实操经验5.1 导出高清图的三个参数尺寸、格式、压缩导出是 artcraft 项目从“玩具”变成“作品”的关键一步。我总结了一个三参数清单参数屏幕展示打印输出激光雕刻分辨率72-150 DPI300 DPI500-1000 DPI格式PNGTIFF/PDFSVG/DXF压缩无损无损矢量无压缩屏幕展示用 PNG 就够了无损压缩文件大小可控。打印输出必须 TIFF 或 PDF因为印刷设备对 PNG 的 DPI 元数据支持不稳定。激光雕刻需要矢量格式SVG 或 DXF因为雕刻机是按路径走的不是按像素。我踩过的一个坑用 PNG 导出 300 DPI 的图结果印刷厂说“文件里没有 DPI 信息”。后来才知道Pillow 保存 PNG 时虽然可以传dpi参数但很多印刷厂的软件不读这个字段。改成 TIFF 之后就没问题了。5.2 分享时的“可复现性”把种子和参数一起发出去artcraft 作品分享出去之后最常被问的问题是“这个效果怎么做的”如果你只发一张图别人只能猜。但如果你把种子值、参数配置、甚至代码片段一起发出去别人就能复现甚至在此基础上改出新的东西。我的做法是每张作品旁边附一个“配方”格式如下种子: 42 线条数量: 200 角度分布: normal(loc0.785, scale0.3) 长度分布: exponential(scale100) 颜色空间: RGB 画布尺寸: 800x800这个配方可以直接贴在任何地方别人复制粘贴就能跑出一样的图。如果他想改改一个参数就行。这种“可复现的分享”比单纯发图有价值得多。5.3 版本管理别用final_v2_真的最终版.pngartcraft 项目迭代很快今天调一个参数明天换一个分布很容易产生几十个版本。我见过有人用final.png、final_v2.png、final_v2_真的最终版.png来命名结果自己都分不清哪个是哪个。我的做法是用种子值 参数哈希作为文件名。比如artcraft_42_n200_a0.3_l100.png。这样一看文件名就知道用了什么参数。如果参数太多就只保留关键几个其他的记在配方的文本文件里。更进一步可以用 Git 管理代码和配方。每次生成新版本提交一次commit message 写清楚改了什么参数、效果有什么变化。这样回溯起来非常方便。6. 那些文档不会写的坑我在 artcraft 项目里踩过的真实教训6.1 浮点数精度导致的“几乎一样但不一样”有一次我做了一个基于正弦波的图案生成器参数是频率f。我设f 0.1和f 0.1000001理论上应该几乎一样但生成出来的图案在局部有肉眼可见的差异。原因是浮点数在累加过程中误差被放大了。解决方案对关键参数做量化。比如频率只保留两位小数round(f, 2)。这样既避免了精度问题也让参数更容易记录和复现。另一个做法是用整数代替浮点数比如用f_int 10表示f 0.1在计算时再除以 100。6.2 颜色叠加时的“脏色”问题在 RGB 空间里两种颜色叠加比如用blend模式很容易产生脏色。比如红色和绿色叠加理论上应该是黄色但实际出来是灰褐色。这是因为 RGB 是加色模式叠加逻辑和颜料混合不一样。如果你想要颜料混合的效果用multiply模式如果你想要光混合的效果用screen模式。在 Pillow 里可以用Image.blend()或ImageChops.multiply()。在 p5.js 里用blendMode(MULTIPLY)或blendMode(SCREEN)。我个人的经验是做实体输出时用multiply做屏幕展示时用screen。因为实体颜料是减色混合屏幕是加色混合。6.3 文件读写时的编码陷阱这个坑很隐蔽。如果你在配方文件里写了中文注释然后用open()读取可能会遇到UnicodeDecodeError。原因是 Python 在 Windows 上默认用gbk编码而文件是utf-8。解决方案永远显式指定编码。with open(recipe.txt, r, encodingutf-8) as f: content f.read()写文件也一样with open(recipe.txt, w, encodingutf-8) as f: f.write(content)这个习惯我养成了之后再也没遇到过编码问题。虽然多打几个字但省下的调试时间远不止这几个字。6.4 内存泄漏循环里创建图像对象如果你在循环里反复Image.new()和ImageDraw.Draw()内存会涨得很快。我试过一个项目循环 1000 次创建小图结果内存从 50MB 涨到 2GB。解决方案复用图像对象。如果每次只是清空重画用draw.rectangle([0, 0, width, height], fillwhite)覆盖而不是新建。如果必须新建确保旧的被del掉或者用with语句管理上下文。在 p5.js 里createGraphics()创建的离屏画布也要注意。如果每帧都创建浏览器会卡死。正确的做法是在setup()里创建一次在draw()里复用。7. 把 artcraft 变成习惯日常练习与灵感来源7.1 每天一个“种子实验”我给自己定了一个规矩每天用不同的种子跑一次同一个脚本把结果存下来。一个月之后我有了 30 张图然后从中挑出 3 张最喜欢的分析它们的种子和参数有什么共同点。这个练习帮我快速建立了“什么参数区间容易出好效果”的直觉。具体操作建一个daily文件夹每天用日期作为种子比如20250101。跑完脚本把图和配方一起存进去。周末的时候翻一遍用红笔圈出喜欢的用蓝笔圈出不喜欢的。一个月后统计一下红色圈里的种子有什么规律。7.2 从其他领域“偷”参数artcraft 的参数不一定要从 artcraft 里找。我试过从音乐里偷参数把一段旋律的音高映射成线条长度把节奏映射成线条密度。也试过从天气数据里偷把温度映射成颜色把风速映射成线条角度。这些跨领域的参数往往能带来意想不到的效果。因为你的直觉不熟悉这些数据的分布所以生成出来的东西会“跳出”你平时的审美习惯。我有一张最喜欢的作品就是用某地一周的天气数据生成的线条的疏密变化完全不像我平时会画的东西。7.3 建立自己的“参数库”每次发现一个好效果就把参数记下来。不用很复杂一个文本文件就行# 参数库 # 格式: 名称 | 种子 | 关键参数 | 效果描述 柔和线条 | 42 | normal(0.785, 0.3), exp(100) | 像风吹过的草 密集网格 | 108 | uniform(0, 6.28), uniform(50, 200) | 像编织物 稀疏点阵 | 256 | poisson(5), radius10 | 像星空这个库积累到几十条之后你就有了一个“灵感菜单”。下次做新项目时不用从零开始想参数直接从库里挑一个改改就行。8. 关于 artcraft 的常见疑问与我的实际回答8.1 “我不会画画能做 artcraft 吗”能。artcraft 的核心不是绘画技巧而是规则设计。你不需要会画一条直线你只需要会定义“什么条件下画一条直线”。这更像是在写菜谱而不是在炒菜。我见过很多完全不会画画的人做出了非常好看的生成作品因为他们对“规则”的直觉很好。8.2 “学 artcraft 需要多少数学”基础部分只需要初中数学坐标、角度、正弦余弦。进阶部分会用到概率分布和噪声函数但这些都有现成的库你只需要知道“正态分布让结果集中指数分布让结果分散”就够了。真正需要深入数学的地方比如分形维数、傅里叶变换那是另一个层次可以等你有兴趣了再学。8.3 “为什么我的作品看起来像随机噪声”因为你的参数太“均匀”了。纯随机均匀分布看起来就是噪声。要让作品有结构必须引入非均匀性。比如让线条角度集中在某个范围让颜色偏向某个色相让大小遵循幂律分布。非均匀性就是“风格”的来源。8.4 “应该学 Processing 还是 p5.js”如果你想要快速看到结果、方便分享学 p5.js。如果你想要更好的性能、更稳定的输出学 Processing。如果你已经会 Python想用 Python 做那就用 Python Pillow。三者不冲突核心概念是通的。我个人的路径是 p5.js 入门Processing 进阶Python 做批量处理。8.5 “怎么判断一个 artcraft 作品好不好”我的标准是它是否让你想再看一眼。如果一张图你看了一秒就划走了那它可能只是“技术演示”。如果一张图让你停下来想看看细节想猜猜怎么生成的那它就是“作品”。技术可以慢慢练但“想再看一眼”的感觉是你做 artcraft 的最终目标。9. 最后分享几个我常用的“作弊”技巧第一个技巧用别人的作品当种子。找一张你喜欢的图把它转成灰度然后取像素值当随机种子。这样生成的图案会和原图有某种“气质上的相似”但又完全不同。我试过用一张老照片当种子生成出来的线条走向和照片的明暗分布有微妙的呼应。第二个技巧限制颜色数量。新手最容易犯的错是用太多颜色。我建议从 2 到 3 种颜色开始最多不超过 5 种。颜色越少结构越清晰。如果你觉得单调就调整颜色的明度和饱和度而不是增加色相。第三个技巧把画布缩小看。在 800x800 的画布上画完之后缩到 200x200 再看一眼。如果缩小之后还能看出结构说明你的构图是稳的。如果缩小之后变成一团糊说明细节太多、结构太弱。这个技巧帮我省了很多次返工。第四个技巧用时间当参数。如果你不知道用什么当种子就用当前时间戳。int(time.time())每次都不一样保证每次运行都是新的。但记得把时间戳记下来否则就复现不了了。第五个技巧别怕删代码。artcraft 项目很容易越写越长最后变成一团乱麻。我每隔一段时间就会把代码删到只剩核心逻辑然后重新加功能。删掉的东西如果真的好你会再写回来的如果没写回来说明它本来就不重要。