如何使用OpenCV扫描图像,查找表格和时间测量代码详解

本篇文章对OpenCV文档中如何使用OpenCV扫描图像,查找表格和时间测量给出的源代码进行详细的注释。本篇文章将会涉及到以下内容。

  • 头文件的作用
  • 颜色空间缩减原理
  • C操作符[ ](指针)、迭代器、即时项目地址计算三种方法的使用及代码注解
  • Visual Studio 2017 命令参数配置方法等

想自己着手写代码,一定得知道各个头文件的用处。在不知道该用什么库的时候,可以直接用

#include <opencv2\opencv.hpp>

缺点是引用OpenCV库之外的其他代码或者不同opencv版本之间的引用可能会报错。这个时候探究每个头文件的作用就是必要的了。下面从头文件开始,为源代码做出注释。

#include <opencv2/core.hpp>  //核心模块,定义了基本的数据结构和算术函数
#include <opencv2/core/utility.hpp>  //包含getTickFrequency()等函数
#include "opencv2/imgcodecs.hpp"  //图片编解码、加载保存之类的函数
#include <opencv2/highgui.hpp>  //视频捕捉、图像和视频的编码解码、图形交互界面的接口
#include <iostream>  //C++中用于数据的流式输入与输出的头文件
#include <sstream>  //使用stringsteam类型需要用到的头文件

using namespace std;
using namespace cv;

这里使用namespace命名空间,省去了之后代码中反复使用std::或cv::的麻烦。


static void help()  //静态函数,这个函数输出函数信息及命令行参数信息
{
    cout
        << "\n--------------------------------------------------------------------------" << endl
        << "This program shows how to scan image objects in OpenCV (cv::Mat). As use case"
        << " we take an input image and divide the native color palette (255) with the " << endl
        << "input. Shows C operator[] method, iterators and at function for on-the-fly item address calculation." << endl
        << "Usage:" << endl
        << "./how_to_scan_images <imageNameToUse> <divideWith> [G]" << endl
        << "if you add a G parameter the image is processed in gray scale" << endl
        << "--------------------------------------------------------------------------" << endl
        << endl;  //ednl结束本行,相当于c语言中的"\n"。注意endl最后一个字符是字母L,不是数字1。
}

下面是C操作符[ ](指针)、迭代器、即时项目地址计算三种方法函数声明,其中的&读作引用,相当于给函数或变量名起了第二个名字,引用初始化某个变量后,可以使用该引用名称或原变量名称指向该变量,和指针有一定的区别,具体请参考C++引用


Mat& ScanImageAndReduceC(Mat& I, const uchar* table);
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* table);
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar * table);

程序主函数


