重现image resize-双线性插值
Opencv水太深,代码看的头疼。
从库一层一层往下看,发现opencv采用了许多代码加速,例如并行计算,SSE等。这几天做的是resize的线性插值的方法。
SSE(Streaming SIMD Extensions)是Intel在3D Now!发布一年之后,在PIII中引入的指令集,是MMX的超集。AMD后来在Athlon XP中加入了对这个指令集的支持。这个指令集增加了对8个128位寄存器XMM0-XMM7的支持,每个寄存器可以存储4个单精度浮点数。使用这些寄存器的程序必须使用FXSAVE和FXRSTR指令来保持和恢复状态。但是在PIII对SSE的实现中,浮点数寄存器又一次被新的指令集占用了,但是这一次切换运算模式不是必要的了,只是SSE和浮点数指令不能同时进入CPU的处理线而已。
SSE2是Intel在P4的最初版本中引入的,但是AMD后来在Opteron 和Athlon 64中也加入了对它的支持。这个指令集添加了对64位双精度浮点数的支持,以及对整型数据的支持,也就是说这个指令集中所有的MMX指令都是多余的了,同时也避免了占用浮点数寄存器。这个指令集还增加了对CPU的缓存的控制指令。AMD对它的扩展增加了8个XMM寄存器,但是需要切换到64位模式(AMD64)才可以使用这些寄存器。Intel后来在其EM64T架构中也增加了对AMD64的支持。
SSE3是Intel在P4的Prescott版中引入的指令集,AMD在Athlon 64的第五个版本中也添加了对它的支持。这个指令集扩展的指令包含寄存器的局部位之间的运算,例如高位和低位之间的加减运算;浮点数到整数的转换,以及对超线程技术的支持。
双线性内插值算法描述如下:
对于一个目的像素,设置坐标通过反向变换得到的浮点坐标为(i+u,j+v) (其中i、j均为浮点坐标的整数部分,u、v为浮点坐标的小数部分,是取值[0,1)区间的浮点数),则这个像素得值 f(i+u,j+v) 可由原图像中坐标为 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1)所对应的周围四个像素的值决定,即:
f(i+u,j+v) = (1-u)(1-v)f(i,j) + (1-u)vf(i,j+1) + u(1-v)f(i+1,j) + uvf(i+1,j+1) 公式1
其中f(i,j)表示源图像(i,j)处的的像素值,以此类推。
比如,象刚才的例子,现在假如目标图的象素坐标为(1,1),那么反推得到的对应于源图的坐标是(0.75 , 0.75), 这其实只是一个概念上的虚拟象素,实际在源图中并不存在这样一个象素,那么目标图的象素(1,1)的取值不能够由这个虚拟象素来决定,而只能由源图的这四个象素共同决定:(0,0)(0,1)(1,0)(1,1),而由于(0.75,0.75)离(1,1)要更近一些,那么(1,1)所起的决定作用更大一些,这从公式1中的系数uv=0.75×0.75就可以体现出来,而(0.75,0.75)离(0,0)最远,所以(0,0)所起的决定作用就要小一些,公式中系数为(1-u)(1-v)=0.25×0.25也体现出了这一特点。
双线性内插法是利用待求象素四个邻象素的灰度在两个方向上作线性内插,如下图所示:
对于 (i, j+v),f(i, j) 到 f(i, j+1) 的灰度变化为线性关系,则有:
f(i, j+v) = [f(i, j+1) - f(i, j)] * v + f(i, j)
同理对于 (i+1, j+v) 则有:
f(i+1, j+v) = [f(i+1, j+1) - f(i+1, j)] * v + f(i+1, j)
从f(i, j+v) 到 f(i+1, j+v) 的灰度变化也为线性关系,由此可推导出待求象素灰度的计算式如下:
f(i+u, j+v) = (1-u) * (1-v) * f(i, j) + (1-u) * v * f(i, j+1) + u * (1-v) * f(i+1, j) + u * v * f(i+1, j+1)
双线性内插法的计算比最邻近点法复杂,计算量较大,但没有灰度不连续的缺点,结果基本令人满意。它具有低通滤波性质,使高频分量受损,图像轮廓可能会有一点模糊。
基于此原理写代码
/*********************Resize_INTER_LINEAR******************************************/
//功能:利用线性插值修改图像尺寸
//参数:原图像,目标图像,目标长,目标宽
void mcv::Resize_INTER_LINEAR(mcv::Image &src, mcv::Image &dst, unsigned int width, unsigned int heigth)
{
matDst1 = Mat(Size(width, heigth), matSrc.type(), Scalar::all(0));//为了显示使用opencv,Size(width,heigth)
dst.height = matDst1.rows;
dst.width = matDst1.cols;
dst.channel = matDst1.channels();
dst.data = matDst1.data;
//缩放倍数
double scale_x = (double)src.width / dst.width;
double scale_y = (double)src.height / dst.height;
for (int j = 0; j < dst.height; ++j)//f( sy+fy )
{
float fy = (float)((j + 0.5) * scale_y - 0.5);
int sy = floor(fy);//整数部分
fy -= sy;//小数部分
sy = min(sy, src.height - 2);//因为有小数部分所以-2
sy = max(0, sy);
//由于cbufx[0]和cbufy[1]都放大了2048(2^11)倍,这里右移是为了还原,
//这样先乘后右移的操作在图像处理中很常见,主要是为了简化计算
short cbufy[2];
if ((1.f - fy) * 2048>32767)
cbufy[0] = 32767;
else if ((1.f - fy) * 2048 < -32768)
cbufy[0] = -32768;
else
cbufy[0] = (1.f - fy) * 2048;
//cbufy[0] = cv::saturate_cast<short>((1.f - fy)*2048);
cbufy[1] = 2048 - cbufy[0];
//float cbufy[2];
//cbufy[0] = cv::saturate_cast<float>((1.f - fy));
//cbufy[1] = 1 - cbufy[0];
for (int i = 0; i < dst.width; ++i)
{
float fx = (float)((i + 0.5) * scale_x - 0.5);
int sx = floor(fx);
fx -= sx;
if (sx < 0) {
fx = 0, sx = 0;
}
if (sx >= src.width - 1) {
fx = 0, sx = src.width - 2;
}
//float cbufx[2];
//cbufx[0] = cv::saturate_cast<float>((1.f - fx));
//cbufx[1] = 1 - cbufx[0];
short cbufx[2];
if ((1.f - fx) * 2048>32767)
cbufx[0] = 32767;
else if ((1.f - fx) * 2048 < -32768)
cbufx[0] = -32768;
else
cbufx[0] = (1.f - fx) * 2048;
//cbufx[0] = cv::saturate_cast<short>((1.f - fx) * 2048);
cbufx[1] = 2048 - cbufx[0];
for (int k = 0; k < src.channel; ++k)
{
*(dst.data + j*dst.width*dst.channel + dst.channel * i + k) = (*(src.data + sy*src.width*src.channel + src.channel * sx + k) * cbufx[0] * cbufy[0] +
*(src.data + (sy + 1)*src.width*src.channel + src.channel * sx + k) * cbufx[0] * cbufy[1] +
*(src.data + sy*src.width*src.channel + src.channel * (sx + 1) + k) * cbufx[1] * cbufy[0] +
*(src.data + (sy + 1)*src.width*src.channel + src.channel * (sx + 1) + k) * cbufx[1] * cbufy[1]) >> 22;
}
}
}
}
这样能够实现图像尺寸的线性插值