前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >降龙算法1:图像的基本数据格式(8K字)

降龙算法1:图像的基本数据格式(8K字)

作者头像
周旋
发布2023-01-30 15:29:00
4990
发布2023-01-30 15:29:00
举报
文章被收录于专栏:行走的机械人行走的机械人

开始更新降龙算法系列了。因为停更近半年了,所以先啰嗦几句话交代一下这个系列的前因后果:

更新这个系列的想法自我大三上学期的时候就开始诞生了,当时也确实产出了一些结果:便是手撕算法系列,共12篇,包含文章和代码(文章可以在公众号查看,代码后台发送【手撕算法】即可获取)。

手撕算法系列已经停更很久了,但却为我公众号带来了很多粉丝,也为我后续大三下的秋招找到满意的工作提供了极大的帮助,并且最重要的,得到了很多小伙伴的认可。

但总归当时大三技术水平还非常青涩,而今公众号停更了半年,本人也已在机器视觉行业工作四个月,技术得到了一些微末的长进,因此终于可以更新降龙算法系列,完成大三时种下的种子与梦想了。

为了这个系列我也做了充分的准备,首先为了算法的演示效果,我为该系列配备了一个基于QT写的通用图像处理算法展示GUI框架:降龙GUI。目前样子长这样:

关于这个图像处理算法演示框架GUI我们后面再说。其余的废话也不多说了,为了系列的完整性,我们先从较为简单但却非常重要,且有很多细节值得学习的图像基本数据格式开始。

1、图像的采集

在日常使用中,图像的获取一般都是本地读取,例如opencv读取一张本地图像:

代码语言:javascript
复制
Mat srcImage = imread("./1.jpg");

但在机器视觉行业,图像更多的是来自相机。每个相机都有一个感光芯片,该芯片由一个个密集排列的感光元件组成,在固定的曝光时间内,该感光元件所接收的能量将会经由转换电路转换得到一个采样结果,而像素值则是该采样结果离散化的表现。

一个黑白相机返回一张单通道图像,该图像便是感光芯片采样的结果;若想得到多通道图像(如RGB三通道图像),最简单的方法就是由三个感光芯片感应经由不同滤光片过滤的光束,得到对应通道的采样结果,再经转换电路处理后返回一张三通道彩色图像即可。多通道图像虽然应该是三维空间结构,但内存上的排布是二维的,也就是每一个像素点由多个通道的数值并排来进行表示。

2、图像数据结构

图像可以被简单的看作是一个二维数组,最常用的图像数据结构就是opencv的Mat类型了。我们来创建一个Mat对象并打印其信息:

代码语言:javascript
复制
Mat srcImage;
cout << "srcImage.rows = " << srcImage.rows << endl;
cout << "srcImage.cols = " << srcImage.cols << endl;
cout << "srcImage.dims = " << srcImage.dims << endl;
cout << "srcImage.step = " << srcImage.step << endl;
cout << "srcImage.depth() = " << srcImage.depth() << endl;
cout << "srcImage.type() = " << srcImage.type() << endl;
cout << "srcImage.channels() = " << srcImage.channels() << endl;
cout << "srcImage.elemSize() = " << srcImage.elemSize() << endl;
cout << "srcImage.total() = " << srcImage.total() << endl;
cout << "srcImage.size() = " << srcImage.size() << endl;

首先声明一个Mat类对象srcImage,C++会自动调用其默认构造函数进行构造,对该图像的结构信息进行打印结果如下:

代码语言:javascript
复制
srcImage.rows = 0
srcImage.cols = 0
srcImage.dims = 0
srcImage.step = 0
srcImage.depth() = 0
srcImage.type() = 0
srcImage.channels() = 1
srcImage.elemSize() = 0
srcImage.total() = 0
srcImage.size() = [0 x 0]
按任意键关闭此窗口. . .

可以看到一个默认对象的长、宽等信息都为默认值0;

下面我们读取一张本地图像并如上打印其信息:

代码语言:javascript
复制
Mat srcImage = imread("./1.jpg");
assert(!srcImage.empty());
imshow("srcImage", srcImage);

打印结果如下:

代码语言:javascript
复制
srcImage.rows = 625
srcImage.cols = 1000
srcImage.dims = 2
srcImage.step = 3000
srcImage.depth() = 0
srcImage.type() = 16
srcImage.channels() = 3
srcImage.elemSize() = 3
srcImage.total() = 625000
srcImage.size() = [1000 x 625]

