数字图像处理与OpenCV初探

什么是数字图像处理?

    当今时代,数字图像无处不在。手机拍照、安防监控、医疗检查、地图导航、工业质检……我们每天都在接收、分析和处理大量图像信息。对于计算机而言,图像并不是一张“看得懂”的照片,而是由数值组成的矩阵。如何让机器也具备“看图”的能力,正是数字图像处理的核心目标。

    简而言之,数字图像处理就是用计算机对图像进行操作和分析,让图像更“清晰”、更“有用”、更“可识别”。举例如下:

  • 拍完照后用手机“自动美颜”一下,可能用到了滤波、边缘平滑、肤色增强等图像处理算法;
  • 医生查看 CT 或眼底图时,图像可能经过了对比度增强或伪彩色处理,使细节更加清晰;
  • 摄像头识别车辆车牌,需要经过颜色识别、轮廓识别、字符识别等操作;

 什么是OpenCV?

    OpenCV(Open Source Computer Vision Library)是一个开源、跨平台的计算机视觉库,最初由英特尔开发,现在已经成为业界和学术界广泛使用的工具之一。OpenCV有如下特性:

  • 跨平台:支持 Windows、Linux、macOS
  • 语言支持丰富:C++/Python 作为主流语言选择,也有部分选择Java、JavaScript等
  • 实时性强:底层基于 C/C++,速度快,能胜任对性能要求高的实时应用
  • 功能强大:从图像读取到复杂特征匹配,从边缘检测到深度学习支持等

    在我们的专栏中,我们的示例主要使用C++,这是工程领域中最合适的使用方式。C++提供的卓越的性能,可以满足很多实时性的应用需求。同时,我们也会适当给出一些Python示例,在深度学习训练阶段,Python是我们的首选语言(一般选择pytorch框架)。OpenCV可以对深度学习进行数据预处理支持。

    对于OpenCV的安装,Python环境下只需要运行以下命令即可:

 pip install opencv-python

    对于C++环境,我们一般都是从源码直接编译,然后再部署到自己的开发环境中。我们这里不讲如何源码编译,大家可以在网上自行搜索。我们稍后会提供一个完整的C++项目,该项目会包含OpenCV所有的依赖库,大家可以基于该项目进行自己的开发工作。

数字图像基本结构

    上图为一个4行8列的矩阵,每个元素的取值范围为[0,256)。我们可以将其看作为一个4*8的灰度图像,灰度图像的取值范围为[0,256)。在现实生活中,我们更多看到的是彩色图像,彩色图像相对于灰度图像来说,每个元素需要3个值表示,分别代表Red,Green和Blue,其数据矩阵如下:   

    以上同样为一个4行8列的矩阵,但每个元素由一个3*1的向量构成,如第0行0列的向量值为[172,47,117],这三个元素具体表示:Blue=172,Green=47,Red=117。特别注意这里的通道排列顺序为BGR,而在生活中我们习惯称呼彩色图像为RGB图像。

    OpenCV提供了函数cv::imread(),该函数可读取多种格式图像,如JPG, BMP, PNG等,其返回值为cv::Mat对象,该对象保存了图像相关的所有信息。不论读取哪种格式图像,只要该图像为三通道数据,读取后的图像在内存中的排列顺序均为BGR(四通道多为BGRA,A表示Alpha通道,用于记录半透明相关信息)。

    OpenCV提供了函数cv::imshow(),该函数用于显示图像,其核心参数为cv::Mat对象。我们通过一个实验来加深通道排列顺序的理解。

import cv2  #导入opencv,可用于读取与显示图像
import matplotlib.pyplot as plt  #用于图像显示img_bgr = cv2.imread('lena.png')  #读取图像, 默认通道为bgrif img_bgr is None:print("图像加载失败,请检查路径是否正确。")
else:img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)  # 转换为rgb顺序cv2.imshow('opencv show', img_bgr)  #使用opencv显示图像cv2.waitKey(0)  #opencv需要调用该函数已阻止程序继续执行cv2.destroyAllWindows()  #用户关闭图像窗口后清除资源#使用matplot显示图像,这里需要传入rgb顺序图像plt.imshow(img_rgb)plt.title("matplot show")plt.show()

     以上一段python代码首先使用OpenCV读取一张图像,然后分别使用OpenCV与matplot库进行显示。需要特别注意,在matplot库中,默认将通道顺序解读为RGB。因此,我们调用了cvtColor函数对其进行通道转换(cv2.COLOR_BGR2RGB),使得matplot可以正确显示图像颜色。以下分别给出正确通道顺序显示结果与错误通道顺序显示结果。

