做生成艺术这几年,我一直留着一个小项目叫artcraft——名字没什么高深含义,就是 art 和 craft 拼在一起:既要“艺术”的审美,也得有“手工活”的耐心。用代码画画这事,听上去很酷,但真要做出能挂上墙、能打印输出、经得起放大细看的作品,远不是跑通一段教程、堆几个随机数那么简单。这个项目里我踩过的坑、总结出来的参数思路,还有整套从零到成品的流程,今天就完整拆给大家。
如果你是设计师想拓展创作媒介,或者程序员想从“逻辑世界”跨界到“视觉表达”,又或者只是好奇生成艺术怎么入门,这篇文章都能给你一套可以直接落地的方案。没有高深的数学推导,全是实际跑过的代码、可复现的参数和真金白银换来的教训。
1. 先想清楚:artcraft 要解决什么问题
1.1 生成艺术到底在生成什么
很多人第一次接触生成艺术,眼睛会被那种“复杂到不像人手工能做出来”的图形震住——几千条流动的线条、几万只随机排布的粒子、无穷变化的几何嵌套。但真正动手之后很快就会意识到:代码能生成的不是“艺术品”,而是“可能性”。同样的随机逻辑,可能跑出 100 张图,其中 99 张是垃圾,只有 1 张让你觉得“有点东西”。
artcraft 这个名字想强调的,恰恰是把那些散落的“可能性”打磨成“确定的作品”的过程。我理解的生成艺术不是把画笔交给随机数,而是在随机和秩序之间找一个平衡点:规则是艺术家定的,意外是算法给的。两者缺一不可。只有规则没有意外,作品是死板的几何图;只有意外没有规则,作品就是噪点图,谈不上审美。
1.2 为什么 90% 的生成艺术一眼就是“AI 味”
我去过几个数字艺术相关的交流活动,也在网上看过大量社区投稿,发现一个问题特别普遍:很多作品技术上很复杂,但整体看过去就是一个词——“脏”。颜色脏、构图脏、元素堆砌没有主次。原因很简单:作者把“随机”用成了“失控”。
举个例子,很多新手写生成线条,逻辑是“每次角度随便转”,于是线条变成一团乱麻。而真正成熟的生成作品,随机都发生在有约束的框架里:步长限制在一个区间、转弯概率遵循某种偏好、颜色从固定的调色板里抽取、元素密度控制在一个视觉舒适的范围。这几层约束就是“craft”的体现。artcraft 这个项目从第一天起就定下一个原则:一切随机必须有边界。边界才是风格,随机只是风格里的呼吸感。
1.3 这个项目适合谁
如果你问我自己在那期间最大的收获,我会说不是学会了某某算法,而是建立起一套“参数思维”——把审美里面说不清道不明的部分,翻译成代码里可调整的变量。
这套思维对三类人非常有用。第一类是设计师:你不需要成为编程高手,只需要理解几个基础变量就能扩展自己的创作工具链;第二类是前端或全栈工程师:你们写惯了业务逻辑,恰好可以用生成艺术重新找回对代码的掌控感和趣味性;第三类是纯艺术爱好者:哪怕一行代码都没写过,跟着流程走一遍,也能理解那些数字艺术作品背后到底发生了什么。
2. 工具链选型与底层原理
2.1 为什么我最终选了 Python + 图像库
生成艺术可以选择的工具其实不少:Processing 是老牌创作环境,语法亲切、上手快;p5.js 适合做交互网页,打开浏览器就能跑;TouchDesigner 偏实时视觉和舞台。这几个我都试过,但 artcraft 项目里最后的主力是 Python。
原因有三点。一是生态成熟,图像处理相关的库非常齐全,生成的结果可以精准导出为 PNG、SVG、PDF,方便后续排版、打印、印刷。二是批处理能力强,一次生成几百张候选图再慢慢挑,脚本处理比在 GUI 软件里手动操作效率高得多。三是参数沉淀方便,所有变量都可以写进配置,一次实验的参数组合可以被完整记录下来,做版本管理非常顺手。
在具体工具上,我用的组合是 Pillow + NumPy,偶尔用 Matplotlib 的矢量导出功能。Pillow 负责像素级操作,NumPy 负责批量计算。如果你只想做纯几何构图,Pillow 一个库就够;如果涉及大量粒子运算或噪声场,NumPy 能省掉一层循环的麻烦。
2.2 三个核心算法:随机、噪声与迭代
理解了工具,再来聊原理。artcraft 里最常用的算法有三个,几乎能覆盖绝大多数生成需求。
第一个是受约束的随机。这是所有生成艺术的地基。代码里常用的random.uniform(a, b)就是纯粹的随机,但你真正写作品时,很少直接把它的输出贴在屏幕上。更常见的做法是给随机值加一个“锚点”:比如以画布中心为锚,位置按半径随机分布,越靠近中心密度越大,这样构图天然聚焦。锚点加随机,出来的东西既有变化也有秩序。
第二个是Perlin 噪声。这个名词听起来吓人,其实就是“平滑的随机”。普通随机数每个值和上一个没有任何关系,画出来是毛刺;Perlin 噪声保证相邻的值连续变化,画出来就是山脉、云朵、水波那种柔和起伏。生成艺术里做流动线条、地形轮廓、渐变纹理,Perlin 噪声是绕不开的基础工具。好消息是 Python 里没有内置函数,但用几行代码实现一个简化版并不复杂,后面我会给出可以直接用的方案。
第三个是迭代扩张。简单理解就是“让图形自己长出来”。从一个基本形状开始,每一步基于当前状态生成下一步的状态。典型的例子是随机游走线条:线条每一帧的位置由上一帧加上一个偏移量决定。偏移量如果完全随机,线条就变成了布朗运动;偏移量如果受噪声控制,线条就会呈现流动感。同样是一条线,两种算法气质天差地别。
2.3 一张图看懂坐标系与画布尺寸的影响
初学生成艺术的人容易忽略一个问题:画布的坐标体系和分辨率设置。Pillow 的图像坐标原点在左上角,x 轴向右,y 轴向下——这和数学课上熟悉的直角坐标系是反的。很多新手在做对称、旋转操作时经常产生镜像错乱,排查半天发现是坐标轴的坑。
分辨率也不只是“清晰度”问题,它直接影响图形的气质。1200×1200 的画布比例和 2400×2400 的容量完全不同:小画布适合做预览草稿,运算速度快,方便调参数;大画布输出细节更丰富,但对线条宽度、间距的数值需要同步放大。我的习惯是先用 800×800 的尺寸调参数,确定效果后再用 4000×4000 的尺寸生成成品,这样既节省调试时间,又不牺牲输出质量。
3. 实操:完成第一幅 artcraft 作品
3.1 环境搭建与最小生成脚本
这一节直接从零开始,带着大家跑出第一张可以看的生成图。不需要复杂的依赖,装好 Python 之后,用 pip 安装两个包就行:
pip install pillow numpy然后新建一个 Python 文件,先把最基础的结构搭出来。我用一个简单的“随机圆点”案例来演示流程——它虽然简单,但足够讲解坐标、颜色、随机约束这几个关键概念。
from PIL import Image, ImageDraw import random # 画布尺寸 W, H = 1200, 1200 # 背景色(深色底容易衬出亮色图形) image = Image.new("RGB", (W, H), "#0f0f1a") draw = ImageDraw.Draw(image) # 随机圆点:画 200 个圆 for i in range(200): # 圆点位置:以画布中心为锚,半径跟随随机分布 angle = random.uniform(0, 3.14159 * 2) radius = random.uniform(0, 450) ** 1.2 # 用幂次让内侧更密集 x = W / 2 + radius * math.cos(angle) y = H / 2 + radius * math.sin(angle) # 圆点直径:小圆居多,偶有稍大圆 d = random.choice([4, 4, 4, 8, 12]) # 颜色:不同透明度叠加(用带 alpha 的 RGB 叠加两层) color = (random.randint(40, 200), random.randint(80, 220), random.randint(160, 255)) draw.ellipse([x - d/2, y - d/2, x + d/2, y + d/2], fill=color) image.save("output_minimal.png")跑完这段代码,你会得到一张蓝紫色系、中心密集边缘疏散的圆点图。它的视觉效果可能很普通,但这个脚本已经包含了 artcraft 的三大核心要素:锚点约束(坐标围绕中心)、参数化随机(位置、大小、颜色的随机范围被仔细限制)、迭代生成(for 循环批量产生元素)。
3.2 让线条“活”起来:从布朗运动到噪声流动
圆点只是前菜,真正让生成艺术有生命感的是线条。很多人的第一版线条程序是这样的:每次取一个随机角度,往前走固定步长,再取下一个随机角度。跑出来的线条是一团锯齿状的乱麻,也就是“布朗运动”。
而让线条变成流动的曲线,关键在于用平滑噪声替代纯随机。这里给一个精简版 Perlin 噪声实现,不需要装第三方库也能跑出效果:
import math, random class SimpleNoise: def __init__(self): self.perm = list(range(256)) random.shuffle(self.perm) self.perm += self.perm def _fade(self, t): return t * t * t * (t * (t * 6 - 15) + 10) def _grad(self, h, x, y): grad_vals = [[1,1],[-1,1],[1,-1],[-1,-1], [1,0],[-1,0],[0,1],[0,-1]] g = grad_vals[h % 8] return g[0] * x + g[1] * y def noise2d(self, x, y): X = int(x) & 255 Y = int(y) & 255 x -= int(x) y -= int(y) u = self._fade(x) v = self._fade(y) a = self.perm[X] + Y b = self.perm[X + 1] + Y return (self._lerp(v, self._lerp(u, self._grad(self.perm[a], x, y), self._grad(self.perm[b], x-1, y)), self._lerp(u, self._grad(self.perm[a+1], x, y-1), self._grad(self.perm[b+1], x-1, y-1))) + 1) / 2 def _lerp(self, t, a, b): return a + t * (b - a)用这个噪声类生成一条流动线条的核心逻辑是:每一帧的角度由当前位置坐标作为输入去查噪声值,而不是用随机数。这样相邻帧之间角度的变化是连续的,线条自然平滑流动。代码如下:
noise = SimpleNoise() # 起始位置在画布中心向外一点 x, y = 600, 600 points = [] for i in range(1000): # 用当前位置的噪声值决定角度 angle = noise.noise2d(x * 0.005, y * 0.005) * 3.14159 * 4 step = 5 x += math.cos(angle) * step y += math.sin(angle) * step # 超出画布就停止 if not (0 < x < W and 0 < y < H): break points.append((x, y))这里有一个全项目最关键的参数:x * 0.005里的缩放系数。它决定了噪声的“频率”——系数越小,线条转弯越缓慢,整体走势越大气;系数越大,线条越曲折复杂。0.005是我在 1200 画布下试出的一个稳妥值,如果你希望线条更粗犷,可以调到0.002;想要更繁复的肌理,就往0.02方向调。这个参数没有标准答案,但每一次改动都会带来肉眼可见的视觉气质变化。
3.3 组合出作品:圆点 + 线条 + 几何嵌套
单一类型元素看久了总能审美疲劳。craft 的体现之一,就是把不同元素有逻辑地叠在一个画面里。我在 artcraft 里最常用的组合套路是:背景用大块渐变或噪声纹理,中层放几条流动的长线条作为骨架,前景散布大小不一的圆点或短线条作为点缀。
背景纹理的做法很简单:遍历画布上的网格点,用噪声值控制每个点的亮度,制造类似云雾的效果。比如这样:
for y in range(0, H, 4): for x in range(0, W, 4): v = noise.noise2d(x * 0.003, y * 0.003) v = int(255 * v) draw.point((x, y), fill=(v // 2, v // 2, v))这幅背景图的价值不是单独展示,而是给前景元素提供一个“有层次的底”,避免画面背景死黑或死白。然后再叠加 2.1 节里的圆点和 3.2 节里的流动线条,一幅完整的生成作品雏形就出来了。整个过程大概是:先定背景气氛,再定骨架线条,最后加细节点缀。这个顺序本身就是一种 craft 的工作流。
4. 色彩是数字艺术的分水岭
4.1 用 HSB 替代 RGB 来思考颜色
在图像库里,颜色默认用 RGB 表示。但做生成艺术的时候,我强烈建议你在心理层面换到 HSB(色相、饱和度、明度)来思考,哪怕代码里需要转回 RGB。
原因很简单:RGB 是给机器看的,HSB 是给人看的。你想调出一组“和谐的紫色调”,在 RGB 里可能要试好几组数字;但在 HSB 里你只需要把色相固定在一个区间,比如 260 到 300 度,然后让饱和度和明度在一个范围内浮动。这样生成的任何颜色,天然就在同一个色系里。
Pillow 里没有直接的 HSB 接口,但 Python 标准库的colorsys模块可以帮你转换:
import colorsys def hsb_to_rgb(h, s, b): r, g, bl = colorsys.hsv_to_rgb(h / 360, s / 100, b / 100) return int(r * 255), int(g * 255), int(bl * 255)有了这个转换函数,调色板定义就变得异常直观。比如“暮色紫”主题:色相锁定在 270±20,饱和度给 40 到 70,明度给 50 到 90。每次生成颜色时,在这个参数区间里随机取值,画面整体一定统一。
4.2 调色板设计:从画作里“偷师”结构化色彩
配色这件事,靠个人感觉是真不行——至少对我这种非科班出身的人不行。我的替代方案是从经典画作中提取成体系的调色板,然后把它们“翻译”成代码里的数据。
具体做法是:找一张你认为配色好看的画,用取色工具提取 5 到 8 个关键颜色,记录下它们的 HSB 数值,归到自己的调色板库里。举个例子,某张我常用的色彩参考提炼出来大概是:
| 颜色角色 | 色相范围 | 饱和度范围 | 明度范围 |
|---|---|---|---|
| 底色 | 220~240 | 15~30 | 10~20 |
| 主色 | 200~220 | 50~70 | 50~70 |
| 强调色 | 30~45 | 70~90 | 70~90 |
| 点缀色 | 320~340 | 60~80 | 80~95 |
在生成时,每种元素的颜色从不同的“颜色角色”对应的范围里取。背景用底色,大块线条用主色,细节圆点用强调色和点缀色。这样出来的作品,哪怕构图一般,色彩的和谐度也足以撑起整体质感。很多生成作品翻车,七成原因不是想法不好,而是颜色没有约束在体系内。
4.3 颜色数量的克制
刚接触生成艺术的那段时间,我特别喜欢把调色板数量堆得很高——总觉得颜色多、画面丰富。结果通常是一幅五颜六色的嘈杂画面,放在屏幕上还凑合,打印出来简直灾难。
后来我给自己定了一条铁律:同一个画面里,角色色不超过五个。底色、主色、次主色、强调色、点缀色,每种角色都有明确的用途,绝不混用。五个角色色看似少,但加上明度和饱和度的浮动范围,实际呈现的色彩变化已经足够丰富。真正耐看的生成作品,往往是颜色数量极少、但层次极多的。
5. 常见翻车现场与排查技巧
5.1 输出偏色:屏幕和打印颜色对不上
有一段时间我生成了一组非常有信心的作品,拿去印刷小样,结果回来之后颜色整体发灰、偏暗,细节糊成一团。排查之后发现问题有两个:一是我的显示器亮度调得偏高,屏幕预览看起来鲜艳,真实亮度其实不够;二是我用了太低的像素密度,大面积色块的边缘出现锯齿状过渡。
现在的可行方案是:显示器亮度校到常规办公亮度,不用额外调鲜艳模式;制图时把关键色彩区域单独放大检查,确认过渡平滑再做整幅输出。另外,如果偏色只是屏幕上出现但打印正常,那往往是显示器的色彩配置文件问题,可以在系统显示设置里切换回默认配置文件再核对一遍。
5.2 同样的代码,换台电脑结果不一样
你一定碰到过这个现象:在家跑同一个脚本,画面是理想效果;换到另一台电脑跑,明明代码一模一样,线条路径和颜色分布却变了。原因出在随机数种子。Python 的random库默认每次运行都会从系统熵源初始化,所以保证可复现的关键是手动设置种子:
import random random.seed(20250214) # 任何固定整数都可以种子设置之后,同一个脚本在任何电脑上跑,输出都会完全一致——前提是你没有在不可控顺序里调用随机函数,比如用集合遍历或依赖字典顺序的随机源。我在 artcraft 的每个生成配置里都会记录一个种子值,这样筛选出一张满意的图之后,随时可以反查出它的完整参数组合,方便后续微调。
5.3 生成过程极慢怎么办
5000 个元素、逐像素画噪点纹理,在纯 Python 循环里跑起来慢得让人崩溃。我的解决方案是让 NumPy 参与计算,把循环改成批量数组操作。比如前面那个逐像素点背景,用 NumPy 重写后速度能快几十倍:
import numpy as np # 生成网格坐标 ys, xs = np.mgrid[0:H:4, 0:W:4] # 向量化计算噪声值(简易版:用低频正弦叠加模拟平滑噪声) v = (np.sin(xs * 0.02) * np.cos(ys * 0.02) + 1) / 2如果是必须用循环的线条逻辑,那就尽量减少每一步里的重复计算,把常量和预计算值挪到循环外面。另外在调试阶段,把画布缩小到 600×600,参数稳定后再全尺寸输出,这个习惯能帮你省掉大量等待时间。
5.4 构图重心偏移,画面失衡
画了上百张图之后,我发现最常见的审美问题不是色彩,而是构图失衡。原因是大脑对中心区域敏感,而很多生成逻辑对全局的分布没有意识。比如圆点随机分布时,虽然数学上均匀,但视觉上人眼总会觉得某个角落特别空,某个区域特别挤。
要改善这一点,我推荐一个简单好使的“密度控制”策略:不直接在一个大画布上撒点,而是把画布切分成若干个大小不一的区域,每个区域单独控制元素数量。想营造中心聚集感,就让中心区域密度高、四周密度低;想营造均衡感,就设置每个区域的元素数量在一个小的浮动区间内。这个思路本质上是把“随机分布”升级成“可控的随机分布”,在保留了意外感的同时,把构图的主动权拿回自己手里。
6. 从 Demo 到完整作品:artcraft 的进阶玩法
6.1 批量生成与人工筛选的工作流
当你掌握了基础技法,下一步需要考虑的就是生产效率。artcraft 的进阶玩法之一就是批量生成:写一个脚本,循环不同的种子和参数组合,自动生成 100 张候选图,然后你像选照片一样从中挑出最满意的 5 张做精修。
这个流程的关键是给每张图加“元数据”。我的做法是在生成文件名里包含参数信息:
filename = f"artcraft_{seed}_freq={freq}_radius={radius}_hue={hue_base}.png"这样筛选时不但能看到图,还能直接追溯到那张图的参数组合。推荐的批量参数设置是:先固定一种图元类型,比如流动线条,然后分别调整频率、步长、线条数量、颜色角色这四个维度。每个维度试 3 到 5 个值,组合起来就是几十张风格差异明显的候选图。
6.2 从数字到实体:打印与文创衍生
生成艺术做到一定深度之后,大概率会想“做成实物”。我个人的经验是,数字作品打印到纸张上,质感提升是飞跃式的。推荐找专业印社测试几种纸材:艺术微喷的棉质纸适合细腻线条,会有手绘般的温润感;光面相纸适合色彩鲜艳的作品,对比度和层次感更强。
打印输出时有一个必须留意的细节:预留出血。如果做装裱,画布内容最好在四周多留 5% 的空白,避免裁切切掉构图边缘的元素。另一个经验是避免直接用 PNG 原图打印,最好从生成脚本重新输出一份矢量格式或超高分辨率的栅格版本,保证大尺寸输出时线条边缘依然锐利。
6.3 动态化:把静态图变成动图或交互屏保
如果你愿意再往前走一步,artcraft 的技法可以延伸到动态领域。最简单的方式是:把生成流程中某个参数做成时间变量,让每一帧的参数随帧号变化,输出成视频序列或 GIF。比如让线条的噪声缩放系数随帧号从 0.002 缓慢增大到 0.02,画面就会从“大气舒缓”逐渐变成“精致细密”,很有观赏性。
在 Pillow 中输出动图可以直接保存 GIF,但要注意 GIF 的颜色位深有限,渐变复杂的画面容易出现色块断层。更稳妥的方式是把每一帧保存为 PNG,然后用视频工具合成 MP4。目前做动态生成的主流选择仍然是前面提到的 p5.js 或 TouchDesigner,Python 适合离线渲染高质量动画序列,两者各有千秋,取决于你想要的是实时交互还是电影级画质。
7. 沉淀一套属于自己的“审美参数库”
artcraft 做久了,你会发现最宝贵的资产不是某一张好看的图,而是那一套你反复调校出来的参数组合。我自己的习惯是维护一份文本记录,每条记录包含:主题名称、使用的图元类型、颜色角色表、关键参数区间、踩过的坑备注。
这些记录越积越多之后,新作品的灵感往往不是从空白开始想的,而是翻一翻旧作品参数,思考“如果把这个线条频率改成两倍,会是什么感觉”“如果把底色改成另一个色相,气质是否完全不同”。生成艺术有趣的地方就在这里:它是探索式创作,算法给出骨架,审美做方向,参数是两者的粘合剂。
回到开头的那个问题——artcraft 到底能做什么。我的答案是:它给你一套把“想法”变成“作品”的可靠流程,同时教会你克制地使用随机性。真正好的生成艺术,就是在规则的框架里恰到好处地留出意外留下的空隙。那些空隙让你觉得这幅图是活的、是有呼吸的,而框架让人一眼看出作者的控制力。你可以从这个项目里学到具体的技术,但更重要的是建立起自己的创作节奏:先搭环境,再定规则,然后让随机在规则里跳舞,最后人工筛选、打磨、沉淀。这大概就是“artcraft”这个词里最珍贵的那层含义。