呼,花了一个下午,终于是写完加调试完了所有的代码。
双三次插值介绍
之前我写的这篇博客中讲了什么是超分,并实现了单线性插值算法和双线性插值算法。在这里将再介绍一种插值算法——双三次插值算法。
首先,双三次插值法需要参考16个点(4x4),因此插值效果会比双线性插值法要好,但同时时间开销也会更大。在 OpenCV 中,可在 cv::resize 函数中使用 cv::INTER_CUBIC 选项选择使用双三次插值算法改变图像大小。
在学习的过程中,我参考了这篇博客,其中的插值算法写成表达式的形式为:
f
(
x
,
y
)
=
∑
i
=
0
3
∑
j
=
0
3
f
(
x
i
,
y
j
)
W
(
x
−
x
i
)
W
(
y
−
y
j
)
f(x,y)=\sum_{i=0}^3\sum_{j=0}^3f(x_i,y_j)W(x-x_i)W(y-y_j)
f(x,y)=i=0∑3j=0∑3f(xi,yj)W(x−xi)W(y−yj)
其中,(x,y) 表示待插值的像素点的坐标,f(x,y)表示经过计算待插值像素点应该插入的值,
(
x
i
,
y
j
)
(x_i,y_j)
(xi,yj)
i
,
j
=
0
,
1
,
2
,
3
i,j=0,1,2,3
i,j=0,1,2,3 表示待插值点附近的 4x4 领域的点。
W W W函数称为 BiCubic 函数。与该博客中不同的是,原博客中的像素点可以是浮点数,而在 OpenCV 中坐标只能为整数。因此在这里 W W W 函数需要做个变换。
原博客中的
W
W
W 函数:
W
(
x
)
=
{
(
a
+
2
)
∣
x
∣
3
−
(
a
+
3
)
∣
x
∣
2
+
1
∣
x
∣
≤
1
a
∣
x
∣
3
−
5
a
∣
x
∣
2
+
8
a
∣
x
∣
−
4
a
1
<
∣
x
∣
<
2
0
e
l
s
e
W(x)=\left\{ \begin{matrix} (a+2)|x|^3-(a+3)|x|^2+1 \qquad|x|\le1\\ a|x|^3-5a|x|^2+8a|x|-4a\qquad1<|x|<2\\ 0\qquad \qquad \qquad else\\ \end{matrix} \right.
W(x)=⎩
⎨
⎧(a+2)∣x∣3−(a+3)∣x∣2+1∣x∣≤1a∣x∣3−5a∣x∣2+8a∣x∣−4a1<∣x∣<20else
在这里使用的
W
W
W 函数:
W
(
x
)
=
{
(
a
+
2
)
∣
x
/
t
∣
3
−
(
a
+
3
)
∣
x
/
t
∣
2
+
1
∣
x
∣
≤
t
a
∣
x
/
t
∣
3
−
5
a
∣
x
/
t
∣
2
+
8
a
∣
x
/
t
∣
−
4
a
t
<
∣
x
∣
<
2
t
0
e
l
s
e
W(x)=\left\{ \begin{matrix} (a+2)|x/t|^3-(a+3)|x/t|^2+1 \qquad |x|\le t\\ a|x/t|^3-5a|x/t|^2+8a|x/t|-4a\qquad t<|x|<2t \\ 0\qquad \qquad \qquad else\\ \end{matrix} \right.
W(x)=⎩
⎨
⎧(a+2)∣x/t∣3−(a+3)∣x/t∣2+1∣x∣≤ta∣x/t∣3−5a∣x/t∣2+8a∣x/t∣−4at<∣x∣<2t0else
其中 t t t 为超分放大倍数, a a a 为指定的值,OpenCV 源码中给的是 -0.75。
代码实现
首先是需要循环遍历每个像素点,逐个计算像素点的值。
进一步地,如何计算像素点的值呢?例如,下图中绿色的点是原图像上的像素点,红色的点是待计算的点,则用黄色框起来的点为参考的像素点。假设其坐标为(i,j)(在dst上,即保存超分结果的图像上),以行为例,则参考src的行号如下图所标注。对于列也是同理。因此我们知道了上述表达式中
x
i
x_i
xi 和
y
j
y_j
yj 的所有取值。
W
(
x
−
x
i
)
W(x-x_i)
W(x−xi) 中的
x
−
x
i
x-x_i
x−xi 表示当前像素点距离参考像素点的
x
x
x 方向上的距离(
y
y
y 方向同理)。根据
W
W
W 函数的表达式可以判断出,这个距离应该要小于两倍的
t
i
m
e
s
times
times (放大倍数)。以左边的参考点为例(向右也是同理),在dst 图像中,对于离待计算点最近的左边的像素点,它们的
x
x
x 方向上的距离为
i
%
t
i
m
e
s
i\%times
i%times 。又因为在 dst 图中,两个绿色的点之间的间距为放大倍数,因此计算点离最左边的参考点的距离为
i
%
t
i
m
e
s
+
t
i
m
e
s
i\%times+times
i%times+times。同时考虑到右侧的点,可以写成更加一般的形式:
i
%
t
i
m
e
s
−
i
∗
t
i
m
e
s
i\%times-i*times
i%times−i∗times。其中
i
=
−
1
,
0
,
1
,
2
i=-1,0,1,2
i=−1,0,1,2 。对于列也是同理。
(注:在代码中,为了与逐个枚举像素点的循环变量区分开,此处的 i j 会写成 r c。其中 r 代表 row,表示行; c 代表 col, 表示列。)
以下是完整的代码:
//权重计算函数
//输入:x:自变量的值 times: 图片超分倍数
//返回值:W(x)计算之后的值
double W(int x,int times){
x=std::abs(x);
//OpenCV 中给的是 -0.75
double a=-0.75;
double abs_=std::abs((double)x/(double)times);
if(x>=2*times) return 0.0;
else if(x>times){
double ans=a*(abs_)*(abs_)*(abs_)-5*a*(abs_)*(abs_)+8*a*(abs_)-4*a;
return ans;
}
else{
double ans=(a+2)*(abs_)*(abs_)*(abs_)-(a+3)*(abs_)*(abs_)+1;
return ans;
}
//不会执行到这里的,哈哈
return -1.0f;
}
//双三次插值
//输入:原图像(单通道),超分倍数
//返回值:超分后的图像
cv::Mat biCubicInterp(const cv::Mat &src, int times){
cv::Mat dst=cv::Mat::zeros(cv::Size(src.cols*times,src.rows*times),CV_8UC1);
for(int i=0;i<dst.rows;i++){
for(int j=0;j<dst.cols;j++){
double val=0.0;
//利用周围16个像素点计算插值
for(int r=-1;r<=2;r++){
//如果参考点超出图像范围,则舍弃
if(i/times+r<0 || i/times+r>=src.rows) continue;
for(int c=-1;c<=2;c++){
//如果参考点超出图像范围,则舍弃
if(j/times+c<0 || j/times+c>=src.cols) continue;
val+=(src.at<uchar>(i/times+r,j/times+c)*W(i%times-r*times,times)*W(j%times-c*times,times));
}
}
//防止越界溢出
if(val>255) val=255;
if(val<0) val=-val;
dst.at<uchar>(i,j)=(unsigned char)val;
}
}
return dst;
}
运行结果
对一张 100x100 分辨率的图片,分别用双线性插值法和双三次插值法放大8倍,处理成 800x800 分辨率的图片。运行结果如下:
双线性插值法:
双三次插值法:
仔细观察可以发现,在像素值发生突变的位置(如数字 5 的周围),双线性插值法超分后的图像会出现方块效应,而使用双三次插值法超分后的图像就显得比较光滑,但是时间开销也会更大。
算法复杂度分析
双线性插值法 O ( 3 n 2 ) O(3n^2) O(3n2)
双三次插值法 O ( 16 n 2 ) O(16n^2) O(16n2)