
简介这是一套基于Python实现人像卡通化的完整工程适合计算机及相关专业学生用于期末大作业、课程设计或毕业设计也适合需要项目实战练习的开发者。项目经过导师指导并顺利验收源码均可本地编译运行难度适中能帮助读者快速理解图像生成类项目的完整流程。资源共24个文件以15个Python脚本为主体覆盖人脸检测、人脸分割、图像预处理、模型训练与测试等环节同时附带数据集压缩包、使用说明、README文档及多张效果示意图片整体压缩包约218MB目录结构清晰便于按需查阅。目前已有44人学习下载。读者可获得可直接运行的源码、配套数据集与预训练模型以及从人脸关键点提取到卡通化输出的实现思路其中内置的UGATIT生成网络和MobileFaceNet特征提取代码对理解对抗生成网络在图像风格迁移中的应用很有参考价值适合对照练习与二次开发。1. 人像卡通化这份资源包到底值不值得你花时间人像卡通化就是把一张普通照片变成动漫风格的角色头像听起来像是个滤镜功能但真正动手做过的人都知道这背后是一个图像到图像的风格迁移问题不是几行滤镜代码能糊弄过去的。这份基于 Python 实现人像卡通化的源码包打包了完整训练代码、数据集和预训练模型适合拿来当 Python 期末大作业、课程设计甚至毕业设计的主体项目。它能解决你三个核心诉求一是有一个能跑通、能演示的完整链路从数据到训练再到推理二是代码结构清晰答辩时讲得出原理三是有预训练权重不想从头训练也能直接出效果。如果你是第一次接触 GAN 相关项目或者想在简历上放一个“做过风格迁移”的实操经历这份资源是一个不错的起点。2. 为什么是 AnimeGAN v2选型前先搞清楚技术路线2.1 传统卡通化方案的瓶颈在哪里在人像卡通化这个方向上最早期的方案走的是传统图像处理路线边缘检测提取轮廓、颜色量化压缩色板、双边滤波或均值漂移做平滑。这套方案的问题很明显——处理结果像“油画滤镜”边缘生硬、颜色块状感强而且对光照和背景复杂的人像照片几乎无解。你调参调到头也很难让脸部五官保持自然。后来有人尝试用分割加重绘的思路先做人像分割再对前景和背景分别做风格化。这个方案的瓶颈在于分割精度头发丝、眼镜框、复杂背景一旦分割出错卡通化结果就出现大量伪影。你可以理解为传统方案是在“修图”而不是“重新绘制”所以上限很低。2.2 生成对抗网络为何成了主流答案真正把人像卡通化做出“重新绘制”感觉的是生成对抗网络GAN。核心思路是用一个生成器把真实照片映射到动漫风格空间用一个判别器判断输出是否“像动漫”。两者对抗训练生成器不断逼近“既保留原图结构、又有动漫风格”的结果。在这个方向上有几个代表性工作。CartoonGAN 是第一波把 GAN 用于卡通化的方案但它对纹理复杂的照片处理不够稳定CycleGAN 能做无配对训练但训练不稳定、容易产生伪影AnimeGAN 系列则在前人基础上做了大量工程化改进。AnimeGAN v2 是目前平衡效果和工程复杂度最好的选择推理速度快单张 256 大小的图片在普通 GPU 上几十毫秒出结果预处理逻辑简单不需要额外的人脸关键点对齐生成的卡通风格接近吉卜力工作室的画风视觉上“像手绘”而不是“像滤镜”。2.3 AnimeGAN v2 的模型结构拆解AnimeGAN v2 的生成器是一个带下采样和上采样对称结构的卷积网络。输入一张 256×256×3 的 RGB 图片经过三个下采样卷积块把特征图降到 32×32然后经过残差块组提取风格特征再通过上采样转置卷积恢复到原分辨率。这里有三个工程细节值得注意。第一网络去掉了 BatchNorm改用 InstanceNorm原因是在风格迁移任务里BatchNorm 依赖 batch 统计量单卡训练 batch 很小的时候统计不稳定InstanceNorm 则对单张图做归一化更符合“逐图风格化”的需求。第二判别器是一个 70×70 PatchGAN 结构它不输出单一真伪概率而是对图像切成多个 patch 分别判断这样能更好地约束局部纹理。第三损失函数由对抗损失、感知损失和灰度引导损失三部分组成。灰度引导损失是 AnimeGAN 系列特有的设计把生成结果转成灰度图和输入照片的灰度图计算 L1 距离目的是保证结构一致性防止生成器为了“更像动漫”而把脸型、姿态改得面目全非。提示你在答辩或写课程设计报告时能讲清楚灰度引导损失的作用比背出一堆网络结构参数更有说服力因为这说明你理解了这个损失项存在的意义。3. 资源包里的文件和数据集动手前先看清家底3.1 源码包的目录结构与职责划分拿到这份资源后第一件事不是急着跑训练而是把目录结构过一遍。常见做法是分为 checkpoints、dataset、src、tools 四个顶层目录这个包也是类似的组织方式. ├── checkpoints/ # 预训练权重和训练日志 │ ├── generator/ # 生成器权重 │ └── discriminator/ # 判别器权重 ├── dataset/ # 训练和测试用图片 │ ├── style/ # 动漫风格参考图 │ ├── photo/ # 真实照片训练集 │ ├── test/ # 测试图片 │ └── train.txt # 训练集路径清单 ├── src/ # 核心源码 │ ├── model.py # 生成器、判别器网络定义 │ ├── train.py # 训练入口 │ ├── infer.py # 推理入口 │ └── utils.py # 数据加载、预处理工具 └── tools/ # 辅助脚本 ├── resize_images.py # 批量缩放图片 └── make_dataset.py # 生成训练集清单train.txt 这个路径清单文件很重要后面训练时会反复用到它是纯文本格式每一行是一个图片文件的绝对或相对路径。作者用这种方式而不是直接扫描目录目的是让你能手动控制哪些图片进训练集、哪些进验证集。3.2 数据集怎么组织才合理卡通化训练需要两类数据真实照片和对应风格的动漫参考图。这个包里的 style 目录放的是从吉卜力作品和高质量插画中截取的区域photo 目录是自然场景和人物照片。注意两者并不需要严格的一一配对AnimeGAN v2 的训练设计允许无配对训练风格参考图的数量不需要和照片一一对应。如果你要自己扩展数据集最稳妥的采集策略是风格图 200 到 500 张覆盖不同的色调和光照真实照片 2000 张左右尽量多样化包含不同肤色、不同背景复杂度、不同光照条件。少于这个量级训练出的效果容易出现色调偏移——比如整体偏紫或偏黄。3.3 训练前的图片预处理清单拿到这个包我建议你先把 test 目录里的图跑一遍推理确认环境没问题再决定是否从头训练。但如果要训练必须做以下三个预处理步骤作者在包里已经放好了脚本但你需要知道每个脚本在干什么# 第一步统一图片尺寸到 256x256 python tools/resize_images.py --input dataset/photo --output dataset/photo_resized --size 256 # 第二步生成训练集路径清单 python tools/make_dataset.py --photo dataset/photo_resized --style dataset/style --output dataset/train.txt # 第三步检查清单文件内容是否符合预期 head -n 10 dataset/train.txt第一行命令把原始照片批量缩放为 256×256AnimeGAN v2 的输入层固定接收这个尺寸不缩放的后果是训练时数据加载器直接报 shape mismatch 错误。第二行命令扫描目录下所有图片生成带路径的清单文件。第三行是为了确认文件名编码没有出现中文乱码或路径分隔符问题在 Windows 上尤其容易踩坑。注意不要用拉伸的方式缩放图片会改变人脸比例。resize_images.py 默认用的是 PIL 的 LANCZOS 插值这个选择是正确的。如果你替换成最近邻插值生成结果的边缘会出现明显的锯齿。4. 把训练和推理跑通从命令行到参数调优4.1 训练入口参数说明与启动方式训练脚本是 src/train.py作者给了默认参数但你需要理解每个参数的含义才能调出好效果。下面是一个典型的训练启动命令python src/train.py \ --dataset dataset/train.txt \ --style_dir dataset/style \ --checkpoints_dir checkpoints \ --batch_size 8 \ --image_size 256 \ --epochs 20 \ --save_per_epoch 1 \ --learning_rate_g 0.0002 \ --learning_rate_d 0.0004batch_size是你的显卡显存决定的。8 在 8GB 显存上比较稳妥12GB 以上可以提到 16但大于 16 对这个任务收益很小反而容易让判别器过拟合。learning_rate_g和learning_rate_d分开设置是有讲究的判别器的学习率是生成器的两倍这是 GAN 训练中常见的经验值目的是让判别器保持足够强的判别力防止生成器过早碾压判别器导致模式崩塌。训练时你会在终端看到类似[Epoch 3/20] [Batch 40/100] D_loss: 0.312 G_loss: 1.204这样的日志输出。判断训练是否正常的经验标准是D_loss 大致围绕 0.5 到 0.7 波动G_loss 不要持续飙升到 5 以上也不要跌到 0.1 以下。持续偏高或持续偏低都说明判别器和生成器节奏失衡。4.2 推理脚本使用与效果验证训练完成后或者在不想训练的情况下直接用预训练模型推理入口是 src/infer.pyimport torch from src.model import Generator # 加载生成器网络 gen Generator() # 加载预训练权重map_location 保证 CPU 环境下也能加载 state_dict torch.load( checkpoints/generator/latest_net_G.pth, map_locationcpu ) gen.load_state_dict(state_dict) gen.eval() # 读取测试图片缩放到 256x256 from PIL import Image import torchvision.transforms as transforms img Image.open(dataset/test/portrait.jpg).convert(RGB) transform transforms.Compose([ transforms.Resize((256, 256)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ]) input_tensor transform(img).unsqueeze(0) # 前向推理得到卡通化结果 with torch.no_grad(): output_tensor gen(input_tensor) # 去归一化并保存输出 output_img output_tensor.squeeze(0).permute(1, 2, 0) * 0.5 0.5 output_pil Image.fromarray((output_img.numpy() * 255).astype(uint8)) output_pil.save(output_cartoon.png)这里有三步需要解释。map_locationcpu是防止你在没有 GPU 的机器上加载权重时报错加了这行代码后代码会自动适配。Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))把像素值从 0 到 255 映射到 -1 到 1这是生成器训练时的输入分布推理时必须一致忘记这一步的输出会整体偏暗或者偏灰。with torch.no_grad()是推理模式必须做的否则显存会被很多中间变量撑爆。跑完上面的代码你会得到一张 256×256 的卡通图。我的习惯是保存后再用 PIL 做一次resize放大到原图尺寸因为直接输出 256 大小在演示时显得不够清晰。4.3 怎么判断一张卡通化图片的质量很多人第一次跑完看到输出图觉得“有点怪”但说不清哪里怪。这里给你一个可落地的质量检查清单第一看五官比例眼睛是否明显变形如果一只眼睛比另一只大 20% 以上说明输入图片角度太偏或者遮挡严重第二看边缘线条头发边缘应该有连续深色描边如果边缘断裂或有白边说明灰度引导损失约束不够可以加大这个损失项的权重第三看背景纹理背景里的草地、砖墙应该是平滑的色块如果还有明显真实纹理残留说明风格迁移不彻底需要更多训练轮次。这个检查清单可以直接写进课程设计报告的“实验评估”部分比只贴一张对比图要专业得多。5. 避坑指南五个让新手翻车的典型问题5.1 训练时报错尺寸不匹配现象RuntimeError: size mismatch for net_G.conv1.weight。原因你自己下载了别人训练的权重但对方使用的输入图像尺寸或通道数与当前代码不一致。常见于 PyTorch 版本不同导致权重文件内部的张量形状有细微差异或者换用了非 256×256 的输入尺寸。解决训练前先打印权重文件的键值形状与模型定义的参数逐一对比ckpt torch.load(checkpoints/generator/latest_net_G.pth, map_locationcpu) for k, v in ckpt.items(): print(k, v.shape)核对第一层卷积的in_channels是否为 3最后一层的输出通道是否为 3。如果不匹配不要妄想通过改代码兼容直接去下载和源码配套的权重。5.2 显存溢出的血泪经验现象CUDA out of memory发生在训练开始后的第一个 batch。原因多张显卡混用、batch_size 过大、或者训练脚本默认开了torch.backends.cudnn.benchmark导致显存预分配过大。解决先用nvidia-smi查看当前显存占用。如果是 8GB 显存把 batch_size 降到 4 或 2同时把训练图像尺寸从 256 降到 192python src/train.py --batch_size 4 --image_size 192尺寸降为 192 不影响最终效果太多因为推理时仍然可以从 192 的输出做上采样。如果降到 192 还溢出那就是代码里有显存泄漏检查是否有loss.backward()之后忘记optimizer.zero_grad()的历史梯度累加问题。5.3 训练 loss 正常但生成图片全是一个颜色现象训练日志看起来正常loss 在合理范围波动但生成的图片整体是单一色块人脸完全消失。原因判别器训练过度生成器的梯度被压制到几乎为零这是 GAN 训练里经典的模式崩塌mode collapse。常见触发条件是学习率设置不合理生成器学习率太低而判别器学习率太高。解决恢复模型到崩溃前的 checkpoint把生成器学习率提升到判别器的 1.5 倍而不是默认的 0.5 倍。或者更新判别器的频率改为每两个 batch 只更新一次判别器# 在 train.py 的训练循环里做如下修改 for i, (photo, style) in enumerate(data_loader): # 判别器每两个 batch 更新一次 if i % 2 0: d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()这种“判别器减速”策略是 GAN 训练调参中非常实用的手段。5.4 图片风格像但人像不像现象输出图片的色调和纹理很像动漫风格但人脸五官扭曲看起来像另一个人。原因这是一个方向性的误用——训练集中照片模型见过的人脸不够多或者输入图片中的人脸占比太小。AnimeGAN v2 是基于整图风格迁移不是人脸专精模型如果输入一张半身照背景的风格化权重会抢走太多表达空间。解决预处理阶段把人脸区域裁剪出来放大到占整张图面积的 60% 以上后再输入模型。你可以用 OpenCV 的CascadeClassifier做人脸检测并自动裁剪import cv2 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) img cv2.imread(input.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5) if len(faces) 0: x, y, w, h faces[0] crop img[y:yh, x:xw] cv2.imwrite(input_cropped.jpg, crop)这段代码的意思是用 OpenCV 自带的 Haar 级联分类器检测第一张人脸取人脸边界框裁剪后保存为新的输入图。裁剪后的人像再送去卡通化五官保留度会有明显提升。5.5 Windows 环境下路径坑现象运行train.py后FileNotFoundError: [Errno 2] No such file or directory: dataset/photo/图片_01.jpg。原因中文文件名或路径中的反斜杠被错误转义。Python 在 Windows 下处理包含中文字符的路径时如果文件系统编码不一致会无法识别。解决不要在路径中使用中文文件名所有数据集文件用拼音或英文命名例如photo_001.jpg而不是图片_001.jpg。在代码开头加上import os os.chdir(os.path.dirname(os.path.abspath(__file__)))这两行强制把工作目录切换到脚本所在目录避免因为 IDE 或终端的工作目录不同导致的相对路径找不到问题。6. 把模型用到实际场景ONNX 导出与摄像头实时卡通化6.1 为什么要把 PyTorch 模型转成 ONNX课程设计答辩现场评委通常会问一句“这个模型能不能部署到实际场景”。如果你用一个 256×256 的 PyTorch 模型逐帧处理摄像头视频帧率可能只有 5 FPS现场演示非常拉胯。转成 ONNX 后用 ONNX Runtime 做 CPU 推理配合预处理优化能把帧率提升到 15 FPS 左右这个性能表现足够支撑一个“实时人像卡通化”的演示 demo。ONNX 转换还有一个工程上的好处它帮你固化模型的输入输出格式转换过程相当于一次结构校验很多 PyTorch 代码里隐藏的 shape 问题会在转换时报错暴露出来。6.2 从 PyTorch 导出到 ONNX 的具体操作import torch from src.model import Generator # 加载训练好的生成器权重 gen Generator() ckpt torch.load(checkpoints/generator/latest_net_G.pth, map_locationcpu) gen.load_state_dict(ckpt) gen.eval() # 创建一个标准尺寸的随机输入 dummy_input torch.randn(1, 3, 256, 256) # 导出 ONNX torch.onnx.export( gen, dummy_input, animegan_v2.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch}, output: {0: batch}, }, opset_version11, )这段代码做了四件事。第一步加载训练好的权重第二步生成一个形状固定的随机输入ONNX 导出需要你先给一个真实的输入张量推导计算图第三步执行导出这里有个关键参数dynamic_axes它允许输入张量在 batch 维度动态变化不设这个参数的话导出模型只能接受固定 batch 的输入第四步opset_version11是一个兼容性较好的算子版本ONNX Runtime 和 OpenCV DNN 都支持。导出完成后你会得到一个animegan_v2.onnx文件大小通常在 30MB 到 50MB 之间。用下面的代码验证是否能正确推理import onnxruntime as ort import numpy as np session ort.InferenceSession(animegan_v2.onnx) input_name session.get_inputs()[0].name output_name session.get_outputs()[0].name # 构造输入1x3x256x256 的 float32 数组值域 [-1, 1] input_data np.random.randn(1, 3, 256, 256).astype(np.float32) result session.run([output_name], {input_name: input_data}) print(输出形状, result[0].shape)onnxruntime.InferenceSession创建一个推理会话get_inputs()[0].name拿到输入节点名称session.run执行推理第一个参数是输出节点列表第二个参数是输入字典。这里只验证了形状正确性真正要用的时候还需要把预处理resize、归一化和推理脚本串联起来。6.3 用摄像头做逐帧卡通化的完整方案有了 ONNX 模型下一步是把摄像头画面实时卡通化。思路是循环读取每一帧先做人脸裁剪再缩放和归一化喂给 ONNX 模型拿到输出后渲染回屏幕import cv2 import numpy as np import onnxruntime as ort # 初始化 ONNX 推理会话 session ort.InferenceSession(animegan_v2.onnx) in_name session.get_inputs()[0].name # 打开摄像头 cap cv2.VideoCapture(0) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) while True: ret, frame cap.read() if not ret: break # 人脸检测并裁剪 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5) # 默认对整帧做处理 roi frame if len(faces) 0: x, y, w, h max(faces, keylambda f: f[2] * f[3]) roi frame[y:yh, x:xw] # 预处理缩放 256归一化到 [-1, 1] roi_resized cv2.resize(roi, (256, 256)) input_tensor roi_resized.astype(np.float32) / 127.5 - 1.0 input_tensor np.transpose(input_tensor, (2, 0, 1)) input_tensor np.expand_dims(input_tensor, axis0) # ONNX 推理 output session.run(None, {in_name: input_tensor})[0] output np.squeeze(output, axis0) # 去掉 batch 维度 output np.transpose(output, (1, 2, 0)) # CHW 转 HWC output ((output 1.0) * 127.5).astype(np.uint8) output_resized cv2.resize(output, (roi.shape[1], roi.shape[0])) # 把卡通结果贴回原帧人脸区域 frame[y:yh, x:xw] output_resized cv2.imshow(Cartoon Live, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这个脚本的技巧在最后一步——把人脸裁剪区域的结果贴回原帧而不是直接在张帧上做卡通化这样背景保持真实感人像部分变成卡通视觉效果更自然。max(faces, keylambda f: f[2] * f[3])选择面积最大的检测框避免误检到背景里的小图案。我自己的教训是第一次跑这个的时候没有做cv2.waitKey(1)的延时控制CPU 直接被占满摄像头画面卡成一帧一帧的。后来加了延时并且在输入前把长边缩到 256 而不是暴力拉伸流畅度才有了质的提升。从那以后我每次做这类实时推理项目都强制先量化计算量再动手写循环体。这个 demo 做出来你的课程设计答辩展示环节就稳了希望帮到你。本文还有配套的精品资源点击获取