图像去噪#DCNN去噪Python代码 双边滤波、双边滤波、巴特沃斯高通滤波、同态滤波、各项异性扩散滤波、均值滤波、维纳滤波、小波三级软阈值去噪、自适应中值滤波、高斯滤波 MATLAB程序代码

前阵子翻到小时候拍的胶卷扫描照,噪点密得像撒了一把细盐,本来想靠手机修图APP磨个皮就完,结果越磨人脸都糊成马赛克了,才想起不如自己写点简单的去噪脚本试试——这就整理了最近摸鱼试的十几种常用去噪方法,从傻瓜式的一键滤波到自己搭的DCNN都有,代码也直接贴出来,嫌麻烦的直接抄就能用。


先从入门级的线性滤波玩起

最基础的就是均值滤波和高斯滤波,属于无脑好用的类型:

  • 均值滤波就是把窗口里所有像素直接拉平均,不管邻域里的像素差多少,适合均匀噪点但容易糊边缘
  • 高斯滤波给邻域像素加了高斯权重,离中心越近的像素越管用,保留细节比均值强太多
import cv2
import numpy as np

noisy_img = cv2.imread("old_photo.jpg")

# 均值滤波,5x5窗口就是周围25个像素平均
mean_filtered = cv2.blur(noisy_img, (5,5))
# 高斯滤波,sigmaX控制高斯分布的宽度,越大越模糊
gaussian_filtered = cv2.GaussianBlur(noisy_img, (5,5), sigmaX=1.5)

cv2.imwrite("mean_result.jpg", mean_filtered)
cv2.imwrite("gaussian_result.jpg", gaussian_filtered)

我当时跑自己的猫片时发现,高斯滤波对付均匀的高斯白噪真的一绝,拍夜景的彩噪压得干干净净,还没把猫的胡须磨糊,但要是碰到那种突然的黑白椒盐噪,这俩直接拉胯。


对付椒盐噪就靠中值滤波

椒盐噪就是那种突然冒出来的黑白点,像素值要么特别亮要么特别暗,普通线性滤波根本搞不定,这时候就得靠中值滤波——把窗口里的像素排序,取中间值替换中心像素,噪点直接被筛掉。

图像去噪#DCNN去噪Python代码 双边滤波、双边滤波、巴特沃斯高通滤波、同态滤波、各项异性扩散滤波、均值滤波、维纳滤波、小波三级软阈值去噪、自适应中值滤波、高斯滤波 MATLAB程序代码

普通中值滤波会把细线条磨平,所以我更爱用自适应中值,会自动调整窗口大小:小窗口对付小噪点,大窗口对付密集噪点,还能保住发丝、边缘这类细节。

