今天讲降龙算法的亚像素拟合算法。这是我们后续学习其它大部分算法的基础,因为只要你想手撕图像处理算法,就必须要面对亚像素精度的计算,这是绕不过去的一个知识点。
今天就来讲解一下亚像素精度拟合的理论知识,并手撕一下亚像素精度算法。
在我刚学习opencv的时候就知道亚像素精度了,但刚开始学的时候一直对亚像素精度不甚理解:像素就已经是图像中的最小的单位了,对不对,再去求亚像素精度,有什么意义吗?
一个像素是图像表现的最小精度,但它不是实际物体的精度。我们拿一个2*2分辨率的相机去拍一个2x2厘米大小的物体,那一个像素就代表了一个1x1厘米面积的实际物体,而实际成像,肯定是很糊的,对吧。那此时我们想提高精度,怎么办?
有两个办法,一个,就是提高我们相机的分辨率,也就是花钱提高我们的硬件设备,将2x2分辨率的相机提高为4x4像素的分辨率,这样一个像素仅代表了0.5x0.5厘米面积的实际物体,精度提高了一倍,清晰度肯定也提高了。
还有一个办法,那就是计算亚像素精度,通过我们已知的四个像素值,去计算剩余的12个像素的值,来得到一个亚像素精度的填充图。两个办法都可以提高我们图像的清晰度,都能使我们得到一个4x4像素大小的图。
这两种方法有区别吗,那肯定是有区别的,第一种方法得到的结果图肯定是准确的,因为我们花钱了。第二种方法的准确度清晰度肯定稍差一些,但我们不需要花钱提升硬件。
第二种方法这种直接从2x2像素变为4x4像素,不就是我们最最最常用的resize图像缩放操作吗?对的,在图像缩放或者其它图像变换中,肯定会用到我们要讲的插值算法。
opencv中的很多函数,都需要我们传入一个插值算法选项,例如resize缩放和warpAffine仿射变换函数。
resize函数的最后一个参数interpolation,就是需要传入一个标志位用来选择使用哪种插值算法来计算像素值。为什么需要插值算法呢?我们最前面的例子已经提到过了,从2x2像素到4x4像素这种一倍尺寸的扩大,我们就需要计算额外的12个像素的值。如何计算呢?就使用亚像素插值拟合算法。
再看warpAffine函数,它是用来计算仿射变换的,它的第五个参数,同样是选择亚像素插值算法的,有默认值INTER_LINERA,也就是双线性插值算法。为什么仿射变换也需要计算亚像素精度呢?看下图。
在上图中已经说的很清楚了,这里再描述一遍,左边的原图,经过仿射变化后得到右边的输出图像。但原图中的每个像素点,很难位于输出图像的整数坐标上,这时我们就需要计算亚像素值了。
再说通俗一点,我们想知道输出图像A点的像素值(也就是输出图像的(3,2)坐标的像素值),需要怎么办呢,需要反仿射变换去找原图中这个点所对应的坐标位置的像素值是多少,然后我们发现,A点在原图中对应的坐标是B点,A点的像素值就等于B点的像素值。
但很可惜,B点并不是一个整数坐标,我们不知道B点的像素值是多少,但我们知道B点四周的点,也就是1、2、3、4点的像素值。
而亚像素精度算法要做的,就是根据1、2、3、4点像素值,通过亚像素拟合算法,计算得到B点的像素值,也就是我们输出图像A点的像素值。
讲到这里我们应该理解为什么会有亚像素精度,也知道了什么情况下需要计算亚像素了,同时我们知道了,亚像素值是通过插值算法来计算的。我们将上面的例子抽象成下面这个图:
opencv中支持的亚像素精度插值算法有很多,它有一个专门的枚举用来标识插值算法类型:
我们不需要详细了解所有的插值算法,只需要掌握最近邻插值INTER_NEAREST和双线性插值INTER_LINEAR即可。
还是结合上面的两张图,最近邻插值就是,想计算上图中B点(1.7,2.6)的像素值,就计算1点(1,2)、2点(2,2)、3点(2,3)、4点(1,3)中哪个距离B点最近,最近点的像素值就是B点的像素值,也就是输出图像中A点的像素值。
如何计算哪个点距离B点最近呢?最朴素的想法,就是通过L2距离范式(x^2+y^2)开根号计算1点和B点的距离,计算2点和B点的距离,3点和B点的距离,4点和B点的距离,然后比较得到最小的距离。
但其实在实际编程中呢,我们只需要对B点坐标进行四舍五入,得到的坐标就是距离B点最近的坐标了,例如B点(1.7,2.6),四舍五入就是(2,3),也就是上图中的3点。很显然肉眼可见,3点确实是距离B点最近的点。
我们设计一个函数,用来实现基于最近邻插值算法的图像缩放操作:
/*
* 最近邻插值算法
* @srcImage:输入原图
* @srcWidth:原图高度
* @srcHeight:原图宽度
* @dstImage:输出图像
* @dstWidth:输出图宽度
* @dstHeight:输出图高度
*/
uint32_t XL_API NearestSubPixel(const unsigned char* srcImage, int srcWidth, int srcHeight,
unsigned char* dstImage, int dstWidth, int dstHeight)
{
float scaleX = static_cast<float>(srcWidth) / dstWidth;
float scaleY = static_cast<float>(srcHeight) / dstHeight;
for (int y = 0; y < dstHeight; ++y)
{
for (int x = 0; x < dstWidth; ++x)
{
int srcX = static_cast<int>(std::round(x * scaleX));
int srcY = static_cast<int>(std::round(y * scaleY));
int srcIndex = srcY * srcWidth + srcX;
int dstIndex = y * dstWidth + x;
dstImage[dstIndex] = srcImage[srcIndex];
}
}
return XL_OK;
}
函数调用示例:定义了一个4x4的原始图像和一个8x8的目标图像。然后使用最近邻插值算法对原始图像进行缩放,得到目标图像。最后输出目标图像的像素值。
int main()
{
// 原始图像尺寸和数据
int srcWidth = 4;
int srcHeight = 4;
unsigned char srcImage[] = {
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12,
13, 14, 15, 16
};
// 目标图像尺寸和数据
int dstWidth = 8;
int dstHeight = 8;
unsigned char dstImage[64];
// 使用最近邻插值算法进行图像缩放
NearestSubPixel(srcImage, srcWidth, srcHeight, dstImage, dstWidth, dstHeight);
// 输出目标图像像素值
for (int y = 0; y < dstHeight; ++y)
{
for (int x = 0; x < dstWidth; ++x)
{
std::cout << static_cast<int>(dstImage[y * dstWidth + x]) << " ";
}
std::cout << std::endl;
}
return 0;
}