OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理

☞ ░ 前往老猿Python博客░ https://blog.csdn.net/LaoYuanPython

一、引言

在《OpenCV-Python图像的加法运算cv2.add函数详解》及《OpenCV-Python图像的减法运算cv2.subtract函数详解以及和矩阵减法的差异对比》详细介绍了图像的加法运算和减法运算,有加减法就有乘除法,本文介绍图像的乘法运算。

图像的乘法有三种,具体参考《对OpenCV中3种乘法操作的理解掌握》,我们在此只关注最后一种,也即cv2.multiply函数提供的乘法。对于两个图像矩阵A、B来说:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第1张图片
该种方式的乘法计算方法如下:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第2张图片

二、图像乘法cv2.multiply的语法

调用语法:

multiply(src1, src2, dst=None, scale=None, dtype=None)

参数说明:

OpenCV手册介绍的乘法相关语法内容解读如下:

  1. src1:作为被乘数的图像数组
  2. src2:作为乘数的图像数组,大小和类型与src1相同
  3. dst:可选参数,输出结果保存的变量,默认值为None,如果为非None,输出图像保存到dst对应实参中,其大小和通道数与输入图像相同,图像的深度(即图像像素的位数)由dtype参数或输入图像确定
  4. scale:可选的结果图像缩放因子,即在src1*src2的基础上再乘scale
  5. mask:图像掩膜,可选参数,为8位单通道的灰度图像,用于指定要更改的输出图像数组的元素,即输出图像像素只有mask对应位置元素不为0的部分才输出,否则该位置像素的所有通道分量都设置为0
  6. dtype:可选参数,输出图像数组的深度,即图像单个像素值的位数(如RGB用三个字节表示,则为24位)。
  7. 返回值:相乘的结果图像

三、图像乘法cv2.multiply的使用场景

关于src1和src2这两个输入数据上,OpenCV帮助文档中乘法和加法、减法的说明不一样,加法和减法中可以使用标量,而在乘法中说明是二者必须大小和类型相同,没有说可以使用标量。我们参考加减法的模式来进行验证说明。

本部分处理案例的源图像使用图片imgs.jpg是一张多个OpenCV处理经典图像的合集,图像如下:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第3张图片
由于图像比较大不好截图,因此后面读取图像时将其强制调整为1000*750.

3.1、src2为标量的情况

以一副图像和一个标量相乘来观察,代码如下:

import numpy as np
import cv2
def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    imgMultiply = cv2.multiply(img,1.2)
    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

我们通过程序调试来观察相乘的结果,在显示图片前设置断点,观看相关变量数据,如图:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第4张图片
从上述标黄色部分的矩阵通道值可以看出,直接用一个常数标量相乘后,原图像矩阵的像素后2个通道值在结果图像中全部为0。继续运行显示图像如下:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第5张图片
可以看到图像为蓝色,与上面三通道只留了第一个通道蓝色通道的数据的一致。

3.2、src2为一个四元组

在前面介绍加减法时说明了OpenCV在对标量运算时转为了四元组进行运算,因此在此我们将上面案例的标量改为四元组再验证。代码如下:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    imgMultiply = cv2.multiply(img,(1.5,1.5,1.5,1.5))
    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

经跟踪观察,所有像素通道分量都乘以了1.5,超过255的按饱和运算模式置为255.显示图像如下:

OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第6张图片
上图跟原图对比,亮度明显增强。实际上当src2设置为一个元素值相等的四元组时,其效果等同于设置为四个元素为1的四元组和图像设置缩放因子为该元素值的情况。我们将上述代码调整为如下:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    imgMultiply = cv2.multiply(img,(1,1,1,1),scale=0.5)
    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

注意:缩放因子之前还有个可选参数dst,因此需要用关键字参数形式。
对应结果图像如下:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第7张图片
可以看到图像亮度明显变暗,但对比度没有变化。

综合3.1和3.2验证的情况(老猿的验证环境为OpenCV-Python4.3.0.36,windows版本),src2可以是一个四元组,当我们想使用multiply调整图像亮度时,可以使用src2为一个四元组,该四元组的所有元素值即为要调整的亮度因子,也可以使用元素全为1的四元组叠加scale调整因子的情况来实现。但这2种形式在OpenCV帮助文档中没有说明,因此虽然现在可行但老猿不建议使用。如果要直接调整图像的亮度,建议直接进行矩阵和标量的算术乘法即可,只是需要注意要采用饱和运算模式。

