OpenCV 笔记(1):图像的读取、显示、创建

news2025/1/12 20:43:00

Part11. 数字图像的含义

OpenCV 中的图像,其实指的是数字图像。在介绍图像这个概念之前,先介绍几个基础的概念:

  • 像素(Pixel)是图像的基本单元或者基本元素,亦或者是图像最小的单位。图像中的像素点包含不同的像素值。对于灰白图像而言,像素值是介于0-255之间的值;对于拥有 RGB 3个通道的彩色图像而言,每个通道的像素值为0-255;对于二维黑白图片而言,这些像素点构成了一个二维矩阵;对于二维彩色图片而言,这些像素点则是一个多维矩阵。

  • 数字图像处理(Digital Image Processing),它被称作计算机图像处理技术。

简单介绍完这些基础概念之后,接下来会介绍 OpenCV 中的 Mat 类。从本文开始,使用 OpenCV 的主要语言是 C++,当然只要有一些常见高级语言的编程基础,理解起来也不是什么难事。

Part22. Mat 的基本结构

自 OpenCV 2.x 之后,Mat 是 OpenCV 最基本也是最重要的类。Mat 是 Matrix 的简称,表示矩阵的意思。它是图像的容器,是一个二维向量。在 OpenCV 中 Mat 用于表示图像的类。对于 C++ 版本而言,Mat 支持自动内存管理,无须申请和释放内存,所以对程序员非常友好。(当然,也可以手工地去创建)

Mat 不仅可以存储图像,还可以存储矩阵。

Mat 由两个数据部分组成:矩阵头和指针。

class CV_EXPORTS Mat{
public:
  // ... a lot of methods ...
  ...
  /*! includes several bit-fields:
  - the magic signature
  - continuity flag
  - depth
  - number of channels
  */
  
  int flags;
  //! the array dimensionality, >= 2
  int dims;
  //! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions
  int rows, cols;
  //! pointer to the data
  uchar* data;
  //! pointer to the reference counter;
  // when array points to user-allocated data, the pointer is NULL
  int* refcount;
  
  // other members
  ...
};
  • 矩阵头:包含矩阵尺寸、存储方法、存储地址等信息。对应 Mat 类中的 flags、dims、rows,cols、data 指针、refcount 指针。所以矩阵头的大小是恒定的

  • 指针:指向包含像素值的矩阵(可以根据选择不同的存储方法,采用任何维度进行存储数据),也就是 data 指针指向的空间。通常,矩阵比矩阵头大几个数量级。

在图像处理中,我们日常使用的最简单的操作就是创建、传递、拷贝 Mat 对象。

图像在创建过程中,大的开销主要来自于矩阵。如果要拷贝、赋值 Mat 对象,并且使用深拷贝的话,效率会大大地降低。恰好 OpenCV 本身采用引用计数机制(可以看到 Mat 类中的 refcount 指针),即 Mat 对象的矩阵指针指向同一地址。这一机制可以让我们使用浅拷贝。

Mat 的构造函数、赋值运算符只拷贝矩阵头和矩阵指针 ,而不拷贝矩阵本身。

Part33. Mat 的读取与创建

接下来,我们来认识如何使用 Mat 对象,并通过它来读取和创建图像。

13.1 图像的读取

在图像创建之前,先介绍一下图像的读取。我们可以从图片中、视频中等读取 Mat 对象。例如,下面的代码是从图片文件中读取 Mat 对象。

String fileName = "/Users/tony/images/test.jpg";
Mat srcImage;
srcImage = imread(fileName);

//判断图像是否加载成功
if (srcImage.empty()){
    cout << "图像加载失败" << endl;
    return -1;
}

cv::imread() 函数用于从文件中读取 Mat 对象。其中,第一个参数 fileName 是包含了文件绝对路径的文件名。

CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR );

它的第二个参数表示图像读取的模式,默认是 IMREAD_COLOR ,它表示将图像转换成三通道 BGR 彩色图像。下面是 imread 函数可以用到的 flags:

enum ImreadModes {
       IMREAD_UNCHANGED            = -1, //按原样返回加载的图像(会带上alpha通道)。忽略EXIF方向。
       IMREAD_GRAYSCALE            = 0,  //将图像转为单通道灰度图
       IMREAD_COLOR                = 1,  //将图像转为BGR三通道彩色图像
       IMREAD_ANYDEPTH             = 2,  //如果图像深度为16-bit/32-bit则会返回该深度图像,否则返回8-bit图像
       IMREAD_ANYCOLOR             = 4,  //按照任意颜色图像格式读取
       IMREAD_LOAD_GDAL            = 8,  //使用gdal驱动程序加载图像
       IMREAD_REDUCED_GRAYSCALE_2  = 16, //将图像转为单通道灰度图且图像尺寸变为1/2
       IMREAD_REDUCED_COLOR_2      = 17, //将图像转为BGR三通道彩色图像且图像尺寸变为1/2
       IMREAD_REDUCED_GRAYSCALE_4  = 32, //将图像转为单通道灰度图且图像尺寸变为1/4
       IMREAD_REDUCED_COLOR_4      = 33, //将图像转为BGR三通道彩色图像且图像尺寸变为1/4
       IMREAD_REDUCED_GRAYSCALE_8  = 64, //将图像转为单通道灰度图且图像尺寸变为1/8
       IMREAD_REDUCED_COLOR_8      = 65, //将图像转为BGR三通道彩色图像且图像尺寸变为1/8
       IMREAD_IGNORE_ORIENTATION   = 128 //忽略EXIF中的方向标识,不旋转图像
     };

如果想以原本类型读取图片,我们可以选择 IMREAD_UNCHANGED,这样图像原本的类型和读进来的类型会保持一致。

有一个 imreadmulti 函数它与 imread 函数类似,用于从一个文件中读取多幅图像。例如,从某个 tiff 文件中读取多个 Mat 对象。

CV_EXPORTS_W bool imreadmulti(const String& filename, CV_OUT std::vector<Mat>& mats, int flags = IMREAD_ANYCOLOR);

23.2 Mat 的创建

Mat 类有很多构造函数,其自身也有很多函数可以用于创建 Mat 对象。本文以及以后的内容并不打算对每个函数进行详细的解释,因此这里只列举实际使用中常见的场景。

3.2.1 使用构造函数创建

创建一个 3*3 的 3 通道矩阵,每个像素点的值为(0,0,255)。

cv::Mat src(3, 3, CV_8UC3, cv::Scalar(0, 0, 255));
std::cout << "src = " << std::endl << src << std::endl;

输出结果:

src = 
[  0,   0, 255,   0,   0, 255,   0,   0, 255;
   0,   0, 255,   0,   0, 255,   0,   0, 255;
   0,   0, 255,   0,   0, 255,   0,   0, 255]

在这里,CV_8UC3 表示为 3 通道 Unsigned 8bits 格式的矩阵,即 BGR 3 通道。

稍微整理一下,矩阵数据类型: CV_<bit_depth>(S|U|F)C<number_of_channels>

  • bit_depth,比特数,例如 8 bits,16 bits,32 bits,64 bits

  • S|U|F S:signed int,有符号整形 U:unsigned int,无符号整形 F:float,单精度浮点型

  • C<number_of_channels>代表一张图片的通道数,例如:1:单通道图像,表示灰度图片。3:3 通道图像,表示 RGB 彩色图像 。4:4 通道图像,表示带 Alpha (透明度)通道的 RGB 图像。

在 OpenCV 中,类似的矩阵数据类型还有 CV_16SC3、CV_32FC3、CV_64FC3 等等。在下一篇,我们会详细介绍矩阵数据类型相关的内容。

再举一个例子,使用构造函数创建一个指定大小(400*400)的 Mat 对象,并指定每个像素点的颜色值(0,0,255)。其实,它会展示一张红色的图。

