缩放原理
提到缩放,正常人的第一直觉是基于输入图像,增加或减少像素,然后得到目标图像。
但实际工程中,一般采用的是逆向映射的方法。
就是遍历目标图像,找到目标图像的像素对应原图的一个或几个像素。
然后通过原图中的一个或几个像素计算出目标像素值。
这样做的好处是,保证目标图像的每一个像素都有一个确定值,
缩放采用的是逆向映射的方法,遍历输出图像中的每一个像素,找到目标图像对应的原图的位置。
那为什么不用正向映射呢?从输入图像中找到输出图像中的像素不是更方便吗?
如果是从输入图像来推算目标图像,可能出现输出图像的像素点没有灰度值的情况。
举个例子,
在GPS导航系统中,我们通常使用逆向映射的方法来确定车辆当前位置在地图上的具体位置。GPS接收器会接收到一系列的卫星信号,然后根据这些信号确定当前的位置坐标。然后通过地图系统将这些坐标映射到地图上的特定位置,这样它收集到的所有信息都能在地图上反映出来。
假设源图像A大小为m * n,缩放K倍后的目标图像B的大小为M * N。
- 缩放比例:k = M / m
- 对于目标图像中的每个像素(X,Y),都可以找到原图中的(x,y)
- 在逆向映射中,(x,y)可能是浮点型的,因为它们不一定正好对应于
A 中的一个像素位置。因此,通常需要使用插值等方法来计算 (x,y) 周围像素的值,以估算
(X,Y) 的值。
最常用的插值算法有三种:最近邻插值、双线性插值、立方卷积插值,其中使用立方卷积插值达到的效果是最佳的。
最近邻插值算法
最简单的插值法是最近邻插值法,也叫零阶插值法[2]。。它的原理很简单,即在缩放过程中,对于目标图像中的每个像素,找到在原始图像中距离最近的像素,并将其像素值赋给目标像素。
如果(i+u, j+v)落在A区,即u<0.5, v<0.5,则将左上角象素的灰度值赋给待求象素,同理,落在B区则赋予右上角的象素灰度值,落在C区则赋予左下角象素的灰度值,落在D区则赋予右下角象素的灰度值。
最邻近元法计算量较小,但可能会造成插值生成的图像灰度上的不连续,在灰度变化的地方可能出现明显的锯齿状。
例程
///scale
typedef struct _VSImage
{
guchar *pixels;
int width;
int height;
int stride;
}VSImage;
static void vs_scanline_resample_nearest_Y (guchar * dest, guchar * src, int src_width,
int n, int increment)
{
int i;
int j;
int x;
for (i = 0; i < n; i++) {
j = acc >> 16;
x = acc & 0xffff;
dest[i] = (x < 32768 || j + 1 >= src_width) ? src[j] : src[j + 1];
acc += increment;
}
}
static void vs_image_scale_nearest_Y (const VSImage * dest, const VSImage * src, guchar * tmpbuf)
{
int acc;
int y_increment;
int x_increment;
int i;
int j;
if (dest->height == 1)
y_increment = 0;
else
y_increment = ((src->height - 1) << 16) / (dest->height - 1);
if (dest->width == 1)
x_increment = 0;
else
x_increment = ((src->width - 1) << 16) / (dest->width - 1);
acc = 0;
for (i = 0; i < dest->height; i++)
{
j = acc >> 16;
vs_scanline_resample_nearest_Y (dest->pixels + i * dest->stride,
src->pixels + j * src->stride, src->width, dest->width, x_increment);
acc += y_increment;
}
}
双线性插值算法
双线性插值又叫一阶插值法[3],它要经过三次插值才能获得最终结果,是对最近邻插值法的一种改进,与最邻近差值算法相比,双线性插值算法考虑了目标像素周围的四个最近像素,从而得到更平滑的结果。
具体步骤如下:
-
对于目标图像中的每个像素 (X,Y),计算其在原始图像中的对应位置
(x,y)。这通常涉及到缩放比例的计算,以及向最近的整数坐标取整。 -
(x,y) 周围的四个最近像素 (x1,y1),(x2,y2),(x1,y2),(x2,y1),这些像素通常位于原始图像中距离 (x,y) 最近的四个整数坐标。
-
计算目标像素 (X,Y) 的像素值,通过对周围四个像素进行加权平均得到。双线性插值的权重基于目标像素与周围四个像素的相对位置关系。
双线性插值算法是一种常用的图像缩放插值方法,它通过对目标图像中的每个像素进行插值来确定其像素值。与最邻近差值算法相比,双线性插值算法考虑了目标像素周围的四个最近像素,从而得到更平滑的结果。
具体步骤如下:
-
对于目标图像中的每个像素 ( (X, Y) ),计算其在原始图像中的对应位置(x, y)。这通常涉及到缩放比例的计算,以及向最近的整数坐标取整。
-
确定 (x, y)周围的四个最近像素 ( (x_1, y_1), (x_2, y_1), (x_1, y_2), (x_2, y_2) )。这些像素通常位于原始图像中距离 ( (x, y) ) 最近的四个整数坐标。
-
计算目标像素(X, Y) 的像素值,通过对周围四个像素进行加权平均得到。双线性插值的权重基于目标像素与周围四个像素的相对位置关系。
假设目标像素为 (X, Y) ,四个最近像素为 (x_1, y_1), (x_2, y_1), (x_1, y_2), (x_2, y_2) ,其对应的像素值分别为 f(x_1, y_1), f(x_2, y_1), f(x_1, y_2), f(x_2, y_2) 。
则目标像素 (X, Y) 的像素值 ( f(X, Y) ) 可以通过以下公式计算得到:
f
(
X
,
Y
)
=
(
1
−
α
)
(
1
−
β
)
f
(
x
1
,
y
1
)
+
α
(
1
−
β
)
f
(
x
2
,
y
1
)
+
(
1
−
α
)
β
f
(
x
1
,
y
2
)
+
α
β
f
(
x
2
,
y
2
)
f(X, Y) = (1 - \alpha)(1 - \beta) f(x_1, y_1) + \alpha(1 - \beta) f(x_2, y_1) + (1 - \alpha)\beta f(x_1, y_2) + \alpha\beta f(x_2, y_2)
f(X,Y)=(1−α)(1−β)f(x1,y1)+α(1−β)f(x2,y1)+(1−α)βf(x1,y2)+αβf(x2,y2)
其中,alpha和beta 分别表示目标像素 ( (X, Y) ) 在 ( x ) 和 ( y ) 方向上与最近像素 ( (x_1, y_1) ) 之间的距离比例。这些比例通常通过插值计算得到。
双线性插值算法通过考虑像素周围的局部信息,可以得到更平滑、更准确的图像缩放结果,相较于最邻近差值算法,在视觉上更加自然。因此,在图像处理和计算机视觉领域,双线性插值算法是一种常用的图像缩放插值方法。
例程
///scale
typedef struct _VSImage
{
guchar *pixels;
int width;
int height;
int stride;
} VSImage;
static void vs_scanline_resample_bilinear_Y(guchar *dest, guchar *src, int src_width,
int n, int increment)
{
int i;
int j;
int x;
for (i = 0; i < n; i++)
{
j = acc >> 16;
x = acc & 0xffff;
// 计算四个最近像素的值
int p00 = src[j];
int p10 = (j + 1 < src_width) ? src[j + 1] : p00;
int p01 = (j + src->stride < src_width) ? src[j + src->stride] : p00;
int p11 = (j + 1 < src_width && j + src->stride < src_width) ? src[j + 1 + src->stride] : p00;
// 双线性插值计算
int value = (p00 * (65536 - x) * (65536 - y) + p10 * x * (65536 - y) +
p01 * y * (65536 - x) + p11 * x * y) >> 32;
dest[i] = value;
acc += increment;
}
}
static void vs_image_scale_bilinear_Y(const VSImage *dest, const VSImage *src, guchar *tmpbuf)
{
int acc;
int y_increment;
int x_increment;
int i;
int j;
if (dest->height == 1)
y_increment = 0;
else
y_increment = ((src->height - 1) << 16) / (dest->height - 1);
if (dest->width == 1)
x_increment = 0;
else
x_increment = ((src->width - 1) << 16) / (dest->width - 1);
acc = 0;
for (i = 0; i < dest->height; i++)
{
j = acc >> 16;
vs_scanline_resample_bilinear_Y(dest->pixels + i * dest->stride,
src->pixels + j * src->stride, src->width, dest->width, x_increment);
acc += y_increment;
}
}
立方卷积插值
双三次插值又称立方卷积插值。三次卷积插值是一种更加复杂的插值方式。该算法利用待采样点周围16个点的灰度值作三次插值,不仅考虑到4 个直接相邻点的灰度影响,而且考虑到各邻点间灰度值变化率的影响。三次运算可以得到更接近高分辨率图像的放大效果,但也导致了运算量的急剧增加。
具体步骤如下,
-
选取(x, y)附近的16个点作为参考像素
如上图所示,当我们求出结果图某个像素对应于原图的像素P00时(映射点),其他的15个像素位置就知道了。
所以关键就在于找到P00。
找到P00很简单,方法如下
首先计算它映射到原图中的坐标(i + v, j + u)
然后卷积计算时,p00点对应(i, j)坐标 -
最后利用BiCubic基函数求出16个像素点的权重,(x,y)的值就等于16个像素点的加权叠加。
不要被下面复杂的公式吓到,它只是用来计算这16个像素点的权重的。
我们要做的就是求出BiCubic函数中的参数x,从而获得上面所说的16个像素所对应的权重W(x)。BiCubic基函数是一维的,而像素是二维的,所以我们将像素点的行与列分开计算。
最终计算公式如下
例程
///scale_cubic
typedef struct _VSImage
{
guchar *pixels;
int width;
int height;
int stride;
} VSImage;
static int cubic_convolution(int p[4], double x)
{
double x2, x3;
x2 = x * x;
x3 = x2 * x;
return (int)(0.5 * (p[1] + (x - 1) * (p[2] - p[0] + (x - 2) * (p[0] - 2 * p[1] + p[2] + (x - 3) * (2 * p[1] - p[0] - p[2] + (x - 4) * (p[0] - 3 * p[1] + 3 * p[2] - p[3]))))));
}
static void vs_scanline_resample_cubic_Y(guchar *dest, guchar *src, int src_width,
int n, double increment)
{
int i, j;
double x;
int p[4];
for (i = 0; i < n; i++)
{
j = acc >> 16;
x = acc & 0xffff;
// 计算周围四个像素的值
p[0] = (j - 1 >= 0) ? src[j - 1] : src[j];
p[1] = src[j];
p[2] = (j + 1 < src_width) ? src[j + 1] : src[j];
p[3] = (j + 2 < src_width) ? src[j + 2] : src[j];
// 计算立方卷积插值
dest[i] = (guchar)cubic_convolution(p, x / 65536.0);
acc += increment;
}
}
static void vs_image_scale_cubic_Y(const VSImage *dest, const VSImage *src, guchar *tmpbuf)
{
int acc;
double y_increment;
double x_increment;
int i, j;
if (dest->height == 1)
y_increment = 0;
else
y_increment = ((src->height - 1) << 16) / (double)(dest->height - 1);
if (dest->width == 1)
x_increment = 0;
else
x_increment = ((src->width - 1) << 16) / (double)(dest->width - 1);
acc = 0;
for (i = 0; i < dest->height; i++)
{
j = acc >> 16;
vs_scanline_resample_cubic_Y(dest->pixels + i * dest->stride,
src->pixels + j * src->stride, src->width, dest->width, x_increment);
acc += (int)y_increment;
}
}