3.3、彩色图像数组和掩膜图像数组相乘

使用如下代码进行彩色图像和掩膜图像的乘法运算:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    mask = np.ones(img.shape[:2],np.uint8)
    mask[100:200,100:200] = 0
    imgMultiply = cv2.multiply(img,mask,scale=0.5)

    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

执行时报错:

"C:\Program Files\Python38\python.exe" F:/study/python/project/cvtest/cvtest.py
Traceback (most recent call last):
  File "F:/study/python/project/cvtest/cvtest.py", line 100, in <module>
    main()
  File "F:/study/python/project/cvtest/cvtest.py", line 94, in main
    imgMultiply = cv2.multiply(img,mask,scale=0.5)
cv2.error: OpenCV(4.3.0) C:\projects\opencv-python\opencv\modules\core\src\arithm.cpp:669: error: (-209:Sizes of input arguments do not match) The operation is neither 'array op array' (where arrays have the same size and the same number of channels), nor 'array op scalar', nor 'scalar op array' in function 'cv::arithm_op'

可见不能这样操作。

3.4、彩色图像数组和彩色图像数组相乘

我们使用如下代码构建两个彩色图像数组的乘法运算,其中的第二个图像是基于原图像同样大小和通道数的矩阵图像,其元素取值只有0和1:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    mask = np.ones(img.shape,np.uint8)
    mask[500:650,10:300] = 0
    imgMultiply = cv2.multiply(img,mask,scale=0.8)

    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

执行后结果图像截图:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第8张图片
可以看到左下角图片部分被黑色替代,如果想使用白色替代,将mask设置为0的通道调整为一个比较大的值使得相乘时饱和运算后值为255即可。如:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    mask = np.ones(img.shape,np.uint8)
    mask[500:650,10:300] = 100
    imgMultiply = cv2.multiply(img,mask,scale=0.8)

    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

执行结果截图:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第9张图片
可以看到,通过这样的两个图像相乘,可以决定结果图像哪些部分保留哪些部分清除。

3.5、彩色图像饱和运算乘

如果两副图像相乘甚至是一副图像自乘,由于图像自身像素通道值一般大于为0或大于1的数,而图像运算是饱和运算,就会导致两副图像任意一副为黑色的部分在结果图像中都为黑色,而其他部分通道值大部分都会大幅增长甚至达到饱和。

上面原图自乘的代码如下:

def  main():
    img = cv2.resize(cv2.imread(r'F:\pic\imgs.jpg'),(1000,750))
    imgMultiply = cv2.multiply(img,img)

    cv2.imshow('img', img)
    cv2.imshow('imgMultiply', imgMultiply)
    cv2.waitKey(0)

main()

自乘之后的结果图像如下:
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第10张图片
所以这种真正的两副图像相乘在低级图像处理来说基本没有意义,中高级图像处理方面老猿没有研究不清楚是否有特殊用途。

3.6、彩色图像乘积的溢出处理

3.6.1、溢出的解决方式

图像的单个通道值用一个无符号字节(numpy.uint8)表示,其值范围在0-255之内,如果图像像素通道值超出范围时,OpenCV的饱和运算机制会将小于0的通道值置为0,大于255的置为255。但这种机制在某些情况下会导致图像像素通道值大范围的被置为255或0。例如从上面3.4可以看到真正两个图像的乘积由于饱和元素导致图像大范围白化。

为了解决这个问题,可以采用如下方法处理:

  1. 乘法时,将图像乘积结果保存到通道值uint16或float32的矩阵中(由于float32可以保存小数,因此老猿建议为float32),这可能有2种方式,一种是将原输入图像矩阵在执行乘运算前将其转为float32矩阵,二是原输入图像矩阵保持不变,将乘积保存到float32图像矩阵,但老猿目前没有在OpenCV-Python中找到第二种方式的实现,只验证通过了先将输入图像转为float32的方式;
  2. 执行乘法的乘积矩阵进行归一化处理。关于归一化处理请参考《opencv中归一化函数normalize()的原理讲解》。
3.6.2、归一化处理的算法考虑

使用OpenCV-Python的normalize函数的NORM_MINMAX模式将矩阵元素值归到[0,255]区间范围内时,OpenCV采用的算法是:
在这里插入图片描述

