开始讲解之前推荐一下我的专栏本专栏的内容支持(分类、检测、分割、追踪、关键点检测),专栏目前为限时折扣欢迎大家订阅本专栏本专栏每周更新5-7篇最新机制更有包含我所有改进的文件和交流群提供给大家本人定期在群内分享发表论文方法和经验。一、本文介绍本文给大家带来的最新改进机制是CVPR顶会中的一种新型颜色空间HVI机制针对低照度图像增强任务中的红色区域断裂和暗区噪声问题。HVI通过极化映射重构色相表示解决HSV中红色不连续问题并引入可学习的强度塌缩机制稳定暗区几何分布。核心设计包括1) 极坐标化HS平面消除红色断裂2) 自适应暗区压缩抑制噪声。本文内容可以无缝插入到任何的机制中实现二次创新同时本文将其集成在YOLOv26的Neck中实现有效涨点。欢迎大家订阅我的专栏一起学习YOLO购买专栏读者联系读者入群获取进阶项目文件文字学不会的读者作者可提供视频学习方法.专栏链接YOLOv26有效涨点专栏包含Conv、注意力机制、主干/Backbone、损失函数、优化器、后处理等改进机制目录一、本文介绍二、原理介绍三、核心代码四、添加方法4.1 修改一4.2 修改二4.3 修改三4.4 修改四五、正式训练5.1 yaml文件5.2 训练代码5.3 训练过程截图五、本文总结二、原理介绍论文链接官方论文链接点击此处即可跳转代码链接官方代码链接点击此处即可跳转HVI 不是一个普通的网络层而是文章最核心的“颜色空间模块”。它的目标不是直接堆叠更复杂的网络而是先从表示空间上解决低照度增强里最棘手的两个问题红色区域的不连续噪声和极暗区域的黑平面噪声。作者认为很多方法在 sRGB 空间里做增强时亮度和颜色强耦合容易出现颜色偏移即便换到 HSV虽然亮度与颜色解耦了但又会引入新的伪影。因此HVI 的本质就是在 HSV 的基础上重新组织色彩表示让“相似颜色在空间里更接近”尤其让红色更连续、暗区更稳定。从文章的逻辑来看HVI 可以概括为两个关键设计极坐标化的 HS 平面和可学习的强度塌缩机制。作者先沿用 HSV 的优点把亮度信息从颜色中分离出来其中亮度并不是靠网络去猜而是依据 Max-RGB 理论直接由输入图像得到强度图。随后作者指出 HSV 的色相轴在红色处存在首尾断裂红色既出现在 (h0) 也出现在 (h6)这会让原本相近的红色点在颜色空间里被拉得很远增强后就容易在红色区域产生斑点和断裂伪影。为了解决这个问题文章把 Hue 进行极化映射用正交的水平/垂直坐标来表示色相方向从而把原先“断开的红色两端”重新接起来。与红色断裂问题对应的第二个核心设计是 HVI 对极暗区域的处理。作者发现HSV 在非常暗的区域会形成所谓的black plane noise也就是黑色附近的颜色点分布很散增强时特别容易把暗区噪声当成真实纹理一起放大。为此HVI 引入了一个可训练的 darkness density 参数 (k)并通过一个基于亮度的intensity collapse function去自适应压缩低亮度区域的半径。直观理解就是亮度很低时把这些颜色点往中心“收拢”亮度逐渐升高时再让颜色分布逐渐展开。这样一来暗区中的杂乱噪声不会被无约束地放大而是先被压缩到更稳定、更紧凑的表示区域里。最终作者用、再与组合成 HVI 表示。也就是说HVI 最终由三个部分组成横向色彩分量、纵向色彩分量和强度分量。如果配合图片来理解图1第2页是最重要的总览图。图中上排展示了从sRGB → HSV → HVI的变化过程下排则展示了相应增强结果。作者想表达的非常清楚在 sRGB 中亮度与颜色耦合太强所以增强后容易整体偏色换到 HSV 后亮度恢复变得更自然但红色区域会出现明显的不连续噪声黑色附近也会产生伪影进一步变成 HVI 后红色区域的连续性恢复了黑色平面也被压缩最终输出图像在颜色自然性和亮度稳定性上都更好。图1其实就是整篇论文的核心思想图HVI 不是简单换颜色空间而是在 HSV 的“色相结构”和“暗区几何”上做了针对低照度任务的重构。图2第5页进一步说明了 HVI 在整个方法中的位置。这里可以看出HVI 不是独立存在的它是后续 CIDNet 的前端表示基础。作者先做HVI Transformation把输入图像分解成HV color map和intensity map然后送入一个双分支增强网络其中HV-branch 负责暗区去噪与色彩恢复I-branch 负责整体照明估计与亮度提升最后再通过Perceptual-inverse HVI Transformation (PHVIT)映射回 sRGB。换句话说HVI 的价值不只是“表示更好”而是它把低照度增强自然拆成了两件事颜色/结构处理和亮度处理。这也是为什么文章后面专门设计双分支网络去匹配 HVI 的解耦特性。图5第8页是最能说明 HVI 两个子设计分别在做什么的消融图。作者分别比较了 sRGB、HSV、只加极化、只加、完整 HVI 的结果。图中可以看到只用 HSV 时红色区域会出现明显黑点和断裂只加 polarization 时红色连续性有所改善但暗区问题仍未彻底解决只加时亮度关系更稳但颜色仍可能混淆甚至在其他区域引入色偏而当polarization 和同时使用时红色斑点和暗区伪影都得到明显抑制。对应的消融结果里完整 HVI-CIDNet 在 LOLv2-Real 上取得了24.111 PSNR / 0.871 SSIM / 0.108 LPIPS优于 sRGB、HSV 以及各单独组件版本。这说明 HVI 的两个设计并不是可替代关系而是互补关系一个负责“修色相拓扑”一个负责“稳暗区几何”。补充材料里的图7、图8、图9第12页对参数 (k) 的作用解释得很直观。图7画出了不同 (k) 取值下 (C_k) 随强度变化的曲线说明 (C_k) 本质上是一个从低亮度到高亮度的重映射函数图8展示了不同 (k) 下 HVI 空间外形的变化可以看到随着 (k) 增大低亮度部分的“底部几何”逐渐变宽图9则直接给出低照度图像在不同 (k) 下的 HV-map可见 (k) 越大暗区噪声越容易被放大细节与噪声之间的冲突也越明显。作者因此把 (k) 视为一个在不同数据集、不同网络条件下调节暗区信噪比的重要参数。也就是说HVI 不是固定死的颜色空间它带有一定的可学习、自适应属性。如果从“模块创新点”角度来总结HVI 的关键贡献可以概括成三句话。第一它抓住了低照度增强里一个常被忽略的问题颜色空间本身也会制造噪声尤其是 HSV 的红色断裂和黑平面问题。第二它不是靠更深的网络硬学而是先通过polarized HS learnable intensity collapse让输入表示更适合增强。第三它不仅自己有效而且还能作为plug-and-play 的颜色空间变换提升其他 LLIE 方法论文表3显示把 HVI 变换接到其他方法前后多个模型的 PSNR、SSIM、LPIPS 都得到改善其中 GSAD 的 PSNR 提升达3.562 dB。这说明 HVI 的价值具有一定通用性而不只是绑定 CIDNet 才有效。如果你想写成一段比较适合博客或论文模块介绍的表述可以直接用下面这段HVI 是针对低照度图像增强任务设计的一种新型颜色空间其核心思想是在 HSV 的基础上进一步重构色彩表示方式以同时抑制红色不连续噪声和极暗区域黑平面噪声。具体而言HVI 通过对 Hue 进行极化映射将原本在 HSV 中首尾断裂的红色区域投影到连续的水平—垂直平面中从而缩小相似红色之间的欧氏距离同时引入由可训练参数 (k) 控制的强度塌缩函数对低亮度区域的颜色半径进行自适应压缩使暗区噪声在增强前得到更稳定的几何约束。最终HVI 以 (\hat H)、(\hat V) 和 (I) 三个分量共同表征图像不仅保留了亮度与颜色解耦的优点还显著提升了低照度增强过程中的颜色自然性、暗区稳定性和整体视觉质量。三、核心代码核心代码的使用方式看章节四from einops import rearrange import torch import torch.nn as nn import torch.nn.functional as F import math __all__ [HVI] class LayerNorm(nn.Module): r LayerNorm that supports two data formats: channels_last (default) or channels_first. The ordering of the dimensions in the inputs. channels_last corresponds to inputs with shape (batch_size, height, width, channels) while channels_first corresponds to inputs with shape (batch_size, channels, height, width). def __init__(self, normalized_shape, eps1e-6, data_formatchannels_first): super().__init__() self.weight nn.Parameter(torch.ones(normalized_shape)) self.bias nn.Parameter(torch.zeros(normalized_shape)) self.eps eps self.data_format data_format if self.data_format not in [channels_last, channels_first]: raise NotImplementedError self.normalized_shape (normalized_shape,) def forward(self, x): if self.data_format channels_last: return F.layer_norm(x, self.normalized_shape, self.weight, self.bias, self.eps) elif self.data_format channels_first: u x.mean(1, keepdimTrue) s (x - u).pow(2).mean(1, keepdimTrue) x (x - u) / torch.sqrt(s self.eps) x self.weight[:, None, None] * x self.bias[:, None, None] return x class NormDownsample(nn.Module): def __init__(self, in_ch, out_ch, scale0.5, use_normFalse): super(NormDownsample, self).__init__() self.use_norm use_norm if self.use_norm: self.norm LayerNorm(out_ch) self.prelu nn.PReLU() self.down nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, stride1, padding1, biasFalse), nn.UpsamplingBilinear2d(scale_factorscale)) def forward(self, x): x self.down(x) x self.prelu(x) if self.use_norm: x self.norm(x) return x else: return x class NormUpsample(nn.Module): def __init__(self, in_ch, out_ch, scale2, use_normFalse): super(NormUpsample, self).__init__() self.use_norm use_norm if self.use_norm: self.norm LayerNorm(out_ch) self.prelu nn.PReLU() self.up_scale nn.Sequential( nn.Conv2d(in_ch, out_ch, kernel_size3, stride1, padding1, biasFalse), nn.UpsamplingBilinear2d(scale_factorscale)) self.up nn.Conv2d(out_ch * 2, out_ch, kernel_size1, stride1, padding0, biasFalse) def forward(self, x, y): x self.up_scale(x) x torch.cat([x, y], dim1) x self.up(x) x self.prelu(x) if self.use_norm: return self.norm(x) else: return x # Cross Attention Block class CAB(nn.Module): def __init__(self, dim, num_heads, bias): super(CAB, self).__init__() self.num_heads num_heads self.temperature nn.Parameter(torch.ones(num_heads, 1, 1)) self.q nn.Conv2d(dim, dim, kernel_size1, biasbias) self.q_dwconv nn.Conv2d(dim, dim, kernel_size3, stride1, padding1, groupsdim, biasbias) self.kv nn.Conv2d(dim, dim * 2, kernel_size1, biasbias) self.kv_dwconv nn.Conv2d(dim * 2, dim * 2, kernel_size3, stride1, padding1, groupsdim * 2, biasbias) self.project_out nn.Conv2d(dim, dim, kernel_size1, biasbias) def forward(self, x, y): b, c, h, w x.shape q self.q_dwconv(self.q(x)) kv self.kv_dwconv(self.kv(y)) k, v kv.chunk(2, dim1) q rearrange(q, b (head c) h w - b head c (h w), headself.num_heads) k rearrange(k, b (head c) h w - b head c (h w), headself.num_heads) v rearrange(v, b (head c) h w - b head c (h w), headself.num_heads) q torch.nn.functional.normalize(q, dim-1) k torch.nn.functional.normalize(k, dim-1) attn (q k.transpose(-2, -1)) * self.temperature attn nn.functional.softmax(attn, dim-1) out (attn v) out rearrange(out, b head c (h w) - b (head c) h w, headself.num_heads, hh, ww) out self.project_out(out) return out def autopad(k, pNone, d1): # kernel, padding, dilation Pad to same shape outputs. if d 1: k d * (k - 1) 1 if isinstance(k, int) else [d * (x - 1) 1 for x in k] # actual kernel-size if p is None: p k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad return p class Conv(nn.Module): Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation). default_act nn.SiLU() # default activation def __init__(self, c1, c2, k1, s1, pNone, g1, d1, actTrue): Initialize Conv layer with given arguments including activation. super().__init__() self.conv nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groupsg, dilationd, biasFalse) self.bn nn.BatchNorm2d(c2) self.act self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity() def forward(self, x): Apply convolution, batch normalization and activation to input tensor. return self.act(self.bn(self.conv(x))) def forward_fuse(self, x): Perform transposed convolution of 2D data. return self.act(self.conv(x)) # Intensity Enhancement Layer class IEL(nn.Module): def __init__(self, dim, ffn_expansion_factor2.66, biasFalse): super(IEL, self).__init__() hidden_features int(dim * ffn_expansion_factor) self.project_in nn.Conv2d(dim, hidden_features * 2, kernel_size1, biasbias) self.dwconv nn.Conv2d(hidden_features * 2, hidden_features * 2, kernel_size3, stride1, padding1, groupshidden_features * 2, biasbias) self.dwconv1 nn.Conv2d(hidden_features, hidden_features, kernel_size3, stride1, padding1, groupshidden_features, biasbias) self.dwconv2 nn.Conv2d(hidden_features, hidden_features, kernel_size3, stride1, padding1, groupshidden_features, biasbias) self.project_out nn.Conv2d(hidden_features, dim, kernel_size1, biasbias) self.Tanh nn.Tanh() def forward(self, x): x self.project_in(x) x1, x2 self.dwconv(x).chunk(2, dim1) x1 self.Tanh(self.dwconv1(x1)) x1 x2 self.Tanh(self.dwconv2(x2)) x2 x x1 * x2 x self.project_out(x) return x class HVI(nn.Module): def __init__(self, channels_in, channels_mid, heads8, use_biasFalse): super().__init__() c_a, c_b channels_in self.align_a self._make_proj(c_a, channels_mid, use_bias) self.align_b self._make_proj(c_b, channels_mid, use_bias) self.pre_norm LayerNorm(channels_mid) self.cross_interact CAB(channels_mid, heads, biasuse_bias) self.detail_refine IEL(channels_mid) staticmethod def _make_proj(cin, cout, use_bias): if cin cout: return nn.Identity() return Conv(cin, cout, 1) def forward(self, feats): feat_a, feat_b feats feat_a self.align_a(feat_a) feat_b self.align_b(feat_b) norm_a self.pre_norm(feat_a) norm_b self.pre_norm(feat_b) feat_a feat_a self.cross_interact(norm_a, norm_b) feat_a feat_a self.detail_refine(self.pre_norm(feat_a)) return feat_a四、添加方法4.1 修改一第一还是建立文件我们找到如下ultralytics/nn文件夹下建立一个目录名字呢就是Addmodules文件夹(用群内的文件的话已经有了无需新建)然后在其内部建立一个新的py文件将核心代码复制粘贴进去即可。4.2 修改二第二步我们在该目录下创建一个新的py文件名字为__init__.py(用群内的文件的话已经有了无需新建)然后在其内部导入我们的检测头如下图所示。4.3 修改三第三步找到如下文件ultralytics/nn/tasks.py进行导入和注册我们的模块(用群内的文件的话已经有了无需重新导入直接开始第四步即可)4.4 修改四找到文件到如下文件ultralytics/nn/tasks.py在其中的parse_model方法中添加即可根据周围代码进行定位即可如果不会入群内有视频讲解。# ------------------------------HVI-------------------------------- elif m is HVI: c1 [ch[x] for x in f] if c2 ! nc: c2 make_divisible(min(args[0],max_channels)*width, 8) args [c1, c2] # ------------------------------HVI--------------------------------到此就修改完成了大家可以复制下面的yaml文件运行,如果不会添加可联系作者入群观看视频教程。五、正式训练5.1 yaml文件训练信息YOLO26-Neck-HVI summary: 318 layers, 3,808,932 parameters, 3,808,932 gradients, 8.6 GFLOPs使用方式在代码中进行了注释大家需要仔细观看.# Ultralytics AGPL-3.0 License - https://ultralytics.com/license # Ultralytics YOLO26 object detection model with P3/8 - P5/32 outputs # Model docs: https://docs.ultralytics.com/models/yolo26 # Task docs: https://docs.ultralytics.com/tasks/detect # Parameters nc: 80 # number of classes end2end: True # whether to use end-to-end mode reg_max: 1 # DFL bins scales: # model compound scaling constants, i.e. modelyolo26n.yaml will call yolo26.yaml with scale n # [depth, width, max_channels] n: [0.50, 0.25, 1024] # summary: 260 layers, 2,572,280 parameters, 2,572,280 gradients, 6.1 GFLOPs s: [0.50, 0.50, 1024] # summary: 260 layers, 10,009,784 parameters, 10,009,784 gradients, 22.8 GFLOPs m: [0.50, 1.00, 512] # summary: 280 layers, 21,896,248 parameters, 21,896,248 gradients, 75.4 GFLOPs l: [1.00, 1.00, 512] # summary: 392 layers, 26,299,704 parameters, 26,299,704 gradients, 93.8 GFLOPs x: [1.00, 1.50, 512] # summary: 392 layers, 58,993,368 parameters, 58,993,368 gradients, 209.5 GFLOPs # YOLO26n backbone backbone: # [from, repeats, module, args] - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2 - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4 - [-1, 2, C3k2, [256, False, 0.25]] - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8 - [-1, 2, C3k2, [512, False, 0.25]] - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16 - [-1, 2, C3k2, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32 - [-1, 2, C3k2, [1024, True]] - [-1, 1, SPPF, [1024, 5, 3, True]] # 9 - [-1, 2, C2PSA, [1024]] # 10 # YOLO26n head head: - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 6], 1, HVI, [512]] # cat backbone P4 - [-1, 2, C3k2, [512, True]] # 13 - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 4], 1, HVI, [256]] # cat backbone P3 - [-1, 2, C3k2, [256, True]] # 16 (P3/8-small) - [-1, 1, Conv, [256, 3, 2]] - [[-1, 13], 1, HVI, [512]] # cat head P4 - [-1, 2, C3k2, [512, True]] # 19 (P4/16-medium) - [-1, 1, Conv, [512, 3, 2]] - [[-1, 10], 1, HVI, [1024]] # cat head P5 - [-1, 2, C3k2, [1024, True, 0.5, True]] # 22 (P5/32-large) - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)5.2 训练代码大家可以创建一个py文件将我给的代码复制粘贴进去配置好自己的文件路径即可运行。import warnings warnings.filterwarnings(ignore) from ultralytics import YOLO if __name__ __main__: model YOLO(替换你的模型配置文件yaml文件地址) # 如何切换模型版本, 上面的ymal文件可以改为 yolov11s.yaml就是使用的v11s, # 类似某个改进的yaml文件名称为yolov11-XXX.yaml那么如果想使用其它版本就把上面的名称改为yolov11l-XXX.yaml即可改的是上面YOLO中间的名字不是配置文件的 # model.load(yolo11n.pt) # 是否加载预训练权重,科研不建议大家加载否则很难提升精度 model.train(datar替换你的数据集配置文件地址, # 如果大家任务是其它的ultralytics/cfg/default.yaml找到这里修改task可以改成detect, segment, classify, pose cacheFalse, imgsz640, epochs150, single_clsFalse, # 是否是单类别检测 batch16, close_mosaic0, workers0, device0, optimizerSGD, # using SGD # resumeruns/train/exp21/weights/last.pt, # 如过想续训就设置last.pt的地址 ampFalse, # 如果出现训练损失为Nan可以关闭amp projectruns/train, nameexp, )5.3 训练过程截图五、本文总结到此本文的正式分享内容就结束了在这里给大家推荐我的YOLOv26改进有效涨点专栏本专栏目前为新开的平均质量分98分后期我会根据各种最新的前沿顶会进行论文复现也会对一些老的改进机制进行补充如果大家觉得本文帮助到你了订阅本专栏关注后续更多的更新~专栏链接