int main(int argc, char* argv[])  // argc是命令行总的参数个数,argv[]是argc个参数,其中第0个参数是程序的全名,后面跟的用户输入的参数
{
    help();
    if (argc < 3)  //判断输入参数是否合法,如果个数小于3个,则输出参数不够
    {
        cout << "Not enough parameters" << endl;
        return -1;
    }

    Mat I, J;  //I为输入矩阵,J为输出矩阵
    if (argc == 4 && !strcmp(argv[3], "G"))  
        I = imread(argv[1], IMREAD_GRAYSCALE);  //如果输入了参数G,图像将以灰度级进行处理
    else
        I = imread(argv[1], IMREAD_COLOR);  //否则,图像将以彩色级进行处理

    if (I.empty())  //判断矩阵I是否载入成功
    {
        cout << "The image" << argv[1] << " could not be loaded." << endl;
        return -1;
    }

    //! [dividewith]
    int divideWith = 0; // 将我们输入的字符串转换为数字 -C++风格
    stringstream s;   //这里用到了stringstream
    s << argv[2];  //将第三个参数复制给字符串s
    s >> divideWith;  //将字符串转化为数字
    if (!s || !divideWith)  //判断输入是否合法,是否已经输入了字符串或输入的字符串是否为零
    {
        cout << "Invalid number entered for dividing. " << endl;
        return -1;
    }

颜色空间缩减原理: 假设dividewith = 10。定义
0~9范围的像素值为0,
10~19范围的像素值为10,
20~29范围的像素值为20,
以此类推,则原单个通道的0~255的256个值,变为了0~25的26个值。

继续主函数,下面是C风格运算符[](指针)、迭代器、即时项目地址计算和LUT四种方法的比较。

    uchar table[256];  //建立一张颜色空间缩减的表格,其实就是数组,方便后边查找赋值
    for (int i = 0; i < 256; ++i)
        table[i] = (uchar)(divideWith * (i / divideWith));
    //! [dividewith]

    const int times = 100;  //定义常量,即值不会改变的量。常量的值为100,目的是计算执行100次的平均时间
    double t;  //平均执行时间

继续主函数, 第一种方法:C风格运算符[](指针)

    t = (double)getTickCount();


    for (int i = 0; i < times; ++i)
    {
        cv::Mat clone_i = I.clone();
        J = ScanImageAndReduceC(clone_i, table);
    }

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();  //时间单位为毫秒
    t /= times;

    cout << "Time of reducing with the C operator [] (averaged for "
        << times << " runs): " << t << " milliseconds." << endl; //输出平均运行时间

继续主函数,第二种方法:迭代器

    t = (double)getTickCount();

    for (int i = 0; i < times; ++i)
    {
        cv::Mat clone_i = I.clone();
        J = ScanImageAndReduceIterator(clone_i, table);
    }

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;

    cout << "Time of reducing with the iterator (averaged for "
        << times << " runs): " << t << " milliseconds." << endl;

继续主函数,第三种方法:即时项目地址计算

    t = (double)getTickCount();

    for (int i = 0; i < times; ++i)
    {
        cv::Mat clone_i = I.clone();
        J = ScanImageAndReduceRandomAccess(clone_i, table);
    }

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;

    cout << "Time of reducing with the on-the-fly address generation - at function (averaged for "
        << times << " runs): " << t << " milliseconds." << endl;

继续主函数,第四种方法LUT

    //! [table-init]
    Mat lookUpTable(1, 256, CV_8U);  //Mat的前两个参数代表矩阵的大小,即1行256列,地址为0~256。CV_8U表示使用8位无符号整数型每一个像素由单个像素组成的单通道
    uchar* p = lookUpTable.ptr();  //获取lookUpTable表的指针
    for (int i = 0; i < 256; ++i)
        p[i] = table[i];
    //! [table-init]

    t = (double)getTickCount();

    for (int i = 0; i < times; ++i)
        //! [table-use]
        LUT(I, lookUpTable, J); //第一个参数,原始图像地址;第二个参数,查找表的地址;第三个参数输出图像地址
    //! [table-use]

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;

    cout << "Time of reducing with the LUT function (averaged for "
        << times << " runs): " << t << " milliseconds." << endl;
    return 0;
}

主函数终于看完了,喘口气,接着看剩下的三个函数。第一个函数,C操作符[ ](指针)方法

//! [scan-c] C操作符[ ](指针)
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);  //CV_Assert()作用:CV_Assert()若括号中的表达式值为false,则返回一个错误信息。

    int channels = I.channels();

    int nRows = I.rows;
    int nCols = I.cols * channels;  //定义临时变量

     //判断矩阵是否是连续的,如果是连续的,就将这个矩阵当作 1 x nCols*nRows ,即 1 x I.rows*I.cols*I.channels的矩阵看待。
    if (I.isContinuous()) 
    {
        nCols *= nRows;
        nRows = 1;
    }

    int i, j;
    uchar* p;
    //查找表进行替换
    for (i = 0; i < nRows; ++i)
    {
        p = I.ptr<uchar>(i);
        for (j = 0; j < nCols; ++j)
        {
            p[j] = table[p[j]];
        }
    }
    return I;
}
//! [scan-c]

参考OpenCV中矩阵类详解之一:Mat ,可以对OpenCV中的Mat有更深入细致的了解。这里只挑出本函数用到各项进行介绍

