第5章 语义分割(证件照制作工具) 视频讲解 5.1任务概述 5.1.1任务背景 通过前面两章实战案例内容的学习,相信读者已经掌握了图像分类和目标检测的基本概念,初步熟悉了图像分类和目标检测的算法原理,也能够运用相关算法套件完成这两类常见的图像处理任务。从局部和全局角度来分析,图像分类是一种全局的图像分析任务,旨在对整张图像进行回归; 目标检测则是一种综合全局和局部的图像分析任务,旨在从整张图像中找到某个感兴趣物体的局部区域。除了这两类任务以外,现实情况中还经常会遇到一类任务,即本章探讨的语义分割。相比而言,语义分割更侧重于局部细节,旨在分割出图像中感兴趣物体的精确边界。 语义分割是图像分割的一种特殊形式,即将图像中的每个像素划分到一组预定义的语义类别中。目前,基于深度学习的语义分割方法能够完成很多复杂的任务。举个简单例子,图5.1(a)所示是一张自然街景图片,图5.1(b)所示是对应的语义分割图,可以看到,分割的结果就是将同类的物体用一种颜色标注出来,每一类物体就是一种语义,如图5.1(b)中“人”是一类语义、“马路”是一类语义、“树”是一类语义、“电线杆”是一类语义等。语义分割需要对图像中的每个像素进行分类,相比于第3章的图像分类问题,语义分割的难度更大,因为该任务需要精确至像素级别。目前,语义分割已经被广泛应用于医疗诊断、自动驾驶、地质检测和农业自动化作业等场景中。 图5.1语义分割示例 本章将基于语义分割技术开发一款用于计算机桌面应用的证件照制作工具,可以让用户方便地制作出不同颜色背景的标准证件照片。 为什么要采用语义分割技术来实现证件照制作呢?这是因为在证件照制作任务中,最难的就是需要对用户上传的照片进行人像提取,其关键技术就是通过深度学习算法模型来精细预测人像边界,然后再将人像提取出来并与另一张纯色背景进行合成。 照片背景替换算法流程如图5.2所示。 图5.2照片背景替换算法流程 由于用户上传的人像照片往往包含不确定的复杂背景,传统图像分割方法无法精确地提取出人像边界,需要依赖深度学习技术进行学习,从而使用上下文信息和人像先验知识实现精准分割,这是一个典型的语义分割问题,这里的语义即指照片中的“人”。 本章将基于语义分割算法来实现证件照去背景/换背景功能,依托PaddleSeg套件全流程研发一款用于桌面PC的证件照制作工具。 5.1.2安装PaddleSeg套件 PaddleSeg是基于PaddlePaddle的图像语义分割算法套件,内置了众多前沿的语义分割算法和预训练模型,支持全流程的数据标注、模型训练、验证和部署等功能,可以有效助力语义分割算法在医疗、工业、遥感、娱乐等场景的应用落地。 PaddleSeg完整功能架构如图5.3所示。 图5.3PaddleSeg完整功能架构 本章将使用PaddleSeg套件来完成整个项目。首先确保已经正确配置好深度学习环境并安装好PaddlePaddle,具体方法请参考2.2节的相关内容。 接下来下载并安装PaddleSeg套件: git clone https://github.com/PaddlePaddle/PaddleSeg.git cd PaddleSeg pip install -v -e . 安装完毕后就可以正常使用了。在使用前,有必要先了解和掌握一些经典的语义分割算法原理,这将为后续的算法选择以及参数配置提供理论参考。 5.2算法原理 语义分割方法按照时间大致可以分为传统方法和深度学习方法两类。传统方法主要采用马尔可夫随机场或条件随机场等方法进行数学建模,这类方法实现相对简单、硬件依赖度低、部署集成方便,缺点是先验知识少、分割精度低。目前主流的语义分割算法都是采用深度学习实现的,深度学习方法可以充分利用大样本数据的先验知识得到更佳的分割精度。本章将介绍4种经典的图像语义分割算法: FCN、UNet、HRNet和OCRNet。 FCN是第一个被提出的基于深度学习的语义分割算法,具有绝对的开创性意义。至今,大部分语义分割算法框架仍沿用了FCN的思路。因此,掌握FCN算法原理对于学习语义分割非常重要。 另一个重要的语义分割算法就是UNet,其独特的U形结构网络使各个语义层的特征可以有效融合,这种结构设计使组网和功能扩展都非常简单,但是分割效果却非常出众。目前,UNet模型及其众多变种一直活跃在深度学习各个领域中。 本章要介绍的另外两个算法是HRNet和OCRNet算法,这两个算法都是基于HRNet密集网络架构实现的。不同于UNet从低分辨率特征上采样到高分辨率特征,HRNet采用并联结构来保持网络中的高分辨率特征,在分割准确率上要优于UNet算法。 下面逐一介绍这四种算法基本原理。 5.2.1FCN算法 语义分割需要判断图像中每个像素点的类别,而传统CNN分类网络在进行卷积和池化的过程中特征图一般会不断变小,最终导致输出特征无法表示物体的精确轮廓。针对这个问题,全卷积网络(Fully Convolutional Networks,FCN)算法被提出来,用于解决图像语义分割任务。自从FCN算法被提出以后,后续相关语义分割算法都是基于这个框架进行改进的。 那么FCN到底是如何实现语义分割的呢? 1. FCN与传统分类网络的不同 对于一般的分类CNN网络,如VGG和ResNet,都会在网络的最后加入一些全连接层,然后再进行flatten操作形成一维向量,最后经过softmax层后获得类别概率信息,如图5.4所示。很明显,这个概率信息是一维的,只能输出整个图片的类别,不能输出每个像素点的类别,所以这种全连接方法不适用于图像分割。 图5.4传统的图像分类网络结构 FCN提出可以把后面几个全连接层都换成二维卷积,中间不使用flatten算子,所有的输出特征依然保持[c,h,w]三个维度,最后再衔接softmax层,从而获得每个像素点的分类信息,这样就解决了像素分割问题,如图5.5所示。 图5.5FCN将全连接层改为卷积层 2. FCN结构设计 FCN模型结构如图5.6所示。 图5.6FCN模型结构图 可以将整个网络分成特征提取模块和特征融合模块两部分。 特征提取模块主要依赖传统图像分类模型(如VGG)提取不同层级的卷积特征,具体步骤如下:  输入图像(image)经过多个卷积层(Conv)和一个最大池化层(max pooling)变为pool1特征图,此时宽、高变为原始输入图像的1/2;  pool1特征图再经过多个卷积层(Conv)和一个最大池化层(max pooling)变为pool2特征图,宽、高变为原始输入图像的1/4;  pool2特征图再经过多个卷积层(Conv)和一个最大池化层(max pooling)变为pool3特征图,宽、高变为原始输入图像的1/8;  依次类推,对pool3、pool4特征图进行处理。直到pool5特征图,宽、高变为原始输入图像的1/32。 得到pool3(1/8)、pool4(1/16)、pool5(1/32)这三个特征图以后,接下来就是特征融合。FCN采用了上采样和特征图累加融合的方式获得最终的语义分割图。这里的上采样可以理解为一种对特征图尺寸进行放大的算子,在PaddlePaddle中对应的是interpolate算子。 那么怎么获得最终的语义分割图呢?FCN尝试了3种方案: (1) FCN32s: 直接对pool5特征进行32倍上采样,此时可以获得跟原图一样大小的特征图,再对这个特征图上每个点做softmax计算,从而获得分割图。 (2) FCN16s: 首先对pool5特征进行2倍上采样获得2×upsampled特征,再把pool4 特征和2×upsampled特征逐点相加(注意到此时两个图都是原图的1/16),然后对相加的特征进行16倍上采样,并进行softmax计算,从而获得最终的分割图。 (3) FCN8s: 按照图5.6所示结构首先进行pool4和2×upsampled特征图的逐点相加得到新的2×upsampled特征图,然后再将pool3和这个新的特征图逐点相加完成多层次特征融合,最后进行8倍上采样并进行softmax计算,从而获得最终的分割图。 在FCN的论文里给出了上述3种方法的实验结果对比,从分割精度上发现FCN32s= thr] = 1 # 掩码保存 maskpath = os.path.join(mask_folder, imgname) cv2.imwrite(maskpath, alpha) 上述脚本读取每张掩码图像,然后进行二值化,使得人像前景区域像素值为1,背景区域像素值为0,最后将新的二值掩码图像保存到P3M_10k/mask目录下面。 这里还需要额外注意,对于语义掩码图来说,不能用JPG等有损压缩的图像格式进行保存,因为这种有损压缩的保存方式会改变邻近像素的类别信息。本书推荐使用PNG格式来保存语义掩码图,这也是大部分语义分割任务采用的格式。 2. 伪彩色可视化 转换完数据集以后,查看P3M_10k/mask目录下的二值掩码图,会发现所有转换后的图像肉眼查看都是黑色的。这是因为每张二值掩码图的像素值只有0或1,这个像素值对于整个灰阶0~255来说是位于黑色视觉范围内的,肉眼只能看到一张黑色图片。因此,尽管转换是正确的,但是这种可视化效果并不直观,如果转换错误很难检查出来。为了便于可视化检查,PaddleSeg提供了伪彩色可视化脚本,可以将上述这种黑色的二值掩码图进一步进行转换成便于肉眼辨识的伪彩色掩码图,并且后续的算法训练、验证等步骤也都支持转换后的伪彩色掩码图。 具体的,可以使用下面的代码进行转换: python tools/data/gray2pseudo_color.py ./dataset/P3M_10k/mask ./dataset/P3M_10k/colormask 转换后的伪彩色掩码图保存在P3M_10k/colormask下面,对比效果如图5.12所示。可以看到,背景区域(像素值为0)以红色显示,人像区域(像素值为1)以绿色显示,这样就可以方便地进行区域辨识了。如果还有其他类别,那么其他类别的像素值也会用对应的其他颜色自动标识出来。 图5.12伪彩色掩码图 至此,数据集已全部转换完毕。所有的原始图像都存放在P3M_10k/img目录下面,图像格式为JPG; 所有的语义掩码图像都存放在P3M_10k/colormask目录下面,图像格式为PNG。 3. 数据集切分 对于整理好的数据集,需要按照比例划分为训练集、验证集和测试集。PaddleSeg提供了切分数据并生成文件列表的脚本。使用方式如下: python tools/data/split_dataset_list.py ./dataset/P3M_10k img colormask --split 0.9 0.1 0.0 --format jpg png 其中,参数split对应的3个数值分别表示训练集、验证集和测试集的占比; 参数format的2个数值分别表示原始图像和掩码图像的存储格式。 切分完成后在数据集根目录P3M_10k下会生成3个列表文件train.txt、val.txt和test.txt,分别存储训练集、验证集和测试集文件列表。列表中的每一行存放着对应的原始图像和真值掩码图像路径,中间用空格分开,如下所示: img/p_20788cd7.jpg colormask/p_20788cd7.png img/p_799e9fa3.jpg colormask/p_799e9fa3.png img/p_c50b6107.jpg colormask/p_c50b6107.png ... 到这里,符合PaddleSeg套件的数据集就准备完毕了。 5.3.2使用Labelme制作自己的语义分割数据集 5.3.1节使用了开源人像抠图数据集作为本章的实验数据,并且针对PaddleSeg套件进行了数据集格式转换。如果想自行制作人像分割数据集或者想要自行添加更多的人像数据,那么该怎么做呢? 同样的可以使用前面4.3.2节介绍的Labelme工具实现。Labelme的基本使用方法请参考4.3.2节内容,本章不再赘述。 下面以人像分割任务为例,讲解如何使用Labelme工具实现语义分割任务的标注。 (1) 基本设置: 首先打开Labelme工具,单击菜单栏File并将其展开,取消选中Save With Image Data复选框,然后选中Save Automatically复选框。这样设置可以使得标注出来的标注文件不会包含冗余的原始图像信息,并且在切换标注图像时可以自动保存标注信息。 (2) 选择标注模式: 单击菜单栏File→Open Dir,打开需要标注的目标图片文件夹。由于本章处理的是语义分割任务,因此选择多边形标注模式。具体地,单击菜单栏Edit→Create Polygons,接下来就可以进行标注了。 (3) 逐点标注: 沿着人像(前景)边界逐个单击画点,最后闭合成一个完整的轮廓(结束时要单击第1个点形成闭合曲线)。闭合后,会自动弹出前景类别定义窗口,输入前景类别名称即可,本章对应的可以输入person,最后单击OK即可完成一张图像的标注,如图5.13所示。 图5.13使用Labelme进行人像标注 (4) 目标包含背景的标注: 如果目标中包含了背景,在标注完目标后,还需要对背景进行专门的标注,以从目标中进行剔除。具体的,在标注完目标轮廓后,再沿背景区域边缘画多边形,并将其标注为_background_类别即可。 (5) 保存标注结果: 标注好以后,可以按Ctrl+S组合键保存,也可以直接按D键切换到下一张待标注图像。由于在第1步中设置了Save Automatically,因此,切换图像时标注信息会自动保存。标注信息将会以与图像同名的JSON文件保存到同名文件夹中,如图5.14所示。 图5.14标注信息保存为同名的JSON文件 (6) 删除标注结果: 如果对前面的标注结果不满意,可以删除标注。具体的,单击菜单栏Edit→Edit Polygons,此时鼠标变成手形,单击标注区域,然后右键选择Delete Polygons即可。 标注好所有图片后怎么将这些JSON文件转换为5.3.1节中的伪彩色掩码图呢? 这里可以使用PaddleSeg中的转换脚本来实现。假设所有图片和标注好的JSON文件放在一个名为photos的文件夹中,可以使用下面的命令来完成转换: python tools/data/labelme2seg.py ./photos 运行完成后在photos目录下会生成一个名为annotations的文件夹,该文件夹包含了对应的转换好的伪彩色掩码图,如图5.15所示。 图5.15转换后的伪彩色掩码图 通过与展示的原图进行比较,该伪彩色掩码图跟标注的区域一致,说明整个转换是正确的。这样就得到了与前面数据集一致的标注文件,读者可以采用上述标注方法自行添加数据。虽然本章任务是针对人像分割的,但是对于其他语义分割任务来说本章标注方法一样有效。 需要指出的是,如果读者针对的是人像抠图任务,那么就需要采用另外一种更加专业的标注技术——PS通道抠图。该技术旨在使用PhotoShop软件的通道分层原理,精细化地提取人像透明度(Alpha)通道,但是该方法对PS技术要求较高,标注难度大,感兴趣的读者可以自行查阅相关资料来学习。 5.3.3算法训练 1. 准备配置文件 同其他算法套件一样,PaddleSeg套件同样是以YML配置文件形式来串联各个模块执行的。相关算法的配置文件设置可以参考PaddleSeg/configs目录下的各个文件。本章使用前面介绍的OCRNet算法来完成算法研发。 首先在PaddleSeg当前目录下新建一个配置文件config.yml,其内容如下: batch_size: 4# 迭代一次送入网络的图片数量,实际batch size等于batch size乘以卡数 iters: 80000 # 模型训练迭代的轮数 train_dataset: type: Dataset dataset_root: ./dataset/P3M_10k# 数据集路径 train_path: ./dataset/P3M_10k/train.txt# 训练样本和真值的路径文档 num_classes: 2 # 类别数量(包含背景类) mode: train# 训练模式,可选train或者val等 transforms: # 数据增强 - type: Resize target_size: [512, 512]# 图片重置大小尺寸,分别为宽和高 - type: RandomHorizontalFlip # 水平翻转 - type: RandomDistort# 随机进行亮度、对比度、饱和度变动 brightness_range: 0.3 contrast_range: 0.3 saturation_range: 0.3 - type: Normalize# 归一化 val_dataset: type: Dataset dataset_root: ./dataset/P3M_10k val_path: ./dataset/P3M_10k/val.txt num_classes: 2 mode: val transforms: - type: Resize target_size: [512, 512] - type: Normalize optimizer: type: SGD # 优化算法 momentum: 0.9 # SGD的动量 weight_decay: 4.0e-5# 权值衰减,防止过拟合 lr_scheduler: # 学习率的相关设置 type: PolynomialDecay # 学习率变化策略 learning_rate: 0.01 # 初始学习率 end_lr: 0 # 结束时的学习率 power: 0.9 model: type: OCRNet # 网络类型为OCRNet backbone:# 骨干网络设置 type: HRNet_W18# 骨干网络类型 pretrained: https://bj.bcebos.com/paddleseg/dygraph/hrnet_w18_ssld.tar.gz num_classes: 2 # 类别数量 backbone_indices: [0] loss: types:# 损失函数类型设置 - type: CrossEntropyLoss# 损失函数类型 - type: CrossEntropyLoss coef: [1, 0.4]# 两个损失函数的系数 配置文件准备完后下面就可以开始算法训练了。 2. 训练 如果是采用单卡进行训练,命令如下: python tools/train.py --config config.yml --do_eval --use_vdl --save_interval 1000 --save_dir output 其中,config参数用来设置配置文件路径; do_eval参数表示在训练时开启可视化工具记录; save_interval参数用来设置模型保存的间隔; save_dir参数用来设置最终训练结果的保存路径。 如果是采用多卡进行训练(以2卡为例),命令如下: export CUDA_VISIBLE_DEVICES=0,1 python -m paddle.distributed.launch tools/train.py --config config.yml --do_eval --use_vdl --save_interval 1000 --save_dir output 如果出现显存不足的问题,那么可以修改配置文件config.yml中的batch_size参数,将其适当调小即可。 在整个训练过程中,可以使用下面的命令打开可视化工具visualdl来更直观地查看训练过程和模型的实时预测性能: visualdl --logdir output 运行成功后打开浏览器访问http://localhost:8040/。训练集上可视化结果如图5.16所示。 图5.16训练集上训练结果可视化 图5.16中,Train/loss为各类损失函数之和; lr为学习率; batch_cost为批训练所耗费时间; reader_cost为数据读取耗费时间。从训练集的整个损失函数loss上来看,随着训练的不断迭代,loss呈现不断递减的趋势,说明在训练集上模型的预测精度在不断提升。 验证集上可视化结果如图5.17所示。 图5.17验证集上训练结果可视化 综合训练集和验证集上的可视化效果来看,整个训练过程没有出现过拟合或欠拟合的现象,这很大程度上归功于本章采用的模型学习能力较强且数据集样本量足够多。在验证集上的最佳mIoU=0.9884,最佳像素预测准确率Acc=0.9946,分割精度较高。 3. 模型评估 训练完成后,可以使用tools/val.py评估模型的精度,执行如下命令进行模型评估: python -m paddle.distributed.launch tools/val.py \ --config config.yml \ --model_path output/best_model/model.pdparams 其中,参数model_path用来指定评估的模型权重路径。在语义分割领域中,评估模型质量主要是通过3个指标进行判断: 准确率(Accuracy,Acc)、平均交并比(Mean Intersection over Union,mIoU)和Kappa系数。从验证集上来看,语义分割的两个常见评价指标mIoU和Acc随着模型训练迭代均呈现不断上涨的趋势,并且从20000次迭代后上涨趋势变缓,逐渐开始收敛。其中准确率指类别预测正确的像素占总像素的比例,准确率越高模型质量越好。平均交并比为对每个语义类别单独进行推理计算,计算出的预测区域和实际区域交集除以预测区域和实际区域的并集,然后将所有语义类别得到的结果取平均。Kappa系数是用于一致性检验的指标,可以用于综合衡量预测的效果。Kappa系数的计算是基于混淆矩阵的,取值为-1~1,Kappa系数越高模型质量越好。 运行上述命令后,输出结果如下: [EVAL] #Images: 1042 mIoU: 0.9884 Acc: 0.9946 Kappa: 0.9883 Dice: 0.9941 [EVAL] Class IoU: [0.9915 0.9852] 4. 动态图预测 如果想基于训练好的动态图模型对未知图片进行预测,可以使用PaddleSeg提供的脚本tools/predict.py来实现。 预测命令如下: python tools/predict.py \ --config config.yml \ --model_path output/best_model/model.pdparams \ --image_path ./dataset/test_human \ --save_dir output/result 其中,image_path可以是一张图片的路径,也可以是一个目录; model_path参数表示训练好的动态图模型路径; save_dir参数表示预测结果的保存路径。 预测完成后会同时生成伪彩色掩码图(位于output/result/pseudo_color_prediction)以及合成的可视化结果图(位于output/result/added_prediction)。部分预测结果如图5.18所示,其中第1行为原始图,第2行为模型预测结果,第3行为可视化合成图。 图5.18动态图预测结果 从图5.18所示的合成图上可以看到,对于半身人像分割任务来说,使用OCRNet算法取得了不错的分割精度。进一步查看训练好的模型PaddleSeg/output/best_model/model.pdparams,可以发现其大小在50MB左右,相对于一般的深度学习网络模型来说其模型并不大,适合在CPU上进行推理。 5. 静态图导出 为了能将训练好的深度学习模型在生产环境中进行部署,需要将动态图模型转换为静态图模型。同其他套件一样,PaddleSeg也提供了静态图模型导出脚本,具体命令如下: python tools/export.py \ --config config.yml \ --model_path output/best_model/model.pdparams \ --save_dir output/inference \ --input_shape 1 3 512 512 最后在output/inference文件夹下面会生成导出后的静态图模型文件,如下所示: output/inference ├── deploy.yaml# 部署相关的配置文件,主要说明数据预处理方式等信息 ├── model.pdmodel# 预测模型的拓扑结构文件 ├── model.pdiparams# 预测模型的权重文件 └── model.pdiparams.info # 参数信息文件,一般无须关注 其中权重文件model.pdiparams的文件大小在50MB左右,这是一个中等量级的模型文件,如果后期想要进一步提高模型预测精度,可以改用更重量级的骨干模型,例如将Backbone改成HRNetw48,但是越重量级模型其资源占用也会越多,推理速度会变慢。 下面可以使用PaddleSeg提供的静态图推理脚本来验证导出的静态图模型是否正确。具体命令如下: python deploy/python/infer.py \ --config ./output/inference/deploy.yaml \ --image_path ./dataset/test_human 导出结果存放在output文件夹下面,以伪彩色掩码图形式给出。 到这里,本节内容完成了整个项目的算法研发,得到了有效的人像分割模型并成功转换成静态图模型。从PaddleSeg的使用体验上来看,PaddleSeg集成的语义分割算法使用非常方便,整个训练和推理都给出了简洁的调用脚本。使用PaddleSeg套件可以极大地减少研发成本。从实际效果上来看,对于人像发丝等较细微的局部区域,该模型还存在不足,无法精确提取此类带有一定透明度的局部前景,建议后续可以改用抠图(Matting)算法来实现,读者可以自行尝试,本书不再深入介绍。 视频讲解 5.4Qt C++桌面客户端部署(Windows CPU推理) 在第3章和第4章均采用Qt作为C++的编程工具,旨在利用Qt强大的代码编辑能力,在Linux终端上编写无界面的C++程序。除了开发常规的C++无界面程序以外,Qt最重要的优势之一就是它的GUI跨平台开发能力。简单来说,用Qt可以开发高效美观的跨平台图形界面应用程序。目前很多流行的桌面客户端程序都是使用Qt开发的,如WPS、YY语音、Skype、豆瓣电台、Adobe Photoshop等。 本章将在Windows下开发带界面的Qt客户端程序,通过使用C++语言将人像分割模型进行集成,最终研发一款能够高效制作证件照片的客户端工具。 进行正式开发前,读者需要在装有Windows 10操作系统的电脑上安装好Qt软件。本书侧重讲解深度学习模型部署,对于Qt的安装和基本环境配置方法本书不再深入介绍,不熟悉的读者可以参考本书配套的资源教程进行学习和操作,教程网址详见前言二维码。 本书使用的Qt版本是Qt5.15.2,对应的编译器为MSVC2019_64bit。 5.4.1Qt基础示例程序介绍 为了方便读者快速上手,本书准备了一个精简的Qt基础示例程序clean_qt_demo,读者可以从本书配套资源网站上进行下载,网址详见前言二维码。下载该示例程序以后使用Qt Creator来打开它。具体的,单击顶部菜单栏“文件”→“打开文件或项目”,然后找到下载程序中的faceEval.pro文件,选择该文件打开即可。 首次打开该项目,Qt会让用户选择对应的编译器环境,可以选择Desktop Qt 5.15.2 MSVC2019 64bit作为编译器环境,最后单击Configure Project按钮完成配置,如图5.19所示。 图5.19选择Qt的编译器环境 打开项目以后,单击左下角图标切换模式为Release模式,如图5.20所示。 该示例程序使用Qt Widget框架进行创建,结合qss进行界面设计,已经搭建好图像读取和处理的基本代码框架,所有功能仅依赖Qt自带的库环境。 程序文件结构目录如图5.21所示。 图5.20切换为Release模式 图5.21示例程序文件目录 faceEval.pro是项目的全局配置文件,用于指定程序的头文件、源文件以及第三方依赖库路径; main.cpp是程序运行的主文件,负责主窗口的启动,在本项目中读者不需要修改该文件; 主窗口程序由mainwindow.h和mainwindow.cpp两个文件组成,这两个文件负责整个程序的UI和逻辑执行; algorithm.h和algorithm.cpp负责具体的算法实现。 读者可以直接按Ctrl+R组合键编译和运行项目,运行后主界面如图5.22所示。 图5.22基础示例程序主界面 整体界面设计比较简单,菜单栏和工具栏仅有3个功能按钮: 选择照片、制作、保存图像。在工具栏按钮下方是两个并列的显示窗体,左边的窗体用于显示原始照片,右边的窗体用于显示制作好的证件照片。 下面介绍这个示例程序的基本功能。 1. 选择照片 支持从本地读取照片,并且根据照片宽高比自适应地显示到“原始照片”窗体上,如图5.23所示。 图5.23加载图像并自适应显示 2. 制作 该部分是部署的核心内容,由于较为复杂,本示例程序仅预留接口,具体的功能实现将在后面给出。 在示例程序中仅完成图像的复制功能,即复制一份原始图像,并显示到“制作效果”窗体中,如图5.24所示。 图5.24制作 算法接口定义在algorithm.h文件的MakeIDPhoto()函数中,后面将针对本章任务继续完善该功能的开发,将算法集成进来。 3. 保存图像 该功能可以将制作好的证件照片自动按照时间保存到计算机C盘下名为Images的文件夹中,如图5.25所示。 图5.25保存图像 通过以上功能介绍,读者可以看到这个示例程序整体比较简洁,相关功能和接口都已经预留好,适合读者快速学习和掌握。对于Qt不熟悉的读者,可以参照本示例程序对照代码来学习,这样可以更快速、更有针对性地掌握Qt。 接下来将以该示例程序作为项目起点,然后逐步添加相关功能模块并实现人像分割模型集成。 5.4.2配置并导入FastDeploy库 前面两章使用FastDeploy工具进行深度学习算法部署,通过FastDeploy的使用极大地减少了部署工作。本章继续使用FastDeploy,讲解如何在x86 CPU的Windows操作系统上进行深度学习算法推理。目前FastDeploy还不支持旧版本的Windows操作系统,因此建议使用Windows 10及以上操作系统。FastDeploy的相关介绍可以参阅3.4.1节内容。 1. 下载FastDeploy的SDK库 由于Windows下的CPU部署是一个比较基础的部署需求,因此,FastDeploy官网对Windows下的部署方案支持力度最大。读者可以按照FastDeploy官网教程自行编译FastDeploy的SDK库,也可以下载和使用官网编译好的SDK库。下载网址详见前言二维码。FastDeploy官网的C++版本SDK库如图5.26所示。 图5.26FastDeploy官网的C++版本SDK库 由于本章任务仅需要CPU进行推理,因此可以选择CPU版的C++ SDK进行下载。单击图5.26中的Windows x64版本对应的链接进行下载。下载后将其解压到指定目录下,完整目录结构如图5.27所示。 图5.27FastDeploy的C++ SDK库目录 2. 在Qt项目中配置FastDeploy库 为了能在Qt项目中使用FastDeploy库,需要对Qt项目进行配置。 首先打开示例程序的faceEval.pro文件,在该文件最后添加FastDeploy库的头文件和库文件,如下所示: INCLUDEPATH+=C:\fastdeploy_cpu\include INCLUDEPATH+=C:\fastdeploy_cpu\third_libs\install\opencv\build\include LIBS += -LC:\fastdeploy_cpu\lib\ -lfastdeploy LIBS += -LC:\fastdeploy_cpu\third_libs\install\opencv\build\x64\vc15\lib\ -lopencv_world3416 引入FastDeploy库的主要目的是实现深度学习算法推理。打开algorithm.h文件,在头部添加如下代码: #include "fastdeploy/vision.h" #include using namespace cv; 到这里,已经配置完了FastDeploy库。下面可以在Qt示例程序中正常使用FastDeploy库进行语义分割模型推理了。 5.4.3编写算法推理模块 在5.4.1节中介绍过,制作模块中已经预留好了深度学习模型推理接口MakeIDPhoto()。具体的,用户单击“制作”按钮,会触发act_detection信号,该信号由类MainWindow的槽函数Process()进行接收,在槽函数Process()内部直接调用了algorithm.h文件中的MakeIDPhoto()函数,最终由MakeIDPhoto()函数负责对传入的QImage图像进行证件照换背景处理并将处理结果返回。具体执行流程请读者自行参阅示例程序代码。 在示例程序MakeIDPhoto()函数中,直接将输入作为输出结果返回,代码如下: QImage MakeIDPhoto(QImage img) { return img; } 下面需要修改MakeIDPhoto()函数中的代码,对输入的照片进行语义分割,然后与新的背景(此处使用蓝色)进行合成,完整代码如下: QImage MakeIDPhoto(QImage img) { //读取模型 auto model_file = "inference/model.pdmodel"; auto params_file = "inference/model.pdiparams"; auto config_file = "inference//deploy.yaml"; auto option = fastdeploy::RuntimeOption(); option.UseCpu(); auto model = fastdeploy::vision::segmentation::PaddleSegModel( model_file, params_file, config_file, option); if (!model.Initialized()) return img; //转换图片 Mat im = QImage2cvMat(img); //推理预测 fastdeploy::vision::SegmentationResult res; if (!model.Predict(im, &res)) return img; //获取掩码图 cv::Mat mask(im.rows, im.cols, CV_8UC1, res.label_map.data()); mask = mask*255; //与蓝色背景合成 Mat bg(im.size(), im.type(), Scalar(219, 142, 67)); cvtColor(mask, mask, COLOR_GRAY2BGR); im.convertTo(im, CV_32FC3); bg.convertTo(bg, CV_32FC3); mask.convertTo(mask, CV_32FC3, 1.0/255); Mat comp = Mat::zeros(im.size(), im.type()); multiply(mask, im, im); multiply(Scalar::all(1.0)-mask, bg, bg); add(im, bg, comp); comp.convertTo(comp, CV_8UC3); //返回图像 img = cvMat2QImage(comp); return img; } 为了便于读者能够清晰地理解整个推理流程,上述代码中将模型的加载、图片读取、推理预测、后处理这几个步骤都写在了MakeIDPhoto()函数中,这样每次单击“制作”按钮时均需要重新读取模型,会造成不必要的资源浪费。对于开发实际的软件产品来说,一般会将模型model定义为类成员变量,并且在类初始化时就将模型提前加载完毕,后面每次执行推理操作时就不再需要重复加载模型了。读者可以自行尝试对上述程序进行修改和优化。 在背景合成部分,采用了抠图领域中常用的图像合成方法,即假设图像是由前景和背景的线性混合叠加而成,对应公式如下: Comp=alpha×F+(1-alpha)×B 其中,Comp表示合成图像; F表示前景图像; B表示背景图像; alpha表示混合比例。对应本章语义分割任务,人像区域对应的alpha值为1,背景区域alpha对应的值为0。 上述代码中需要将Qt的QImage图像类和OpenCV的Mat图像类进行相互转换,相关转换代码如下: // cv::Mat转换成QImage QImage cvMat2QImage(const Mat& mat) { const uchar *pSrc = (const uchar*)mat.data; QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); return image.rgbSwapped(); } // QImage转换成cv::Mat Mat QImage2cvMat(QImage img) { Mat mat = Mat(img.height(), img.width(), CV_8UC4, (void*)img.constBits(), img.bytesPerLine()); cv::cvtColor(mat, mat, COLOR_BGRA2BGR); return mat; } 到这里整个的核心代码就编写完成了。可以看到,使用FastDeploy工具部署语义分割模型是非常方便的。 5.4.4集成依赖库和模型 编写好代码以后就可以编译并运行程序了。首先单击Qt Creator左下角的锤子状按钮进行编译,编译完成后会生成对应的release可执行程序文件夹,在该文件夹生成的faceEval.exe就是最终的可执行程序。直接双击运行该可执行程序会遇到dll缺失的错误,这是因为该可执行程序依赖FastDeploy以及Qt本身的dll依赖库,因此需要将这些依赖文件准确找到后再复制到release文件夹下面。 1. 集成FastDeploy的依赖库 在Windows平台上,FastDeploy提供了fastdeploy_init.bat工具来管理FastDeploy中所有的依赖库。进入FastDeploy的SDK根目录,运行install命令,可以将SDK中所有的依赖文件安装到指定的目录。 具体的,可以在FastDeploy的SDK根目录下创建一个临时的bin文件夹,然后运行如下命令: ./fastdeploy_init.bat install %cd% bin 运行结果如图5.28所示。 图5.28收集FastDeploy库的dll文件 然后按照提示输入y执行即可。最终在bin文件夹下会生成FastDeploy所有的依赖文件,只需要把所有这些文件复制到前面由Qt生成的release目录下即可。 2. 集成Qt的依赖库 与FastDeploy类似,Qt也提供了windeployqt.exe工具用来管理Qt的所有依赖库。 具体的,在开始界面找到Qt的命令行工具,然后根据生成exe文件所用的编译器选择相应的命令行工具Qt 5.15.2(MSVC 2019 64bit),如图5.29所示。 接下来在Qt安装目录下找到windeployqt.exe(注意: 该工具会存在几个不同的版本,需要选择对应编译器文件夹下的程序),如图5.30所示。 图5.29Qt命令行工具 图5.30windeployqt.exe工具路径 根据找到的windeployqt.exe所在目录,运行下述命令: C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe C:\Users\qb\Desktop\release\faceEval.exe 其中,C:\Users\qb\Desktop\release\faceEval.exe为Qt最终生成的程序所在路径。windeployqt.exe会自动根据Qt生成的exe文件分析其依赖库,并将Qt相关依赖库复制到faceEval.exe所在文件夹下面。 3. 集成深度学习模型 最后一步,需要将前面训练好的静态图模型复制到程序目录下面,即将inference文件夹复制到faceEval.exe所在目录下面。 到这里,程序所有的依赖库和模型文件都已准备完毕。双击faceEval.exe即可正常运行,最终实现效果如图5.31所示。 图5.31程序最终运行效果图 经过测试,算法在CPU上的整体运行速度平均为2s。从测试图的效果上来看,尽管提供的测试照片背景比较复杂(局部头发区域和背景区域相似度较高),但是通过语义分割模型的处理,依然能够准确地将人像区域提取出来。后续读者如果想进一步优化人像边缘的分割效果,可以参考PaddleSeg套件中的人像抠图方案。 到这里,本章任务结束,相关素材可以从本书配套资源中获取。 5.5小结 本章围绕图像语义分割,重点介绍了基于深度学习的FCN、UNet、HRNet和OCRNet这几种典型算法及其实现原理。在算法原理基础上,重点讲解了如何使用图像语义分割套件PaddleSeg来开发一款用于PC桌面的证件照制作小工具,全流程地实现了数据预处理、训练、验证和推理,并最终将研发的模型通过FastDeploy套件在Windows 10平台上实现了部署和应用。 读者学完本章内容后,应掌握基本的图像语义分割算法原理,能够利用PaddleSeg套件按照本章流程研发自己的图像分割类产品。