原文链接:http://www.juzicode.com/archives/5321
通过前面的几篇文章我们了解了图像的加法、减法和乘法,今天就来聊聊除法。
1、图像除法divide()
divide()有2种用法:
dst = cv2.divide( src1, src2[, dst[, scale[, dtype]]] ):第1个和第2个位置参数都是图像对象,可选参数scale参数指定src1的放大倍数,dst=saturate(src1*scale/src2);
dst = cv2.divide( scale, src2[, dst[, dtype]] ):第1个位置参数为数值类型,第2个位置参数为图像对象,dst=saturate(scale/src2)。
如果是uint8等整数类型的除法,运算后的结果会做四舍五入取整。divide()除法也遵守“饱和运算规则”。
下面的例子分别用lena.jpg和opencv-logo.png互相作为除数和被除数计算:
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = cv2.imread('..\\lena.jpg')[0:512,0:512] #截取部分,保证大小一致
img2 = cv2.imread('..\\opencv-logo.png' )[0:512,0:512]
print('divide(img,img2):')
img_ret = cv2.divide(img,img2)
print('img[161,199]:',img[161,199])
print('img2[161,199]:',img2[161,199])
print('img_ret[161,199]:',img_ret[161,199])
print('img[100,200]:',img[100,200])
print('img2[100,200]:',img2[100,200])
print('img_ret[100,200]:',img_ret[100,200])
cv2.imshow('divide(img,img2)',img_ret)
print('divide(img2,img):')
img_ret = cv2.divide(img2,img)
print('img2[161,199]:',img2[161,199])
print('img[161,199]:',img[161,199])
print('img_ret[161,199]:',img_ret[161,199])
print('img2[100,200]:',img2[100,200])
print('img[100,200]:',img[100,200])
print('img_ret[100,200]:',img_ret[100,200])
cv2.imshow('divide(img2,img)',img_ret)
cv2.waitKey(0)
运行结果:
cv2.__version__: 4.5.2
divide(img,img2):
img[161,199]: [109 105 201]
img2[161,199]: [ 0 0 255]
img_ret[161,199]: [0 0 1]
img[100,200]: [118 119 209]
img2[100,200]: [ 0 0 255]
img_ret[100,200]: [0 0 1]
divide(img2,img):
img2[161,199]: [ 0 0 255]
img[161,199]: [109 105 201]
img_ret[161,199]: [0 0 1]
img2[100,200]: [ 0 0 255]
img[100,200]: [118 119 209]
img_ret[100,200]: [0 0 1]
img除以img2时[161,199]像素点的R通道相除的直接结果为201/255=0.8,四舍五入后的结果为1,divide()计算[161,199]像素点的R通道也为1。
从像素点的计算结果和显示的图像看,结算结果都是接近0的值,所以图像显示几乎都是黑色。
divide()方法还支持可选的scale参数,在2种接口中虽然名称上都叫scale,但是用法是有差异的,这个地方先挖个坑,后面再详细探讨。
2、符号除法/
符号除法实际就是numpy数组的除法。 下面的例子分别用lena.jpg和opencv-logo.png互相作为除数和被除数计算:
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = cv2.imread('..\\lena.jpg')[0:512,0:512] #截取部分,保证大小一致
img2 = cv2.imread('..\\opencv-logo.png' )[0:512,0:512]
print('img.dtype:',img.dtype)
print('img2.dtype:',img2.dtype)
print('img/img2:')
img_ret = img/img2
print('img_ret.dtype:',img_ret.dtype)
print('img[161,199]:',img[161,199])
print('img2[161,199]:',img2[161,199])
print('img_ret[161,199]:',img_ret[161,199])
print('img[100,200]:',img[100,200])
print('img2[100,200]:',img2[100,200])
print('img_ret[100,200]:',img_ret[100,200])
cv2.imshow('img/img2',img_ret)
print('img2/img:')
img_ret = img2/img
print('img2[161,199]:',img2[161,199])
print('img[161,199]:',img[161,199])
print('img_ret[161,199]:',img_ret[161,199])
print('img2[100,200]:',img2[100,200])
print('img[100,200]:',img[100,200])
print('img_ret[100,200]:',img_ret[100,200])
cv2.imshow('img2/img',img_ret)
cv2.waitKey(0)
运行结果:
VX公众号: 桔子code / juzicode.com
cv2.__version__: 4.5.2
img.dtype: uint8
img2.dtype: uint8
img/img2:
img_ret.dtype: float64 #新图像的数据类型发生了变化
char-divide-img.py:16: RuntimeWarning: divide by zero encountered in true_divide
img_ret = img/img2
char-divide-img.py:16: RuntimeWarning: invalid value encountered in true_divide
img_ret = img/img2
img[161,199]: [109 105 201]
img2[161,199]: [ 0 0 255]
img_ret[161,199]: [ inf inf 0.78823529]
img[100,200]: [118 119 209]
img2[100,200]: [ 0 0 255]
img_ret[100,200]: [ inf inf 0.81960784]
img2/img:
char-divide-img.py:26: RuntimeWarning: divide by zero encountered in true_divide
img_ret = img2/img
char-divide-img.py:26: RuntimeWarning: invalid value encountered in true_divide
img_ret = img2/img
img2[161,199]: [ 0 0 255]
img[161,199]: [109 105 201]
img_ret[161,199]: [0. 0. 1.26865672]
img2[100,200]: [ 0 0 255]
img[100,200]: [118 119 209]
img_ret[100,200]: [0. 0. 1.22009569]
imread()读入图像默认的数据类型为uint8,符号除法得到的数据类型发生了改变,变成了float64。
当除数为0时,numpy除法打印了告警信息:divide by zero encountered in true_divide,直接计算结果为inf,但是转换为OpenCV的图像是有实际意义的0。
3、divide()除法中的0
从前面计算结果看,当有元素为0且作为被除数时,divide()计算仍然是有实际意义的,这点和常规的理解存在差异。为了方便观察,我们用numpy构造图像(实际是numpy数组)来看看如果0参与了图像除法会是什么效果。
这里实验只需要生成一个单通道的图像,因为在OpenCV中多通道图像的运算实际上仍然是分成多个单通道单独处理的,实验时只使用单通道效果是一样的。
先看uint8类型(对应OpenCV的CV_8U)的0作为除数:
import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = np.arange(0,128*256,1,dtype=np.uint8).reshape(128,256)
img[0:64,:] = 0 #前64行设置为0
img2 = np.zeros((128,256),dtype=np.uint8)
img2[:,128:256] = 255 #后128列设置为255
print('img:\n',img)
print('img2:\n',img2)
img_ret = cv2.divide(img,img2)
print('divide(img,img2):\n',img_ret)
cv2.imshow('divide(img,img2)',img_ret)
cv2.waitKey(0)
运行结果:
cv2.__version__: 4.5.2
img:
[[ 0 0 0 ... 0 0 0]
[ 0 0 0 ... 0 0 0]
[ 0 0 0 ... 0 0 0]
...
[ 0 1 2 ... 253 254 255]
[ 0 1 2 ... 253 254 255]
[ 0 1 2 ... 253 254 255]]
img2:
[[ 0 0 0 ... 255 255 255]
[ 0 0 0 ... 255 255 255]
[ 0 0 0 ... 255 255 255]
...
[ 0 0 0 ... 255 255 255]
[ 0 0 0 ... 255 255 255]
[ 0 0 0 ... 255 255 255]]
divide(img,img2):
[[0 0 0 ... 0 0 0]
[0 0 0 ... 0 0 0]
[0 0 0 ... 0 0 0]
...
[0 0 0 ... 1 1 1]
[0 0 0 ... 1 1 1]
[0 0 0 ... 1 1 1]]
img的前面64行全0,后64行每行的数值从0~255,img2的左边128列为0,右边128列为255,这样2幅图像大概就分成左上、右上、左下、右下4个区域,左上角img全0除以img2的全0得到的结果为0,右上角img全0除以img2右上角255得到的结果为0,左下角img非0除以img2的全0得到的结果为0,右下角img非0除以img2非0,最后结果四舍五入取整。从运行结果看,当除数为0时,其计算结果记为0,如果被除数和除数都为0,结果也仍然为0,有0参与的除法运算都为0。
再以浮点类型的float32(对应OpenCV的CV32F)为例,当仅仅除数为0,以及被除数和除数都为0的情况:
import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = np.arange(0,128*256,1,dtype=np.float32).reshape(128,256)
img[0:64,:] = 0 #前64行设置为0
img2 = np.zeros((128,256),dtype=np.float32)
img2[:,128:256] = 255 #后128列设置为255
print('img:\n',img)
print('img2:\n',img2)
img_ret = cv2.divide(img,img2)
print('divide(img,img2):\n',img_ret)
cv2.imshow('divide(img,img2)',img_ret)
cv2.waitKey(0)
运行结果:
cv2.__version__: 4.5.2
img:
[[ 0. 0. 0. ... 0. 0. 0.]
[ 0. 0. 0. ... 0. 0. 0.]
[ 0. 0. 0. ... 0. 0. 0.]
...
[32000. 32001. 32002. ... 32253. 32254. 32255.]
[32256. 32257. 32258. ... 32509. 32510. 32511.]
[32512. 32513. 32514. ... 32765. 32766. 32767.]]
img2:
[[ 0. 0. 0. ... 255. 255. 255.]
[ 0. 0. 0. ... 255. 255. 255.]
[ 0. 0. 0. ... 255. 255. 255.]
...
[ 0. 0. 0. ... 255. 255. 255.]
[ 0. 0. 0. ... 255. 255. 255.]
[ 0. 0. 0. ... 255. 255. 255.]]
divide(img,img2):
[[ nan nan nan ... 0. 0. 0. ]
[ nan nan nan ... 0. 0. 0. ]
[ nan nan nan ... 0. 0. 0. ]
...
[ inf inf inf ... 126.48235 126.486275 126.4902 ]
[ inf inf inf ... 127.486275 127.4902 127.49412 ]
[ inf inf inf ... 128.49019 128.49411 128.49803 ]]
仍然像uint8类型那样2幅图像大概就分成左上、右上、左下、右下4个区域,左上角被除数和除数都为0时,计算的结果为nan,转换为图像显示为黑色的数值0。左下角被除数非0除数为0时,计算的结果为inf,虽然直观理解inf为无穷大显示应该为白色,但是实际上并不是白色而是黑色。我们可以用astype()转换检查下:
img_ret2 = img_ret.astype(np.uint8)
print('img_ret2:\n',img_ret2)
运行结果:
img_ret2:
[[ 0 0 0 ... 0 0 0]
[ 0 0 0 ... 0 0 0]
[ 0 0 0 ... 0 0 0]
...
[ 0 0 0 ... 126 126 126]
[ 0 0 0 ... 127 127 127]
[ 0 0 0 ... 128 128 128]]
从astype()转换的结果看inf和nan都转换成了0。
在OpenCV的C++接口中,如果除数为0以及除数和被除数都为0的情况会是怎样呢?下面我们写个测试例子看下:
//VX公众号:桔子code / juzicode.com
#include "opencv2/opencv.hpp"
#include "iostream"
using namespace std;
using namespace cv;
int main()
{
Mat img = Mat(5, 5, CV_32FC3, Scalar(5,0,0));
cout << "img:" << endl << img << endl;
Mat img2 = Mat(5, 5, CV_32FC3, Scalar(0,0,5));
cout << "img2:" << endl << img2 << endl;
Mat img_ret;
divide(img, img2, img_ret);
cout << "img_ret:" << endl << img_ret << endl;
//imshow("img_ret", img_ret);
//waitKey();
return 0;
}
运行结果:
img:
[5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0;
5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0;
5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0;
5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0;
5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0]
img2:
[0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5;
0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5;
0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5;
0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5;
0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5, 0, 0, 5]
img_ret:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
从运行结果看,C++接口的divide()计算中除数为0以及除数和被除数都为0时,计算结果也都为0.
4、divide()的scale参数
在cv2.divide( src1, src2[, dst[, scale[, dtype]]] )中的scale参数必须是数值型数据,先用src1乘以scale再除以src2,如果src1是多通道图像,scale会作用到src1的所有通道上。
import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = np.arange(0,30,1,dtype=np.float32).reshape(2,5,3)
print('img:\n',img)
img2 = np.arange(50,80,1,dtype=np.float32).reshape(2,5,3)
print('img2:\n',img2)
img_ret2 = cv2.divide(img2,img)
print('divide(img2,img):\n',img_ret2)
img_ret3 = cv2.divide(img2,img,scale=10)
print('divide(img2,img,scale=10):\n',img_ret3)
运行结果:
cv2.__version__: 4.5.2
img:
[[[ 0. 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.]]]
img2:
[[[50. 51. 52.]
[53. 54. 55.]
[56. 57. 58.]
[59. 60. 61.]
[62. 63. 64.]]
[[65. 66. 67.]
[68. 69. 70.]
[71. 72. 73.]
[74. 75. 76.]
[77. 78. 79.]]]
divide(img2,img):
[[[ inf 51. 26. ]
[17.666666 13.5 11. ]
[ 9.333333 8.142858 7.25 ]
[ 6.5555553 6. 5.5454545]
[ 5.1666665 4.8461537 4.571429 ]]
[[ 4.3333335 4.125 3.9411764]
[ 3.7777777 3.631579 3.5 ]
[ 3.3809524 3.2727273 3.173913 ]
[ 3.0833333 3. 2.9230769]
[ 2.851852 2.7857144 2.724138 ]]]
divide(img2,img,scale=10):
[[[ inf 510. 260. ]
[176.66667 135. 110. ]
[ 93.333336 81.42857 72.5 ]
[ 65.55556 60. 55.454544]
[ 51.666668 48.46154 45.714287]]
[[ 43.333332 41.25 39.411766]
[ 37.77778 36.31579 35. ]
[ 33.809525 32.727272 31.73913 ]
[ 30.833334 30. 29.23077 ]
[ 28.518518 27.857143 27.241379]]]
从上面对比scale参数是否传值的结果看,scale=10的时候所有通道的值都放大了10倍。
cv2.divide(scale, src2[, dst[, dtype]] )的scale参数虽然名称和前者一样,直接按照位置参数使用时效果却不一样。scale参照C++接口是double类型,转换为Python为float类型,我们看下如果是单个float类型的数值,会是什么效果。这里为了方便观察我们构造一个3通道的图像(numpy数组),然后用一个float数值传给scale:
import numpy as np
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img = np.arange(0,30,1,dtype=np.float32).reshape(2,5,3)
print('img:\n',img)
img_ret = cv2.divide(250.0,img)
print('divide(2500.0,img):\n',img_ret)
运行结果:
cv2.__version__: 4.5.2
img:
[[[ 0. 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.]]]
divide(250.0,img):
[[[ inf 0. 0. ]
[83.333336 0. 0. ]
[41.666668 0. 0. ]
[27.777779 0. 0. ]
[20.833334 0. 0. ]]
[[16.666666 0. 0. ]
[13.888889 0. 0. ]
[11.904762 0. 0. ]
[10.416667 0. 0. ]
[ 9.259259 0. 0. ]]]
从运行结果看scale参数只作用到了src1的第1个通道,scale被第2、3通道用过除法之后得到的结果都为0,相当于用0除以2、3通道。这一点和OpenCV-Python教程:图像的减法运算、标量加减运算中多通道图像和一个数值相减的效果类似,这个数值只作用域第1通道,其他的通道相当于减去0。这种用法相当于divide()的第1种用法中src1是标量类型且scale=1的特例。
要想达到单个数值型的scale和图像所有通道都起作用,可以用下面这种显式声明形参的方式:
img_ret = cv2.divide(scale=250.0,src2=img)
print('divide(250.0,img):\n',img_ret)
运行结果:
divide(250.0,img):
[[[ inf 250. 125. ]
[ 83.333336 62.5 50. ]
[ 41.666668 35.714287 31.25 ]
[ 27.777779 25. 22.727272]
[ 20.833334 19.23077 17.857143]]
[[ 16.666666 15.625 14.705882]
[ 13.888889 13.157895 12.5 ]
[ 11.904762 11.363636 10.869565]
[ 10.416667 10. 9.615385]
[ 9.259259 8.928572 8.620689]]]
从运行结果看,第2、3通道的数值不再为0,而是250除以src2图像的值,这时单个的scale数值已经作用到了第2和第3通道,符合函数的定义。
小结:图像的除法运算divide()和add()、subtract()、multiply()一样也遵守“饱和运算”规则,最终的计算结果在特定的数据类型表示范围内截断。除法运算中当元素值为0时,不管是除数或被除数为0,计算的结果都为0。scale参数的用法比较特殊,要想实现scale/src2的用法,必须显式地声明形参的名称。
原文链接:http://www.juzicode.com/archives/5321