openCV开发学习

计算机视觉是一门研究如何使计算机通过分析图像和视频数据来「看」和「理解」世界的科学,更进一步来说,是指用 camera 和计算机代替人眼对目标进行识别跟踪和测量,旨在模仿甚至扩展人类视觉的能力,赋予机器识别、追踪、和解释视觉信息的能力。

第一章概念性的内容 ,尤其是对图像处理方法的分类,从属关系,概念厘清尚有不清楚的地方。

暂且保留,日后商榷。

第一章 计算机视觉概述

1.1 图像的基本概念

​ “图”是指物体透射光或反射光的分布,”像”是人的视觉对”图”的认识。”图像”是两者的结合,图像既是一种光的分布,又包含人的心理因素。

​ 数字图像有如下几个特点:

(1) 信息量大

​ 一张图片包含的像素数巨大,算上多通道以及视频的多帧内容,针对图片尤其是视频的数字处理可能需要庞大的算力消耗,为了降低运算量和运算成本,对数据的剪裁和化简成为一个在图像处理领域要考虑的重要课题。

(2)占用带宽大

(3)相关性大

​ 每幅图像中相邻像素之间很不独立,具有很大的相关性,很多时候相邻像素间具有相同或者接近的灰度。这代表图像信息的压缩潜力很大。 同理对于视频来说连续的两帧图像,在相同位置的像素通常不会剧烈变化,前后两帧图像的相关系数往往有0.95以上。

(4)非客观性

1.2 图像噪声

一般来说图像噪声是不可预测的随机信号,只能用概率统计的方式去认识。 噪声作用,存在于图像处理的输入、采集、处理和输出的全过程。

​ 图像噪声从来源上说,大致可以分为外部噪声和内部噪声:

​ 外部噪声指的是在系统外部出现的以电磁波为代表的干扰噪声,如充电设备或者天体放电等现象引入到系统的噪声。

​ 而内部噪声又可以分有四种: 1. 由于光和电的基本性质引起的噪声(如热噪声)2. 由于机械运动产生的噪声(如因接头抖动引起电流变化产生的 )3. 器件材料本身引起的噪声 4. 系统内部设备电路引起的噪声。

1.2.1 噪声滤除

​ 减少图像中的噪声的主要方法是图像平滑。合理合适的图像平滑可以显著改善图像质量,有利于对抽取对象进行分析。

​ 经典的方法是采用局部算子,对单一像素的局部小邻域的像素进行处理。近年来出现了一些新的技术,通过综合运用模糊数学理论、小波分析、数学形态学、粗糙集理论等理论,结合人眼的视觉特性进行图像平滑处理。

模糊数学理论(Fuzzy Mathematics),概率论(Probability Theory)和灰色系统理论并称为研究不确定性的三种方法。 概率论和模糊数学理论的基础都是集合,概率论建立在经典集合之上,而模糊数学理论建立在模糊集合上。对于模糊集合中的元素,我们无法绝对地认为该元素是属于还是不属于该集合,而是说该元素在多大程度上属于该集合,即模糊集合没有明确边界。

1982年波蘭學者Z. Paw lak 提出了粗糙集理論——它是一種刻畫不完整性和不確定性的數學工具,能有效地分析不精確,不一致(inconsistent)、不完整(incomplete) 等各種不完備的信息,還可以對數據進行分析和推理,從中發現隱含的知識,揭示潛在的規律。

​ 灰度图像常用的滤波方法主要分为线性和非线性两大类:

​ 线性滤波方法一般是通过取模板做离散卷积来实现,这种方法在平滑噪声时会导致图像模糊,损失图像细节信息。

​ 非线性滤波方法中应用最多的是中值滤波:中值滤波可以有效滤除噪声,同时也有较好的边缘保持特性,并且易于实现。但是因为是应用于全局的,中值滤波也会改变未受噪声污染的像素的灰度值,使图像变得模糊。 可能的改进方法比如可以在滤波处理的过程中加入一个判断的过程,根据噪声检测的结果判断输出在中值滤波,其他滤波器,或者原像素之间切换,于是这种有选择的滤波的方法,在一定程度上避免了不必要的滤波操作和图像的模糊。但是对于比较明亮或者较暗的图像,容易产生噪声的误判和漏判,影响了滤波效果和速度。

1.2.2 常见噪声及其滤除方法

高斯噪声

+++

​ 高斯噪声是由多种随机过程的结果叠加而成的,如电子电路的热噪声(Thermal Noise)、传感器暗电流(Dark Current)等。其会造成图像整体看起来像是有均匀分布的灰尘或者砂砾般的外观, 图像的边缘和细节可能变得不那么清晰。

​ 高斯噪声主要受高斯分布的系数 k 和方差 $\sigma $ 的影响(通常不考虑改变均值 $\mu$,默认为0)。因为高斯分布的性质,改变方差和改变系数效果相似,这里拿方差来具体分析:

噪声取不同值时的图片:

改变sigma

只改变sigma

噪声图片:

1_Guassion_noise.png

可以用高斯模糊滤波器(如高斯低通滤波器)或双边滤波器以有效减少高斯噪声,同时保持边缘信息。

椒盐噪声(脉冲噪声)

+++

​ 椒盐噪声是影像信号收到突如其来的强烈干扰产生,模数转换器或者 bit-flip 等等许多因素累加造成的,比如失效的传感器像素值为最小值,而饱和的传感器返回的像素值为最大值。 表现为图像中随机分布的暗点(椒)和亮点(盐)。这种噪声通常处于值域的极端,及最暗和最亮。

lenna
椒盐 lenna

椒盐噪声生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import cv2 as cv
import random
import matplotlib.pyplot as plt
import numpy as np


def sp_noise(image, prob):
output = np.zeros(image.shape,np.uint8)
thres = 1 - prob
for i in range(image.shape[0]):
for j in range(image.shape[1]):
rdn = random.random()
if rdn < prob:
output[i][j] = 0
elif rdn > thres:
output[i][j] = 255
else:
output[i][j] = image[i][j]
return output