def adaptive_median_filter(img, s_max=7):
    h, w, c = img.shape
    res = np.zeros_like(img)
    for channel in range(c):
        for i in range(h):
            for j in range(w):
                s = 1
                while s <= s_max:
                    # 自动调整窗口范围,别超出图片边界
                    top = max(0, i - s)
                    bottom = min(h-1, i + s)
                    left = max(0, j - s)
                    right = min(w-1, j + s)
                    window = img[top:bottom+1, left:right+1, channel].flatten()
                    window_sorted = np.sort(window)
                    med = window_sorted[len(window_sorted)//2]
                    min_val, max_val = window_sorted[0], window_sorted[-1]
                    
                    # 判断当前像素是不是噪点,不是就保留原图
                    if min_val < med < max_val:
                        if min_val < img[i,j,channel] < max_val:
                            res[i,j,channel] = img[i,j,channel]
                        else:
                            res[i,j,channel] = med
                        break
                    else:
                        s +=1
                # 到最大窗口还没搞定就直接用中值
                if s > s_max:
                    res[i,j,channel] = med
    return res

# 跑起来试试
adaptive_median_result = adaptive_median_filter(noisy_img)
cv2.imwrite("adaptive_median.jpg", adaptive_median_result)

亲测这个代码对付密集椒盐噪特别好用,连猫的胡须都没被磨平,比我之前用的手机修图APP强多了。


保边去噪选双边滤波

之前用这个给奶奶的老照片去噪,她脸上的皱纹一点没被磨掉,比高斯滤波靠谱太多。原理就是不仅看像素的空间距离,还看像素值的差异:边缘的像素值差大,权重就低,所以不会把边缘糊掉。

# opencv自带的函数,参数d是窗口直径,sigmaColor和sigmaSpace控制滤波强度
bilateral_filtered = cv2.bilateralFilter(noisy_img, d=9, sigmaColor=75, sigmaSpace=75)
cv2.imwrite("bilateral.jpg", bilateral_filtered)

唯一的缺点就是慢,毕竟要算每个像素的空间和颜色权重,处理大尺寸图片得等一会儿,但日常修修小图完全ok。


对付光照不均的同态滤波

老照片经常一边亮一边暗,还带着扫描的网点噪点,这时候同态滤波就派上用场了。它会把图像拆成光照分量和反射分量,先把光照拉平,再去掉噪点,一举两得。

from skimage import exposure, img_as_float

def homomorphic_filter(img, cutoff=30, gamma_h=1.5, gamma_l=0.5):
    img_float = img_as_float(img)
    # 取对数分离光照和反射分量
    img_log = np.log1p(img_float)
    f = np.fft.fft2(img_log)
    fshift = np.fft.fftshift(f)
    
    # 构建巴特沃斯高通滤波器,压低频光照分量
    rows, cols = img.shape[:2]
    crow, ccol = rows//2, cols//2
    mask = 1 - np.exp(-((np.arange(rows)[:,None]-crow)**2 + (np.arange(cols)-ccol)**2)/(2*cutoff**2))
    
    fshift_filtered = fshift * mask
    img_back = np.real(np.fft.ifft2(np.fft.ifftshift(fshift_filtered)))
    # 取指数还原图像
    img_exp = np.expm1(img_back)
    return exposure.rescale_intensity(img_exp, out_range=(0,1))*255

# 处理灰度图效果最好
homomorphic_result = homomorphic_filter(cv2.cvtColor(noisy_img, cv2.COLOR_BGR2GRAY))
cv2.imwrite("homomorphic.jpg", homomorphic_result)

我一般把cutoff设成30左右,太大容易把细节滤掉,太小又拉不平光照,调起来也很省心。


频域滤波选手:巴特沃斯高通滤波+维纳滤波

巴特沃斯高通滤波

专门用来去掉低频的平滑噪点,比如扫描的网点纹、背景的模糊噪点,也可以用来提取图像边缘:

from scipy import ndimage

def butterworth_highpass(img, cutoff=10, order=2):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    f = np.fft.fft2(img_gray)
    fshift = np.fft.fftshift(f)
    rows, cols = img_gray.shape
    crow, ccol = rows//2, cols//2
    
    # 巴特沃斯高通掩码
    mask = 1 / (1 + (cutoff / np.sqrt((np.arange(rows)[:,None]-crow)**2 + (np.arange(cols)-ccol)**2)))**(2*order)
    img_back = np.real(np.fft.ifft2(np.fft.ifftshift(fshift*mask)))
    return np.uint8(255*(img_back - img_back.min())/(img_back.max()-img_back.min()))
维纳滤波

比高斯滤波更智能的统计滤波,会估计原始图像和噪声的功率谱,适合已知噪声方差的场景,比如热成像图、相机噪点:

from skimage.restoration import wiener

img_gray = cv2.cvtColor(noisy_img, cv2.COLOR_BGR2GRAY)
# 先估计一下噪声方差
noise_var = np.var(img_gray - cv2.GaussianBlur(img_gray, (5,5), 1.5))
wiener_result = wiener(img_gray, balance=noise_var/0.1)
cv2.imwrite("wiener.jpg", wiener_result*255)

更硬核的保边:各项异性扩散滤波

也就是Perona-Malik滤波,比双边滤波的保边效果还强,会根据像素梯度调整扩散速度:边缘梯度大的地方扩散慢,不会磨糊边缘。

def perona_malik_filter(img, iterations=50, k=50, option=1):
    img = img_as_float(img)
    if len(img.shape) ==3:
        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    h, w = img.shape
    I = img.copy()
    for _ in range(iterations):
        # 计算四个方向的像素梯度
        N = np.zeros_like(I); S = np.zeros_like(I)
        E = np.zeros_like(I); W = np.zeros_like(I)
        N[1:,:] = I[:-1,:] - I[1:,:]
        S[:-1,:] = I[1:,:] - I[:-1,:]
        E[:,1:] = I[:,:-1] - I[:,1:]
        W[:,:-1] = I[:,1:] - I[:,:-1]
        
        # 计算权重,两种可选方案
        if option ==1:
            c = np.exp(-(np.abs([N,S,E,W]))/k)
        else:
            c = 1/(1 + (np.abs([N,S,E,W])/k)**2)
        
        # 更新图像
        I = I + 0.25*(c[0]*N + c[1]*S + c[2]*E + c[3]*W)
    return np.uint8(I*255)

pm_result = perona_malik_filter(noisy_img, iterations=30, k=30)
cv2.imwrite("pm_filter.jpg", pm_result)

这个处理星空图特别好用,星星的边缘一点没糊,噪点全没了,就是迭代次数多的话会慢一点,不过胜在效果自然。


小波三级软阈值去噪

属于频域去噪的天花板之一,把图像分解成小波系数,把小的噪点系数阈值掉,再重构图像,对高斯白噪的效果特别好。

import pywt

def wavelet_soft_threshold_filter(img, wavelet='db4', level=3, threshold=0.2):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # 小波三级分解
    coeffs = pywt.wavedec2(img_gray, wavelet, level=level)
    # 软阈值处理细节系数
    for i in range(1, level+1):
        cA, (cH, cV, cD) = coeffs[i-1], coeffs[i]
        coeffs[i] = (cA, (pywt.threshold(cH, threshold, 'soft'),
                          pywt.threshold(cV, threshold, 'soft'),
                          pywt.threshold(cD, threshold, 'soft')))
    # 重构图像
    img_recon = pywt.waverec2(coeffs, wavelet)
    return np.uint8(np.clip(img_recon,0,1)*255)

wavelet_result = wavelet_soft_threshold_filter(noisy_img, threshold=0.15)
cv2.imwrite("wavelet.jpg", wavelet_result)

阈值别设太大,不然会把细节磨没,我一般用0.1到0.2之间的数,小波基用db4就够用,不用搞太复杂的。


最后上深度学习:DCNN去噪

传统方法再强也不如深度学习的效果好,这里贴一个极简的U-Net风格去噪网络,适合新手快速上手:

import torch
import torch.nn as nn
import torch.nn.functional as F

class DenoiseCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        self.conv2 = nn.Conv2d(64, 64, 3, padding=1)
        self.conv3 = nn.Conv2d(64, 3, 3, padding=1)
    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = self.conv3(x)
        return x

# 推理的时候直接加载训练好的模型就行
def dcnn_denoise(img, model_path="denoise_model.pth"):
    model = DenoiseCNN()
    model.load_state_dict(torch.load(model_path, map_location="cpu"))
    model.eval()
    
    img_tensor = torch.from_numpy(img.transpose(2,0,1)).float()/255.0
    img_tensor = img_tensor.unsqueeze(0)
    
    with torch.no_grad():
        out = model(img_tensor)
    out_img = np.clip(out.squeeze(0).numpy().transpose(1,2,0),0,1)*255
    return np.uint8(out_img)

这个需要用带噪和干净的数据集训练,我之前用DIV2K训了一个小模型,处理手机拍的噪点图,效果比所有传统方法都好,就是对新手不太友好,不过套现成的预训练模型就能直接用。


补一个MATLAB版的常用代码

很多做图像处理的同学还是习惯用MATLAB,贴个自适应中值滤波的MATLAB版:

function img_out = adaptive_median_filter(img, s_max)
    if nargin < 2
        s_max = 7;
    end
    [h, w, c] = size(img);
    img_out = zeros(h, w, c, 'like', img);
    for ch = 1:c
        for i = 1:h
            for j = 1:w
                s = 1;
                while s <= s_max
                    top = max(1, i - s);
                    bottom = min(h, i + s);
                    left = max(1, j - s);
                    right = min(w, j + s);
                    window = img(top:bottom, left:right, ch);
                    window_sorted = sort(window(:));
                    med = window_sorted(floor(length(window_sorted)/2)+1);
                    min_val = window_sorted(1);
                    max_val = window_sorted(end);
                    
                    if min_val < med && med < max_val
                        if min_val < img(i,j,ch) && img(i,j,ch) < max_val
                            img_out(i,j,ch) = img(i,j,ch);
                        else
                            img_out(i,j,ch) = med;
                        end
                        break;
                    else
                        s = s + 1;
                    end
                end
                if s > s_max
                    img_out(i,j,ch) = med;
                end
            end
        end
    end
end
% 调用示例
% noisy_img = imread('old_photo.jpg');
% img_filtered = adaptive_median_filter(noisy_img);
% imwrite(img_filtered, 'adaptive_median_matlab.jpg');

其实这些方法没有绝对的好坏,得看你的噪点类型是什么:比如椒盐噪就用中值滤波,高斯白噪就用小波或者高斯滤波,光照不均的老照片就用同态滤波,要是追求效果又有训练数据就上DCNN。我一般都是先试传统方法,不行再上深度学习,毕竟调参比训模型快多了😎

要是你有更好的去噪方法,或者调参踩过的坑,欢迎在评论区唠唠。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