扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

基于OpenCV的自适应阴影校正

01/20 10:17
754
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

想象一下,在阳光明媚的日子里拍摄了一张完美的风景照,却发现刺眼的阴影遮蔽了关键细节,扭曲了色彩。同样,在计算机视觉项目中,阴影也会干扰目标检测算法,导致结果不准确。阴影是图像处理中常见的干扰因素,它会引入不均匀的光照,从而影响图像的美观性和功能分析。

本文中,我们将正面应对这一挑战,采用OpenCV实现阴影校正的实用方法。我们的方法利用多尺度 Retinex (MSR)进行光照归一化,并结合LAB和HSV色彩空间中的自适应阴影掩蔽。该技术不仅能有效去除阴影,还能保留自然的色彩和纹理。

阴影如何影响图像外观

在深入探讨解决方案之前,我们先来了解一下阴影及其在图像处理中的挑战。当物体阻挡光线时,就会形成阴影,从而降低物体表面的光照强度。这会使该区域变暗,但不会改变物体的固有属性。

需要考虑的关键点

阴影影响照明,但不影响反射率(物体的真实颜色和材质)。

同一个物体在阴影中可能看起来很暗,在光线下可能看起来很亮,这会令观看者和算法感到困惑。

阴影有软有硬,有的阴影过渡平滑,有的阴影边缘锐利,需要精确检测以防止出现瑕疵。

简单地提亮图像并不能修复阴影;它可能会导致高光过曝或色彩失真。有效的校正方法是将光照与反射区分开来。图像模型为I = R × L,其中I表示观测图像,R表示反射率,L表示光照。为了恢复 R,需要估计并归一化 L,通常为了保证稳定性而使用对数。

现实世界的例子表明,阴影会导致光照不均匀,而我们的方法通过隔离和调整这些组成部分来纠正这种情况。

这些图像展示了阴影造成的光线不均匀,指导我们采取方法来保持色彩的真实性。

理解基本原理

在深入代码之前,让我们先打好关键概念的基础。

色彩空间详解

图像通常以 RGB(红、绿、蓝)表示,但对于去除阴影,其他颜色空间更合适,因为它们将亮度(明度)与色度(颜色)分开。

LAB 色彩空间:这是一个感知上均匀的色彩空间,其中L代表亮度(0-100),A代表绿-红轴,B代表蓝-黄轴。它非常适合阴影校正,因为我们可以独立地操作 L 通道而不会影响颜色。在 OpenCV 中,我们使用cv.cvtColor(img, cv.COLOR_BGR2LAB)进行转换。

HSV 色彩空间:色相 (H)、饱和度 (S) 和明度 (V)。阴影通常表现为饱和度和明度较低的区域。我们使用饱和度 (S) 通道来帮助识别阴影,因为阴影往往会降低颜色的饱和度。

切换到这些空间可以让我们更精确地瞄准阴影。

Retinex理论基础

雷蒂内克斯理论由埃德温·兰德于20世纪70年代提出,它模拟了人类视觉系统如何实现颜色恒常性,即在不同光照条件下始终如一地感知颜色,这与我们的眼睛适应不同光照而不改变对物体颜色的感知方式非常相似。其核心思想是,图像可以分解为反射率(物体固有属性,例如表面材质)和光照(光照变化,例如阴影或高光)。

多尺度 Retinex (MSR)通过在多个尺度上应用高斯模糊来估计光照,从而扩展了这一方法,其灵感来源于人类视觉中的多分辨率处理。对于每个尺度:

模糊图像以近似光照分量并消除局部变化。

从原始图像的对数中减去模糊图像的对数(为了处理光照效果的乘法性质,因为对数将乘法转换为加法,以便更容易分离)。

跨尺度平均以获得稳健的估计,平衡局部和全局修正。

这样可以得到一幅图像增强版,阴影减少,动态范围扩大,低光区域的对比度也更好。在我们的代码中,为了提高效率,我们仅对 L 通道应用 MSR,重点关注阴影主要影响亮度的亮度部分。

阴影检测挑战

简单的亮度阈值分割方法行不通,因为阴影的强度各不相同(从细微的渐变到深邃的黑暗),而且可能与本身较暗的物体无缝融合,导致误报或漏检。我们需要一种能够考虑上下文的自适应方法:

