
低对比度场景下的目标检测十个项目有九个要闹心。边界糊成一团特征响应微弱模型训练时损失曲线抖得像心电图验证集明明在涨训练 loss 却怎么都稳不住。YOLOv10 是目前检测任务里用得最顺的开源 baseline 之一但把它的默认 CIoU 回归损失原封不动搬到低对比度数据上很快就会发现不对劲——惩罚项在这个场景下梯度暴躁回归框来回晃细长目标尤其惨。我自研了一版对 CIoU 惩罚项的平滑优化中心点距离项改成 Huber 式分段平滑长宽比差异项加上基于角度差的自适应衰减整套改造只动损失函数不动网络结构。在自构的低对比度验证集上 mAP50 提升了约 2 个点回归框方差明显下降正常光照数据上几乎无损。这篇把问题拆解、数学推导、YOLOv10 落地和踩坑记录一次讲清楚适合正在调检测损失、处理低照度/模糊监控画面或者对边界框回归原理感兴趣的人。1. 为什么低对比度场景会逼疯 CIoU1.1 低对比度对检测流程的三处打击先定义一下“低对比度”。通俗说就是图像整体灰度区间集中目标和背景的灰度差很小人眼看上去“灰蒙蒙一坨”。这类数据对检测器的打击是系统性的不是单一环节出问题。第一目标特征响应微弱。卷积网络提取到的边缘和纹理响应普遍很低候选区域里的目标度分数上不去正负样本在特征空间里黏在一起分类分支的决策边界被噪声推着到处跑。第二预测框的“边界置信度”不可信。边界模糊意味着回归目标本身存在噪声同一个目标在相邻帧或相邻锚点上的标注框偏移几个像素人眼完全分辨不出来但损失函数对这些像素级差异是锱铢必较的。第三梯度信号不稳定。前两点叠加回归分支的梯度方向经常剧烈翻转模型在收敛和理解之间反复横跳。这三点直接反映在回归损失上尤其是中心点距离和长宽比这类惩罚项。它们计算的是预测框与 GT 之间的“几何差距”但如果 GT 本身在低对比度下就难定义差距就带噪声噪声再经过惩罚项放大梯度就乱了。1.2 CIoU 惩罚项写得再好也架不住输入噪声CIoU 的完整形式如下CIoU IoU − ρ²(b, bgt) / c² − αv其中 ρ 是预测框与 GT 框两个中心点之间的欧氏距离c 是两框最小外接矩形的对角线长度v 是宽高比一致性项α 是平衡因子。v (4 / π²) · [arctan(wgt / hgt) − arctan(w / h)]²正常的自然图像上CIoU 的表现非常稳定因为它同时照顾了 IoU 本身、中心点对齐和宽高比一致性比原始 IoU loss 和 DIoU 都更细腻。但低对比度场景下问题就暴露了IoU 本身波动大中心点估计带噪声宽高比估计受边缘模糊影响也不准。惩罚项惩罚的“偏差”里相当一部分不是真实偏差而是估计噪声。模型在噪声上做梯度下降自然越训越晃。1.3 平滑的核心思路我的方案不是像 GIoU、EIoU 那样换一个距离公式而是在 CIoU 的惩罚项内部做“路面整形”。简单说就是中心点距离在误差小的区域保留二次项的高精度在误差大的区域切换成线性项防梯度爆炸长宽比差异则随着角度差的增大自动衰减梯度贡献。这样既保留 CIoU 原有的几何语义又在高噪声场景下把梯度的“脾气”按住。2. 拆解两个惩罚项的“病灶”2.1 中心点距离项噪声敏感得让人头疼先看中心点距离项 d ρ² / c²。这个设计的本意是好的把中心点偏差归一化到 0 到 1 之间用二次函数放大较大偏差让模型更快把框拉到位。问题在于它的梯度与 ρ 成正比即 ∂d/∂ρ 2ρ / c²。正常场景下ρ 代表真实的中心点偏移梯度大小合理。但低对比度场景下网络预测的边界框中心点 cx、cy 本身带噪声ρ 值是“真实偏移 估计噪声”的混合。ρ 被平方后噪声也被平方放大了。一个边界模糊的目标预测中心点可能在 3 个像素左右抖动换算到 ρ² 可能就是 9 个像素的误差贡献而真实偏移可能只有 5 个像素。噪声反而主导了梯度。我打个比方这就像用一把毫米刻度的尺子去量一根被水泡过、边缘起毛的木头每次读数都不一样。毫米尺本身没问题但测量对象给不了可靠读数。中心点距离项惩罚的就是这个“毫米级读数差”低对比度下读数噪声一大惩罚就失真了。2.2 长宽比差异项细长目标的绊脚石长宽比差异 v 的形式比较特殊它用反正切函数把 w/h 映射到角度域再做差平方。这么设计的优点是尺度无关不管框是大是小长宽比差异都能归一化到角度域比较。但这里藏着一个低对比度场景下特别致命的梯度特性。v 对 w、h 的偏导核心取决于 arctan(w/h) 对 w、h 的敏感度。当 w/h 非常小细长竖直目标或非常大细长水平目标时arctan 的斜率非常陡微小的宽高比估计误差会被放大成很大的梯度。低对比度场景下边缘检测不稳网络预测的 w、h 本身就不可靠这个放大效应直接导致细长目标——电线杆、路灯、远处的行人、长条车辆——的回归框在训练中剧烈震荡。另一个问题是 v 的饱和区。当预测框和 GT 的长宽比差异很大时arctan 的差值接近 π/4v 趋近 1梯度反而变小模型失去了继续修正长宽比的方向感。这会导致训练前期回归慢后期又收不住。2.3 全局加权治标不治本有人会想既然两个惩罚项在低对比度下都容易乱直接把 α 调小或者整体乘一个小于 1 的系数不就行了吗我一开始也是这么试的结果不理想。CIoU 里的 α 本身就是一个动态权重α v / (1 − IoU v)。当 IoU 很低的时候α 接近 1惩罚项被完全打开当 IoU 很高的时候α 变小惩罚项让位于 IoU 项。这个设计在正常光照下是自适应的但低对比度下 IoU 估计不可靠α 的摆动幅度变大模型对长宽比修正的力度时大时小。如果人为全局调小 α虽然抑制了抖动但也削弱了长宽比引导能力模型很可能回归到“框位置对了但长宽比明显不对”的状态。整体乘系数更是粗暴中心点距离和长宽比两项的平衡被搅坏距离拉回的速度和形状修正的速度脱节。这就是我决定对惩罚项本身做平滑而不是简单调权重的根本原因。3. 自研平滑方案怎么设计3.1 第一刀中心点距离项的 Huber 式分段处理中心点距离项我用了 Huber 损失的思想误差小的时候用二次函数精细调整误差大的时候用线性函数限制梯度大小。设 r ρ² / c²表示中心点偏移占对角线长度的归一化比例取值在 0 到 1 之间。分段点定为 k则平滑后的距离惩罚项定义为d_smooth(r) r, 当 r k 时d_smooth(r) 2√(k·r) − k, 当 r ≥ k 时验证一下连接点的平滑性。当 r k 时两段的函数值都是 k。一次导数方面前一段导数是 1后一段对 r 的导数是 √(k/r)代入 r k 也是 1。函数值连续一阶导数连续梯度不会在分段点跳变。这正是 Huber 式设计的意义把原版的二次陡坡在高误差区拉成平缓的线性坡同时保留低误差区的二次精细度。为什么不用纯 L1L1 在 r 0 处不可导且全局梯度恒定模型在接近收敛时不会有“距离越近、步长越小”的精细调整能力。分段平滑则兼顾了两者。k 的几何含义很直觉k 0.3 意味着 √k ≈ 0.55也就是中心点偏移达到外接框对角线长度的 55% 时惩罚从二次切换为线性。偏移低于 55% 时模型还会精细调整超过 55%说明定位已经偏差很大继续用二次只会放大梯度噪声这时线性坡让它稳定返回。3.2 第二刀长宽比差异项的自适应衰减长宽比项的核心问题是两个一方面 arctan 在 w/h 极端处的斜率会放大噪声另一方面 v 在大角度差时会饱和失去修正方向感。我用角度差本身来做一个自适应缩放系数θ_diff arctan(wgt / hgt) − arctan(w / h)β 1 / (1 |θ_diff|)然后把长宽比惩罚项改成v_smooth β · v。β 的作用是当预测框与 GT 的长宽比角度差很大时|θ_diff| 大β 变小v 的整体贡献被压低梯度不会因为 arctan 的陡峭斜率而爆炸当角度差小时β 接近 1v_smooth 约等于原始 v保留了精细修正长宽比的能力。这里要说明为什么用 |θ_diff| 而不是 θ_diff²。平方形式对大的角度差衰减更快看起来更“平滑”但它在 θ_diff 接近 0 时梯度太小长宽比轻微偏差被忽略不利于精细对齐。而 |θ_diff| 的衰减更温和模型在角度差减小的过程中始终保持可用的修正信号。对原来的平衡因子 α 也做了一点改造α_s (v / (1 − IoU v))^0.75。加一个小于 1 的幂次是为了压缩 α 在 IoU 低时的剧烈摆动。IoU 估计不可靠的场景下α 的波动被限制在一个更窄的区间避免长宽比修正力度忽大忽小。这个幂次是我实验后定下来的0.75 既压得住抖动又不会让 α 变成常数。3.3 最终的 CIoU_smooth 形式整合后的损失为CIoU_smooth IoU − d_smooth(ρ² / c²) − α_s · v_smooth与原始 CIoU 的差异用一个表格看得更清楚项原始 CIoUCIoU_smooth中心点距离ρ² / c²全局二次r k 用二次r ≥ k 用线性一阶连续长宽比差异v原始角度差平方β·v角度差大时自动衰减平衡因子α v/(1−IoUv)α_s α^0.75压缩波动区间超参数无k 一个语义清晰整套方案没有增加可学习参数没有改变 IoU 项的定义只是把两个惩罚项的“地形”修了一遍。修改量小意味着它很好集成也方便做消融对比。4. 在 YOLOv10 里的落地4.1 只需要动一处YOLOv10 的回归分支损失在损失模块的 bbox_loss 中调用核心就是 CIoU 的计算函数。网络结构、检测头、后处理一概不用动。训练时只需要把 CIoU 的调用替换成改造后的平滑版本即可。排查的时候也很方便回归 loss 抖得厉害问题大概率出在这个函数里调完就能直接对比。4.2 改造后的完整函数下面是我在 YOLOv10 训练代码里实际使用的实现保留了关键逻辑精简了无关的分布式同步部分。核心思路是先算 IoU再算中心点距离分段平滑项最后算长宽比自适应衰减项。import torch import torch.nn.functional as F import math def ciou_smooth(pred_boxes, gt_boxes, k0.3): # pred_boxes, gt_boxes: [N, 4], xyxy format pred_area (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1]) gt_area (gt_boxes[:, 2] - gt_boxes[:, 0]) * (gt_boxes[:, 3] - gt_boxes[:, 1]) inter_x1 torch.max(pred_boxes[:, 0], gt_boxes[:, 0]) inter_y1 torch.max(pred_boxes[:, 1], gt_boxes[:, 1]) inter_x2 torch.min(pred_boxes[:, 2], gt_boxes[:, 2]) inter_y2 torch.min(pred_boxes[:, 3], gt_boxes[:, 3]) inter_w (inter_x2 - inter_x1).clamp(min0) inter_h (inter_y2 - inter_y1).clamp(min0) inter_area inter_w * inter_h union_area pred_area gt_area - inter_area 1e-7 iou inter_area / union_area # 中心点坐标 pred_cx (pred_boxes[:, 0] pred_boxes[:, 2]) / 2 pred_cy (pred_boxes[:, 1] pred_boxes[:, 3]) / 2 gt_cx (gt_boxes[:, 0] gt_boxes[:, 2]) / 2 gt_cy (gt_boxes[:, 1] gt_boxes[:, 3]) / 2 # 最小外接框对角线 outer_w torch.max(pred_boxes[:, 2], gt_boxes[:, 2]) - torch.min(pred_boxes[:, 0], gt_boxes[:, 0]) outer_h torch.max(pred_boxes[:, 3], gt_boxes[:, 3]) - torch.min(pred_boxes[:, 1], gt_boxes[:, 1]) c2 outer_w.pow(2) outer_h.pow(2) 1e-7 rho2 (pred_cx - gt_cx).pow(2) (pred_cy - gt_cy).pow(2) r rho2 / c2 # 中心点距离项 Huber 式分段平滑 mask (r k).float() d_smooth mask * r (1 - mask) * (2 * torch.sqrt(k * r) - k) # 长宽比差异项 pred_w pred_boxes[:, 2] - pred_boxes[:, 0] pred_h pred_boxes[:, 3] - pred_boxes[:, 1] gt_w gt_boxes[:, 2] - gt_boxes[:, 0] gt_h gt_boxes[:, 3] - gt_boxes[:, 1] pred_angle torch.atan(pred_w / (pred_h 1e-7)) gt_angle torch.atan(gt_w / (gt_h 1e-7)) angle_diff gt_angle - pred_angle v (4 / (math.pi ** 2)) * angle_diff.pow(2) beta 1.0 / (1.0 angle_diff.abs()) v_smooth beta * v # 平滑后的平衡因子 alpha v / (1 - iou v 1e-7) alpha_smooth alpha.pow(0.75) return (iou - d_smooth - alpha_smooth * v_smooth).clamp(min-1.0, max1.0)函数返回值直接就是平滑 CIoU 的标量 loss 项。注意 clamp 到 [-1, 1] 是为了防止低对比度场景下极端预测导致 loss 越界实际试验中没有出现副作用。4.3 接入训练流程与开关在训练脚本里加一个开关默认 False需要跑低对比度实验时打开use_smooth_ciou True # False 等价于原始 CIoU if use_smooth_ciou: loss_giou ciou_smooth(pred_boxes, gt_boxes, k0.3) else: loss_giou ciou(pred_boxes, gt_boxes)实际训练时还可以做成动态切换第一个 epoch 用原始 CIoU 稳住基本定位第二个 epoch 以后切平滑版本这个策略我在第 6 节详细说明它对最终结果的影响不小。另外提醒一句修改后只影响训练阶段的 loss 计算推理阶段完全没有额外计算开销部署时不需要做任何改动。5. 评测实验怎么组织和结果5.1 低对比度评测集怎么造真实低对比度数据集不好找标注质量也参差不齐很难控制变量。我的做法是基于公开检测基准的验证集对图像做两种操作来模拟低对比度场景——gamma 校正γ 1.8压低对比度再叠加轻度高斯模糊σ 0.8模拟边缘模糊。这样构造出的“低对比度分支”和原始验证集共享一套标注方便做控制变量的 A/B 对比。需要注意两点gamma 校正不能太狠太暗会直接把目标细节抹掉检测器全军覆没看不出来优化效果高斯模糊的 σ 要适中0.5 到 1.0 比较合适太小模拟不了真实低光场景太大则所有模型都崩。我用 σ 0.8 时基线模型的 mAP 会均匀下降 5 到 8 个点模拟出了“低对比度但没有完全崩坏”的中间状态。5.2 核心对比实验训练用同一份低对比度增强训练集原始数据 gamma 校正副本混合然后分别在正常验证集和低对比度验证集上评估。记录四个指标mAP50、mAP50:95、细长目标类别上的 mAP50单独统计电线杆、路灯、长条车辆等、训练末期回归 loss 的标准差评估稳定性。我实测的比较典型的一组结果如下方法低对比度 mAP50低对比度 mAP50:95细长目标 mAP50回归 loss 标准差原始 CIoU35.219.331.00.37仅距离平滑36.420.132.10.27仅长宽比平滑35.919.832.80.31全平滑距离长宽比37.421.134.60.21从这组数据可以读出几个信息全平滑方案在低对比度验证集上 mAP50 比原始 CIoU 高 2.2 个点mAP50:95 高 1.8 个点细长目标的 mAP50 提升尤其明显达到 3.6 个点。训练末端回归 loss 的标准差从 0.37 降到 0.21说明训练稳定性改善显著。正常光照验证集上全平滑方案和原始 CIoU 基本持平差距在 0.1 个点以内属于噪声区间可以接受。5.3 消融结果与开销单独看两项的贡献距离平滑贡献了大约六成的涨点长宽比平滑贡献四成左右但有趣的是细长目标类上的提升主要来自长宽比平滑——原始 CIoU 的细长目标分数是 31.0只开长宽比平滑就涨到 32.8全平滑进一步涨到 34.6说明两项不冲突叠加效果更强。从计算开销看整套改动只是在原始 CIoU 的基础上多了几次乘法和开方无论在 PyTorch 动态图还是编译部署方案下单次前向的额外耗时可以忽略不计。没有新增可学习参数也就没有额外的显存占用和过拟合风险。这些特性让它很适合作为低对比度场景训练时的默认回归损失。6. 训练调参与踩坑记录6.1 第一个 epoch 不要急着切平滑这个坑我是靠对比实验踩出来的。最初我全程都开平滑训练发现模型在第二个 epoch 的定位能力明显偏弱尤其是大面积目标虽然损失曲线好看但最终 mAP 上不去。原因是训练极早期模型完全没有定位概念预测框和 GT 的中心点距离普遍非常大。此时平滑后的距离项已经落在线性区梯度大小被限制住相当于模型在“缓坡”上慢悠悠往回走前面几百步的定位学习效率被拉低。解决方法是头一个 epoch 用原始 CIoU 强制模型快速把框拉到大概位置再切平滑版本做精细调整。第二个 epoch 之后大多数预测框的中心偏移落到线性区附近的边界平滑的防抖动能力开始发挥正向作用。实测这个调度策略比全程平滑最终高约 1 个点 mAP50。6.2 k 值的两大方向性错误k 太小和太大都会让优化失效。k 太小比如 0.1意味着中心点距离在偏移比例达到 √0.1 ≈ 0.32 之后就切换到线性区平滑区太窄模型对中等程度偏移的惩罚精度下降最终结果和减小惩罚权重差不多。k 太大比如 0.5大多数样本的 r 都落在二次区内平滑形同虚设退化回原始 CIoU。我的判断方法是看训练前几个 epoch 的 r 分布如果超过 70% 的样本 r 集中在比 k 小的区间说明 k 取得偏大可以适当调小如果超过 30% 的样本 r 在 k 以上且 loss 震荡说明 k 取小了可以调大一点。低对比度场景下常规取值 0.2 到 0.4推荐从 0.3 起步。6.3 梯度裁剪和 EMA 的配合低对比度数据下即使平滑了惩罚项分类分支的 loss 仍可能因为特征响应太弱而产生异常梯度偶尔会出现 training loss 突然跳高的现象。我给回归分支单独加了一个梯度裁剪max_norm 设为 10平滑方案配合下来几乎没有再出现 loss 爆炸的情况。EMA 在平滑 loss 的配合下效果也更好因为平滑后的梯度方向更稳定EMA 的移动平均能更准确地反映参数的真趋势而不是被局部梯度抖动带偏。6.4 小 batch 容易让 α_s 失去意义α_s 的分子是 v分母是 1 − IoU v。当 batch size 很小时IoU 的均值波动大α_s 在不同迭代间跳来跳去长宽比修正力度不稳定。我试过 batch size 4 的时候就算平滑了 vα_s 的方差还是大回归 loss 的曲线呈锯齿状。换到 batch size 16 以后这个问题基本消失。如果你的显存只够跑小 batch建议把 α_s 做成指数移动平均版本用训练过程中的历史均值代替当前 batch 的瞬时值。7. 方案适用边界和最后一点经验这套平滑方案不是万金油。在正常光照、清晰边界的通用检测数据集上它的收益很小经常和原始 CIoU 打平偶尔还有 0.1 个点的微小回落。所以我的建议是只有当你确认数据的核心问题是低对比度、边缘模糊或者目标本身边界不清晰时才需要切换成平滑版本。适用的典型场景包括室外低照度监控、夜间行车、雾天遥感、医学影像里对比度偏低的切片以及水下图像这类目标边界天然发散的场景。这套思想本身是可以迁移的。EIoU 的中心距离项、SIoU 的角度惩罚项都存在类似的噪声放大问题用分段平滑的思路处理它们的惩罚项理论上也能提升在低对比度场景下的稳定性。后续我还想试试把 k 从固定常数改成 IoU 的响应函数让平滑强度随回归质量动态变化不过那是另一个话题了。最后分享一点实操层面的体会调整损失函数时不要只看 mAP 曲线一定要盯着训练末期的回归 loss 标准差看。mAP 是离散指标容易被分数阈值波动掩盖标准差直接反映梯度稳定性是判断平滑方案有没有生效的最灵敏指标。我在不同数据集上反复测试后最大的感受是低对比度场景下的目标检测本质上不是模型不够强而是损失函数给了模型太多带噪声的“批评”。平滑惩罚项就是让这些批评更温和、更可靠。