src = cv.imread("lenna.jpg")
img = src.copy()


img_sp = sp_noise(img, prob=0.05)

cv.imshow("src", img)
cv.imshow("sp noise", img_sp)
cv.waitKey(0)
cv.destroyAllWindows()
cv.imwrite("lenna_sp.jpg", img_sp)

可以用中值滤波器对图像做平滑化处理,它可以替换图像中每个像素值为其邻域像素值的中位数。

泊松噪声(光子噪声)

​ 泊松噪声由光电转换过程中光子的随机到达过程引起,与光照水平有关。其表现依赖于图像的局部亮度,在亮区域,噪音水平更高。在亮区域,图像看起来会有更多的「颗粒感」或者「波动」,而在较暗区域,这种噪声不那么显著:

Poisson Noise

​ 因为在这种噪声呈现有规律的区域性分布,可以使用非局部均值去噪算法或泊松图像编辑技术来减少噪声。

1.3 图像处理

​ 图像图像处理从方法介质上说可以分为三类:光学模拟处理;电学模拟处理和计算机数字处理。

从形式上讲,都要用到计算机,它们之间没有明显的界限:光学模拟处理建立在傅里叶光学基础上,电学模拟处理通过把光强度信号转化为电信号,然后用电学的方法对信号进行加减乘除分割放大合成等处理。(模拟计算机?)而这里,本书本 tutorial 主要探讨讲解的就是计算机数字图像处理方法。

对于一个图像处理系统来说,流程上一般可以将处理分为三个阶段:1. 图像预处理阶段 2. 图像特征提取阶段 3. 识别分析阶段

而这里的预处理阶段尤其重要,如果处理的不好会直接导致后面的工作无法展开,而平滑处理滤除噪音和图像增强又是图像预处理阶段的重要步骤。

数字图像常用的处理方法有:图像变换,图像增强,图像分割,图像描述,图像分类(识别),图像重建。 它们并不是严格的并列关系,常常共存于图像处理的全流程中,互为上下游关系。例如: 图像增强广泛应用于图像预处理阶段,而图像描述分类识别通常已经是最后的阶段了。

1.3.1 图像变换

图像处理大致上分有空间域和频域变换处理两种。

​ 空间域变换就是直接在图像上对像素矩阵进行几何变换或者仿射变换,会在后续章节展开。

​ 频域变换利用傅里叶变换、沃尔什变换、离散余弦变换等技术将空间域的处理转换为频域等变换域进行处理,可以有效减小计算量, 提高处理效率(比如在频域下更方便进行数字滤波处理),常用于光学成像处理。

沃尔什变换(Walsh Transform)是在频谱分析上作为离散傅里叶方案的一种替代方案。

因为傅里叶变换中,资料向量(原始信号或者处理目标对象)必须要乘上复数系数的矩阵处理,并且每个复系数的实部虚部是三角函数,因此大部分的系数都是浮点数,而复数浮点数的运算复杂度高,误差大。而在沃尔什变换中主修要乘以实数矩阵且系数绝对值为1,使得我们不需要去做复数浮点数的乘法运算,moreover 因为这个绝对值大小相同的性质,所以不需要乘法只需要做加法就可以。

使用离散傅立叶变换相当于把信号拆解成在不同频率的正弦函数与余弦函数的分量,而使用沃尔什转换相当于把信号拆解成在许多不同震荡频率的方波上,因此,除非所要分析的信号拥有类似方波组合的特性,使用沃尔什转换作频谱分析的效果会比使用离散傅立叶变换分析的效果要差,这是降低运算复杂度所要付出的代价。

离散余弦变换(Discrete Cosine Transform, DCT)也是一种只用实数的类似离散傅里叶变换:常用于信号处理和图像处理,对信号和图像进行有损数据压缩。 (这是由于离散余弦变换具有很强的”能量集中”特性)

1.3.2 图像增强

图像增强的目的是为了突出图像中感兴趣的部分,没有统一的标准,要么是为了提高图像质量,要么是为了方便计算机处理。

根据信息的不同,图像增强可以分为边缘增强、灰度增强、色彩饱和度增强等等。

根据处理过程所在的空间不同,可以分为空域计算和频域计算:

图像增强

1.3.3 图像分割

​ 图像分割就是将图像中有意义的特征部分提取出来,包括图像中的边缘,区域等,可以在图像预处理中应用去划定感兴趣区域(Region of Interest, ROI),这是进一步图像识别分析和理解的基础。

​ 语义分割是处在图像分割和图像识别边缘的概念,可以说是图像理解的基石性技术:图像是由许多像素组成,而语义分割就是将像素按照图像中表达语义含义的不同进行分组(Grouping)、分割(Segmentation)。这是一个自动驾驶中的图像语义分割示意,机器能够自动分割并识别出图像中的内容,比如给出这张公路上的照片, 机器判断后能做出标注,红色标注是人,蓝色是车。

语义分割 基于深度学习的图像语义分割技术虽然可以取得比传统方法好得多的分割效果,但是其对于数据标注的要求过高:不仅需要大量的图像数据用作训练,同时这些图像还需要提供精确到像素级别的标注信息(Semantic Labels)。因此越来越多的研究开始将注意力转移到弱监督条件下的语义分割上,图像仅需要提供图像级别的标注(比如,有「人」有「车」没有「动物」)。

1.3.4 图像描述

​ 图像描述是图像识别和理解的必要前提。对于简单的二值图像 ,可以采用几何特性描述物体特性,可以分为有边界描述和区域描述两类方法。对于特殊的纹理图像,还可以用二维纹理特征进行描述。 随着图像处理研究的深入发展,已经开始进行三维物体描述的研究。

图像描述(image caption)用简单的话就是说,输入给模型一张图像,模型输出是一句能够描述图像场景的文本句子。
比如下面那张「鸟」的图片,模型就会输出 “a bird flying over a body of water.” 至于是中文的还是英文的,就取决于手头的数据集了。

img

1.3.5 图像分类(识别)