将低亮度(L < 阈值)与低饱和度(S < 阈值)结合起来,因为阴影不仅会使颜色变暗,还会通过降低光强度来降低颜色饱和度,而不会添加新的色调。

使用形态学操作,例如闭运算来填充掩模中的小间隙,以及开运算来去除孤立的噪声斑点,来改进掩模,从而提高精度和连续性。

使用高斯模糊平滑蒙版,以实现无缝融合,防止校正后的图像中出现可见的边缘或光晕。

这样可以确保我们只校正阴影区域,而不会过度处理图像的其余部分,从而保持自然过渡并避免伪影。

阴影移除流程概述

我们的处理流程逐步处理图像,以实现有效的阴影校正:

加载和预处理:读取图像并调整大小以加快预览速度(例如,50% 缩放)。

颜色空间转换:转换为 LAB(用于亮度/色度)和 HSV(用于饱和度)。

计算 Retinex:对 L 通道应用多尺度 Retinex,以创建光照归一化版本。

生成阴影蒙版:对归一化的 L 和 S 使用自适应条件,然后模糊以使其柔和。

去除阴影:将原始 L 色与 Retinex L 色在阴影区域进行混合。对于 A/B 通道,请使用预估的背景颜色进行混合,以避免颜色偏移。

交互式调优:使用 OpenCV 滑块实时调整强度、灵敏度和模糊度。

显示结果:并排显示原始图像、蒙版图像和校正后的图像。

这种方法是自适应的,这意味着它可以对图像内容做出响应,并且参数允许针对各种光照条件进行定制。

深入代码:逐步解析

让我们来分析一下这个Python脚本。我们假设你已经安装了OpenCV和NumPy(pip install opencv-python numpy)。

先决条件

Python 3.x

OpenCV (cv2)

NumPy(np)

核心功能

多尺度光照归一化(Retinex处理)

该函数计算亮度通道上的多尺度 Retinex 值。

def multiscale_retinex(L):    scales = [31, 101, 301]  # Small, medium, large scales for different illumination sizes    retinex = np.zeros_like(L, dtype=np.float32)    for k in scales:        blur = cv.GaussianBlur(L, (k, k), 0)  # Blur to estimate illumination        retinex += np.log(L + 1) - np.log(blur + 1)  # Log subtraction for reflectance    retinex /= len(scales)  # Average across scales    retinex = cv.normalize(retinex, None, 0, 255, cv.NORM_MINMAX)  # Scale to 0-255    return retinex

为什么选择这些尺度?较小的卷积核可以捕捉精细的细节,较大的卷积核可以处理大范围的阴影。+1 可以避免 log(0) 问题。归一化确保输出与输入范围相匹配。

自适应阴影检测和掩模生成

创建二值阴影蒙版并柔化它。

def compute_shadow_mask_adaptive(L, S, sensitivity=1.0, mask_blur=21):    shadow_cond = (L < 0.5 * sensitivity) & (S < 0.5)  # Low brightness and saturation    mask = shadow_cond.astype(np.float32)  # 0 or 1 float    mask_blur = mask_blur if mask_blur % 2 == 1 else mask_blur + 1  # Ensure odd for Gaussian    mask = cv.GaussianBlur(mask, (mask_blur, mask_blur), 0)  # Soften edges    return mask

灵敏度调节亮度阈值,从而可以针对微弱或黑暗的阴影进行微调。模糊效果可以防止生硬的过渡。

掩模引导阴影去除和色彩保留

校正的核心在于:优化蒙版并混合通道。

def remove_shadows_adaptive_v3(L, A, B, L_retinex, strength=0.9, mask=None, mask_blur=31):    kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, (7, 7))  # Elliptical kernel for morphology    shadow_mask = cv.morphologyEx(mask, cv.MORPH_CLOSE, kernel)  # Close gaps    shadow_mask = cv.morphologyEx(shadow_mask, cv.MORPH_OPEN, kernel)  # Remove noise    shadow_mask = cv.dilate(shadow_mask, kernel, iterations=1)  # Expand slightly    shadow_mask = cv.GaussianBlur(shadow_mask, (mask_blur, mask_blur), 0)  # Smooth    mask_smooth = np.power(shadow_mask, 1.5)  # Non-linear for stronger effect in core shadows     L_final = (1 - strength * mask_smooth) * L + (strength * mask_smooth) * L_retinex  # Blend L    L_final = np.clip(L_final, 0, 255)  # Prevent overflow     mask_inv = 1 - mask_smooth  # Non-shadow areas    A_bg = np.sum(A * mask_inv) / (np.sum(mask_inv) + 1e-6)  # Average A in non-shadows    B_bg = np.sum(B * mask_inv) / (np.sum(mask_inv) + 1e-6)  # Average B     A_final = (1 - strength * mask_smooth) * A + (strength * mask_smooth) * A_bg  # Blend A/B    B_final = (1 - strength * mask_smooth) * B + (strength * mask_smooth) * B_bg     return L_final, A_final, B_final

