当前位置:   article > 正文

python-opencv学习笔记2 核心运算_opencv 将三个ndarray合并

opencv 将三个ndarray合并

2.1 图像的基本操作

基本操作:

  • 访问像素值并修改它们
  • 访问图像属性
  • 设置感兴趣的区域(ROI)
  • 分割和合并图像

注意:本节中几乎所有的操作都主要与Numpy而不是OpenCV有关。

重要函数

  1. numpy.ndarray.item()
    作用:将数组的元素复制到标准Python标量(scalar)并返回。
    原型:ndarray.item(*args)
    参数 :*args :Arguments (variable number and type)
    - 当参数是一个数字时,数组会先被展开,然后再获取数字对应的项
    - 当参数是一个元组时,会按照N维矩阵方式获得对应项
>>> np.random.seed(123)							# 设置随机数种子 
>>> x = np.random.randint(9, size=(3, 3))		# 生成随机数矩阵
>>> x
array([[2, 2, 6],
[1, 3, 6],
[1, 0, 1]])

>>> x.item(3)			# 参数为一个数字;展开数组并获得index为3的项,注意:index从0开始
1
>>> x.item(7)			# 同上
0
>>> x.item((0, 1))		# 参数为一个元组;以n维的方式获得对应项  第一维index为0([2, 2, 6]),第二维index为1(即[2, 2, 6]中的第二个元素 2)
2
>>> x.item((2, 2))		# [1, 0, 1]中的第3个元素 1
1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注意:可分别用来根据位置获取numpy矩阵中的值和为给定位置赋新值,速度比用index快。

  1. numpy.ndarray.itemset()
    作用:把 ndarray 中的某个元素(值)改掉
    原型:ndarray.itemset(*args, newValue)
    参数1 :*args :Arguments (variable number and type)
    - 当参数是一个数字时,数组会先被展开,然后再获取数字对应的项
    - 当参数是一个元组时,会按照N维矩阵方式获得对应项
    参数2:newValue 新的值
>>> x = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]], np.int32)
>>> x
array([[[ 1,  2,  3],
        [ 4,  5,  6]],
       [[ 7,  8,  9],
        [10, 11, 12]]], dtype=int32)    # 三维数组 shape = (2,2,3)
# 把 index = 1 的 value 改成 999
>>> x.itemset(1, 999)   			    # 参数1是数字1,那么,先展开数组后,将index=1的元素 改为999, 即[1,2,3....11,12]中的2改为999
>>> x
array([[[  1, 999,   3],
        [  4,   5,   6]],
        [[  7,   8,   9],
        [ 10,  11,  12]]], dtype=int32)
# 把 index = (1, 1, 2) 的值改成 888
>>> x.itemset((1, 1, 2), 888)			
# 参数1是元组(1,1,2);那么,按矩阵方式,获取一维数组index=1的元素(即:[[7,8,9],[10,11,12]]),再获得第二维index=1的元素(即:[10,11,12]),再获得第三维index=2的元素(即:12);将这元素12 改为 888
>>> x
array([[[  1, 999,   3],
        [  4,   5,   6]],
       [[  7,   8,   9],
        [ 10,  11, 888]]], dtype=int32)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. cv.split()
    作用:将图像分割为单独的通道
    参数:图像数组 numpy.ndarray数组对象
    返回值:3个通道 b, g, r 的numpy.ndarray数组对象
>>> b,g,r = cv.split(img)       # 分割图像