​ 图像分类(识别)在近年来伴随着深度学习的广泛应用迎来了较大的发展。总的来说就是图像经过某些预处理后进行图像分割和特征提取,据此进行分类。图像分类使用模式识别(pattern recognition)方法。

什么是模式识别


​ 模式识别是指人类的一项基本智能,在日常生活中,人们经常进行”模式识别”。随着计算机的出现和人工智能的兴起,希望用电脑来代替或扩展人类的脑力劳动的需求日益迫切。电脑的模式识别在 20 世纪 60 年代迅速发展为一门新学科。

​ 模式识别是指对表征事物或现象的各种形式的信息(数值的、文字的甚至逻辑关系的)进行处理和分析,并据此对事物或现象进行描述,解读的过程。

模式识别的问题分类


模式识别面对的问题从解决问题的方式上大致可以分为有监督的分类(Supervised Classification)和无监督的分类(Unsupervised Classification)两种。这两者的主要差别在于实验样本所属的类别是否已知。一般来说,有监督的分类需要提供大量已知类别的样本,但在实际场景中,这是存在一定困难的,因此研究无监督的分类就变得十分有必要了。

什么是有监督和无监督?

有监督的过程为先通过已知的训练样本(已知输入和对应输出)来训练, 从而得到一个最优模型,再将这个模型应用到新的数据上,映射为输出结果。

而无监督相比于有监督,没有训练的过程,而是自己去拿数据进行建模分析。这个过程听起来不思议,但是我们自身在认识世界的过程中也会经历无监督学习的过程:哪怕对艺术一无所知,在对一系列美术作品欣赏后也能对作品进行一个大致的分类。

  1. 有标签 Vs. 无标签

    有监督的核心是分类,无监督的核心是聚类(将数据集分成由类似的对象组成的多个类)。

  2. 分类 Vs. 聚类

    有监督的工作是选择分类器和确定权值,无监督的工作是密度估计(寻找描述数据统计值),也就是无监督算法只需要知道如何计算相似度就可以了。

  3. 同维 Vs. 降维

    有监督的学习如何输入是 n 维,特征即被认定为 n 维,通常不具有降维的能力和需求。而无监督经常要参与深度学习做特征提取,或者干脆采用顶层聚类或者项聚类以减少数据特征的维度,比如用主成分分析(Primary Content Analysis, PCA)或者奇异值分解(Singular Value Decomposition, SVD )去压缩数据,之后这些数据可被用于深度神经网络或其他监督式学习算法。

  4. 分类时定性 Vs. 先聚类后定性

    有监督学习分类同时定性,无监督学习先聚类后定性质(打标签)。

  5. 独立 Vs. 非独立

    独立分布数据更适合有监督,非独立数据更适合无监督。

  6. 不透明 Vs. 可解释

    有监督算法只返回根据学习计算得到的参数作为分类(Classification)的根据,但没有原因;无监督算法可解释聚类(Clustering)的原因,在不同的特征有多少的一致性,所以才被聚类为一组。于是可以根据这些一致性进一步将其总结成规则,在这个过程中聚类的原因昭然若揭。

​ 什么是聚类,聚类是什么样的 开坑

模式识别的方法


​ 有监督或者无监督是由模式识别所要处理的问题的性质,是否有预先已知的分类标签直接决定的。

​ 而不管哪种方式,模式识别的处理思路主要有 3 种:

a. 决策理论方法

​ 又称统计方法,是发展较早也较为成熟的一种方法,是我们这里讨论的重点,数字图像处理的起点。流程是首先数字化被处理对象,然后进行预处理,随后进行特征抽取,特征抽取后可进行分类,最后通过鉴别函数比较实行分类。

  1. 所谓特征抽取是选择一种度量,对一般的变形和失真保持不变或几乎不变,并且只含尽可能少的冗余信息。 特征抽取过程输入模式从对象空间映射到特征空间。这时,模式可用特征空间一个点或一个特征矢量去表示。这种映射不仅压缩了信息量,而且易于处理分类。
  2. 特征空间映射到决策空间,引入鉴别函数,由特征矢量计算出相应属于各类别的鉴别函数值,通过鉴别函数值的比较进行分类。
b. 句法方法

​ 又称结构方法或者语言学方法, 其基本思想在于把一个模式层层分解 ,最终描述为最基本的子模式的组合,最终形成一个树状的结构描述(NN)。模式由一组基本元(最小子模式)和他们的组合关系来描述,称为模式描述语句。这和语言中句子由短语搭配语法粘合,短语由字词搭配语法构成一样,所以称为句法方法。基本元合成模式的规则,由所谓语法/句法来指定。

c. 统计模式识别

​ 统计模式识别(Statistic Pattern Recognition)的基本原理是:有相似性的样本在模式 中互相接近,并形成 “group” 。统计模式识别的主要方法是:判别函数法、近邻分类法、非线性映射法、特征分析法、主成分析法等

​ 在统计模式识别中,贝叶斯决策从理论上解决了最优分类器的设计问题, 但实施却必须面对更麻烦的概率密度估计问题。现代统计学习理论(VC理论)新的学习方法,支持向量机(Support Vector Machine, SVM)。

1.3.6 图像重建

​ 成像领域概念 ,通过一些低维渠道获取的信息,根据一些先验知识和计算尝试重建成像对象的原本结构,比较成功的例子有 CT(Computed Tomography) 技术。

第三章 OpenCV 基本操作

3.1 OpenCV 与 Python

​ OpenCV 针对不同的应用划分了不同的模块,每个模块专注了不同的功能。在模块下的函数和变量都在命名空间 cv 下。

​ cv2.cp310-win_amd64.pyd 文件提供了 opencv-python 大部分的功能。

​ 其中 cv2 并不代表 OpenCV 的版本号是 2。OpenCV 是基于 C/C++ 的,而 “cv”,”cv2” 表示的是底层 C API和 C++ API 的区别, 这里的 cv2 表示使用的是 C++ 的 API。

​ cp310 代表该库是专为 Python 3.10版本编译的,其中 cp 代表 cPython,它是 Python 的官方和最常用的实现。