形态学操作可优化蒙版:闭合操作填充孔洞,打开操作去除斑点,膨胀操作确保覆盖范围。幂函数使深阴影区域的混合效果更显著。A/B 对比的背景颜色估计可保持色度。

轨道杆回调实用程序

这是 OpenCV 要求的用于轨道条回调的占位符。

def nothing(x):    pass

完整代码:

入口点处理图像加载、设置和交互循环。

import cv2 as cvimport numpy as np # Retinex (compute once) def multiscale_retinex(L):    scales = [31, 101, 301]    retinex = np.zeros_like(L, dtype=np.float32)    for k in scales:        blur = cv.GaussianBlur(L, (k, k), 0)        retinex += np.log(L + 1) - np.log(blur + 1)    retinex /= len(scales)    retinex = cv.normalize(retinex, None, 0, 255, cv.NORM_MINMAX)    return retinex# Adaptive Shadow Maskdef compute_shadow_mask_adaptive(L, S, sensitivity=1.0, mask_blur=21):    shadow_cond = (L < 0.5 * sensitivity) & (S < 0.5)    mask = shadow_cond.astype(np.float32)    mask_blur = mask_blur if mask_blur % 2 == 1 else mask_blur + 1    mask = cv.GaussianBlur(mask, (mask_blur, mask_blur), 0)    return mask#  Shadow Removal def remove_shadows_adaptive_v3(L, A, B, L_retinex, strength=0.9, mask=None, mask_blur=31):    kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, (7, 7))    shadow_mask = cv.morphologyEx(mask, cv.MORPH_CLOSE, kernel)    shadow_mask = cv.morphologyEx(shadow_mask, cv.MORPH_OPEN, kernel)    shadow_mask = cv.dilate(shadow_mask, kernel, iterations=1)    shadow_mask = cv.GaussianBlur(shadow_mask, (mask_blur, mask_blur), 0)    mask_smooth = np.power(shadow_mask, 1.5)    L_final = (1 - strength * mask_smooth) * L + (strength * mask_smooth) * L_retinex    L_final = np.clip(L_final, 0, 255)    mask_inv = 1 - mask_smooth    A_bg = np.sum(A * mask_inv) / (np.sum(mask_inv) + 1e-6)    B_bg = np.sum(B * mask_inv) / (np.sum(mask_inv) + 1e-6)    A_final = (1 - strength * mask_smooth) * A + (strength * mask_smooth) * A_bg    B_final = (1 - strength * mask_smooth) * B + (strength * mask_smooth) * B_bg    return L_final, A_final, B_finaldef nothing(x):    pass#  Main if __name__ == "__main__":    img = cv.imread("image.jpg")    if img is None:        raise IOError("Image not found")    scale = 0.5    img_preview = cv.resize(img, None, fx=scale, fy=scale, interpolation=cv.INTER_AREA)    lab = cv.cvtColor(img_preview, cv.COLOR_BGR2LAB).astype(np.float32)    L, A, B = cv.split(lab)    L_retinex = multiscale_retinex(L)    hsv = cv.cvtColor(img_preview, cv.COLOR_BGR2HSV).astype(np.float32)    S = hsv[:, :, 1] / 255.0    cv.namedWindow("Shadow Removal", cv.WINDOW_NORMAL)    cv.createTrackbar("Strength", "Shadow Removal", 90, 200, nothing)    cv.createTrackbar("Sensitivity", "Shadow Removal", 90, 200, nothing)    cv.createTrackbar("MaskBlur", "Shadow Removal", 31, 101, nothing)    while True:        strength = cv.getTrackbarPos("Strength", "Shadow Removal") / 100.0        sensitivity = cv.getTrackbarPos("Sensitivity", "Shadow Removal") / 100.0        mask_blur = cv.getTrackbarPos("MaskBlur", "Shadow Removal")        mask_blur = max(3, mask_blur)        mask_blur = mask_blur if mask_blur % 2 == 1 else mask_blur + 1        mask = compute_shadow_mask_adaptive(L / 255.0, S, sensitivity, mask_blur)        L_final, A_final, B_final = remove_shadows_adaptive_v3(            L, A, B, L_retinex, strength, mask, mask_blur        )        lab_out = cv.merge([L_final, A_final, B_final]).astype(np.uint8)        result = cv.cvtColor(lab_out, cv.COLOR_LAB2BGR)        #  BUILD RGB VIEW         orig_rgb = cv.cvtColor(img_preview, cv.COLOR_BGR2RGB)        mask_rgb = cv.cvtColor((mask * 255).astype(np.uint8), cv.COLOR_GRAY2RGB)        result_rgb = cv.cvtColor(result, cv.COLOR_BGR2RGB)        combined_rgb = np.hstack([orig_rgb, mask_rgb, result_rgb])        # Convert back so OpenCV shows correct colors        combined_bgr = cv.cvtColor(combined_rgb, cv.COLOR_RGB2BGR)        cv.imshow("Shadow Removal", combined_bgr)        key = cv.waitKey(30) & 0xFF        if key == 27 or cv.getWindowProperty("Shadow Removal", cv.WND_PROP_VISIBLE) < 1:            break     cv.destroyAllWindows()