.\tutorial_example.exe (进程 17056)已退出,代码为 0。
按任意键关闭此窗口. . .

需要注意的是,Mat类其实可以分为两部分,一部分为Mat类对象本身,它是一个图像头,包含了该类的属性,方法,等等,不管你创建一个多大的图像,其数据头的大小是固定的,因为类的实现是固定的,对其求sizeof,大小恒为96字节:

代码语言:javascript
复制
assert(sizeof(srcImage) == 96);

类中包含一个特殊成员,即data指针,该指针所指向的,才是真正的图像存储区域内存,因此在绝大多数情况下,你所对Mat类型所做的操作,都是浅拷贝:

代码语言:javascript
复制
Mat srcImage = imread("./1.jpg");
assert(!srcImage.empty());
imshow("srcImage", srcImage);
Mat dstImage = srcImage;
assert(srcImage.data == dstImage.data);//图像内存依然是同一块

rows:图像行数

rows代表了图像的行数,也就是图像的高,这个没有什么歧义。

cols:图像列数

cols代表了图像的列数,也就是图像的宽,该参数对于同一张大小的图像是固定的,不会受图像的通道数的影响,我们假设一张6行3列的3通道的Mat图像,则图像数据的排布应该如下图所示:

虽然其数组结构在内存中因为三通道的原因保存为“9列”,但就图像格式的抽象层面上来说,其宽度,也就是cols列数,依然是3列,没三个通道看作同一个像素点的不同通道数值,共同组合起来成为一个三通道像素点,像素点才是图像最基本的组成单位。

dims:图像的维度

dims表示图像的维度,或者说是数组的维度,一般图像,如上图625*1000的图像,就是二维图像,若创建一个15 * 100 * 5的Mat,则维度就是3。

维度和通道数不是一个概念,二维图像三通道,表示在srcImage[0][0]的位置像素有三个值(即为三通道),而三维图像,则应该用srcImage[0][0][0]来表示第一个像素值了。

setp:图像的步长

图像的步长其实是一个用来标记单元大小的数值,单位为字节,和Int类型为4个字节,char类型为1个字节的步长意义相同。在opencv中我们可以通过Mat::step属性来访问。需要注意的是,step是一个数组,其长度为dims,也就是说图像每一维都有一个步长。

我们以一个示例Mat对象来说明,首先创建一个对象并打印其信息:

代码语言:javascript
复制
cv::Mat sampleImage(cv::Size(3, 6), CV_8UC3, cv::Scalar(1, 2, 3));
cout << sampleImage << endl;
cout << "sampleImage.rows = " << sampleImage.rows << endl;
cout << "sampleImage.cols = " << sampleImage.cols << endl;
cout << "sampleImage.dims = " << sampleImage.dims << endl;
cout << "sampleImage.step = " << sampleImage.step << endl;
cout << "sampleImage.step[0] = " << sampleImage.step << endl;
cout << "sampleImage.step[1] = " << sampleImage.step[1] << endl;
cout << "sampleImage.step[2] = " << sampleImage.step[2] << endl;
cout << "sampleImage.step[3] = " << sampleImage.step[3] << endl;
cout << "sampleImage.channels() = " << sampleImage.channels() << endl;
cout << "sampleImage.elemSize() = " << sampleImage.elemSize() << endl;

我们创建了一个6行3列的3通道图像,其每个像素值都为(1,2,3),将图像打印并打印其基本信息:

代码语言:javascript
复制
[ 1,   2,   3,   1,   2,   3,   1,   2,   3;
   1,   2,   3,   1,   2,   3,   1,   2,   3;
   1,   2,   3,   1,   2,   3,   1,   2,   3;
   1,   2,   3,   1,   2,   3,   1,   2,   3;
   1,   2,   3,   1,   2,   3,   1,   2,   3;
   1,   2,   3,   1,   2,   3,   1,   2,   3]
sampleImage.rows = 6
sampleImage.cols = 3
sampleImage.dims = 2
sampleImage.step = 9
sampleImage.step[0] = 9
sampleImage.step[1] = 3
sampleImage.step[2] = 4607182418800017408
sampleImage.step[3] = 4611686018427387904
sampleImage.channels() = 3
sampleImage.elemSize() = 3

.\tutorial_example.exe (进程 17056)已退出,代码为 0。
按任意键关闭此窗口. . .

