一、Image Signal Processor
图像信号处理器(ISP)是进行数字图像处理的应用处理器,专门用于做从RAW图像到RGB/YUV图像的转换。
本篇文章主要对ISP Pipeline做一个整体概述,让大家对整个流程有一个整体的概念。
如下图所示,ISP处理分为Raw域处理、GRB域处理和YUV域处理,Raw域的处理包括坏点矫正、黑电平矫正、镜头阴影矫正、自动白平衡、去马赛克;RGB域处理包括Gamma曲线矫正、色彩矫正矩阵、色彩空间转换;YUV域处理包括亮度降噪、色彩降噪、边缘增强、色调饱和度控制、对比度亮度调节。
二、ISP之Raw域处理
RAW域(RAW Domain),英文意思是原始的,也就是刚从sensor输出的图。
2.1 坏点矫正(Dead Pixel Correction)
坏点一般是由于制造工艺的问题造成的,它是指某个像素点与周围像素有较大的差异。一般是在全黑的环境下出现白点或者高亮的环境下出现黑点。
基于缺陷像素的状态,它们分为静态坏点(始终恒定)和动态坏点(受曝光或温度等条件的影响而变化)。 坏点矫正分两步进行,第一步是坏点检测,第二步通过插值替换坏点。
对于静态坏点,拍一张全黑图像和全白图像既可以确定坏点位置。
对于动态坏点,通常通过与域邻域像素进行对比来定位。具体方法如下:
计算中心像素和相邻 8 个像素之间的差异:
事先定义一个阈值来判断是否是坏点:
完成坏点检测后,即可执行校正。这里列出了两个插值方法。一个是均值滤波器,另一个是基于梯度的滤波器。 对于均值滤波,相对简单:
对于基于梯度的滤波器,首先计算相邻像素在不同方向上的梯度。
从上图中,计算出四个方向的梯度:
输出像素是所选方向上相邻像素的平均值:
参考代码:
#!/usr/bin/python
import numpy as np
class DPC:
'Dead Pixel Correction'
def __init__(self, img, thres, mode, clip):
self.img = img
self.thres = thres
self.mode = mode
self.clip = clip
def padding(self):
img_pad = np.pad(self.img, (2, 2), 'reflect')
return img_pad
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def execute(self):
"""
Pixel array in code is showed above:
p1 p2 p3
p4 p0 p5
p6 p7 p8
it makes sense for calculating follow-up gradients of pixel values (horizontal,vertical,left/right diagonal).
"""
img_pad = self.padding()
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
dpc_img = np.empty((raw_h, raw_w), np.uint16)
# change uint16 to int_, still exists overflow warning in the following abs calculation
for y in range(img_pad.shape[0] - 4):
for x in range(img_pad.shape[1] - 4):
p0 = img_pad[y + 2, x + 2].astype(int)
p1 = img_pad[y, x].astype(int)
p2 = img_pad[y, x + 2].astype(int)
p3 = img_pad[y, x + 4].astype(int)
p4 = img_pad[y + 2, x].astype(int)
p5 = img_pad[y + 2, x + 4].astype(int)
p6 = img_pad[y + 4, x].astype(int)
p7 = img_pad[y + 4, x + 2].astype(int)
p8 = img_pad[y + 4, x + 4].astype(int)
if (abs(p1 - p0) > self.thres) and (abs(p2 - p0) > self.thres) and (abs(p3 - p0) > self.thres) \
and (abs(p4 - p0) > self.thres) and (abs(p5 - p0) > self.thres) and (abs(p6 - p0) > self.thres) \
and (abs(p7 - p0) > self.thres) and (abs(p8 - p0) > self.thres):
if self.mode == 'mean':
p0 = (p2 + p4 + p5 + p7) / 4
elif self.mode == 'gradient':
dv = abs(2 * p0 - p2 - p7)
dh = abs(2 * p0 - p4 - p5)
ddl = abs(2 * p0 - p1 - p8)
ddr = abs(2 * p0 - p3 - p6)
if (min(dv, dh, ddl, ddr) == dv):
p0 = (p2 + p7 + 1) / 2
elif (min(dv, dh, ddl, ddr) == dh):
p0 = (p4 + p5 + 1) / 2
elif (min(dv, dh, ddl, ddr) == ddl):
p0 = (p1 + p8 + 1) / 2
else:
p0 = (p3 + p6 + 1) / 2
dpc_img[y, x] = p0.astype('uint16')
self.img = dpc_img
return self.clipping()
2.2 黑电平补偿(Black Level Compensation)
黑电平本身的定义是黑色的最低值,也就是说sensor感光为零的时候输出的值就是黑电平。形成原因有以下两种:
-
sensor将模拟信号转换到数字信号时,由于转换精度限制无法将电压值很小的一部分给区分开来,故需要加上一个值来保证图像暗部细节。
-
电流噪声的干扰,只要sensor工作必然存在电流噪声干扰。这样即使全黑环境,sensor输出依然有信号强度,量化后必然不为零。
补偿方法就是在现有基础上减去一个值,这个值可以使用像素区前几行不感光的区域平均值作为校正值进行补偿。常用的补偿公式如下:
参考代码:
#!/usr/bin/python
import numpy as np
class BLC:
'Black Level Compensation'
def __init__(self, img, parameter, bayer_pattern, clip):
self.img = img
self.parameter = parameter
self.bayer_pattern = bayer_pattern
self.clip = clip
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def execute(self):
bl_r = self.parameter[0]
bl_gr = self.parameter[1]
bl_gb = self.parameter[2]
bl_b = self.parameter[3]
alpha = self.parameter[4]
beta = self.parameter[5]
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
blc_img = np.empty((raw_h,raw_w), np.int16)
if self.bayer_pattern == 'rggb':
r = self.img[::2, ::2] + bl_r
b = self.img[1::2, 1::2] + bl_b
gr = self.img[::2, 1::2] + bl_gr + alpha * r / 256
gb = self.img[1::2, ::2] + bl_gb + beta * b / 256
blc_img[::2, ::2] = r
blc_img[::2, 1::2] = gr
blc_img[1::2, ::2] = gb
blc_img[1::2, 1::2] = b
elif self.bayer_pattern == 'bggr':
b = self.img[::2, ::2] + bl_b
r = self.img[1::2, 1::2] + bl_r
gb = self.img[::2, 1::2] + bl_gb + beta * b / 256
gr = self.img[1::2, ::2] + bl_gr + alpha * r / 256
blc_img[::2, ::2] = b
blc_img[::2, 1::2] = gb
blc_img[1::2, ::2] = gr
blc_img[1::2, 1::2] = r
elif self.bayer_pattern == 'gbrg':
b = self.img[::2, 1::2] + bl_b
r = self.img[1::2, ::2] + bl_r
gb = self.img[::2, ::2] + bl_gb + beta * b / 256
gr = self.img[1::2, 1::2] + bl_gr + alpha * r / 256
blc_img[::2, ::2] = gb
blc_img[::2, 1::2] = b
blc_img[1::2, ::2] = r
blc_img[1::2, 1::2] = gr
elif self.bayer_pattern == 'grbg':
r = self.img[::2, 1::2] + bl_r
b = self.img[1::2, ::2] + bl_b
gr = self.img[::2, ::2] + bl_gr + alpha * r / 256
gb = self.img[1::2, 1::2] + bl_gb + beta * b / 256
blc_img[::2, ::2] = gr
blc_img[::2, 1::2] = r
blc_img[1::2, ::2] = b
blc_img[1::2, 1::2] = gb
self.img = blc_img
return self.clipping()
2.3 镜头阴影矫正(Lens Shading Correction)
在透镜的边缘,光线与透镜的光轴有很大的角度。光的强度随着光轴距离的增加而降低。同时,不同颜色的折射率也不同。因此,镜头阴影会导致两种阴影,即亮度阴影和颜色阴影。
亮度阴影导致图像强度不均匀。与图像中心相比,图像的角落较暗。
色彩阴影会导致图像的颜色失真。这通常反映了图像中心和角落的颜色不一致。
镜头阴影校正的本质是能量有衰减,反过来为了矫正就用该点的像素值乘以一个gain值,让其恢复到衰减前的状态,所以矫正的本质就是找到这个gain值。常用的方法存储增益法、多项式拟合法。
2.4 自动白平衡(Auto White Blance Gain Control)
引入这个模块是由于人眼具有色彩恒常性,就是说人眼感知物体的颜色不会因为色温产生变化。
但是sensor并没有人眼这么强大,sensor在不同色温下白色会呈现出不同的颜色,例如晴朗的天空(高色温)下会偏蓝,烛光(低色温)下会偏红。
为了模拟人眼的成像效果,保证任何色温场景下都是白色,引入了AWB这个模块。
当前AWB主要使用的方法是灰度世界法,完美反射法。
灰度世界法:在任何一个图像中,只要色彩足够变化,它的RGB三个分量的值会趋于相等,也就是灰色。因此,它会根据每个颜色通道的平均值来得到一个增益值,对每个像素都使用这个增益值进行校正。
完美反射法:利用图片中最亮的点,也就是RGB值最大的点,计算得到增益值,再对每个像素都使用这个增益值进行校正。
参考代码:
#!/usr/bin/python
import numpy as np
class WBGC:
'Auto White Balance Gain Control'
def __init__(self, img, parameter, bayer_pattern, clip):
self.img = img
self.parameter = parameter
self.bayer_pattern = bayer_pattern
self.clip = clip
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def execute(self):
r_gain = self.parameter[0]
gr_gain = self.parameter[1]
gb_gain = self.parameter[2]
b_gain = self.parameter[3]
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
awb_img = np.empty((raw_h, raw_w), np.int16)
if self.bayer_pattern == 'rggb':
r = self.img[::2, ::2] * r_gain
b = self.img[1::2, 1::2] * b_gain
gr = self.img[::2, 1::2] * gr_gain
gb = self.img[1::2, ::2] * gb_gain
awb_img[::2, ::2] = r
awb_img[::2, 1::2] = gr
awb_img[1::2, ::2] = gb
awb_img[1::2, 1::2] = b
elif self.bayer_pattern == 'bggr':
b = self.img[::2, ::2] * b_gain
r = self.img[1::2, 1::2] * r_gain
gb = self.img[::2, 1::2] * gb_gain
gr = self.img[1::2, ::2] * gr_gain
awb_img[::2, ::2] = b
awb_img[::2, 1::2] = gb
awb_img[1::2, ::2] = gr
awb_img[1::2, 1::2] = r
elif self.bayer_pattern == 'gbrg':
b = self.img[::2, 1::2] * b_gain
r = self.img[1::2, ::2] * r_gain
gb = self.img[::2, ::2] * gb_gain
gr = self.img[1::2, 1::2] * gr_gain
awb_img[::2, ::2] = gb
awb_img[::2, 1::2] = b
awb_img[1::2, ::2] = r
awb_img[1::2, 1::2] = gr
elif self.bayer_pattern == 'grbg':
r = self.img[::2, 1::2] * r_gain
b = self.img[1::2, ::2] * b_gain
gr = self.img[::2, ::2] * gr_gain
gb = self.img[1::2, 1::2] * gb_gain
awb_img[::2, ::2] = gr
awb_img[::2, 1::2] = r
awb_img[1::2, ::2] = b
awb_img[1::2, 1::2] = gb
self.img = awb_img
return self.clipping()
2.5 去马赛克
彩色图像在每个像素位置至少需要三个颜色样本。计算机图像通常使用红色、绿色和蓝色。一台相机需要三个独立的传感器才能完全测量图像。
使用多个传感器来检测可见光谱的不同部分需要对进入相机的光线进行分离,以便将场景成像到每个传感器上。然后需要精确配准以对齐三个图像。这些额外的要求给系统增加了一大笔费用。
因此,许多相机使用带有彩色滤光片阵列的单个传感器阵列。彩色滤光片阵列只允许光谱的一部分传递到传感器,因此每个像素只测量一种颜色。这意味着相机必须估计每个像素上缺少的两个颜色值。此过程称为去马赛克。
最常见的阵列是拜耳滤色片阵列。拜耳阵列测量梅花网格上的绿色图像和矩形网格上的红色和蓝色图像。绿色图像是以更高的采样率测量的,因为人类视觉系统的峰值灵敏度位于中波长,对应于光谱的绿色部分。
对于去马赛克的算法,当前主要有双线性插值算法和自适应插值算法。其核心就是在每个像素中根据周围的颜色信息插值,获取到每个像素的RGB三个分量。详细信息如下:
#!/usr/bin/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
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.int32)
raw_h = self.img.shape[0]
raw_w = self.img.shape[1]
cfa_img = np.empty((raw_h, raw_w, 3), np.int16)
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()
三、ISP之RGB域处理
Demosaic插值后,RAW图就转换为了RGB图,在RGB域中进行Gamma矫正和CCM颜色校正,最后在CSC模块中将RGB转为YUV图。
3.1 Gamma矫正(Gamma Correction)
人眼在黑暗环境下对亮度感知更敏感而sensor获取光信号后显示出来整个过程都是线性的,与人眼的效果不一致。
Gamma校正就是对图像的灰度进行非线性处理,这个曲线类似于指数关系,最终处理后图像灰度是类似人眼的非线性效果,这个指数就是Gamma。
3.2 色彩矫正矩阵(Color Correction Matrix)
有许多变化导致成像系统中难以准确再现颜色。包括:
-
光学元件(透镜、滤光片)的光谱特性
-
光源变化,如日光、荧光灯或钨丝灯
-
传感器滤色片的特性
色彩校正矩阵用于校正源成像系统与目标显示器或接收器系统之间的差异。对于ISP系统来说,传感器的色彩空间不同于显示设备或我们人眼的色彩空间。
#!/usr/bin/python
import numpy as np
class CCM:
'Color Correction Matrix'
def __init__(self, img, ccm):
self.img = img
self.ccm = ccm
def execute(self):
img_h = self.img.shape[0]
img_w = self.img.shape[1]
img_c = self.img.shape[2]
ccm_img = np.empty((img_h, img_w, img_c), np.uint32)
for y in range(img_h):
for x in range(img_w):
mulval = self.ccm[:,0:3] * self.img[y,x,:]
ccm_img[y,x,0] = np.sum(mulval[0]) + self.ccm[0,3]
ccm_img[y,x,1] = np.sum(mulval[1]) + self.ccm[1,3]
ccm_img[y,x,2] = np.sum(mulval[2]) + self.ccm[2,3]
ccm_img[y,x,:] = ccm_img[y,x,:] / 1024
self.img = ccm_img.astype(np.uint8)
return self.img
3.3 色彩空间转换(Color Space Convert)
这个部分就是将RGB图转为YUV图,以便在YUV域进行最后的处理。
为何会有色彩空间的转换呢?
我们图像的采集和处理在RGB空间就已经有很好的效果了,但是显示和信号的处理多数在YUV空间下进行。
显示主要指的是电视,使用YUV格式可以兼容黑白电视和彩色电视,只有Y分量就是黑白图,Y分量和U、V分量都有就可以使用在彩色电视上。
#!/usr/bin/python
import numpy as np
from scipy.ndimage import correlate
class CSC:
'Color Space Conversion'
def __init__(self, img, csc):
self.img = img
self.csc = csc
def execute(self):
img_h = self.img.shape[0]
img_w = self.img.shape[1]
img_c = self.img.shape[2]
csc_img = np.empty((img_h, img_w, img_c), np.uint32)
csc_img[:, :, 0] = self.img[:, :, 0] * self.csc[0, 0] + self.img[:, :, 1] * self.csc[0, 1] + self.img[:, :, 2] * self.csc[0, 2] + self.csc[0, 3]
csc_img[:, :, 1] = self.img[:, :, 0] * self.csc[1, 0] + self.img[:, :, 1] * self.csc[1, 1] + self.img[:, :, 2] * self.csc[1, 2] + self.csc[1, 3]
csc_img[:, :, 2] = self.img[:, :, 0] * self.csc[2, 0] + self.img[:, :, 1] * self.csc[2, 1] + self.img[:, :, 2] * self.csc[2, 2] + self.csc[2, 3]
csc_img = csc_img / 1024
self.img = csc_img.astype(np.uint8)
return self.img
四、ISP之YUV域处理
经过CSC色彩空间转换后将RGB格式转为YUV格式,在YUV域中再进行降噪、锐化,增强等一些算法。在这个部分,每个厂家就各显神通,处理方法、处理模块、处理流程也不尽相同,我们就挑选一些常见的YUV域处理模块给大家介绍。
4.1 降噪
经过之前的处理,产生了很多噪声,需要在YUV域做降噪处理。在YUV域中分别对亮度(Luma)和彩度(Chroma)进行降噪,因为转换到YUV域中,Y分量就是亮度,Y域下出现的Luma Noise,UV分量表示的色彩,UV域下就是Chroma Noise。
讨论亮度噪点时,指的是灰度图像,不考虑色彩因素。照射到物体的表面越亮,光信号越强,信噪比越高,噪点越少。
讨论彩度噪点时,彩噪(Chroma Noise)是YUV域中UV分量的噪声,特指像素之间的色彩波动,Chroma部分的噪声。
当环境亮度不足的时候,U、V分量的绝对值很小,UV分量的噪声就非常明显,在图像上就是彩色的噪斑,对画质的破坏力要比亮度噪点Luma Noise更明显。
对于噪声的处理,通常在空域(Spatial Domain) 、频域(Frequency Domain) 上进行,或者根据图像的相似特性,在一个滤波窗口进行计算。
4.2 边缘增强(Edge Enhance)
由于之前的域中会多次进行降噪处理,而降噪不可避免的会将图像中一些细节也捎带着消除了,导致图像模糊。为了将图像细节还原,减少图像损失,需要对其进行增强,但是不能再次引入噪声,就出现了Edge Enhance 边缘增强这类处理模块。
边缘增强 和我们日常说的锐化sharp是比较相似的。但是二者也有区别:锐化针对图像所有内容增加锐利度,边缘增强只是针对边缘,避免噪声也会锐化放大。 图像的边缘往往会出现灰度值不连续的现象,因此边缘像素中包含更丰富的图像信息。
对于边缘识别现在有很多成熟的算法,例如,Sobel算子、Canny边缘检测等。由此可以看出边缘增强只能在YUV域进行,因为只对Y分量进行处理。
整个Edge Enhance边缘增强流程是:先获取图像边缘,对图像平坦部分不做增强,在边缘部分做滤波和增强处理。
#!/usr/bin/python
import numpy as np
class EE:
'Edge Enhancement'
def __init__(self, img, edge_filter, gain, thres, emclip):
self.img = img
self.edge_filter = edge_filter
self.gain = gain
self.thres = thres
self.emclip = emclip
def padding(self):
img_pad = np.pad(self.img, ((1, 1), (2, 2)), 'reflect')
return img_pad
def clipping(self):
np.clip(self.img, 0, 255, out=self.img)
return self.img
def emlut(self, val, thres, gain, clip):
lut = 0
if val < -thres[1]:
lut = gain[1] * val
elif val < -thres[0] and val > -thres[1]:
lut = 0
elif val < thres[0] and val > -thres[1]:
lut = gain[0] * val
elif val > thres[0] and val < thres[1]:
lut = 0
elif val > thres[1]:
lut = gain[1] * val
# np.clip(lut, clip[0], clip[1], out=lut)
lut = max(clip[0], min(lut / 256, clip[1]))
return lut
def execute(self):
img_pad = self.padding()
img_h = self.img.shape[0]
img_w = self.img.shape[1]
ee_img = np.empty((img_h, img_w), np.int16)
em_img = np.empty((img_h, img_w), np.int16)
for y in range(img_pad.shape[0] - 2):
for x in range(img_pad.shape[1] - 4):
em_img[y,x] = np.sum(np.multiply(img_pad[y:y+3, x:x+5], self.edge_filter[:, :])) / 8
ee_img[y,x] = img_pad[y+1,x+2] + self.emlut(em_img[y,x], self.thres, self.gain, self.emclip)
self.img = ee_img
return self.clipping(), em_img
4.3 色调饱和度控制(Hue & Saturation)
饱和度Saturation就是说色彩的鲜艳程度。色调(Hue)在ISP中的含义是色彩转换的功能,当颜色转动120°,就是原本的红色变成绿色,绿色变为蓝色,蓝色变为红色,可以用到特效上,以及一些特殊的效果。
#!/usr/bin/python
import numpy as np
class HSC:
'Hue Saturation Control'
def __init__(self, img, hue, saturation, clip):
self.img = img
self.hue = hue
self.saturation = saturation
self.clip = clip
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def lut(self):
ind = np.array([i for i in range(360)])
sin = np.sin(ind * np.pi / 180) * 256
cos = np.cos(ind * np.pi / 180) * 256
lut_sin = dict(zip(ind, [round(sin[i]) for i in ind]))
lut_cos = dict(zip(ind, [round(cos[i]) for i in ind]))
return lut_sin, lut_cos
def execute(self):
lut_sin, lut_cos = self.lut()
img_h = self.img.shape[0]
img_w = self.img.shape[1]
img_c = self.img.shape[2]
hsc_img = np.empty((img_h, img_w, img_c), np.int16)
hsc_img[:,:,0] = (self.img[:,:,0] - 128) * lut_cos[self.hue] + (self.img[:,:,1] - 128) * lut_sin[self.hue] + 128
hsc_img[:,:,1] = (self.img[:,:,1] - 128) * lut_cos[self.hue] - (self.img[:,:,0] - 128) * lut_sin[self.hue] + 128
hsc_img[:,:,0] = self.saturation * (self.img[:,:,0] - 128) / 256 + 128
hsc_img[:,:,1] = self.saturation * (self.img[:,:,1] - 128) / 256 + 128
self.img = hsc_img
return self.clipping()
4.4 对比度亮度调节(Contrast & Brightnes)
亮度调节(Brightness)是一般摄像头都会有的功能,顾名思义,就是控制亮度来改变图片效果。
在YUV域中,Y分量就表示亮度。如果直接调节Y分量,就会让图片发白或者发黑,这不是我们理想的亮度控制。通常的做法是将Y、U、V三个分量同时进行亮度调节,这样既保证了亮度,也会保证图像的鲜艳度。
对比度调节(Contrast)也叫做对比度增强Contrast Enhancement,就是增强图片的对比度。目前对比度增强主要的算法是直方图均衡化(Histogram),根据原始图像的亮度数据进行重新分布,使图片的亮度分布更加均匀
#!/usr/bin/python
import numpy as np
class BCC:
'Brightness Contrast Control'
def __init__(self, img, brightness, contrast, clip):
self.img = img
self.brightness = brightness
self.contrast = contrast
self.clip = clip
def clipping(self):
np.clip(self.img, 0, self.clip, out=self.img)
return self.img
def execute(self):
img_h = self.img.shape[0]
img_w = self.img.shape[1]
bcc_img = np.empty((img_h, img_w), np.int16)
bcc_img = self.img + self.brightness
bcc_img = bcc_img + (self.img - 127) * self.contrast
self.img = bcc_img
return self.clipping()
参考:
https://zhuanlan.zhihu.com/p/389334269
https://github.com/cruxopen/openISP
一个专注于“嵌入式知识分享”、“DIY嵌入式产品”的技术开发人员,关注我,一起共创嵌入式联盟。公众号:“嵌入式产品侠”