掘金 人工智能 08月14日
数字图像处理与OpenCV初探
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入浅出地介绍了数字图像处理的核心概念,阐述了图像在计算机眼中是如何由数值矩阵表示的。文章重点讲解了强大的跨平台计算机视觉库OpenCV,包括其丰富的语言支持、强大的功能和实时性。通过C++和Python的示例,详细说明了彩色图像与灰度图像的数据结构,以及如何高效地读取和修改图像像素。特别强调了通道顺序(BGR与RGB)和内存访问效率的重要性,并深入剖析了OpenCV核心数据结构cv::Mat的组成部分和操作方式,为读者提供了实用的工程实践指导。

📊 数字图像在计算机眼中是数值组成的矩阵,数字图像处理的目标是让机器能够“看懂”并分析图像,使其更清晰、有用和可识别,例如手机美颜、医学图像增强和车牌识别等都运用了图像处理技术。

🚀 OpenCV是一个功能强大、跨平台的开源计算机视觉库,支持C++和Python等主流语言,以其高实时性和丰富的功能(从图像读取到深度学习支持)成为业界和学术界的常用工具,特别是在需要高性能的实时应用和深度学习数据预处理方面。

🔀 彩色图像通常由BGR(蓝、绿、红)三个通道组成,而灰度图像则由一个通道表示。OpenCV在读取图像时默认采用BGR通道顺序,在使用matplotlib等其他库显示时,需要进行通道转换以确保颜色正确显示。

⚡ 在C++中,高效访问和修改图像像素的关键在于直接使用指针操作,例如通过`ptr(row)`获取行指针。遍历时应遵循内存连续性原则,按行遍历以提高缓存命中率,避免因遍历顺序不当导致性能下降。

🧩 cv::Mat是OpenCV的核心数据结构,包含了指向图像数据的指针(`data`)、图像尺寸(`rows`, `cols`)、每行字节数(`step`)、通道数(`channels`)和数据类型(`type`)等关键信息,并通过引用计数(`refcount`)支持内存共享和拷贝,是进行图像数据操作的基础。

什么是数字图像处理?

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

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

什么是OpenCV?

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

在我们的专栏中,我们的示例主要使用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++),如下:

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

每个通道亮度翻倍后图像

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

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

{    // 该代码运行效率会非常低,由于违背了内存连续性访问原则,// 导致频繁的缓存命中失败,严重降低数据访问效率!// 遍历每一列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代码片段,以适应不同应用场景需求。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

数字图像处理 OpenCV 计算机视觉 图像数据结构 C++ Python
相关文章