文章目录
-
-
- 1.算法原理
- 2.算法搜索策略
- 3.算法代码实现细节
- 3.测试代码
-
- [noise reduction](#noise reduction)
- 均值图像的NLM处理
-
注意NLM算法广泛应用在灰度图以及YUV色彩空间的Y通道上,我的理解是这样计算相对简单.NLM算法的核心假设是图像中存在灰度相似度(欧氏距离),来进行加权平均去噪.单通道处理起来比较简单.后面的测试程序就是基于灰度图像来处理的.
1.算法原理
非局部均值(NLM)是一种经典的基于图像自相似性的降噪算法。与局部滤波器不同,NLM 认为图像中相距较远的区域可能具有相似的纹理结构,这些相似区域都可以参与当前像素的估计。
核心数学公式
NLM(x) = Σ w(x,y) × I(y) / Σ w(x,y)
这个公式的本质是一个加权平均过程,它决定了去噪后像素 x 的最终灰度值。
- NLM(x):这是算法的最终输出,即像素 x 经过 NLM 滤波后的"无噪估计值"。
- I(y):代表在搜索窗口内,某个候选像素 y 的原始灰度值。
- w(x,y):这是 NLM 算法的"灵魂",即权重。它决定了候选像素y 对目标像素 x 的贡献程度。
- Σ w(x,y) × I(y):这是分子部分,表示将搜索窗口内所有候选像素的值,按照它们各自的权重进行加权求和。
- Σ w(x,y):这是分母部分,即所有权重之和(在文献中通常记为归一化常数
C(x) 或 Z(x)。除以这个总和是为了进行归一化,确保最终输出的像素值不会发生整体偏移或溢出。
权重计算公式
权重基于两个邻域块(patch)的欧式距离:
w(x,y) = exp(-||P(x) - P(y)||² / h²)
- P(x) 和 P(y):分别表示以目标像素 x 和候选像素 y 为中心的局部邻域块(例如 3×3 或 7×7 的小方块)。NLM 认为,仅仅比较单个像素点的灰度值是不够的,必须比较它们周围的一小块区域(结构)。针对于下面的案例,P(x)是以 x 为中心的 (2ds+1)×(2ds+1) 邻域块,其中ds=1.
- ||P(x) - P(y)||²:这表示两个图像块之间的欧氏距离(即平方误差之和)。它量化了这两个小块在结构上的差异程度。距离越小,说明这两个块长得越像;距离越大,说明差异越大。针对于Y通道来说,这里就是灰度值差的平方. 越相似,值越低.
- h²:这是滤波参数 h 的平方。 h 相当于一个"平滑度阈值"或"衰减带宽"。
- exp(- ... / h²):这是一个指数衰减函数。
- 当两个块非常相似时(距离趋近于0),指数部分接近0,权重 w(x,y) 接近最大值 1。看下面衰减图像
- 当两个块差异很大时(距离很大),指数部分为一个很大的负数,权重 w(x,y) 会迅速衰减并趋近于 0。
- 参数 h 控制着衰减的"陡峭程度"。 h 越大,对差异的容忍度越高,更多的像素会参与平均(去噪强但易模糊); h 越小,只有极度相似的块才能获得权重(保细节但去噪弱)。
注意: 这里的权重是一个减函数,越相似,权重越大.

2.算法搜索策略
openISP的调用参数 :ds=1(3×3 块),Ds=4(9×9 搜索窗口)。
搜索窗口大小:(2×Ds+1) × (2×Ds+1)
邻域块大小: (2×ds+1) × (2×ds+1)
有效搜索半径:Ds - ds 个位置
3.算法代码实现细节
- wmax 技巧:将中心像素的权重设为邻域中最大权重,避免退化
- 使用均匀加权核
kernel = ones / (2ds+1)²计算块距离 - 输入 padding
Ds像素,仅处理 Y(亮度)通道 - 逐像素双重遍历,计算复杂度高(大图上很慢)
python
class NLM:
'Non-Local Means Denoising'
def __init__(self, img, ds, Ds, h, clip):
self.img = img
self.ds = ds # neighbour window size - 1 /2
self.Ds = Ds # search window size - 1 / 2
self.h = h
self.clip = clip
def padding(self):
img_pad = np.pad(self.img, (self.Ds, self.Ds), 'reflect')
return img_pad
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def calWeights(self, img, kernel, y, x):
wmax = 0
sweight = 0
average = 0
for j in range(2 * self.Ds + 1 - 2 * self.ds - 1):
for i in range(2 * self.Ds + 1 - 2 * self.ds - 1):
start_y = y - self.Ds + self.ds + j
start_x = x - self.Ds + self.ds + i
neighbour_w = img[start_y - self.ds:start_y + self.ds + 1, start_x - self.ds:start_x + self.ds + 1]
center_w = img[y-self.ds:y+self.ds+1, x-self.ds:x+self.ds+1]
if j != y or i != x:
sub = np.subtract(neighbour_w, center_w)
dist = np.sum(np.multiply(kernel, np.multiply(sub, sub)))
w = np.exp(-dist/pow(self.h, 2)) # replaced by look up table
if w > wmax:
wmax = w
sweight = sweight + w
average = average + w * img[start_y, start_x]
return sweight, average, wmax
def execute(self):
img_pad = self.padding()
img_pad = img_pad.astype(np.uint16)
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
nlm_img = np.empty((raw_h, raw_w), np.uint16)
kernel = np.ones((2*self.ds+1, 2*self.ds+1)) / pow(2*self.ds+1, 2)
for y in range(img_pad.shape[0] - 2 * self.Ds):
for x in range(img_pad.shape[1] - 2 * self.Ds):
center_y = y + self.Ds
center_x = x + self.Ds
sweight, average, wmax = self.calWeights(img_pad, kernel, center_y, center_x)
average = average + wmax * img_pad[center_y, center_x]
sweight = sweight + wmax
nlm_img[y,x] = average / sweight
self.img = nlm_img
return self.clipping()
3.测试代码
noise reduction
python
def test_noise_reduction(self):
"""含高斯噪声图像经 NLM 后标准差应减小。"""
rng = np.random.default_rng(7)
img = (128 + rng.integers(-40, 40, size=(12, 12))).clip(0, 255).astype(np.uint16)
std_before = float(img.astype(float).std())
nlm = NLM(img.copy(), ds=1, Ds=3, h=20, clip=255)
out = nlm.execute()
show_gray_images(img, out, "left", "right-nosie-reduction")
-
测试效果图(h=20)

-
测试效果图2(h=10)
对比上面,h越大,平滑效果越好.这里h=10,基本没什么变化.

均值图像的NLM处理
python
def test_uniform_image_unchanged(self):
"""均匀图像:所有 patch 距离为 0,权重相等,均值 = 中心值。"""
img = make_gray(12, 12, val=150)
nlm = NLM(img.copy().astype(np.uint16), ds=1, Ds=3, h=10, clip=255)
out = nlm.execute()
show_gray_images(img, out, "left", "right-uniform")
np.testing.assert_array_equal(out, 150)
- 测试效果图
