文章目录
1.算法原理
在 Bayer 域中,R 和 B 通道的采样密度是 G 通道的一半,因此更容易受到噪声影响。色度噪声表现为 R/B 像素相对于周围 G 像素异常偏高。CNF 在去马赛克之前直接在 Bayer 域中检测并校正这种噪声。
噪声检测(CND)
在 8×8 邻域内,在实际python代码一般使用 range(-4, 4)来遍历(这里更容易表现出像素坐标偏移,我们需要统计当前像素上下左右的像素),分别统计三类像素的均值:
- avgG:绿色像素平均值(偶行奇列 + 奇行偶列,共 40 个)
- avgC1:同行同色(R 或 B)均值(25 个)
- avgC2:对角同色均值(16 个)
检测条件: 当前像素 center 与 avgG 和 avgC2 的差值均超过阈值 thres,且 avgC1 也同样异常时,判定为色度噪声。
下面是8x8的bayer图像,从这里直观看出上面3个方向的像素个数,但这里能看到像素的个数和上面对应的像素个数不一样,这里留个疑问吧 ? 先继续

噪声校正(CNC)
上面检测到色度异常像素后,然后校正采用自适应融合策略:
- 阻尼因子(dampFactor) 根据 AWB 增益自适应调整(增益越大,阻尼越强):
- r_gain ≤ 1.0 → dampFactor = 1.0
- r_gain ∈ (1.0, 1.2] → dampFactor = 0.5
- r_gain > 1.2 → dampFactor = 0.3
- 色度校正值:
chromaCorrected = max(avgG, avgC2) + dampFactor × (center - max(avgG, avgC2))
-
亮度衰减因子(fade1, fade2) 在亮区减小校正强度(避免破坏高亮细节)
-
最终输出:
output = (1 - fadeTot) × center + fadeTot × chromaCorrected
2.算法代码
实现细节:
- 仅对 R 和 B 通道进行校正,G 通道直接保留
- 边界 padding 为 4 像素(reflect 模式)
- 逐 2×2 Bayer 块遍历
python
class CNF:
'Chroma Noise Filtering'
def __init__(self, img, bayer_pattern, thres, gain, clip):
self.img = img
self.bayer_pattern = bayer_pattern
self.thres = thres
self.gain = gain
self.clip = clip
def padding(self):
img_pad = np.pad(self.img, ((4, 4), (4, 4)), 'reflect')
return img_pad
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def cnc(self, is_color, center, avgG, avgC1, avgC2):
'Chroma Noise Correction'
r_gain = self.gain[0]
gr_gain = self.gain[1]
gb_gain = self.gain[2]
b_gain = self.gain[3]
dampFactor = 1.0
signalGap = center - max(avgG, avgC2)
if is_color == 'r':
if r_gain <= 1.0:
dampFactor = 1.0
elif r_gain > 1.0 and r_gain <= 1.2:
dampFactor = 0.5
elif r_gain > 1.2:
dampFactor = 0.3
elif is_color == 'b':
if b_gain <= 1.0:
dampFactor = 1.0
elif b_gain > 1.0 and b_gain <= 1.2:
dampFactor = 0.5
elif b_gain > 1.2:
dampFactor = 0.3
chromaCorrected = max(avgG, avgC2) + dampFactor * signalGap
if is_color == 'r':
signalMeter = 0.299 * avgC1 + 0.587 * avgG + 0.114 * avgC2
elif is_color == 'b':
signalMeter = 0.299 * avgC2 + 0.587 * avgG + 0.114 * avgC1
if signalMeter <= 30:
fade1 = 1.0
elif signalMeter > 30 and signalMeter <= 50:
fade1 = 0.9
elif signalMeter > 50 and signalMeter <= 70:
fade1 = 0.8
elif signalMeter > 70 and signalMeter <= 100:
fade1 = 0.7
elif signalMeter > 100 and signalMeter <= 150:
fade1 = 0.6
elif signalMeter > 150 and signalMeter <= 200:
fade1 = 0.3
elif signalMeter > 200 and signalMeter <= 250:
fade1 = 0.1
else:
fade1 = 0
if avgC1 <= 30:
fade2 = 1.0
elif avgC1 > 30 and avgC1 <= 50:
fade2 = 0.9
elif avgC1 > 50 and avgC1 <= 70:
fade2 = 0.8
elif avgC1 > 70 and avgC1 <= 100:
fade2 = 0.6
elif avgC1 > 100 and avgC1 <= 150:
fade2 = 0.5
elif avgC1 > 150 and avgC1 <= 200:
fade2 = 0.3
elif avgC1 > 200:
fade2 = 0
fadeTot = fade1 * fade2
center_out = (1 - fadeTot) * center + fadeTot * chromaCorrected
return center_out
def cnd(self, y, x, img):
'Chroma Noise Detection'
avgG = 0
avgC1 = 0
avgC2 = 0
is_noise = 0
for i in range(y - 4, y + 4, 1):
for j in range(x - 4, x + 4, 1):
if i % 2 == 1 and j % 2 == 0:
avgG = avgG + img[i,j]
elif i % 2 == 0 and j % 2 == 1:
avgG = avgG + img[i, j]
elif i % 2 == 0 and j % 2 == 0:
avgC1 = avgC1 + img[i,j] # weights are equal, could be as gaussian dist
elif i % 2 == 1 and j % 2 == 1:
avgC2 = avgC2 + img[i,j]
avgG = avgG / 40
avgC1 = avgC1 / 25
avgC2 = avgC2 / 16
center = img[y, x]
if center > avgG + self.thres and center > avgC2 + self.thres:
if avgC1 > avgG + self.thres and avgC1 > avgC2 + self.thres:
is_noise = 1
else:
is_noise = 0
else:
is_noise = 0
return is_noise, avgG, avgC1, avgC2
def cnf(self, is_color, y, x, img):
is_noise, avgG, avgC1, avgC2 = self.cnd(y, x, img)
if is_noise:
pix_out = self.cnc(is_color, img[y,x], avgG, avgC1, avgC2)
else:
pix_out = img[y,x]
return pix_out
def execute(self):
img_pad = self.padding()
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
cnf_img = np.empty((raw_h, raw_w), np.uint16)
# 以下能看到,X,y坐标去掉四周的margin, 以及x和y的方向都是每次+2的不要搞错了.
for y in range(0, img_pad.shape[0] - 8 - 1, 2):
for x in range(0, img_pad.shape[1] - 8 - 1, 2):
if self.bayer_pattern == 'rggb': #下面示例代码就是这个patter
r = img_pad[y + 4, x + 4]
gr = img_pad[y + 4, x + 5]
gb = img_pad[y + 5, x + 4]
b = img_pad[y + 5, x + 5]
cnf_img[y, x] = self.cnf('r', y + 4, x + 4, img_pad)
cnf_img[y, x + 1] = gr
cnf_img[y + 1, x] = gb
cnf_img[y + 1, x + 1] = self.cnf('b', y + 5, x + 5, img_pad)
elif self.bayer_pattern == 'bggr':
b = img_pad[y + 4, x + 4]
gb = img_pad[y + 4, x + 5]
gr = img_pad[y + 5, x + 4]
r = img_pad[y + 5, x + 5]
cnf_img[y, x] = self.cnf('b', y + 4, x + 4, img_pad)
cnf_img[y, x + 1] = gb
cnf_img[y + 1, x] = gr
cnf_img[y + 1, x + 1] = self.cnf('r', y + 5, x + 5, img_pad)
elif self.bayer_pattern == 'gbrg':
gb = img_pad[y + 4, x + 4]
b = img_pad[y + 4, x + 5]
r = img_pad[y + 5, x + 4]
gr = img_pad[y + 5, x + 5]
cnf_img[y, x] = gb
cnf_img[y, x + 1] = self.cnf('b', y + 4, x + 5, img_pad)
cnf_img[y + 1, x] = self.cnf('r', y + 5, x + 4, img_pad)
cnf_img[y + 1, x + 1] = gr
elif self.bayer_pattern == 'grbg':
gr = img_pad[y + 4, x + 4]
r = img_pad[y + 4, x + 5]
b = img_pad[y + 5, x + 2]
gb = img_pad[y + 5, x + 5]
cnf_img[y, x, :] = gr
cnf_img[y, x + 1, :] = self.cnf('r', y + 4, x + 5, img_pad)
cnf_img[y + 1, x, :] = self.cnf('b', y + 5, x + 4, img_pad)
cnf_img[y + 1, x + 1, :] = gb
self.img = cnf_img
return self.clipping()
3. 算法测试代码
python
def test_spike_noise_suppressed(self):
"""
CNF 检测+修正的完整路径验证。
设计条件:
- R 背景 = 80,G/B 背景 = 50
- 单个 R 尖峰 = 300
- 在 8×8 扫描窗口内:avgC1 ≈ 60,avgG ≈ 40,avgC2 ≈ 50
- 满足检测条件(300>50, 60>50)且 fade2 > 0(avgC1=60 ∈ (50,70])
- 期望 center_out ≈ 174 < 300(修正发生)
"""
img = np.zeros((24, 24), dtype=np.uint16)
img[0::2, 0::2] = 80 #80 # R 背景(全图 R 通道中等值)
img[0::2, 1::2] = 50 # Gr
img[1::2, 0::2] = 50 # Gb
img[1::2, 1::2] = 50 # B
# 在偶行偶列位置注入单个 R 尖峰
img[8, 8] = 500 #300
gains = [1.5, 1.0, 1.0, 1.1]
cnf = CNF(img.copy(), 'rggb', thres=0, gain=gains, clip=1023)
out = cnf.execute()
show_bayer_images('rggb', img, out, "left", "right-rggb-cnf")
self.assertLess(int(out[8, 8]), 300,
f"CNF 未能抑制 R 尖峰,输出值 {out[8, 8]}")
- 测试效果
能看看到我们把8,8的R通道的像素置为一个很亮的值,这里图片已经经过bayer->RGB了,所以左侧图片看到8,8邻域内像素被拉高. 经过CNF后,8,8像素R通道拉低,变为174.符合预期
