[OpenCV实战]44 使用OpenCV进行图像超分放大
图像超分辨率(Image Super Resolution)是指从低分辨率图像或图像序列得到高分辨率图像。图像超分辨率是计算机视觉领域中一个非常重要的研究问题,广泛应用于医学图像分析、生物识别、视频监控和安全等领域。随着深度学习技术的发展,基于深度学习的图像超分方法在多个测试任务上,相比传统图像超分方法,取得了更优的性能和效果。
文章目录
关于基于深度学习的图像超分辨率的综述可以见文章:
关于基于深度学习的图像超分辨率放大的介绍和最新进展可以见文章:
OpenCV contrib库中dnn_superres模块用于实现基于深度学习的图像超分放大,本文主要介绍使用此模块进行超分放大。关于dnn_superres模块的代码介绍可以见:
本文需要OpenCV contrib库,OpenCV contrib库的编译安装见:
本文所有代码见:
1 OpenCV dnn_superres模块介绍
dnn_superres包含四种基于深度学习的算法,用于放大图像,这些模型能让图像放大2~4倍。具体模型介绍如下:
EDSR
- 模型和官方代码地址:EDSR_Tensorflow
- 论文:Enhanced Deep Residual Networks for Single Image Super-Resolution
- 模型大小:〜38.5MB。这是一个量化版本,因此可以将其上传到GitHub。(原始模型大小为150MB。)
- 模型参数:提供x2,x3,x4训练模型
- 优点:高精度
- 缺点:模型文件大且运行速度慢
- 速度:在Intel i7-9700K CPU上的256x256图像,每个放大比例所需时间均小于3秒。
ESPCN
- 模型和官方代码地址:TF-ESPCN
- 论文:Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network
- 模型大小:〜100kb
- 模型参数:提供x2,x3,x4训练模型
- 优点:体积小,速度快,并且仍然表现良好
- 缺点:与更新的、更健壮的模型相比,在视觉上表现更差。
- 速度:在Intel i7-9700K CPU上的256x256图像上,每个放大比例所需时间均小于0.01秒。
FSRCNN
- 模型和官方代码地址:FSRCNN_Tensorflow
- 论文:Accelerating the Super-Resolution Convolutional Neural Network
- 模型大小:〜40KB(对于FSRCNN-small,约为9kb)
- 模型参数:提供x2,x3,x4训练模型和small训练模型
- 优点:快速,小巧
- 缺点:不够准确
- 速度:在Intel i7-9700K CPU上的256x256图像上,每个放大比例所需时间均小于0.01秒。
- 其他:FSRCNN-small具有较少的参数,因此精度较低,但速度更快。
LapSRN
- 模型和官方代码地址:TF-LAPSRN
- 论文:Deep laplacian pyramid networks for fast and accurate super-resolution
- 模型大小:1-5Mb之间
- 模型参数:提供x2,x4,x8训练模型
- 优点:该模型可以通过一次向前传递进行多尺度超分辨率。可以支持2x,4x,8x和[2x,4x]和[2x,4x,8x]超分辨率。
- 缺点:它比ESPCN和FSRCNN慢,并且精度比EDSR差。
- 速度:在Intel i7-9700K CPU上的256x256图像上,每个放大比例所需时间均小于0.1秒。。
2 OpenCV dnn_superres模块使用
2.1 图像超分放大单输出
2.1.1 接口介绍
在本节中,我们将学习如何使用dnn_superres中的函数,通过已有训练的神经网络对图像进行放大。实际上就是调用模型构造模型,只不过dnn_superres对这些模型的调用函数进行了封装,并且建立了通用接口。调用方法如下:
C++
// Make dnn super resolution instance
// 创建dnn超分辨率对象
DnnSuperResImpl sr;
// 读取模型
sr.readModel(path);
// 设定算法和放大比例
sr.setModel(algorithm, scale);
// 放大图像
sr.upsample(img, img_new);
Python
# 创建模型
sr = dnn_superres.DnnSuperResImpl_create()
# 读取模型
sr.readModel(path)
# 设定算法和放大比例
sr.setModel(algorithm, scale)
# 放大图像
img_new = sr.upsample(img)
2.1.2 示例代码
主要展示通过OpenCV自带resize函数或调用深度学习将一张图像发大指定倍数,C++代码和Python代码如下。
C++/dnn_superres.cpp
// 图像超分放大单输出
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn_superres.hpp>
using namespace std;
using namespace cv;
using namespace dnn;
using namespace dnn_superres;
int main()
{
string img_path = string("./image/image.png");
// 可选择算法,bilinear, bicubic, edsr, espcn, fsrcnn or lapsrn
string algorithm = string("fsrcnn");
// 放大比例,可输入值2,3,4
int scale = 4;
// 模型路径
string path = "./model/FSRCNN-small_x4.pb";
// Load the image
// 载入图像
Mat img = cv::imread(img_path);
// 如果输入的图像为空
if (img.empty())
{
std::cerr << "Couldn't load image: " << img << "\n";
return -2;
}
Mat original_img(img);
// Make dnn super resolution instance
// 创建dnn超分辨率对象
DnnSuperResImpl sr;
// 超分放大后的图像
Mat img_new;
// 双线性插值
if (algorithm == "bilinear")
{
resize(img, img_new, Size(), scale, scale, cv::INTER_LINEAR);
}
// 双三次插值
else if (algorithm == "bicubic")
{
resize(img, img_new, Size(), scale, scale, cv::INTER_CUBIC);
}
else if (algorithm == "edsr" || algorithm == "espcn" || algorithm == "fsrcnn" || algorithm == "lapsrn")
{
// 读取模型
sr.readModel(path);
// 设定算法和放大比例
sr.setModel(algorithm, scale);
// 放大图像
sr.upsample(img, img_new);
}
else
{
std::cerr << "Algorithm not recognized. \n";
}
// 如果失败
if (img_new.empty())
{
// 放大失败
std::cerr << "Upsampling failed. \n";
return -3;
}
cout << "Upsampling succeeded. \n";
// Display image
// 展示图片
cv::namedWindow("Initial Image", WINDOW_AUTOSIZE);
// 初始化图片
cv::imshow("Initial Image", img_new);
//cv::imwrite("./saved.jpg", img_new);
cv::waitKey(0);
return 0;
}
Python/dnn_superres.py
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 20 20:08:22 2020
@author: luohenyueji
图像超分放大单输出
"""
import cv2
from cv2 import dnn_superres
def main():
img_path = "./image/image.png"
# 可选择算法,bilinear, bicubic, edsr, espcn, fsrcnn or lapsrn
algorithm = "bilinear"
# 放大比例,可输入值2,3,4
scale = 4
# 模型路径
path = "./model/LapSRN_x4.pb"
# 载入图像
img = cv2.imread(img_path)
# 如果输入的图像为空
if img is None:
print("Couldn't load image: " + str(img_path))
return
original_img = img.copy()
# 创建模型
sr = dnn_superres.DnnSuperResImpl_create()
if algorithm == "bilinear":
img_new = cv2.resize(img, None, fx=scale, fy=scale, interpolation=cv2.INTER_LINEAR)
elif algorithm == "bicubic":
img_new = cv2.resize(img, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
elif algorithm == "edsr" or algorithm == "espcn" or algorithm == "fsrcnn" or algorithm == "lapsrn":
# 读取模型
sr.readModel(path)
# 设定算法和放大比例
sr.setModel(algorithm, scale)
# 放大图像
img_new = sr.upsample(img)
else:
print("Algorithm not recognized")
# 如果失败
if img_new is None:
print("Upsampling failed")
print("Upsampling succeeded. \n")
# Display
# 展示图片
cv2.namedWindow("Initial Image", cv2.WINDOW_AUTOSIZE)
# 初始化图片
cv2.imshow("Initial Image", img_new)
cv2.imwrite("./saved.jpg", img_new)
cv2.waitKey(0)
if __name__ == '__main__':
main()
2.1.3 结果
放大四倍,不同算法效果如下所示:
方法 | 结果 |
---|---|
原图 | |
bilinear | |
bicubic | |
edsr | |
espcn | |
fsrcnn | |
fsrcnn-small | |
lapsrn |
2.2 图像超分放大多输出
2.2.1 接口介绍
本节主要介绍如何通过LapSRN多输出来放大图像。如果给出了节点的名称,OpenCV的dnn模块支持一次推断访问多个节点。LapSRN模型可以在一次推理运行中提供更多输出。现在,LapSRN模型可以支持2x,4x,8x和(2x,4x)和(2x,4x,8x)超分辨率。经过训练的LapSRN模型文件具有以下输出节点名称:
- 2x模型:NCHW_output
- 4x模型:NCHW_output_2x,NCHW_output_4x
- 8x模型:NCHW_output_2x,NCHW_output_4x,NCHW_output_8x
其次这个功能用处不那么大,LapSRN效果很一般。不过看看挺好的。
由于Python相关实现代码有所问题,因此该部分只提供C++代码。调用方法如下。相比单输出放大,需要设定输出层名字并通过upsampleMultioutput输出各输出层的放大结果。
// 可选多输入放大比例2,4,8。','分隔放大比例
string scales_str = string("2,4,8");
// 可选模型输出放大层比例名,NCHW_output_2x,NCHW_output_4x,NCHW_output_8x
// 需要根据模型和输入放大比例共同确定确定
string output_names_str = string("NCHW_output_2x,NCHW_output_4x,NCHW_output_8x");
// 创建Dnn Superres对象
DnnSuperResImpl sr;
// 获得最大放大比例
int scale = *max_element(scales.begin(), scales.end());
std::vector<Mat> outputs;
// 读取模型
sr.readModel(path);
// 设定模型输出
sr.setModel("lapsrn", scale);
// 多输出超分放大图像
sr.upsampleMultioutput(img, outputs, scales, node_names);
2.2.2 示例代码
C++/dnn_superres_multioutput.cpp
// 图像超分放大多输出
#include <iostream>
#include <sstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn_superres.hpp>
using namespace std;
using namespace cv;
using namespace dnn_superres;
int main()
{
// 图像路径
string img_path = string("./image/image.png");
if (img_path.empty())
{
printf("image is empty!");
}
// 可选多输入放大比例2,4,8。','分隔放大比例
string scales_str = string("2,4,8");
// 可选模型输出放大层比例名,NCHW_output_2x,NCHW_output_4x,NCHW_output_8x
// 需要根据模型和输入放大比例共同确定确定
string output_names_str = string("NCHW_output_2x,NCHW_output_4x,NCHW_output_8x");
// 模型路径
std::string path = string("./model/LapSRN_x8.pb");
// Parse the scaling factors
// 解析放大比例因子
std::vector<int> scales;
char delim = ',';
{
std::stringstream ss(scales_str);
std::string token;
while (std::getline(ss, token, delim))
{
scales.push_back(atoi(token.c_str()));
}
}
// Parse the output node names
// 解析模型放大层参数
std::vector<String> node_names;
{
std::stringstream ss(output_names_str);
std::string token;
while (std::getline(ss, token, delim))
{
node_names.push_back(token);
}
}
// Load the image
// 导入图片
Mat img = cv::imread(img_path);
Mat original_img(img);
if (img.empty())
{
std::cerr << "Couldn't load image: " << img << "\n";
return -2;
}
// Make dnn super resolution instance
// 创建Dnn Superres对象
DnnSuperResImpl sr;
// 获得最大放大比例
int scale = *max_element(scales.begin(), scales.end());
std::vector<Mat> outputs;
// 读取模型
sr.readModel(path);
// 设定模型输出
sr.setModel("lapsrn", scale);
// 多输出超分放大图像
sr.upsampleMultioutput(img, outputs, scales, node_names);
for (unsigned int i = 0; i < outputs.size(); i++)
{
cv::namedWindow("Upsampled image", WINDOW_AUTOSIZE);
// 在图上显示当前放大比例
cv::putText(outputs[i], format("Scale %d", scales[i]), Point(10, 30), FONT_HERSHEY_PLAIN, 2.0, Scalar(255, 0, 255), 2, LINE_AA);
cv::imshow("Upsampled image", outputs[i]);
cv::imwrite(to_string(i) + ".jpg", outputs[i]);
cv::waitKey(-1);
}
return 0;
}
2.2.3 结果
放大二倍、四倍、八倍的LapSRN算法效果如下所示:
方法 | 结果 |
---|---|
原图 | |
LapSRN_x2 | |
LapSRN_x4 | |
LapSRN_x8 |
2.3 视频超分放大
实际视频超分放大输出,就是把视频每一帧提取出来,超分放大每一帧图像。代码如下,实际上如果电脑配置很一般不建议视频超分放大,对电脑配置性能要求很高,建议使用opencv cuda进行运算。
C++/dnn_superres_video.cpp
// 视频超分放大多输出
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn_superres.hpp>
using namespace std;
using namespace cv;
using namespace dnn_superres;
int main()
{
string input_path = string("./video/chaplin.mp4");
string output_path = string("./video/https://gitee.com/luohenyueji/article_picture_warehouse/raw/master/CSDN/%5BOpenCV%E5%AE%9E%E6%88%98%5D44%20%E4%BD%BF%E7%94%A8OpenCV%E8%BF%9B%E8%A1%8C%E5%9B%BE%E5%83%8F%E8%B6%85%E5%88%86%E6%94%BE%E5%A4%A7/out_chaplin.mp4");
// 选择模型 edsr, espcn, fsrcnn or lapsrn
string algorithm = string("lapsrn");
// 放大比例,2,3,4,8,根据模型结构选择
int scale = 2;
// 模型路径
string path = string("./model/LapSRN_x2.pb");
// 打开视频
VideoCapture input_video(input_path);
// 输入图像编码尺寸
int ex = static_cast<int>(input_video.get(CAP_PROP_FOURCC));
// 获得输出视频图像尺寸
Size S = Size((int)input_video.get(CAP_PROP_FRAME_WIDTH) * scale,
(int)input_video.get(CAP_PROP_FRAME_HEIGHT) * scale);
VideoWriter output_video;
output_video.open(output_path, ex, input_video.get(CAP_PROP_FPS), S, true);
// 如果视频没有打开
if (!input_video.isOpened())
{
std::cerr << "Could not open the video." << std::endl;
return -1;
}
// 读取超分放大模型
DnnSuperResImpl sr;
sr.readModel(path);
sr.setModel(algorithm, scale);
for (;;)
{
Mat frame, output_frame;
input_video >> frame;
if (frame.empty())
break;
// 上采样图像
sr.upsample(frame, output_frame);
output_video << output_frame;
namedWindow("Upsampled video", WINDOW_AUTOSIZE);
imshow("Upsampled video", output_frame);
namedWindow("Original video", WINDOW_AUTOSIZE);
imshow("Original video", frame);
char c = (char)waitKey(1);
// esc退出
if (c == 27)
{
break;
}
}
input_video.release();
output_video.release();
return 0;
}
Python/dnn_superres_video.py
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 20 21:08:22 2020
@author: luohenyueji
视频超分放大
"""
import cv2
from cv2 import dnn_superres
def main():
input_path = "./video/chaplin.mp4"
output_path = "./video/https://gitee.com/luohenyueji/article_picture_warehouse/raw/master/CSDN/%5BOpenCV%E5%AE%9E%E6%88%98%5D44%20%E4%BD%BF%E7%94%A8OpenCV%E8%BF%9B%E8%A1%8C%E5%9B%BE%E5%83%8F%E8%B6%85%E5%88%86%E6%94%BE%E5%A4%A7/out_chaplin.mp4"
# 选择模型 edsr, espcn, fsrcnn or lapsrn
algorithm = "lapsrn"
# 放大比例,2,3,4,8,根据模型结构选择
scale = 2
# 模型路径
path = "./model/LapSRN_x2.pb"
# 打开视频
input_video = cv2.VideoCapture(input_path)
# 输入图像编码尺寸
ex = int(input_video.get(cv2.CAP_PROP_FOURCC))
# 获得输出视频图像尺寸
# 如果视频没有打开
if input_video is None:
print("Could not open the video.")
return
S = (
int(input_video.get(cv2.CAP_PROP_FRAME_WIDTH)) * scale, int(input_video.get(cv2.CAP_PROP_FRAME_HEIGHT)) * scale)
output_video = cv2.VideoWriter(output_path, ex, input_video.get(cv2.CAP_PROP_FPS), S, True)
# 读取超分放大模型
sr = dnn_superres.DnnSuperResImpl_create()
sr.readModel(path)
sr.setModel(algorithm, scale)
while True:
ret, frame = input_video.read() # 捕获一帧图像
if not ret:
print("read video error")
return
# 上采样图像
output_frame = sr.upsample(frame)
output_video.write(output_frame)
cv2.namedWindow("Upsampled video", cv2.WINDOW_AUTOSIZE);
cv2.imshow("Upsampled video", output_frame)
cv2.namedWindow("Original video", cv2.WINDOW_AUTOSIZE);
cv2.imshow("Original video", frame)
c = cv2.waitKey(1);
# esc退出
if 27 == c:
break
input_video.release()
output_video.release()
if __name__ == '__main__':
main()
3 不同图像超分算法性能比较
3.1 不同图像超分算法效果评估
通过PSNR和SSIM来评估图像放大后的效果,PSNR越大,图像失真越小。SSIM也是越大,图像失真越小。PSNR和SSIM介绍见博客:PSNR和SSIM
本节对比四类算法放大图像后的PSNR值和SSIM值,因为电脑性能原因只放大2倍。具体放大倍数可自行调试。代码如下:
C++/dnn_superres_benchmark_quality.cpp
// 不同图像超分算法效果评估
#include <iostream>
#include <opencv2/opencv_modules.hpp>
#include <opencv2/dnn_superres.hpp>
#include <opencv2/quality.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
using namespace dnn_superres;
// 展示图片
static void showBenchmark(vector<Mat> images, string title, Size imageSize,
const vector<String> imageTitles,
const vector<double> psnrValues,
const vector<double> ssimValues)
{
// 文字信息
int fontFace = FONT_HERSHEY_COMPLEX_SMALL;
int fontScale = 1;
Scalar fontColor = Scalar(255, 255, 255);
// 图像数量
int len = static_cast<int>(images.size());
int cols = 2, rows = 2;
// 建立背景图像
Mat fullImage = Mat::zeros(Size((cols * 10) + imageSize.width * cols, (rows * 10) + imageSize.height * rows),
images[0].type());
stringstream ss;
int h_ = -1;
// 拼接显示图片
for (int i = 0; i < len; i++)
{
int fontStart = 15;
int w_ = i % cols;
if (i % cols == 0)
h_++;
Rect ROI((w_ * (10 + imageSize.width)), (h_ * (10 + imageSize.height)), imageSize.width, imageSize.height);
Mat tmp;
resize(images[i], tmp, Size(ROI.width, ROI.height));
ss << imageTitles[i];
putText(tmp,
ss.str(),
Point(5, fontStart),
fontFace,
fontScale,
fontColor,
1,
16);
ss.str("");
fontStart += 20;
ss << "PSNR: " << psnrValues[i];
putText(tmp,
ss.str(),
Point(5, fontStart),
fontFace,
fontScale,
fontColor,
1,
16);
ss.str("");
fontStart += 20;
ss << "SSIM: " << ssimValues[i];
putText(tmp,
ss.str(),
Point(5, fontStart),
fontFace,
fontScale,
fontColor,
1,
16);
ss.str("");
fontStart += 20;
tmp.copyTo(fullImage(ROI));
}
namedWindow(title, 1);
imshow(title, fullImage);
imwrite("save.jpg", fullImage);
waitKey();
}
static Vec2d getQualityValues(Mat orig, Mat upsampled)
{
double psnr = PSNR(upsampled, orig);
// 前两个参数为对比图片,第三个参数为输出数组
Scalar q = quality::QualitySSIM::compute(upsampled, orig, noArray());
double ssim = mean(Vec3d((q[0]), q[1], q[2]))[0];
return Vec2d(psnr, ssim);
}
int main()
{
// 图片路径
string img_path = string("./image/image.png");
// 算法名称 edsr, espcn, fsrcnn or lapsrn
string algorithm = string("lapsrn");
// 模型路径,根据算法确定
string model = string("./model/LapSRN_x2.pb");
// 放大系数
int scale = 2;
Mat img = imread(img_path);
if (img.empty())
{
cerr << "Couldn't load image: " << img_path << "\n";
return -2;
}
// Crop the image so the images will be aligned
// 裁剪图像,使图像对齐
int width = img.cols - (img.cols % scale);
int height = img.rows - (img.rows % scale);
Mat cropped = img(Rect(0, 0, width, height));
// Downscale the image for benchmarking
// 缩小图像,以实现基准质量测试
Mat img_downscaled;
resize(cropped, img_downscaled, Size(), 1.0 / scale, 1.0 / scale);
// Make dnn super resolution instance
// 超分模型初始化
DnnSuperResImpl sr;
vector<Mat> allImages;
// 放大后的图片
Mat img_new;
// Read and set the dnn model
// 读取和设定模型
sr.readModel(model);
sr.setModel(algorithm, scale);
// 放大图像
sr.upsample(img_downscaled, img_new);
vector<double> psnrValues = vector<double>();
vector<double> ssimValues = vector<double>();
// DL MODEL
// 获得模型质量评估值
Vec2f quality = getQualityValues(cropped, img_new);
// 模型质量评价PSNR
psnrValues.push_back(quality[0]);
// 模型质量评价SSIM
ssimValues.push_back(quality[1]);
// 数值越大图像质量越好
cout << sr.getAlgorithm() << ":" << endl;
cout << "PSNR: " << quality[0] << " SSIM: " << quality[1] << endl;
cout << "----------------------" << endl;
// BICUBIC
// INTER_CUBIC - 三次样条插值放大图像
Mat bicubic;
resize(img_downscaled, bicubic, Size(), scale, scale, INTER_CUBIC);
quality = getQualityValues(cropped, bicubic);
psnrValues.push_back(quality[0]);
ssimValues.push_back(quality[1]);
cout << "Bicubic " << endl;
cout << "PSNR: " << quality[0] << " SSIM: " << quality[1] << endl;
cout << "----------------------" << endl;
// NEAREST NEIGHBOR
// INTER_NEAREST - 最近邻插值
Mat nearest;
resize(img_downscaled, nearest, Size(), scale, scale, INTER_NEAREST);
quality = getQualityValues(cropped, nearest);
psnrValues.push_back(quality[0]);
ssimValues.push_back(quality[1]);
cout << "Nearest neighbor" << endl;
cout << "PSNR: " << quality[0] << " SSIM: " << quality[1] << endl;
cout << "----------------------" << endl;
// LANCZOS
// Lanczos插值放大图像
Mat lanczos;
resize(img_downscaled, lanczos, Size(), scale, scale, INTER_LANCZOS4);
quality = getQualityValues(cropped, lanczos);
psnrValues.push_back(quality[0]);
ssimValues.push_back(quality[1]);
cout << "Lanczos" << endl;
cout << "PSNR: " << quality[0] << " SSIM: " << quality[1] << endl;
cout << "-----------------------------------------------" << endl;
// 要显示的图片
vector<Mat> imgs{ img_new, bicubic, nearest, lanczos };
// 要显示的标题
vector<String> titles{ sr.getAlgorithm(), "Bicubic", "Nearest neighbor", "Lanczos" };
showBenchmark(imgs, "Quality benchmark", Size(bicubic.cols, bicubic.rows), titles, psnrValues, ssimValues);
waitKey(0);
return 0;
}
Python/dnn_superres_benchmark_quality.py
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 20 22:08:22 2020
@author: luohenyueji
不同图像超分算法效果评估
"""
import cv2
from cv2 import dnn_superres
import numpy as np
# TODO 绘图
def showBenchmark(imgs, titles, psnrValues, ssimValues):
# 绘图
for i in range(0, len(imgs)):
# 标题绘图
cv2.putText(imgs[i], titles[i], (10, 30), cv2.FONT_HERSHEY_PLAIN, 1.5,
(255, 0, 255), 2, cv2.LINE_AA)
# psnr值
cv2.putText(imgs[i], "PSNR: " + str(psnrValues[i]), (10, 60), cv2.FONT_HERSHEY_PLAIN, 1.5,
(255, 0, 255), 2, cv2.LINE_AA)
# ssim值
cv2.putText(imgs[i], "SSIM: " + str(ssimValues[i]), (10, 90), cv2.FONT_HERSHEY_PLAIN, 1.5,
(255, 0, 255), 2, cv2.LINE_AA)
# 图片拼接展示
img = np.vstack([np.hstack([imgs[0], imgs[1]]), np.hstack([imgs[2], imgs[3]])])
cv2.imshow("Quality benchmark", img)
cv2.waitKey(0)
# TODO 图像质量评估
def getQualityValues(upsampled, orig):
psnr = cv2.PSNR(upsampled, orig)
q, _ = cv2.quality.QualitySSIM_compute(upsampled, orig)
ssim = (q[0] + q[1] + q[2]) / 3
return round(psnr, 3), round(ssim, 3)
def main():
# 图片路径
img_path = "./image/butterfly.png"
# 算法名称 edsr, espcn, fsrcnn or lapsrn
algorithm = "lapsrn"
# 模型路径,根据算法确定
model = "./model/LapSRN_x2.pb"
# 放大系数
scale = 2
psnrValues = []
ssimValues = []
img = cv2.imread(img_path)
if img is None:
print("Couldn't load image: " + str(img_path))
# Crop the image so the images will be aligned
# 裁剪图像,使图像对齐
width = img.shape[0] - (img.shape[0] % scale)
height = img.shape[1] - (img.shape[1] % scale)
cropped = img[0:width, 0:height]
# Downscale the image for benchmarking
# 缩小图像,以实现基准质量测试
img_downscaled = cv2.resize(cropped, None, fx=1.0 / scale, fy=1.0 / scale)
# Make dnn super resolution instance
# 超分模型初始化
sr = dnn_superres.DnnSuperResImpl_create()
# Read and set the dnn model
# 读取和设定模型
sr.readModel(model)
sr.setModel(algorithm, scale)
# 放大图像
img_new = sr.upsample(img_downscaled)
# DL MODEL
# 获得模型质量评估值
psnr, ssim = getQualityValues(cropped, img_new)
psnrValues.append(psnr)
ssimValues.append(ssim)
print(sr.getAlgorithm() + "\n")
print("PSNR: " + str(psnr) + " SSIM: " + str(ssim) + "\n")
print("-" * 50)
# INTER_CUBIC - 三次样条插值放大图像
bicubic = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
psnr, ssim = getQualityValues(cropped, bicubic)
psnrValues.append(psnr)
ssimValues.append(ssim)
print("Bicubic \n")
print("PSNR: " + str(psnr) + " SSIM: " + str(ssim) + "\n")
print("-" * 50)
# INTER_NEAREST - 最近邻插值
nearest = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
psnr, ssim = getQualityValues(cropped, nearest)
psnrValues.append(psnr)
ssimValues.append(ssim)
print("Nearest neighbor \n")
print("PSNR: " + str(psnr) + " SSIM: " + str(ssim) + "\n")
print("-" * 50)
# Lanczos插值放大图像
lanczos = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_LANCZOS4);
psnr, ssim = getQualityValues(cropped, lanczos)
psnrValues.append(psnr)
ssimValues.append(ssim)
print("Lanczos \n")
print("PSNR: " + str(psnr) + " SSIM: " + str(ssim) + "\n")
print("-" * 50)
imgs = [img_new, bicubic, nearest, lanczos]
titles = [sr.getAlgorithm(), "Bicubic", "Nearest neighbor", "Lanczos"]
showBenchmark(imgs, titles, psnrValues, ssimValues)
if __name__ == '__main__':
main()
通过lapsrn模型进行超分放大,结果如图所示。可以知道的是lapsrn模型效果实际最好,但是实际中resize函数调用不同选项也会有类似结果,差距没有想象那么大。
3.2 不同图像超分算法速度评估
本节对比四类算法差分放大所需时间,因为电脑性能原因只放大2倍。具体放大倍数可自行调试。代码如下:
C++/dnn_superres_benchmark_time.cpp
// 不同图像超分算法速度评估
#include <iostream>
#include <opencv2/dnn_superres.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
using namespace dnn_superres;
static void showBenchmark(vector<Mat> images, string title, Size imageSize,
const vector<String> imageTitles,
const vector<double> perfValues)
{
int fontFace = FONT_HERSHEY_COMPLEX_SMALL;
int fontScale = 1;
Scalar fontColor = Scalar(255, 255, 255);
int len = static_cast<int>(images.size());
int cols = 2, rows = 2;
Mat fullImage = Mat::zeros(Size((cols * 10) + imageSize.width * cols, (rows * 10) + imageSize.height * rows),
images[0].type());
stringstream ss;
int h_ = -1;
for (int i = 0; i < len; i++)
{
int fontStart = 15;
int w_ = i % cols;
if (i % cols == 0)
h_++;
Rect ROI((w_ * (10 + imageSize.width)), (h_ * (10 + imageSize.height)), imageSize.width, imageSize.height);
Mat tmp;
resize(images[i], tmp, Size(ROI.width, ROI.height));
ss << imageTitles[i];
putText(tmp,
ss.str(),
Point(5, fontStart),
fontFace,
fontScale,
fontColor,
1,
16);
ss.str("");
fontStart += 20;
ss << perfValues[i];
putText(tmp,
ss.str(),
Point(5, fontStart),
fontFace,
fontScale,
fontColor,
1,
16);
ss.str("");
tmp.copyTo(fullImage(ROI));
}
namedWindow(title, 1);
imshow(title, fullImage);
imwrite("save.jpg", fullImage);
waitKey();
}
int main()
{
// 图片路径
string img_path = string("./image/butterfly.png");
// 算法名称 edsr, espcn, fsrcnn or lapsrn
string algorithm = string("lapsrn");
// 模型路径,根据算法确定
string model = string("./model/LapSRN_x2.pb");
// 放大系数
int scale = 2;
Mat img = imread(img_path);
if (img.empty())
{
cerr << "Couldn't load image: " << img << "\n";
return -2;
}
// Crop the image so the images will be aligned
// 对齐图像
int width = img.cols - (img.cols % scale);
int height = img.rows - (img.rows % scale);
Mat cropped = img(Rect(0, 0, width, height));
// Downscale the image for benchmarking
// 缩小图像,以实现基准测试
Mat img_downscaled;
resize(cropped, img_downscaled, Size(), 1.0 / scale, 1.0 / scale);
// Make dnn super resolution instance
DnnSuperResImpl sr;
Mat img_new;
// Read and set the dnn model
// 读取模型
sr.readModel(model);
sr.setModel(algorithm, scale);
double elapsed = 0.0;
vector<double> perf;
TickMeter tm;
// DL MODEL
// 计算时间
tm.start();
sr.upsample(img_downscaled, img_new);
tm.stop();
// 运行时间s
elapsed = tm.getTimeSec() / tm.getCounter();
perf.push_back(elapsed);
cout << sr.getAlgorithm() << " : " << elapsed << endl;
// BICUBIC
Mat bicubic;
tm.start();
resize(img_downscaled, bicubic, Size(), scale, scale, INTER_CUBIC);
tm.stop();
elapsed = tm.getTimeSec() / tm.getCounter();
perf.push_back(elapsed);
cout << "Bicubic" << " : " << elapsed << endl;
// NEAREST NEIGHBOR
Mat nearest;
tm.start();
resize(img_downscaled, nearest, Size(), scale, scale, INTER_NEAREST);
tm.stop();
elapsed = tm.getTimeSec() / tm.getCounter();
perf.push_back(elapsed);
cout << "Nearest" << " : " << elapsed << endl;
// LANCZOS
Mat lanczos;
tm.start();
resize(img_downscaled, lanczos, Size(), scale, scale, INTER_LANCZOS4);
tm.stop();
elapsed = tm.getTimeSec() / tm.getCounter();
perf.push_back(elapsed);
cout << "Lanczos" << " : " << elapsed << endl;
vector <Mat> imgs{ img_new, bicubic, nearest, lanczos };
vector <String> titles{ sr.getAlgorithm(), "Bicubic", "Nearest neighbor", "Lanczos" };
showBenchmark(imgs, "Time benchmark", Size(bicubic.cols, bicubic.rows), titles, perf);
waitKey(0);
return 0;
}
Python/dnn_superres_benchmark_time.py
# -*- coding: utf-8 -*-
"""
Created on Fri Aug 20 22:38:22 2020
@author: luohenyueji
不同图像超分算法速度评估
"""
import cv2
from cv2 import dnn_superres
import numpy as np
# TODO 绘图
def showBenchmark(imgs, titles, perf):
# 绘图
for i in range(0, len(imgs)):
# 标题绘图
cv2.putText(imgs[i], titles[i], (10, 30), cv2.FONT_HERSHEY_PLAIN, 1.5,
(255, 0, 255), 2, cv2.LINE_AA)
# psnr值
cv2.putText(imgs[i], str(round(perf[i], 3)), (10, 60), cv2.FONT_HERSHEY_PLAIN, 1.5,
(255, 0, 255), 2, cv2.LINE_AA)
# 图片拼接展示
img = np.vstack([np.hstack([imgs[0], imgs[1]]), np.hstack([imgs[2], imgs[3]])])
cv2.imshow("Quality benchmark", img)
cv2.waitKey(0)
def main():
# 图片路径
img_path = "./image/image.png"
# 算法名称 edsr, espcn, fsrcnn or lapsrn
algorithm = "lapsrn"
# 模型路径,根据算法确定
model = "./model/LapSRN_x2.pb"
# 放大系数
scale = 2
# 时间系数
perf = []
img = cv2.imread(img_path)
if img is None:
print("Couldn't load image: " + str(img_path))
# Crop the image so the images will be aligned
# 裁剪图像,使图像对齐
width = img.shape[0] - (img.shape[0] % scale)
height = img.shape[1] - (img.shape[1] % scale)
cropped = img[0:width, 0:height]
# Downscale the image for benchmarking
# 缩小图像,以实现基准质量测试
img_downscaled = cv2.resize(cropped, None, fx=1.0 / scale, fy=1.0 / scale)
# Make dnn super resolution instance
# 超分模型初始化
sr = dnn_superres.DnnSuperResImpl_create()
# Read and set the dnn model
# 读取和设定模型
sr.readModel(model)
sr.setModel(algorithm, scale)
timer = cv2.TickMeter()
timer.start()
# 放大图像
img_new = sr.upsample(img_downscaled)
timer.stop()
# 运行时间s
elapsed = timer.getTimeSec() / timer.getCounter()
perf.append(elapsed)
print(sr.getAlgorithm() + " : " + str(elapsed))
# INTER_CUBIC - 三次样条插值放大图像
timer.start()
bicubic = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
timer.stop()
# 运行时间s
elapsed = timer.getTimeSec() / timer.getCounter()
perf.append(elapsed)
print("Bicubic" + " : " + str(elapsed))
# INTER_NEAREST - 最近邻插值
timer.start()
nearest = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_NEAREST)
timer.stop()
# 运行时间s
elapsed = timer.getTimeSec() / timer.getCounter()
perf.append(elapsed)
print("Nearest" + " : " + str(elapsed))
# Lanczos插值放大图像
timer.start()
lanczos = cv2.resize(img_downscaled, None, fx=scale, fy=scale, interpolation=cv2.INTER_LANCZOS4);
timer.stop()
# 运行时间s
elapsed = timer.getTimeSec() / timer.getCounter()
perf.append(elapsed)
print("Lanczos" + " : " + str(elapsed))
imgs = [img_new, bicubic, nearest, lanczos]
titles = [sr.getAlgorithm(), "Bicubic", "Nearest neighbor", "Lanczos"]
showBenchmark(imgs, titles, perf)
if __name__ == '__main__':
main()
通过lapsrn模型进行超分放大,结果如图所示。图中单位为秒/s。lapsrn是OpenCV提供速度最快和精度最低的DNN超分模块,比resize普通算法效果更好都是耗时更多。
3.3 官方超分放大基准测试
OpenCV官方文档给了数据集下的基础测试结果,具体见:Super-resolution benchmarking
在Ubuntu 18.04.02 OS的Intel i7-9700K CPU上数据集超分放大算法结果如下所示。
2倍超分放大
方法 | 平均时间(s)/cpu | 平均PSNR | 平均SSIM |
---|---|---|---|
ESPCN | 0.008795 | 32.7059 | 0.9276 |
EDSR | 5.923450 | 34.1300 | 0.9447 |
FSRCNN | 0.021741 | 32.8886 | 0.9301 |
LapSRN | 0.114812 | 32.2681 | 0.9248 |
Bicubic | 0.000208 | 32.1638 | 0.9305 |
Nearest neighbor | 0.000114 | 29.1665 | 0.9049 |
Lanczos | 0.001094 | 32.4687 | 0.9327 |
3倍超分放大
方法 | 平均时间(s)/cpu | 平均PSNR | 平均SSIM |
---|---|---|---|
ESPCN | 0.005495 | 28.4229 | 0.8474 |
EDSR | 2.455510 | 29.9828 | 0.8801 |
FSRCNN | 0.008807 | 28.3068 | 0.8429 |
LapSRN | 0.282575 | 26.7330 | 0.8862 |
Bicubic | 0.000311 | 26.0635 | 0.8754 |
Nearest neighbor | 0.000148 | 23.5628 | 0.8174 |
Lanczos | 0.001012 | 25.9115 | 0.8706 |
4倍超分放大
方法 | 平均时间(s)/cpu | 平均PSNR | 平均SSIM |
---|---|---|---|
ESPCN | 0.004311 | 26.6870 | 0.7891 |
EDSR | 1.607570 | 28.1552 | 0.8317 |
FSRCNN | 0.005302 | 26.6088 | 0.7863 |
LapSRN | 0.121229 | 26.7383 | 0.7896 |
Bicubic | 0.000311 | 26.0635 | 0.8754 |
Nearest neighbor | 0.000148 | 23.5628 | 0.8174 |
Lanczos | 0.001012 | 25.9115 | 0.8706 |
此外,官方也给出了不同图片在不同算法和不同比例下超分放大的结果,如下所示:
4倍放大一张768x512大小的图像
方法 | 时间(s)/cpu | SNR | SSIM |
---|---|---|---|
ESPCN | 0.01159 | 26.5471 | 0.88116 |
EDSR | 3.26758 | 29.2404 | 0.92112 |
FSRCNN | 0.01298 | 26.5646 | 0.88064 |
LapSRN | 0.28257 | 26.7330 | 0.88622 |
Bicubic | 0.00031 | 26.0635 | 0.87537 |
Nearest neighbor | 0.00014 | 23.5628 | 0.81741 |
Lanczos | 0.00101 | 25.9115 | 0.87057 |
2倍放大一张256x256大小的图像
Set5: butterfly.png | size: 256x256 | ||
---|---|---|---|
Original | Bicubic interpolation | Nearest neighbor interpolation | Lanczos interpolation |
PSRN / SSIM / Speed (CPU) | 26.6645 / 0.9048 / 0.000201 | 23.6854 / 0.8698 / 0.000075 | 26.9476 / 0.9075 / 0.001039 |
ESPCN | FSRCNN | LapSRN | EDSR |
29.0341 / 0.9354 / 0.004157 | 29.0077 / 0.9345 / 0.006325 | 27.8212 / 0.9230 / 0.037937 | 30.0347 / 0.9453 / 2.077280 |
3倍放大一张1024x644大小的图像
Urban100: img_001.png | size: 1024x644 | ||
---|---|---|---|
Original | Bicubic interpolation | Nearest neighbor interpolation | Lanczos interpolation |
PSRN / SSIM / Speed (CPU) | 27.0474 / 0.8484 / 0.000391 | 26.0842 / 0.8353 / 0.000236 | 27.0704 / 0.8483 / 0.002234 |
LapSRN无三倍放大 | |||
ESPCN | FSRCNN | LapSRN | EDSR |
28.0118 / 0.8588 / 0.030748 | 28.0184 / 0.8597 / 0.094173 | 30.5671 / 0.9019 / 9.517580 |
4倍放大一张250x361大小的图像
Set14: comic.png | size: 250x361 | ||
---|---|---|---|
Original | Bicubic interpolation | Nearest neighbor interpolation | Lanczos interpolation |
PSRN / SSIM / Speed (CPU) | 19.6766 / 0.6413 / 0.000262 | 18.5106 / 0.5879 / 0.000085 | 19.4948 / 0.6317 / 0.001098 |
ESPCN | FSRCNN | LapSRN | EDSR |
20.0417 / 0.6302 / 0.001894 | 20.0885 / 0.6384 / 0.002103 | 20.0676 / 0.6339 / 0.061640 | 20.5233 / 0.6901 / 0.665876 |
4倍放大一张1356x2040大小的图像
Div2K: 0006.png | size: 1356x2040 | |
---|---|---|
Original | Bicubic interpolation | Nearest neighbor interpolation |
PSRN / SSIM / Speed (CPU) | 26.3139 / 0.8033 / 0.001107 | 23.8291 / 0.7340 / 0.000611 |
Lanczos interpolation | LapSRN | |
26.1565 / 0.7962 / 0.004782 | 26.7046 / 0.7987 / 2.274290 |
3.4 超分算法选择总结
OpenCV中的dnn_superres模块提供的四种图像超分放大深度学习模型,在实践中用的最多的就是EDSR模型。其他三类模型和OpenCV自带的resize函数视觉上差别并不大。但是EDSR模型推理速度太慢,2倍放大和4倍放大可以使用ESPCN代替,4倍和8倍放大可以使用LapSRN。但是总体来说还是使用EDSR为好,毕竟超分放大需要高性能运算,还是用高性能显卡运算较为合适。
此外OpenCV的dnn_superres模块不适用于移动端设备或嵌入式设备,因为OpenCV对设备性能有一定要求。所以移动端可以看看ncnn的超分放大实现。具体见:
ncnn用的是srmd超分放大模型,srmd官方代码和ncnn官方实现代码见
事实上srmd超分放大性能效果高于OpenCV提供的EDSR模型,但SRMD需要显卡进行运算,ncnn在移动端使用vulkan实现加速运算,在PC端如果有显卡也通过ncnn调用SRMD模型。
4 参考
4.1 相关论文
Enhanced Deep Residual Networks for Single Image Super-Resolution
Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network
Accelerating the Super-Resolution Convolutional Neural Network
Deep laplacian pyramid networks for fast and accurate super-resolution
4.2 参考代码
Super Resolution using Convolutional Neural Networks
EDSR_Tensorflow
TF-ESPCN
FSRCNN_Tensorflow
TF-LAPSRN
OpenCV-Practical-Exercise
SRMD
srmd-ncnn-vulkan
4.3 参考文档
超分辨率基准测试
Super-resolution benchmarking
【超分辨率】—图像超分辨率(Super-Resolution)技术研究
【超分辨率】—基于深度学习的图像超分辨率最新进展与趋势
PSNR和SSIM
OpenCV_contrib库在windows下编译使用指南
srmd ncnn vulkan 通用图片超分放大工具
[OpenCV实战]44 使用OpenCV进行图像超分放大的更多相关文章
- ACNet:用于图像超分的非对称卷积网络
编辑:Happy 首发:AIWalker Paper:https://arxiv.org/abs/2103.13634 Code:https://github.com/hellloxiaotian/A ...
- HMS Core机器学习服务图像超分能力,基于深度学习提升新闻阅读体验
在移动端阅读资讯时,人们对高分辨率.高质量的图像要求越来越高.但受限于网络流量.存储.图片源等诸多因素,用户无法便捷获得高质量图片.移动端显示设备的高分辨率图片获得问题亟待解决.不久前,HMS Cor ...
- 体验SRCNN和FSRCNN两种图像超分网络应用
摘要:图像超分即超分辨率,将图像从模糊的状态变清晰. 本文分享自华为云社区<图像超分实验:SRCNN/FSRCNN>,作者:zstar. 图像超分即超分辨率,将图像从模糊的状态变清晰.本文 ...
- OpenCV实战:人脸关键点检测(FaceMark)
Summary:利用OpenCV中的LBF算法进行人脸关键点检测(Facial Landmark Detection) Author: Amusi Date: 2018-03-20 ...
- OpenCV计算机视觉学习(8)——图像轮廓处理(轮廓绘制,轮廓检索,轮廓填充,轮廓近似)
如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 1, ...
- OpenCV计算机视觉学习(13)——图像特征点检测(Harris角点检测,sift算法)
如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 前言 ...
- 项目实战:Qt+OpenCV大家来找茬(Qt抓图,穿透应用,识别左右图区别,框选区别,微调位置)
前言 本项目的出现理由只是笔者的一个念头,于是利用专业Qt和Opencv相关的知识开发一个辅助工具,本文章仅用于Qt和Opencv结合的学习. Demo演示效果 运行包下载地 ...
- OpenCV中IplImage图像格式与BYTE图像数据的转换
最近在将Karlsruhe Institute of Technology的Andreas Geiger发表在ACCV2010上的Efficent Large-Scale Stereo Matchin ...
- OpenCV学习笔记:如何扫描图像、利用查找表和计时
目的 我们将探索以下问题的答案: 如何遍历图像中的每一个像素? OpenCV的矩阵值是如何存储的? 如何测试我们所实现算法的性能? 查找表是什么?为什么要用它? 测试用例 这里我们测试的,是一种简单的 ...
- OpenCV 编程简单介绍(矩阵/图像/视频的基本读写操作)
PS. 因为csdn博客文章长度有限制,本文有部分内容被截掉了.在OpenCV中文站点的wiki上有可读性更好.而且是完整的版本号,欢迎浏览. OpenCV Wiki :<OpenCV 编程简单 ...
随机推荐
- 如何去了解Spring
对于你想了解的技术 官方总是一个合适的选择 首先,我们所指的Spring 一般指的是Spring Framework,伴随着的时代的进步,Spring全家桶也逐渐完善起来 Spring 1.Why S ...
- Hbase之权限控制
Hbase之权限控制 -- 只读权限 grant '{userName}','R','{namespaceName:tableName}' -- 写入权限 grant '{userName}','W' ...
- Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2020-02-06'; nested exception is java.lang.IllegalArgumentException]解决
今天做springbook项目前端输入日期传到数据库保存报了一下错误 Whitelabel Error Page This application has no explicit mapping fo ...
- Python全栈工程师之从网页搭建入门到Flask全栈项目实战(3) - 入门Flask微框架
1.安装Flask 方式一:使用pip命令安装 pip install flask 方式二:源码安装 python setup.py install 验证 第一个Flask程序 程序解释 参数__na ...
- 在不受支持的 Mac 上安装 macOS Ventura、Monterey、Big Sur (OpenCore Legacy Patcher)
请访问原文链接:https://sysin.org/blog/install-macos-13-on-unsupported-mac/,查看最新版.原创作品,转载请保留出处. 作者主页:www.sys ...
- JS 可编辑表格的实现
1.实现效果 用户点击语文,数学,英语部分的单元格,可以实现分数的编辑,总分也会随之变化.先看下效果,如图: 2.设计思路 先通过HTML5+CSS3绘制表格,添加input的样式和err提示动画. ...
- v-for中key的作用与原理
一.虚拟DOM中key的作用 key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM,随后Vue会对新虚拟DOM与旧虚拟DOM的差异进行比较. 二.如何选择key 最好使 ...
- 缺省源&一些常用的码头
#include <bits/stdc++.h> #define N 1000010 #define M 2000010 #define pii pair<int,int> # ...
- 解决pip下载速度慢问题
解决pip下载速度慢的问题 痛点:当我们pip 安装第三方库的时候,由于是访问的国外地址,所以会出现下载很慢!干等..... 解决方案: # 1.在C盘目录-->Users-->用户--& ...
- excel公式与快捷操作
将首行的公式,运用到这一整列 1.选中要输入公式的第一个单元格,SHIFT+CTRL+方向键下,在编辑栏中输入公式,按下CTRL+回车: 2.先输入要填充的公式,按下SHIFT+CTRL+方向键下,再 ...