ARTICLE DETAIL

资讯详情

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

BiSeNet人脸解析实战:从PyTorch训练到ONNX int8量化与端侧部署全链路

BiSeNet人脸解析实战:从PyTorch训练到ONNX int8量化与端侧部署全链路 人脸解析这个方向我从早期用FCN硬啃到后来切到BiSeNet做实时分割前后踩了不少坑。BiSeNet这个模型结构其实不算新但它在人脸解析这个细分任务上一直很能打——速度快、精度够、19类语义输出直接可用。不过真正落地的时候问题往往不在模型本身而在部署链路上PyTorch训练完怎么转ONNX、ONNX怎么量化到int8、量化后精度掉了怎么找回来、端侧推理框架怎么选。这篇就把我从训练到部署的完整链路拆开讲包括BiSeNet的结构细节、19类标签的实际含义、ONNX导出的坑、int8量化的校准策略以及在不同推理后端上的实测表现。不管你是刚接触语义分割的新手还是已经在做端侧部署的老手应该都能从里面找到能直接用的东西。1. 人脸解析到底在解决什么问题1.1 从像素级理解人脸区域人脸解析Face Parsing本质上是语义分割在人脸这个特定域上的应用。给定一张人脸图像模型需要为每个像素分配一个类别标签比如这个像素属于左眼、右眼、鼻子、上唇、下唇、皮肤、头发、背景等等。和通用语义分割不同的是人脸解析的类别定义更细区域边界更微妙而且对实时性要求往往更高——毕竟大部分应用场景是视频流处理或者移动端实时特效。我第一次接触这个任务的时候以为用通用分割模型直接跑就行了结果发现通用模型在人脸区域的边界处理上非常粗糙嘴唇和牙齿经常混在一起眼睛和眼镜框也分不开。后来才明白人脸解析需要专门的模型结构和训练策略因为人脸区域的类间差异极小类内差异却很大不同人的嘴唇颜色、形状差异巨大这对模型的特征提取能力提出了很高要求。BiSeNet之所以在人脸解析上表现好核心在于它的双路结构设计。一路是Spatial Path负责保留高分辨率的空间细节用很少的通道数但很大的特征图来捕捉边缘和纹理信息另一路是Context Path用深层网络提取语义上下文分辨率低但感受野大。两路通过Feature Fusion Module融合最终输出精细的分割结果。这个设计思路其实和很多实时分割网络类似但BiSeNet在融合模块上的设计更轻量推理速度优势明显。1.2 19类标签的实际含义与标注体系19类人脸解析的标签体系在不同数据集上略有差异但主流的基本一致。我整理了一份常用的标签映射表这个在训练和推理后处理时都会用到类别ID标签名称说明0background背景区域1skin面部皮肤2nose鼻子3eye_g眼镜4l_eye左眼5r_eye右眼6l_brow左眉7r_brow右眉8l_ear左耳9r_ear右耳10mouth嘴巴区域含唇间11u_lip上唇12l_lip下唇13hair头发14hat帽子15ear_r耳环16neck_l脖子含项链区域17neck脖子18cloth衣服实际使用中类别0到18的索引顺序一定要和训练时保持一致否则推理结果会完全错乱。我见过有人换了数据集但忘了改标签映射结果把头发识别成衣服排查了半天才发现是标签顺序的问题。标注体系方面主流数据集如CelebAMask-HQ提供了高质量的19类标注但标注成本极高。如果要做自定义数据集建议先用预训练模型做伪标注再人工修正边界区域。标注时特别注意嘴唇和牙齿的区分、眼镜框和眼睛的区分这两个地方是最容易标错的。1.3 和实例分割的本质区别很多人会把语义分割和实例分割搞混这里简单说清楚。语义分割是给每个像素分配一个类别标签不区分同一类别的不同个体。比如画面里有两个人语义分割会把两个人的皮肤都标成skin不会区分这是张三的皮肤还是李四的皮肤。实例分割则需要在语义分割的基础上进一步区分同一类别的不同实例输出每个实例的mask和类别。在人脸解析场景下通常只需要语义分割就够了因为我们关心的是这个像素属于哪个面部区域而不是这个像素属于哪个人脸的第几个区域。但如果是多人场景下的精细化处理比如要单独给每个人的嘴唇上不同颜色那就需要先做实例分割或者人脸检测再对每个人脸区域单独做解析。YOLO系列最近出的分割版本同时支持实例分割和语义分割模式但人脸解析这个任务上BiSeNet这类专门优化的轻量分割网络在速度和精度平衡上仍然更有优势。YOLO的分割头更偏向通用目标对人脸这种细粒度区域的分割精度不如专门设计的网络。2. BiSeNet网络结构的核心设计逻辑2.1 Spatial Path为什么用三层卷积就够了Spatial Path的设计非常克制只有三层卷积每层stride2最终输出特征图是原图的1/8分辨率。很多人第一次看这个结构会觉得太简单了但正是这种简单让它能高效保留空间信息。三层卷积的通道数分别是64、128、256每层后面跟BN和ReLU。关键点在于Spatial Path不追求语义抽象能力它的任务就是保留边缘、纹理这些低级特征。如果层数太深感受野变大反而会丢失细节。我试过把Spatial Path加深到5层结果边缘精度反而下降了因为深层特征的空间分辨率被过度压缩。另一个细节是Spatial Path的输出分辨率为1/8而不是1/4或1/16。1/8是一个平衡点再高的话计算量太大再低的话细节丢失严重。实际部署时如果输入是512x512Spatial Path输出就是64x64这个分辨率对于人脸区域来说刚好够用。2.2 Context Path的快速下采样策略Context Path用的是类似ResNet的残差结构但做了大量优化。首先它用了一个快速下采样模块在前两层用较大的stride快速降低分辨率减少计算量。然后堆叠多个残差块每个残差块都是标准的bottleneck结构。这里有个工程上的取舍Context Path的通道数比原始ResNet要少目的是控制整体参数量。BiSeNet的完整版本参数量大约在49M左右而轻量版BiSeNetV2只有3.4M左右。如果做端侧部署建议直接用BiSeNetV2精度损失在可接受范围内但速度提升非常明显。Context Path还引入了注意力细化模块ARM在每个stage的输出上做通道注意力。这个模块的加入让模型能自适应地关注重要通道对最终精度有1-2个点的提升。不过ARM会增加一些计算量如果对速度极度敏感可以考虑去掉ARM精度大概掉0.5个点。2.3 特征融合模块的实际效果FFMFeature Fusion Module是BiSeNet的另一个核心设计。它把Spatial Path的输出和Context Path的输出拼接后先做一次卷积融合再通过通道注意力重新加权。具体流程是拼接后的特征先经过一个1x1卷积降维然后全局池化得到通道描述符再通过两个全连接层生成通道权重最后和原始特征相乘。这个设计的直觉是Spatial Path和Context Path的特征重要性在不同通道上是不一样的FFM让网络自己学习哪些通道更重要。实测下来FFM对边界区域的精度提升最明显尤其是嘴唇和眼睛这些细节区域。我在部署时发现FFM的计算量虽然不大但在某些推理框架上全局池化和全连接层的组合会导致额外的内存拷贝。如果遇到性能瓶颈可以尝试把FFM简化成直接相加或者拼接后卷积精度损失大约1个点但速度能提升10%左右。3. 从PyTorch到ONNX的导出实战3.1 导出前的模型准备与检查PyTorch转ONNX看起来简单但坑非常多。首先确保模型处于eval模式这步不做的话BN层和Dropout的行为会不对。然后要确认输入输出的张量形状BiSeNet的输入通常是1x3x512x512输出是1x19x512x512。导出命令的基本形式import torch import torch.onnx model BiSeNet(num_classes19) model.load_state_dict(torch.load(bisenet.pth)) model.eval() dummy_input torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy_input, bisenet.onnx, opset_version11, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )opset_version建议用11或12这两个版本对分割网络的支持最稳定。用13以上版本有时候会遇到插值算子不支持的问题。dynamic_axes设置batch维度为动态方便后续做批量推理。导出后一定要用onnxruntime验证一遍对比PyTorch和ONNX的输出差异。我一般会跑100张测试图计算逐像素的argmax一致率正常应该在99.9%以上。如果低于99%说明导出过程有问题通常是某个算子的实现差异导致的。3.2 常见导出错误与修复方案最常见的错误是插值算子的问题。BiSeNet的上采样用的是双线性插值PyTorch的interpolate在转ONNX时如果align_corners设置不对会导致输出偏移。建议统一设置align_cornersFalse并在导出后用onnxruntime验证。另一个常见问题是自适应池化。如果模型里用了AdaptiveAvgPool2dONNX对动态输入尺寸的支持不好。解决方案是固定输入尺寸或者把自适应池化替换成普通平均池化。还有一个坑是BatchNorm的折叠。导出时如果开了constant foldingBN层会被折叠进卷积这本身没问题但如果后续要做量化折叠后的模型反而不好处理。建议导出时关闭constant folding保持BN层独立。torch.onnx.export( model, dummy_input, bisenet.onnx, opset_version11, do_constant_foldingFalse, # 关闭常量折叠 input_names[input], output_names[output] )3.3 ONNX模型的结构验证与简化导出后的ONNX模型往往包含很多冗余算子比如多余的Transpose、Reshape。用onnx-simplifier可以自动简化pip install onnx-simplifier python -m onnxsim bisenet.onnx bisenet_sim.onnx简化后的模型不仅体积更小推理速度也会有所提升。我实测过一个BiSeNet模型简化后体积从49MB降到47MB推理速度提升约5%。验证模型结构可以用netron可视化重点检查几个地方输入输出形状是否正确、有没有异常的算子、BN层是否还在。如果发现模型里有大量的Identity算子说明导出时有些层没有被正确优化可以手动清理。4. int8量化的校准策略与精度恢复4.1 量化为什么会导致精度下降int8量化的本质是把float32的权重和激活值映射到int8的整数范围。这个映射过程会引入量化误差尤其是当激活值的分布不均匀时误差会更大。人脸解析任务对边界敏感量化误差在边界区域会被放大导致分割边缘变得锯齿状。精度下降的另一个原因是激活值的动态范围。BiSeNet的某些层激活值范围很大直接量化到int8会丢失很多信息。解决方案是用校准数据集统计激活值的分布生成更合理的量化参数。4.2 校准数据集的选取原则校准数据集不需要标注但需要能代表实际推理时的数据分布。我一般从训练集里随机抽100-500张图做校准数量不用太多但覆盖面要广不同光照、不同角度、不同人种、有无眼镜、有无帽子等。校准数据的预处理要和推理时完全一致包括归一化参数、输入尺寸。如果推理时用了动态输入尺寸校准数据也要覆盖不同的尺寸。from onnxruntime.quantization import quantize_static, CalibrationDataReader class FaceCalibrationReader(CalibrationDataReader): def __init__(self, image_paths): self.image_paths image_paths self.index 0 def get_next(self): if self.index len(self.image_paths): return None img preprocess(self.image_paths[self.index]) self.index 1 return {input: img}校准方法建议用Entropy或者Percentile这两种方法对激活值分布的鲁棒性更好。MinMax方法虽然简单但对异常值太敏感容易导致量化范围过大。4.3 量化后的精度恢复技巧量化后如果精度掉得太多可以尝试以下几种方法第一种是混合量化对敏感层保持float32其他层用int8。BiSeNet里面对精度影响最大的层通常是FFM模块和最后的输出卷积把这些层排除在量化之外精度能恢复不少。第二种是量化感知训练QAT在训练阶段就模拟量化误差让模型适应量化后的数值分布。QAT能把int8量化的精度损失控制在0.5个点以内但需要重新训练成本较高。第三种是调整量化参数手动设置某些层的scale和zero_point。这需要对模型结构非常熟悉一般不建议新手操作。我实测下来PTQ训练后量化在BiSeNet上精度损失大约2-3个点QAT能控制在1个点以内。如果对精度要求极高建议直接上QAT如果只是做demo或者对精度要求不那么苛刻PTQ加混合量化就够了。5. 端侧推理框架的选型与实测对比5.1 ONNX Runtime vs TensorFlow Lite vs NCNN端侧推理框架的选择直接决定了部署的难易程度和最终性能。我分别在PC和移动端上测了ONNX Runtime、TFLite和NCNN三个框架测试模型是BiSeNetV2输入512x512int8量化。框架平台推理耗时(ms)模型体积(MB)精度(mIoU)ONNX RuntimePC (x86)28120.782ONNX RuntimeARM65120.782TFLiteARM52110.775NCNNARM45100.778ONNX RuntimePC (GPU)8120.782从数据看NCNN在ARM平台上的速度最快模型体积也最小但精度略低。TFLite的精度和速度比较均衡而且工具链最成熟。ONNX Runtime在PC上表现最好GPU加速后速度极快但在ARM上性能一般。选型建议如果目标平台是Android优先考虑TFLite或NCNN如果是iOSCore ML是更好的选择如果是PC端或者服务端ONNX Runtime最方便。如果要做跨平台部署ONNX Runtime的兼容性最好但需要在性能上做一些妥协。5.2 ONNX转NCNN的实操流程ONNX转NCNN的流程比较直接但有几个细节要注意。首先用onnx-simplifier简化模型然后用ncnn的onnx2ncnn工具转换onnxsim bisenet.onnx bisenet_sim.onnx onnx2ncnn bisenet_sim.onnx bisenet.param bisenet.bin转换后可能会遇到不支持的算子比如某些版本的HardSwish或者自定义的插值算子。这时候需要手动修改param文件或者用ncnn的custom layer来实现。转换完成后建议用ncnn的benchmark工具测一下速度确认没有性能异常。如果发现某个层特别慢可能是算子实现的问题可以尝试替换成等效的其他算子。5.3 移动端部署的性能优化要点移动端部署有几个通用的优化点。第一是输入尺寸512x512在移动端偏大可以降到256x256或者384x384速度能提升2-4倍精度损失大约3-5个点。如果应用场景对精度要求不高这个取舍很划算。第二是线程数设置移动端通常设置2-4个线程比较合适太多线程反而会因为调度开销导致速度下降。NCNN和TFLite都支持设置线程数建议根据实际设备测试后确定。第三是内存复用推理框架通常支持内存池或者内存复用机制开启后能减少内存分配的开销。TFLite的XNNPACK后端和NCNN的Vulkan后端都支持内存优化。第四是算子融合把连续的卷积、BN、ReLU融合成一个算子能减少内存访问次数。ONNX Runtime和TFLite都支持自动算子融合NCNN需要手动在param文件里配置。6. 19类分割结果的后处理与应用6.1 从logits到可视化的完整链路模型输出的是19通道的logits需要经过softmax和argmax才能得到最终的类别图。后处理流程如下import numpy as np def postprocess(output_logits): # output_logits: [1, 19, H, W] prob softmax(output_logits, axis1) pred np.argmax(prob, axis1) # [1, H, W] return pred def softmax(x, axis1): e_x np.exp(x - np.max(x, axisaxis, keepdimsTrue)) return e_x / e_x.sum(axisaxis, keepdimsTrue)得到pred后可以用颜色映射表把类别图转成彩色可视化结果。颜色映射可以自定义建议用对比度高的颜色方便区分相邻类别。COLORS [ [0, 0, 0], # background [204, 0, 0], # skin [76, 153, 0], # nose [204, 204, 0], # eye_g [51, 51, 255], # l_eye [204, 0, 204], # r_eye [0, 255, 255], # l_brow [255, 204, 204], # r_brow [102, 51, 0], # l_ear [255, 0, 0], # r_ear [153, 204, 0], # mouth [255, 153, 0], # u_lip [255, 102, 0], # l_lip [255, 255, 0], # hair [0, 204, 204], # hat [0, 0, 153], # ear_r [255, 255, 153], # neck_l [0, 153, 0], # neck [0, 0, 255], # cloth ]6.2 边缘平滑与噪声去除模型输出的分割图在边界处往往有锯齿尤其是量化后的模型。可以用形态学操作或者条件随机场CRF做后处理。CRF效果最好但速度慢适合离线处理形态学操作速度快适合实时场景。我一般用简单的开闭运算做边缘平滑import cv2 def smooth_mask(pred, kernel_size3): kernel np.ones((kernel_size, kernel_size), np.uint8) smoothed cv2.morphologyEx(pred.astype(np.uint8), cv2.MORPH_CLOSE, kernel) smoothed cv2.morphologyEx(smoothed, cv2.MORPH_OPEN, kernel) return smoothed如果对边缘精度要求极高可以用guided filter以原图为引导图做滤波能很好地保留边缘同时去除噪声。6.3 实际应用场景的对接方式人脸解析的典型应用场景包括虚拟试妆、人脸特效、视频会议背景替换、人脸属性分析等。不同场景对后处理的要求不同。虚拟试妆场景下需要精确的嘴唇和眼睛区域mask对边界精度要求极高建议用高分辨率输入加CRF后处理。人脸特效场景下更关注实时性可以用低分辨率输入加形态学平滑。视频会议背景替换场景下主要用到头发和皮肤的mask对边界要求中等可以用中等分辨率加guided filter。对接方式上通常是把分割结果作为mask和原始图像做alpha blending。比如给嘴唇上色def apply_lipstick(image, pred, color): lip_mask (pred 11) | (pred 12) # u_lip and l_lip lip_mask lip_mask.astype(np.float32) lip_mask cv2.GaussianBlur(lip_mask, (5, 5), 0) lip_mask lip_mask[..., np.newaxis] color_layer np.ones_like(image) * color result image * (1 - lip_mask) color_layer * lip_mask return result.astype(np.uint8)7. 自定义数据集训练与微调经验7.1 数据标注的坑与效率提升方法自定义数据集最大的成本是标注。19类的逐像素标注非常耗时一张图熟练的标注员也要10-15分钟。提升效率的方法有几个第一是用预训练模型做伪标注然后人工修正。BiSeNet在CelebAMask-HQ上预训练后对新人脸数据的伪标注质量已经不错人工只需要修正边界区域和错误类别效率能提升3-5倍。第二是用半自动标注工具比如基于SAMSegment Anything Model的交互式标注。SAM能根据点或框提示生成高质量mask标注员只需要点几下就能得到大部分区域的标注然后手动修正细节。第三是数据增强通过对现有标注数据做几何变换、颜色变换、遮挡模拟等扩充训练集。人脸解析对几何变换比较敏感建议用小幅度的旋转、缩放、平移避免过度变形。7.2 损失函数的选择与调参人脸解析常用的损失函数是交叉熵加Dice Loss的组合。交叉熵负责逐像素分类Dice Loss负责区域重叠度。两者加权求和权重比一般是1:1或者1:2。def combined_loss(pred, target, dice_weight1.0): ce_loss F.cross_entropy(pred, target) dice_loss dice_loss_fn(pred, target) return ce_loss dice_weight * dice_loss如果某些类别样本极少比如耳环、帽子可以用Focal Loss或者类别加权交叉熵来缓解类别不平衡。我一般会给稀有类别2-5倍的权重具体倍数根据类别频率调整。学习率策略上用余弦退火或者多项式衰减效果比较好。初始学习率设0.01warmup 500步然后余弦衰减到1e-5。Batch size根据显存调整一般8-16比较合适。7.3 微调预训练模型的注意事项微调BiSeNet预训练模型时建议冻结Context Path的前几个stage只训练后面的stage和Spatial Path。这样能保留预训练学到的通用特征同时适应新数据的分布。冻结层数根据新数据集的大小决定数据量小于1000张冻结前3个stage1000-5000张冻结前2个stage大于5000张可以全部解冻微调。微调时的学习率要比从头训练小一个数量级建议设0.001。如果发现loss震荡严重进一步降低学习率或者增加warmup步数。还有一个容易忽略的点是BN层的处理。微调时如果batch size太小BN的统计量会不准确建议用SyncBN或者冻结BN层。我一般在小batch场景下直接冻结BN用预训练的统计量效果更稳定。8. 部署上线的性能监控与迭代8.1 推理延迟的分解与瓶颈定位上线后如果发现推理延迟不达标需要先定位瓶颈。把推理过程拆成预处理、模型推理、后处理三段分别计时。预处理通常是resize和归一化后处理是argmax和颜色映射。如果模型推理是瓶颈进一步用profiler分析每一层的耗时。ONNX Runtime和TFLite都支持逐层profiling。常见的瓶颈层包括第一个卷积层输入分辨率大、FFM模块全局池化、最后的输出卷积通道数多。如果预处理是瓶颈检查resize的实现是否用了高效的插值算法。OpenCV的resize在大多数场景下够用但如果对速度极度敏感可以用GPU加速或者固定输入尺寸避免resize。8.2 精度监控与数据回流机制上线后需要持续监控模型精度。没有标注数据的情况下可以用一些代理指标分割结果的连通域数量、边界像素比例、各类别像素占比的分布变化。如果这些指标出现异常波动说明数据分布可能发生了变化。数据回流机制是指把线上推理的困难样本比如置信度低的区域、后处理异常的结果保存下来定期人工标注后加入训练集重新训练。这个闭环能持续提升模型在真实场景下的表现。我一般会设置一个置信度阈值低于阈值的像素比例超过10%就触发回流。回流数据每周处理一次标注后加入训练集每月重新训练一次模型。8.3 模型版本管理与灰度发布模型迭代时建议用版本管理工具比如DVC或者MLflow管理模型文件和训练配置。每次上线新模型前先在灰度环境验证对比新旧模型在相同测试集上的精度和速度。灰度发布时可以按用户ID或者请求ID做分流比如10%的流量走新模型90%走旧模型。观察一段时间后如果新模型指标正常再逐步扩大流量比例。回滚机制也要准备好一旦新模型出现严重问题能快速切回旧版本。模型文件建议保留最近3个版本方便快速回滚。9. 一些实际踩过的坑和解决思路9.1 量化后嘴唇区域消失的问题有一次做int8量化后发现嘴唇区域的分割结果几乎消失了嘴唇像素被误分类成皮肤。排查后发现是量化校准数据里嘴唇区域的样本太少导致量化参数对嘴唇区域的激活值范围估计不准。解决方案是在校准数据集里增加嘴唇区域的样本比例尤其是不同颜色、不同光照下的嘴唇。另外把嘴唇相关的层输出卷积的前几个通道排除在量化之外保持float32精度。这两个措施结合后嘴唇区域的精度恢复了90%以上。9.2 不同推理框架输出不一致的排查同一个ONNX模型在ONNX Runtime和TFLite上推理结果有细微差异。排查后发现是插值算子的实现差异ONNX Runtime用的是align_cornersTrueTFLite用的是align_cornersFalse。统一设置后差异缩小到可忽略范围。另一个原因是量化参数的差异。不同框架的量化工具生成的scale和zero_point可能不同导致量化后的数值有偏差。解决方案是用同一个量化工具生成量化模型然后转换成不同框架的格式。9.3 移动端内存溢出的处理在低端Android设备上部署时遇到内存溢出。排查后发现是模型加载时同时保留了float32和int8两份权重加上输入输出的中间张量总内存超过了设备限制。解决方案是第一用内存映射方式加载模型避免一次性读入内存第二推理时复用输入输出缓冲区避免频繁分配释放第三降低输入分辨率减少中间张量的大小。这三个措施结合后内存占用降低了60%以上。9.4 动态输入尺寸导致的性能波动为了支持不同分辨率的输入模型导出时设置了动态尺寸。但实测发现动态尺寸下推理速度波动很大有时候比固定尺寸慢一倍。原因是动态尺寸下推理框架无法预先分配内存和优化计算图每次推理都要重新做shape推断和内存分配。解决方案是固定几个常用尺寸比如256、384、512分别导出模型推理时根据输入选择最接近的尺寸。这样虽然模型文件多了几个但性能稳定很多。10. 从项目落地角度再看BiSeNet的取舍BiSeNet在人脸解析这个任务上我的整体评价是结构设计合理部署友好精度和速度的平衡做得好。但它也不是万能的有几个场景需要慎重考虑。如果应用场景对精度要求极高比如医疗级的面部分析BiSeNet的19类分割精度可能不够需要考虑更大的模型或者专门设计的网络。如果应用场景对速度要求极高比如60fps以上的实时视频处理BiSeNetV2在移动端可能也吃力需要进一步压缩模型或者用更轻量的结构。从工程落地的角度我建议的路线是先用BiSeNetV2做原型验证确认精度满足需求后再根据目标平台做量化和框架适配。不要一上来就追求极致优化先把链路跑通再逐步优化性能瓶颈。另外数据质量比模型选择更重要。我见过太多项目在模型上反复折腾但训练数据的标注质量很差最终效果怎么调都上不去。与其花时间换模型不如先把标注数据做好把边界区域标清楚把稀有类别的样本补足。这部分工作的投入产出比远高于模型调优。最后说一个实际经验人脸解析的部署链路里预处理和后处理的代码量往往比模型推理本身还多。resize的插值方式、归一化的参数、颜色映射表、边缘平滑算法这些细节对最终效果的影响不比模型小。建议在项目初期就把这些后处理逻辑标准化做成可配置的模块后续换模型或者调参时会方便很多。
返回列表