OpenCV-Python教程:图像加减乘除运算的共性问题

原文链接:http://www.juzicode.com/archives/6109

返回Opencv-Python教程

在前面的4篇文章中我们分别介绍了图像的加减乘除四种运算,这四种运算函数接口长得比较像,用法类似,有必要总结对比下。

1、函数接口

OpenCV-Python是OpenCV的Python接口,通过对比原生的C++接口,可以更详细地了解函数的使用方法。

运算方式 C++接口 Python接口
加法 void cv::add ( InputArray src1,
InputArray src2,
OutputArray dst,
InputArray mask = noArray(),
int dtype = -1
)
dst = cv2.add( src1, src2[, dst[, mask[, dtype]]] )
减法 void cv::subtract ( InputArray src1,
InputArray src2,
OutputArray dst,
InputArray mask = noArray(),
int dtype = -1
)
dst=cv2.subtract(src1, src2[, dst[, mask[, dtype]]])
乘法 void cv::multiply ( InputArray src1,
InputArray src2,
OutputArray dst,
double scale = 1,
int dtype = -1
)
dst = cv2.multiply( src1, src2[, dst[, scale[, dtype]]] )
除法 void cv::divide ( InputArray src1,
InputArray src2,
OutputArray dst,
double scale = 1,
int dtype = -1
)
void cv::divide ( double scale,
InputArray src2,
OutputArray dst,
int dtype = -1
)
dst = cv2.divide( src1, src2[, dst[, scale[, dtype]]] )

dst = cv2.divide( scale, src2[, dst[, dtype]] )

从上面可以看到C++接口的函数返回值都是void,返回图像都是通过dst传递出来的;mask掩码默认为noArray(),未传入掩码图像;scale参数默认为1,表示不作缩放;dtype为-1,根据src1和src2自动推导dst的数据类型,如果src1和src2图像的数据类型不一致时则需要显式的指定。

四则运算的函数入参几乎都长得一样,先做下入参的说明:

  • src1:源图像1,可以是图像对象或标量数据;
  • src2:源图像2,可以是图像对象或标量数据;
  • dst:目标图像,一般在Python接口中因为函数直接返回了新生成的目标图像,可以不传入;
  • mask:掩码;
  • scale:缩放比例,用于乘法和除法中,先和src1相乘再作用于src2,关于divide函数scale变量OpenCV官方文档写的有点歧义,OpenCV-Python教程:图像的除法运算中做了特别说明;
  • dtype:数据类型,如果src1和src2都是图像对象且数据类型一致,则可以不用设置;如果src1或src2其中1个是标量数据,另外一个是图像对象,也可以不设置,生成的目标图像数据类型和图像对象一致。如果src1和src2都是图像对象且数据类型不一致,则需要显式说明;

 

2、入参传递方法

我们先以add为例看下入参的书写形式:dst = cv.add( src1, src2[, dst[, mask[, dtype]]] ),这里dst之后的参数就不是必须要传入的参数,这种写法的含义表示如果在传参的时候不写形参名称,就必须按照位置参数的方式依次传递,第3个位置参数是dst,第4个位置参数为mask,第5个位置参数为dtype。如果不想传入dst或者mask参数,但是又必须传入dtype参数,一种方法是指明dtype参数名称的方式书写比如dtype=xxx,或者将第3和第4个位置参数传入None”占位”,再传第5个位置参数作为dtype:

dst = cv2.add(src1, src2, dtype=cv2.CV_8UC3)
dst = cv2.add(src1, src2, None, None, cv2.CV_8UC3)

下面是一个对比2种传参方式的完整例子,这个例子中构造了2个3通道2×5大小的图像对象(numpy数组),然后用不同的传参方式进行add()运算:

import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img1 = np.arange(0,2*5*3,1,dtype=np.uint8).reshape(2,5,3)
img2 = np.arange(200,200+2*5*3,1,dtype=np.uint8).reshape(2,5,3)
print('img1:\n',img1)
print('img2:\n',img2)

