Retinex去雾算法Python实现:从SSR到MSRCR的完整指南

📅 发布时间:2026/9/28 15:00:55
Retinex去雾算法Python实现:从SSR到MSRCR的完整指南
简介这份资源面向图像处理初学者与算法爱好者提供基于Retinex理论的Python去雾实现用于改善雾天、低光照或高对比度环境下的图像质量帮助读者理解并复现经典图像增强算法。压缩包共3个文件均为py源码整体约2KB体量轻便便于快速阅读与调试其中包含算法核心实现、功能版本以及负责读取图像、设置参数、执行去雾并展示结果的主运行程序结构清晰适合直接运行验证效果。目前已有2195人学习下载说明该主题在图像增强方向具有较高关注度。通过源码读者可掌握多尺度滤波、局部亮度估计、对比度增强与大气散射模型去雾等关键环节并在此基础上进行二次开发与参数调优是入门Retinex去雾与图像增强的实用起点。1. Retinex 去雾到底在做什么一张雾图为什么需要“拆光”雾天拍出来的照片最直观的感受是发灰、发白、对比度低远处细节像被一层牛奶糊住。很多人第一反应是拉对比度、调曲线结果近处过曝、远处依然糊。Retinex 去雾的核心思路不是“加对比”而是把图像拆成照明分量和反射分量照明决定整体亮暗和雾的分布反射决定物体本来的颜色和纹理。把照明估计准了再把它压掉或均衡掉反射分量自然就透出来了。这个方向适合两类人一类是手里有雾天/低照度图像、想用 python 快速跑出可用结果的工程同学另一类是想搞懂去雾算法内部到底在算什么、方便后续做 retinex 改进的算法同学。标题里“Retinex_RETINEX去雾_python_去雾_去雾算法_”其实指向一个很具体的落地场景用 python 实现 Retinex 系列去雾从单尺度到多尺度再到带颜色恢复的变体。下面按“先立住原理、再动手复现、最后讲坑”的顺序推下去。2. Retinex 去雾的数学骨架与 python 选型从 SSR 到 MSRCR2.1 单尺度 Retinex 的公式到底在估什么Retinex 理论把图像 I 写成照明 L 和反射 R 的乘积I(x,y) L(x,y) · R(x,y)。去雾要的是 R所以对两边取对数log R log I − log L。照明 L 通常用高斯模糊对 I 做低通滤波来近似因为照明是缓变的反射是突变的。单尺度 RetinexSSR就是选一个高斯核尺度 σ对每个通道分别算import numpy as np import cv2 def ssr(img, sigma80): # img: float32, 范围 0~255BGR 三通道 img img.astype(np.float32) 1.0 retinex np.zeros_like(img) for i in range(3): # 高斯模糊估计照明分量 blur cv2.GaussianBlur(img[:, :, i], (0, 0), sigma) # 对数域相减得到反射分量 retinex[:, :, i] np.log10(img[:, :, i]) - np.log10(blur 1.0) # 线性拉伸到 0~255 for i in range(3): r retinex[:, :, i] r (r - np.min(r)) / (np.max(r) - np.min(r) 1e-6) * 255 retinex[:, :, i] r return retinex.astype(np.uint8)这段代码里 sigma 是唯一需要调的参数。sigma 小照明估计偏局部细节强但颜色容易失真sigma 大照明估计偏全局颜色稳但去雾力度弱。常见做法是 sigma 取 80 到 120 之间图像分辨率高就往上加。加 1.0 是为了避免 log(0)拉伸那一步是必须的否则输出是负值和小数没法直接存图。2.2 多尺度 MSR 为什么比单尺度稳单尺度最大的问题是尺度选不对就翻车选小了颜色发灰选大了雾去不干净。多尺度 RetinexMSR用三个高斯核分别算再按权重加权平均。典型权重是 1/3、1/3、1/3尺度常取 15、80、250。这样小尺度管细节、中尺度管过渡、大尺度管整体亮度比单尺度鲁棒得多。def msr(img, sigmas(15, 80, 250), weights(1/3, 1/3, 1/3)): img img.astype(np.float32) 1.0 retinex np.zeros_like(img) for sigma, w in zip(sigmas, weights): for i in range(3): blur cv2.GaussianBlur(img[:, :, i], (0, 0), sigma) retinex[:, :, i] w * (np.log10(img[:, :, i]) - np.log10(blur 1.0)) for i in range(3): r retinex[:, :, i] r (r - np.min(r)) / (np.max(r) - np.min(r) 1e-6) * 255 retinex[:, :, i] r return retinex.astype(np.uint8)参数说明sigmas 三个值分别对应细节、中观、全局分辨率 1080p 以上可以把 250 提到 300 到 400。weights 一般保持均等如果发现颜色偏得厉害可以把大尺度权重降到 0.25小尺度提到 0.4。注意 MSR 是逐通道做的通道之间独立拉伸会破坏颜色比例这是后面 MSRCR 要解决的问题。2.3 MSRCR 的颜色恢复因子怎么加MSR 跑完常见现象是颜色变淡、发白因为三个通道各自拉伸后比例乱了。带颜色恢复的多尺度 RetinexMSRCR在 MSR 结果上乘一个颜色恢复因子def msrcr(img, sigmas(15, 80, 250), weights(1/3, 1/3, 1/3), alpha125, beta46, G192, b30): msr_out msr(img, sigmas, weights).astype(np.float32) 1.0 img_f img.astype(np.float32) 1.0 # 颜色恢复因子利用原图通道比例约束输出 color_restore np.zeros_like(img_f) for i in range(3): color_restore[:, :, i] beta * (np.log10(alpha * img_f[:, :, i] 1.0) - np.log10(img_f.sum(axis2) 1.0)) out G * (np.log10(msr_out) color_restore) b out np.clip(out, 0, 255) return out.astype(np.uint8)alpha、beta、G、b 是经验参数alpha 控制非线性强度beta 控制颜色恢复力度G 和 b 是增益和偏置。常见做法是先用默认值跑一版如果颜色过饱和就降 beta如果整体偏暗就升 b。这一步是 retinex 改进里最常被动手的地方很多论文就是换颜色恢复函数或换照明估计方式。2.4 python 环境怎么配才不折腾跑上面代码只需要 numpy 和 opencv-python。用 pip 装python -m pip install numpy opencv-python如果用的是 vscode选好解释器后直接运行即可。注意 opencv 读图默认是 BGR不是 RGB通道顺序搞反会导致颜色恢复因子算错输出偏蓝或偏红。读图后如果要做通道相关操作先 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转一下算完再转回来存。这个坑几乎每个人都会踩一次。3. 用 python 跑通一条去雾流水线读图、处理、存图、批量3.1 最小可运行脚本与参数落点把前面函数拼起来一个完整脚本长这样import cv2 import numpy as np import os def dehaze_one(path, out_path, modemsrcr): img cv2.imread(path) if img is None: print(读图失败:, path) return if mode ssr: out ssr(img, sigma100) elif mode msr: out msr(img, sigmas(15, 80, 250)) else: out msrcr(img, sigmas(15, 80, 250), alpha125, beta46, G192, b30) cv2.imwrite(out_path, out) print(已保存:, out_path) if __name__ __main__: dehaze_one(fog.jpg, fog_dehazed.jpg, modemsrcr)逻辑说明读图后按 mode 选算法SSR 适合快速看效果MSR 适合对比度优先MSRCR 适合颜色保真优先。参数落点集中在 sigmas、alpha、beta、G、b 五个地方调参时一次只动一个否则说不清是谁起的作用。3.2 批量处理与命名规则实际项目里不会只处理一张图批量脚本要处理目录、过滤格式、避免覆盖def dehaze_batch(in_dir, out_dir, modemsrcr, exts(.jpg, .png, .jpeg)): os.makedirs(out_dir, exist_okTrue) for name in os.listdir(in_dir): if not name.lower().endswith(exts): continue in_path os.path.join(in_dir, name) out_path os.path.join(out_dir, dehazed_ name) dehaze_one(in_path, out_path, modemode)参数说明exts 控制处理哪些格式out_dir 自动创建输出文件名加前缀避免和原图混。如果目录里有非图像文件靠后缀过滤能挡掉大部分。注意 os.listdir 不保证顺序需要稳定顺序就加 sorted()。3.3 结果怎么判断好坏三个可量化指标肉眼看不准建议至少算三个指标对比度标准差、信息熵、平均梯度。标准差越大对比度越高信息熵越大细节越丰富平均梯度越大边缘越清晰。但要注意指标高不等于好看过度增强会把噪声也拉起来。常见做法是同时看指标和肉眼指标只用来横向对比不同参数。def metrics(img): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY).astype(np.float32) std gray.std() hist cv2.calcHist([gray.astype(np.uint8)], [0], None, [256], [0, 256]) hist hist / hist.sum() entropy -np.sum(hist * np.log2(hist 1e-12)) gx cv2.Sobel(gray, cv2.CV_32F, 1, 0) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1) avg_grad np.mean(np.sqrt(gx**2 gy**2)) return std, entropy, avg_grad这三个值没有绝对标准同一场景不同参数对比才有意义。如果 MSRCR 的熵比 MSR 高但标准差低说明细节多了但对比度弱了可以适当升 G 或降 beta。3.4 处理大图时的内存与速度取舍Retinex 逐通道做高斯模糊1080p 图大概几百毫秒4K 图会到几秒。如果批量处理上千张建议先把长边缩到 1600 以内再处理处理完如果需要原尺寸再放大。高斯核 sigma 越大opencv 内部核越大速度越慢。可以用 cv2.GaussianBlur 的 sigma 自动推导也可以手动指定 ksize 加速但 ksize 和 sigma 不匹配会引入振铃。常见做法是 sigma 固定ksize 设 0 让 opencv 自己算。4. 避坑与排查Retinex 去雾最常见的 5 个翻车现场4.1 输出全黑或全白现象跑完图要么一片黑要么一片白。原因对数域相减后没做拉伸或者拉伸时 min 和 max 相等导致除零。解决拉伸前加 1e-6 保护确认输入是 float32 且加了 1.0检查是否误把 uint8 直接送进 log。4.2 颜色严重偏蓝或偏红现象MSRCR 输出整体偏蓝。原因opencv 读图是 BGR颜色恢复因子按 RGB 顺序算通道错位。解决读图后转 RGB 再算或者把颜色恢复因子的通道顺序改成 BGR 对应。这个坑血泪经验最多建议在函数入口统一转 RGB出口再转回 BGR。4.3 去雾后噪声爆炸现象雾去掉了但暗部噪点全出来了。原因Retinex 本质是提升暗部噪声也被放大。解决处理前先做一次轻度双边滤波或非局部均值去噪或者处理后在暗部做 gamma 压缩。注意去噪要在去雾前做顺序反了噪声已经被放大再去噪会糊掉细节。4.4 天空区域出现过增强光晕现象天空和物体交界处出现白边或光晕。原因高斯模糊在强边缘处估计照明不准产生振铃。解决换引导滤波或双边滤波做照明估计它们能保边。如果不想换算法可以把 sigma 调大让照明估计更平滑代价是细节弱一点。4.5 批量处理时部分图报错退出现象批量跑一半崩了后面的图没处理。原因某张图读进来是 None或者通道数不是 3灰度图或带 alpha。解决读图后判断 img is None 就跳过判断 img.shape[2] 是否为 3不是就转换或跳过。批量脚本一定要加 try/except单张失败不影响整体。5. 进阶技巧把 Retinex 当预处理而不是终点Retinex 去雾跑通之后很多人会停在“能出图”这一步。但实际项目里Retinex 更适合当预处理后面接一个轻量增强或融合。我一般会做两件事一是把 MSRCR 输出和原图做加权融合权重 0.7 比 0.3保留一点原始氛围避免过度去雾导致画面假二是对 MSRCR 输出做一次 CLAHE限制对比度自适应直方图均衡只在亮度通道做进一步拉开局部对比。def post_enhance(dehazed, original, w0.7): # 去雾结果与原图融合保留部分原始氛围 fused cv2.addWeighted(dehazed, w, original, 1 - w, 0) # 转 LAB只对 L 通道做 CLAHE lab cv2.cvtColor(fused, cv2.COLOR_BGR2LAB) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) lab[:, :, 0] clahe.apply(lab[:, :, 0]) return cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)融合权重 w 是关键w 越大去雾越彻底w 越小越自然。CLAHE 的 clipLimit 控制对比度增强上限2.0 是常用起点tileGridSize 控制局部窗口8x8 适合大多数分辨率。注意 CLAHE 只在 L 通道做直接对 RGB 做会偏色。验证方法上我习惯固定三张典型图一张浓雾、一张薄雾、一张低照度。每次改参数都跑这三张对比指标和肉眼。如果三张都稳再上批量。这个习惯帮我省了很多后悔药因为单张调好的参数换场景经常翻车。最后说一个我自己的教训早期我总想把 sigma 调到最优后来发现 Retinex 的瓶颈不在 sigma而在照明估计的方式。高斯模糊只是最省事的近似换成引导滤波或基于暗通道的照明估计效果提升比调 sigma 明显得多。所以如果你已经跑通基础版下一步值得投入的方向是换照明估计器而不是继续磨参数。希望帮到你。本文还有配套的精品资源点击获取