要点:

调整大小可以加快预览处理速度。

为了提高效率,Retinex 在循环外只计算一次。

循环会在滑块变化时更新,重新计算掩码并进行校正。

显示原始图像、掩膜图像(灰度图像,RGB 格式)和结果图像以进行比较。

运行代码和调整参数

安装说明

将代码保存为 .py 格式。

将“image.jpg”替换为您的图像路径(JPEG、PNG 等)。

运行:python shadow_removal.py。

将出现一个带有滑块和并排视图的窗口。

交互式演示

强度(0-2.0):控制混合强度。数值越高,校正效果越强,但出现伪影的风险也越大。

灵敏度(0-2.0):调整阴影检测阈值。数值越低,可检测越细微的阴影;数值越高,可检测越明显的阴影。

MaskBlur(3-101,奇数):柔化蒙版边缘。值越大,大面积阴影的过渡越平滑。

潜在改进和局限性

增强功能

批量处理:扩展管道以处理多个图像或视频帧,从而能够在实时或大规模应用程序中使用。

ML 集成:整合深度学习模型(如 U-Net),利用 ISTD 等数据集生成更准确、语义更丰富的阴影掩膜。

彩色阴影处理:通过检测和纠正彩色或间接照明引起的颜色偏移来提高鲁棒性

性能优化:通过并行化 Retinex 缩放或处理降采样输入来加快大型图像的处理速度。

局限性

视觉瑕疵:在纹理区域或阴影边界附近,混合可能会引入光晕或不一致,需要更精细的蒙版。

计算成本:对于高分辨率图像,具有大核的多尺度 Retinex 算法速度可能较慢;通常需要进行下采样等预处理步骤。

照明假设:该方法最适用于中性(无彩色)阴影,在彩色或复杂的照明条件下可能会遇到困难。

低光照噪声放大:阴影增强可能会放大暗部的图像噪声;可能需要事先进行降噪处理。

与深度学习相比:OpenCV 方法在复杂阴影去除方面不如深度学习,对于阴影较重的图像,很难完全校正。

总的来说,这对于很多场景来说都是一个可靠的基准,通过针对特定图像和光照条件调整参数,可以提高性能。

结 论

阴影在图像增强中是一个挑战,因为它们会影响光照,却不会改变物体的属性。本文介绍了一种基于 OpenCV 的自适应阴影校正流程,该流程结合了多尺度 Retinex 和基于色彩空间的阴影检测,能够在减少阴影的同时保持自然色彩。交互式参数调优使得该方法能够灵活地应用于不同的图像。虽然它在处理复杂场景时无法完全媲美深度学习方法,但它提供了一个轻量级且有效的基准,可以进一步改进或扩展。

相关推荐