img_ret = cv2.add(img1,img2,None,None,cv2.CV_32FC3)
print('img_ret:\n',img_ret)
img_ret2 = cv2.add(img1,img2,dtype=cv2.CV_32FC3)
print('img_ret2:\n',img_ret2)

对比2种方法传参方法的效果是一样的:

img_ret:
 [[[200. 202. 204.]
  [206. 208. 210.]
  [212. 214. 216.]
  [218. 220. 222.]
  [224. 226. 228.]]

 [[230. 232. 234.]
  [236. 238. 240.]
  [242. 244. 246.]
  [248. 250. 252.]
  [254. 256. 258.]]]
img_ret2:
 [[[200. 202. 204.]
  [206. 208. 210.]
  [212. 214. 216.]
  [218. 220. 222.]
  [224. 226. 228.]]

 [[230. 232. 234.]
  [236. 238. 240.]
  [242. 244. 246.]
  [248. 250. 252.]
  [254. 256. 258.]]]

 

3、dst参数和返回值关系

dst参数在四则运算的Python接口中是可以不传值的,当不传值时函数返回值就是运算后的结果。如果dst传值,dst经过计算后的实例是否和函数返回值一致呢?下面通过一个例子来看下,这个例子中构造了2个单通道的3×5大小的图像对象(numpy数组),用id()函数获取dst和函数返回值img_ret的唯一标识符,二者相等说明是同一个实例,另外当修改img_ret后同时dst也被修改,这一点也证实二者确实是同一个实例:

import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img1 = np.arange(0,3*5,1,dtype=np.uint8).reshape(3,5) #创建3行5列数组
img2 = np.arange(200,200+3*5,1,dtype=np.uint8).reshape(3,5)
dst = np.zeros((3,5),dtype=np.uint8) 
print('img1:\n',img1)
print('img2:\n',img2)

img_ret = cv2.add(img1,img2,dst)
print('img_ret:\n',img_ret)
print('dst:\n',dst)

print('dst和img_ret是否同一实例:',id(dst) == id(img_ret))
img_ret[:,:2] = 0
print('img_ret:\n',img_ret)
print('dst:\n',dst)

运行结果:

img1:
 [[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]]
img2:
 [[200 201 202 203 204]
 [205 206 207 208 209]
 [210 211 212 213 214]]
img_ret:
 [[200 202 204 206 208]
 [210 212 214 216 218]
 [220 222 224 226 228]]
dst:
 [[200 202 204 206 208]
 [210 212 214 216 218]
 [220 222 224 226 228]]
dst和img_ret是否同一实例: True
img_ret:
 [[  0   0 204 206 208]
 [  0   0 214 216 218]
 [  0   0 224 226 228]]
dst:
 [[  0   0 204 206 208]
 [  0   0 214 216 218]
 [  0   0 224 226 228]]

 

4、图像和标量数据的运算

src1和src2如果都是图像对象(numpy数组),二者则要求shape属性一致,这样二者做四则运算时,相同下标的数据相互之间进行计算。如果其中之一为标量数据类型,则标量数据和图像对象的每一个元素都进行一次计算。

需要特别注意的是,如果其中的图像对象是多通道的数据时,但标量数据是单个数值,这时只会作用到图像对象的第1个通道,其他的通道则会和0进行计算,如果要多通道都和该数值作用,则需要构建一个包含4个数值的元组。即使是3通道的图像,也要构建一个4元组!

下面是一个3通道图像和单个数值进行计算的例子:

import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img1 = np.arange(0,2*3*3,1,dtype=np.uint8).reshape(2,3,3)
print('img1:\n',img1) 
img_add = cv2.add(img1,100)
print('img_add:\n',img_add) 
img_div = cv2.divide(100,img1)
print('img_div:\n',img_div)

运行结果:

cv2.__version__: 4.5.2
img1:
 [[[ 0  1  2]
  [ 3  4  5]
  [ 6  7  8]]

 [[ 9 10 11]
  [12 13 14]
  [15 16 17]]]