​ amd64 代表该库是为 Windows 操作系统的 x64 架构编译的。因为 AMD 是第一个设计并且实现这种扩展传统 32 位 x86 架构到 64 位的公司,虽然稍后 Intel 也采用了这种架构,但 amd64 的名称已经被广泛使用在文件名或者库名称中指代所有 x86_64 的处理器。

.pyd文件是 Python 在 Windows上使用的动态链接库,通常是用 C 或者 C++ 编写并编译成与 Python 兼容的二进制格式,然后通过 Python 的 C 扩展机制导入和使用。这样,Python程序就可以调用这些编译后的二进制代码。这种方式使得 Python 程序能够执行一些底层操作的任务,同时保持Python代码的简洁性。

Python 不愧为能够「黏合」用各种语言写成的程序块的 「胶水语言」。

3.2 图像的输入和输出

3.2.1 读

​ 函数 imread 可以从指定的文件路径读取图像。支持多种读取模式和常见的图像格式。

路径:


​ 图像路径支持相对路径和绝对路径。不管是绝对路径还是相对路径,都可以用到斜杠(slash)、双斜杠(double slash)、双反斜杠(double backslash),唯独不可以用反斜杠(backslash),因为在 Python 字符串中反斜杠是用作转义符的前缀,可能会导致路径错误或者无法正确解析。

​ 但是 imread 的文件路径不支持中文字符,如果要支持中文路径,就可以用函数 imdecode

imread 只支持文件的路径字符串, 而 imdecode 支持字节串形式的从文件 、数据库或网络来源读取到的原始数据, 放入缓存区 buf,例如:

1
2
#	cv.imdecode(buf, flag)
img = cv.imdecode(np.fromfile(imgPath, dtype=np.unit8),-1)

​ 这里因为 buf 参数支持更广,fromfile 支持中文路径,他直接读取(比如包含中文)路径,在内存中构造数组。

​ 本质上说,选择imread还是imdecode取决于图像数据是存储在文件中还是已经作为字节串存在于内存中。

支持的文件格式

​ imread 支持常见的图像文件格式

JPEG/JPGPNGBMPTIFFRASPP(B/G)M

​ 值得注意的是,和 Linux 系统特性一样,函数 imread 是根据文章内容而不是扩展名来确定图像的类型比如把一个 bmp bitmap 图像文件后缀改为 mp3,imread 依然能够检测到这个文件是 bmp 文件。

读取模式


​ 读取模式中,常见的 flag 的取值有:

cv.IMREAD_UNCHANGED 默认值是 -1,不加任何改变地加载原图

cv.IMREAD_GRAYSCALE 默认值是 0,图像转换为 1 通道灰度图

cv.IMREAD_COLOR 默认值是 1,图像转为 RGB 3 通道彩色图

cv.IMREAD_ANYDEPTH 默认值是 2, 若载入的图像深度为 16 或 32 位,就返回对应深度的图像,否则转换为 8 位返回

具体更多内容可见于document

返回值


​ 如果读取图像失败会返回 none, 可以通过判断返回是否为 none 来判断函数是否读取正确

​ 实际上 cv.imread 的返回值是 Numpy 的 array 数组,它包含了每个像素点存储的数据,因而我们可以访问,改动数组值来对每一个像素点进行具体的更改。

​ 对于矩阵就要有行列两个属性,这正对应了图像的高度和宽度。可以用 shape 函数得到矩阵的具体尺寸,shape[0] 存放矩阵的行数, shape[1] 存放矩阵的列数,shape[2] 存放矩阵的通道数。

这也就是为什么 Numpy,包括 Pillow 习惯以左上角为原点,x 向右为正 y 向下为正。与惯常描述图片,以左下角为原点,经典笛卡尔坐标系,左边原点在左下角, xy 向右向上为正的 Matplot不同,这点要注意可能需要转换。

​ 这里说通常在图像处理会有三个维度,但也有例外,例如

在视频处理领域,可能会要用到四维数组,形式为(帧数, 高度, 宽度, 通道数),其中每一帧都是一个三维的图像数据。

遥感、医学成像等领域,可能会使用多光谱或超光谱图像,其中图像包含大于3个的通道,用于存储不同频率的信息。这种情况下,图像数据的维度为(高度, 宽度, 通道数),其中“通道数”可能远远超过3,但它仍然被是三维数据,只是每个像素点包含更多的信息。

3.2.2 写

​ 函数 imwrite 保存图片,可以将图片写入到指定的路径,支持多种文件格式,但一定要加上文件后缀名。

​ 但是保存 BGRA 图片时只有 PNGgifTIFF 还有 PSD 支持 Alpha 通道。

BGR 和 RGB 没有本质区别, OpenCV 和 PIL 读取写入图片的格式分别是 BGR 和 RGB。

A 代表在此基础上加上 Alpha 通道,代表透明度

还有 imshow 函数用作显示。

3.3 OpenCV 界面编程

​ 所谓界面编程就是对窗口和相关事件直接进行操作处理的过程,主要由 High-level-GUI(高层次图形用户界面)模块支持

3.3.1 窗口

​ 函数 namedWindow 用来新建窗口

1
namedWindow(winName[, flags])

​ winName 代表窗口名字,flags 存放窗口的标识,比如窗口自适应图片大小不可手动更改,用户可以手动改变窗口尺寸,或者创建窗口支持 OpenGL(Open Graphics Library,开放图形库)。

​ imshow 函数用于显示窗口,但是 imshow 函数需要配合 waitKey 函数,否则窗口将一闪而过,只停留一瞬。

1
waitKey([delay])

delay 表示一个延时值,单位为 ms,默认为 0。当 delay $\leq$ 0 时,函数 waitKey 无限期地等待一个用户按键事件,当 delay $\gt$ 0 时,则等待 delay 毫秒。如果在指定时间之前没有按下任何键,就返回 -1,如果在制定时间有按键事件,则返回该案件的对应值。

感觉这是一个在规定时间读取用户一次按键事件的函数,还能够返回按键值,但常常被用作,在一段时间或用户按键前,卡住程序,什么也不做。

