文章目录
1.算法原理
Bayer 阵列每个像素只记录一种颜色,去马赛克(Demosaicing)通过插值恢复每个位置的完整 RGB 值。openISP 的CFA模块实现了 Malvar-He-Cutler (2004) 算法,这是经典的基于梯度修正的线性插值 方法。CFA就是把bayer域的图像转换成RGB域的图像

2.Malvar 算法
Malvar 算法的核心是:在基础双线性插值 的基础上,加入梯度修正项来减少伪彩色。每种 Bayer 位置(R、Gr、Gb、B)对应一组固定的 5×5 卷积系数。
下面以 在R 位置插值 G 和 B 为例(系数已除以 8):
示例:在 R 位置插值 G
当前像素为R,则给当前R插入G分量
G = (4×中心 - 上上 - 下下 - 左左 - 右右
+ 2×上 + 2×下 + 2×左 + 2×右) / 8
-
5x5卷积核为:
0 0 -1 0 0 0 0 2 0 0 -1 2 4 2 -1 0 0 2 0 0 0 0 -1 0 0
加权系数如下所示:

示例:在 R 位置插值 B
当前像素是R,给当前像素插入B分量
B = (6×中心 - 3/2×(上上+下下+左左+右右)
+ 2×(左上+右上+左下+右下)) / 8
5x5卷积核为:
0 0 -1.5 0 0
0 2 0 2 0
-1.5 0 6 0 -1.5
0 2 0 2 0
0 0 -1.5 0 0
其实加权系数如下所示:

各 Bayer 位置的插值方向
| 位置 | 已知颜色 | R 插值方向 | G 插值方向 | B 插值方向 |
|---|---|---|---|---|
| R | R | 直接 | 十字 | 对角 |
| Gr | G | 水平 | 直接 | 垂直 |
| Gb | G | 垂直 | 直接 | 水平 |
| B | B | 对角 | 十字 | 直接 |
上面说的都概括在下面这张图中(取至openISP官方文档), 对应8中卷积核.