cv::Mat src(cv::Size(400,400), CV_8UC3, cv::Scalar(0,0,255));
imshow("red",src);

输出结果:11231d28ee21f1a40c132f0c520e0c14.jpeg

Scalar 字面意思是标量,它是从 Vec 派生的 4 个向量元素的模板类。Scalar 类型在 OpenCV 中广泛用于传递像素值。

Scalar 常见的构造函数为

Scalar_();
Scalar_(_Tp v0, _Tp v1, _Tp v2=0, _Tp v3=0);
Scalar_(_Tp v0);

Scalar_(const Scalar_& s);
Scalar_(Scalar_&& s) CV_NOEXCEPT;

当 Scalar 表示颜色时,单通道图像使用下标 [0] 表示,三通道图像使用下标 [0]、[1]、[2] 表示 B、G、R 通道。所以,cv::Scalar(0,0,255) 对应的就是红色。

3.2.2 使用数组创建

同样,使用构造函数创建一个 3*3 的矩阵。

int array[2] = { 3, 3 };
cv::Mat src(2, array, CV_8UC1, cv::Scalar::all(0));
std::cout << "src = " << std::endl << src << std::endl;

输出结果:

src = 
[  0,   0,   0;
   0,   0,   0;
   0,   0,   0]

该构造函数的第一个参数表示矩阵的维数,第二个参数表示指定 n 维数组形状的整数数组。所以,这里的 array 数组表示的是每一维数的数量。

3.2.3 使用 create 函数创建

使用 create() 函数创建一个 3*3 的二维单通道矩阵。

cv::Mat src;
src.create(3, 3, CV_8UC1);
std::cout << "src = " << std::endl << src << std::endl;

输出结果:

src = 
[  0,   0,  51;
   2,   0,  96;
   0,   0,  64]

create() 函数只能创建一个指定大小、指定矩阵数据类型的矩阵,并不能为矩阵设置初始值。它在改变矩阵尺寸时,为矩阵数据重新分配了内存,因此其所创建的矩阵中每个数据都是一个随机值

3.2.4 特殊矩阵创建

OpenCV 中有类似 MATLAB 那样可以快速赋值、创建矩阵的函数,生成全 0 矩阵、单位矩阵、对角矩阵。

cv::Mat mat_zeros = cv::Mat::zeros(3,3,CV_8UC1); // 全 0 矩阵
cout<<"mat_zeros="<<endl<<mat_zeros<<endl;
cv::Mat mat_ones = cv::Mat::ones(3,3,CV_8UC1);   // 单位矩阵
cout<<"mat_ones="<<endl<<mat_ones<<endl;
cv::Mat mat_eye = cv::Mat::eye(3,3,CV_8UC1);     // 对角矩阵
cout<<"mat_eye="<<endl<<mat_eye<<endl;

输出结果:

mat_zeros=
[  0,   0,   0;
   0,   0,   0;
   0,   0,   0]
mat_ones=
[  1,   1,   1;
   1,   1,   1;
   1,   1,   1]
mat_eye=
[  1,   0,   0;
   0,   1,   0;
   0,   0,   1]

3.2.5 使用自定义矩阵 Mat 创建

我们也可以自己定义一些数据量比较小的矩阵,例如:

cv::Mat src = (cv::Mat_<double>(3, 3) << 1, 0, 0, 0, 1, 0, 0, 0, 1);
std::cout << "src = " << std::endl << src << std::endl;

输出结果:

src = 
[1, 0, 0;
 0, 1, 0;
 0, 0, 1]

3.2.6 提取某个 Mat 对象的 ROI

ROI(Region Of Interest),表示感兴趣的区域。通常,提取 ROI 能够便于进一步分析图像。

常用的提取 ROI 区域的方法包括:

  • 使用 cv::Rect 指定矩形的左上角的坐标,以及它的宽和高,提取 ROI 区域。