我们打印了图像的step参数,上文所说step属性是一个数组,其长度等于图像的维度dims,6*3的sampleImage为二维图像,因此step数组的长度为2,我们打印了step数组的成员信息:

代码语言:javascript
复制
sampleImage.step = 9
sampleImage.step[0] = 9
sampleImage.step[1] = 3
sampleImage.step[2] = 4607182418800017408
sampleImage.step[3] = 4611686018427387904

可以看到sampleImage.step和sampleImage.step[0]必然是相等的,因为数组首地址和第一个元素的地址是相同的。

sampleImage.step[0] = 9为二维图像的第二维(也就是整个图像面)的步长(面的步长自然就是行),所以

代码语言:javascript
复制
9 == 列 * 通道数 == sampleImage.cols * sampleImage.channels == 3 * 3

也就是在内存上一行所占的长度。

所以sampleImage.step[1] = 3就好理解了,表示二维图像的第一维(也就是行)的步长(行的步长自然就是点),也就是一像素点的步长了,因为是3通道图像,所以step[1] == 3,如果是一维图像,则step[1]==1。

总结来说,step数组表示图像的步长,其长度和图像的维度相等,且最后一个步长必然是点的步长。

我们可以简单的验证一下,因为一个Mat图像在内存中是连续的,所以如下图所示,ptr1内存坐标和ptr2的内存坐标一定是相等的,如何得到ptr1的坐标呢?Mat::data代表了图像数据的首地址,该首地址加上一行的步长,就是行尾ptr1的地址了,获取ptr2的地址也很简单,直接对第1行0列的像素点取地址即可:

代码语言:javascript
复制
cv::Mat sampleImage(cv::Size(3, 6), CV_8UC3, cv::Scalar(1, 2, 3));
cout << "ptr1 = " << (int*)(sampleImage.data + sampleImage.step[0]) << endl;
cout << "ptr2 = " << (int*)(&sampleImage.at<uchar>(1, 0)) << endl;

打印结果如下,可以看到两者地址是相等的:

代码语言:javascript
复制
ptr1 = 0000019F0995E709
ptr2 = 0000019F0995E709

在上段代码中,图像虽然是二维图像,但我们还是打印了step[2]和step[3]:

代码语言:javascript
复制
sampleImage.step[2] = 4607182418800017408
sampleImage.step[3] = 4611686018427387904

其实是无效值,这也是在意料之内的。因为超出数组长度的地址,访问到的结果必然是无法预知的。

type():图像类型

如上图所示为opencv Mat数据所能包含的类型,而我们最常用的是使用CV_8UC1和CV_8UC3来存储图像。

代码语言:javascript
复制
cv::Mat sampleImage_8UC3(cv::Size(3, 6), CV_8UC3, cv::Scalar(1, 2, 3));
cv::Mat sampleImage_8UC1(cv::Size(3, 6), CV_8UC1, cv::Scalar(1, 2, 3));
cout << "sampleImage_8UC3:\n" << sampleImage_8UC3 << endl;
cout << "sampleImage_8UC1:\n" << sampleImage_8UC1 << endl;

打印结果如下:

代码语言:javascript
复制
sampleImage_8UC3:
[  1,   2,   3,   1,   2,   3,   1,   2,   3;
  1,   2,   3,   1,   2,   3,   1,   2,   3;
  1,   2,   3,   1,   2,   3,   1,   2,   3;
  1,   2,   3,   1,   2,   3,   1,   2,   3;
  1,   2,   3,   1,   2,   3,   1,   2,   3;
  1,   2,   3,   1,   2,   3,   1,   2,   3]
sampleImage_8UC1:
[  1,   1,   1;
  1,   1,   1;
  1,   1,   1;
  1,   1,   1;
  1,   1,   1;
  1,   1,   1]

.\tutorial_example.exe (进程 17056)已退出,代码为 0。
按任意键关闭此窗口. . .

其中U代表了unsigned char型的数据, 其表示的范围为0 到 255; C3表示三个通道, 也就是同一个位置能存放几个数字;CV_8UC3和CV_8UC1是Mat数据结构存储图像最理想的类型,因为图像像素值同样被量化到了0-255的离散空间之内,使用uchar型数据范围刚好用来保存256级的像素,而没有多余的浪费空间。

但Mat不仅只能用来存储图像,因此其类型也不仅仅只有CV_8UC1和CV_8UC3两种。例如当我们使用Mat进行矩阵计算时,就需要double级别的精度,此时CV_64F类型就可以派上用场了。

