苦学Opencv的第三天:像素的操作

Python OpenCV入门到精通学习日记:像素的操作

前言

像素是图像的最小单位。每一幅图像都是由M行N列的像素组成的,其中每一个像素都存储一个像素值。以灰度图像为例,计算机通常把灰度图像的像素处理为256个灰度级别,256个灰度级别分别使用区间[0, 255]中的整数数值表示。其中,“0”表示纯黑色;“255”表示纯白色。今天内容会比较多,因为涉及到了如何使用NumPy模块操作像素。

像素操作
像素
Numpy模块操作像素
确定像素的位置
获取像素的BGR值
修改像素的BGR值
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增加了更多种类的数值类型,如表所示:

数据类型描述
int88位整数(-128到127)
int1616位整数(-32768到32767)
int3232位整数(-2147483648到2147483647)
int6464位整数(-9223372036854775808到9223372036854775807)
uint88位无符号整数(0到255)
uint1616位无符号整数(0到65535)
uint3232位无符号整数(0到4294967295)
uint6464位无符号整数(0到18446744073709551615)
float1616位半精度浮点数
float3232位单精度浮点数
float6464位双精度浮点数
complex6432位单精度复数(实部和虚部各占16位)
complex12864位双精度复数(实部和虚部各占32位)
complex256128位扩展精度复数(实部和虚部各占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 对应于 1False 对应于 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进行结合了,冲!!!
大家可以在评论区聊聊哇,一起交流。除此之外,在这我说一下,我可能关于数组运算我只给了代码示例没有具体介绍,因为都是些很简单的运算,我觉得应该大家都会,如果有疑问,可以在评论区留言哦

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吃点李子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值