3.3.1 单窗口多图片

​ 因为在 OpenCV 图像处理中,图像的本质是数组,根据这一特性利用 hstack 函数就可以对数组堆叠,从而实现一个窗口显示多个图像。

​ 但是直接用 hstack 只能把行数相同的数组从左往右排列起来,有 hstack 水平堆叠 ,还有 vstack 垂直堆叠,我们可以通过 hstack 和 vstack 的组合,实现想要的效果。

1
2
# 但是不管是 hstack 还是 vstack 都只能处理行数/列数相同的图像,可以活用 resize 调整帧的大小以匹配
frame2 = cv2.resize(frame2, (frame1.shape[1], frame1.shape[0]))

​ 然后再合理先后搭配 hstackvstack 去实现想要的组合效果。

3.3.2 销毁窗口

​ 利用函数 destroyWindow 和destroyAllWindows 实现。

​ 所有的图片窗口会在程序解除占用,waitKey 全部结束后自动关闭,但是如果有多个视频窗口并且我们不希望它持续显示,可以用 destroy 清除。

3.3.1 鼠标事件

​ OpenCV 提供了 setMousecallback 作为设置鼠标回调单数

1
2
3
4
# 声明内容 setMousecallback(windowName, onMouse, para=none)
# onMouse 是鼠标事件响应的回调函数指针(鼠标事件返回的参数给这个函数用); para 是传给回调函数的可选参数
def mouseCallback(event, x, y, flags, param)
# 其中 event 表示鼠标事件, x,y 表示鼠标事件的坐标, flag 是标志,param 是可选参数,这个函数名可改

这是一个调用鼠标事件函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cv2 as cv
import numpy as np

img=np.zeros((1200,1200))

def draw_circle(event,x,y,flags,param):
if flags==cv.EVENT_FLAG_LBUTTON :
cv.circle(img,(x,y),20,255,-1)

cv.namedWindow('img')
cv.setMouseCallback('img',draw_circle)
while(1):
cv.imshow('img',img)
n=cv.waitKey(5)
if n==ord('q'):
break
elif n==ord('s'):
cv.imwrite("draw.jpg",img);
print("succeed!")

cv.destroyAllWindows()

3.3.1 键盘事件

waitKey

另外 Python 有提供pygamepynput 等库可以监听键盘事件行为。

3.3.1 滑动条事件

类似于鼠标事件

OpenCV 提供了 创建滑动条及其相关函数

1
2
3
4
5
6
7
# 声明内容 createTrackbar(trackbarName, windowName, value, count, onChange)
# value 是滑动条值的内容,count 是最大值的内容,onChange 是是回调函数指针,按钮移动时回调函数自动调用
def trackbarCallback(pos, userdata)
# pos 是滑块当前位置,userdata 是传给回调函数的可选参数
# 也有 GetTrackbarCallback(trackbarName, windowName)
# GetTrackbarCallback(trackbarName, windowName)
# 这两个函数可以方便设置和获取滑动条的位置

这是一个调用滑动条函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import cv2 as cv
import numpy as np

def updateImage(x=None):
# 使用全局变量
global img, img2, alpha, beta
# 从滑块读取alpha和beta的值,并进行适当的缩放和转换
alpha = cv.getTrackbarPos('Alpha', 'image') * 0.01
beta = cv.getTrackbarPos('Beta', 'image')
# 调整图像亮度和对比度
img = np.clip((alpha * img2 + beta), 0, 255).astype(np.uint8)
cv.imshow('image', img)

# 初始化参数
alpha = 0.3
beta = 80
img_path = "test.jpg"
# 读取图像
img = cv.imread(img_path)
img2 = img.copy()

# 创建窗口和滑块,同时设置回调函数
cv.namedWindow('image',cv.WINDOW_NORMAL)
cv.createTrackbar('Alpha', 'image', int(alpha*100), 300, updateImage)
cv.createTrackbar('Beta', 'image', beta, 255, updateImage)

# 使用初始参数显示图像
updateImage()

# 主循环
while True:
if cv.waitKey(1) == 27: # 按Esc键退出
break

cv.destroyAllWindows()

np.clip 截断式限制

在Python中,如果预期函数有时会被事件触发器(如OpenCV的滑块回调)调用,有时又可能直接被其他函数或主程序调用,而这两种情况下参数的需求不同,通常会给函数参数一个默认值。在updateImage函数中使用x=None作为参数就是这样一个例子。这里x参数是为了兼容OpenCV的回调函数签名,它在滑块移动时被自动传入(尽管在函数内部并未使用)。给x一个默认值None,意味着你也可以在不提供任何参数的情况下直接调用updateImage函数,这在初始化或需要手动触发更新时很有用。

第四章 数组矩阵

4.1 Numpy

​ 标准的 Python 可以使用 list(列表)当做数组使用,但由于 list 的元素可以是任何对象,因此 list 中所保存的是对象指针:例如保存 [1, 2, 3, 4] 就需要有 4 个指针和 4 个整数对象,会导致效率降低。

​ 也可以使用 array 模块的 array 对象作为数组,它不需要指针可以直接保存数值,但是不支持多维数组,并且不包含能够做数值运算的相关函数。

​ 在这一背景下,Numpy 包应运而生(不知道是不是这么生的)。它克服了上述两种的不足,它提供了ndarrayfunc分别用来存放相同数据类型的多维数组和提供对数组进行运算处理的函数。

这里我们需要明晰包,模块,对象等概念的关系

对象(Objects)

​ 对象是 Python 中所有数据的基础。在 Python 中,一切皆对象,所有的数据类型都是以对象的形式存在的,例如现在创建了一个列表 newList = [1, 2, 3],这里的 newList 就是一个列表对象 ,拥有列表的所有属性和方法(继承?)。

类(Classes)

​ 类是创建对象的蓝图或模板,定义了对象的属性和方法。类支持面向对象编程的核心概念,如封装、继承和多态。列表,数组等是 Python 的自有类,我们也可以自定义新的类以供使用。


函数(Functions)