depth:深度,即每一个像素的位数(bits),在opencv的Mat.depth()中得到的是一个 0 – 6 的数字,分别代表不同的位数:enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 }; 可见 0和1都代表8位, 2和3都代表16位,4和5代表32位,6代表64位;

channels:通道,矩阵中的每一个矩阵元素拥有的值的个数。比如 3 * 4 矩阵中一共 12 个元素,如果每个元素有三个值,那么就说这个矩阵是 3 通道的,即 channels = 3。常见的是一张彩色图片有蓝、绿、红(BGR)三个通道。

rows: Mat矩阵的行数。

cols: Mat矩阵的列数。

isContinuous: 矩阵是否连续。矩阵连续是指如果在每一行的结尾无间隙连续存储矩阵的元素,该方法返回 true。否则,它将返回 false。很明显,1 x 1 或 1xN 矩阵始终是连续的。使用 Mat::create() 创建矩阵是始终是连续的。

第二个函数,迭代器方法。

//! [scan-iterator]
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);

    const int channels = I.channels();
    switch (channels)
    {
    case 1:  //如果是单通道
    {
        MatIterator_<uchar> it, end;  //MatIterator_是Mat的迭代器
        //在下面的循环中,使用了Mat的begin和end函数,使迭代器分别指向Mat I数据部分的开头和结尾。
        for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)  //遍历象素,进行替换
            *it = table[*it];
        break;
    }

    //图像存储没有双通道的,这里自然就没有 case 2 了

    case 3:  //如果是三通道
    {
        MatIterator_<Vec3b> it, end;
        for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
        {
            (*it)[0] = table[(*it)[0]];
            (*it)[1] = table[(*it)[1]];
            (*it)[2] = table[(*it)[2]];
        }
    }
    }

    return I;
}
//! [scan-iterator]

不熟悉面向对象编程中迭代器的概念的读者,可以阅读与STL中迭代器相关的入门书籍和文字。用关键字“STL 迭代器”进行搜索可以找到各种相关的博文和资料

第三个函数,即时项目地址计算方法

//! [scan-random]
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);

    const int channels = I.channels();
    switch (channels)
    {
    case 1:
    {
        for (int i = 0; i < I.rows; ++i)
            for (int j = 0; j < I.cols; ++j)
                I.at<uchar>(i, j) = table[I.at<uchar>(i, j)]; //at返回对指定数组元素的引用。
        break;
    }
    case 3:
    {
        Mat_<Vec3b> _I = I;
        //大多数情况下Mat就足够用了,但如果使用大量元素访问操作并且在编译时知道矩阵类型,则Mat_会更方便。
        //比如'Mat :: at(int y,int x)'和'Mat _ :: operator()(int y,int x)'在相同时条件下,后者所花费的时间更短
        for (int i = 0; i < I.rows; ++i)
            for (int j = 0; j < I.cols; ++j)
            {
                _I(i, j)[0] = table[_I(i, j)[0]];
                _I(i, j)[1] = table[_I(i, j)[1]];
                _I(i, j)[2] = table[_I(i, j)[2]];
            }
        I = _I;
        break;
    }
    }

    return I;
}
//! [scan-random]

至此,整篇代码就解读完毕了。让我们运行一下看看结果吧。
这里写图片描述

这段程序主函数中没有输入函数,所以不能直接在运行后输入所需参数。这里提供一种解决方案:Visual Studio 2017运行此段程序参数配置过程:视图–>其他窗口–>属性管理器–>在右侧出现的属性管理器窗口双击解决方案(或右键解决方案)–>调试–>命令参数。在这里添加
< 图片的路径 > < dividewith的值 > < 是否以灰色图像形式载入 >

  • 三个参数以空格隔开,所以图片的路径中不能含有空格。
  • 不需要输入尖括号<>。
  • 以上添加的三个参数分别是help()函数中提到的主函数的第二三四个参数,第一个参数不需要添加,vs2017会默认为当前工程的名字。如果希望将图片以灰度图形式载入,则添加G,否则不填

如果希望看到图片效果或保存图片,可以自己添加imshow(),imwrite()函数。具体参考这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值