'''
numpy的等效方法,推荐
numpy的方法比cv.split()更快
b == img[:,:,0]
g == img[:,:,1]
r == img[:,:,2]
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

警告:cv.split()是一个耗时的操作。所以只有在必要时才使用它。

  1. cv.merge()
    作用:合并通道
    参数:b, g, r 三色通道
    返回值:三维图像数组

注意:b,g,r尺寸应该一致

>>> img = cv.merge((b,g,r))     # 合并通道
  • 1
  1. cv.copyMakeBorder()
    作用:用于画边框,卷积操作、零填充
    参数:
    • src - 输入图像
    • top, bottom, left, right - 相应方向的边框宽度,以像素数计
    • borderType - 定义要添加的边界类型的标志。它可以是以下类型:
      • cv.BORDER_CONSTANT - 添加一个恒定颜色的边框。该值应作为下一个参数给出;
      • cv.BORDER_REFLECT - 边框将是边框元素的镜像反射,像这样:Fedcba|abcdefgh|hgfedcb;
      • cv.BORDER_REFLECT_101 或 cv.BORDER_DEFAULT - 与上述相同,但有轻微变化,像这样:gfedcb|abcdefgh|gfedcba
      • cv.BORDER_REPLICATE - 最后一个元素被整体复制,像这样:aaaaa|abcdefgh|hhhhh;
      • cv.BORDER_WRAP - 无法解释,看起来会像这样:cdefgh|abcdefgh|abcdefg;
    • value - 如果边框类型是cv.BORDER_CONSTANT,它指的是边框的颜色。
      返回值:

访问和修改像素值

让我们先加载一个彩色图像。

>>> import numpy as np
>>> import cv2 as cv
>
>>> img = cv.imread('messi5.jpg')     # cv.imread 读取图像 需要注意的是CV图像的像素是BGR排序的。一般彩色图像为RGB
  • 1
  • 2
  • 3
  • 4

你可以通过其行和列坐标访问一个像素值

  • 对于BGR图像,它返回一个蓝、绿、红值的数组。
  • 对于灰度图像,只返回相应的强度。
# img 为numpy.ndarray数组,且img.shape为3维。
# 第一维表示图像的行数(即:图像的高),第二维表示图像的列数(即:图像的宽),第三维表示图像的深度(彩色为3,分别BGR,灰度为1,即强度)

>>> px = img[100,100]    # 获取图像的一个像素;[100,100] 指定了第一维(行)和第二维(列)
>>> print( px )
# 输出结果:
[157 166 200]			 # 这证明了 彩色图像的深度为3,即:3个字节,分别是B G R

# accessing only blue pixel
>>> blue = img[100,100,0]			# 只访问一个像素的blue分量
>>> print( blue )
# 输出结果
157
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

笔记:第一维表示图像的行数(即:图像的高),第二维表示图像的列数(即:图像的宽),第三维表示图像的深度(彩色为3,分别BGR,灰度为1,即强度)

你可以用同样的方法修改像素值。

>>> img[100,100] = [255,255,255]
>>> print( img[100,100] )
[255 255 255]
  • 1
  • 2
  • 3

警告Numpy是一个用于快速数组计算的优化库。因此,简单地访问每一个像素值并对其进行改将是非常缓慢的,我们不鼓励这样做。

注释:上述方法通常用于选择一个数组的某个区域(即:数组切片),例如前5行和后3列。
对于单个像素的访问,Numpy数组方法,array.item()和array.itemset()被认为更好。然而,它们总是返回一个标量,所以如果你想访问所有的B、G、R值,你将需要为每个值分别调用array.item()。

更好的像素访问和编辑方法:

# accessing RED value
>>> img.item(10,10,2)      # numpy.ndarray.item()方法
59
# modifying RED value
>>> img.itemset((10,10,2),100)   # numpy.ndarray.itemset()方法
>>> img.item(10,10,2)
100
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

访问图像属性

图像属性包括行、列和通道的数量;图像数据的类型像素的数量等。

  • 图像形状(行、列、通道)
  • 数据类型
  • 像素数量

图像形状属性 img.shape

图像的形状是由img.shape访问的。它返回一个包含行数、列数和通道数(如果图像是彩色的)的元组。

>>> print( img.shape )   # 彩色图像  (342, 548, 3)   灰度图像 (342, 548) 也可以是(342, 548, 1) 
(342, 548, 3)
  • 1
  • 2

如果一个图像是灰度的,返回的元组只包含行(高)和列(宽)的数量,所以这是一个很好的方法来检查加载的图像是灰度还是彩色。

图像总像素数属性 img.size

总像素数由Img.size访问。

>> print( img.size )  # ndarray 展开后的元素数量
562248
  • 1
  • 2

图像数据类型属性 img.dtype

图像数据类型由img.dtype 获得。

>> print( img.dtype )    # 数据类型: 如果是彩色B,G,R  其中一个分量的值由几位存储。例如B,需要一个8位的字节进行存储
uint8
  • 1
  • 2

注意: img.dtype在调试时非常重要,因为OpenCV-Python代码中大量的错误是由无效的数据类型引起的。

图像ROI

有时,你必须对图像的某些区域进行处理。对于图像中的眼睛检测,首先在整个图像上进行人脸检测。当得到一个人脸时,我们单独选择人脸区域并在其中搜索眼睛,而不是搜索整个图像。它提高了准确性(因为眼睛总是在脸上)和性能(因为我们在一个小区域内搜索)。

使用Numpy索引再次获得ROI。这里我选择了球,并将其复制到图像的另一个区域。

>> ball = img[280:340, 330:390]    # 切片 注意:切片并不会创建新的内存,而是引用了img数组对应的部分;
>> img[273:333, 100:160] = ball    # 切片赋值给另一个切片 
  • 1
  • 2

笔记:切片并不会创建新的内存,而是引用了img数组对应的部分;

分割和合并图像通道

有时你需要分别处理图像的B、G、R通道。在这种情况下,你需要将BGR图像分割成单个通道。在其他情况下,你可能需要将这些单独的通道连接起来以创建一个BGR图像。你可以通过以下方式简单地做到这一点。

>>> b,g,r = cv.split(img)       # 分割图像
>>> img = cv.merge((b,g,r))     # 合并通道
  • 1
  • 2

或者:

>>> b = img[:,:,0]             # 切片的方式获得通道0 即:b
  • 1

将一个通道设置为一个数 ,Numpy切片速度更快

假设你想把所有的红色像素设置为零–你不需要先分割通道。Numpy索引的速度更快

>>> img[:,:,2] = 0
  • 1

警告cv.split()是一个耗时的操作。所以只有在必要时才使用它。否则,请使用Numpy索引。

为图像制作边框(填充)

如果你想在图像周围创建一个边框,类似于一个相框,你可以使用cv.copyMakeBorder()。但它在卷积操作、零填充等方面有更多应用。这个函数需要以下参数:

  • src - 输入图像
  • top, bottom, left, right - 相应方向的边框宽度,以像素数计
  • borderType - 定义要添加的边界类型的标志。它可以是以下类型:
    • cv.BORDER_CONSTANT - 添加一个恒定颜色的边框。该值应作为下一个参数给出;
    • cv.BORDER_REFLECT - 边框将是边框元素的镜像反射,像这样:Fedcba|abcdefgh|hgfedcb;
    • cv.BORDER_REFLECT_101 或 cv.BORDER_DEFAULT - 与上述相同,但有轻微变化,像这样:gfedcb|abcdefgh|gfedcba
    • cv.BORDER_REPLICATE - 最后一个元素被整体复制,像这样:aaaaa|abcdefgh|hhhhh;
    • cv.BORDER_WRAP - 无法解释,看起来会像这样:cdefgh|abcdefgh|abcdefg;
  • value - 如果边框类型是cv.BORDER_CONSTANT,它指的是边框的颜色。

下面是一个演示所有这些边框类型的示例代码,以便更好地理解:

import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt

BLUE = [255,0,0]   # 一个纯蓝像素
img1 = cv.imread('opencv-logo.png')     # 读取一幅图像
# 画边框  后会获得一幅新图像(即:为replicate数组新建内存)
replicate = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REPLICATE)  # 
reflect = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT)
reflect101 = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_REFLECT_101)
wrap = cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_WRAP)
constant= cv.copyMakeBorder(img1,10,10,10,10,cv.BORDER_CONSTANT,value=BLUE)     # 固定颜色边框
plt.subplot(231),plt.imshow(img1,'gray'),plt.title('ORIGINAL')
plt.subplot(232),plt.imshow(replicate,'gray'),plt.title('REPLICATE')
plt.subplot(233),plt.imshow(reflect,'gray'),plt.title('REFLECT')
plt.subplot(234),plt.imshow(reflect101,'gray'),plt.title('REFLECT_101')
plt.subplot(235),plt.imshow(wrap,'gray'),plt.title('WRAP')
plt.subplot(236),plt.imshow(constant,'gray'),plt.title('CONSTANT')
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.2 图像上的算术操作

学习图像上的几种算术运算,如加法、减法、位运算等。

重要函数

  1. cv.add()
    作用:图像相加
    参数:

    1. img1
    2. img2
      返回值:新的图像(新建内存)
  2. cv.addWeighted()
    作用:图像加权相加 即:图像混合

  3. cv.bitwise_and()
    作用:位与

  4. cv.bitwise_or()
    作用:位或

  5. cv.bitwise_not()
    作用:位反

  6. cv.bitwise_xor()
    作用:位异或

  7. mask 掩膜参数

    1. 在有些图像处理的函数中有的参数里面会有mask参数,即此函数支持掩膜操作,首先何为掩膜以及有什么用,如下:
      数字图像处理中的掩膜的概念是借鉴于PCB制版的过程,在半导体制造中,许多芯片工艺步骤采用光刻技术,用于这些步骤的图形“底片”称为掩膜(也称作“掩模”),其作用是:在硅片上选定的区域中对一个不透明的图形模板遮盖,继而下面的腐蚀或扩散将只影响选定的区域以外的区域。
      图像掩膜与其类似,用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。
      数字图像处理中,掩模为二维矩阵数组,有时也用多值图像,图像掩模主要用于:
      ①提取感兴趣区ROI,用预先制作的感兴趣区掩模与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0。
      ②屏蔽作用,用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
      结构特征提取,用相似性变量图像匹配方法检测和提取图像中与掩模相似的结构特征。
      ④特殊形状图像的制作。
    2. 在所有图像基本运算的操作函数中,凡是带有掩膜(mask)的处理函数,其掩膜都参与运算(输入图像运算完之后再与掩膜图像或矩阵运算)。

下为脸部面具图,背景为白色,利用按位操作及掩膜技术清晰抠出面具轮廓。
在这里插入图片描述

// C++
// 转换目标(一个面具)为灰度图像
cvtColor(faceMaskSmall, grayMaskSmall, CV_BGR2GRAY);
// 隔离图像上像素的边缘,仅与面具有关(即面具的白色区域剔除),下面函数将大于230像素的值置为0,小于的置为255
threshold(grayMaskSmall, grayMaskSmallThresh, 230, 255, CV_THRESH_BINARY_INV);
// 通过反转上面的图像创建掩码(因为不希望背景影响叠加)
bitwise_not(grayMaskSmallThresh, grayMaskSmallThreshInv);
//使用位“与”运算来提取面具精确的边界
bitwise_and(faceMaskSmall, faceMaskSmall, maskedFace, grayMaskSmallThresh);
// 使用位“与”运算来叠加面具
bitwise_and(frameROI, frameROI, maskedFrame, grayMaskSmallThreshInv);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

利用掩膜(mask)进行“与”操作,即掩膜图像白色区域是对需要处理图像像素的保留,黑色区域是对需要处理图像像素的剔除,其余按位操作原理类似只是效果不同而已。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

算术操作全部函数

# 注意 以下为C++版本 
/*
两幅图像可以相加、相减、相乘、相除、位运算、平方根、对数、绝对值等;
图像也可以放大、缩小、旋转,还可以截取其中的一部分作为ROI(感兴趣区域)进行操作,各个颜色通道还可以分别提取及对各个颜色通道进行各种运算操作
*/
// 相加
void add(InputArray src1, InputArray src2, OutputArray dst,InputArray mask=noArray(), int dtype=-1);//dst = src1 + src2
// 相减
void subtract(InputArray src1, InputArray src2, OutputArray dst,InputArray mask=noArray(), int dtype=-1);//dst = src1 - src2
// 相乘
void multiply(InputArray src1, InputArray src2,OutputArray dst, double scale=1, int dtype=-1);//dst = scale*src1*src2
// 除法
void divide(InputArray src1, InputArray src2, OutputArray dst,double scale=1, int dtype=-1);//dst = scale*src1/src2
void divide(double scale, InputArray src2,OutputArray dst, int dtype=-1);//dst = scale/src2

void scaleAdd(InputArray src1, double alpha, InputArray src2, OutputArray dst);//dst = alpha*src1 + src2
// 加权相加  即:图像混合
void addWeighted(InputArray src1, double alpha, InputArray src2,double beta, double gamma, OutputArray dst, int dtype=-1);//dst = alpha*src1 + beta*src2 + gamma
// 平方根
void sqrt(InputArray src, OutputArray dst);//计算每个矩阵元素的平方根
// 幂运算
void pow(InputArray src, double power, OutputArray dst);//src的power次幂
// 指数运算
void exp(InputArray src, OutputArray dst);//dst = e**src(**表示指数的意思)
// 对数运算(幂运算的逆运算)
void log(InputArray src, OutputArray dst);//dst = log(abs(src))


/*位运算*/
//bitwise_and、bitwise_or、bitwise_xor、bitwise_not这四个按位操作函数。
// 位与
void bitwise_and(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray());//dst = src1 & src2
// 位或
void bitwise_or(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray());//dst = src1 | src2
// 位异或
void bitwise_xor(InputArray src1, InputArray src2,OutputArray dst, InputArray mask=noArray());//dst = src1 ^ src2
// 位反
void bitwise_not(InputArray src, OutputArray dst,InputArray mask=noArray());//dst = ~src
  • 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
  • 37
  • 38

图像加法

你可以用OpenCV函数cv.add()将两幅图像相加,或者简单地用numpy操作res = img1 + img2
图像加法运算的要求:

  • 两幅图像应该是相同的深度类型
  • 或者第二幅图像可以只是一个标量值

注意:
OpenCV的加法和Numpy的加法是有区别的。OpenCV加法是一个饱和操作,而Numpy加法是一个模数操作

笔记:opencv中加法是饱和操作,也就是有上限值,numpy会对结果取模。

  • 饱和操作 opencv
    举例说明:

    • 如果结果应该是256,但因为输出矩阵的类型也为CV_8U,而CV_8U的范围为0~255,所以值被置为了255(也就是有上限)。
    • 如果结果应该是-252,但因为输出矩阵的类型也为CV_8U,而CV_8U的范围为0~255,所以值被置为了0。
  • 模数操作 numpy
    取模运算是求两个数相除的余数。
    其运算法则:目标图像 = 图像1 + 图像2,运算结果进行取模计算

    • 当像素值<= 255时,结果为“图像1 + 图像2”,例如128 + 26 = 154
    • 当像素值>= 255时,结果为对255去模的结果,例如:(255+64)% 255 = 64

推荐使用:cv.add()

例如,考虑下面的例子:

>>> x = np.uint8([250])
>>> y = np.uint8([10])
>>> print( cv.add(x,y) ) # 250+10 = 260 => 255
[[255]]
>>> print( x+y )          # 250+10 = 260 % 256 = 4
[4]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当你添加两张图片时,这将更加明显。
请使用OpenCV函数,因为它们会提供一个更好的结果。
笔记:numpy.+的图像运算取模显然是不合理的(用在图像上不合理),因此,图像相加用opencv.add()

图像混合

也是图像添加,但对图像给予不同的权重,以便给人以混合或透明的感觉。图像的添加是按照下面的公式进行的:

在这里插入图片描述

通过改变α从0→1,你可以在一个图像和另一个图像之间进行很酷的过渡。
这里我取了两张图片来混合。第一张图片的权重为0.7,第二张图片的权重为0.3。cv.addWeighted()对图片应用了以下公式。
在这里插入图片描述

这里的γ取0。

img1 = cv.imread('ml.png')
img2 = cv.imread('opencv-logo.png')
dst = cv.addWeighted(img1,0.7,img2,0.3,0)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

请看下面的结果:
在这里插入图片描述

位操作

这包括按位的AND、OR、NOT和XOR操作。
笔记:XOR 异或,英文为exclusive OR,缩写成xor。
它们在提取图像的任何部分(正如我们将在接下来的章节中看到的那样)、定义和处理非矩形的ROI等方面将非常有用。下面我们将看到一个如何改变图像中某一区域的例子。

我想把OpenCV的标志放在一张图片上面。

  • 如果我将两张图片相加cv.add(),它将改变颜色(笔记:对应像素逐一相加,并进行饱和运算)。
  • 如果我把它们混合起来cv.addWeighted(),我就会得到一个透明的效果(笔记:对应像素逐一权重相加)。

但我希望它是不透明的。如果它是一个矩形区域,我可以像我们在上一章做的那样使用ROI。但是OpenCV的标志不是一个矩形的形状(比如:抠图)。所以你可以用位操作来做,如下图所示。

# Load two images
img1 = cv.imread('messi5.jpg')
img2 = cv.imread('opencv-logo-white.png')

# I want to put logo on top-left corner, So I create a ROI
rows,cols,channels = img2.shape   
# 笔记:图像的形状,返回值=行,列,通道;那么反推 创建时形状参数[512,256,3],第一维512是行数,第二维256是列数,第三维3维是通道
roi = img1[0:rows, 0:cols]    									# roi切片,  整幅图像
# Now create a mask of logo and create its inverse mask also
img2gray = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)					# 获得灰度图像
ret, mask = cv.threshold(img2gray, 10, 255, cv.THRESH_BINARY)	# 阈值化图像 获得二值图像
mask_inv = cv.bitwise_not(mask)									# 掩膜  笔记:用二值化图计算掩膜

# Now black-out the area of logo in ROI
img1_bg = cv.bitwise_and(roi,roi,mask = mask_inv)      			# 现在将ROI中的logo区域涂黑
# Take only region of logo from logo image.	
img2_fg = cv.bitwise_and(img2,img2,mask = mask)					# 从标志图像中只取标志的区域。
# Put logo in ROI and modify the main image
dst = cv.add(img1_bg,img2_fg)									# 在ROI中放置logo,并修改主图像
img1[0:rows, 0:cols ] = dst
cv.imshow('res',img1)
cv.waitKey(0)
cv.destroyAllWindows()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

笔记:图像的形状,返回值 行,列,通道;那么反推 创建时形状参数[512,256,3],第一维长度512 = 行数,第二维长度256 = 列数,第三维长度3=通道

请看下面的结果。左图是我们创建的遮罩。右图是最终的结果。为了更好地理解,显示上述代码中的所有中间图像,特别是img1_bg和img2_fg。
在这里插入图片描述

2.3 性能测量和改进技术

在图像处理中,由于你要每秒处理大量操作,你的代码不仅要提供正确的解决方案,而且要以最快的方式提供,这是必须的。因此,在本章中,你将学习:测试代码的性能;一些提高代码性能的技巧。

重要函数:

  • cv.getTickCount
    与C++中getTickCount()函数作用相同(感觉是同一个)
    作用:
    返回一个参考事件(比如机器被打开的那一刻)到这个函数被调用的那一刻之后的时钟周期的数量

  • cv.getTickFrequency
    作用:
    返回时钟周期的频率,或每秒的时钟周期数。

  • time模块

  • profile模块
    作用:Python代码性能分析
    用法:
    需要查看哪个函数的性能时,可在 profile.run() 中输入要分析的函数名。

import profile    # 调用profile模块

def a():
    sum = 0
    for i in range(1,10001):
        sum += i
    return sum

def b():
    sum = 0
    for i in range(1,100):
        sum += a()
    return sum

if __name__ == "__main__":
    profile.run("b()")       # profile模块使用方法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • memory_profiler 模块
    需要单独安装该模块;用 pip 安装 memory_profiler
    • 使用方法一:调试和运行是要增删 @profile
import time
			 # 注意正式运行时,需要删除@profile
@profile     # 在需要做性能分析的函数前面加装饰器 @profile
def my_func():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    time.sleep(10)
    del b
    del a
    print("+++++++++")

if __name__ == '__main__':
    my_func()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

然后在执行文件所在目录下,打开命令行窗口,输入 python -m memory_profiler xxx.py

即可返回该 xxx.py 函数的性能分析结果

得到的结果中:

Mem usage :内存占用情况

Increment :执行该行代码后新增的内存

Occurences :执行次数

Line Contents :行内容
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
- 使用方法二:引入模块,在调试和运行时使用
  • 1
from memory_profiler import profile

@profile(precision=4,stream=open('memory_profiler.log','w+'))
# precision:精确到小数后几位
# stream:此模块分析结果会保存到 'memory_profiler'日志文件,若无此参数,则结果会在控制台展示
# @profile 当不需要分析该函数性能时,将该行注释掉即可
def test1():
    c=0
    for item in xrange(100000):
        c+=1
    print c

if __name__=='__main__':
    test1()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

注意:性能分析过程本身的耗时不低于代码运行时间,因为性能分析的过程其实也相当于跑了遍程序。

  • IPython
    IPython 是一个 python 的交互式 shell,比默认的python shell 好用得多,支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多很有用的功能和函数。
    安装:
    在ubuntu 下只要 sudo apt-get install ipython 就装好了,通过 ipython 启动。

  • Cython库
    Cython语言使得Python语言的C扩展与Python本身一样简单。Cython是基于Pyrx的源代码转换器,但支持更多的边缘功能和优化。Cython语言是Python语言的一个超集(几乎所有的Python代码是有效的,但Cython Cython代码)还支持可选的静态类型来调用C函数,使用C++类和声明块C类型变量和类的属性。这允许编译器从Cython代码生成非常高效的C代码

这使得Cython编写外部C / C++库代码的理想语言,和快速的C模块,提高Python代码的执行速度。

  • SSE2,AVX,SIMD优化
    SIMD、SSE、AVX指令集
    指令集是指CPU能执行的所有指令的集合,每一指令对应一种操作,任何程序最终要编译成一条条指令才能让CPU识别并执行。CPU依靠指令来计算和控制系统,所以指令强弱是衡量CPU性能的重要指标,指令集也成为提高CPU效率的有效工具。

CPU都有一个基本的指令集,比如说目前英特尔和AMD的绝大部分处理器都使用的是X86指令集,因为它们都源自于X86架构。但无论CPU有多快,X86指令也只能一次处理一个数据,这样效率就很低下,毕竟在很多应用中,数据都是成组出现的,比如一个点的坐标(XYZ)和颜色(RGB)、多声道音频等。为了提高CPU在某些方面的性能,就必须增加一些特殊的指令满足时代进步的需求,这些新增的指令就构成了扩展指令集。该指令集采用单指令多数据(single instruction multiple data,简称 SIMD)扩展技术。

演变历史:
MMX
英特尔在1996年率先引入了MMX(Multi Media eXtensions)多媒体扩展指令集,也开创了SIMD(Single Instruction Multiple Data,单指令多数据)指令集之先河
SSE
SSE(Streaming SIMD Extensions,流式单指令多数据扩展)指令集是1999年英特尔在Pentium III处理器中率先推出的,并将矢量处理能力从64位扩展到了128位。
AVX
2007年8月,AMD抢先宣布了SSE5指令集(SSE到SSE4均为英特尔出品),英特尔当即黑脸表示不支持SSE5,转而在2008年3月宣布Sandy Bridge微架构将引入全新的AVX指令集,同年4月英特尔公布AVX指令集规范,随后开始不断进行更新,业界普遍认为支持AVX指令集是Sandy Bridge最重要的进步,没有之一。
AVX(Advanced Vector Extensions,高级矢量扩展)指令集借鉴了一些AMD SSE5的设计思路,进行扩展和加强,形成一套新一代的完整SIMD指令集规范。

  • cv.useOptimized()

check if optimization is enabled
检测cv函数优化功能是否开启。

  • cv.setUseOptimized()

设置cv函数优化功能是否启用/禁用

除了OpenCV之外,Python还提供了一个模块time,这对测量执行时间很有帮助。另一个模块profile有助于获得代码的详细报告,比如代码中每个函数花了多少时间,函数被调用了多少次,等等。但是,如果你使用的是IPython,所有这些功能都以一种用户友好的方式整合在一起。我们将看到一些重要的功能,更多的细节,请查看附加资源部分的链接。

用OpenCV测量性能

cv.getTickCount函数返回一个参考事件(比如机器被打开的那一刻)到这个函数被调用的那一刻之后的时钟周期的数量。因此,如果你在函数执行之前和之后调用它,你可以得到执行一个函数所使用的时钟周期数。

cv.getTickFrequency函数返回时钟周期的频率,或每秒的时钟周期数。

所以要找到以秒为单位的执行时间,你可以做以下工作。

e1 = cv.getTickCount()   # 获得时钟周期的数量
# your code execution
e2 = cv.getTickCount()	
time = (e2 - e1)/ cv.getTickFrequency()   # 时钟周期的数量 / 频率(周期/秒) = 时间
  • 1
  • 2
  • 3
  • 4

我们将用下面的例子来证明。下面的例子应用中值滤波,其内核大小从5到49不等。 不要担心结果会是什么样子–那不是我们的目标:

img1 = cv.imread('messi5.jpg')
e1 = cv.getTickCount()
for i in range(5,49,2):
    img1 = cv.medianBlur(img1,i)
e2 = cv.getTickCount()
t = (e2 - e1)/cv.getTickFrequency()
print( t )
# Result I got is 0.521107655 seconds
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

你可以用时间模块做同样的事情。不使用cv.getTickCount,而使用time.time()函数。然后取这两个时间的差值。

OpenCV中的默认优化

OpenCV的许多函数都使用SSE2AVX等进行了优化。它也包含未经优化的代码。因此,如果我们的系统支持这些功能,我们应该利用它们(几乎所有的现代处理器都支持它们)。在编译的时候,它是默认启用的。所以,如果OpenCV启用了优化代码,它就会运行优化的代码,否则就会运行未优化的代码。你可以使用**cv.useOptimized()**来检查它是否被启用/禁用,**cv.setUseOptimized()**来启用/禁用它。让我们看一个简单的例子。

# check if optimization is enabled
In [5]: cv.useOptimized()
Out[5]: True
In [6]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop
# Disable it
In [7]: cv.setUseOptimized(False)
In [8]: cv.useOptimized()
Out[8]: False
In [9]: %timeit res = cv.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

正如你所看到的,优化的中值滤波比未优化的版本快2倍。如果你检查它的源代码,你可以看到中值滤波是SIMD优化的。因此,你可以用它来在你的代码顶部启用优化(记住它是默认启用的)。

在IPython中衡量性能

有时你可能需要比较两个类似操作的性能。IPython给了你一个神奇的命令timeit来执行这个任务。它将代码运行数次,以获得更准确的结果。但是,它适合于测量单行的代码。

例如,你知道下面的运算哪个更快,x=5;y=x**2,x=5;y=xx,x=np.uint8([5]);y=xx,或者y=np.square(x)?我们将通过IPython shell中的timeit来找出答案。

In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

你可以看到,x = 5 ; y = x*x是最快的,与Numpy相比,它大约快20倍。如果你也考虑到数组的创建,它可能达到100倍的速度。(Numpy的开发者们正在解决这个问题)。

注意:Python的标量操作要比Numpy的标量操作快。所以对于包括一个或两个元素的操作,Python标量比Numpy数组更好。当数组的大小稍微大一点时,Numpy有优势。

我们将再试一个例子。这一次,我们将比较cv.countNonZero()和np.count_nonzero()对同一图像的性能:

In [35]: %timeit z = cv.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop
In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop
  • 1
  • 2
  • 3
  • 4

看,OpenCV函数比Numpy函数快了近25倍。

注意:通常情况下,OpenCV函数比Numpy函数快。所以对于同样的操作,OpenCV函数是首选。但是,也可能有例外**,特别是当Numpy使用视图而不是拷贝时**。

更多的IPython魔法命令

还有其他一些神奇的命令来测量性能、剖析、行剖析、内存测量等等。它们都有很好的文档。所以这里只提供这些文档的链接。建议有兴趣的读者可以尝试一下。

性能优化技术

有几种技术和编码方法可以发挥Python和Numpy的最大性能。这里只指出了相关的技术和方法,并给出了重要来源的链接。这里需要注意的是,首先尝试以一种简单的方式实现算法。一旦它开始工作,对它进行剖析,找到瓶颈,并对其进行优化。

  1. 尽可能避免在Python中使用循环,特别是双倍/三倍循环等。它们本身就很慢。
  2. 尽可能地将算法/代码矢量化,因为Numpy和OpenCV是为矢量操作而优化的。 利用高速缓存的一致性。
  3. 除非有必要,否则不要对数组进行复制。尽量使用视图(笔记:切片)来代替。阵列的复制是一个昂贵的操作。
  4. 如果你的代码在做完所有这些操作后仍然很慢,或者不可避免地要使用大的循环,请使用额外的库,如Cython,使其更快。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/942675
推荐阅读
相关标签
  

闽ICP备14008679号