文章目录
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
- 效果图
能够看到效果是好了很多

4786

被折叠的 条评论
为什么被折叠?



