Python数字图像处理:基于LAB空间的图像去阴影方法详解

目录

整体架构流程

(1)阴影区域检测

①LAB颜色空间

②阴影检测

③代码 

(2)阴影去除

①在LAB空间上对单独目标区域去除阴影

②处理每个阴影区域

③代码

(3)阴影边缘校正

①中值滤波器的实现

②调用中值滤波器

③代码

效果展示

①环境图片

②文档上的阴影

全部代码

基于CNN的进阶方法

参考文献


概要

阴影检测和去除是许多计算机视觉应用中的一项重要的预处理任务。在图像分割过程中,阴影可能会产生错误的片段。此外,在对象检测算法中,阴影可能被错误地检测为对象。目前已有多种研究提出了各种基于像素和基于区域的方法来检测并去除图像中的阴影。

大多数的阴影检测方法都需要多幅图像来进行相机的校准,但是最好能够从单一图像中提取阴影。同时,从单一图像中区分黑暗物体和阴影也具有一定的难度。在阴影区域检测和去除的工作上,基于LAB等效图像的RGB图像的平均值选择阴影检测方法卓有成效。

整体架构流程

本文提供的图像去阴影方法分为三个流程:阴影区域检测、阴影去除、阴影边缘校正。

(1)阴影区域检测

阴影出现在光源由于某些物体的阻碍而不能直接到达的区域。一个对象也可以向自己投射一个阴影。在这项工作中,阴影检测是在LAB颜色空间中完成的。所以我们首先需要将RGB图像转换为其LAB等效图像;再根据图像在A平面和B平面上的平均值进行分类讨论将可能为影的区域检测出来。

①LAB颜色空间