正确通道顺序显示结果
错误通道顺序显示结果

    接下来我们给出一段C++代码,该代码实现了图像读取与显示,处理语法上的差异,与python代码基本一致。

int main()
{cv::Mat img_bgr = cv::imread("lena.png", cv::IMREAD_COLOR);cv::imshow("opencv show", img_bgr);cv::waitKey(0);cv::destroyAllWindows();return 0;
}

数字图像元素读取与修改 

     到目前为止,我们了解了图像数据的基本结构,也能正确读取和显示图像。那么,我们应该如何读取或修改图像单个元素数据呢? 

    有如下方法可以读取或修改图像像素数据(C++),如下:

  • 使用cv::Mat.at<>()方法,该方法适合读取少量数据。由于函数会进行边界检查,其速度较慢,在图像处理算法实践中,我们基本不会使用该函数读取数据。以下给出示例代码
{   // 读取100行,150列数据,该数据为三通道数据cv::Vec3b val = img_bgr.at<cv::Vec3b>(100, 150);  uchar b = val[0];uchar g = val[1];uchar r = val[2];// 读取单通道数据(即灰度图)uchar gray = img_bgr.at<uchar>(100, 150);
}
  • 在实际项目中,我们总是直接访问指针以获得最佳的访问效率,以下给出示例代码
{// // 3通道图像(bgr)访问// 遍历每一行for (int row = 0; row < img_bgr.rows; ++row){// 获取每一行的起始指针cv::Vec3b* ptr = img_bgr.ptr<cv::Vec3b>(row);// 遍历每一个元素(cv::Vec3b)for (int col = 0; col < img_bgr.cols; ++col){// 获取每个通道的值uchar b = ptr[col][0];uchar g = ptr[col][1];uchar r = ptr[col][2];// 每个通道亮度*2// 由于每个通道取值范围为[0,255],因此需要确保不越界!b = b * 2 > 255 ? 255 : b * 2;g = g * 2 > 255 ? 255 : g * 2;r = r * 2 > 255 ? 255 : r * 2;// 将修改后值赋给原通道ptr[col][0] = b;ptr[col][1] = g;ptr[col][2] = r;}}// // 单通道(灰度图像)访问// 遍历每一行for (int row = 0; row < img_bgr.rows; ++row){// 获取每一行的起始指针uchar* ptr = img_bgr.ptr<uchar>(row);// 遍历每一个元素(uchar)for (int col = 0; col < img_bgr.cols; ++col){// 获取灰度值uchar gray = ptr[col];// 每个通道亮度*2// 由于取值范围为[0,255],因此需要确保不越界!gray = gray * 2 > 255 ? 255 : gray * 2;// 将修改灰度值赋给原图像ptr[col] = gray;}}
}

   通过以上程序,我们可以得到一个亮度更高的图像,效果如下: 

每个通道亮度翻倍后图像

    虽然直接访问指针可以获得最佳的运行效率,然而我们也可能因为访问不当而产生以下不良后果,典型错误为内存越界错误,这可能导致整个程序崩溃。所以,在实际项目中,我们需要慎重使用指针,确保代码正确性以避免内存越界错误!

    另外,一些性能优化的常识可以让我们避免一些极端低效的代码,如下代码大大降低运行效率:

{// 该代码运行效率会非常低,由于违背了内存连续性访问原则,// 导致频繁的缓存命中失败,严重降低数据访问效率!// 遍历每一列for (int col = 0; col < img_bgr.cols; ++col){// 遍历每一行for (int row = 0; row < img_bgr.rows; ++row){cv::Vec3b val = img_bgr.ptr<cv::Vec3b>(row)[col];uchar b = val[0];uchar g = val[1];uchar r = val[2];}}
}

    观察以上代码,我们for循环顺序发生了改变,该代码对图像元素的访问顺序为:

    0行0列->1行0列->2行0列...->0行1列->1行1列->2行1列....

    也就是说在列方向上遍历,而图像元素在行方向上连续存储,从而每次访问都可能导致缓存命中失败,从而严重影响访问效率!

     一般情况下,C++提供了非常灵活的图像数据读取方式,有时候我们可能也会使用Python进行少量的数据读取操作,以下给出使用Python读取图像数据的方法:

