这是对你关于MATLAB中
imresize
和OpenCV中
cv::resize
在使用双三次插值时差异的
之前 问题的跟进。
我自己也很感兴趣,想要找出差异的原因。这是我的研究结果(如果我有任何错误,请指正)。
将图像大小调整视为从大小为
M乘以N
的输入图像到大小为
scaledM乘以scaledN
的输出图像的平面变换。
问题在于点不一定适配离散网格,因此为了获得输出图像中像素的强度,我们需要插值某些相邻样本的值(通常按照反向顺序执行,即对于每个输出像素,我们找到其对应的非整数点在输入空间中进行插值)。
这就是插值算法的差异之处,通过选择邻域的大小和给予该邻域中每个点的权重系数来实现。关系可以是一阶或高阶(涉及的变量是从反映射的非整数样本到原始图像网格上的离散点的距离)。通常将更接近的点赋予更高的权重。
查看MATLAB中的imresize
,以下是线性和立方卷积核的权重函数:
function f = triangle(x)
f = (1+x) .* ((-1 <= x) & (x < 0)) + ...
(1-x) .* ((0 <= x) & (x <= 1));
end
function f = cubic(x)
absx = abs(x);
absx2 = absx.^2;
absx3 = absx.^3;
f = (1.5*absx3 - 2.5*absx2 + 1) .* (absx <= 1) + ...
(-0.5*absx3 + 2.5*absx2 - 4*absx + 2) .* ((1 < absx) & (absx <= 2));
end
这些函数基本上返回样本与插值点之间的差距,从而确定样本所占的比重。
这些函数长这个样子:
>> subplot(121), ezplot(@triangle,[-2 2]) % triangle
>> subplot(122), ezplot(@cubic,[-3 3]) % Mexican hat
![interpolation_kernels](https://istack.dev59.com/JJ6ll.webp)
请注意,线性核(在[-1,0]和[0,1]间隔上的分段线性函数,在其他地方为零)适用于2个相邻点,而立方核(在区间[-2,-1],[-1,1]和[1,2]上的分段三次函数,在其他地方为零)适用于4个相邻点。
以下是一维情况的示例,显示如何使用立方核从离散点
f(x_k)
中插值得到值
x
:
![1d_interpolation](https://istack.dev59.com/eHSvT.webp)
核函数 h(x)
以点 x
为中心,即待插值点的位置。插值值 f(x)
是离散相邻点(左边2个和右边2个)的加权和,乘上这些离散点处插值函数的值。
假设 x
与最近点之间的距离为 d
(0 <= d < 1
),则在位置 x
处的插值值为:
f(x) = f(x1)*h(-d-1) + f(x2)*h(-d) + f(x3)*h(-d+1) + f(x4)*h(-d+2)
下面显示了点的顺序(注意 x(k+1)-x(k) = 1
):
x1 x2 x x3 x4
o--------o---+----o--------o
\___/
distance d
现在,由于点是离散的且以均匀间隔采样,并且内核宽度通常很小,因此插值可以简洁地表述为卷积操作:
![interp_conv_equation](https://istack.dev59.com/Aghi4.webp)
该概念在二维情况下,首先沿着一个维度进行插值,然后使用上一步骤的结果沿另一个维度进行插值即可实现。以下是双线性插值的示例,在二维中考虑4个相邻点:
![bilinear_interpolation](https://istack.dev59.com/RBHwC.webp)
在二维中双三次插值使用16个相邻点:
![bicubic](https://istack.dev59.com/MmBCw.webp)
首先,我们使用16个网格样本(粉色)沿着行方向进行插值(红点)。然后,使用上一步中的插值点沿另一个维度进行插值(红线)。在每个步骤中,进行常规的一维插值。对我来说,这些方程太长、太复杂了,无法手工计算!
现在如果我们回到MATLAB中的“cubic”函数,它实际上与
参考文献中所示的卷积核定义(方程4)匹配。下面是从
维基百科中提取的同样内容:
![conv_kernel](https://istack.dev59.com/Dn6rm.webp)
你可以看到在上述定义中,MATLAB选择了
a=-0.5
的值。
现在MATLAB和OpenCV实现之间的差异是
OpenCV选择了
a=-0.75
的值。
static inline void interpolateCubic( float x, float* coeffs )
{
const float A = -0.75f;
coeffs[0] = ((A*(x + 1) - 5*A)*(x + 1) + 8*A)*(x + 1) - 4*A;
coeffs[1] = ((A + 2)*x - (A + 3))*x*x + 1;
coeffs[2] = ((A + 2)*(1 - x) - (A + 3))*(1 - x)*(1 - x) + 1;
coeffs[3] = 1.f - coeffs[0] - coeffs[1] - coeffs[2];
}
这可能不是立即显而易见的,但代码确实计算了三次卷积函数的项(在论文中的25号公式右侧列出)。
![bicubic_kernel](https://istack.dev59.com/sb53K.webp)
我们可以借助符号数学工具箱进行验证:
A = -0.5;
syms x
c0 = ((A*(x + 1) - 5*A)*(x + 1) + 8*A)*(x + 1) - 4*A;
c1 = ((A + 2)*x - (A + 3))*x*x + 1;
c2 = ((A + 2)*(1 - x) - (A + 3))*(1 - x)*(1 - x) + 1;
c3 = 1 - c0 - c1 - c2;
这些表达式可以重写为:
>> expand([c0;c1;c2;c3])
ans =
- x^3/2 + x^2 - x/2
(3*x^3)/2 - (5*x^2)/2 + 1
- (3*x^3)/2 + 2*x^2 + x/2
x^3/2 - x^2/2
从上面的方程式中可以匹配出相应的项。
显然,MATLAB和OpenCV之间的区别在于使用不同的自由项a
值。根据论文作者的说法,0.5
是首选值,因为与a
的其他任何选择相比,它意味着更好的近似误差性能。
a
的值在两个实现中都是硬编码的。如果我们想让它们匹配,在MATLAB中,您需要修改内置的imresize
函数(我从不喜欢这样做),而在OpenCV中,您需要编译整个源代码,只为了翻转一个细微的值!有趣的是,如果OP在OpenCV中将a
更改为-0.5
并验证我们在两个实现之间获得相同的结果,那将是很有意思的。上一次我尝试过,记得编译OpenCV从头开始要花费10分钟。 - Amroa = -0.5f
与MATLAB给出的结果完全相同。我已经更新了我的答案以回答你的另一个问题,但我也会在这里添加一些内容。顺便说一下,使用普通硬盘,我的i7笔记本电脑编译大约需要6分钟。 :) 只需禁用CUDA! - chappjc