LAB颜色空间的三个通道含义如下:

  • L通道:亮度通道,取值范围为[0, 100],这个值越大表示对应区域亮度越大;
  • A通道:RGB通道下的红绿比,取值范围为[-128, 128];
  • B通道:RGB通道下的黄蓝比,取值范围为[-128, 128];
  •      

    ②阴影检测

    阴影检测是在LAB颜色空间中完成的。在这一部分首先将RGB图像转换为LAB图像并计算图片的每个像素在LAB通道的均值,接下来的工作就是识别阴影区域了。我们识别一个阴影区域的讨论方法如下:

  • 当A和B通道的平均值之和小于一个给定的阈值时开始对阴影的情况分类讨论:将满足L≤[mean(L)-std(L)/3] 中的值的像素分类为阴影像素,并将其他像素分类为非阴影像素;
  • 另外,将L和B平面上值较低的像素(低于某个给定的阈值)分类为阴影像素,其他像素分类为非阴影像素。
  • 定义calculate_mask()完成阴影识别:它接受三个参数:org_image表示原始图像,ab_threshold表示A和B通道的平均值阈值,region_adjustment_kernel_size表示区域调整的核大小。

    1.首先使用cv.cvtColor函数将原始图像转换为LAB颜色空间的图像,将LAB图像的数据类型转换为int16,并对L通道进行范围调整,使其取值范围变为0到100;

    2.计算LAB图像中每个通道的平均值,并根据平均值计算阈值。阈值的计算方法是平均值减去每个通道的标准差的1/3;

    3.判断A和B通道的平均值之和是否小于等于ab_threshold。

  • 如果是:将满足条件的像素点归类为阴影,使用cv.inRange函数根据阈值范围创建一个二值掩码图像;
  • 否则:将L和B通道低于阈值的部分归类为阴影,同样使用cv.inRange函数创建一个二值掩码图像。
  • 4.使用cv.getStructuringElement()创建一个椭圆形的结构元素,用于形态学操作;使用cv.morphologyEx()对掩码图像进行闭运算和开运算,以去除噪声并填充阴影区域。

    5.最后返回处理后的二值掩码图像:阴影区域在掩码图像中表示为白色,其他区域表示为黑色。

    ③代码 
    # 识别阴影区域
    def calculate_mask(org_image: np.ndarray,
                       ab_threshold: int,
                       region_adjustment_kernel_size: int) -> np.ndarray:
        # 将原图转换为LAB空间图片
        lab_img = cv.cvtColor(org_image, cv.COLOR_BGR2LAB)
    
        # L以及A、B的取值范围
        l_range = (0, 100)
        ab_range = (-128, 127)
    
        # 调整参数
        lab_img = lab_img.astype('int16')
        lab_img[:, :, 0] = lab_img[:, :, 0] * l_range[1] / 255
        lab_img[:, :, 1] += ab_range[0]
        lab_img[:, :, 2] += ab_range[0]
    
        # 计算所有像素的LAB平均值及其阈值
        means = [np.mean(lab_img[:, :, i]) for i in range(3)]
        thresholds = [means[i] - (np.std(lab_img[:, :, i]) / 3) for i in range(3)]
    
        # mean(A) + mean(B)<threshold, 归类为阴影
        if sum(means[1:]) <= ab_threshold:
            mask = cv.inRange(lab_img, (l_range[0], ab_range[0], ab_range[0]),
                                       (thresholds[0], ab_range[1], ab_range[1]))
        # 否则将L、B低于threshold的部分归类为阴影
        else:  
            mask = cv.inRange(lab_img, (l_range[0], ab_range[0], ab_range[0]),
                                       (thresholds[0], ab_range[1], thresholds[2]))
    
        kernel_size = (region_adjustment_kernel_size, region_adjustment_kernel_size)
        kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, kernel_size)
        cv.morphologyEx(mask, cv.MORPH_CLOSE, kernel, mask)
        cv.morphologyEx(mask, cv.MORPH_OPEN, kernel, mask)
    
        return mask

    (2)阴影去除

    阴影去除是通过使用适当的常数与阴影像素的R、G和B通道相乘来完成的。这里采用单独考虑每个阴影区域的做法,将近非阴影区域的每个通道的平均值与阴影区域的平均值之比作为每个通道的常数。虽然利用近非阴影区域的平均亮度近似阴影区域的亮度可以使阴影区域实现的照明几乎与非阴影区域相同,但是这种做法可能导致阴影区域的边缘出现过度照明的问题。而这个问题就要靠阴影边缘矫正来“兜底”了。

    阴影去除的实现是通过计算阴影/非阴影区域的颜色比率,并利用该比率对阴影区域的色彩进行放缩实现的,定义correct_region_lab()以及process_regions()实现阴影去除:

    ①在LAB空间上对单独目标区域去除阴影

    correct_region_lab()接受四个参数:org_image表示原始图像,shadow_clear_img表示去除阴影后的图像,shadow_indices表示阴影区域的索引,non_shadow_indices表示非阴影区域的索引。该函数用于对LAB版本的图像进行颜色修正。

    1.首先计算阴影区域和非阴影区域的平均LAB值并计算颜色修正的比率,即非阴影区域的LAB平均值除以阴影区域的LAB平均值;

    2.使用cv.cvtColor()将去除阴影后的图像转换为LAB颜色空间;

    3.根据阴影区域的索引将阴影区域的LAB值乘以颜色修正的比率;

    4.使用cv.cvtColor()将图像转换回BGR颜色空间并返回修正后的图像。

    同时也可以定义correct_region_bgr(),它与correct_region_lab()的功能类似,区别在于它对RGB版本的图像进行颜色修正。但是总体来说仍然是LAB版本的阴影去除方法表现得更加优秀。

    ②处理每个阴影区域

    process_regions()是阴影去除的主函数,接受多个参数:org_image表示原始图像,mask表示阴影掩码图像,lab_adjustment表示是否使用LAB版本进行颜色修正。shadow_dilation_kernel_size表示阴影膨胀的核大小,shadow_dilation_iteration表示阴影膨胀的迭代次数,shadow_size_threshold表示阴影区域的最小像素数阈值,verbose表示是否显示详细信息。

    1.使用cv.cvtColor()将原始图像转换为LAB颜色空间的图像,并创建一个与原始图像相同的图像副本shadow_clear_img、measure.label函数对阴影掩码图像进行标记,得到每个阴影区域的标签;

    2.循环处理每个阴影区域:判断阴影区域是否足够大,如果不满足阈值要求,则不进行处理;

    3.通过cv.dilate()对阴影区域进行膨胀操作,得到非阴影区域;

    4.按位异或对非阴影区域和阴影区域进行操作,得到阴影区域的边缘;

    5.cv.findContours()提取阴影区域的轮廓,根据lab_adjustment参数的值,调用相应的颜色修正函数进行颜色修正;

    6.edge_median_filter()对阴影区域的边缘进行中值滤波;

    7.返回去除阴影后的图像。

    ③代码
    # 阴影去除的具体流程,分为LAB版本和RGB版本
    def correct_region_lab(org_img: np.ndarray,
                           shadow_clear_img: np.ndarray,
                           shadow_indices: np.ndarray,
                           non_shadow_indices: np.ndarray) -> np.ndarray:
        # 计算阴影区域及非阴影区域的平均RGB值
        shadow_average_lab = np.mean(org_img[shadow_indices[0], shadow_indices[1], :], axis=0)
    
        # 边缘区域的RGB均值
        border_average_lab = np.mean(org_img[non_shadow_indices[0], non_shadow_indices[1], :],
                                     axis=0)
    
        # 使用常数比率修正色彩
        lab_ratio = border_average_lab / shadow_average_lab
    
        shadow_clear_img = cv.cvtColor(shadow_clear_img, cv.COLOR_BGR2LAB)
        shadow_clear_img[shadow_indices[0], shadow_indices[1]] = np.uint8(
            shadow_clear_img[shadow_indices[0],
                             shadow_indices[1]] * lab_ratio)
        shadow_clear_img = cv.cvtColor(shadow_clear_img, cv.COLOR_LAB2BGR)
    
        return shadow_clear_img
    
    
    def correct_region_bgr(org_img: np.ndarray,
                           shadow_clear_img: np.ndarray,
                           shadow_indices: np.ndarray,
                           non_shadow_indices: np.ndarray) -> np.ndarray:
        # 计算阴影区域及非阴影区域的平均RGB值
        shadow_average_bgr = np.mean(org_img[shadow_indices[0], shadow_indices[1], :], axis=0)
    
        # 边缘区域的RGB均值
        border_average_bgr = np.mean(org_img[non_shadow_indices[0], non_shadow_indices[1], :], axis=0)
        bgr_ratio = border_average_bgr / shadow_average_bgr
    
        # 使用常数比率修正色彩
        shadow_clear_img[shadow_indices[0], shadow_indices[1]] = np.uint8(
            shadow_clear_img[shadow_indices[0],
                             shadow_indices[1]] * bgr_ratio)
    
        return shadow_clear_img
    
    # 阴影去除
    def process_regions(org_image: np.ndarray,
                        mask: np.ndarray,
                        lab_adjustment: bool,
                        shadow_dilation_kernel_size: int,
                        shadow_dilation_iteration: int,
                        shadow_size_threshold: int,
                        verbose: bool) -> np.ndarray:
        
        # 转LAB空间
        lab_img = cv.cvtColor(org_image, cv.COLOR_BGR2LAB)
        shadow_clear_img = np.copy(org_image)
    
        # 用标签表示每个阴影区域
        labels = measure.label(mask)
    
        non_shadow_kernel_size = (shadow_dilation_kernel_size, shadow_dilation_kernel_size)
        non_shadow_kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, non_shadow_kernel_size)
    
        CHANNEL_MAX = 255
    
        # 单独处理每个阴影区域
        for label in np.unique(labels):
            if not label == 0:
                temp_filter = np.zeros(mask.shape, dtype="uint8")
                temp_filter[labels == label] = CHANNEL_MAX
    
                # 只有够大的阴影区域才会被处理
                if cv.countNonZero(temp_filter) >= shadow_size_threshold:
                    shadow_indices = np.where(temp_filter == CHANNEL_MAX)
    
                    # 各点像素值被替换为对应邻域上的最大值
                    non_shadow_temp_filter = cv.dilate(temp_filter, non_shadow_kernel,
                                                       iterations=shadow_dilation_iteration)
    
                    # 与掩膜进行按位异或运算
                    non_shadow_temp_filter = cv.bitwise_xor(non_shadow_temp_filter, temp_filter)
                    non_shadow_indices = np.where(non_shadow_temp_filter == CHANNEL_MAX)
    
                    # 提取阴影区域的轮廓
                    contours, hierarchy = cv.findContours(temp_filter, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    
                    # 是否要利用LAB空间进行阴影去除
                    if lab_adjustment:
                        shadow_clear_img = correct_region_lab(lab_img, shadow_clear_img,
                                                              shadow_indices, non_shadow_indices)
                    else:
                        shadow_clear_img = correct_region_bgr(org_image, shadow_clear_img,
                                                              shadow_indices, non_shadow_indices)
    
                    # 阴影区域边缘校正
                    shadow_clear_img = edge_median_filter(cv.cvtColor(shadow_clear_img, cv.COLOR_BGR2HSV),
                                                          contours)
                    if verbose:
                        display_region(org_image, shadow_clear_img, label, temp_filter, contours)
    
        return shadow_clear_img

    (3)阴影边缘校正

    由于阴影区域不均匀照明,整个阴影区域的相同常数可能在阴影边缘附近创建过度照明的区域。这可以通过在过亮区域应用中值滤波器来克服。中值滤波器是一种常用的图像处理算法,用于去除图像中的噪声,它的原理是将每个像素点周围的像素值排序,然后取中间值作为该像素点的值。这样就得到了一个边缘无过亮或过亮现象有所缓解的无阴影图像。

    ①中值滤波器的实现

    median_filter()接受三个参数:img表示输入图像,point表示需要进行中值滤波的像素点的坐标,filter_size表示滤波器的大小;

    1.根据滤波器的大小和中心像素点的坐标,计算出需要参与计算中值的像素点的坐标。这里使用了两个嵌套的循环,分别遍历了滤波器范围内的所有像素点的坐标;

    2.filter()过滤掉超出图像范围的像素点的坐标。然后,初始化一个长度为3的列表pixel_values,用于存储每个颜色通道的像素值;

    3.循环遍历每个颜色通道,将每个像素点的颜色通道值存入pixel_values列表中;

    4.np.median()计算pixel_values列表中每个颜色通道的中值,得到一个长度为3的列表pixel_values;

    5.最后,返回pixel_values列表作为中值滤波后的像素值。

    ②调用中值滤波器

    edge_median_filter()接受三个参数:img表示输入图像,contours_list表示图像中的轮廓列表,filter_size表示滤波器的大小,默认为7。

    1.np.copy()创建一个与输入图像相同的临时图像temp_img;

    2.循环遍历轮廓列表中的每个轮廓,并遍历每个轮廓中的每个点;对于每个点,将其坐标传递给median_filter()进行中值滤波,并将滤波后的像素值赋给temp_img对应位置的像素点;

    3.最后通过cv.cvtColor()将滤波后的图像从HSV颜色空间转换为BGR颜色空间,并将结果返回。

    ③代码
    # 中值滤波器
    def median_filter(img: np.ndarray,
                      point: np.ndarray,
                      filter_size: int) -> List:
        indices = [[x, y]
                   for x in range(point[1] - filter_size // 2, point[1] + filter_size // 2 + 1)
                   for y in range(point[0] - filter_size // 2, point[0] + filter_size // 2 + 1)]
    
        indices = list(filter(lambda x: not (x[0] < 0 or x[1] < 0 or
                                             x[0] >= img.shape[0] or
                                             x[1] >= img.shape[1]), indices))
    
        pixel_values = [0, 0, 0]
    
        # 计算像素点对应中值
        for channel in range(3):
            pixel_values[channel] = list(img[index[0], index[1], channel] for index in indices)
        pixel_values = list(np.median(pixel_values, axis=1))
    
        return pixel_values
    
    def edge_median_filter(img: np.ndarray,
                           contours_list: tuple,
                           filter_size: int = 7) -> np.ndarray:
        temp_img = np.copy(img)
    
        for partition in contours_list:
            for point in partition:
                temp_img[point[0][1]][point[0][0]] = median_filter(img,
                                                                   point[0],
                                                                   filter_size)
    
        return cv.cvtColor(temp_img, cv.COLOR_HSV2BGR)

    效果展示

    ①环境图片

    输入原始图片后,程序将给出原始图片Original Image、识别的阴影区域Shadow Regions、去除阴影后的图片Correct Image,三者用同一张图对比结果如下:

    原始图片如下,该图有较多的阴影区域:

    输出的最终效果如图,许多原本较暗的区域的亮度已经变得接近非阴影邻域的亮度,虽然在边缘的处理还是有瑕疵,但是总体来说效果还是不错的。如果要实现更加完美的去除阴影应该要使用更加高级的如神经网络方法了:

    ②文档上的阴影

    为了与后续的基于神经网络的去阴影方法进行对比,这里特别地针对文档上的具有阴影的图片进行测试,测试结果如下:

    首先是原图、阴影区域以及修复后结果的对比:

    原图如下:

    修复后的图像的阴影区域整体会过亮,但边缘和阴影区域内部的亮度确实没有太大区别。这可能可以通过调整去除阴影所使用的系数做得更好,但不能保证在别的图片上就有更好的效果了。所以要真正做得更好还要通过更高级的方法:

    全部代码

    import cv2 as cv
    import numpy as np
    from skimage import measure
    from matplotlib import pyplot as plt
    from typing import Tuple, List
    
    # 中值滤波器
    def median_filter(img: np.ndarray,
                      point: np.ndarray,
                      filter_size: int) -> List:
        indices = [[x, y]
                   for x in range(point[1] - filter_size // 2, point[1] + filter_size // 2 + 1)
                   for y in range(point[0] - filter_size // 2, point[0] + filter_size // 2 + 1)]
    
        indices = list(filter(lambda x: not (x[0] < 0 or x[1] < 0 or
                                             x[0] >= img.shape[0] or
                                             x[1] >= img.shape[1]), indices))
    
        pixel_values = [0, 0, 0]
    
        # 计算像素点对应中值
        for channel in range(3):
            pixel_values[channel] = list(img[index[0], index[1], channel] for index in indices)
        pixel_values = list(np.median(pixel_values, axis=1))
    
        return pixel_values
    
    def edge_median_filter(img: np.ndarray,
                           contours_list: tuple,
                           filter_size: int = 7) -> np.ndarray:
        temp_img = np.copy(img)
    
        for partition in contours_list:
            for point in partition:
                temp_img[point[0][1]][point[0][0]] = median_filter(img,
                                                                   point[0],
                                                                   filter_size)
    
        return cv.cvtColor(temp_img, cv.COLOR_HSV2BGR)
    
    def display_region(org_image: np.ndarray,
                       shadow_clear_image: np.ndarray,
                       label: int,
                       label_region: np.ndarray,
                       contours: tuple) -> None:
        # For debugging, cut the current shadow region from the image
        reverse_mask = cv.cvtColor(cv.bitwise_not(label_region), cv.COLOR_GRAY2BGR)
        img_w_hole = org_image & reverse_mask
    
        temp_filter = cv.cvtColor(label_region, cv.COLOR_GRAY2BGR)
        cv.drawContours(temp_filter, contours, -1, (255, 0, 0), 3)
    
        fig, axes = plt.subplots(2, 2)
    
        ax = axes.ravel()
    
        plt.title(f"Shadow Region {label}")
    
        ax[0].imshow(cv.cvtColor(org_image, cv.COLOR_BGR2RGB))
        ax[0].set_title("Original Image")
    
        ax[1].imshow(cv.cvtColor(temp_filter, cv.COLOR_BGR2RGB))
        ax[1].set_title("Shadow Region")
    
        ax[2].imshow(cv.cvtColor(img_w_hole, cv.COLOR_BGR2RGB))
        ax[2].set_title("Shadow Region Cut")
    
        ax[3].imshow(cv.cvtColor(shadow_clear_image, cv.COLOR_BGR2RGB))
        ax[3].set_title("Corrected Image")
    
        plt.tight_layout()
        plt.show()
    
    # 阴影去除的具体流程,分为LAB版本和RGB版本
    def correct_region_lab(org_img: np.ndarray,
                           shadow_clear_img: np.ndarray,
                           shadow_indices: np.ndarray,
                           non_shadow_indices: np.ndarray) -> np.ndarray:
        # 计算阴影区域及非阴影区域的平均RGB值
        shadow_average_lab = np.mean(org_img[shadow_indices[0], shadow_indices[1], :], axis=0)
    
        # 边缘区域的RGB均值
        border_average_lab = np.mean(org_img[non_shadow_indices[0], non_shadow_indices[1], :],
                                     axis=0)
    
        # 使用常数比率修正色彩
        lab_ratio = border_average_lab / shadow_average_lab
    
        shadow_clear_img = cv.cvtColor(shadow_clear_img, cv.COLOR_BGR2LAB)
        shadow_clear_img[shadow_indices[0], shadow_indices[1]] = np.uint8(
            shadow_clear_img[shadow_indices[0],
                             shadow_indices[1]] * lab_ratio)
        shadow_clear_img = cv.cvtColor(shadow_clear_img, cv.COLOR_LAB2BGR)
    
        return shadow_clear_img
    
    
    def correct_region_bgr(org_img: np.ndarray,
                           shadow_clear_img: np.ndarray,
                           shadow_indices: np.ndarray,
                           non_shadow_indices: np.ndarray) -> np.ndarray:
        # 计算阴影区域及非阴影区域的平均RGB值
        shadow_average_bgr = np.mean(org_img[shadow_indices[0], shadow_indices[1], :], axis=0)
    
        # 边缘区域的RGB均值
        border_average_bgr = np.mean(org_img[non_shadow_indices[0], non_shadow_indices[1], :], axis=0)
        bgr_ratio = border_average_bgr / shadow_average_bgr
    
        # 使用常数比率修正色彩
        shadow_clear_img[shadow_indices[0], shadow_indices[1]] = np.uint8(
            shadow_clear_img[shadow_indices[0],
                             shadow_indices[1]] * bgr_ratio)
    
        return shadow_clear_img
    
    # 阴影去除
    def process_regions(org_image: np.ndarray,
                        mask: np.ndarray,
                        lab_adjustment: bool,
                        shadow_dilation_kernel_size: int,
                        shadow_dilation_iteration: int,
                        shadow_size_threshold: int,
                        verbose: bool) -> np.ndarray:
        
        # 转LAB空间
        lab_img = cv.cvtColor(org_image, cv.COLOR_BGR2LAB)
        shadow_clear_img = np.copy(org_image)
    
        # 用标签表示每个阴影区域
        labels = measure.label(mask)
    
        non_shadow_kernel_size = (shadow_dilation_kernel_size, shadow_dilation_kernel_size)
        non_shadow_kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, non_shadow_kernel_size)
    
        CHANNEL_MAX = 255
    
        # 单独处理每个阴影区域
        for label in np.unique(labels):
            if not label == 0:
                temp_filter = np.zeros(mask.shape, dtype="uint8")
                temp_filter[labels == label] = CHANNEL_MAX
    
                # 只有够大的阴影区域才会被处理
                if cv.countNonZero(temp_filter) >= shadow_size_threshold:
                    shadow_indices = np.where(temp_filter == CHANNEL_MAX)
    
                    # 各点像素值被替换为对应邻域上的最大值
                    non_shadow_temp_filter = cv.dilate(temp_filter, non_shadow_kernel,
                                                       iterations=shadow_dilation_iteration)
    
                    # 与掩膜进行按位异或运算
                    non_shadow_temp_filter = cv.bitwise_xor(non_shadow_temp_filter, temp_filter)
                    non_shadow_indices = np.where(non_shadow_temp_filter == CHANNEL_MAX)
    
                    # 提取阴影区域的轮廓
                    contours, hierarchy = cv.findContours(temp_filter, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
    
                    # 是否要利用LAB空间进行阴影去除
                    if lab_adjustment:
                        shadow_clear_img = correct_region_lab(lab_img, shadow_clear_img,
                                                              shadow_indices, non_shadow_indices)
                    else:
                        shadow_clear_img = correct_region_bgr(org_image, shadow_clear_img,
                                                              shadow_indices, non_shadow_indices)
    
                    # 阴影区域边缘校正
                    shadow_clear_img = edge_median_filter(cv.cvtColor(shadow_clear_img, cv.COLOR_BGR2HSV),
                                                          contours)
                    if verbose:
                        display_region(org_image, shadow_clear_img, label, temp_filter, contours)
    
        return shadow_clear_img
    
    # 识别阴影区域
    def calculate_mask(org_image: np.ndarray,
                       ab_threshold: int,
                       region_adjustment_kernel_size: int) -> np.ndarray:
        # 将原图转换为LAB空间图片
        lab_img = cv.cvtColor(org_image, cv.COLOR_BGR2LAB)
    
        # L以及A、B的取值范围
        l_range = (0, 100)
        ab_range = (-128, 127)
    
        # 调整参数
        lab_img = lab_img.astype('int16')
        lab_img[:, :, 0] = lab_img[:, :, 0] * l_range[1] / 255
        lab_img[:, :, 1] += ab_range[0]
        lab_img[:, :, 2] += ab_range[0]
    
        # 计算所有像素的LAB平均值及其阈值
        means = [np.mean(lab_img[:, :, i]) for i in range(3)]
        thresholds = [means[i] - (np.std(lab_img[:, :, i]) / 3) for i in range(3)]
    
        # mean(A) + mean(B)<threshold, 归类为阴影
        if sum(means[1:]) <= ab_threshold:
            mask = cv.inRange(lab_img, (l_range[0], ab_range[0], ab_range[0]),
                                       (thresholds[0], ab_range[1], ab_range[1]))
        # 否则将L、B低于threshold的部分归类为阴影
        else:  
            mask = cv.inRange(lab_img, (l_range[0], ab_range[0], ab_range[0]),
                                       (thresholds[0], ab_range[1], thresholds[2]))
    
        kernel_size = (region_adjustment_kernel_size, region_adjustment_kernel_size)
        kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, kernel_size)
        cv.morphologyEx(mask, cv.MORPH_CLOSE, kernel, mask)
        cv.morphologyEx(mask, cv.MORPH_OPEN, kernel, mask)
    
        return mask
    
    
    def remove_shadows(org_image: np.ndarray,
                       ab_threshold: int,
                       lab_adjustment: bool,
                       region_adjustment_kernel_size: int,
                       shadow_dilation_iteration: int,
                       shadow_dilation_kernel_size: int,
                       shadow_size_threshold: int,
                       verbose: bool) -> Tuple[np.ndarray, np.ndarray]:
        
        mask = calculate_mask(org_image,
                              ab_threshold,
                              region_adjustment_kernel_size)
    
        shadow_clear_img = process_regions(org_image,
                                           mask,
                                           lab_adjustment,
                                           shadow_dilation_kernel_size,
                                           shadow_dilation_iteration,
                                           shadow_size_threshold,
                                           verbose)
    
        mask = cv.cvtColor(mask, cv.COLOR_GRAY2RGB)
    
        return shadow_clear_img, mask
    
    # 主要处理过程, 返回RGB色彩空间的图片
    def process_image_file(img_name: str,
                           save: bool = False,
                           ab_threshold: int = 256,
                           lab_adjustment: bool = True,
                           region_adjustment_kernel_size: int = 10,
                           shadow_dilation_kernel_size: int = 5,
                           shadow_dilation_iteration: int = 3,
                           shadow_size_threshold: int = 2500,
                           verbose: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        org_image = cv.imread(img_name)
        print("读取图片 {}".format(img_name))
    
        # 阴影去除过程
        shadow_clear, mask = remove_shadows(org_image,
                                            ab_threshold,
                                            lab_adjustment,
                                            region_adjustment_kernel_size,
                                            shadow_dilation_iteration,
                                            shadow_dilation_kernel_size,
                                            shadow_size_threshold,
                                            verbose=verbose)
    
        # 以下是展示图片的代码
        _, axes = plt.subplots(1, 3)
        ax = axes.ravel()
    
        plt.title("最终效果")
    
        ax[0].imshow(cv.cvtColor(org_image, cv.COLOR_BGR2RGB))
        ax[0].set_title("Original Image")
    
        ax[1].imshow(cv.cvtColor(mask, cv.COLOR_BGR2RGB))
        ax[1].set_title("Shadow Regions")
    
        ax[2].imshow(cv.cvtColor(shadow_clear, cv.COLOR_BGR2RGB))
        ax[2].set_title("Corrected Image")
    
        plt.tight_layout()
        plt.show()
    
        if save:
            f_name = img_name[:img_name.index(".")] + "_shadowClear" + img_name[img_name.index("."):]
            cv.imwrite(f_name, shadow_clear)
            print("Saved result as " + f_name)
    
        return org_image, mask, shadow_clear
    
    image_path = "images/test.jpg"
    
    org_image, mask, image_clear = process_image_file(image_path, ab_threshold = 4, save = True, verbose = True)

    基于CNN的进阶方法

    论文笔记【Document Image Shadow Removal Guided by Color-Aware Background】+代码复现

    参考文献

    [1] Murali, Saritha, and V. K. Govindan."Shadow detection and removal from a single image using LAB color space."Cybernetics and information technologies 13.1 (2013): 95-103.

    [2] Murali, Saritha, and V. K. Govindan."Removal of shadows from a single image."the Proceedings of First International Conference on Futuristic Trends in Computer Science and Engineering. Vol. 4.

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python数字图像处理:基于LAB空间的图像去阴影方法详解

    发表回复