(b, g, r) = img_bgr[100, 150]  #获取第100行第150列的B、G、R通道值
blue_channel = img_bgr[:, :, 0]  #获取蓝通道数据

cv::Mat关键元素 

    cv::Mat是 OpenCV中最核心的数据结构之一,用于表示图像、视频帧、矩阵等二维数据。理解 cv::Mat的内部结构对于高效图像处理非常关键。早期的C接口使用IplImage结构,除了兼容需求,我们不再使用IplImage接口了。

    以下是cv::Mat的基本数据结构:

    cv::Mat
     ├── data           → 指向图像数据的指针
     ├── rows          → 行数(即图像高度)
     ├── cols           → 列数(即图像宽度)
     ├── step           → 每行占用的字节数(stride)
     ├── channels    → 通道数(通过 type 解析)
     ├── type            → 数据类型和通道数的编码
     ├── depth()       → 每个通道的数据类型(如 CV_8U)
     ├── refcount      → 引用计数指针(实现共享内存)
     └── others         → flags、allocator 等

    data为一个uchar*类型数据,指向图像像素数据的首地址,可以直接通过指针操作像素,如:

uchar* p = img_bgr.data;  p[0] = 255; p[1] = 255;

    rows和cols分别代表图像的行数与列数,也即图像的高度与宽度。

    step表示图像每一行占用的总字节数,利用该数据可以准确跳转到每行数据首指针上,以下两种写法均可以跳转到第10行首指针处,故data1与data2为相等指针。

cv::Vec3b* data1 = (cv::Vec3b*)(img_bgr.data + img_bgr.step * 10);
cv::Vec3b* data2 = img_bgr.ptr<cv::Vec3b>(10);

    type()函数返回一个整数,该整数编码了通道数与数据类型信息。一般情况下,我们可以分别调用channels()与depth()函数来分别获取通道数与数据类型。

    在常规数字图像中,通道数一般返回为1,3,4通道数据,分别表示灰度图,真彩色,带Alpha通道真彩色。当然,在其他应用中,也可以返回任意通道,如2通道可以编码图像梯度信息。

    图像数据类型主要定义了数据精度与数据符号,如CV_8U为8位无符号整数,CV_8S为8位有符号整数,CV_16U/CV_16S定义了16位整数,CV_32S定义了32位有符号整数(注意没有CV_32U!),CV_32F/CV_64F分别定义了单精度与双精度浮点类型。

int depth = img_bgr.depth();
int channels = img_bgr.channels();

    elemSize()表示一个像素占用的字节数,elemSize1()表示一个通道占用的字节数,使用elemSize() / elemSize1()可计算处通道数,等价于channnels()函数。 

    refcount作为内存引用计数,在浅拷贝时共享内存数据,仅增加引用计数,代码如下:

cv::Mat img = cv::imread("lena.png", cv::IMREAD_COLOR);int* ref = img.refcount;  // 引用计数为1cv::Mat img2 = img;  //浅拷贝,img与img2公用内存int* ref2 = img2.refcount; // 浅拷贝后引用计数增加到2img.release();   // 释放img,引用计数减1int* ref3 = img2.refcount; // 释放img后,引用计数减少到1

    除了浅拷贝之外,我们在很多时候有深拷贝需求(即不共享内存数据),函数copyTo()与clone()均可实现该目标,代码如下:

// 方式 1:clone(返回新对象)
cv::Mat img_clone = img.clone();// 方式 2:copyTo(拷贝到已有对象)
cv::Mat img_copy;
img.copyTo(img_copy);