img_add:
 [[[100   1   2]
  [103   4   5]
  [106   7   8]]

 [[109  10  11]
  [112  13  14]
  [115  16  17]]]
img_div:
 [[[ 0  0  0]
  [33  0  0]
  [17  0  0]]

 [[11  0  0]
  [ 8  0  0]
  [ 7  0  0]]]

从运行结果看,单个数值只作用到了图像的第1通道上。如果要作用图像的多个通道,则需要传入一个包含4个数值的元组:

img1 = np.arange(0,2*3*3,1,dtype=np.uint8).reshape(2,3,3)
print('img1:\n',img1) 
img_add = cv2.add(img1,(100,100,100,0))  #包含4个元素的元组
print('img_add:\n',img_add) 
img_div = cv2.divide((100,100,100,0),img1)
print('img_div:\n',img_div)

运行结果:

img1:
 [[[ 0  1  2]
  [ 3  4  5]
  [ 6  7  8]]

 [[ 9 10 11]
  [12 13 14]
  [15 16 17]]]
img_add:
 [[[100 101 102]
  [103 104 105]
  [106 107 108]]

 [[109 110 111]
  [112 113 114]
  [115 116 117]]]
img_div:
 [[[  0 100  50]
  [ 33  25  20]
  [ 17  14  12]]

 [[ 11  10   9]
  [  8   8   7]
  [  7   6   6]]]

从前面的介绍可以看到除法运算有2种接口形式divide( src1, src2[, dst[, scale[, dtype]]] )、divide( scale, src2[, dst[, dtype]] ),第1种接口形式下src1或者src2可以是标量数据类型,但是当src1是一个数值型的标量数据类型时,这时第1个位置参数是当做标量数值只作用于第1个通道(第1种接口形式),还是当成float型的scale变量作用于所有的通道(第2种接口形式),就产生了参数解析的“二义性”,如果是在C++接口里就要报编译错误啦。在 OpenCV-Python教程:图像的除法运算 中我们看到这种情况实际是按照第1种形式下的标量数值来处理的。如果要调用第2种形式的接口必须显式地写明形参变量的名称。

 

5、dtype在什么时候需要显式声明

在src1和src2都是图像对象时,如果二者的数据类型不一致,无法自动推导出返回的图像实例该采用哪种数据类型,这时就需要传入dtype参数指明生成图像所采用的数据类型,否则会报“functions have different types”错误。需要注意的是dtype参数不是用numpy的uint8,float32等数据类型,而是采用OpenCV的CV_8U、CV_32F等数据类型。

import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)

img1 = np.arange(0,2*3,1,dtype=np.uint8).reshape(2,3)
img2 = np.arange(0,2*3,1,dtype=np.float32).reshape(2,3)
img_add = cv2.add(img1,img2,dtype=cv2.CV_8UC1)
print('img_add:\n',img_add) 
img_mult = cv2.multiply(img1,img2,dtype=cv2.CV_32FC1)
print('img_mult:\n',img_mult) 

运行结果:

cv2.__version__: 4.5.2
img_add:
 [[ 0  2  4]
 [ 6  8 10]]
img_mult:
 [[ 0.  1.  4.]
 [ 9. 16. 25.]]

 

小结:这篇文章总结了加减乘除四种运算的共性,比如dst入参的使用、入参传递的用法、标量和图像运算的特点、以及dtype参数在图像数据类型不一致时必须显式声明。

 

 原文链接:http://www.juzicode.com/archives/6109

扩展阅读:

  1. OpenCV-Python教程
  2. OpenCV-Python教程:图像的加法运算
  3. OpenCV-Python教程:图像的减法运算、标量加减运算
  4. OpenCV-Python教程:图像的乘法运算
  5. OpenCV-Python教程:图像的除法运算
上一篇:【python】匿名函数与装饰器


下一篇:DataFrame 求存在空值的行或列