原文链接:http://www.juzicode.com/opencv-python-add-subtract-multiply-divide
在前面的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