结语 

    通过介绍数字图像处理与OpenCV的基本知识,我们理解了数字图像的基本结构,以及如何高效的访问图像中的任意元素。同时对通道顺序以及内存连续性问题进行特别讲解,使得我们可以在工程实践中避免一些微妙的错误,提升程序的效率。最后,我们讲解了OpenCV中最为重要的数据结构cv::Mat,通过该数据结构,可以实现图像数据的所有基本操作。

    在工程应用中,为了运行效率我们一般会选择OpenCV的C++接口。然而在某些情况下,Python接口也发挥了重要的作用。如在深度学习的训练过程中,我们一般使用pytorch框架。此时,使用OpenCV的Python接口进行数据预处理是非常必要的。因此,在博文中,我们同步给出了C++与Python代码片段,以适应不同应用场景需求。

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

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

相关文章

ubuntu网络连接失败 + mobaxterm拖拽文件出错等问题解决方法

网络连接问题&#xff0c;表现在不能通过源下载以及更新 终端问题显示【通过 ip a 命令获得】 kejiubuntu:~/Desktop$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00…

C# CS_Prj01 串口通信控制台程序

一直以来&#xff0c;玩8088单板机&#xff0c;上位机都是使用的绿色现成的串口软件。 今天&#xff0c;感觉8088单板机的各部分测试都基本完成了。 本着玩的精神&#xff0c;自己写一个上位机的简单串口程序&#xff0c;与自己的8088单板机通讯。 功能&#xff1a;一个完整…

40套精品大气黑金系列行业PPT模版分享

黑金系列PPT模版&#xff0c;优秀员工颁奖典礼PPT模版&#xff0c;消费订货会PPT模版&#xff0c;共赢未来PPT模版&#xff0c;投资类PPT模版&#xff0c;双12年终盛典PPT模版&#xff0c;商业计划书PPT模版&#xff0c;高端通用企业文化PPT模版&#xff0c;公司喜报企业捷报PP…

SAP学习笔记 - 开发31 - 前端Fiori开发 Device Adaptation(设备自适应)

上一章讲了Fiori开发中的 Responsiveness&#xff08;响应式设计&#xff09;。 SAP学习笔记 - 开发30 - 前端Fiori开发 Responsiveness&#xff08;响应式设计&#xff09;-CSDN博客 本章继续学习Fiori 开发中的知识。 目录 1&#xff0c;Device Adaptation&#xff08;设备…

网络的那些事——初级——OSPF(2)

前面说了OSPF的状态机和一起简单的OSPF配合&#xff0c;接下这章继续写OSPFV2 IP frr和OSPFV3. 什么是OSPF IP FRR? OSPF IP FRR&#xff08;Fast Reroute&#xff09;利用全网链路状态数据库&#xff0c;预先计算出备份路径保存在转发表中&#xff0c;以备在故障时提供流量保…

C++(初阶)(二十一)——unordered_set和unordered_map

二十二&#xff0c;unordered_set和unordered_map的使用 1.unordered_set 1.1介绍 c11 template<class Key,class Hash std::hash<Key>,class KeyEqual std::equal_to<Key>,class Allocator std::allocator<Key> > class unordered_set;c17 na…

Java面试题:分布式ID时钟回拨怎么处理?序列号耗尽了怎么办?

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 有很多很多不足的地方&#xff0c;欢迎评论交流&#xff0c;感谢您的阅读和评论&#x1f604;。 目录 引言1 分布式ID2 问题2.1 时钟回拨2.1.1 毫秒级时…

影视剧学经典系列-梁祝-陶渊明《感士不遇赋并序》

1、背景 《感士不遇赋并序》是东晋诗人陶渊明创作的一篇抒发怀才不遇之慨的辞赋作品。受董仲舒《士不遇赋》和司马迁《悲士不遇赋》启发&#xff0c;陶渊明借古喻今&#xff0c;批判“真风告逝&#xff0c;大伪斯兴”的乱世。社会批判‌以“密网裁而鱼骇&#xff0c;宏罗制而鸟…

Spring Cloud Gateway 全面学习指南

Spring Cloud Gateway 全面学习指南 学习目录 第一部分&#xff1a;基础概念与核心架构 API网关概述与Spring Cloud Gateway简介Spring Cloud Gateway核心架构与工作原理Spring Cloud Gateway与Zuul的对比分析Spring Cloud Gateway核心组件详解 第二部分&#xff1a;基础配…

蛋白分析工具和数据库