这个算法的思想是将原矩阵中最大元素值映射为区间的上限如255,将原矩阵中最小元素值映射为区间的下限如0,其他中间值映射为该值与矩阵中最小元素值的差与原矩阵中最大最小元素值差的比例乘以映射区间的乘积再加上映射区间下限值,即映射过程的线性变换是以原元素值与原矩阵最小值的差异值作为线性变换的,而不是直接以元素值作为线性变换的。

从算法上来说,这种方式比较科学,但老猿认为图像处理的情况比较特殊,如果一副图像全部是由深色图像构成,所有像素的每个通道值都比较大,就会导致图像的乘积归一化到[0-255]之间时差异会很小,并且图像单通道都在[0-255]之间,因此两个图像的乘积特别是自乘的乘积理论上以0作为下限来考虑进行归一化处理可能在特定场景下更合理,当然在其他归一化场景下(如图像加法)可能OpenCV采用的NORM_MINMAX模式更合理。

3.6.3、图像自乘及归一化处理案例

针对OpenCV-Python的归一化和老猿考虑的归一化两种算法我们来实现图片imgs.jpg的图像自乘之后的归一化效果对比。对应代码如下:

def main():
    img = cv2.imread(r'F:\pic\imgs.jpg')
    img32 = img.astype(np.float32)

    imgMultiply = cv2.multiply(img32, img32)

    imgNormalizeOpenCV = cv2.normalize(imgMultiply,None,0,255,cv2.NORM_MINMAX) #opencv归一化处理

    #老猿的归一化处理
    maxv = np.max(imgMultiply)
    imgNormalizeLaoyuan = (imgMultiply / maxv) * 255

    cv2.imshow('imgNormalizeOpenCV', imgNormalizeOpenCV.astype(np.uint8))
    cv2.imshow('imgNormalizeLaoyuan', imgNormalizeLaoyuan.astype(np.uint8))

    cv2.waitKey(0)


main()

我们将原图、两种归一化处理的图竖直叠加在一起对比一下看看图像自乘再归一化处理的效果。下面图片中第一行图片是原图的,第二行是OpenCV归一化处理的,第三行是老猿的归一化方式处理。
OpenCV-Python图像乘法运算cv2.multiply函数详解及像素值溢出归一化处理_第11张图片
从上面图片对照可以看出,自乘归一化处理后图像的对比度增大,这是因为自乘后放大了差异,归一化处理不会改变这种变化趋势。同时可以看到两种归一化处理图像效果差不多,老猿没有仔细分析数据,想来也差不多,这是因为imgs.jpg这幅图像黑色和白色都存在,导致这两种算法处理时实际结果应该基本相同。

四、小结:

本文详细介绍了OpenCV-Python图像乘法运算cv2.multiply函数的调用语法,并分析了OpenCV乘法的几种使用场景以及图像溢出的归一化处理,通过这些分析可以知道OpenCV图像的乘法运算主要有三种作用:

  1. 图像和标量的乘法可以调节图像的明暗度;
  2. 图像和掩膜的乘法可以控制输出图像的范围;
  3. 图像自乘可以调节图像的对比度。

更多OpenCV-Python的介绍请参考专栏《OpenCV-Python图形图像处理 》

博文地址:https://blog.csdn.net/laoyuanpython/category_9979286.html

关于老猿的付费专栏

老猿的付费专栏《使用PyQt开发图形界面Python应用 》(https://blog.csdn.net/laoyuanpython/category_9607725.html)专门介绍基于Python的PyQt图形界面开发基础教程,付费专栏《moviepy音视频开发专栏》 (https://blog.csdn.net/laoyuanpython/category_10232926.html)详细介绍moviepy音视频剪辑合成处理的类相关方法及使用相关方法进行相关剪辑合成场景的处理,两个专栏都适合有一定Python基础但无相关知识的小白读者学习。

付费专栏文章目录:《moviepy音视频开发专栏文章目录》(https://blog.csdn.net/LaoYuanPython/article/details/107574583)、《使用PyQt开发图形界面Python应用专栏目录 》(https://blog.csdn.net/LaoYuanPython/article/details/107580932)。

对于缺乏Python基础的同仁,可以通过老猿的免费专栏《专栏:Python基础教程目录》(https://blog.csdn.net/laoyuanpython/category_9831699.html)从零开始学习Python。

如果有兴趣也愿意支持老猿的读者,欢迎购买付费专栏。

跟老猿学Python、学OpenCV!

☞ ░ 前往老猿Python博文目录 https://blog.csdn.net/LaoYuanPython ░

你可能感兴趣的:(老猿Python,opencv,python,图像乘法运算,图形图像处理,计算机视觉)