数字图像处理-3-图像平滑处理的滤波方法-二值图像噪声消除和邻域平均法

1. 噪声产生

后面介绍的几个都是降噪算法,这里我们先了解,如何生成一张有噪声的图片

1.1 随机噪声

这个用来模拟相机传感器的热噪声或量化误差
代码实现起来,也不复杂, img对象是位图对象,其内部数据也是按着位图格式解析的.

  • python代码
def _clamp(value):
    """将整数或浮点数截断到 [0, 255],对应 C++ 中的像素赋值溢出保护"""
    v = int(value)
    if v < 0:   return 0
    if v > 255: return 255
    return v
def add_random_noise(img):
    """
    随机噪声(模拟高斯噪声)

    原理:
        在每个像素叠加一个小幅随机量,模拟相机传感器热噪声。

        公式:output = original × (224/256) + rand() / 1024
          - 先将原值略微压暗(乘以 224/256 ≈ 0.875)
          - 再叠加 [0, ~32] 范围内的均匀随机量
          - 整体效果:图像偏暗,叠加细粒度随机斑点
    """
    result = img.copy()
    for y in range(img.height):
        for x in range(img.width):
            noise = random.randint(0, 32767) // 1024  # rand()/1024 ≈ [0,32]
            if img.bit_count == 8:
                val = img.data[y][x]
                result.data[y][x] = _clamp(val * 224 // 256 + noise)
            else: # 如果是24位真彩,则直接在RGB各通道,添加随机噪声
                px = img.data[y][x]
                result.data[y][x] = [
                    _clamp(px[0] * 224 // 256 + noise),
                    _clamp(px[1] * 224 // 256 + noise),
                    _clamp(px[2] * 224 // 256 + noise),
                ]
    return result
  • 图像效果
    这里输入的是一张512x512的lenna的24位bmp图像,能看到右边明显颗粒度拉满.这就是我们添加的噪声.
    在这里插入图片描述

1.2 椒盐噪声

说到椒盐,脑海中直接想到的是吃烧烤时的椒盐调料,里面包含,细盐,芥末,以及各种西颗粒调料.没错,这次产生椒盐的方法,如上面椒盐调料一样,颗粒感拉满. 它的原理很简单,就是随机的将图像像素置黑.

  • python代码
def add_salt_pepper_noise(img):
    """
    椒盐噪声(Salt-and-Pepper Noise / 脉冲噪声)

    原理:
        以低概率将随机像素强制设为黑色(0),模拟信道传输中的
        脉冲干扰或感光元件局部失效。本实现添加的是"椒"(黑点)噪声。
        与盐噪声(白点)的区别仅在于置为 0 还是 255。

        判断条件:rand() > 31500  (概率 ≈ 4%)

    提示:椒盐噪声是中值滤波(非线性滤波)的典型测试场景,
          均值滤波对此噪声效果较差。
    """
    result = img.copy()
    for y in range(img.height):
        for x in range(img.width):
            if random.randint(0, 32767) > 31500:  # ~4% 概率变为噪点
                if img.bit_count == 8: #如果是灰度图,则直接置黑
                    result.data[y][x] = 0
                else: #反之,为24bit真彩色,RGB各通道置黑
                    result.data[y][x] = [0, 0, 0] 
    return result
  • 效果图
    确实颗粒感拉满
    在这里插入图片描述

2. 二值图像噪声消除方法

2.1 二值图像的黑白点噪声滤波

2.1.1 图像二值化

这里在进行二值图像的滤波时,首先图像要是二值图像.下面是二值图像的处理方法.

  • 如果是8位灰度图像,如果灰度值小于阈值,则直接置黑
  • 如果是24位真彩色,则需要根据RGB分量,先转换为灰度图像,然后再和阈值比较
  • python代码
    从下面代码可以看到图像灰度值为0和255,非黑即白.
def fixed_threshold_binarize(img, threshold=100):
    """
    固定阈值二值化

    原理:
        以固定阈值 T 将灰度图像转换为只含 0(黑)和 255(白)的二值图像。
        这是最简单的图像分割方法,适用于前景/背景灰度差异明显的场景。

        公式:
            output(x,y) = 255,若 pixel(x,y) > T
            output(x,y) = 0,  否则
    参数:
        threshold: 阈值 T,范围 0~255,默认 100
    """
    result = img.copy()
    for y in range(img.height):
        for x in range(img.width):
            if img.bit_count == 8:
                val = img.data[y][x]
                result.data[y][x] = 255 if val > threshold else 0
            else:
                r, g, b = img.data[y][x]
                # 彩色图像先转灰度(ITU-R BT.601 加权系数)
                gray = (r * 299 + g * 587 + b * 114) // 1000
                v = 255 if gray > threshold else 0
                result.data[y][x] = [v, v, v]
    return result
  • 二值化后的效果
    在这里插入图片描述

2.1.2 黑白翻转消噪原理

适用于二值化后的图像。对于每个像素,计算其 8 邻域像素的均值;若均值与当前像素值之差的绝对值 > 127.5,认为该像素与邻域"极不一致"(即孤立噪点),将其翻转(0→255 或 255→0)。

  • python代码
def black_white_flip_denoise(img):
    """
        判断条件:|mean(8邻域) - pixel| > 127.5
        物理意义:孤立黑点周围均是白像素,均值≈255,与自身(0)差距达255,
                  远大于127.5,因此被翻转为白色,完成去噪。

    注意:边缘各1行/列像素保持不变(无法构成完整的 3×3 邻域)。
    """
    result = img.copy()
    for y in range(1, img.height - 1): # 这里排除最外边的像素
        for x in range(1, img.width - 1):
            if img.bit_count == 8: # 8位灰度图像
                s = 0
                for dy in range(-1, 2): #Y方向的偏移-1,0,1
                    for dx in range(-1, 2):
                        if dy != 0 or dx != 0:
                            s += img.data[y + dy][x + dx]
                avg = s / 8.0
                val = img.data[y][x]
                if abs(avg - val) > 127.5:
                    result.data[y][x] = 255 - val
            else: # 24位真彩色
                for c in range(3): # RGB各分量偏移
                    s = 0
                    for dy in range(-1, 2):
                        for dx in range(-1, 2):
                            if dy != 0 or dx != 0:
                                s += img.data[y + dy][x + dx][c]
                    avg = s / 8.0
                    val = img.data[y][x][c]
                    if abs(avg - val) > 127.5:
                        result.data[y][x][c] = 255 - val
    return result
  • 效果图
    下面帽顶的黑色边沿都被消除了.
    在这里插入图片描述

2.2 消除孤立黑像素点(连通域分析)

检测被白色像素完全"包围"的孤立黑点并将其消除(置为白色)。"完全包围"的定义取决于连通性参数:

  • 8连通:8个方向的邻居均为白色(更严格)
  • 4连通:上下左右4个方向的邻居均为白色
  • python代码
def remove_isolated_black(img, connectivity=8):
    """
    消除孤立黑点(连通域分析)
        算法步骤:
          1. 遍历图像中每个非边界像素
          2. 若该像素为黑色(0)
          3. 且所有邻居均为白色(255)
          4. 则将其置为白色(255)
    参数:
        connectivity: 连通性,4 或 8,默认 8
    """
    result = img.copy()

    if connectivity == 8:
        offsets = [(dy, dx) for dy in range(-1, 2) for dx in range(-1, 2)
                   if dy != 0 or dx != 0]
    else:  # 4连通
        offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]

    for y in range(1, img.height - 1):
        for x in range(1, img.width - 1):
            if img.bit_count == 8:
                if img.data[y][x] == 0:
                    # 检查是否所有邻居都是白色
                    isolated = all(img.data[y + dy][x + dx] != 0
                                   for dy, dx in offsets)
                    if isolated:
                        result.data[y][x] = 255
            else:
                for c in range(3):
                    if img.data[y][x][c] == 0:
                        isolated = all(img.data[y + dy][x + dx][c] != 0
                                       for dy, dx in offsets)
                        if isolated:
                            result.data[y][x][c] = 255
    return result
  • 效果图
    下图仍然能看到lenna的帽子和脸上也都有黑像素, 不过对比之前好多了.这里因为上面的检测原理是,检测黑像素四周是不是都是白色,这也就意味着, 只要周围有不是白色的,则这个黑点就无法被消除掉, 这个问题等用后面其它滤波器可以解决.
    在这里插入图片描述

3. 邻域平均法

3.1 3x3 均值滤波

用当前像素 8 邻域(不含中心点)的算术均值替换中心像素。这是最基础的线性低通滤波器,相当于一个盒式卷积核,如果之前没有学习过卷积核相关的东西,不要被它吓到.这里其实就是一个加权求和,然后求平均值.

            卷积核(系数 1/8):
            1 1 1
            1 0 1    → 8个邻居的均值(不含中心)
            1 1 1

说白了,就是用四周8个像素的平均值来代替当前像素的值.

  • python代码
def mean_filter_3x3(img):
    """
    效果:消除高频噪声,但同时模糊图像边缘。
    """
    result = img.copy()
    for y in range(1, img.height - 1):
        for x in range(1, img.width - 1):
            if img.bit_count == 8:
                s = 0
                for dy in range(-1, 2):
                    for dx in range(-1, 2):
                        if dy != 0 or dx != 0:
                            s += img.data[y + dy][x + dx]
                result.data[y][x] = _clamp(s // 8) # _clamp函数确保像素值不超过255
            else:
                for c in range(3):
                    s = 0
                    for dy in range(-1, 2):
                        for dx in range(-1, 2):
                            if dy != 0 or dx != 0:
                                s += img.data[y + dy][x + dx][c] # 求和
                    result.data[y][x][c] = _clamp(s // 8) # 求平均值
    return result
  • 效果图
    能看到,确实噪声少了许多,但是图像看起来朦朦胧胧的,这也是这个算法的弊端.
    在这里插入图片描述

3.2 NxN均值滤波器

将 3×3 均值滤波推广到任意奇数大小的 n×n 窗口。用 n×n 邻域内所有 n² 个像素(含中心)的均值替换中心像素

  • python代码:
def mean_filter_nxn(img, n=5):
    """
    n×n 均值滤波



    参数:
        n: 滤波器大小,必须为奇数且 ≥ 3(如 3, 5, 7, 9)

    特点:
        - n 越大,平滑效果越强,但边缘模糊越严重
        - 计算复杂度 O(n²) × 图像像素数
        - 当 n=3 时,与 mean_filter_3x3 类似(但本函数含中心点)
    """
    if n < 3 or n % 2 != 1:
        raise ValueError(f'n 必须是奇数且 ≥ 3,当前 n={n}')

    half  = n // 2
    count = n * n
    result = img.copy()

    for y in range(half, img.height - half):
        for x in range(half, img.width - half):
            if img.bit_count == 8:
                s = 0
                for dy in range(-half, half + 1):
                    for dx in range(-half, half + 1):
                        s += img.data[y + dy][x + dx]
                result.data[y][x] = _clamp(s // count)
            else:
                for c in range(3):
                    s = 0
                    for dy in range(-half, half + 1):
                        for dx in range(-half, half + 1):
                            s += img.data[y + dy][x + dx][c]
                    result.data[y][x][c] = _clamp(s // count)
    return result
  • 效果图:
    这里我默认用的是卷积核是5x5的,当前像素周围25个像素的平均值,替代当前的像素的值. 因为卷积核更大,图像看起来虽然噪点更少了,但图像变的更模糊了.
    在这里插入图片描述

3.3 超限邻域平均法

这也叫做"阈值自适应均值滤波", 它的原理是对每个像素,先计算 3×3 邻域(含中心,共 9 个像素)的均值 avg;只有当 |原像素 - avg| > T 时,才用 avg 替换原像素;否则保持不变。

    判断条件:|pixel - avg| > T → 更新为 avg

物理意义:

  • 若像素与邻域均值差距很小(阈值以内),说明该区域平坦或是真实边缘
    保持原值以保护图像细节。

  • 若差距过大(>T),说明该像素极可能是噪声,用均值替换。

  • 当 T=0 时退化为标准 3×3 均值滤波。

  • python代码

def threshold_adaptive_filter(img, threshold=150):
    """
    参数:
        threshold: 触发平滑的差值阈值 T,范围 0~255
                   T 越大,越少像素被平滑,边缘保持越好
    """
    result = img.copy()
    for y in range(1, img.height - 1):
        for x in range(1, img.width - 1):
            if img.bit_count == 8:
                s = 0
                for dy in range(-1, 2):
                    for dx in range(-1, 2):
                        s += img.data[y + dy][x + dx]
                avg = s / 9.0
                val = img.data[y][x]
                if abs(val - avg) > threshold: # 超过阈值,则使用平均值替换当前像素
                    result.data[y][x] = _clamp(avg)
            else:
                for c in range(3):
                    s = 0
                    for dy in range(-1, 2):
                        for dx in range(-1, 2):
                            s += img.data[y + dy][x + dx][c]
                    avg = s / 9.0
                    val = img.data[y][x][c]
                    if abs(val - avg) > threshold:
                        result.data[y][x][c] = _clamp(avg)
    return result
  • 效果图
    对比上面的3x3, NxN均值滤波,当前这种效果好多了
    在这里插入图片描述

3.4 局部平滑

也叫"局部最小方差平滑(局部平均法", 这是一种更智能的自适应滤波器,利用局部统计特性自动判断使用, 哪个区域的均值来替换当前像素。

    算法步骤:
      1. 定义 9 个局部 3×3 区域,分别以如下位置为中心:
             (y-1,x-1) (y-1,x) (y-1,x+1)
             (y,  x-1) (y,  x) (y,  x+1)    共 9 个中心
             (y+1,x-1) (y+1,x) (y+1,x+1)
      2. 对每个区域计算均值 mean 和方差 variance:
             mean     = (1/9) × Σ pixels
             variance = (1/9) × Σ (pixel - mean)²
      3. 选择方差最小的区域,用该区域的均值替换中心像素

根据上面算法的原理,很容实现算法,不过算法的运算量非常大,本人再电脑上处理lenna的图片是512 x 512 24位(RGB分量相等),整个处理时间是20.83秒,这显示是无法忍受的,本例子只是学习

  • python代码
def local_min_variance_filter(img):
    """
        物理意义:
          - 方差小 → 区域内像素值均匀 → 该区域不跨越边缘
          - 用最均匀区域的均值填充,既能平滑噪声又不破坏边缘
          - 比上面的超限邻域法更自适应,无需人工设定阈值
    注意:需要距边界至少 2 个像素的安全边距。
    性能说明:纯 Python 实现较慢,512×512 图像约需数秒至数十秒。
    """
    result = img.copy()

    # 9 个区域中心相对于当前像素 (y,x) 的偏移
    region_centers = [
        (-1, -1), (-1,  0), (-1,  1),
        ( 0, -1), ( 0,  0), ( 0,  1),
        ( 1, -1), ( 1,  0), ( 1,  1),
    ]

    for y in range(2, img.height - 2):
        for x in range(2, img.width - 2):
            if img.bit_count == 8:
                min_var  = float('inf')
                best_avg = float(img.data[y][x])

                for ry, rx in region_centers:
                    cy, cx = y + ry, x + rx
                    # 该区域中心的 3×3 窗口必须在图像内
                    if cy < 1 or cy >= img.height - 1 or \
                       cx < 1 or cx >= img.width  - 1:
                        continue

                    # 收集 3×3 区域内 9 个像素
                    vals = []
                    for dy in range(-1, 2):
                        for dx in range(-1, 2):
                            vals.append(img.data[cy + dy][cx + dx])

                    mean = sum(vals) / 9.0
                    var  = sum((v - mean) ** 2 for v in vals) / 9.0

                    if var < min_var:
                        min_var  = var
                        best_avg = mean

                result.data[y][x] = _clamp(best_avg)

            else:
                for c in range(3):
                    min_var  = float('inf')
                    best_avg = float(img.data[y][x][c])

                    for ry, rx in region_centers:
                        cy, cx = y + ry, x + rx
                        if cy < 1 or cy >= img.height - 1 or \
                           cx < 1 or cx >= img.width  - 1:
                            continue

                        vals = []
                        for dy in range(-1, 2):
                            for dx in range(-1, 2):
                                vals.append(img.data[cy + dy][cx + dx][c])

                        mean = sum(vals) / 9.0
                        var  = sum((v - mean) ** 2 for v in vals) / 9.0

                        if var < min_var:
                            min_var  = var
                            best_avg = mean

                    result.data[y][x][c] = _clamp(best_avg)

    return result
  • 效果图
    能够看到效果是好了很多
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值