十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于生成对抗网络的人像卡通化:原理、模型与Python实战

基于生成对抗网络的人像卡通化:原理、模型与Python实战 简介这是一份基于Python与PyTorch实现的人像卡通化项目资源面向对生成对抗网络、非真实感图像渲染感兴趣的算法开发者与学习者。项目采用非成对图像转换unpaired image translation思路在无需成对样本的情况下学习从真实照片到卡通画的映射同时保持原图ID信息和纹理细节适合作为图像风格迁移、人像生成等方向的学习与二次开发样例。压缩包共233个文件包含16个Python源码脚本、206张图片样本、PyTorch权重文件与pb分割模型以及卡通画数据集和onnx推理模型整体大小约217.78MB兼顾训练、推理与部署需求。资源内附人脸识别预训练模型、头像分割模型和可复用的预训练权重可直接测试效果或在此基础上微调节省从头训练的时间与数据成本。目前已有804人学习下载。1. 人像卡通化要解决的不只是加一层滤镜把一张真实自拍送进“卡通相机”几秒钟后人还在、表情还在但色调、笔触、高光全都换成了插画语言——这就是非真实感图像生成的典型场景。人像卡通化的难点不在“把照片改成假人”而在于五官的辨识度要保得住眼睛不能变形到另一个角度嘴型要对应原图的情绪肤色改变可以大胆但用户得认得出自己。围绕这个目标一个可靠的基于 Python 的实现要把三块材料备齐能训练也能拿来推理的源码、覆盖不同光照和姿态的人脸数据集、以及收敛后还保持人脸特征的生成模型。下文按模型选型、数据预处理、源码解读、训练与验证这条线把真实照片到卡通图像的落地路径一步步拆开。2. 人像卡通化的技术路线与模型选型依据2.1 CartoonGAN 和 AnimeGAN 凭什么能处理非成对数据常见做法是用对抗生成网络来做这个任务。CartoonGAN 先立起了整体框架一个带残差块的生成器一个能判断“真实照片还是卡通图”的判别器再叠加预训练 VGG 网络提取的感知损失让生成结果的内容结构偏向原始照片。这一套结构只要求生成图像的分布靠近卡通图不要求照片和卡通图一一配对所以数据准备只需要两个目录一个放真实人像照片一个放风格目标图。非成对这个特性非常重要——如果要求人工给几万张人脸配对应的卡通图项目基本开不了工。CartoonGAN 的短处也出现在细节上人脸区域的眉毛、睫毛、眼角很容易糊成一片因为判别器在整张图上下结论并没有对五官区域做重点约束。AnimeGAN 在损失函数里加了灰度图结构约束相当于强制生成结果的边缘位置对齐输入照片的灰度边缘同时把生成器的卷积结构改成更轻量的设计减少高频细节丢失。改完以后边缘保真度提升明显但人脸偶尔会出现“平均脸”问题——风格统一了脸型轮廓也趋向雷同个性和辨识度反而下降。U-GAT-IT 是另一条常见的非监督路线用注意力机制自动定位需要重点迁移的区域人像和背景都能兼顾。但它的注意力权重不稳定训练时容易在背景区域投入太多容量人脸细节反而被压平。我一般遇到人脸为主的产品需求不会只依赖 AnimeGANv2 单个模型而是把白盒方案当作更可控的备选。2.2 White-box Cartoonization 的三分量设计更适合做人像白盒卡通化的核心思路是先把卡通画拆解成三种可解释的中间表示表面表示对应色块和整体色调结构表示对应轮廓和边缘线条纹理表示对应笔触和细节层次。生成器分三路抽取这些表示再融合成一张卡通图。拆分带来的直接好处是可控性调整某一维度不会影响另外两个维度。比如希望人脸皮肤区域保留更多原始纹理只调整纹理分支的权重即可不必像端到端 GAN 那样从头重来。工程上常见的做法是两阶段训练。第一阶段不接判别器只用表面、结构、纹理三个分支的重建损失让生成器先收敛第二阶段固定已经学好的表面与结构表示再引入判别器做对抗优化。两阶段训练下的结果通常五官位置更稳定因为第一阶段的重建任务让网络理解了人脸几何结构第二阶段只负责把风格做真。所以做选型判断如果目标是二次元动漫风格用 AnimeGANv2 就够如果目标是“保留用户长相的仿真插画”白盒方案在可控性上明显占优。下文的数据处理和训练参数按白盒方案的主干来写用 AnimeGANv2 的读者可以对照替换损失项。2.3 三条技术路线的选型对照方案数据要求人脸细节保留实现复杂度训练耗时参考CartoonGAN非成对两个目录线条偏糊低单卡 12 天AnimeGANv2非成对两个目录边缘提升脸型容易趋同低单卡 23 天白盒卡通化非成对目标风格图五官稳定可分支调参中单卡 35 天耗时只是参考量级实际跟图片分辨率、ResBlock 数量、是否做多尺度输入直接相关。做选型时我建议先拿 500 张测试照片跑一遍推理只看一个标准放大到 1080p 后眼睛睫毛和牙齿边缘是不是还干净。这个特征保不住损失权重后面调再久也很难补回来。3. Python 环境与人像数据集处理的完整流程3.1 Python 版本与依赖选型从工程角度我固定用 Python 3.10 加 PyTorch 2.x 的组合。原因是 dlib 在 Python 3.11、3.12 上经常要现场编译遇到没有编译器的机器很折腾MediaPipe 不需要编译但 API 版本变动频繁。为了避免人像卡通化源码在不同机器上跑不一致创建独立虚拟环境是第一步python -m venv cartoon_env source cartoon_env/bin/activate # Windows 下换成 cartoon_env\Scripts\activate python -m pip install --upgrade pip setuptools wheel pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python dlib numpy matplotlib scikit-image pip install lpips tb-nightlycu118 版 PyTorch 是为了让 CUDA 11.8 驱动的程序在 30 系及更新显卡上都能跑纯 CPU 环境调试时去掉--index-url即可。dlib 负责 68 点人脸关键点lpips 用于训练完成后的效果验证tb-nightly 用来记录 loss 曲线。装完先跑一句python -c import torch, cv2, dlib确认三个关键库没有 ImportError。依赖这里有个常见的坑opencv-python和opencv-contrib-python不能共存pip 会把另一个覆盖掉。如果后续要用 SIFT 等功能统一装opencv-contrib-python并在 requirements.txt 里锁死版本。3.2 人脸区域检测、裁剪和对齐脚本人像卡通化的训练样本不像分类任务那样随便缩放就能用。同一个人的脸在画面里的角度和尺寸不统一会让生成器以为卡通化要连脸部朝向一起改掉训练出的模型在侧脸上很容易翻车。我通常先把所有人脸对齐到统一模板再裁剪import cv2 import numpy as np import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def align_face(image, size256): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) rects detector(gray, 1) if len(rects) 0: return None landmarks predictor(gray, rects[0]) # 左右眼各自取平均得到两个眼睛中心 left_eye np.mean([(landmarks.part(i).x, landmarks.part(i).y) for i in range(36, 42)], axis0) right_eye np.mean([(landmarks.part(i).x, landmarks.part(i).y) for i in range(42, 48)], axis0) # 计算两眼连线与水平方向的夹角把脸转正 angle np.degrees(np.arctan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0])) center tuple(((left_eye right_eye) / 2).astype(int)) rot_mat cv2.getRotationMatrix2D(center, angle, scale1.0) rotated cv2.warpAffine(image, rot_mat, (image.shape[1], image.shape[0])) # 以瞳孔连线中点为锚点向上留 45% 空间给额头和头发 top int(center[1] - size * 0.45) left int(center[0] - size // 2) crop rotated[top:top size, left:left size] if crop.shape[0] size or crop.shape[1] size: return None return cv2.resize(crop, (size, size))这段代码最关键的是top和left两个裁剪坐标。两个人脸如果朝向不同直接裁剪会让眼睛在画面里的位置差出几十像素训练时生成器必须额外学“把脸转到统一位置”白白浪费模型容量。对齐后再裁剪输入分布就规范了。闭眼、超大侧脸、遮挡这些 dlib 检测不到的情况我直接丢掉不缩放硬塞进训练集。卡通化任务是风格转换不是识别任务少几十张难样本不影响整体效果反而避免模型在困难样本上产生伪影。3.3 非成对样本的组织与数据增强数据目录按下面方式组织dataloader 只读两个列表训练时不需要临时判断文件类别data/ train/ photo/ # 真实人像统一 256x256jpg 质量 90 style/ # 目标卡通风格图同尺寸 val/ photo/ style/两个目录的数量不必相等。风格图可以比照片少很多只要色调和笔触分布覆盖足够多样生成器采样就不会缺样本。这里我更建议把同一个人的多张照片分到不同 batch而不是在一个 batch 里塞 8 个不同人否则生成器容易为了“平均所有人”而丢掉个人特征。数据增强按这个配置import albumentations as A train_aug A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.8), A.Affine(scale(0.9, 1.1), translate_percent0.05, rotate(-8, 8), p0.6), A.ColorJitter(brightness0.1, contrast0.1, saturation0.1, hue0.02, p0.5), ])垂直翻转基本不开人脸翻转后五官布局不自然模型会学到错误的下巴和发际线对称关系。旋转幅度控制在 ±8 度以内超过这个范围关键点对齐的坐标误差会向外发散。饱和度抖动幅度也别开太大卡通风格本身的颜色分布窄抖动过头会让生成器把肤色改得发绿。数据组织完之后用一个普通 dataloader 把 photo 和 style 两个列表按 epoch 打乱每个 batch 返回 photo_batch 和 style_batch 两个张量。白盒方案训练时需要的配对重建图是在训练流程里动态生成的并不需要预先在磁盘上存一份。4. 训练主流程源码与关键参数设置4.1 生成器的 ResBlock 与残差结构代码生成器主干采用带残差连接的编码器-解码器前几层把输入降到 64×64 左右的特征图中间用多组 ResBlock 做特征重建后面通过转置卷积恢复到 256×256。核心的 ResBlock 很紧凑import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels): super().__init__() self.conv1 nn.Conv2d(in_channels, in_channels, kernel_size3, padding1) self.in1 nn.InstanceNorm2d(in_channels) self.conv2 nn.Conv2d(in_channels, in_channels, kernel_size3, padding1) self.in2 nn.InstanceNorm2d(in_channels) def forward(self, x): out self.in1(self.conv1(x)) out nn.LeakyReLU(0.2)(out) out self.in2(self.conv2(out)) return x out残差结构让生成器训练早期只学“增量”不用从零重建整张卡通人脸收敛速度快很多。InstanceNorm 在这里比 BatchNorm 更合适单张人像的统计量就是当前图的统计量不同人的肤色差异不该被同一 batch 里的其他人平均掉。完整生成器的通道配置通常如下模块输入通道 → 输出通道空间下采样倍数作用ConvBlock ×23 → 642初步压缩空间尺寸ResBlock ×464 → 641大感受野特征提取ResBlock ×464 → 1282更深层结构特征ResBlock ×4128 → 1281细节重建UpSample ×2128 → 30.25恢复原分辨率见过有人把 ResBlock 加到 12 甚至 16 个想提升油画纹理丰富度确实有一点效果但训练速度明显变慢人脸五官反而容易过平滑。对 256×256 的输入我一般停在 444 或 66再多 FID 也提不上去只增加显存压力。4.2 损失函数组合与权重设置人像卡通化的训练不能靠单一大而全的公式要拆成多项叠加。损失函数代码片段def total_g_loss(fake, photo, gray_struct, d_out, vgg_feat): # 对抗损失让输出分布靠近目标卡通风格 adv_loss torch.mean((d_out - 1) ** 2) # 灰度结构损失强制边缘位置与输入照片一致 struct_loss torch.mean(torch.abs(rgb_to_gray(fake) - gray_struct)) # 感知损失取 VGG 的 relu3_3 层特征做内容约束 perc_loss torch.mean(torch.abs(vgg_feat(fake) - vgg_feat(photo))) # 颜色重建损失限制整体色调不漂移 color_loss torch.mean(torch.abs(fake.mean(dim(2, 3)) - photo.mean(dim(2, 3)))) return adv_loss 5.0 * struct_loss 0.5 * perc_loss 0.1 * color_loss运行时参考权重损失项默认权重训练早期的作用对抗损失1.0让整体分布靠近卡通灰度结构损失5.0先把五官轮廓位置钉住感知损失0.5保内容可辨识避免乱画颜色重建损失0.1防止输入输出色调偏差过大调节顺序比数值更重要。我一般前 5000 步把结构损失权重放在 5.0等脸型轮廓稳定后逐步降到 2.0对抗损失保持 1.0不要在早期给生成器太大压力否则它会走捷径输出颜色正确但轮廓模糊的图。4.3 训练循环与 checkpoint 设置训练循环先更新判别器再更新生成器gen_optim torch.optim.Adam(gen.parameters(), lr2e-4, betas(0.5, 0.999)) dsc_optim torch.optim.Adam(dsc.parameters(), lr8e-5, betas(0.5, 0.999)) for step, (photo, style) in enumerate(train_loader): photo, style photo.to(device), style.to(device) fake gen(photo) d_real dsc(style) d_fake dsc(fake.detach()) d_loss torch.mean((d_real - 1) ** 2) torch.mean(d_fake ** 2) dsc_optim.zero_grad() d_loss.backward() dsc_optim.step() d_fake_for_g dsc(fake) g_loss total_g_loss(fake, photo, gray_struct(photo), d_fake_for_g, vgg) gen_optim.zero_grad() g_loss.backward() gen_optim.step() if step % 500 0: torch.save({gen: gen.state_dict(), dsc: dsc.state_dict()}, fcheckpoint/step_{step:06d}.pth)生成器和判别器的学习率不对等是有意为之。判别器收敛太快生成器还没学会细节就被否定容易出现模式坍缩输出总是同一张脸。判别器学习率低一些相当于戴上一副“慢速眼镜”让生成器有时间逼近真实分布。checkpoint 我只保存 gen 和 dsc 的 state_dict不保存完整优化器状态因为增删训练数据后旧优化器的动量信息意义不大。每 500 步存一次是折中值再频繁写硬盘会成为训练瓶颈。如果中途 OOM把 batch_size 从 8 降到 4同时把生成器 ResBlock 数量减半是最快的解法。5. 模型推理、效果验证与常见失败模式5.1 最短推理脚本与命令行参数训练完成后推理端就是一个加载 checkpoint、喂图、保存输出的流程。要让不同分辨率的人像都能跑需要把 resize、推理、后处理放进一个脚本用命令行参数控制关键选项python inference.py \ --checkpoint checkpoint/step_120000.pth \ --input_dir data/test/photo \ --output_dir output/cartoon \ --size 256 \ --device cuda:0默认参数说明参数默认值说明--checkpoint必填训练生成的生成器权重--size256推理输入尺寸需与训练一致--face_onlyfalse是否先裁剪人脸区域再生成--post_processtrue是否做非锐化掩膜后处理face_only参数值得单独说明。整图推理对多数测试照片没问题但遇到半身照、多人合影人脸区域只占图像一小块生成器会把背景风格化过度人脸反而不清楚。打开face_only后代码先用人脸检测框裁出区域单独过生成器再贴回原图位置。裁切边缘有一点拼接痕迹配合后处理的羽化融合可以缓解。推理大尺寸图是另一个常见场景。输入超过 1024×1024 时可以先切块推理再拼接否则显存直接占满。切块时相邻块重叠 32 像素拼回时按中心距离加权融合能避免块与块之间的接缝。5.2 从训练指标到主观评价训练时的 loss 曲线与上线后的效果好坏并不完全是一回事。loss 降得漂亮但人脸糊掉的情况很常见。我自己的验证次序是先跑 FID再跑 LPIPS最后拿固定 50 张测试图做三档人工对比。import lpips loss_fn lpips.LPIPS(netalex) with torch.no_grad(): score loss_fn(fake_tensor, real_tensor) # 越小表示感知结构越接近验证项参考范围说明FID6090分布距离越低越好但受测试集组成影响大LPIPS0.150.3感知相似度越低越接近原图人工评分保留度 / 风格 / 综合上两个指标替代不了最终目测FID 对人像数据集很敏感。测试集里如果背景差异大于风格差异FID 会给虚高分数所以测试集的背景构成要和训练集接近不能为了指标好看只留纯色背景照片。5.3 常见失败模式与处理对策训练到接近收敛时最容易遇到的问题按现象分三类。五官糊掉看不出脸部边界。先看训练日志中结构损失是否一直降不下去。一般是灰度结构图的边缘约束被对抗损失冲淡了对策是把结构损失权重从 5.0 提回 8.0同时把感知损失的 relu3_3 换成 relu4_2感知约束更靠近整体语义。输出整体偏色肤色发青发灰。如果只在推理阶段出现就在输出端做一次均值和方差修正把输出通道均值和输入图对齐不需要重新训练十分钟能解决。import numpy as np def color_match(fake, photo): fake fake.astype(np.float32) photo photo.astype(np.float32) for c in range(3): delta photo[:, :, c].mean() - fake[:, :, c].mean() fake[:, :, c] delta return np.clip(fake, 0, 255).astype(np.uint8)多人合影只处理了其中一个人。这个问题在face_only模式最常见。先看人脸检测置信度阈值从 0.5 降到 0.4同时把检测框外扩 20%避免额头和下巴被裁掉。生成结果四周有完整头部贴回去就不会出现半脸。提示后处理阶段的阈值调整要记录在配置中心不要直接改代码里的魔法值。否则后面换参数训练时很难回查是模型变了还是后处理变了。6. 用关键点热图提升人像五官稳定性的技巧6.1 从推理侧给五官“钉”住卡通化模型的通病是眼睛和嘴唇区域被过度平滑眼睫毛和唇线消失。这个问题可以在推理侧挽回不必重训模型。思路是用 dlib 检测出的 68 个面部关键点把眼睛、眉毛、嘴唇勾勒成一张高斯模糊的热图再把它作为权重图在原图与生成图之间做加权融合。def build_landmark_mask(img, landmarks, sigma4): eye list(range(36, 48)) # 双眼轮廓 brow list(range(17, 27)) # 眉毛 lip list(range(48, 60)) # 嘴唇 mask np.zeros((img.shape[0], img.shape[1]), dtypenp.float32) for indices in (eye, brow, lip): pts np.array([(landmarks.part(i).x, landmarks.part(i).y) for i in indices], dtypenp.int32) cv2.polylines(mask, [pts], isClosedFalse, color1.0, thickness2) mask cv2.GaussianBlur(mask, (0, 0), sigma) return mask拿到 mask 后在模型输出和原图之间做加权合成mask 值大的位置保留原始照片边缘mask 值小的位置保持完整卡通化效果mask_t torch.from_numpy(mask).float().unsqueeze(0).unsqueeze(0).to(device) result fake * (1 - mask_t) photo * mask_tsigma 直接影响五官区域的融合范围。sigma 取 2 时睫毛边缘锐利但容易出锯齿取 4 时过渡自然取 8 时融合平滑但边缘保留明显减弱。头发区域不在关键点覆盖范围内所以发丝仍走完整卡通化不会被“钉”得太实。6.2 用边缘保持度验证这个技巧要量化这个技巧带来的收益可以对比开/关 mask 时的边缘密度。对同一张测试图分别计算眼睛区域 Sobel 梯度的均值mask 开启后的值提升 15%30% 属于正常范围。这项验证不用标注数据跑一遍测试集就能得到稳定的对比结论。如果是合成视频流前后帧五官遮罩会因为检测坐标的微小抖动出现轻微闪烁。把 sigma 提高到 6并用前后帧 mask 做时间上的指数滑动平均可以压掉大部分闪烁。如果想进一步消融融合痕迹把高斯模糊换成双边滤波睫毛、眉毛这些细边缘的交接处会保留得更自然。本文还有配套的精品资源点击获取
返回列表