artcraft:程序化生成与人工调校结合的创作工作流
开头“artcraft”这个名字放在一起很有意思一半是 art艺术一半是 craft手艺。我在做这个项目的时候越来越觉得这两个词缺一不可——程序化生成只是把“艺术”的门槛降低真正让作品有味道的还是后面那一半“手艺”也就是你对参数的拿捏、对随机结果的选择、对人机协作节奏的控制。简单来说artcraft 是一套把程序化生成与人工调校结合在一起的创作工作流核心目标是用代码批量产出高质量的艺术图像素材同时保留每一张图的独特性和可控性。它能解决的问题是当你需要几十张风格统一但细节完全不同的配图、底纹、装饰素材时靠手绘或者反复拖拽设计软件实在太低效而完全丢给纯随机生成又很难控制风格方向。artcraft 的思路是在两者之间搭一座桥——让机器负责批量、负责探索、负责产出候选让人负责审美筛选和方向修正。适合来看这篇文章的是那些对创意编程、生成艺术、自动化设计工具感兴趣的人哪怕你只是刚接触 Python也没关系。我会把项目从设计思路、核心模块到具体落地的每一步都拆开讲包括我踩过的坑和最终留下的经验。这篇文章不是拿代码炫技而是想让你也能照着搭一套属于自己的生成素材工作流。1. 项目定位与整体设计思路1.1 为什么叫“artcraft”它不是什么新发明而是一种工作流很多人一看到程序化生成第一反应是“这不就是把画图交给代码吗”或者“是不是学完这个就能用 AI 批量出图”。实际上artcraft 并不是一个纯粹的自动化工具它更像是一套“有审美参与的流水线”。核心区别在于随机生成只是第一步之后还需要一个人工筛选、参数回溯、方向微调的过程。换句话说机器提供的是“可能性”人负责的是“选择意义”。这就像做手工艺品车床能把木头车出胚型但最后修形、打磨、上油都是靠手上的感觉。所以我更愿意把 artcraft 描述成一种“数字手工艺实践”而不是“全自动生成器”。做这个项目最初的触发点很简单某一次做视觉展示需要一套统一风格但细节各异的背景纹理手绘了几张之后发现工作量太大而且很容易陷入“越画越像”的重复感。当时就想能不能写一个程序把“风格”拆解成一系列可控参数再让参数组合自动产生大量变体——比如同样的配色倾向、同样的构图逻辑、同样的笔触密度但每一张的局部结构完全不同。这正是 artcraft 想解决的问题批量生成但保留变化保留变化但仍在可控范围内。1.2 方案选型为什么选择程序化生成作为核心路径程序化生成并不是唯一的选择市面上也有不少素材网站、AI 图像生成服务但它们各有问题。素材网站下载的图片往往带有明确的版权限制风格也未必贴合自己的需求AI 图像生成虽然灵活但可控性和精细度不够而且如果要生成几十张统一风格的系列图提示词的一致性很难保证。相比之下程序化生成有它不可替代的优势参数完全透明、可复现、可批量运行而且每张图之间的关系是明确的你可以清楚地知道改变某个参数会对全局产生什么影响。从技术选型层面我选择了 Python 作为主力语言。原因很直接Python 的图像处理生态足够丰富Pillow、numpy、noise 等库让像素操作变得非常容易上手而且批量处理和参数管理都很方便。核心渲染并不是走复杂的 3D 管线而是用二维图层合成的方式——因为它处理抽象几何、花纹、渐变底纹这类素材已经绰绰有余。如果你要做的东西偏写实或者需要物理正确那可能需要换更重的渲染引擎但就 artcraft 当前的目标而言轻量、快速、可控是优先级最高的需求。一个值得提前说明的设计理念是不要追求单个生成器能够包揽所有风格。与其写一个“什么都能生成但什么都一般”的巨型脚本不如为每一种风格单独写一个生成器模块再通过统一接口管理和调用。比如“柔和水彩风”“几何拼贴风”“极简线条风”每个生成器只负责一件事但把它做好。这就是 craft 那部分的价值——对每一种视觉风格的深入打磨而不是一股脑地堆功能。1.3 需求拆解从最终用途倒推功能边界做这类项目最容易犯的错是上来就写代码写到一半才发现做出来的东西不是自己真正需要的。我在拿到需求之后第一件事是把最终用途列出来再倒推功能边界。artcraft 的典型使用场景是生成系列素材用于展示背景、卡片底纹、海报装饰、包装纹理或者作为更有设计感的占位图。这些用途有几个共同要求输出尺寸要够大、色彩模式要可控、系列整体风格统一、单张之间差异明显。回到功能设计上这决定了几个关键取舍。第一渲染分辨率按 3000×3000 甚至更高来算插值算法和抗锯齿处理必须提前考虑不然后期放大全是马赛克。第二颜色不能只在 RGB 空间里瞎调要引入一套色板机制让生成器从预定义的、人为挑选过的色板中取色而不是让程序自己满色域跑。第三风格统一性靠的是“全局参数”和“局部随机”两层结构全局参数控制配色范围、构图方式、元素密度局部随机控制每个点、每根线、每个形状的具体形态。这个分层结构是整条流程最核心的骨架。还有一个容易被忽略的需求是“可回溯”。你生成一百张图总有那么一两张特别满意如果程序没有记录生成参数你永远无法再复现它也无法基于它做微调。所以在设计阶段就要强制给每一次输出附加一份参数记录文件。这听起来像是小事但在实际使用中这一条直接决定了你愿不愿意持续跑批量生成——没有回溯能力的批量生成本质上是碰运气。2. 核心模块与关键技术拆解2.1 种子体系一切随机的基础设施如果只用一个词来概括程序化生成项目的骨架那就是“种子”。种子的作用是确定一组随机序列同一个种子配合同一种逻辑会生成完全一样的结果。这个机制的价值不是让你“重复出图”而是让“随机”变得可管理——当你想让背景元素随机排布但又需要整体系列风格一致就可以用一个总的风格种子去控制全局另加局部种子去控制每一张图的细节。我一开始犯过一个非常典型的错误在代码里全局调用随机函数结果发现每次运行结果都完全无法复现——因为随机流里还混入了系统时间、内存地址等因素。后来把设计改成了严格的“种子树”结构才彻底解决这个问题。种子树的具体做法不复杂最顶层是项目种子把它固定后就能派生出风格种子、构图种子、纹理种子等等。每个子种子只负责自己那一层级的随机需求。import random project_seed 20250214 rng random.Random(project_seed) style_seed rng.randint(0, 10**8) composition_seed rng.randint(0, 10**8) texture_seed rng.randint(0, 10**8)这样做的直观好处是你调整纹理种子不会影响构图因为构图用的随机序列根本不再依赖同一个随机流。这个解耦能力在日常使用中非常奢侈——当预览图出来后想优化纹理细节却不希望版式变动你只需要单独更换纹理种子就行。所有种子状态都写进参数文件即使隔了很久也能准确找出是哪一组种子生成了你喜欢的那张图。2.2 参数空间设计把“风格”翻译成数字风格这个词太虚了要把它交给程序必须先翻译成一组可量化的参数。artcraft 里我习惯把参数分成三组全局色调参数、结构密度参数、局部扰动参数。全局色调参数至少包括主色、辅色、点缀色的色板索引以及背景亮度、色彩对比度。结构密度参数常见的有元素数量、线条间距、形状缩放范围。局部扰动参数有随机偏移量、旋转角度范围、透明度变化幅度。把好看的设计拆分到这些数字之后程序才知道你想让它往哪个方向生成。有一个让我印象很深的教训一开始我把参数定得太死比如线条宽度固定是 2 像素结果出来的图远看清一色近看又密又乱。后来把“线条宽度”改成一个范围比如 0.8 到 4 像素并在生成时按正态分布采样效果立刻好了很多。原因是现实中的手绘线条本来就有粗细变化这种不均匀感恰恰是“生成艺术”区别于“贴图感”的关键。参数空间的另一个核心设计是“参数域要有边界”。不是每个参数都可以随便乱调尤其是色调你让程序在 RGB 三通道独立随机出来的颜色大概率是一坨脏色。更靠谱的做法是预定义几组经过人为筛选的色板程序只允许在色板内部及其邻近色之间取色——palettes { forest: [(34, 67, 54), (78, 105, 67), (194, 179, 128)], sunset: [(244, 162, 97), (213, 94, 83), (104, 42, 61)], ocean: [(23, 82, 110), (64, 135, 166), (195, 219, 222)], }只有当你把某些事情限制住程序才可能在另一些维度释放出惊人的创造力。2.3 图像生成管线从噪声到画面的多层合成artcraft 的图像生成管线核心思路是“分图层合成”。就像你用设计软件做图一样背景一层、结构一层、装饰一层、调色一层只不过这里是用代码操作像素。每一层都负责画面的一部分职责最后合并到一起形成完整的图像。第一层是背景层通常是用渐变或者噪声生成的底色制造出一个有层次、不单调的空间感。第二层是结构层它是画面的骨架可能是网格、几何形状、线条也可能是某种重复性的纹理排列。第三层是装饰层它负责增加局部细节比如散布在画面中的小圆点、短线、花瓣形元素让画面不空旷。最后是调色层我会叠加一个整体的色彩映射让所有图层的颜色统一到同一个氛围里。噪声函数这一步值得多说一点。如果你直接用纯随机生成纹理效果会是满屏电视雪花完全没有视觉美感。更好的方法是使用平滑噪声比如 Perlin 噪声或者 value 噪声这类噪声产生的是连续的、逐渐变化的随机场拿来做有机纹理、水波感、云纹效果非常合适。实现上可以用 noise 库也可以手写一个简单的平滑随机网格。为了让云纹效果更丰富我会叠加几个不同频率的噪声再经过一个曲线映射把灰度值压到想要的范围这一步通常能瞬间让画面从“工程感”变成“艺术感”。from noise import pnoise2 def gradient_noise(width, height, scale0.01): pixels [] for y in range(height): row [] for x in range(width): value pnoise2(x * scale, y * scale, octaves4) row.append((value 1) / 2) pixels.append(row) return pixels2.4 输出管理系统批量生成之后如何挑图只写生成逻辑不写输出管理会让你迅速陷入“文件堆成山”的困境。我建议把输出管理当成一个独立模块来设计而不是在生成脚本末尾随便加几行保存代码。一个好的输出管理需要做三件事统一命名、附加参数、自动归类。统一命名规则至少要包含生成器类型、种子号、生成日期这几个关键信息。比如geo_forest_20250214_seed88312.png这样一眼就能看出是哪个生成器、用的哪个色板、哪一天产的。附加参数更关键每一次生成必须同步产出一个 JSON 文件里面完整记录所有参数。自动归类则是按日期或者系列分批建文件夹避免所有图都堆在同一个目录下。批量生成完一百张图之后还需要一个快速预览的方式。我会用脚本生成一个 HTML 预览页把图片缩略图和参数说明放在一起这样筛选时根本不用去挨个打开大图。看预览页就像逛画廊几十秒就能扫完一百张图挑出五张中意的再根据参数记录回溯它们的种子去做细化调整。3. 实操过程与核心环节实现3.1 环境准备与项目初始配置我用的环境是 Python 3.10windows 和 mac 上都跑过依赖主要是 Pillow、numpy、noise 这三个库。如果只是生成 2D 抽象图像这些足够完全不需要装深度学习相关的庞然大物。pip install pillow numpy noise初始化项目时我会把目录结构规划成四个核心模块generators存放不同的风格生成器pipeline存放图层合成和渲染逻辑output存放生成图片和参数文件tools存放预览脚本、批量运行脚本。这样做的直接好处是每新增一个风格生成器不需要改动原有模块只是往generators里加一个文件而已。有一点值得新手注意不要在一开始就追求生成效果的完美先把整个链路跑通——哪怕是生成一张纯色背景加上几个随机圆形也好过花两周做高难度纹理模块却迟迟输出不了第一张图。链路能跑通意味着后续所有的调试和优化都有一个稳定的基准。3.2 核心生成器示例从零构建几何抽象风我以“几何抽象拼贴”这一个生成器为例讲讲从零构建的完整过程。这个风格的特征是干净的背景色、若干个有重叠关系的大几何形状、以及少量细节点缀。实现逻辑并不复杂但在细节上有不少讲究。第一步是创建画布和背景层。背景不用纯色可以用一个带轻微径向渐变的渐变底色这样画面不会死板。from PIL import Image, ImageDraw, ImageFilter import random def make_background(width, height, base_color, edge_color): img Image.new(RGB, (width, height), base_color) gradient Image.new(L, (1, height)) for y in range(height): t y / height gradient.putpixel((0, y), int(255 * (1 - t))) gradient gradient.resize((width, height)) overlay Image.new(RGB, (width, height), edge_color) img Image.composite(overlay, img, gradient) return img.filter(ImageFilter.GaussianBlur(2))这里用了一张灰度渐变图作为蒙版来做两个颜色的柔化混合效果比直接遍历像素要快得多这也是 Pillow 操作里很实用的一个技巧。第二步是生成结构层的几何形状。形状种类我控制在圆形、三角形、矩形三种。为了让画面有“拼贴感”形状之间会有重叠而且重叠部分采用一定的透明度让下面的颜色隐隐透出来。def add_shape(draw, shape_type, position, size, color, alpha): overlay Image.new(RGBA, size, (0, 0, 0, 0)) odraw ImageDraw.Draw(overlay) bbox (0, 0, size[0], size[1]) if shape_type circle: odraw.ellipse(bbox, fillcolor (alpha,)) elif shape_type triangle: mid_x, mid_y size[0] / 2, size[1] / 2 odraw.polygon([(mid_x, 0), (0, size[1]), (size[0], size[1])], fillcolor (alpha,)) else: odraw.rectangle(bbox, fillcolor (alpha,)) return overlay使用 RGBA 模式叠加是做出丰富的颜色层次感最直接的手段。每一次叠加都保留透明度形状交界处自然产生新的混色效果这种效果靠纯手动设计非常费时而程序生成只是顺手的事。第三步是加一层细节点缀。点缀通常是细线条、小圆点或者十字标记。这一步的作用是打破画面里大型几何块的呆板感让视线有停留点。我习惯让点缀元素集中在形状的交界处或者画布的三等分线附近这样既自然又有一定的视觉引导作用。最后一步是整体调色。我用一个颜色映射函数把画面颜色统一向目标色板靠拢降低颜色之间的突兀感。这一步做完画面基本就完整了导出前再对边缘做一次轻度柔化就大功告成。3.3 参数调优实战一次完整的调参过程调参是一个生成器从“能看”到“好看”的关键阶段也是最考验功力的部分。我用最近一次调参过程举个例子。当时的目标是“柔和但略有层次感的卡片底纹”初始参数是主色色板用ocean圆形的数量是 20透明度 180扰动范围 0.5。结果生成的画面太平均了所有圆形大小接近、分布均匀反而显得机械。第一次调整是把圆形的半径范围拉开从固定大小改成 30 到 120 之间的随机值。效果好了一些但画面仍然缺少主次。第二次调整是引入“焦点”机制在画布中心附近放置一个大圆让它比其他元素大一到两倍这样视觉立刻有了重心。第三次调整集中在透明度上。我发现透明度 180 时叠色效果不明显颜色之间缺少融合感改成 120 之后底层颜色透上来的程度更强画面的色彩层次一下子丰富了。但透明度过低也会有问题——大量颜色叠加之后容易产生浑浊感所以最终档位落在 130 附近比较理想。这次调参给我的体会是一次不要调多个参数。你一次改三个东西效果确实可能变好但你不知道到底是哪一个参数起了作用。最有效的调法是锁定其他变量只动一个参数然后连续生成几张预览图逐张对比。虽然看起来慢但每一轮都能得到确切结论长线来看反而是最快的路径。3.4 批量运行与自动筛选解放双手不瞎跑单张生成跑通之后批量运行就是一个体力活与艺术决策分离的关键节点。我写了一个批量脚本按参数网格展开。比如我想试三种色板、四种结构密度得到十二种组合每种组合生成十张图一共一百二十张输出。听起来很多但自动跑下来也就几分钟的事。批量脚本的核心是生成参数文件。每一次输出都同步生成一个 JSON 文件记录项目名、生成器名、种子、各个参数值。这样之后不管是手动挑图还是做自动化筛选都有一个明确的数据依据。自动筛选这一块我强烈建议加两道过滤。第一道是图像质量过滤检查生成图像是否纯色过多或者完全没有内容。这种检查可以用 numpy 统计颜色分布如果某个颜色占比超过 95%基本可以判定这张图“空白”直接删掉。第二道是去重检查。生成一百二十张图难免会有几张极其相似的用感知哈希pHash算法可以快速对比图像间的相似度把相似度超过阈值的图只保留一张。这一步能极大减轻人工挑图的负担毕竟人的精力应该花在“选好看的”上而不是花在一堆几乎一样的图里翻来覆去。def dhash(image, hash_size8): resized image.resize((hash_size 1, hash_size)) diff [] for row in range(hash_size): for col in range(hash_size): left resized.getpixel((col, row)) right resized.getpixel((col 1, row)) diff.append(left right) return diff感知哈希算法原理很简单——比较相邻像素的亮度高低形成一个二进制序列两个序列的汉明距离如果在阈值以内就认为两张图相似。虽然它不能精确感知构图差异但对于筛选重复底纹、近似渐变这类情况已经特别实用了。3.5 一个完整的端到端运行实例为了让整个过程更可感知我展示一个端到端的运行实例。假设项目目标是“生成十二张海洋风底纹素材风格统一但互不相同”。运行过程如下设置项目种子为 20250214生成器选择“几何抽象风”色板选择ocean结构密度设为中等单张尺寸设置为 2400×2400批量数量十二张。运行完之后程序自动输出十二张图片和十二个 JSON 参数文件。我打开预览页首先排除掉两张颜色过于单调的再排除掉三张构图过于平均的剩下七张里有五张符合需求方向。最终选中三张决定以其中一张为基础做进一步微调——通过读取它对应的 JSON 文件把它作为初始参数微调透明度从 130 调到 150重新生成一个新变体效果立刻更贴近设计需求。这种“生成—筛选—回溯—微调”的闭环模式是 artcraft 工作流的本质特征。它不像传统设计那样一笔一笔画出来也不像纯随机工具那样只能碰运气而是介于两者之间机器负责提供候选人负责做选择和修正。谁跟这个工作流磨合得越久谁就越能理解“手艺”二字在数字时代仍然没有过时。4. 常见问题与排查技巧实录4.1 生成结果不可复现随机数使用不规范的坑最常见的问题是同样一条命令跑两遍结果完全不一样。这个问题的根因几乎总是随机数生成器的调用顺序和状态没有被控制好。如果你在代码里混用了random.random()、random.randint()以及np.random它们的随机序列彼此独立会共同导致结果无法复现。解决方法是统一随机源。我的经验是项目内部只用random.Random(seed)这一套同时禁止在模块里直接调用random.random()统一通过一个传入的随机对象去取随机值。与此同时numpy 的随机操作也要单独指定种子不让它依赖全局状态。提示检查随机源的最佳方式是先锁定主种子然后跑两次脚本对比输出参数文件中记录的种子状态是否完全一致。如果状态一致性全部通过但图像仍不同八成是某些缓存或者多线程导致了顺序错乱。4.2 颜色发脏、灰度浑浊调色前先统一色彩空间生成结果总是“灰蒙蒙”“脏脏的”这个问题几乎每个人都会遇到。原因通常是两个一是直接在两三个 RGB 值之间线性插值产生中间色这些中间色往往偏灰二是多层高透明度叠加时颜色累积导致饱和度不断下降。解决方案分两步。第一步是避免在所有颜色之间做均匀插值改在 HSL 空间内做一些微调——比如固定色相只调整饱和度和亮度这样中间颜色也会保持干净。第二步是每次叠加完成后做一次饱和度的补偿把整幅图像的饱和度略微拉回来恢复颜色的精神气。在程序里做全局饱和度调整并不复杂转成 HSV 之后把 S 通道整体乘以一个系数即可。不过要注意饱和度增益不宜过高否则颜色会溢出产生刺眼的荧光感。我常用的增益范围是 1.05 到 1.2。4.3 图像边缘锯齿、清晰度不足插值与分辨率策略当你把图像缩略图放大的时候发现边缘有锯齿或者在低分辨率下直接输出大图时细节糊成一片这通常是插值和渲染分辨率策略的问题。以 Pillow 为例保存 PNG/JPEG 时默认的缩放操作可能使用的是快速但粗糙的最近邻插值导致硬边变得粗糙。解决方案很简单在最终导出前如果需要对图像进行缩放统一使用Image.LANCZOS插值算法这是 Pillow 里效果最好的标准插值算法之一。而且抗锯齿不能依赖后期补要在生成期间就做多重采样。我常用的做法是按最终尺寸的两倍渲染画面在最后一步缩小到目标尺寸缩小过程会自然柔化边缘产生抗锯齿效果。注意多重采样放大会增加内存和计算时间。生成 6000×6000 的临时图像再做缩放对于复杂生成器来说耗时非常明显。合理的做法是只在需要的图片上开启高倍采样批量初筛阶段用低分辨率快速出图即可。4.4 系列图风格不齐局部随机与全局参数没分离做系列素材最怕的是十张图放一块儿看起来像来自十个不同项目。风格不齐的根本原因是全局参数没有被锁住导致每张图的基础风格都发生了漂移。比如色板是同一组但每次运行时背景亮度参数都被随机重置了最终成品自然风格混乱。正确做法是将风格控制相关的参数固定为全局值放进项目配置文件中只有允许变化的参数才在每次生成时重新采样。我通常把色板索引、构图类型、元素类型范围、整体色调映射方式设为全局锁定参数把元素数量、大小范围、具体位置、颜色微调设定为局部可变参数。这样生成出来的系列图风格统一感会自然浮现——因为骨架一样只是血与肉各有各的长相。4.5 大参数空间容易“乱花渐欲迷人眼”网格采样策略当参数组合特别多时你会陷入选择困难症。比如三种色板、四种密度、五种装饰风格理论上六十个组合每个组合跑二十张总共就是一千二百张图把硬盘撑爆不说预览难点也剧增。更合理的做法是分阶段采样。第一阶段先用小批量每种组合三张跑一遍从中选出最有可能出现好结果的组合方向第二阶段对选出的组合方向做高密度生成。这样能避免在没意义的组合上浪费大量算力也能保证最终素材的质量更高。调参和投资一样先小额试错再重仓买入整个过程靠的是逐步收敛而不是一锤定音。4.6 独家避坑技巧不要迷信随机要培养“视觉味觉”最后分享一个不算技术但少走弯路的经验。我看到很多人拿到生成器第一件事就是狂跑几千张然后从中翻找“惊喜”。这种模式偶尔会出一两张厉害的作品但长期来看非常消耗时间而且很容易让你失去对风格的敏感度。更好的做法是“小步快跑加人工干预”每次只生成二三十张认真看每一张弄清楚是哪些参数组合带来的观感差异。当你对参数与画面之间的映射关系越来越熟悉你就有了某种“视觉味觉”——一眼看到预览图就能猜到大概是哪些参数处于什么范围。这种直觉是机器不具备的也是 artcraft 把“art”和“craft”结合起来的真正价值所在。结尾做到这一步artcraft 已经从一个只有功能型脚本的工具慢慢变成了我个人创作习惯的一部分。现在每次做视觉素材我不会第一时间去打开设计软件而是先想清楚这批素材的风格边界在哪里然后打开终端跑一批生成图再从结果里挑出最顺眼的几张做深化。反而是在这种离线的、慢速的筛选过程中审美被一点点磨得更敏锐。最后再分享一个小技巧在批量的参数记录文件里除了技术参数我会额外加一个字段叫“mood”手动给每张图标注当时看到它时的直观感受比如“安静”“跳跃”“复古”“冷淡”。这个字段在最初看起来没什么用但积累了几百张图之后你就能慢慢发现自己偏好的视觉偏好在参数空间里是什么分布。这时候再去做新系列起步方向会明确很多因为你已经知道自己真正想要的是什么了。这个项目后续还可以往交互方向扩展比如做一套参数面板把所有关键调整项变成实时滑杆操作者拖动参数的同时即时预览效果。这样不仅适合自己用也可以开放给团队里其他不熟悉代码的伙伴让更多人参与到生成过程里来。但那是后面的事了——至少目前把生成器、参数管理、批量筛选这一套基础打扎实已经足够让创作效率提升一个台阶。