Python OpenCV入门到精通学习日记:像素的操作
前言
像素是图像的最小单位。每一幅图像都是由M行N列的像素组成的,其中每一个像素都存储一个像素值。以灰度图像为例,计算机通常把灰度图像的像素处理为256个灰度级别,256个灰度级别分别使用区间[0, 255]中的整数数值表示。其中,“0”表示纯黑色;“255”表示纯白色。今天内容会比较多,因为涉及到了如何使用NumPy模块操作像素。
因为内容比较多,我列举了目录,如下:
像素操作与Numpy模块操作像素
1 像素
图像是由许多小方块组成的,通常把一个小方块称作一个像素。因此,一个像素是具有一定面积的一个块,而不是一个点。需要注意的是,像素的形状是不固定的,大多数情况下,像素被认为是方形的,但有时也可能是圆形的或者是其他形状的。
1.1 确定像素的位置
我们该如何确定像素的位置呢?我们可以将一张图片看做是一张围棋的棋盘,棋盘上的每一个方格看做一个像素,假设水平方向的像素是219个,与其对应的是x轴的取值范围,即0~218;同理,在垂直方向的像素是292个,与其对应的是y轴的取值范围,即0~291。在水平方向和垂直方向的像素绘制坐标系,以图像左上角为原点,水平方向为x轴,垂直方向为y轴,这样,就能够通过坐标来确定某个像素在图中的位置。在OpenCV中,正确表示图4.1中某个像素坐标的方式是(y, x)。
代码示例如下:
import cv2
img = cv2.imread("img.png") # 读取同目录下的img.png图像文件
px = img[291,218] # 获取坐标(291,218)上的像素
1.2 获取像素的BGR值
运行上面的代码,我们可以得到了坐标(291, 218)上的像素px
。现使用print()
方法打印这个像素,将得到这个像素的BGR值:
print("坐标(291, 218)上的像素的BGR值是",px)
运行结果为:坐标(291, 218)上的像素的BGR值是 [ 99 107 220]
在这里要介绍一个新的概念:三基色。三基色分别是指红色,绿色,蓝色,而通过这三种颜色以不同比例的方式混合,就能产生各种各样的颜色。说到这里,大家也应该知道BGR是什么意思了,以较为常用的RGB色彩空间为例,在RGB色彩空间中,存在3个通道,即R通道、G通道和B通道。其中,R通道指的是红色通道;G通道指的是绿色通道;B通道指的是蓝色通道;并且每个色彩通道都在区间[0, 255]内取值。这样,计算机将利用3个色彩通道的不同组合来表示不同的颜色。
这里有一点要注意:在RGB色彩空间中,彩色图像的通道顺序是R→G→B;但是,在OpenCV中,RGB色彩空间被BGR色彩空间取代,使得彩色图像的通道顺序变为了B→G→R。
在获取这三个通道的方法在python上有两种办法:
1.同时获取坐标(291, 218)上的像素的B通道、G通道和R通道的值
px = img[291,218]
print(px)
运行结果如下:[ 99 107 220]
2.分别获取坐标(291, 218)上的像素的B通道、G通道和R通道的值
blue = img[291,218,0] # 坐标(291, 218)上的像素的B通道的值
green = img[291,218,1] # 坐标(291, 218)上的像素的G通道的值
red = img[291,218,2] # 坐标(291, 218)上的像素的R通道的值
print(blue,green,red)
运行结果如下:99 107 220
0,1,2分别表示B,G,R
1.3 修改像素的BGR值
学到现在,我们已经可以获取图像中某个坐标的像素的BGR值了,现在我们可以将某个像素的BGR值进行修改。
px = [255,255,255] # 把坐标(291, 218)上的像素的值修改为[255, 255, 255]
print("坐标(291, 218)上的像素修改后的BGR值是", px)
运行结果如下:坐标(291, 218)上的像素修改后的BGR值是 [255, 255, 255]
当每个像素的BGR值都相等时,就可以获得灰度图像。其中,B=G=R=0为纯黑色,B=G=R=255为纯白色。
学到这里,我们就可以尝试修改某个指定区域内的所有像素了。
import cv2
img = cv2.imread("img.png")
cv2.imshow("women",img) # 显示图像
# 开始一个嵌套循环,用于处理图像中的一个特定区域。
# 外层循环变量i从241行开始,到291行结束,不包括292行。
for i in range(241,292): # i变量在这个循环中遍历的是图像的第241行到第291行,所以i表示横坐标,在区间[241,291]内取值
# 内层循环变量j从第168列开始,到第218列结束,不包括219列。
for j in range(168,219): # j变量在这个循环中遍历的是图像的第168列到第218列,所以j表示纵坐标,在区间[168,218]内取值
img[i,j] = [255,255,255]
cv2.imshow("women_t",img)
cv2.waitKey()
cv2.destroyAllWindows()
运行后会生成如下内容:
到这里,大家可能如果和我一样比较小白的话会比较懵,为什么range(241,291)
是第241行到第291行,我自己的理解就是,在python中区间是左闭右开的,所以最后一个值不取。其他的内容我注释写的很详细,大家可以看看,如果哪里有错误,还希望在评论区指出!
2 使用NumPy模块操作像素
因为图像在OpenCV中以二维或三维数组表示,数组中的每一个值就是图像的像素值,所以善于操作数组的NumPy
模块就成了OpenCV的依赖包。OpenCV中很多操作都要依赖NumPy
模块,今天先学习些简单的内容。
2.1 NumPy概述
NumPy
,它是Python数组计算、矩阵运算和科学计算的核心库,NumPy
来源于Numerical和Python两个单词。NumPy
提供了一个高性能的数组对象,以及可以轻松创建一维数组、二维数组和多维数组等大量实用方法,帮助开发者轻松地进行数组计算,从而广泛地应用于数据分析、机器学习、图像处理和计算机图形学、数学任务等领域中。由于NumPy
是由C语言实现的,所以其运算速度非常快。具体功能如下:
- 有一个强大的N维数组对象
ndarray
。 - 广播功能方法。
- 线性代数、傅里叶变换、随机数生成、图形操作等功能。
- 整合C/C++/Fortran代码的工具。
2.2 数组的类型
在对数组进行基本操作前,首先了解一下NumPy
的数据类型。NumPy
比Python增加了更多种类的数值类型,如表所示:
数据类型 | 描述 |
---|---|
int8 | 8位整数(-128到127) |
int16 | 16位整数(-32768到32767) |
int32 | 32位整数(-2147483648到2147483647) |
int64 | 64位整数(-9223372036854775808到9223372036854775807) |
uint8 | 8位无符号整数(0到255) |
uint16 | 16位无符号整数(0到65535) |
uint32 | 32位无符号整数(0到4294967295) |
uint64 | 64位无符号整数(0到18446744073709551615) |
float16 | 16位半精度浮点数 |
float32 | 32位单精度浮点数 |
float64 | 64位双精度浮点数 |
complex64 | 32位单精度复数(实部和虚部各占16位) |
complex128 | 64位双精度复数(实部和虚部各占32位) |
complex256 | 128位扩展精度复数(实部和虚部各占64位) |
bool_ | 布尔类型,只能取True或False |
object | 用于存储Python对象的数组 |
str_ | 用于存储Unicode字符串的数组 |
void | 用于存储任意数据类型的数组,不占用空间 |
每一种数据类型都有相应的数据转换方法。
import numpy as np
print(np.int8(3.141))
print(np.float64(8))
print(np.float64(True))
运行结果如下:
3
8.0
1.0
需要注意的是,书上给的是np.float(True),但是运行后会发生报错:
AttributeError: module 'numpy' has no attribute 'float'
这是因为np.float是一个已经被弃用的别名,在 NumPy 1.20 版本中,这个别名被弃用,因为它经常引起混淆。
如果你需要更多关于这个弃用的信息,或者想要了解如何更新你的代码以避免这个问题,你可以访问 NumPy 1.20
的发布说明,链接是: NumPy 1.20.0 Release Notes
在这可以使用np.float64(),np.float32(),np.float16()
。
np.float64(True)
这个表达式在 Python 中使用NumPy
库将布尔值 True
强制转换为数据类型为 np.float64
(即双精度浮点数)的数值。
当你使用 np.float64(True)
,你实际上是在创建一个数据类型为np.float64
的新数值,其值等同于布尔值True
在数值上的等价值,即 1.0
这个操作是类型转换(type casting)的一个例子,即将一种数据类型的值转换成另一种数据类型的值。在这个特定的例子中,布尔值True
被转换成了np.float64
类型的数值 1.0
。相对的,如果使用 False
,它会被转换成 0.0
在 NumPy
中,这种转换是隐式进行的,因为 NumPy
支持布尔值和数值之间的自然映射,其中 True
对应于 1
,False
对应于 0
。这种特性在进行数值计算时非常有用,因为它允许布尔数组直接用于数值运算,而不需要显式的类型转换。
print(1+np.float64(True))
会输出
2.0
,即1.0+1.0
2.3 创建数组
2.3.1 最常规的array()方法
NumPy
创建简单的数组主要使用array()方法,通过传递列表、元组来创建NumPy
数组,其中的元素可以是任何对象,语法如下:
numpy.array(object, dtype, copy, order, subok, ndmin)
参数说明:
- object:任何具有数组接口方法的对象。
- dtype:数据类型。
- copy:可选参数,布尔型,默认值为True,则object对象被复制;否则,只有当__array__返回副本,object参数为嵌套序列,或者需要副本满足数据类型和顺序要求时,才会生成副本。
- order:元素在内存中的出现顺序,其值为K、A、C、F。如果object参数不是数组,则新创建的数组将按行排列(C),如果值为F,则按列排列;如果object参数是一个数组,则以下顺序成立:C(按行)、F(按列)、A(原顺序)、K(元素在内存中的出现顺序)。
- subok:布尔型。如果值为True,则传递子类,否则返回的
数组将强制为基类数组(默认值)。
- ndmin:指定生成数组的最小维数。
在书中有这么一个说明:
当order是’ A ‘,object是一个既不是’ C ‘也不是’ F ‘order的数组,并且由于dtype的更改而强制执行了一个副本时,那么结果的顺序不一定是’ C '。这可能是一个bug。
这段话可以大家听得有点懵,我在这里解释一下:首先你设置了’A’,那么你是要求数组要按照原顺序排列的,但你要求更改dtype,那么numpy将会生成一个副本来适应新的数据类型,但是这时可能numpy无法确定原始数据的顺序,那么结果通常默认是’C’,但却可能不是,所以说这是个bug。说的我自己都有点绕了,这是书中的一个说明,如果我的解释有问题,希望大家可以在评论区指出。
总之,简单来说就是当你告诉 NumPy 保持数组的原始顺序,但同时需要改变数据类型时,NumPy 可能不会按照你期望的方式去做,这可能会在一些特定情况下导致问题。
好了,说完难的,说点简单的,看看如何使用array()创建一二维数组:
n1 = np.array([1,2,3]) # 创建一个简单的一维数组
n2 = np.array([0.1,0.2,0.3]) # 创建一个包含小数的一维数组
n3 = np.array([[1,2],[3,4]]) # 创建一个简单的二维数组
print(n1)
print(n2)
print(n3)
运行结果如下:
[1 2 3]
[0.1 0.2 0.3]
[[1 2]
[3 4]]
创建浮点类型数组:
list = [1,2,3] # 列表
# 创建浮点型数组
# n1 = np.array(list,dtype=np.float64)
# 或者
n4 = np.array(list,dtype=float)
print(n4)
print(n4.dtype)
print(type(n4[0]))
运行结果如下:
[1. 2. 3.]
float64
<class ‘numpy.float64’>
创建三维数组:
nd1 = [1,2,3]
nd2 = np.array(nd1,ndmin=3) # 三维数组
print(nd2)
运行结果如下:[[[1 2 3]]]
2.3.2 创建指定维度和数据类型未初始化的数组
创建2行3列的未初始化数组
n = np.empty([2,3])
print(n)
运行结果如下:
[[2.28895787e+243 5.59121958e+252 7.52318511e+199]
[5.30479514e+180 7.69843740e+218 2.08887652e-312]]
2.3.3 创建用0填充的数组
创建用0填充的数组需要使用zeros()方法,该方法创建的数组元素均为0。OpenCV经常使用该方法创建纯黑图像。
n = np.zeros((3,3),np.uint8)
print(n)
运行结果如下:
[[0 0 0]
[0 0 0]
[0 0 0]]
2.3.4 创建用1填充的数组
创建用1填充的数组需要使用ones()方法,该方法创建的数组元素均为1。OpenCV经常使用该方法创建纯掩模、卷积核等用于计算的二维数据。
创建3行、3列、数字类型为无符号8位整数的纯1数组
n = np.ones((3,3),np.uint8)
print(n)
运行结果如下:
[[1 1 1]
[1 1 1]
[1 1 1]]
2.3.5 创建随机数组
randint()
方法用于生成一定范围内的随机整数数组,左闭右开区间([low,high)),语法如下:
numpy.random.randint(low,high,size)
参数说明:
- low:随机数最小取值范围。
- high:可选参数,随机数最大取值范围。若high为空,取值范围为(0,low)。若high不为空,则high必须大于low。
- size:可选参数,数组维数。
生成一定范围内的随机数组:
n1 = np.random.randint(1,3,10)
print('随机生成10个1~3且不包括3的整数:')
print(n1)
n2 = np.random.randint(5, 10)
print('size数组大小为空随机返回一个整数:')
print(n2)
n3 = np.random.randint(5, size=(2, 5))
print('随机生成5以内二维数组:')
print(n3)
运行结果如下:
随机生成10个1~3且不包括3的整数:
[1 2 2 2 2 2 2 1 2 2]
size数组大小为空随机返回一个整数:
8
随机生成5以内二维数组:
[[4 2 2 1 0]
[3 1 4 1 1]]
2.4 操作数组
不用编写循环即可对数据执行批量运算,这就是NumPy数组运算的特点,NumPy称为矢量化。大小相等的数组之间的任何算术运算都可以用NumPy实现。我的理解就是矩阵运算就用它,不然要运算矩阵需要些很多循环来运算矩阵,为什么要循环呢?那得去学学线性代数了。
2.4.1 加法运算
使用NumPy创建2个数组,并让2个数据进行加法运算
n1 = np.array([1,2])
n2 = np.array([3,4])
print(n1+n2)
运行结果如下:[4 6]
2.4.2 减法和乘除法运算
使用NumPy创建2个数组,并让2个数组进行减法、乘法和除法运算
n1 = np.array([1,2])
n2 = np.array([3,4])
print(n1-n2)
print(n1*n2)
print(n1/n2)
运行结果如下:
[-2 -2]
[3 8]
[0.33333333 0.5 ]
2.4.3 幂运算
使用NumPy创建2个数组,并让2个数组做幂运算
n1 = np.array([1,2])
n2 = np.array([3,4])
print(n1**n2)
运行结果如下:[ 1 16]
2.4.4 比较运算
使用NumPy创建2个数组,分别使用“>=”“==”“<=”和“!=”运算符比较2个数组
n1 = np.array([1,2])
n2 = np.array([3,4])
print(n1>=n2) #大于等于
print(n1==n2) #等于
print(n1<=n2) #小于等于
print(n1!=n2) #不等于
运行结果如下:
[False False]
[False False]
[ True True]
[ True True]
2.4.5 复制数组
n2 = np.array(n1, copy=True)
,但n2 = n1.copy()
更常用。
这两种方法都可以按照原数组的结构、类型、元素值创建出一个副本,修改副本中的元素不会影响到原数组。
使用copy()方法复制数组,比较2个数组是否相同。修改副本数组中的元素值后,再查看2个数组是否相同
n1 = np.array([1,2])
n2 = n1.copy()
print(n1==n2)
n2[0] = 9
print(n1)
print(n2)
print(n1==n2)
运行结果如下:
[ True True]
[1 2]
[9 2]
[False True]
2.5 数组的索引和切片
NumPy数组元素是通过数组的索引和切片来访问和修改的,因此索引和切片是NumPy中最重要、最常用的操作。
2.5.1 索引
数组的索引其实就是数组的元素的序号,值得注意的是,索引是从0开始的,NumPy数组可以使用标准Python语法x[obj]的语法对数组进行索引,其中x是数组,obj是选择方式。
查找数组n1索引为0的元素
n1 = np.array([1,2])
print(n1[0])
运行结果如下:1
2.5.2 切片式索引
顾名思义,就是对数组进行切割,来对数组进行范围操作。语法如下:
[start:stop:step]
参数说明:
- start:起始索引,若不写任何值,则表示从0开始的全部索引。
- stop:终止索引,若不写任何值,则表示直到末尾的全部索引。
- step:步长。
切片式索引操作获取数据中某范围的元素
n1 = np.array([1,2,3])
print(n1[0])
print(n1[1])
print(n1[0:2])
print(n1[1:])
print(n1[:2])
运行结果如下:
1
2
[1 2]
[2 3]
[1 2 3]
切片式索引操作需要注意以下几点:
- 索引是左闭右开区间,如上述代码中的
n1[0:2]
,只能取到索引从0~1的元素,而取不到索引为2的元素。 - 当没有start参数时,代表从索引0开始取数,如上述代码中的
n1[:2]
。 - start、stop和step 3个参数都可以是负数,代表反向索引。
示意图有些难画,我直接引用了书上的原图:
分别演示start、stop、step 3种索引的切片场景
n = np.array([0,1,2,3,4,5,6,7,8,9])
print(n) # [0 1 2 3 4 5 6 7 8 9]
print(n[:3]) # [0 1 2]
print(n[3:6]) # [3 4 5]
print(n[6:]) # [6 7 8 9]
print(n[::]) # [0 1 2 3 4 5 6 7 8 9]
print(n[:]) # [0 1 2 3 4 5 6 7 8 9]
print(n[::2]) # [0 2 4 6 8]
print(n[1::5]) # [1 6]
print(n[2::6]) # [2 8]
# start,stop,setp 为负数时
print(n[::-1]) # [9 8 7 6 5 4 3 2 1 0]
print(n[:-3:-1]) # [9 8]
print(n[-3:-5:-1]) # [7 6]
print(n[-5::-1]) # [5 4 3 2 1 0]
2.5.3 二维数组索引
二维数组索引可以使用array[n,m]的方式,以逗号分隔,表示第n个数组的第m个元素。
分别获取二维数组中索引为1的元素、第2行第3列的元素、索引为-1的元素
n=np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]])
print(n[1])
print(n[1,2])
print(n[-1])
运行结果如下:
[4 5 6 7]
6
[ 8 9 10 11]
2.5.4 二维数组切片式索引
二维数组也支持切片式索引操作,也就是获取二维数组中某一块区域的索引。
创建二维数组,对该数组进行切片式索引操作
n = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(n[:2,1:])
print(n[1,:2])
print(n[:2,2])
print(n[:,:1])
运行结果如下:
[[2 3]
[5 6]]
[4 5]
[3 6]
[[1]
[4]
[7]]
这里要注意:
数组行索引=像素所在行数-1=像素纵坐标。
数组列索引=像素所在列数-1=像素横坐标。
小结
今天已经把所有numpy的基本操作学了学,明天要开始将图像和numpy进行结合了,冲!!!
大家可以在评论区聊聊哇,一起交流。除此之外,在这我说一下,我可能关于数组运算我只给了代码示例没有具体介绍,因为都是些很简单的运算,我觉得应该大家都会,如果有疑问,可以在评论区留言哦