Image Signal Processing(ISP)-第四章-LSC, CC的原理和软件实现

3 篇文章 10 订阅

Hello!距这个小ISP的软件实现大概过去两年了,一直没有打起精神来将连载写完。但是我们绝不烂尾,绝不!

连载
Image Signal Processing(ISP)-第一章-ISP基础以及Raw的读取显示
Image Signal Processing(ISP)-第二章-Demosaic去马赛克以及BMP软件实现
Image Signal Processing(ISP)-第三章-BCL, WB, Gamma的原理和软件实现
Image Signal Processing(ISP)-第四章-LSC, CC的原理和软件实现

Github
BoyPao/ImageSignalProcessing-ISP

上文我们介绍了BLC,WB,Gamma三种基础的优化操作,并对图像数据进行了这几种基础矫正,图片的效果向人眼真实感受靠近。但明显的是,与商用isp处理的效果有非常大的差距,所以同志们要继续努力呀。
让我们先回顾一下上文结束时的修改效果以及图片仍然存在的问题吧。

在这里插入图片描述

此阶段主要问题:1.图片边角的亮度低,2. 图片色彩饱和度很低。

本文将介绍针对这两个问题的解决方法:1. LSC,2.CC
首先我们先解决亮度不均的问题。

1. Lens Shading Correction (LSC)

我们看到上图的亮度是不均匀的,较亮的部分集中在图像的中央,而周围的部分相比而言要暗很多。这种亮度不均的现象主要是由于镜头对光线的折射造成的。Lens shading译为镜头阴影,其指的就是这种现象。

在这里插入图片描述

如上图,光学镜头一般采用镜片组实现对外界光的采集,由于CMOS的尺寸一般较小,镜片组对光路的导向总体上需要是集中的。因此大部分光将打到CMOS的中间区域,而四周的光强会相对小一些。对于手机镜头,安防镜头,无人机镜头等这类小镜头来说,这种显现更为明显。

1.1 LSC原理

Lens Shading Correction(LSC)就是处理 lens shading 这种亮度不均的一种矫正方法。当然了,原理是非常简单的,我们只需将成像的四周进行相应的增益计算,就可以使四周亮度看起来和中央亮度近似了。

在这里插入图片描述

上图表示的是存在lens shading现象的镜头对白色卡片或墙面拍照输出图片的最中间的一行以及任意某行的亮度分布。对这两行的中央较亮的像素点,设置增益趋近于1,让他们尽量保持这个像素值,而对于两侧较暗的像素,逐渐增大增益以提高矫正后的像素值。用这样的方法,来解决lens shading。

说到Shading,还有一种叫color shading的现象。指的是由于镜片对不同频率的光折射率不同造成各种颜色的光的lens shading 程度轻重不同的现象。也就是说对同一镜片组来说折射率大的色光,其lens shading更严重一些。为解决这个问题,我们的方法也很简单,就是对各种颜色通道采取不同的增益,来分而治之。

这里需要强调的是,我们对各通道采取不同的增益并不意味着我们是在改变色调,因为事实上各通道的增益都是中间小四周大的,差异值是非常小的数字。而亮度值就是各个通道像素值经过加权平均所得,因此此处对各色彩通道采用略有差异的增益,实质上是希望改变整体亮度,而不是色调的。

1.2 LSC软件实现

以下贴出软件实现。