Mat src = imread("/Users/tony/images/test.jpg");
Mat roi = src(Rect(300, 400, 200, 300));//Rect 四个形参分别表示 x 坐标,y 坐标,宽,高
  • 使用 cv::Range 指定感兴趣的行或列的范围,提取 ROI 区域。

Mat src = imread("/Users/tony/images/test.jpg");
Mat roi = src(Range(150, 150 + 100), Range(250, 250 + 100));//Range两个形参分别是:起始行或列,起始行或列+偏移量

再举个例子,读取一张图片,并显示图像。然后提取图像的 ROI,并显示该 ROI。

cv::Mat src = imread("...");
imshow("src",src);
Mat roi = src(Rect(1200, 800, 1500, 2500));
imshow("roi",roi);

输出结果:19761eafc75788cec8164dbc94bb9b24.jpeg

Part44. Mat 的赋值

34.1 浅拷贝与深拷贝

浅拷贝:只复制指向某个对象的指针,而不复制对象本身。新旧对象会共享同一块内存,修改任何一方都会影响到另一方。深拷贝:创造一个一模一样的对象,新旧对象不共享内存,修改任何一方不会影响到另一方。

之前提到过,Mat 的拷贝构造函数、赋值运算符都是浅拷贝,另外 ROI 的提取也是浅拷贝。例如:

Mat a = imread("/Users/tony/images/test.jpg");
Mat b(a);  // 拷贝构造函数
Mat c = a; // 赋值运算符

当删除 a、b、c 中任何一个对象,其余两个对象都不会指向一个空数据。只有当三个对象都删除时,才会真正释放矩阵数据。前面提到过,这依赖于 Mat 对象的引用计数机制(refcount 指针),它的本质是用于指向相同数据地址的不同类对象的内存管理,只有当矩阵数据引用次数为 0 的时候才会真正释放矩阵数据。

44.2 clone() 与 copyTo()

有时候,我们还是需要创建一个全新的 Mat 对象,拷贝矩阵数据本身。那么我们可以使用  clone() 、 copyTo() 函数实现深拷贝

例如:

Mat a;
Mat b = a.clone();// 对 a 进行克隆
Mat c;
a.copyTo(c);// 将 a 拷贝到 c 对象

当更改 a、b、c 中任何一个对象,其余两个对象都不会受到影响。

copyTo() 函数有两种形式:

  • srcImage.copyTo(dstImage):将 srcImage 的内容复制到 dstImage;

  • srcImage.copyTo(dstImage, mask):mask 是一个掩模,当 srcImage 与 mask 进行运算后,将得到的结果拷贝给 dstImage。其中,mask 必须为 CV_8U 类型,大小与 srcImage、dstImage 保持一致。

掩模的运算规则:

在图像的任意位置(x,y),如果 mask 的像素值等于 1,则 dstImage(x,y) = srcImage(x,y)。如果 mask 的像素值等于 0,则 dstImage(x,y) = 0

因此,使用 copyTo() 函数时,将原先的 srcImage 在  mask 上不为 0 的,所对应的像素点进行拷贝。拷贝的结果复制到目标对象 dstImage 上。

举个 copyTo() 函数并且使用 mask 的例子:

cv::Mat a = (cv::Mat_<double>(3, 3) << 0, 0, 0, 0, 0, 0, 0, 240, 0);
std::cout << "a = " << std::endl << a << std::endl;

Mat mask = Mat::eye(3,3,CV_8UC1);
std::cout << "mask = " << std::endl << mask << std::endl;

Mat roi;
a.copyTo(roi,mask);
std::cout << "roi = " << std::endl << roi << std::endl;

输出结果:

a = 
[0, 0, 0;
 0, 0, 0;
 0, 240, 0]
mask = 
[  1,   0,   0;
   0,   1,   0;
   0,   0,   1]
roi = 
[0, 0, 0;
 0, 0, 0;
 0, 0, 0]

Part55. 总结

