OpenCV实战——实现高效图像扫描循环
- 0. 前言
- 1. 测量代码运行时间
- 2. 计算不同扫描算法的执行时间
- 4. 完整代码及运行结果
- 相关链接
0. 前言
在《像素操作》一节中,我们介绍了扫描图像以处理其像素的不同方法。在本节中,我们将学习比较这些方法的计算效率。编写图像处理函数时,效率通常是一个需要考虑的问题。设计函数时,经常需要检查代码的计算效率,以检测处理中可能减慢程序速度的瓶颈操作。
但是,需要注意的是,除非必要,否则不应以降低程序明确性为代价进行优化。简单的代码相对更容易调试和维护,只有对程序效率至关重要的代码部分才应进行大量优化。
1. 测量代码运行时间
为了测量函数或某一部分代码的执行时间,可以使用 OpenCV
函数 cv::getTickCount()
,此函数可以提供自上次启动计算机以来发生的时钟周期数。由于通常我们希望以秒为单位给出代码部分的执行时间,因此我们需要使用另一种方法 cv::getTickFrequency()
来获取每秒的周期数。总体而言,为了获得给定函数(或某一部分代码)的执行时间,通常使用以下方式:
(1) 获取开始时间点:
const int64 start = cv::getTickCount();
(2) 调用要测量的函数或代码:
colorReduce(image);
(3) 计算执行时间,使用结束时间点减去开始时间点再除以每秒周期数:
double duration = (cv::getTickCount()- start)/cv::getTickFrequency();
在以上代码中,度量了 colorReduce()
函数的执行时间,绝对运行时间会因机器而异。同时,运行时间还取决于用于生成可执行文件的特定编译器。
2. 计算不同扫描算法的执行时间
(1) 首先,我们比较使用指针扫描图像一节中介绍的三种计算颜色减少的函数。可以看到,整数除法公式和按位运算符的执行时间几乎相同,即 20
毫秒。然而,基于模运算符的版本需要 31
毫秒。这表示最快和最慢的程序之间几乎有 50%
的差异,因此,重要的是要花时间确定在图像循环中获取计算结果的最高效方法。当指定需要重新分配的输出图像而不是就地处理时,执行时间变为 22
毫秒,额外时间用于内存分配的开销。
(2) 在循环中,应该避免重复计算可以预先计算的值。例如,以下减色函数的内部循环:
int nc = image.cols * image.channels();
uchar div2 = div>>1;
for (int i=0; i<nc; i++){
*data++ += div2
}
如果将其替换为以下内容:
for (int i=0; i<image.cols*image.channels(); i++){
*data++ += div>>1
}
那么在此循环中,就需要一次又一次地计算一行中的元素总数 image.cols*image.channels()和 div>>1
;此时,运行时间将延长至 40
毫秒,这明显比原始版本慢(原始版本需要 20
毫秒)。需要注意的是,某些编译器可能能够优化这些类型的循环并仍然可以获得高效的代码。
(3) 使用迭代器的减色函数需要的运行时间为 31
毫秒,迭代器的主要目标是简化图像扫描过程并使其不易出错。
(4) 为完整起见,我们还可以实现使用 at()
方法进行像素访问的函数版本:
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
image.at<cv::Vec3b>(j,i)[0] = image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1] = image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2] = image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
}
}
使用此方法,需要大约 43
毫秒的运行时间,相对而言要慢得多,这种方法应该只用于图像像素的随机访问,而非扫描图像时。
(5) 即使处理的元素总数相同,具有较少语句的较短循环通常比在单个语句上执行较长循环更高效。类似地,如果有 N
种不同的计算要应用于一个像素,需要在一个循环中全部应用,而不是编写 N
个连续循环(每个循环完成一个计算)。
(6) 我们还可以进行连续图像测试,在连续图像的情况下使用一个循环,而不是在行和列上使用双循环。对于尺寸较大的图像,这种优化并不重要(仅从 22
毫秒将至 20
毫秒),但总的来说,使用这种策略是一个很好的做法。
(7) 多线程是另一种提高算法效率的方法,尤其是在多核处理器出现之后。OpenMP
和英特尔线程构建块 (Intel Threading Building Blocks
, TBB
) 是两种流行的 API
,用于并发编程以创建和管理线程。此外,C++11
以上版本也提供了对线程的内置支持。
4. 完整代码及运行结果
完整代码 () 示例如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
// 使用指针扫描图像
void colorReduce(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols * image.channels();
for (int j=0; j<nl; j++) {
// 获取输入图像第j行地址
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
data[i] = data[i]/div*div + div/2;
}
}
}
// 带有输入输出的颜色减少函数
void colorReduceIO(const cv::Mat &image, cv::Mat &result, int div=64) {
int nl = image.rows;
int nc = image.cols;
int nchannels = image.channels();
// 为输出图像分配内存
result.create(image.rows, image.cols, image.type());
for (int j=0; j<nl; j++) {
// 获取输入与输出图像第j行地址
const uchar* data_in = image.ptr<uchar>(j);
uchar* data_out = result.ptr<uchar>(j);
for (int i=0; i<nc*nchannels; i++) {
data_out[i] = data_in[i] / div * div + div / 2;
}
}
}
// 使用解引用操作的颜色减少函数
void colorReduce1(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols * image.channels();
uchar div2 = div >> 1;
for (int j=0; j<nl; j++) {
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
*data++ += *data / div * div + div2;
}
}
}
// 使用求模运算符的颜色减少函数
void colorReduce2(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols * image.channels();
uchar div2 = div >> 1;
for (int j=0; j<nl; j++) {
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
int value = *data;
*data++ = value - value % div + div2;
}
}
}
// 使用二进制掩码的颜色减少函数
void colorReduce3(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols * image.channels();
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
uchar div2 = 1<<(n-1); // div = div/2
for (int j=0; j<nl; j++) {
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
*data &= mask; // 掩码处理
*data++ |= div2; // 加div/2
}
}
}
// 使用低阶(直接)指针的颜色减少函数
void colorReduce4(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols * image.channels();
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
int step = image.step; // 图像有效宽度
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
uchar div2 = 1<<(n-1); // div = div/2
// 获取图像缓冲区指针
uchar *data = image.data;
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
*(data+i) &= mask; // 掩码处理
*(data+i) |= div2; // 加div/2
}
data += step;
}
}
// 在循环中每次计算行大小的颜色减少函数
void colorReduce5(cv::Mat image, int div=64) {
int nl = image.rows;
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
for (int j=0; j<nl; j++) {
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<image.cols* image.channels(); i++) {
*data &= mask;
*data++ += div/2;
}
}
}
// 图像为连续图像时的颜色减少函数
void colorReduce6(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols;
if (image.isContinuous()) {
// 无填充像素
nc = nc * nl;
nl = 1; // 1D 阵列
}
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
uchar div2 = 1<<(n-1); // div = div/2
// 对于连续图像,只需执行一次循环
for (int j=0; j<nl; j++){
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++){
*data &= mask;
*data++ += div2;
}
}
}
// 使用reshape函数整形连续图像的颜色减少函数
void colorReduce7(cv::Mat image, int div=64) {
if (image.isContinuous()) {
// 无填充像素
image.reshape(
1, // 新图像通道数
1 // 新图像行数
);
}
int nl = image.rows; // 图像行数
int nc = image.cols * image.channels();
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
uchar div2 = 1<<(n-1); // div = div/2
// 对于连续图像,只需执行一次循环
for (int j=0; j<nl; j++){
uchar* data = image.ptr<uchar>(j);
for (int i=0; i<nc; i++){
*data &= mask;
*data++ += div2;
}
}
}
// 使用Mat_迭代器处理3个通道的颜色减少函数
void colorReduce8(cv::Mat image, int div=64) {
// 获取迭代器
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
uchar div2 = div >> 1;
for (; it!=itend; ++it) {
(*it)[0] = (*it)[0]/div*div + div2;
(*it)[1] = (*it)[1]/div*div + div2;
(*it)[2] = (*it)[2]/div*div + div2;
}
}
// 在Vec3b上使用迭代器的颜色减少函数
void colorReduce9(cv::Mat image, int div=64) {
// 获取迭代器
cv::MatIterator_<cv::Vec3b> it = image.begin<cv::Vec3b>();
cv::MatIterator_<cv::Vec3b> itend = image.end<cv::Vec3b>();
const cv::Vec3b offset(div/2,div/2,div/2);
for (; it!=itend; ++it) {
*it = *it/div*div + offset;
}
}
// 使用带有二进制掩码的迭代器的颜色减少函数
void colorReduce10(cv::Mat image, int div=64) {
// div 必须是2的幂次
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
uchar div2 = 1<<(n-1); // div = div/2
// 获取迭代器
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
for (; it!=itend; ++it) {
(*it)[0] &= mask;
(*it)[0] += div2;
(*it)[1] &= mask;
(*it)[1] += div2;
(*it)[2] &= mask;
(*it)[2] += div2;
}
}
// 使用Mat_迭代器的颜色减少函数
void colorReduce11(cv::Mat image, int div=64) {
// 获取迭代器
cv::Mat_<cv::Vec3b> cimage = image;
cv::Mat_<cv::Vec3b>::iterator it = cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend = cimage.end();
uchar div2 = div>>1;
for (; it!=itend; ++it) {
(*it)[0] = (*it)[0]/div*div + div2;
(*it)[1] = (*it)[1]/div*div + div2;
(*it)[2] = (*it)[2]/div*div + div2;
}
}
// 使用at方法的颜色减少函数
void colorReduce12(cv::Mat image, int div=64) {
int nl = image.rows;
int nc = image.cols;
uchar div2 = div>>1;
for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
image.at<cv::Vec3b>(j,i)[0] = image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1] = image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2] = image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
}
}
}
// 使用Mat重载运算符的颜色减少函数
void colorReduce13(cv::Mat image, int div=64) {
int n = static_cast<int>(log(static_cast<double>(div))/log(2.0)+0.5);
// 用于舍入像素值的掩码
uchar mask = 0xFF<<n; // 例如,对于div=16的情况,mask=0xF0
image = (image&cv::Scalar(mask,mask,mask))+cv::Scalar(div/2,div/2,div/2);
}
// 使用查找表的颜色减少函数
void colorReduce14(cv::Mat image, int div=64) {
cv::Mat lookup(1,256,CV_8U);
for (int i=0; i<256; i++) {
lookup.at<uchar>(i)=i/div*div+div/2;
}
cv::LUT(image, lookup, image);
}
#define NTESTS 15
#define NITERATIONS 10
int main() {
// 读取图像
cv::Mat image = cv::imread("1.png");
// 统计处理图像所用时间
const int64 start = cv::getTickCount();
colorReduce(image, 64);
double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
// 打印执行时间
std::cout << "Duration = " << duration << "secs" << std::endl;
cv::namedWindow("Image");
cv::imshow("Image", image);
cv::waitKey();
// 测试不同函数
int64 t[NTESTS], tinit;
// 初始化计时器
for (int i=0; i<NTESTS; i++) {
t[i] = 0;
}
cv::Mat images[NTESTS];
cv::Mat result;
// 需要进行测试的函数
typedef void(*FunctionPointer)(cv::Mat, int);
FunctionPointer functions[NTESTS] = {colorReduce, colorReduce1, colorReduce2, colorReduce3, colorReduce4,
colorReduce5, colorReduce6, colorReduce7, colorReduce8, colorReduce9, colorReduce10, colorReduce11,
colorReduce12, colorReduce13, colorReduce14
};
// typedef void(*FunctionPointer)(cv::Mat, int);
// FunctionPointer functions[NTESTS] = { colorReduce, colorReduce1, colorReduce2, colorReduce3, colorReduce4,
// colorReduce5, colorReduce6, colorReduce7, colorReduce8, colorReduce9,
// colorReduce10, colorReduce11, colorReduce12, colorReduce13, colorReduce14};
// 重复测试数次
int n = NITERATIONS;
for (int k=0; k<n; k++) {
std::cout << k << " of " << n << std::endl;
for (int c=0; c<NTESTS; c++) {
images[c] = cv::imread("1.png");
// 设定计时器并调用函数
tinit = cv::getTickCount();
functions[c](images[c], 64);
t[c] += cv::getTickCount() - tinit;
std::cout << ".";
}
std::cout << std::endl;
}
// short description of each function
std::string descriptions[NTESTS] = {
"original version:",
"with dereference operator:",
"using modulo operator:",
"using a binary mask:",
"direct ptr arithmetic:",
"row size recomputation:",
"continuous image:",
"reshape continuous image:",
"with iterators:",
"Vec3b iterators:",
"iterators and mask:",
"iterators from Mat_:",
"at method:",
"overloaded operators:",
"look-up table:",
};
for (int i=0; i<NTESTS; i++) {
cv::namedWindow(descriptions[i]);
cv::imshow(descriptions[i], images[i]);
}
// 打印平均执行时间
std::cout << std::endl << "-----------------------------" << std::endl;
for (int i=0; i<NTESTS; i++) {
std::cout << i << ". " << descriptions[i] << 1000.*t[i] / cv::getTickFrequency() / n << "ms" << std::endl;
}
cv::waitKey();
return 0;
}
编译并执行以上代码,可以得到以下结果图像:
相关链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(4)——像素操作