BZResult BZ_LensShadingCorrection(void* data, LIB_PARAMS* pParams, ISP_CALLBACKS CBs, ...)
{
	BZResult result = BZ_SUCCESS;
	(void)CBs;

	if (!data) {
		result = BZ_INVALID_PARAM;
		BZLoge("data is null! result:%d", result);
	}

	if (SUCCESS(result)) {
		int32_t width, height;
		LIB_BAYER_ORDER bayerOrder = LIB_BAYER_ORDER_NUM;
		int32_t i, j;
		float* pR = nullptr;
		float* pGr = nullptr;
		float* pGb = nullptr;
		float* pB = nullptr;
		float* pLut = nullptr;
		width = pParams->info.width;
		height = pParams->info.height;
		bayerOrder = pParams->info.bayerOrder;
		BZLogd("width:%d height:%d type:%d", width, height, bayerOrder);
		switch (bayerOrder)
		{
		case LIB_RGGB:
			pR = pParams->LSC_param.GainCh1;
			pGr = pParams->LSC_param.GainCh2;
			pGb = pParams->LSC_param.GainCh3;
			pB = pParams->LSC_param.GainCh4;
			break;
		case LIB_BGGR:
			pB = pParams->LSC_param.GainCh1;
			pGb = pParams->LSC_param.GainCh2;
			pGr = pParams->LSC_param.GainCh3;
			pR = pParams->LSC_param.GainCh4;
			break;
		case LIB_GRBG:
			pGr = pParams->LSC_param.GainCh1;
			pR = pParams->LSC_param.GainCh2;
			pB = pParams->LSC_param.GainCh3;
			pGb = pParams->LSC_param.GainCh4;
			break;
		case LIB_GBRG:
			pGb = pParams->LSC_param.GainCh1;
			pB = pParams->LSC_param.GainCh2;
			pR = pParams->LSC_param.GainCh3;
			pGr = pParams->LSC_param.GainCh4;
			break;
		default:
			result = BZ_INVALID_PARAM;
			BZLoge("Unsupported bayer order:%d result:%d", bayerOrder, result);
			break;
		}

		if (SUCCESS(result)) {
			for (i = 0; i < height; i++) {
				for (j = 0; j < width; j++) {
					if (i % 2 == 0 && j % 2 == 0) {
						pLut = pB;
					}
					if (i % 2 == 0 && j % 2 == 1) {
						pLut = pGb;
					}
					if (i % 2 == 1 && j % 2 == 0) {
						pLut = pGr;
					}
					if (i % 2 == 1 && j % 2 == 1) {
						pLut = pR;
					}
					static_cast<uint16_t*>(data)[i * width + j] = static_cast<uint16_t*>(data)[i * width + j] *
						LSCInterpolation(width, height,
								*(pLut + (LSC_LUT_HEIGHT - 1) * i / height * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width),
								*(pLut + (LSC_LUT_HEIGHT - 1) * i / height * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width + 1),
								*(pLut + ((LSC_LUT_HEIGHT - 1) * i / height + 1) * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width),
								*(pLut + ((LSC_LUT_HEIGHT - 1) * i / height + 1) * LSC_LUT_WIDTH + (LSC_LUT_WIDTH - 1) * j / width + 1),
								i,
								j);
				}
			}
		}
	}

	return result;
}

上面的函数写了很多行,但其实前面大部分代码是支持各种Bayer排布的代码,主要是最后调用了LSCInterpolation()来获取每个像素点的增益,再乘上像素点的值来实现矫正。

LSCInterpolation()是一个增益插值函数。为啥需要这个函数呢?前面说到了对不同颜色需要分而治之,那么在bayer域做 lsc 就需要4个 lut (lut是啥上一章介绍过) 来支持 4个颜色通道,这会造成数据量变得庞大。所以采取将图片划分为少量几块,每一块用一个lut值,最后每个像素通过临域块的 lut 值插值得到该像素 lsc增益的方法来降低 lut 的数据量。

用到的LSCInterpolation() 函数如下。

float LSCInterpolation(int32_t WIDTH, int32_t HEIGHT,
	float LT, float RT, float LB, float RB,
	int32_t row, int32_t col)
{
	float TempT, TempB, result;
	TempT = LT - (LT - RT) * (col % (WIDTH / (LSC_LUT_WIDTH - 1))) * (LSC_LUT_WIDTH - 1) / WIDTH;
	TempB = LB - (LB - RB) * (col % (WIDTH / (LSC_LUT_WIDTH - 1))) * (LSC_LUT_WIDTH - 1) / WIDTH;
	result = TempT - (TempT - TempB) * (row % (HEIGHT / (LSC_LUT_HEIGHT - 1))) * (LSC_LUT_HEIGHT - 1) / HEIGHT;
	return result;
}

其lsc的部分lut如下。

