资讯动态

NFT艺术品随机生成器源码解析:从图层组合到元数据铸造

发布时间:2026/10/27 23:11:41 来源:尧图企业网站定制
简介面向NFT艺术生成场景的Java源码包实现动物主题NFT图片的批量随机生成。程序将背景、身体、饰品、头部、头发和眼镜等独立PNG元素通过随机算法组合赋予每张图片独特标识适合对NFT应用、Java图像处理及随机生成感兴趣的开发者阅读和二次开发。资源共55个文件包含36个PNG素材、9个Java源码文件以及说明txt、JSON配置、README、License等压缩包22.5MB目录区分HipsterDogs与HipsterCats主题素材便于按模块研究。通过源码可掌握BufferedImage素材加载、Graphics2D图层绘制与随机选择机制结合说明文件可快速运行项目并进一步扩展元素、调整策略或设计新风格。已有4314人学习适合作为NFT生成器入门与进阶参考。1. NFT艺术品随机生成器源码它到底在生成什么NFT艺术品随机生成器源码听起来像是一个“自动拼图”脚本但实际要解决的是三件事用有限素材生成足够多的唯一组合、让每个组合在视觉和规则上都成立、把组合信息转成链上能读的元数据。很多人以为跑一个脚本就能得到一万张图结果组合数不够、元数据错位、权重反而坑了自己最后卡在铸造这一步。真正做数字藏品项目的团队多半不是去接一个现成SaaS而是先找一份能改的源码把生成逻辑捏在自己手里。它不适合零基础读者更适合手里已经有一批PNG素材、想跑通生成和铸造流程的开发者。我会按我自己的落地顺序讲先跑通随机合成再调稀有度和约束最后做元数据复现。2. 随机生成器的核心逻辑从图层到唯一组合拿到一份随机生成器源码后我最关心的不是代码有多长而是它把“随机”放在了哪一层。常见做法是把画布拆成多个图层每个图层放一组变体生成时各自抽一个再用Image.alpha_composite一层层贴上去。很多朋友看到“python源码”就直接改文件夹路径但我会先花十分钟把目录结构和图层顺序理顺不然后面稀有度、去重全都会乱。2.1 程序化生成的基本模型图层、特征码与组合数一张最终作品实际上是一个“图层组合”。我习惯把项目目录组织成layers/ ├── background/ │ ├── blue.png │ ├── gold.png │ └── red.png ├── body/ │ ├── alien.png │ └── robot.png └── eyes/ ├── normal.png └── laser.png运行时按顺序从每个目录里随机取一个文件。为了去重和生成元数据我会把每个被选中的变体名称拼成一个特征码例如background_gold-body_alien-eyes_laser。这个字符串就是后面去重和attributes的原材料。组合数的计算很简单把所有目录的变体数相乘。上面这个例子是3 * 2 * 2 12只能生成12个唯一组合。很多项目想要“一万个不重复”如果图层数只有三四个变体数不够就必须增加图层或增加变体不然就只能接受大量重复。换个说法随机生成器的“容量”不是由生成次数决定而是由组合数决定。这个数在一开始就值得算清楚否则后面所有稀有度分析都是在沙子上盖楼。这里有一个边界点很多项目会在中间图层加入“无”这个选项。比如帽子图层有一个none.png表示不戴帽子它也是一个变体。这个选项会显著增加组合数但在元数据里应该作为普通属性保留判断稀有度时一般不把它当稀有特征。另外如果图层目录里混入了Thumbs.db这类系统文件os.listdir会把它们也当成变体轻则生成废稿重则程序崩溃。所以我进场第一件事就是给图层目录写一个白名单过滤只保留.png和.jpg两类文件。2.2 最小可跑的Python生成脚本用PIL把图层叠起来我常用Python原生的Pillow库做合成。原因只有一个它对透明PNG的支持最省事alpha_composite能把多层透明图层按顺序叠在一张画布上不需要处理像素合并之类的细节。下面是一个最小脚本它不处理权重先验证素材能不能合成。# generate_one.py # 依赖pip install pillow import os import random from PIL import Image LAYERS [background, body, eyes, mouth] BASE_DIR ./layers # 图层素材根目录 OUTPUT_FILE output.png def pick_variant(layer_name: str) - str: 从某个图层的目录里随机挑一个变体文件名 layer_dir os.path.join(BASE_DIR, layer_name) variants os.listdir(layer_dir) return random.choice(variants) def generate_one() - dict: composite None traits {} for layer in LAYERS: variant pick_variant(layer) path os.path.join(BASE_DIR, layer, variant) img Image.open(path).convert(RGBA) if composite is None: composite Image.new(RGBA, img.size, (0, 0, 0, 0)) composite Image.alpha_composite(composite, img) traits[layer] variant composite.save(OUTPUT_FILE) return traits if __name__ __main__: print(generate_one())这段代码有三个参数值得关注。第一是LAYERS的顺序背景应该在最前装饰在最后顺序错了会把前一层盖住。第二是Image.new(RGBA, img.size, (0, 0, 0, 0))最后一个(0,0,0,0)是全透明底色如果写成纯黑或纯白合成结果会出现大面积不想要的底色。第三是每个图层素材必须统一尺寸alpha_composite对两张尺寸不一致的图会直接抛异常这也是新手最常见的报错。还有一个容易忽略的参数输出格式。合成结果必须存成PNG不要用JPG因为JPG会压缩掉透明通道并将背景填成黑色导致刚生成的透明背景图变成黑底。Pillow的save默认会根据扩展名推断格式所以文件名里写output.png而不是output.jpg。提示不要把图层素材放在某个盘符的根目录尤其不要用中文路径。Pillow在Windows下遇到非UTF-8路径会有诡异的报错这种问题排查起来很费时间。2.3 组合的唯一性与指纹如何判断两个结果不重复当生成数量接近组合数时完全靠重新抽容易踩进“随机碰撞”的坑。最简单的去重方法是用特征码集合。# generate_unique.py import os import random LAYERS [background, body, eyes, mouth] BASE_DIR ./layers SEED 20240101 # 固定种子后面会讲它的作用 rng random.Random(SEED) def generate_unique(count: int) - list: seen set() generated [] while len(generated) count: traits {} for layer in LAYERS: variants sorted(os.listdir(os.path.join(BASE_DIR, layer))) traits[layer] rng.choice(variants) fingerprint |.join(f{k}:{v} for k, v in traits.items()) if fingerprint in seen: continue seen.add(fingerprint) generated.append(traits) return generated这里的sorted不是可有可无。os.listdir的返回顺序在操作系统层面并不保证稳定同一台机器不同时间、不同文件系统可能顺序不同。如果随机源用了固定种子但目录顺序在变同一种子生成同一tokenId的图就会不一样这个坑会在后面专门展开。指纹字符串用|连接键值对顺序固定为LAYERS的顺序这样可以避免因dict遍历顺序不同导致同一个组合被当成两个组合。实际工程里我会在这个指纹上再做一次SHA256但去重时直接用原字符串就够了。更严格的去重还需要检查图片文件本身的二进制哈希防止两个变体内容相同但文件名不同。但直接对PNG文件做哈希也有一个陷阱Pillow保存PNG时会写入一些元数据两张内容完全相同的图可能因为保存参数不同得到不同哈希。如果要做视觉去重我一般先把图像数据重新保存成统一格式再哈希或者用感知哈希做近似去重不过后者在严格要求“每个tokenId必须有区别”的场景下会有误判风险。3. 稀有度设计权重、约束与概率陷阱随机生成不只是“平均随机”。如果每个变体等概率那么背景、身体、眼睛各自出现次数接近均衡对买家来说就没有收藏盼头。这一章讲的是如何把“稀有度”从概念变成可计算的参数。先说明这里的权重表和约束规则本身也应该作为源码的一部分被保存因为它们是决定项目调性的核心。3.1 权重不是百分比常见加权方式与计算我见过不少人直接在代码里写random.randint(1,100)然后判断是否小于某个阈值来实现“10%概率”。这种方式在小项目里能跑但一旦权重表变大很难维护。正确做法是把权重和素材分开用random.choices按权重抽取。import json import random from pathlib import Path def load_weights(layer_name: str) - dict: # weights.json 形如{eyes: {normal: 90, laser: 10}} with open(./weights.json, encodingutf-8) as f: data json.load(f) return data.get(layer_name, {}) def pick_weighted(layer_name: str, rng: random.Random) - str: layer_dir Path(f./layers/{layer_name}) variants sorted([p.name for p in layer_dir.iterdir() if p.suffix .png]) full_weights load_weights(layer_name) weights [full_weights.get(v, 1) for v in variants] # 没配置的默认权重1 return rng.choices(variants, weightsweights, k1)[0]random.choices的weights是相对权重不是最终概率。比如“laser”权重10、“normal”权重90那“laser”的概率是10%但如果“normal”权重改成180“laser”概率就变成约5%。所以调权重时不要只看单个数字要留意同图层内的权重总和。还有一个容易忽略的点整件作品的稀有度是各图层选中概率的乘积。假设背景某变体单层概率是1%眼睛某变体单层概率也是1%两者同时出现的概率就是0.01%。如果你只知道某个图层里的概率而不知道组合后的理论概率后期很容易被市场反馈打脸。我一般会在生成前写一个统计脚本把所有合法组合的概率算出来保底也能排个稀有度名次。这一步用不了多少代码但能回答“这个项目到底存在多少个不同稀有度层级”。3.2 必选项与互斥规则用约束条件避免废件随机组合会产生视觉上“打架”的属性。比如背景是“海洋”头饰是“火焰”单看没问题叠在一起就很违和更严重的是一只手和一个背景图层发生遮挡整个头不见了。这种问题靠素材修不现实应该在生成逻辑里加约束。def validate(traits: dict) - bool: 返回 False 表示这个组合不合法需要重新抽 if traits.get(background) ocean and traits.get(headdress) flame: return False if traits.get(eyes) closed and traits.get(mouth) open_smile: return False return True在生成循环里这样用先抽一组不合法就重新抽直到合法或达到重试上限。上限我一般设10次因为如果约束太严连续抽十次都不合法说明权重和约束是冲突的继续重试只会让程序空转。出现这种情况我会先检查是约束写错还是某些变体权重低到几乎抽不到。约束还有一个副作用它会少掉大量组合。设计阶段最好先把组合数重新算一遍避免把组合数从10000压到2000产出数量却还按10000来规划。我通常会把约束也纳入组合数枚举脚本每加一条规则就重新算一次这个习惯能避免很多临发售时的数据翻车。3.3 给每个Token写元数据JSON结构与属性描述生成图片只是第一步链上合约和交易市场读的是元数据JSON。不同市场对字段要求略有差异但核心结构是一样的。我常用这个表作为字段清单字段类型说明namestring作品名通常包含tokenIddescriptionstring项目描述会原样展示imagestring图片URI指向去中心化存储attributesarray属性数组每个元素包含trait_type和valueattributes是买家判断稀有度的主要入口所以这段最不能错。写JSON的代码很直白import json import os def write_metadata(token_id: int, traits: dict, image_uri: str) - None: attributes [ {trait_type: layer, value: value} for layer, value in traits.items() ] metadata { name: fPixel Kit #{token_id}, description: A collection of randomly generated pixel cats., image: image_uri, attributes: attributes, } out_path f./metadata/{token_id}.json with open(out_path, w, encodingutf-8) as f: json.dump(metadata, f, ensure_asciiFalse, indent2)这里有两个细节。第一traits的遍历顺序必须固定否则不同机器生成的JSON里属性排列顺序会变虽然不影响合约但会影响市场页面的展示顺序买家看着会像“每张图属性乱跳”。第二name里的tokenId要和文件名一致1.json里写#1不要写#0001又在文件名里用001这种不一致后期没法批量修。value字段我通常会去掉.png后缀只保留变体名这样在交易市场里显示更干净。如果你想要属性显示成“金色稀有”这类可读标签也可以单独维护一张变体名到显示名的映射表但千万别在生成代码里写死因为后期改素材文件名会导致元数据全部重做。注意有些市场读的是attributes里的value作为字符串少数读整数。生成前先确认目标市场格式再决定是保留.png后缀还是只留变体名。4. 铸造前的工作元数据上传与链上关联图生成完、JSON写完生成器源码的工作还没结束。不要把这个生成器当成黑匣子跑完图片就往上传。NFT铸造时合约通常只存一个URI买家浏览器和市场要靠这个URI去拉图片和属性。所以这章讲两个问题图片放哪、JSON怎么组织和校验。很多人把生成器跑完就当结束实际上铸造前的数据准备才是决定项目能不能顺利上线的关键。4.1 为什么不建议把图片直接上链链上存储按字节计费一张1MB的PNG上链成本很高而且大部分公链对单笔交易数据大小有上限。常见做法是图片放去中心化存储链上只存指向元数据JSON的URI。这样合约体积小图片也可以做到高清。当然也有团队直接把图转成base64塞进JSON但那适合极小尺寸的图或测试网主网项目我还是建议走正经的去中心化存储流程。生成器源码可以不管上传这一步但JSON里的image字段必须按“最终要上传到哪里”来设计否则后面还要改全部JSON。我见过最折腾的情况是先写死了http://localhost:8080/...等上传服务器后又要跑批量替换白白增加一次全量修改的风险。4.2 本地文件指纹与IPFS路径组织我生成的图片和JSON会按下面的目录组织上传时整个目录保持相对路径不变build/ ├── images/ │ ├── 1.png │ ├── 2.png │ └── ... └── metadata/ ├── 1.json ├── 2.json └── ...每张PNG生成后我会顺手算一个SHA256记录到manifest.json内容大致是{ 1: { image_sha256: a3f1..., traits: background:gold|body:alien|..., seed: 20240101 } }这个manifest有两个用途。第一上传前可以校验文件有没有损坏第二如果有人改了图片文件hash立刻能看出来。很多翻车现场都是“图片被二次压缩软件处理过和metadata里名字对不上”manifest就是后悔药。在上传之前JSON里的image字段其实还没有真正的CID我通常先写一个占位符比如ipfs://__CID__/images/1.png等目录上传拿到根CID后再统一替换成真实值。import os def replace_cid(meta_dir: str, cid: str) - None: for fname in os.listdir(meta_dir): if not fname.endswith(.json): continue path os.path.join(meta_dir, fname) with open(path, encodingutf-8) as f: text f.read() text text.replace(__CID__, cid) with open(path, w, encodingutf-8) as f: f.write(text)替换逻辑很简单但它能把CID写错的概率降到零。这里要注意的是replace必须作用于整个JSON文本而不是先解析JSON再改字段因为解析再序列化会打乱原本的键值顺序导致一份JSON变成另一种格式某些严格的市场会拒绝读取。4.3 元数据JSON与Token ID的对应关系生成一万个JSON后靠肉眼检查不现实。我会跑一个校验脚本把每个JSON的文件名、name字段里的编号、attributes的数量都打印出来做一致性检查。import json import os def validate_metadata(meta_dir: str, expected_attrs: int) - None: for fname in sorted(os.listdir(meta_dir)): if not fname.endswith(.json): continue token_id int(fname.split(.)[0]) with open(os.path.join(meta_dir, fname), encodingutf-8) as f: meta json.load(f) if meta[name].split(#)[-1] ! str(token_id): print(f[FAIL] {fname}: name 里 tokenId 不一致) if len(meta[attributes]) ! expected_attrs: print(f[FAIL] {fname}: attributes 数量不对) if not meta[image].startswith(ipfs://): print(f[WARN] {fname}: image 不是 ipfs:// 开头)这个脚本的作用不是修复而是把问题暴露在铸造之前。发现attributes数量不对说明生成时某些图层缺失或文件名没对齐发现name不一致说明并发写文件时tokenId串了。这些问题如果等到链上才暴露修复成本会高出很多。Windows用户还要注意一个隐藏坑os.path.join在Windows下会生成反斜杠路径如果把该路径写进JSON市场端在Linux服务器上可能无法解析。我的习惯是所有写入JSON的路径都强制用正斜杠最简单的方法就是像上面这样写死f./metadata/{token_id}.json而不是让os.path.join去拼。5. 随机生成器源码里的5个常见坑现象、原因、解决这部分写的是我在实跑中遇到的坑每一条都对应一个能复现的现象。如果你正在改一份生成器源码可以按“现象 - 原因 - 解决”的顺序对号入座。不少从“免费python源码”里捞项目的人跑通生成后都觉得没事了其实麻烦全在后面这几个地方。5.1 生成数量不等于组合数随机碰撞与去重现象计划生成10000个跑完后统计唯一指纹只有9300多个剩下的全是重复组合。我一开始还以为是生成循环bug后来发现是概率问题。原因这是随机抽样理论问题。当组合数只有11000左右时独立抽样10000次重复是必然的。很多人只把目标数量设为“生成多少次”没有把“唯一指纹数”作为停止条件。解决把停止条件改成“唯一指纹数达到目标值”。一种办法是在循环里维护指纹集合重复就重抽组合数不大时更推荐先枚举所有合法组合再用洗牌的方式按顺序抽。后者能精确控制每个组合最多出现一次。5.2 属性权重与市场预期背离现象某个属性在权重表里只给了1%按单层概率看确实是“稀有”但生成的5000张里它出现了100多次市场售价并不高。原因因为概率是乘法叠加。单层1%的属性只要其他几个图层的高频属性同时出现组合后的感知稀有度就不高而市场买家的“稀有”判断往往看整体视觉不是看某一个trait。另外如果目标生成5000次单层1%也会出现约50次这并不算罕见。解决生成前把所有属性组合概率算出来按整件作品概率排序。权重表的调整也应按组合数来验证不能只改一层。我习惯维护一张“理论概率-实际数量”对照表每次调权重都重新生成对比偏差超过5%就要去查约束条件是不是把某些组合过滤掉了。5.3 背景层遮挡导致视觉废稿现象生成的图里有一批主体角色被背景的“手”或“云”挡住一部分收藏者看缩略图根本分不清是哪张。这种图即使在稀有度排序里排第一也没人愿意接盘。原因图层顺序虽然写着background在最前但素材本身的透明区域控制不严背景图层里有些变体占满了全画布把后面的图层物理遮住。我还见过更隐蔽的情况背景图用的是RGB模式没有透明通道合成时直接把整个底板涂成了不透明色后续图层全部白叠。解决统一所有素材的画布尺寸背景类素材必须带透明通道。生成后加一个废稿检测统计最终图里非透明像素的面积占比同时抽几张图把图层拆开看确认背景变体没有大面积非透明遮挡。这个检查最好写进生成循环而不是等全部生成完再人工翻。5.4 预算超支生成10万张图的资源消耗现象想生成5万张跑到两万张时磁盘满了内存也飙到几个GB程序被迫中断。日志里没有任何异常就是机器先受不了。原因大家容易只算“代码耗时”忘了图片文件的体积。一张1024x1024的PNG压缩后可能1MB5万张就是50GBPillow合成过程还把每层PNG读进内存开太多进程会更早耗尽内存。我列一个参考量画布尺寸单张PNG参考大小5万张预计占用512x512200KB-400KB10GB-20GB1024x1024600KB-1.5MB30GB-75GB解决先压缩画布尺寸能用512就别用1024除非你的目标市场强制要求高清。写盘采用增量式每生成一张就把图片对象销毁不要用list保存所有图。并发数按内存反推不要盲目开满CPU核数。每生成一批就把manifest落盘这样中途断了也能从断点续跑不用从头再来。这里有一条血泪经验磁盘空间预算往往比代码耗时更早成为瓶颈。5.5 代码乱改导致“同一特征码不同图”现象修了一个背景透明的问题后重新跑同一种子结果同一tokenId的图和之前不一样线上展示和元数据无法对应。原因生成逻辑变了但原来的种子和变体文件顺序没有固定更隐蔽的原因是某个图层素材文件被覆盖文件名没变内容变了。这两种情况都不会报错但会让“可复现性”彻底失效。解决把随机源换成独立Random实例并用种子固定遍历目录时用sorted固定顺序给每个素材文件建hash清单运行前比对。这样任何改动都能在生成前被拦下来。我再加一个习惯每次改源码都改一下SEED或者加版本号后缀新版本走新的生成目录不要覆盖旧产物。这样即使出了问题上一轮结果还能留作对比。6. 进阶把生成器做成可复现的流水线前面几章已经把生成、稀有度、元数据讲完了但源码真正能长期用还要解决“可复现”。这一章我给两个我一直在用的方法。6.1 种子随机数与版本锁定同一个tokenId无论什么时候重新生成图片和JSON都应当完全一致。做法是为每个tokenId派生一个随机种子import random SEED 20240101 # LAYERS 和 layer_weights 由前面的配置文件加载layer_weights 结构 # {eyes: {laser: 10, normal: 90}} def generate_by_token(token_id: int) - dict: rng random.Random(f{SEED}-{token_id}) traits {} for layer in LAYERS: variants sorted(os.listdir(os.path.join(BASE_DIR, layer))) weights [layer_weights[layer].get(v, 1) for v in variants] traits[layer] rng.choices(variants, weightsweights, k1)[0] return traits字符串种子的好处是它不依赖Python版本的哈希算法换机器、换操作系统也能得到同样的序列。sorted保证目录顺序稳定这一点前面说过是“可复现”里最容易被忽略的细节。如果在代码里看到有人用全局random.seed然后直接random.choice我建议尽快改成独立Random实例否则同一个tokenId在不同线程里可能受到全局随机状态干扰。版本锁定的思路是把SEED和图层素材hash一起写进manifest。每次生成前跑一个preflight脚本比对素材hash清单只要有任何素材被改动就拒绝生成或强制换新版本号。这样“同一特征码不同图”的问题就变成了“版本号不同图不同”至少线上对得上版本不用靠猜。6.2 验证你的生成结果特征统计与抽样生成结束后我会写一个统计脚本把每个属性的实际出现次数打出来和理论概率对比。偏差在±1%左右正常但如果某个属性差了一倍多半是权重或约束逻辑写错。再抽几十张图拼成一个缩略图网格人眼扫一遍废稿。这两步加起来不超过半小时但能把后面几天的返工省掉。我现在每做一个生成项目都会先把“预检-生成-校验”三条命令固化下来甚至不介意多写一个Makefile。数据流水线做得越机械人出错的空间就越小。很多收藏品项目最后崩在数据不一致上而这个风险是生成器源码层面完全能控制的。希望这些经验能帮到你至少不要等到铸造前一天才发现图是错的。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