OpenCV-Python教程:图像的除法运算(divide)

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

返回Opencv-Python教程

通过前面的几篇文章我们了解了图像的加法、减法和乘法,今天就来聊聊除法。

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

扩展阅读:

  1. OpenCV-Python教程

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注