3.CFA算法实现
实现细节
- 输入 padding 2 像素(reflect 模式)
- 图像转
int32防止中间计算溢出 (这里在写测试代码一直有溢出问题,我的代码中就先改成uint32类型) - 逐 2×2 Bayer 块遍历,一次处理 4 个像素
- 输出三通道 RGB,clip 到
[0, cfa_clip]
python
import numpy as np
class CFA:
'Color Filter Array Interpolation'
def __init__(self, img, mode, bayer_pattern, clip):
self.img = img
self.mode = mode
self.bayer_pattern = bayer_pattern
self.clip = clip
def padding(self):
img_pad = np.pad(self.img, ((2, 2), (2, 2)), 'reflect')
return img_pad
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
# z
def malvar(self, is_color, center, y, x, img):
if is_color == 'r':
r = center
g = 4 * img[y,x] - img[y-2,x] - img[y,x-2] - img[y+2,x] - img[y,x+2] \
+ 2 * (img[y+1,x] + img[y,x+1] + img[y-1,x] + img[y,x-1])
b = 6 * img[y,x] - 3 * (img[y-2,x] + img[y,x-2] + img[y+2,x] + img[y,x+2]) / 2 \
+ 2 * (img[y-1,x-1] + img[y-1,x+1] + img[y+1,x-1] + img[y+1,x+1])
g = g / 8
b = b / 8
elif is_color == 'gr':
r = 5 * img[y,x] - img[y,x-2] - img[y-1,x-1] - img[y+1,x-1] - img[y-1,x+1] - img[y+1,x+1] - img[y,x+2] \
+ (img[y-2,x] + img[y+2,x]) / 2 + 4 * (img[y,x-1] + img[y,x+1])
g = center
b = 5 * img[y,x] - img[y-2,x] - img[y-1,x-1] - img[y-1,x+1] - img[y+2,x] - img[y+1,x-1] - img[y+1,x+1] \
+ (img[y,x-2] + img[y,x+2]) / 2 + 4 * (img[y-1,x] + img[y+1,x])
r = r / 8
b = b / 8
elif is_color == 'gb':
r = 5 * img[y,x] - img[y-2,x] - img[y-1,x-1] - img[y-1,x+1] - img[y+2,x] - img[y+1,x-1] - img[y+1,x+1] \
+ (img[y,x-2] + img[y,x+2]) / 2 + 4 * (img[y-1,x] + img[y+1,x])
g = center
b = 5 * img[y,x] - img[y,x-2] - img[y-1,x-1] - img[y+1,x-1] - img[y-1,x+1] - img[y+1,x+1] - img[y,x+2] \
+ (img[y-2,x] + img[y+2,x]) / 2 + 4 * (img[y,x-1] + img[y,x+1])
r = r / 8
b = b / 8
elif is_color == 'b':
r = 6 * img[y,x] - 3 * (img[y-2,x] + img[y,x-2] + img[y+2,x] + img[y,x+2]) / 2 \
+ 2 * (img[y-1,x-1] + img[y-1,x+1] + img[y+1,x-1] + img[y+1,x+1])
g = 4 * img[y,x] - img[y-2,x] - img[y,x-2] - img[y+2,x] - img[y,x+2] \
+ 2 * (img[y+1,x] + img[y,x+1] + img[y-1,x] + img[y,x-1])
b = center
r = r / 8
g = g / 8
return [r, g, b]
def execute(self):
img_pad = self.padding()
img_pad = img_pad.astype(np.uint32)
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
cfa_img = np.empty((raw_h, raw_w, 3), np.uint16)
for y in range(0, img_pad.shape[0]-4-1, 2):
for x in range(0, img_pad.shape[1]-4-1, 2):
if self.bayer_pattern == 'rggb':
r = img_pad[y+2,x+2]
gr = img_pad[y+2,x+3]
gb = img_pad[y+3,x+2]
b = img_pad[y+3,x+3]
if self.mode == 'malvar':
cfa_img[y,x,:] = self.malvar('r', r, y+2,x+2, img_pad)
cfa_img[y,x+1,:] = self.malvar('gr', gr, y+2,x+3, img_pad)
cfa_img[y+1,x,:] = self.malvar('gb', gb, y+3,x+2, img_pad)
cfa_img[y+1,x+1,:] = self.malvar('b', b, y+3,x+3, img_pad)
elif self.bayer_pattern == 'bggr':
b = img_pad[y+2,x+2]
gb = img_pad[y+2,x+3]
gr = img_pad[y+3,x+2]
r = img_pad[y+3,x+3]
if self.mode == 'malvar':
cfa_img[y,x,:] = self.malvar('b', b, y+2,x+2, img_pad)
cfa_img[y,x+1,:] = self.malvar('gb', gb, y+2,x+3, img_pad)
cfa_img[y+1,x,:] = self.malvar('gr', gr, y+3,x+2, img_pad)
cfa_img[y+1,x+1,:] = self.malvar('r', r, y+3,x+3, img_pad)
elif self.bayer_pattern == 'gbrg':
gb = img_pad[y+2,x+2]
b = img_pad[y+2,x+3]
r = img_pad[y+3,x+2]
gr = img_pad[y+3,x+3]
if self.mode == 'malvar':
cfa_img[y,x,:] = self.malvar('gb', gb, y+2,x+2, img_pad)
cfa_img[y,x+1,:] = self.malvar('b', b, y+2,x+3, img_pad)
cfa_img[y+1,x,:] = self.malvar('r', r, y+3,x+2, img_pad)
cfa_img[y+1,x+1,:] = self.malvar('gr', gr, y+3,x+3, img_pad)
elif self.bayer_pattern == 'grbg':
gr = img_pad[y+2,x+2]
r = img_pad[y+2,x+3]
b = img_pad[y+3,x+2]
gb = img_pad[y+3,x+3]
if self.mode == 'malvar':
cfa_img[y,x,:] = self.malvar('gr', gr, y+2,x+2, img_pad)
cfa_img[y,x+1,:] = self.malvar('r', r, y+2,x+3, img_pad)
cfa_img[y+1,x,:] = self.malvar('b', b, y+3,x+2, img_pad)
cfa_img[y+1,x+1,:] = self.malvar('gb', gb, y+3,x+3, img_pad)
self.img = cfa_img
return self.clipping()
4.CFA 测试代码
'RGGB' bayer pattern 转RGB
python
def test_uniform_bayer_rgb_values(self):
h, w = 32, 32
# 第一组参数图像显示太白,不利于展示
#img = make_bayer_rggb(h, w, r_val=400, g_val=600, b_val=300)
img = make_bayer_rggb(h, w, r_val=200, g_val=300, b_val=400)
cfa = CFA(img.copy().astype(np.uint16), 'malvar', 'rggb', clip=1023)
out = cfa.execute()
show_bayer_RGB_images('rggb', img, out, "left", "right-rggb-malvar")
# 取中心区域避开边界插值误差
center = out[8:24, 8:24, :]
r_mean = float(center[:, :, 0].mean())
g_mean = float(center[:, :, 1].mean())
b_mean = float(center[:, :, 2].mean())
- 测试效果图
上面一开始在构建rggb bayer图像使用的是r_val=400, g_val=600, b_val=300,这个会有溢出风险,展示先改用暗光元素r_val=200, g_val=300, b_val=400, G和B分量最多,图像显示青绿色.下图中的左图使用opencv2的库直接根据bayer格式转成的RGB, 右边没有进行格式转换,直接显示的(我们转换后的图像就是RGB图像).