本文作为入门的准备,简单介绍了图像相关的基础知识、Mat 的基本结构、Mat 的创建/读取/赋值。在此基础上也引申出很多知识,比如矩阵的数据类型、掩模等,这些内容都是非常重要的。因此,后续的内容都会用到它们,因此也会会更加进一步详细地介绍它们。

Java与Android技术栈】公众号

关注 Java/Kotlin 服务端、桌面端 、Android 、机器学习、端侧智能

更多精彩内容请关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1099641.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flume 整合 Kafka

1.背景 先说一下&#xff0c;为什么要使用 Flume Kafka&#xff1f; 以实时流处理项目为例&#xff0c;由于采集的数据量可能存在峰值和峰谷&#xff0c;假设是一个电商项目&#xff0c;那么峰值通常出现在秒杀时&#xff0c;这时如果直接将 Flume 聚合后的数据输入到 Storm 等…

Vue-props配置功能

Vue-props配置功能 props概述 功能&#xff1a;接收从其他组件传过来的数据&#xff0c;将数据从静态转为动态注意&#xff1a; 同一层组件不能使用props&#xff0c;必须是父组件传子组件的形式。父组件传数据&#xff0c;子组件接收数据。不能什么数据都接收&#xff0c;可…

【Spring Cloud】网关Gateway的请求过滤工厂RequestRateLimiterGatewayFilterFactory

概念 关于微服务网关Gateway中有几十种过滤工厂&#xff0c;这一篇博文记录的是关于请求限流过滤工厂&#xff0c;也就是标题中的RequestRateLimiterGatewayFilterFactory。这个路由过滤工厂是用来判断当前请求是否应该被处理&#xff0c;如果不会被处理就会返回HTTP状态码为42…

三相异步电机动态数学模型及矢量控制仿真

三相异步电机动态数学模型及矢量控制仿真 本文带你一步步推倒三相异步电机动态数学模型&#xff0c;按基于转子磁链定向的矢量控制进行 matlab 仿真&#xff0c;实现较好的控制效果。 1、异步电机三相方程 2、坐标变换 3、磁链3/2变换推导 4、两相静止坐标系下的方程 5、…

python 图片下面加边框TK界面

python 对图片增加边框&#xff0c;logo贴图&#xff0c;获取图片exif参数&#xff0c;填写图片文本内容-CSDN博客 import tkinter as tk from tkinter import ttk import os import glob import json import tkinter.messagebox as messagebox # 弹出提示框 from PIL import…

Vue 如何检测 data 中 数组的变化?

Vue 可以使用 watch 和 computed 监听数组的变化。 ① 使用 watch 监听数组 可以通过 deep 选项深度监听数组内部元素的变化 ② 使用 computed 监听数组 创建一个计算属性&#xff0c;返回数组的长度或者某个数组元素的值&#xff0c;当数组发生变化时&#xff0c;计算属性会…

在nodejs中实现调度任务

在nodejs中实现调度任务 node.js帮助开发人员简化了工作流程&#xff0c;创建了高效的应用程序。它的许多有用功能之一是任务调度。本文将探讨在nodejs中调度任务的重要性、各种使用第三方库的代码示例&#xff0c;以及需要遵循的一些有用的操作。 为什么我们需要安排任务 调…

#创作纪念日#我的256天创作纪念日

我的创作256天纪念日 机缘收获日常成就憧憬 机缘 机缘……好像128天的时候已经写过了…… 小升初时&#xff0c;我开始接触编程&#xff0c;进入了一个全新的世界。刚开始学习编程时&#xff0c;我只是对电脑的一些操作比较感兴趣&#xff0c;但慢慢地&#xff0c;我开始对编…

模式植物GO背景基因集制作

一边学习&#xff0c;一边总结&#xff0c;一边分享&#xff01; 写在前面 关于GO背景基因集文件的制作&#xff0c;我们在很早以前也发过。近两天&#xff0c;自己在分析时候&#xff0c;也是被搞了头疼。想重新制作一份GO背景基因集&#xff0c;进行富集分析。但是结果&…