LSC_PARAM LSCPARM_1920x1080_D65_1000Lux = {
	//bGain
	{{ 2.934418,   2.6059866,  2.220798,   1.94021428, 1.74472368, 1.600148,   1.50334871, 1.44453084, 1.42655516, 1.45087564, 1.51691735, 1.620021,   1.77389872, 1.984156,   2.26868367, 2.685018,   3.086324   },
	 { 2.780221,   2.42652345, 2.055793,   1.81020319, 1.61290979, 1.47168612, 1.37039435, 1.312223,   1.292875,   1.31676388, 1.381954,   1.48782563, 1.63721418, 1.83661532, 2.09502649, 2.48011971, 2.89426875 },
	 。。。。。。}

 //gbGain
	{{ 3.20395136, 2.84868073, 2.42183638, 2.12311363, 1.91179454, 1.756917,   1.64773118, 1.58069026, 1.55754256, 1.5834856,  1.645207,   1.74964738, 1.90877008, 2.1196,     2.41837454, 2.8678813,  3.28876615 },
	 { 3.04708457, 2.663059,   2.24919081, 1.97792578, 1.76208353, 1.60036659, 1.48812246, 1.417937,   1.39421225, 1.41655445, 1.48318923, 1.59559619, 1.75348139, 1.96583772, 2.233202,   2.655679,   3.10571742 },
	 。。。。。。}

 //grGain
	{{ 3.25572634, 2.89292026, 2.4517138,  2.14141822, 1.91608775, 1.75056577, 1.6297735,  1.55698335, 1.53054082, 1.55629158, 1.62307167, 1.74096286, 1.90679991, 2.12313032, 2.435579,   2.887383,   3.319794   },
	 { 3.1139915,  2.71121383, 2.28292322, 2.001553,   1.773555,   1.60334086, 1.47839546, 1.40285826, 1.37624037, 1.39933944, 1.46882379, 1.58867931, 1.75515807, 1.97776151, 2.256206,   2.685162,   3.150439   },
	 。。。。。。}

 //rGain
	 {{ 3.26140237, 2.90829229, 2.44395447, 2.14117551, 1.92580986, 1.76098931, 1.64909816, 1.57704759, 1.554534,   1.58389091, 1.65474868, 1.77599037, 1.95483339, 2.17210364, 2.49666047, 2.99027562, 3.42638421 },
	  { 3.10735345, 2.697388,   2.26802278, 1.99615026, 1.77538717, 1.60802543, 1.48951221, 1.41702485, 1.39288378, 1.420929,   1.49541867, 1.61999846, 1.79255354, 2.01902175, 2.304034,   2.75090647, 3.242971   },
	  。。。。。。}

在ISP中调用上面介绍的函数以实现LSC操作。

1.3 LSC效果

让我们看看矫正后的结果吧。
在这里插入图片描述

可以看到四周的亮度被提高了,看起来整幅图片的亮度的分布比较均匀了。

让我们看看与商用isp的对比。
在这里插入图片描述

现在看来我们急需要解决饱和度的问题了。那还等什么呢?

2. Color Correction (CC)

要解决饱和度低的问题,就需要对色彩进行矫正,也就是这一章我们要介绍的color correction。
对颜色的矫正需求来源于sensor输出的数据和人眼对实物的感受之间存在的色彩差异。而Color correction其本质就是对数据进行人眼真实感受值映射的一种过程。

但无论我们采用什么样的映射方法,首先要确定的是描述颜色的方法。此前我们说过该 ISP 的 CC 操作位于 RGB域,也就是说当前步骤采用RGB色彩空间来描述颜色。

所以我们的CC就是在RGB域进行的对sensor输出数据向人眼真实感受值的一种映射。

2.1 CC原理

为了实现这种映射过程,我们需要获取到数据和感官真实值之间的具体的映射方式。确定映射方式的一种方法是标定法。即利用标定好的某种颜色的标准值和sensor拍摄该值实物图片所输出值做差,来求得CC对该颜色需要做的映射。

在这里插入图片描述

我们常见的color checker 色卡就是标定好的颜色,我们知道色卡上每个色块在RGB域的真实感受值,所以我们可以在某种色温环境下对该色卡进行拍摄,输出一份未作CC的数据,用这个数据和标定值做差来确定为该色温环境下这个sensor需要做的CC映射。

此后在该色温环境下使用该sensor时,对其输出作CC,就可以还原出人眼真实感受值了。

由于在RGB域有三个通道,我们的这种映射关系可以由矩阵来表示,这就是我们常说的Color Correction Metric(CCM)。

2.2 CC软件实现

cc的软件实现采用了CCM进行矩阵计算,参考了imtest官网对CCM的介绍。以下贴出代码。

BZResult BZ_ColorCorrection(void* data, LIB_PARAMS* pParams, ISP_CALLBACKS CBs, ...)
{
	BZResult result = BZ_SUCCESS;
	(void)CBs;

	if (!data) {
		result = BZ_INVALID_PARAM;
		BZLoge("data is null! result:%d", result);
	}

	if (SUCCESS(result)) {
		int32_t width, height;
		float* pCcm;
		int32_t i, j;
		width = pParams->info.width;
		height = pParams->info.height;
		pCcm = pParams->CC_param.CCM;
		BZLogd("width:%d height:%d", width, height);
		for (i = 0; i < CCM_HEIGHT; i++) {
			BZLogd("CCM[%d]:%f CCM[%d]:%f CCM[%d]:%f", i * CCM_WIDTH, pCcm[i * CCM_WIDTH],
							i * CCM_WIDTH + 1, pCcm[i * CCM_WIDTH + 1],
							i * CCM_WIDTH + 2, pCcm[i * CCM_WIDTH + 2]);
		}

		Mat CCM = Mat::zeros(3, 3, CV_32FC1);
		for (i = 0; i < 3; i++) {
			for (j = 0; j < 3; j++) {
				CCM.at<float>(i, j) = *(pCcm + i * CCM_WIDTH + j);
			}
		}

		uint16_t* B = static_cast<uint16_t*>(data);
		uint16_t* G = B + width * height;
		uint16_t* R = G + width * height;

		Mat outMatric;
		Mat inMatric(width * height, 3, CV_32FC1);

		for (i = 0; i < height * width; i++) {
			inMatric.at<float>(i, 0) = (float)B[i];
			inMatric.at<float>(i, 1) = (float)G[i];
			inMatric.at<float>(i, 2) = (float)R[i];
		}

		outMatric = inMatric * CCM;

		for (i = 0; i < height * width; i++) {
			if (outMatric.at<float>(i, 0) > 1023)
				outMatric.at<float>(i, 0) = 1023;
			if (outMatric.at<float>(i, 0) < 0)
				outMatric.at<float>(i, 0) = 0;
			if (outMatric.at<float>(i, 1) > 1023)
				outMatric.at<float>(i, 1) = 1023;
			if (outMatric.at<float>(i, 1) < 0)
				outMatric.at<float>(i, 1) = 0;
			if (outMatric.at<float>(i, 2) > 1023)
				outMatric.at<float>(i, 2) = 1023;
			if (outMatric.at<float>(i, 2) < 0)
				outMatric.at<float>(i, 2) = 0;
			B[i] = (uint16_t)outMatric.at<float>(i, 0);
			G[i] = (uint16_t)outMatric.at<float>(i, 1);
			R[i] = (uint16_t)outMatric.at<float>(i, 2);
		}
	}
	return result;
}

这里采用了Opencv的 Mat 类快速地实现矩阵相乘,由于结果可能超出此时10bit 的编码上限,因此需要对超出的数据进行修正,修正为10bit的上限值1023。

这里的CCM为:

CC_PARAM CCPARAM_1920x1080_D65_1000Lux{
	{{ 1.819,	-0.248,		0.213  },
	 { -1.069,	1.322,		-1.078 },
	 { 0.250,	-0.074,		1.865  }}
};

由CCM第一行可以看出,我们对B通道的映射是将其蓝色分量进行强调,红色分量稍微提高,绿色分量稍微减弱,总体上实现了B通道数据在RGB域向B轴正方向的偏移,以此提高了B通道的饱和度。第二行和第三行都实现了类似的偏移,饱和度都得到了提高。需注意,该CCM只适用于在D65光源下我测试使用的这个sensor。

2.3 CC效果

现在让我们看看CC处理前后的效果对比吧。
在这里插入图片描述

很明显经过CC,图片的色彩得到了改善。

让我们看一下这一章介绍的LSC以及CC的总体效果吧。
在这里插入图片描述

如此看来,是不是已经很接近商用ISP的效果了呢?让我们放大看看细节吧。
在这里插入图片描述

可以看出目前我们的ISP已经对raw作出了很大的改善,但我们的输出有很多红绿的色斑,并且对比度不够高,锐度不够强呀。哈哈哈哈,所以下一章,也就是此系列的终章,我会针对这个两个问题做一些改善。

敬请期待!

在Matlab中实现LSC(Least Square Conformal Maps)算法的步骤如下: 1. 读取图像并进行预处理,包括将图像转换为灰度图像、调整图像大小等。 2. 选择控制点,即将源图像和目标图像中对应的特征点进行配对。控制点选择的数量要足够,以保证映射的准确性。 3. 计算每个控制点的权重,即根据控制点的位置和距离计算其权重。权重的计算可以采用高斯加权函数或者逆距离权重函数等。 4. 采用LSC算法计算源图像和目标图像之间的映射关系。LSC算法采用最小二乘法,通过求解线性方程组得到映射矩阵。 5. 对源图像进行插值处理,即根据映射矩阵将源图像中的像素点映射到目标图像中,并根据插值算法计算映射后的像素值。 6. 将映射后的图像保存为目标图像。 以下是Matlab代码示例: ```matlab % 读取源图像和目标图像 source_img = imread('source.jpg'); target_img = imread('target.jpg'); % 预处理图像 source_gray = rgb2gray(source_img); target_gray = rgb2gray(target_img); % 选择控制点 source_points = ginput(4); target_points = ginput(4); % 计算权重 weights = get_weights(source_points, target_points); % 计算映射矩阵 M = get_mapping_matrix(source_points, target_points, weights); % 对源图像进行插值处理 [X, Y] = meshgrid(1:size(target_gray, 2), 1:size(target_gray, 1)); X_source = M(1,1)*X + M(1,2)*Y + M(1,3); Y_source = M(2,1)*X + M(2,2)*Y + M(2,3); source_interp = interp2(double(source_gray), X_source, Y_source); % 保存映射后的图像 imwrite(uint8(source_interp), 'result.jpg'); ``` 其中,`get_weights`函数用于计算控制点的权重,`get_mapping_matrix`函数用于计算映射矩阵。
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值