1. 图画的特点

1.1 Mat 的首要特点

在前文中,咱们大致了解了 Mat 的根本结构以及它的创建与赋值。接下来咱们经过一个比如,来看看 Mat 所包含的常用特点。

先创建一个 3*4 的四通道的矩阵,并打印出其相关的特点,稍后会详细解释每个特点的意义。

Mat srcImage(3, 4, CV_16UC4, Scalar_<uchar>(1, 2, 3, 4));
cout << srcImage << endl;
cout << "dims:" << srcImage.dims << endl;
cout << "rows:" << srcImage.rows << endl;
cout << "cols:" << srcImage.cols << endl;
cout << "channels:" << srcImage.channels() << endl;
cout << "type:" << srcImage.type() << endl;
cout << "depth:" << srcImage.depth() << endl;
cout << "elemSize:" << srcImage.elemSize() << endl;
cout << "elemSize1:" << srcImage.elemSize1() << endl;
cout << "step:" << srcImage.step << endl;
cout << "step[0]:" << srcImage.step[0] << endl;
cout << "step[1]:" << srcImage.step[1] << endl;
cout << "step1[0]:" << srcImage.step1(0) << endl;
cout << "step1[1]:" << srcImage.step1(1) << endl;

输出成果:

[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4;
 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4;
 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
dims:2
rows:3
cols:4
channels:4
type:26
depth:2
elemSize:8
elemSize1:2
step:32
step[0]:32
step[1]:8
step1[0]:16
step1[1]:4

在上述比如中咱们打印了 Mat 的很多特点,它们首要包含:

  • rows: 表明图画的高度。
  • cols:表明图画的宽度。
  • dims:表明矩阵的维度
  • data:表明 Mat 目标中的指针(uchar 类型的指针),指向内存中寄存矩阵数据的一块内存 (uchar* data)。
  • channels:表明通道数量;例如常见的 RGB、HSV 五颜六色图画,则 channels=3;若为灰度图,则 channels=1。
  • depth:表明图画的深度,它用来衡量每一个像素中每一个通道的精度,它本身与通道数无关,它的数值越大表明精度越高。
数据类型 depth 的值 数据类型 取值规模 对应 C++ 的类型
CV_8U 0 8 位无符号类型 0—255 uchar, unsigned char
CV_8S 1 8 位有符号类型 -128—127 char
CV_16U 2 16 位无符号类型 0—65535 ushort, unsigned short, unsigned short int
CV_16S 3 16 位有符号类型 -32768—32767 short, short int
CV_32S 4 32 位整数数据类型 -2147483648—2147483647 int, long
CV_32F 5 32 位浮点数类型 (1.18e-38……3.40e38) float
CV_64F 6 32 位双精度类型 (2.23e-308……1.79e308) double
  • type:表明矩阵的数据类型,它包含矩阵中元素的类型以及通道数信息。
数据类型 1 2 3 4
CV_8U CV_8UC1 CV_8UC2 CV_8UC3 CV_8UC4
CV_8S CV_8SC1 CV_8SC2 CV_8SC3 CV_8SC4
CV_16U CV_16UC1 CV_16UC2 CV_16UC3 CV_16UC4
CV_16S CV_16SC1 CV_16SC2 CV_16SC3 CV_16SC4
CV_32S CV_32SC1 CV_32SC2 CV_32SC3 CV_32SC4
CV_32F CV_32FC1 CV_32FC2 CV_32FC3 CV_32FC4
CV_64F CV_64FC1 CV_64FC2 CV_64FC3 CV_64FC4
  • elemSize:表明矩阵中每一个元素的数据大小,它与通道数相关,单位是字节。 举几个比如: 假如 Mat 中的数据类型是 CV_8UC1 或 CV_8SC1,那么 elemSize=1(1 * 8 / 8 = 1 bytes); 假如 Mat 中的数据类型是 CV_8UC3 或 CV_8SC3,那么 elemSize=3(3 * 8 / 8 = 3 bytes); 假如 Mat 中的数据类型是 CV_16UC3 或 CV_16SC3,那么 elemSize=6(3 * 16 / 8 = 6 bytes); 假如 Mat 中的数据类型是 CV_32SC3 或 CV_32FC3,那么 elemSize=12(3 * 32 / 8 = 12 bytes);

  • elemSize1:表明矩阵中每一个元素单个通道的数据大小,单位是字节。满意: elemSize1=elemSize/channelselemSize1=elemSize/channels

  • step: 字面意思是“步长”,实践上它描绘了矩阵的形状。 step[] 为一个数组,矩阵有几维,step[] 数组就有几个元素。以一个三维矩阵为例,step[0] 表明一个平面的字节总数,step[1] 表明一行元素的字节总数,step[2] 表明每一个元素的字节总数。

在 OpenCV 的官方文档中,关于解释 step 时曾说到矩阵数据元素(i0,i1,…im−1)({i_{0}, i_{1}, … i_{m-1}})的地址addr(Mi0,i1,…im−1)=M.data+M.step[0]∗i0+M.step[1]∗i1+…+M.step[M.dims−1]∗iMdims−1addr(M_{i_{0}, i_{1}, … i_{m-1}}) = M.data + M.step[0] * i_{0} + M.step[1] * i_{1} + … + M.step[M.dims – 1] * i_{M_{dims-1}}

关于咱们常用的二维数组,上述公式可化简为: addr(Mi,j)=M.data+M.step[0]∗i+M.step[1]∗jaddr(M_{i,j}) = M.data + M.step[0] * i + M.step[1] * j

这儿的 step[0] 表明一行元素的字节总数,step[1] 表明每一个元素的字节总数。

OpenCV 笔记(2):图画的特点以及像素相关的操作

  • step1: step1 也是一个数组。step1 不再以字节为单位,而是以 elemSize1 为单位,满意: step1[i]=step[i]/elemSize1step1[i]=step[i]/elemSize1

2. 图画的像素操作

2.1 像素的类型

咱们最常用的图画是二维数组,灰度图画(CV_8UC1)会寄存 C++ 的 uchar 类型,RGB 五颜六色图画一般会寄存 Vec3b 类型。

其间,单通道数据寄存格局:

OpenCV 笔记(2):图画的特点以及像素相关的操作

三通道数据寄存格局:

OpenCV 笔记(2):图画的特点以及像素相关的操作

关于五颜六色图画而言,在 OpenCV 中通道的次序是 B、G、R,这跟咱们通常所说的 RGB 三原色正好相反。

当然,灰度图画也不必定都是 CV_8UC1 类型,也或许是 CV_16SC1、CV_32FC1 等,它们会寄存 C++ 的 short、float 等根本类型。相似地,五颜六色图画也或许是 CV_16SC3、CV_32FC3 等,那它们是怎样寄存的呢?

OpenCV 界说了一系列的 Vec 类,它是一个一维的向量,代表像素的类型

typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;

其间 b、s、w、i、f、d 别离表明如下的意义:

数据类型
b unsigned char
s short int
w unsigned short
i int
f float
d double

Vec 类又被称为固定向量类,在编译时就知道向量的大小。相似 Vec 这样的类还有:Matx、Point、Size、Rect

咱们用一张表,总结一下矩阵中的数据类型和像素的类型的对应关系:

数据类型 C1 C2 C3 C4 C6
CV_8U uchar Vec2b Vec3b Vec4b
CV_8S char Vec<char, 2> Vec<char, 3> Vec<char, 4>
CV_16U ushort Vec2w Vec3w Vec4w
CV_16S short Vec2s Vec3s Vec4s
CV_32S int Vec2i Vec3i Vec4i
CV_32F float Vec2f Vec3f Vec4f Vec6f
CV_64F double Vec2d Vec3d Vec4d Vec6d

根据上述表格咱们能够回答方才的问题,CV_16SC3 类型的图画寄存的是 Vec3s 类型,CV_32FC3 类型的图画寄存的是 Vec3f 类型。

2.2 像素点的读取

Mat 的 at() 函数实现了对矩阵中的某个像素的读写操作

下面的代码展现了 at() 函数对灰度图画像素的读写:

Scalar value = grayImage.at<uchar>(y, x);
Scalar.at<uchar>(y, x) = 128;

三通道五颜六色的图画的读取:

Vec3b value = image.at<Vec3b>(y, x);
uchar blue = value.val[0];
uchar green = value.val[1];
uchar red = value.val[2];

三通道五颜六色图画的赋值:

image.at<Vec3b>(y,x)[0]=128;
image.at<Vec3b>(y,x)[1]=128;
image.at<Vec3b>(y,x)[2]=128;

下面的比如结合像素的类型,展现了将加载的图画转换成灰度图画,以及对灰度图画进行取反的操作。

Mat srcImage = imread("/Users/tony/beautiful.jpg");
if (srcImage.empty())
{
    cout << "could not load image ..." << endl;
    return -1;
}
imshow("src", srcImage);
Mat grayImage;
cvtColor(srcImage, grayImage, COLOR_BGR2GRAY); // 灰度处理
imshow("gray",grayImage);
int height = grayImage.rows;
int width  = grayImage.cols;
for (int row=0; row<height; row++)
{
    for (int col=0; col<width; col++)
    {
        int gray = grayImage.at<uchar>(row, col);
        grayImage.at<uchar>(row, col) = 255- gray;
    }
}
imshow("invert", grayImage);

OpenCV 笔记(2):图画的特点以及像素相关的操作

简单提一下,上述比如中 cvtColor() 函数的作用是将图画从一个色彩空间转换到另一个色彩空间。例如,能够将图画从 BGR 色彩空间转换成灰度色彩空间,或许从 BGR 色彩空间转换成 HSV 色彩空间等等。

2.3 图画的遍历

2.3.1 根据数组遍历

前面 2.2 介绍过 at() 函数能够对某个像素进行读写操作,并用比如展现了对单通道进行遍历。

关于三通道的五颜六色图画能够这样遍历。

for(int i=0;i<srcImage.rows;i++){
    for(int j=0;j<srcImage.cols;j++){
        srcImage.at<Vec3b>(i,j)[0]=...  //B通道
        srcImage.at<Vec3b>(i,j)[1]=...  //G通道
        srcImage.at<Vec3b>(i,j)[2]=...  //R通道
    }
}

2.3.2 根据指针遍历

Mat 类供给了更高效的 ptr() 函数,它能够得到图画恣意行首地址

下面的代码,它返回第 i+1 行的首地址,也就是指向第 i+1 行第一个元素的指针。

uchar* data = srcImage.ptr<uchar>(i);

at() 函数跟 ptr() 函数在运用上有必定的区别:

at<类型>(i,j) ptr<类型>(i)

当然,运用 ptr() 函数访问某个像素也是能够的,选用如下的方法:

mat.ptr<type>(row)[col]

它返回的是 <> 中的模板类型指针,指向的是第 row+1 行 col+1 列的元素。

关于单通道图画的遍历:

for(int i=0;i<srcImage.rows;i++){
    uchar* data=srcImage.ptr<uchar>(i);
    for(int j=0;j<srcImage.cols;j++){
        data[j]=...
    }
}

关于三通道图画的遍历:

for(int i=0;i<srcImage.rows;i++){
    Vec3b* data=srcImage.ptr<Vec3b>(i);
    for(int j=0;j<srcImage.cols;j++){
        data[j][0]=...  //B通道
        data[j][1]=...  //G通道
        data[j][2]=...  //R通道
    }
}

2.3.3 根据迭代器遍历

C++ STL 对每个调集类都界说了对应的迭代器类,OpenCV 也供给了 cv::Mat 的迭代器类,并且与 C++ STL 中的标准迭代器兼容。

关于单通道图画的遍历:

Mat_<uchar>::iterator begin = srcImage.begin<uchar>();
Mat_<uchar>::iterator end = srcImage.end<uchar>();
for (auto it = begin; it != end; it++)
{
    *it = ...
}

迭代器 Mat_ 是 Mat 的模版子类,它重载了 operator() 让咱们能够更便利的取图画上的点。相似的迭代器还有 Matlterator_。

关于三通道图画的遍历:

Mat_<cv::Vec3b>::iterator begin = srcImage.begin<cv::Vec3b>();
Mat_<cv::Vec3b>::iterator end = srcImage.end<cv::Vec3b>();
for (auto it = begin; it != end; it++)
{
    (*it)[0] = ... //B通道
    (*it)[1] = ... //G通道
    (*it)[2] = ... //R通道
} 

运用迭代器遍历图画会快捷一些,但是功率没有运用指针的功率高。

2.3.4 根据 LUT 遍历

LUT (LOOK -UP-TABLE) 意为查找表。

在数据结构中,查找表是由同一类型的 数据元素 构成的调集,它是一种以查找为“中心”,同时包含其他运算的非常灵敏的数据结构。

在图画处理中,常常会经过事前建立一张查找表对图画进行映射。

例如,将灰度图由某个区间映射到另一个区间,或许将单通道映射到三通道。它们都是以像素灰度值作为索引,以灰度值映射后的数值作为表中的内容,经过索引号与映射后的输出值建立联络。

一般灰度图画会有 0-255 个灰度值,有时咱们不需要这么准确的灰度级,例如是非图画。下面咱们来展现怎么建立一个 LUT,将 64 到 196 之间的灰度值变成 0,其余变成 1。

Mat lut(1, 256, CV_8U);
for (int i = 0; i < 256; i++)
{
    if (i > 64 and i < 196)
    {
        lut.at<uchar>(i) = 0;
    }
    else
    {
        lut.at<uchar>(i) = i;
    }
}

从上述代码能够看出,经过改变图画中像素的灰度值,LUT 能够降低灰度级进步运算速度。

LUT 只适用于 CV_8U 类型的图画。

当然,查找表并不必定都是单通道的。

  • 假如输入图画为单通道,那么查找表为单通道
  • 假如输入图画为三通道,那么查找表能够为单通道或许三通道

运用 LUT 进行遍历,选用的是色彩空间减缩的方法:把 unsigned char 类型的值除以一个 int 类型的值,得到仍然是一个 char 类型的数值。

咱们选用如下的公式:Inew=(Iold/Q)∗QInew=(Iold/Q)*Q

其间,Q 表明量化等级,当 Q= 10 时则灰度值 1-10 用灰度值 1 表明,灰度值 11-20 用灰度值 11 表明,以此类推。256 个灰度值的灰度图画能够用 26 个数值表明,那么五颜六色的图画就能够用 26 * 26 * 26 个数值表明,比原先小了很多。

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
#define QUAN_VAL1          10
#define QUAN_VAL2          20
#define QUAN_VAL3          100
void createLookupTable(Mat& table, uchar quanVal)
{
    table.create(1,256,CV_8UC1);
    uchar *p = table.data;
    for(int i = 0; i < 256; ++i)
    {
        p[i] = quanVal*(i/quanVal); // 色彩减缩运算
    }
}
int main()
{
    Mat srcImage = imread("/Users/tony/beautiful.jpg");
    if (srcImage.empty())
    {
        cout << "could not load image ..." << endl;
        return -1;
    }
    imshow("src", srcImage); // 原图
    Mat table,dst1,dst2,dst3;
    createLookupTable(table, QUAN_VAL1);
    LUT(srcImage, table, dst1);
    createLookupTable(table, QUAN_VAL2);
    LUT(srcImage, table, dst2);
    createLookupTable(table, QUAN_VAL3);
    LUT(srcImage, table, dst3);
    imshow("dst1", dst1); // Q=10
    imshow("dst2", dst2); // Q=20
    imshow("dst3", dst3); // Q=100
    waitKey(0);
    return 0;
}

OpenCV 笔记(2):图画的特点以及像素相关的操作

上述比如在创建查找表时,遍历了矩阵的每一个像素以及运用色彩空间减缩的运算公式。并且别离展现了原图、Q=10、Q=20、Q=100 的图片。能够看到当 Q = 100 时,图画压缩得比较凶猛丢掉了很多信息。

3. 图画像素值的计算

3.1 均值与标准差

均值和标准差是计算学的概念。

均值的公式:=∑i=1NxiN均值的公式:mu = frac{sum_{i = 1}^N x_i}{N} 标准差公式:=2=∑i=1N(xi−)2N标准差公式:delta = sqrt{delta^2}=sqrt{frac{sum_{i = 1}^N (x_i-mu)^2}{N}}

在图画处理中,它们能协助咱们了解图画通道中像素值的分布状况。均值表明图画全体的亮暗程度,图画的均值越大则表明图画越亮。标准差表明图画中明暗变化的比照程度,标准差越大表明图画中明暗变化越显着。

在图画分析的时候,咱们经过图画像素值的计算,能够对图画的有用信息作出判别。当标准差很小时,图画所携带的有用信息会很少,便于咱们判别这是否是咱们所需要的图画。说一个题外话,曾经我看到过一段很震动的代码,某搭档写的判别传送带上手机是否亮屏。其时的代码或许是为了偷懒,只经过判别图画的均值,当均值超过某个阈值时就认为手机是亮屏的。后来我接手后,当即做了大量的修改。

下面举个比如,经过 meanStdDev() 函数获取图画的均值和标准差,以及每个通道的均值和标准差。

Mat srcImage = imread("/Users/tony/beautiful.jpg");
if (srcImage.empty())
{
    cout << "could not load image ..." << endl;
    return -1;
}
imshow("src", srcImage);
Mat mean, stddev;
meanStdDev(srcImage, mean, stddev);
std::cout << "mean:" << std::endl << mean << std::endl;
std::cout << "stddev:" << std::endl<< stddev << std::endl;
printf("blue channel mean:%.2f, stddev: %.2f n", mean.at<double>(0, 0), stddev.at<double>(0, 0));
printf("green channel mean:%.2f, stddev: %.2f n", mean.at<double>(1, 0), stddev.at<double>(1, 0));
printf("red channel mean:%.2f, stddev: %.2f n", mean.at<double>(2, 0), stddev.at<double>(2, 0));

输出成果:

mean:
[91.28189117330051;
 104.7030620995939;
 118.9715339648672]
stddev:
[77.24017058254671;
 79.5424883584348;
 83.89088339080149]
blue channel mean:91.28, stddev: 77.24 
green channel mean:104.70, stddev: 79.54 
red channel mean:118.97, stddev: 83.89 

4. 总结

本文经过一个简单的比如,介绍了 Mat 常常运用的特点和方法。后续还介绍了像素的类型和多种图画遍历的方法、像素值的计算。

在几种图画遍历方法中,除了 LUT 遍历外,其他的几种方法它们的功率从高到低依次为:指针 > 迭代器 > 数组。在实践生产环境中,咱们常常会用指针遍历的方法

本文介绍的内容是对前面一篇文章内容的弥补,它们都是 OpenCV 最基础的内容,接下来的文章会常常运用这些内容。本文还引申出了 LUT 以及图画像素值的计算, 特别是均值和标准差它们在图画预处理中常常用到。