vueday01——ref响应式

特性&#xff1a;持续监控某个响应式变量的属性名变化&#xff0c;可以使用shallowRef来取消这一特性&#xff0c;只监控对象整体的变化 ref测试代码&#xff1a; <template><div :id"idValue" ref"myDiv">打印obj{{ obj }}</div><…

大数据Flink(九十七):EXPLAIN、USE和SHOW 子句

文章目录 EXPLAIN、USE和SHOW 子句 一、EXPLAIN 子句 二、USE 子句

QQd挂源码已更新最新加速项目程序全开源

1、99 公益日活动加速任务已全部完成适配&#xff0c;空间公益说说和评论并分享小世界内容任务在已有的功能上进行挂机&#xff0c; 其中【发小世界】功能暂时更名为【公益小世界】。 2、上线新功能【公益答题】用于完成参加 Qbox 公益答题任务&#xff0c;等级套装有任意一项…

期中考Web复现

第一题 1z_php <?php //Yeedo told you to study hard! echo !(!(!(!(include "flag.php")||(!error_reporting(0))||!isset($_GET[OoO])||!isset($_GET[0o0])||($_GET[OoO]2023)||!(intval($_GET[OoO][0])2023)||$_GET[0o0]$_GET[OoO]||!(md5($_GET[0o0])md5($_…

Java Static

Static 变量被 static 修饰 static 修饰的变量在类中只有一份&#xff0c;可以称为类变量&#xff0c;其他变量称为实例变量在方法区加载类的时候&#xff0c;会检查类中是否存在静态变量&#xff0c;如果存在则会在堆内存区域开辟一块空间用于存储静态变量。方法区中的静态变…

A114-经典赛题-Web应用程序文件包含安全攻防

实验步骤: Web应用程序文件包含安全攻防 任务环境说明&#xff1a; 服务器场景&#xff1a;WebServ2003&#xff08;用户名&#xff1a;administrator&#xff1b;密码&#xff1a;空&#xff09; 服务器场景操作系统&#xff1a;Microsoft Windows2003 Server 服务器场景…

IPv6知识概述 - ND协议

IPv6知识概述 - ND协议 参考文章&#xff1a;https://blog.csdn.net/Gina_wj/article/details/106708770 IPv6基础篇&#xff08;四&#xff09;&#xff1a;邻居发现协议NDP ND协议功能概述 ND&#xff08;Neighbor Discovery&#xff0c;邻居发现&#xff09;协议是IPv6的…

01【Git的基本使用与底层原理】

下一篇&#xff1a;02【Git的分支与数据恢复】 目录&#xff1a;【Git系列教程-目录大纲】 文章目录 一、Git概述1.1 Git简介1.2 集中式与分布式1.2.1 集中式版本控制1.2.2 分布式版本控制 1.3 Git的使用流程1.3.1 本地仓库1.3.2 协同开发 1.4 Git的配置1.4.1 Git的配置等级1…

从头开始机器学习:线性回归

从头开始机器学习&#xff1a;线性回归 跟随 16 分钟阅读 28月 <> 1 一、说明 本篇实现线性回归的先决知识是&#xff1a;基本线性代数&#xff0c;微积分&#xff08;偏导数&#xff09;、梯度和、Python &#xff08;NumPy&#xff09;&#xff1b;从线性方程入手。 代…

【LeetCode刷题(数据结构与算法)】:用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾 int pop() 从队列的开头移除并返回元素 int peek() 返回队列开头…

运维 | 如何在 Linux 系统中删除软链接 | Linux

运维 | 如何在 Linux 系统中删除软链接 | Linux 介绍 在 Linux 中&#xff0c;符号链接&#xff08;symbolic link&#xff0c;或者symlink&#xff09;也称为软链接&#xff0c;是一种特殊类型的文件&#xff0c;用作指向另一个文件的快捷方式。 使用方法 我们可以使用 ln…