原文链接:http://www.juzicode.com/archives/6109
返回Opencv-Python教程
在前面的4篇文章中我们分别介绍了图像的加减乘除四种运算,这四种运算函数接口长得比较像,用法类似,有必要总结对比下。
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图像的数据类型不一致时则需要显式的指定。
四则运算的函数入参几乎都长得一样,先做下入参的说明:
我们先以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.]]]
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]]
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种形式的接口必须显式地写明形参变量的名称。
在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