最近老师给了我一个课题,让我研究一下传统的视频超分方法。(同时还要学 HM 并复现论文,还要学制作操作系统。我真的哭了 TvT)。正好这几天 HM 手册翻译得差不多了(需要补知识,要不然翻译不下去了。。),花了几天时间了解了一下。
什么是超分
超分,全称超分辨率(super resolution),就是将分辨率低的图像通过一定的算法转化为分辨率高的图像,使图像更加清晰,细节更加明显。如下图:
超分算法
超分算法有传统超分和深度学习超分,其中传统超分算法可以使用插值法和 SVM。对于插值法,我现在实现了单线性插值和双线性插值算法,当然还有更多的插值算法,如双线性三插值 and so on 。SVM 方法有点思路,还在尝试。
另外,由于我做的是视频超分算法,而上面提到的这些都是针对单帧图像的,也就是帧内超分。所以我有个大胆的想法:有没有可能研究出帧间超分的算法呢?(当然想想就好,以我现阶段的能力估计还做不到。)
单线性插值
这是我第一个想到的算法,插值过程如下图所示:
1.原图像(绿点表示原来的像素点,空白的表示插值的位置)
2. 第一次插值(蓝色的1)
此处先进行行插值。当然也可以选择先进行列插值。
- 第二次插值(红色的2)
至此,整个插值算法结束。
当然这个插值算法是有很大的不足的,因为在插值过程中只单独参考了行或者列周围的像素值,并没有综合参考行和列,因此行列间落差会很大,且先行后列和先列后行的顺序不同也会导致处理后得到的图片不同。
代码如下:
(src: 源图像,times: 放大倍数,返回值:超分完成的图像)
cv::Mat singleLinear(const cv::Mat&src,int times=2){
cv::Mat dst;
dst=cv::Mat::zeros(cv::Size(src.rows*times,src.cols*times),CV_8UC1);
//第一次插值
for(int j=0;j<src.cols;j++){
for(int i=1;i<src.rows;i++){
double delta=(double)(src.at<uchar>(i-1,j)-src.at<uchar>(i,j))/times*1.0;
double current_val=src.at<uchar>(i-1,j);
for(int k=0;k<times;k++){
//防止越界溢出
if(current_val<=0) current_val=0;
if(current_val>=255) current_val=255;
dst.at<uchar>((i-1)*times+k,j*times)=(unsigned char)current_val;
current_val+=delta;
}
}
}
//第二次插值
for(int i=0;i<dst.cols;i++){
for(int j=0;j<src.rows;j++){
double delta=(double)(dst.at<uchar>(i,(j+1)*times)-dst.at<uchar>(i,j*times))/times*1.0;
double current_val=dst.at<uchar>(i,j*times);
for(int k=1;k<times;k++){
//防止越界溢出
if(current_val<=0) current_val=0;
if(current_val>=255) current_val=255;
dst.at<uchar>(i,j*times+k)=(unsigned char)current_val;
current_val+=delta;
}
}
}
return dst;
}
所使用的源图像(分辨率:25x25)
超分后的结果(放大20倍,即500x500)
可以看到,效果确实不尽人意,像素块非常的明显
双线性插值
参考这篇博客
双线性插值算法的优点有:每次插值用上4个点,同时考虑到行和列元素的值,使得插值效果更好。该算法也是 OpenCV 中 cv::resize 函数默认采用的算法。
程序代码:
cv::Mat bitLinear(const cv::Mat &src,int times){
cv::Mat dst=cv::Mat::zeros(cv::Size(src.cols*times,src.rows*times),CV_8UC1);
//先对边界进行单线性插值处理
for(int j=0;j<dst.cols;j++){
double lamda=(j%times)/(double)times;
dst.at<uchar>(0,j)=(1-lamda)*src.at<uchar>(0,j/times)+lamda*src.at<uchar>(0,j/times+1);
dst.at<uchar>(dst.rows-1,j)=(1-lamda)*src.at<uchar>(src.rows-1,j/times)+lamda*src.at<uchar>(src.rows-1,j/times+1);
}
for(int i=0;i<dst.rows-1;i++){
double lamda=(i%times)/(double)times;
dst.at<uchar>(i,0)=(1-lamda)*src.at<uchar>(i/times,0)+lamda*src.at<uchar>(i/times+1,0);
dst.at<uchar>(i,dst.cols-1)=(1-lamda)*src.at<uchar>(i/times,dst.cols-1)+lamda*src.at<uchar>(i/times+1,dst.cols-1);
}
for(int i=1;i<dst.rows-1;i++){
double lamda_rows=(i%times)/(double)times;
for(int j=1;j<dst.cols-1;j++){
double lamda_cols=(j%times)/(double)times;
double top_mid=(1-lamda_cols)*src.at<uchar>(i/times,j/times)+lamda_cols*src.at<uchar>(i/times,j/times+1);
double bot_mid=(1-lamda_cols)*src.at<uchar>(i/times+1,j/times)+lamda_cols*src.at<uchar>(i/times+1,j/times+1);
dst.at<uchar>(i,j)=(1-lamda_rows)*top_mid+lamda_rows*bot_mid;
}
}
return dst;
}
我这里是先对边界进行处理,存在一些问题,会导致边界上像素值突变。但并不影响我们的观感
采用图片:
运行结果(放大20倍):
跟单线性插值相比,看起来真的舒服多了,是吧 ~