​ 定义一段可执行代码的块,函数可以接收输入参数并返回结果。

模块(Modules)

​ 模块是包含 Python 定义和声明的文件,文件名就是模块名加上.py 后缀。模块可以包含函数、类、变量定义和可执行代码。 如一个main.py 代码可能包含数学运算的函数定义

包(Packages)

​ 包是一种包含多个模块的命名空间。Python中的包是一个含有__init__.py文件的目录,该文件定义了包的属性和方法。如 numpyscipy 是科学计算领域广泛使用的两个包,它们包含了多个模块,提供了大量数学和统计函数。

命名空间(Namespaces):一个包含变量名与对象之间映射关系的容器。

4.2 数组

ndarry 对象有提供丰富的参数支持广泛的需求,可以指定数据类型,对象是否需要复制,行方向或者列方向,指定生成数组的最小维度。

例如:

1
newArray = np.array([1, 2, 3, 4, 5], ndmin = 2, dtype = complex)

数据类型

​ numpy 内置支持了差不多与 C 完全对应的数据类型。

Tips:

int8、int16、int32、int64 可以使用字符串 ‘i1’、’i2’、’i4’、’i8’ 来代替

同理,字符串b, i, u, f, c, m, O, S/a, U 分别对应了布尔型,有符号,无符号整形,浮点型,复数浮点型,Python 对象,字符串,Unicode

dtype 可以用来结构化数据类型:

1
2
3
4
5
6
7
dt = np.dtype(['age', np.int8])
age = np.array([(10,), (20,), (30,)], dtype = dt)
print(age)
print(age['age'])

# result: [(10,)(20,)(30,)]
# result: [10 20 30]

数组属性

数组对象还有一些属性可以直接返回

比如 .ndim 是数组的秩,.shape 是数组的维度, .size 元素总个数,还有数据类型,每个元素大小,实部虚部,缓存区。

创建数组

主要有三种办法创建数组:

第一种是直接新建一个新的数组

np.empty, np.zeros, np.ones,分别是创建一个未初始化的数组,一个元素以 0 来填充的数组,一个元素以 1 来填充的数组。

​ 它们有共同的三个参数输入,shape,dtype 和 oder,shape 表示数组形状(占位),dtype 表示数据类型,optional,order 表示行优先(C)或者列优先(F),默认行优先。

第二种是通过数值范围创建数组

​ 这种方法和上一种类似,有 np.arrange(start, stop, step, dtype) ,start 默认 0, stop 代表终点(not include),step 默认 1,dtype 默认 auto

​ 还有 np.linspace(start, stop, num=50, endpoint=True,retstep=False, dtype=None) ,没有特殊说明,起始终止点全部包含,前述赋值为默认值。

​ 还有np.logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None) 创建等比数列, 其实是指数函数,base 是底数。

也可以通过 np.asaerray 来继承已有的数组,参数同上新建数组的方式,只是数组形状换成了要继承的来源,可以是列表,列表的元组 ,元组,元组的元组,元组的列表和多维数组。

也有类似的方法如 np.frombuffernp.fromiter

切片和索引

可以用 slice() 或者直接在数组上以 [start:stop:step] 的形式切,stop(not include)

其中可以用: 代表一个区间的元素,如果只有: 就可以代表全部, 比如 arr[0, :] 代表数组 arr 的第一行的所有元素。

甚至可以用数组索引,可以在数组内定义想要索引的坐标,也可以用数组分别存放行,列坐标:

1
2
3
img = np.array([[]])
points = img[[y1, y2, y3], [x1, x2, x3]]
# points 就包含了在 x1y1,x2y2,和 x3y3 处的点

还支持布尔索引,就是比如:

1
2
3
4
5
6
7
8
9
10
11
12
# 只有np。array 才有资格使用布尔索引
x = np.array([, , , , , ])
y = np.array([, , , , , ])

print(newArray[x == y])
print(newArray[x >= y])

# 还可以用来过滤 NaN,复数等
valueArray = np.array([])#
filteredArr[~np.isnana(valueArray)]
np.iscomplex(valueArray)

花式索引(Fancy index)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

newArray = np.arange(32).reshape(8, 4)
print(newArray[[4, 2, 1, 7]])

# results:
#[[16 17 18 19]
# [ 8 9 10 11]
# [ 4 5 6 7]
# [28 29 30 31]]
# [Finished in 272ms]
# 可以看到按第 4, 2, 1, 7 行的形式显示了
# 负数表示倒序索引
# np.ix_ 传入多个索引数组

数组迭代

nditer 可以对数组进行迭代,简单来说就是按照标准 C 的顺序(行优先)的顺序对数组遍历输出,但这个顺序是以内存为准,也就是说第一次存入的状态决定了以后的迭代结果

比如:

1
2
3
4
5
6
7
8
9
10
11
12
arr =np.range(6).reshape(2, 3)
for num in np.nditer(arrayA.T):
# 虽然转置了,但得到的结果仍然是 0, 1, 2, 3, 4, 5,因为这个顺序在声明的时候就已经在内存上写好了
# 后天改变,转置,不会影响
# 但是如果重新分配内存,比如

for num in np.nditer(arr.copy(order="C")): # C 表示 row-major order 或者说 C-order
# 结果就应该是 0, 3, 1, 4, 2, 5

# 在参数中也可以强制顺序
for num in np.nditer(arr.T, order="F"):

这与 Python 中的 for element in arr.flat: 或者 for i in range(arr.shape[0]): for j in range(arr.shape[1]) 功能几乎一致,但 np.nditer 是 C 实现,在遍历(traversal)大的数组时,相比于纯 Python 方法实现,会有明显的效率优势。

数组操作

np.reshape 改变形状,个数必须维持不变

np.ndarray.flat元素迭代器,同前述 nditer

np.ndarray.flatten 类似于 flat + copy

np.ravel 展平数组元素,不同于nditer 的是,转置或者改变原数组,得到的 ravel 结果也改变

np.transpose 对换 x y 维度(转置),默认其实是np.transpose(1, 0),也可以简写为 np.ndarray.T

