openISP学习10-NLM — Non-Local Means Denoising(非局部均值降噪)

文章目录

注意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)
  • 测试效果图
相关推荐
armwind3 小时前
openISP学习15-BCC — Brightness/Contrast Control(亮度/对比度控制)
图像处理·计算机视觉
装不满的克莱因瓶4 小时前
掌握条件生成对抗网络(Conditional GAN)模型结构——从无条件生成到可控生成的进阶
人工智能·pytorch·python·深度学习·神经网络·生成对抗网络·计算机视觉
Deitymoon4 小时前
RV1126——OSD模块
计算机视觉·音视频·rv1126·osd
放大的EZ4 小时前
Comfyui 教程-22
图像处理·人工智能·计算机视觉
YOLO数据集集合5 小时前
无人机航拍桥梁巡检数据集 | 桥梁结构缺陷检测 深度学习目标检测数据10338期
深度学习·yolo·目标检测·计算机视觉·无人机
armwind5 小时前
openISP学习12-EE — Edge Enhancement(边缘增强)
图像处理·计算机视觉
Litluecat5 小时前
配合多角色提示语4,学习AI漫剧(刚开始学)
人工智能·学习·计算机视觉
qq_8573058195 小时前
OpenCV入门
人工智能·opencv·计算机视觉