elemSize:最小单元大小

在开始说elemSize之前,我们先回顾一下图像Mat的step属性,步长属性是个数组,其最后一个数值代表了图像最小维度的步长,也就是点维度所占字节。而elemSize表示了图像最小单元的大小,单位也是字节。在此我们先下一个定论:图像的elemSize总是和最小维度的步长相等。

举个例子,创建一个CV_32FC3类型,6行3列的图像:

代码语言:javascript
复制
cv::Mat sampleImage_32FC3(cv::Size(3, 6), CV_32FC3, cv::Scalar(1, 2, 3));
cout << "sampleImage_32FC3.rows = " << sampleImage_32FC3.rows << endl;
cout << "sampleImage_32FC3.cols = " << sampleImage_32FC3.cols << endl;
cout << "sampleImage_32FC3.dims = " << sampleImage_32FC3.dims << endl;
cout << "sampleImage_32FC3.step = " << sampleImage_32FC3.step << endl;
cout << "sampleImage_32FC3.step[0] = " << sampleImage_32FC3.step << endl;
cout << "sampleImage_32FC3.step[1] = " << sampleImage_32FC3.step[1] << endl;
cout << "sampleImage_32FC3.elemSize() = " << sampleImage_32FC3.elemSize() << endl;
assert(sampleImage_32FC3.step[1] == sampleImage_32FC3.elemSize());

输出结果如下:

代码语言:javascript
复制
sampleImage_32FC3.rows = 6
sampleImage_32FC3.cols = 3
sampleImage_32FC3.dims = 2
sampleImage_32FC3.step = 36
sampleImage_32FC3.step[0] = 36
sampleImage_32FC3.step[1] = 12
sampleImage_32FC3.elemSize() = 12

.\tutorial_example.exe (进程 17056)已退出,代码为 0。
按任意键关闭此窗口. . .

因为图像为2维,所以其最小维度的步长为sampleImage_32FC3.step[1] = 12,因为数据类型是32F类型,所以每个数值占4个字节,又因为为三通道图像,每个像素点有三个数值来表示,因此4*3=12个字节。这就是该图像点维度的步长,也就是最小单元(像素点)elemSize所占大小。

total:图像中元素个数

一幅图像有多少个元素,也就是有多少个像素点,这个很好理解,total() = cols * rows。该参数同样与通道数无关。

depth:图像位深度

在opencv Mat类型有一个depth()方法返回一个深度值,该值标识的并不是图像的位深度,而只是像素类型的深度。例如我们还是读取文章开头的那张图像:

代码语言:javascript
复制
Mat srcImage = imread("./1.jpg");
assert(!srcImage.empty());
imshow("srcImage", srcImage);
cout << "srcImage.rows = " << srcImage.rows << endl;
cout << "srcImage.cols = " << srcImage.cols << endl;
cout << "srcImage.depth() = " << srcImage.depth() << endl;
cout << "srcImage.channels() = " << srcImage.channels() << endl;

输出结果:

代码语言:javascript
复制
srcImage.rows = 625
srcImage.cols = 1000
srcImage.depth() = 0
srcImage.channels() = 3

该图像为CV_8UC3类型,opencv返回的深度便是CV_8U,标识的是像素类型的深度:

代码语言:javascript
复制
CV_8U   0
CV_8S   1
CV_16U  2
CV_16S  3
CV_32S  4
CV_32F  5
CV_64F  6
CV_USRTYPE1 7

使用depth()*channels()所得到的,才是真正的图像位深度,该图位深度也就是8*3=24,可以在图像属性里验证:

图像位深度是指存储每个像素所用的位数,也用于度量图像的色彩分辨率。图像深度确定彩色图像的每个像素可能有的颜色数,或者确定灰度图像的每个像素可能有的灰度级数。比如一幅单色图像,若每个像素深度有8位,则最大灰度数目为2的8次方,即256。

THE END

参考文档

  • 4.5.5 cv::Mat Class Reference
  • 《机器视觉算法与应用(第二版)
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 周旋机器视觉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • cols:图像列数
  • dims:图像的维度
  • setp:图像的步长
  • type():图像类型
  • elemSize:最小单元大小
  • total:图像中元素个数
  • depth:图像位深度
相关产品与服务
图像处理
图像处理基于腾讯云深度学习等人工智能技术,提供综合性的图像优化处理服务,包括图像质量评估、图像清晰度增强、图像智能裁剪等。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档