简介:一份面向图像处理学习者与开发者的Python Retinex去雾算法代码包,针对低光照、雾天环境下图像模糊、对比度不足等问题,提供了完整的算法实现与可直接运行的示例。资源包整体仅2KB,共含3个Python源文件,分别承担算法核心逻辑、功能测试与主运行入口,完整覆盖从图像预处理、多尺度高斯滤波、局部亮度估计到结合大气散射模型去雾的各个关键步骤。目前已有2197人学习下载,代码结构紧凑、注释清晰,非常适合初学者对照理解Retinex理论从原理到落地的全过程。读者可借助该资源快速上手图像增强技术,依据实际场景调整参数或扩展功能,将其作为二次开发的基础参考工具。资源虽小,但涵盖了去雾算法的主要脉络,是学习与实战兼顾的实用资料。
1. Retinex 去雾算法:为什么我先抛弃了直方图均衡化
做过图像去雾的人大概都有同感:直方图均衡化一上来就把对比度拉满,雾是淡了,天空跟着变成水彩画,人脸肤色直接失真。我踩过几次之后才转向 Retinex 去雾算法,用 Python 把这套逻辑跑进真实项目里。Retinex 的出发点和直方图完全不同——它模拟人眼对“相对亮度”的感知,把图像拆成入射光和物体反射两部分,只保留反射分量。这个思路天然适合低光照、逆光、雾天这类场景,去雾的同时能把暗部细节提出来。这份资源里包含 Retinex_1.py、Retinex.py、Retinex_run.py 三个文件,覆盖了从核心算法到主程序调用的完整链路。适合两类人:一类是刚入门想在 Python 里跑通去雾算法的学生,另一类是想找一个能改参数、能嵌进预处理管线的从业者。
2. Retinex 理论先立住:SSR、MSR 与 MSRCR 的选择逻辑
2.1 从成像模型到反射分量:Retinex 到底在算什么
Retinex 这个名称来自 Retina 和 Cortex 的组合,核心假设很朴素:一张图像可以拆成入射光 L 和反射率 R 的乘积。
I(x, y) = L(x, y) × R(x, y)
人眼感知到的颜色和亮度,主要由反射率 R 决定,入射光 L 是环境光照带来的干扰。雾天恰恰是大气散射给 L 加了大量均匀的“白噪声”,所以去雾的本质可以转成:从 I 中估计 L,然后在对数域里把 L 减掉,得到的 ln R 就是增强后的结果。取对数的原因是把乘除运算变成加减,数值上更稳定,也更贴合人眼对亮度的非线性感知。代码里最常见的写法是:
def retinex_ssr(img, sigma): # img: 0-255 的 float32 图像,形状 (H, W, C) # sigma: 高斯滤波的尺度参数 blur = cv2.GaussianBlur(img, (0, 0), sigma) # 估计入射光 L log_img = np.log(np.float32(img) + 1.0) # 对数域 I log_blur = np.log(blur + 1.0) # 对数域 L log_reflect = log_img - log_blur # 对数域反射分量 R return log_reflect这段代码里最容易忽略的是+ 1.0这个偏移量。像素值可能为 0,直接取对数会出现负无穷,加 1 是为了保证对数运算有定义,同时让输出范围保持在合理区间。高斯滤波的核大小没有显式给出,OpenCV 在传入(0, 0)时会根据 sigma 自动计算,sigma 越大,入射光估计得越平滑,增强效果越柔和。
2.2 三个文件的分工:Retinex_1.py、Retinex.py、Retinex_run.py 怎么配合
从压缩包的文件命名和项目描述来看,这三个文件的职责是比较标准的 Retinex 工程结构。我拿到任何去雾代码包,第一步永远是先读文件、理清谁是被调用方,谁是入口。
| 文件 | 推测角色 | 在链路里的位置 |
|---|---|---|
| Retinex_1.py | 基础版单尺度 SSR 实现 | 包含最核心的 log 域差分逻辑 |
| Retinex.py | 完整 Retinex 算法(可能是 MSR/MSRCR) | 多尺度加权、色彩恢复因子都在这里 |
| Retinex_run.py | 主程序入口 | 负责读图、初始化参数、调用算法、保存结果 |
常见做法是Retinex_run.py在最外层,Retinex.py提供类或函数封装,Retinex_1.py则可能是早期版本或某个具体功能的独立实现。你在二次开发时可以只改Retinex_run.py里的输入输出路径和参数,核心算法不要动。如果你要换算法变体,比如从 SSR 换到 MSRCR,就到Retinex.py里追加对应函数。
2.3 SSR、MSR、MSRCR 三选一:参数表和适用场景
单尺度 Retinex 很好理解,但对不同尺寸的纹理响应不一致;多尺度 Retinex 把多个 sigma 的 SSR 结果做加权,取长补短。MSRCR 则是在 MSR 基础上加了颜色恢复因子,解决多尺度处理后颜色被冲淡、整体偏灰的问题。
| 算法 | sigma 数量 | 颜色处理 | 适合场景 | 代价 |
|---|---|---|---|---|
| SSR | 1 个 | 无特殊处理 | 光照不均、单尺度细节增强 | 边缘光晕明显 |
| MSR | 3 个左右 | 无 | 通用去雾、中等雾浓度 | 颜色饱和度下降 |
| MSRCR | 3 个左右 | 色彩恢复因子 C | 雾天、低光照、色彩要求高 | 计算量大,参数敏感 |
如果是第一次跑这份代码,我建议先从 Retinex_1.py 的单尺度版本起步,确认链路通顺,再切到 Retinex.py 的多尺度版本。直接上 MSRCR 会面临一堆参数调整,容易把精力耗在调参上,而不是理解算法本身。雾浓度中等、背景以景物为主时,MSRCR 效果最好;纯天空占比很大的图,MSRCR 容易出现色彩过饱和,这是它原理决定的。
3. 把 Retinex_run.py 跑起来:依赖安装、参数设置与第一张去雾图
3.1 环境准备:Python、numpy、opencv 的版本匹配
这套代码基于 Python 实现,核心依赖是 numpy 和 opencv-python。新手最容易在安装阶段翻车,先确认 Python 版本是 3.8 以上,然后一次性装齐基础库。numpy 的安装没什么玄学,opencv-python 的导入名是 cv2,不是 opencv,这两个坑会让新手卡在第一步。
python -m pip install numpy opencv-python matplotlib装完在 Python 交互环境里验证一下:
python -c "import numpy, cv2; print(numpy.__version__, cv2.__version__)"只要不报 ModuleNotFoundError 就说明环境没问题。这里不推荐用 conda 强行指定 numpy 版本,最新版对多数 Retinex 项目足够。如果后续发现代码里用了较老的 numpy API,再降版本不迟。opencv 的读取函数返回的是 BGR 顺序,这一点在第 4 章会详细展开,它几乎是去雾项目里最常见的一个坑。
3.2 修改输入的三个关键点:图片路径、scale 列表、输出目录
Retinex_run.py 主程序通常会有这样几个可配置区域:输入图片路径、算法参数、输出路径。我第一次跑类似项目时习惯用一个简单参数区把变量集中管理,比如 image_path、output_path、scales。多尺度 Retinex 的核心参数是 scales,也就是 sigma 列表。通常取三个尺度,小尺度提取局部细节,大尺度估计全局光照分布,中间尺度兼顾两者。
# Retinex_run.py 里主要调整的参数区 image_path = "./foggy_input.jpg" # 输入有雾图像路径 output_path = "./output.jpg" # 去雾结果保存路径 scales = [15, 80, 250] # 多尺度 Retinex 的 sigma 列表 gain = 1.2 # 增益系数,控制整体亮度 offset = 0.0 # 偏移量,控制输出亮度整体平移scales 的选择直接决定效果:三个尺度差距越大,细节增强越强,但也越容易出现光晕。gain和offset在 MSR 公式里用于把对数域结果线性映射回显示范围,gain = 1.2是经验值,偏暗的图可以把 gain 提到 1.5,偏亮的图压到 1.0。改完这些参数直接运行主程序:
python Retinex_run.py如果程序内部已经写死路径,就手动把 image_path 变量替换成你自己的图片路径。常见的做法是把雾图放在与脚本同目录下,用相对路径避免转义问题,Windows 下尤其注意路径分隔符,写./foggy_input.jpg比写绝对路径省心。
3.3 运行脚本与结果验证:怎么看效果好不好
程序正常跑完后,打开输出图,先看三个地方。
第一看整体亮度是否合理。Retinex 去雾不等于无脑提亮,雾天图像通常整体偏低,增强后应有明显层次感,但天空区域不能过曝。第二看暗部纹理,比如树叶缝隙、建筑边缘这些地方是不是恢复了细节。第三看有没有大面积灰白色残留,如果图像看起来像是蒙了一层纱,说明入射光估计偏高,需要减小 sigma 或增大 gain。
# 快速验证脚本:对比原图和输出图的灰度直方图分布 import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread("output.jpg", cv2.IMREAD_GRAYSCALE) hist = cv2.calcHist([img], [0], None, [256], [0, 256]) plt.plot(hist) plt.xlim([0, 256]) plt.show()直方图是最直观的验证工具。雾图的直方图通常集中在中间灰度段且峰型尖锐,去雾后直方图应该被拉伸开,左右两端都有分布。如果直方图仍然挤在中间,说明算法没有生效,基本就是参数没匹配上或者输入图像读取失败。这一步能帮你快速判断是算法问题还是数据问题。
4. Retinex 去雾常见问题与排查:五条踩坑记录
4.1 颜色偏紫偏蓝:通道顺序背锅
现象:跑完 Retinex 后,图像整体发蓝发紫,尤其皮肤和天空区域颜色完全不对。这不是算法本身的问题,也不是 RGB 和 HSV 色彩空间的差异,而是 opencv 读取图片默认用 BGR 顺序,很多 Retinex 代码是按 RGB 顺序写的。
原因:cv2.imread 读进来的图像通道顺序是 BGR,直接按 RGB 通道处理会导致蓝通道被当成红通道增强,色偏自然跑偏。
解决:在处理前先转换通道顺序,处理完再转回去,或者全程按 BGR 处理。一般我是在读取后立刻执行 cvtColor:
img = cv2.imread("foggy_input.jpg") img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 对 img_rgb 做 Retinex 处理 … result_bgr = cv2.cvtColor(result_rgb, cv2.COLOR_RGB2BGR) cv2.imwrite("output.jpg", result_bgr)这是一条血泪经验:只要结果颜色不对,先怀疑通道顺序,不用急着调 Retinex 参数。
4.2 暗部全黑或亮部过曝:对数域数学的锅
现象:输出图暗部区域黑成一片,完全没有细节;亮部区域又白到刺眼。看起来像是对比度被过度拉伸。
原因:对数域差分后,ln R 的数值范围可能很大,直接归一化到 0-255 时,低值部分被压缩到肉眼不可分辨的范围。很多实现直接用 min-max 归一化,但 outlier 会把有效数据挤到很小的一段区间。
解决:不要直接 min-max,改用 clip 配合线性拉伸。我这边的做法是把结果限制在 0 到 1 之间,然后乘以 255,再做一次饱和处理:
# 对对数域结果做饱和线性映射 def linear_stretch(gray, low=0.05, high=0.95): # gray: log 域反射分量 low_val, high_val = np.percentile(gray, [low * 100, high * 100]) stretched = (gray - low_val) / (high_val - low_val + 1e-6) return np.clip(stretched, 0, 1) result = linear_stretch(log_reflect) result = (result * 255).astype(np.uint8)percentile 截断两个端的离群值,比 min-max 稳得多。low 和 high 控制截断比例,0.05 和 0.95 是通用配置,图像过暗可以调成 0.01 和 0.99,让更多灰度参与映射。
4.3 光照边缘出现白色光晕:高斯尺度配错了
现象:增强后的图里,物体边缘会有一圈白色的光晕,类似 HDR 过度处理的鬼影。这在 Retinex 里是常见翻车现场。
原因:入射光 L 是通过高斯滤波估计的,如果 sigma 过小,高斯核范围小于物体尺寸,物体内部区域会被误判为入射光的一部分;而 sigma 过大,入射光在边缘处过渡太慢,物体与背景交界处就会出现过渡带,视觉上就是光晕。
解决:光晕集中在边缘,说明 sigma 的尺度分层不合理。从 15、80、250 这种阶梯式设置开始,逐步缩小尺度差就会好很多。代码里如果高斯滤波是手动指定核大小的,确保核大小是奇数且接近 sigma 的 6 倍,否则滤波效果与预期不一致。
# 手动指定高斯核大小时参考这个关系 ksize = int(sigma * 6) if ksize % 2 == 0: ksize += 1 blur = cv2.GaussianBlur(img, (ksize, ksize), sigma)4.4 结果与原图几乎没差或更差
现象:跑了算法,输出图看起来只是亮度变了一点,雾气根本没有消除。
原因:说明入射光估计值接近原始图像本身,ln R 被压缩到接近 0。这种情况通常出现在 sigma 设置过小、增强强度不足,或者输入图像已经是 0-1 浮点图,而代码仍然按 0-255 处理。浮点范围和整型范围不匹配是常见的隐蔽问题。
解决:先打印输入图像的数据范围和 dtype,确认是 uint8 还是 float32。如果是 float 图,先转成 uint8 再走算法。sigma 如果只有个位数值,把它拉到至少 15 以上。另外确认 Retinex_run.py 里不是拿原图覆盖了输出图,这种低级错误出现过不止一次。
4.5 运行慢到怀疑人生:多尺度循环的坑
现象:大图 4000 × 3000,跑一次要几十秒甚至几分钟,连续跑十几张图根本没法用。
原因:每个通道每个尺度都要做高斯滤波,numpy 处理和图像尺寸成正比,多尺度叠加后计算量线性上涨。
解决:核心思想是控制输入尺寸。先按长边缩放到 1024 以内,处理完再 resize 回来。还有一种常见做法是把图像转成 float32 后,对每个尺度先用小尺寸估计入射光,再放大,但这个方案实现复杂。先做缩放是性价比最高的解法:
img = cv2.imread("large_fog.jpg") h, w = img.shape[:2] scale = min(1024 / max(h, w), 1.0) if scale < 1.0: img_resized = cv2.resize(img, (int(w * scale), int(h * scale))) # 对 img_resized 做 Retinex 处理 … result = cv2.resize(result, (w, h))5. 从“能跑”到“好用”:Retinex 与暗通道先验的取舍和改造方向
5.1 Retinex 和暗通道先验:两张牌怎么选
暗通道先验是另一条去雾路线,由何恺明提出,理论依据是有雾图像暗通道亮度偏高,通过最小通道估计透射率,再反推清晰图像。它和 Retinex 的差别在于是“物理模型”还是“感知模型”。暗通道先验在浓雾场景、远景建筑上效果很强悍,但是天空区域往往过暗或产生色偏,因为天空不满足“暗通道亮度接近零”的假设。Retinex 没有这个前提,雾天、低光照、逆光都能用,但增强结果不保证物理正确,更像是视觉增强。
| 对比项 | Retinex | 暗通道先验 |
|---|---|---|
| 理论基础 | 人眼感知,入射光/反射率 | 大气散射物理模型 |
| 适合场景 | 低光照、逆光、中低浓度雾 | 均匀浓雾、远景清晰化 |
| 天空表现 | 较自然,可能饱和度偏高 | 容易出现偏色或过暗 |
| 参数复杂度 | 尺度参数和增益参数 | 大气光与透射率阈值 |
| 运行速度 | 多尺度滤波,速度一般 | 依赖导向滤波精化 |
我现在的选择标准是:雾天图像偏灰但光照均匀,用暗通道先验;逆光、夜视、光照分布不均,用 Retinex。更多时候是把 Retinex 作为预处理去雾步骤,后续再接检测或识别模型。
5.2 给 Retinex 加色彩恢复:MSRCR 实现里的两个细节
多尺度 Retinex 会牺牲彩色饱和度,原因是三个通道被独立拉伸,通道间比例关系被破坏。MSRCR 的做法是加一个色彩恢复因子,让颜色比例回归。常见实现是计算彩色恢复系数 C:
def msrcr(img, scales=[15, 80, 250], beta=46.0, alpha=125.0): img = img.astype(np.float32) / 255.0 msr = np.zeros_like(img) for sigma in scales: blur = cv2.GaussianBlur(img, (0, 0), sigma) msr += np.log(img + 1e-6) - np.log(blur + 1e-6) msr /= len(scales) # 色彩恢复因子 sum_rgb = np.sum(img, axis=2, keepdims=True) + 1e-6 color_restore = beta * (np.log(alpha * img + 1e-6) - np.log(sum_rgb + 1e-6)) result = msr * color_restore # 截断归一化 result = (result - np.min(result)) / (np.max(result) - np.min(result) + 1e-6) result = np.clip(result * 255, 0, 255).astype(np.uint8) return result两个细节值得注意。第一个是beta和alpha的取值组合:beta 取 46、alpha 取 125 是论文里给出的经验值,直接改这两个数会显著影响色彩饱和度,调参时每次只动一个变量。第二个是代码加了1e-6而非1.0,因为输入被归一化到 0-1 之后,加 1 会把对数域数值整体推偏,加一个足够小的 epsilon 就能既避免 log 0,又不会改变数值分布。
5.3 改造思路:把 Retinex 塞进自己的预处理管线
Retinex 跑通以后,最常见的改造方向是把它接到目标检测或图像配准流程前面。我一般会把去雾脚本打包成一个函数,输入路径输出数组,后面直接接模型推理,而不是每次跑完整流程再读图。还有两个实用的二次开发切入点:一个是把 gain 和 offset 改成根据图像亮度自动计算,比如计算灰度均值后决定拉伸强度;另一个是把关键参数写进配置文件,而不是改脚本源码,方便不同场景切换参数组。
这种封装方式最大的好处是快速对比不同参数组的输出,为每个场景固定一套参数。我经手的项目里,白天雾和夜间雾的参数完全是一组截然不同的值,写死参数等于把后路堵死。
6. 批量去雾与视频逐帧处理:把 Retinex 用到真实数据上的一个技巧
6.1 批量跑一个文件夹的雾图
单张图跑通只是开始,真实任务往往是几十张甚至几百张图。批量处理的思路很简单:遍历输入目录,对每张图调用同一个去雾函数,输出到带后缀的结果文件。注意一点,不同图片的亮度分布不同,固定参数对一部分图效果很好,对另一部分图会过曝,批量处理前先抽样三张图确认参数稳定性。
import os input_dir = "./fog_images" output_dir = "./results" os.makedirs(output_dir, exist_ok=True) for fname in os.listdir(input_dir): if not fname.lower().endswith((".jpg", ".png", ".jpeg")): continue path = os.path.join(input_dir, fname) img = cv2.imread(path) enhanced = msrcr(img, scales=[15, 80, 250]) # 复用上一节 out_name = os.path.splitext(fname)[0] + "_enhanced.jpg" cv2.imwrite(os.path.join(output_dir, out_name), enhanced)6.2 视频逐帧去雾与自适应参数
视频去雾在安防监控里很常见,但逐帧调用处理函数会遇到两个问题:一是稳定性,参数抖动会让画面亮度闪烁,一眼能看出来;二是性能,1080p 视频逐帧跑多尺度卷积帧率上不去。
我一般会先缩放到 720p,再把每 3 帧取 1 帧做关键帧参数计算,中间帧沿用上一帧参数组,这样既稳定又省算力。视频去雾算法的参数不是越激进越好,亮度的连续变化比单帧的极限画质更重要。
从那以后我每次跑去雾脚本,都会先拿三张典型图试参数、看直方图、再决定是否批量,而不是拿到代码就全量跑。这个方法帮我少走了很多弯路,希望帮到你。压缩包里的 Retinex_1.py、Retinex.py、Retinex_run.py 已经把整条链路搭好,直接从第三章的环境准备开始复现就行。
本文还有配套的精品资源,点击获取