本文介绍几种经典插值的原理,代码过程以及对性能作出比较。
简单说来,在对原始图像进行缩放、旋转变换的时候,因为在目标图像中像素分布发生了变换,所以需根据一定的映射规则建立从原始图像到目标图像的转换。
顾名思义,最近邻插值就是从原始目标中找到最匹配的位置。不多说,直接上代码。
static int nearestArea(Mat &src, float scale_x, float scale_y, char *argc)
{
Mat dstMat1 = Mat(src.rows * scale_y, src.cols * scale_x, src.type());
typedef BOOST_TYPEOF(*src.data) ElementType;
int WidthSrc = src.cols;
int HeightSrc = src.rows;
int WidthDst = dstMat1.cols;
int HeightDst = dstMat1.rows;
int chal_num = src.channels();
char name[128];
char **ful_name = &argc;
char *prefix = nullptr;
char *subfix = nullptr;
prefix = strsep(ful_name, ".");
subfix = strsep(ful_name, ".");
if (!prefix || !subfix) {
printf("Get subfix from file name failed\n");
return -1;
}
snprintf(name, sizeof(name), "%s_%.2f_%.2f_Nearest.%s", prefix, scale_x, scale_y, subfix);
clock_t start = clock();
for (int w = 0; w < WidthDst; w++) {
float rx = ((float)w + 0.5) / scale_x - 0.5;
int sx = cvFloor(rx);
sx = MIN(sx, WidthSrc - 2);
sx = MAX(0, sx);
short rx_sh_buf[2];
rx_sh_buf[0] = (short)((1.f - rx) * 2048);
rx_sh_buf[1] = 2048 - rx_sh_buf[0];
for (int h = 0; h < HeightDst; h++) {
float ry = ((float)h + 0.5) / scale_y - 0.5;
int sy = cvFloor(ry);
sy = MIN(sy, HeightSrc - 2);
sy = MAX(0, sy);
switch (chal_num) {
case 1:
dstMat1.at(h, w) =
src.at(sy, sx);
break;
case 2:
for (int m = 0; m < 2; m++) {
dstMat1.at2>>(h, w)[m] =
src.at2>>(sy, sx)[m];
}
break;
case 3:
for (int m = 0; m < 3; m++) {
dstMat1.at3>>(h, w)[m] =
src.at3>>(sy, sx)[m];
}
break;
default:
break;
}
}
}
clock_t end = clock();
printf("Interpolation time: %f\n", (float)(end - start) / CLOCKS_PER_SEC * 1000);
if (dstMat1.data) {
imwrite(name, dstMat1);
}
return 1;
}
双线性插值的思路就是利用映射到原始图像匹配点像素临近四个点对目标图像的像素点进行求解。原因在于反向求解匹配像素点的时候,会产生小数,舍入之后会产生较大误差。计算思路可以用下图表示:
X方向
f(R1)≈x−x1x2−x⋅f(Q21)+x2−xx2−x1⋅f(Q11)
f(R2)≈x−x1x2−x⋅f(Q12+x2−xx2−x1⋅f(Q22
Y方向
f(P)≈y−y1y2−y1⋅f(R1)+y2−yy2−y1⋅f(R2)
将求得的像素点表示为 (x+u,y+v) , 其中u, v表示小数部分
根据上述推导,目标像素值可以表示为:
f(P)≈(1−u)(1−v)(f(x,y))+(1−u)vf(x,j+1)+u(1−v)f(x+1,y)+uvf(x,y)
将上述公式转化为代码:
static int bilinear(Mat &src, float scale_x, float scale_y, char* argc)
{
Mat dstMat1 = Mat(src.rows * scale_y, src.cols * scale_x, src.type());
typedef BOOST_TYPEOF(*src.data) ElementType;//用boost 在编译阶段获得数据类型
int WidthSrc = src.cols;
int HeightSrc = src.rows;
int WidthDst = dstMat1.cols;
int HeightDst = dstMat1.rows;
int chal_num = src.channels();
char name[128];
char **ful_name = &argc;
char *prefix = nullptr;
char *suffix = nullptr;
prefix = strsep(ful_name, ".");
subfix = strsep(ful_name, "."); //Get Image type through suffix
if (!prefix || ! suffix) {
printf("Get suffix from file name failed\n");
return -1;
}
snprintf(name, sizeof(name), "%s_%.2f_%.2f.%s", prefix, scale_x, scale_y, suffix);
for(int w = 0; w < WidthDst; w++) {
float rx = ((float)w + 0.5) / scale_x - 0.5; //根据中心对称,向左上平移,优化插值
int sx = cvFloor(rx);
rx -= sx;
sx = MIN(sx, WidthSrc - 2);
sx = MAX(0, sx);
for (int h = 0; h < HeightDst; h++) {
float ry = ((float)h + 0.5) / scale_y - 0.5;
int sy = cvFloor(ry);
ry -= sy;
sy = MIN(sy, HeightSrc - 2);
sy = MAX(0, sy);
switch (chal_num) {
case 1:
dstMat1.at(h, w) =
(1 - rx)*(1 - ry)*(src.at(sy, sx)) +
(1 - rx)*ry*(src.at(sy, sx + 1)) +
rx*(1 - ry)*(src.at(sy + 1, sx)) +
rx*ry*(src.at(sy + 1, sx + 1));
break;
case 2:
for (int m = 0; m < 2; m++) {
dstMat1.at2>>(h, w)[m] =
(1 - rx)*(1 - ry)*(src.at2>>(sy, sx)[m]) +
(1 - rx)*ry*(src.at2>>(sy, sx + 1)[m]) +
rx*(1 - ry)*(src.at2>>(sy + 1, sx)[m]) +
rx*ry*(src.at2>>(sy + 1, sx + 1)[m]);
}
break;
case 3:
for (int m = 0; m < 3; m++) {
dstMat1.at3>>(h, w)[m] =
(1 - rx)*(1 - ry)*(src.at3>>(sy, sx)[m]) +
(1 - rx)*ry*(src.at3>>(sy, sx + 1)[m]) +
rx*(1 - ry)*(src.at3>>(sy + 1, sx)[m]) +
rx*ry*(src.at3>>(sy + 1, sx + 1)[m]);
}
}
}
}
if (dstMat1.data) {
imwrite(name, dstMat1);
}
return 1;
}
本质上插值的过程是目标图像到原始图像的映射,由此需要保证的一点就是前后两者之间的中心点应该是对齐的。实际运算中,目标图像像素值计算有可能会在原始图像中有所偏移。
查看上图,因为直接计算会导致目标图像映射到原始图像的像素值向左上角偏移,所以在计算之前做了一个 (dx,dy) 偏移, 实际过程中取 dx=0.5,dy=0.5 。