'BGGR' bayer pattern 转RGB
python
def test_bayer_pattern_bggr(self):
"""bggr 模式:B 在偶行偶列,R 在奇行奇列,R 通道值应接近 400。"""
h, w = 32, 32
#img = np.zeros((h, w), dtype=np.int32)
img = np.zeros((h, w), dtype=np.uint16)
img[0::2, 0::2] = 200 #300 # B
img[0::2, 1::2] = 300 #600 # Gb
img[1::2, 0::2] = 300 #600 # Gr
img[1::2, 1::2] = 100 #400 # R
cfa = CFA(img.copy(), 'malvar', 'bggr', clip=1023)
out = cfa.execute()
show_bayer_RGB_images('bggr', img, out, "left", "right-bggr-malvar")
center_r = float(out[8:24, 8:24, 0].mean())
self.assertAlmostEqual(center_r, 400, delta=20, msg=f"bggr R 均值 {center_r:.1f} 偏差过大")
- 测试效果图
构造的bayer图像,GR和GB分量最大,图像偏绿,下图中的左图使用opencv2的库直接根据bayer格式转成的RGB, 右边没有进行格式转换,直接显示的(我们转换后的图像就是RGB图像).

图像显示函数的更改
这个函数稍微更新了下,可以一起显示bayer图像和RGB图像. bayer图像需要根据patter格式,转成RGB图像再显示. 而第二个直接就是RGB图像,不用进行格式转换.
python
def show_bayer_RGB_images(pattern, img1_data, img2_data, name1="Image 1", name2="Image 2"):
"""
输入1张Bayer格式图像和1张RGB格式图像,将其转换为RGB后左右显示在桌面上。
参数:
pattern (str): img1_data 的Bayer排列模式 ("bggr", "rggb", "gbrg", "grbg")
img1_data (numpy.ndarray): 第一张图像的Bayer原始数据 (H, W)
img2_data (numpy.ndarray): 第二张图像的去马赛克后RGB数据 (H, W, 3)
name1 (str): 第一张图片的标题名称
name2 (str): 第二张图片的标题名称
"""
# --- 第一步:处理第一张图像 (Bayer 转 RGB) ---
# OpenCV默认使用BGR色彩空间,根据传入的pattern进行转换
if pattern == "bggr":
rgb_img1 = cv2.cvtColor(img1_data, cv2.COLOR_BAYER_BG2BGR)
#rgb_img2 = cv2.cvtColor(img2_data, cv2.COLOR_BAYER_BG2BGR)
elif pattern == "rggb":
rgb_img1 = cv2.cvtColor(img1_data, cv2.COLOR_BAYER_RG2BGR)
#rgb_img2 = cv2.cvtColor(img2_data, cv2.COLOR_BAYER_RG2BGR)
elif pattern == "gbrg":
rgb_img1 = cv2.cvtColor(img1_data, cv2.COLOR_BAYER_GB2BGR)
#rgb_img2 = cv2.cvtColor(img2_data, cv2.COLOR_BAYER_GB2BGR)
elif pattern == "grbg":
rgb_img1 = cv2.cvtColor(img1_data, cv2.COLOR_BAYER_GR2BGR)
#rgb_img2 = cv2.cvtColor(img2_data, cv2.COLOR_BAYER_GR2BGR)
# --- 第二步:处理第二张图像 (已经是RGB格式) ---
# 如果img2_data是由OpenCV生成的BGR图像,请取消下面这行注释进行转换:
# img2_data = cv2.cvtColor(img2_data, cv2.COLOR_BGR2RGB)
# 注意这里没有进行格式转换,直接显示
rgb_img2 = img2_data
# --- 第三步:创建画布并左右显示 ---
fig, axs = plt.subplots(1, 2, figsize=(8, 4))
# 在左侧子图显示第一张图片
axs[0].imshow(rgb_img1)
axs[0].set_title(name1)
axs[0].axis('off') # 隐藏坐标轴
# 在右侧子图显示第二张图片
axs[1].imshow(rgb_img2)
axs[1].set_title(name2)
axs[1].axis('off') # 隐藏坐标轴
# 自动调整布局,防止标题重叠
plt.tight_layout()
# 弹出窗口显示
plt.show()