UniProt&#xff08;Universal Protein Resource&#xff09;是一个综合性的蛋白质数据库&#xff0c;提供了全球范围内已知的蛋白质序列和功能信息。其中&#xff0c;UniProtKB&#xff08;UniProt Knowledgebase&#xff09;是最核心的组成部分&#xff0c;包含了经过注释和分…

Docker -- 快速入门

镜像与容器 当我们使用Docker安装应用时&#xff0c;Docker会自动搜索并下载应用镜像&#xff08;image&#xff09;。镜像不仅包含应用本身&#xff0c;还包含应用运行所需要的环境&#xff0c;配置、系统函数库。Docker会在运行镜像时创建一个隔离环境&#xff0c;称为容器&…

输入数量未知如何设置输入

在 C 的算法题中&#xff0c;如果你不知道输入数据有多少组&#xff08;即测试用例的数量未知&#xff09;&#xff0c;通常的处理方式是使用 循环读取输入直到文件结束 &#xff08;EOF&#xff09;。这类题目常见于在线评测系统&#xff08;如 LeetCode、牛客网、POJ 等&…

如何在Windows上使用qemu安装ubuntu24.04服务器?

2025年6月15日&#xff0c;周日晚上 在Windows上使用QEMU安装Ubuntu 24.04需要完成环境配置、镜像准备、虚拟机创建及系统安装等步骤。以下是综合多个搜索结果后的详细指南&#xff1a; 1. 安装QEMU环境 下载QEMU Windows版 从QEMU官网下载64位安装包&#xff08;如qemu-w64-s…

前端开发面试题总结-vue2框架篇(二)

Vue2高频问答 一、为什么 Vue 的 data 属性必须声明为返回一个初始数据的函数? 回答重点&#xff1a; Vue 的 data 属性声明成一个返回初始数据的函数&#xff0c;是为了确保每个组件实例都有独立的状态。通过这种方式&#xff0c;避免了组件使用相同的数据对象导致的状态共…

Web第二次方向考核复盘

一、简答题 1. &#xff08;1&#xff09;为什么要清除浮动&#xff1f; 答&#xff1a;当子元素浮动时会脱离文档流&#xff0c;父元素无法正确计算子元素高度导致高度、边框异常显示。同时会影响后续文档流布局。 <style>.box1 {border: solid 2px #000;}.child1 {fl…

Linux入门(十八)read函数

read 读取控制台输入 基本语法 read 选项 参数 选项 -p 指定读取值时的提示符 -t 指定读取值时等待的时间&#xff08;秒&#xff09;&#xff0c;如果没有在指定的时间内输入&#xff0c;就不再等待了 参数 变量&#xff1a;指定读取值的变量名 实例&#xff1a; 1、读取控制…

Python 文件操作详解

文章目录 Python 文件操作详解一、文件操作的基本流程二、文件打开模式详解1. 基本模式2. 扩展模式3. 模式组合示例 三、文件操作方法大全1. 打开和关闭文件2. 读取文件内容3. 写入文件内容4. 文件指针操作 四、文件编码处理五、二进制文件操作六、常见文件操作场景1. 文件内容…

用AI配合MCP快速生成n8n工作流

在数字化时代的浪潮中&#xff0c;AI技术正以前所未有的速度改变着我们的生活和工作方式。从智能家居到智能办公&#xff0c;从数据分析到自动化流程&#xff0c;AI的应用场景无处不在。今天&#xff0c;我们将带你走进一个充满创新与效率的世界&#xff0c;探索如何通过AI大眼…

ArkUI-X框架LogInterface使用指南

ArkUI-X框架支持日志拦截能力&#xff0c;Android侧提供原生接口&#xff0c;用于注入LogInterface接口&#xff0c;框架日志及ts日志通过该接口输出&#xff0c;本文的核心内容是介绍如何在Android平台上有效利用ArkUI-X框架的LogInterface拦截日志。 Android平台创建ArkUI-X…

函数重载与函数模板

函数重载与函数模板 函数重载 函数组成 返回类型 函数名称(参数列表){函数体}函数签名&#xff1a;函数名称(参数列表) C 允许定义同名函数&#xff0c;前提是它们具有不同的签名。这被称为函数重载 。 C 编译器通过检查调用中参数的数量、类型和顺序来选择要调用的适当函…