​ 三维数组的转置有 `np.transpose(1, 0 ,2) 012, 021, 210, 201 等可能

np.rollaxis 选取特定的轴向后滚动到特定位置。

np.swapaxes 用于交换两个轴,类似 np.transpose ,但只能交换两个轴。

第五章 图像处理模块

5.1 颜色变换和画基本图形

颜色变换

可以通过 cv.cvtColor() 进行色彩空间的转换

基本图形

画点是对一个或者多个索引到的像素点赋值

rectangle 根据对角线上两个顶点绘制矩形,可以定义线条颜色 ,粗细

注意包括这里,很多以这种方式去定义的矩形都只能定义正矩形,就是说矩形的长宽需要和图像的长宽平行

circle 根据圆心和半径画圆,其他配置同上

ellipse 可以画椭圆,也可以只取其中一段弧,有长轴,圆心,偏转角 ,起始结束角等定义,可以看官方文档

line 画线段,方式与矩形绘制相同

polylines 绘制多条线,有 isClosed 参数决定是否闭合,可以用来绘制多边形

fillPoly 填充封闭图形,除了默认封闭,不需要 isClosed参数外与绘制多条线基本相同,另有 offset 表示等高线所有点的偏移,这两个函数输入参数的点集顺序敏感

5.2 文字

putText(img, text, org, fontFace, color[, thickness[, lineType[, bottomLeftOrigin]]]) -> None

text 是文字,字符串形;org 是文字的左下角点坐标,fontFace 字体;lineType 表示线型(4 邻域或 8 邻域,默认8)

还可以用 getTextSize 有返回值,得到位置和文本框大小

5.3 为图像加上边框

​ 使用 cv.copyMakeBorder(src, top, bottom, left, right, boarderType[, dst[, value]])

其中,src 代表输入的原图像,随后的四个参数代表原图像在上下左右四个方向上扩充多少像素, borderType 表示边界类型,取值有复制最边缘像素,以最边缘像素为轴对称填充,以一个常量像素值(value 变量)填充扩充的边界等。

5.4 在图像中查找轮廓

​ 轮廓查找函数 cv.findContours 就可以检测物体的轮廓,但是输入的 image 参数必须是单通道图像矩阵,可以是灰度图,但更常用的是二值图像。 这是一个应用领域非常窄的函数,要求严苛,泛用性差 ,稍微复杂的轮廓就需要边缘检测方法。

​ 另外,因为检测轮廓后常需要表示,把它绘制出来,有搭配的轮廓绘制图像 cv.drawContours(), 参数可选外轮廓还是内轮廓。

第六章 灰度变换和直方图修正

6.1 点运算

​ 在前面我们说过,图像增强在空域上主要有点运算和模板运算两种。

点运算或称点处理(Point Operation)是对全局每个点做处理映射,主要有灰度变换和直方图两种方法

6.2 灰度图

​ 在图像处理的领域中,处理对象有两个,一个是色彩,一个是轮廓。这里主要针对图像轮廓中包含的信息做处理,色彩变换处理和重建是另一个领域和学科。

​ 因为我们只关心,图像本身, 这时多通道的色彩对我们来说就是冗余信息无关紧要。灰度化就是要优先剪裁掉这些冗余信息。

6.2.1 灰度化

​ 因为色彩不重要,我们要的只是轮廓信息,所以得到灰度图像也有好几种方式。

加权法

​ 根据 RGB 色彩理论,白色是由等量的三原色叠加出来的,所以灰度图的灰度值是可以由多通道的色彩值,等量叠加得到的。但大量的实验数据表明,人眼对于不同频率(色彩)的光的敏感程度并不相同,所以标准的灰度亮度可以表示为:

RGB luminance value = 0.3 R + 0.59 G + 0.11 B

这个公式称为经验公式,是通过已有的大量经验总结得出

RGB color

As seen on a grayscale screen

极大值法

+++++

直接取 RGB 中的最大值得到灰度值

RGB luminance value = max(R, G, B)

平均值法

+++++

取 RGB 中的最大值得到灰度值

RGB luminance value = (R + G + B) / 3

​ 那么在实际表现来看,他们有什么区别呢

这里用基本的色块来做测试,在横纵两个坐标上分别设置三个块,分别赋予 BGR 三个通道 127 的值叠加,得到的三通道图如下所示:

color_block

分别测试直接 cv.imread 传入灰度图参数和三种灰度化的方式得到的灰度图对比,左上为直接调用 cv.imread,右上为根据经验方程,带有权重的灰度化结果,左下角为最大值法,右下角为平均值法。

combined_window

同理,测试 lenna 图的结果

lenna_grey

可以看到如果是平均值法对比度降低,最大值法对比度有提升(颜色深),经验分布更符合人眼所见。

速度优化

​ 灰度变换需要遍历图像上的每一个像素(点运算),再去做运算转换成灰度图是非常耗时的。可以考虑的策略有: 1. 将浮点运算转换为整数运算比如 0.3 改成 300/1000 的形式。 2. 因为在整数运算时不得不引入除法,可以把除法问题转化,利用移位来实现,二进制移位的速度要快得多。3. 对于计算机视觉来说,图像的精度通常来说不重要,可以在一定程度上损失精度来降低运算量

OpenCV 采用高度优化的 C++ 代码,可能还有针对特定硬件的优化,如使用 SIMD 指令集(如 SSE2、AVX2)或多线程处理,来进一步加快颜色空间转换的速度。此外,OpenCV 也支持通过 OpenCL 加速图像处理运算,这在配备了兼容 GPU 的系统中可以显著提升性能。

这种转换不仅限于单个像素级别的操作,而且在整个图像数组上高效执行,使得即使是大型图像也能快速转换。这是为什么在实时视频处理和复杂图像分析应用中,OpenCV 能够提供强大支持的原因之一。

6.2.2 灰度变换

​ 如果在拍照时曝光不足或者曝光过度,会导致输入图像亮度的动态范围变小,直观表现就是照片显得灰蒙蒙或者发白。

​ 灰度变换是图像处理最基本的方法之一,可以使图像的动态范围增大,对比度增强,图像清晰,特征明晰。

​ 简单的灰度变换有灰度的线性变换和分段线性变换,而复杂一些的是灰度的对数或指数变换等:

线性变换可以在整体上对图像进行一个变换,分段线性变换把原图的灰度范围分为两段或者更多段,对感兴趣的区域或者灰度区间进行增强,而对其他不感兴趣的进行抑制。所以分段线性变换常用于红外图像的增强。

对数变换,顾名思义是在灰度上进行对数形式的变换,它可以增强图像暗部的细节。

幂律变换,也称伽马变换或者指数变换,就是形式为指数的灰度变换,形如:

$s = c * x^\gamma$

指数变换

$\gamma$ 通过取不同的值,也会有不同的效果,可以达到增强低灰度或者高灰度部分细节的作用。

6.3 直方图修正

6.3.1 直方图

把图像内每一个灰度值的像素数都统计出来,然后把该像素数和像素值的关系以直方图的形式呈现出来,也可以用像素数出现的频率而非数量。

​ 直方图的计算在 opencv 有通用的函数可以同时计算多个图像,多通道,不同范围的直方图:

calHist([images], [channels], mask, hiistSize, ranges[, hist[, accumulate]]) -> hist

images 表示图像矩阵数组,这些图像必须有相同的大小,深度和通道数。因为函数支持计算多张图像的直方图,所以即使只有一张图像,也需要放到一个列表中。

channels 表示一个通道索引的列表。

如果只是要处理单张灰度图,那可以有更为简单的方法去实现:

plt.hist(img.ravel(), 256, [0, 256]) 其中 np.ravel 代表把图像矩阵铺平,然后用 plt.hist 就可以直接对一组一维数组画直方图

具体的 BGR 直方图如下例:

1
2
3
4
5
6
7
8
9
10
11
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread("test.jpg", 1)
color = ("b", "g", "r")
for channel, color in enumerate(color):
histr = cv2.calcHist([img], [channel], None, [256], [0, 256])
plt.plot(histr, color=color)
plt.xlim([0, 256])
plt.show()

6.3.2 直方图修正

​ 直方图修正,或者说直方图均衡化,是指把原始图像的灰度直方图从比较集中的某个灰度区间,变成在全部灰度范围内的均匀分布。

​ 基于信息熵的概念,当一个图像的直方图均匀时,这意味着图像的每个像素出现的频率几乎相同,那么我们可以认为这个图像的直方图是均匀分布的。在这种情况下,每个像素值的出现都不携带特定的预测性信息,因此每个像素都为图像贡献了最大的信息量。

​ 信息熵(信息论)

香农在寻求一种量化信息内容的方式时,希望找到一个度量,它能满足以下几个关键条件:

  1. 概率性:如果一个事件的发生是确定的(即概率为 1),那么它不应携带任何信息(熵为 0)。相反,如果事件的发生完全不确定,则应携带最大的信息量。

  2. 可加性:如果两个事件是独立的,那么它们带来的信息量应该是各自信息量的总和。

具体公式: $H(X) = -\sum_{i=1}^n p(x_i) \log_b p(x_i)$,这里 $p(x_i)$ 是概率

直方图进行均衡化的过程中必须要满足条件:1.像素无论怎么映射,应当保证像素值或灰度值的大小关系不变,较亮的区域依旧是较亮的,暗的区域依旧是较暗的,只是对比度增大,不可以明暗颠倒,2. 值应该处在值域内不能越界。

​ 累积分布函数(Cumulative Distribution Function, CDF)作为变换函数是个很好的选择。累积分布函数,又叫分布函数,是概率密度函数的积分。因为其积分性质,所以他是一个单调增函数(解决大小控制问题)且值域只可以取 0 ~ 1(解决越界问题)。

​ 在决定好旧分布和新的分布后,应当选取灰度接近的区域以作变换而不是随机处理,才不会引入过多的噪声。

​ OpenCV 有提供专门的函数提供直方图均衡化服务:

equalizeHist(src[, ) 输出结果有和输入同样的尺寸和类型。如果有需要彩色图像的色彩均衡化的需求,则需要将彩色图像的三个通道用split()拆分成三个通道分别进行均衡化,然后再用merge() 函数进行合并。

灰度图直方图均衡

gray and equalizeHist

三通道直方图均衡

3Ch equalizeHist

可以看到,不管是单通道还是多通道的直方图均衡操作都可以达到增大对比度,图片锐化的效果,但是因为三个通道的不同步变化导致了明显的画面色彩失真,尤其是在某些颜色通道的光照条件差异较大时更为明显。例如,原图中的白色或灰色区域在均衡化后可能会呈现出色偏,因为红、绿、蓝三个通道的亮度增强不同。

而 YUV 色彩空间或 Lab 色彩空间将亮度通道和色彩通道分开, 可以在改善图像对比度时,避免对色彩造成过度的扭曲或失真,相比直接在 RGB 空间内工作更能保护图像的色彩质感和平衡。

YUV qualizHist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cv2 as cv
import numpy as np

img = cv.imread("test.jpg", 1)

imgYUV = cv.cvtColor(img, cv.COLOR_BGR2YCrCb)
# cv2.imshow("src", img)


channelsYUV = list(cv.split(imgYUV))
channelsYUV[0] = cv.equalizeHist(channelsYUV[0])

channels = cv.merge(channelsYUV)
result = cv.cvtColor(channels, cv.COLOR_YCrCb2BGR)
# cv2.imshow("dst", result)

combined_results = np.hstack((img, result))
cv.imshow("src and dst", combined_results)

cv.waitKey(0)

​ 可以看到因为 YUV 色彩空间单独把亮度分离作为一个通道,因为直方图均衡正是希望对图像强度做处理,所以可以避免影响到色准,色彩失真或扭曲。

​ 除此之外,


openCV开发学习
http://pafl.top/2024/04/11/openCV开发/
Author
Paf
Posted on
April 11, 2024
Licensed under