储存图像数据的Mat对象

为了避免命名冲突,本书使用如下方式载入pyopencv模块:

import pyopencv as cv

让我们从读入并显示一幅图像开始,在IPython中运行如下语句,将会看到一个显示美女“lena”的窗口。

12-opencv/opencv_show_img.py

用OpenCV显示一幅图片

>>> img = cv.imread("lena.jpg")
>>> cv.namedWindow("demo1")
>>> cv.imshow("demo1", img)

首先imread()从指定的文件路径读入图像数据,它支持许多常用的图像格式,在不同的操作系统下它所支持的图像格式可能有所不同。请读者阅读C++文档了解imread()的详细信息。此外,OpenCV还提供了imwrite()将图像数据写入文件。

如果不是在交互式环境下运行上面的程序,则需在程序最后添加“cv.waitKey(0)”,否则图像窗口在显示之后将立即关闭,结束程序运行。这里waitKey()等待用户按下按键,其参数为等待的毫秒数,0表示永远等待。

为了方便用户快速观察图像处理的效果,OpenCV提供了一些简单的GUI功能。这里调用namedWindow(),创建一个名为“demo1”的窗口,最后调用imshow()将图像显示到所创建的窗口中。imshow()的第一个参数是窗口名,第二个参数是表示图像的Mat对象。实际上,如果第一个参数所指定的窗口不存在,则imshow()会自动创建一个新窗口,因此这里可以省略namedWindow()的调用。

在OpenCV中,Mat对象表示图像,它是整个系统的核心,几乎所有的函数都和它相关。下面我们从它的基本属性开始逐步学习。

>>> type(img)
<class 'pyopencv.cxcore_hpp_point_ext.Mat'>
>>> img.size()
Size2i(width=512, height=393)
>>> img.channels()
3
>>> img.depth()
0
>>> img.type()
16

Mat对象提供了size()、channels()和depth()等方法分别获得图像的大小、通道数和数值类型。上面的例子中,图像img的宽为512个像素,高为393个像素,有3个通道(channels),即图像中的每个像素的颜色用三个数值表示。每个通道的数值类型(depth)的编号为0,0表示无符号8bit整数。另外还有type()方法可以获得一个同时表示通道数和数值类型的编号,我们可以将它理解为像素类型。关于这方面的内容将在下一节详细介绍。

Mat对象有许多属性和方法,请读者使用IPython的自动完成功能查看img对象的属性和方法,并和C++文档的说明进行比较。例如我们可以找到一个row()方法,它获得图像的指定行,返回一个新的Mat对象,但是它和原图像共享图像数据,这一点和NumPy的数组视图很相似。

>>> type(img.row(10))
<class 'pyopencv.cxcore_hpp_point_ext.Mat'>
>>> img.row(10).size()
Size2i(width=512, height=1)

Mat和NumPy数组

Mat对象本身提供的众多属性和方法并不符合Python的风格,因此PyOpenCV对Mat类进行了扩展,使得它能像NumPy的数组一样使用。下面的程序用“img[:]”从Mat对象img创建一个表示整个图像的数组,并查看数组的shape和strides属性:

>>>  img[:].shape
(393, 512, 3)
>>>  img[:].strides
(1536, 3, 1)

请注意Mat对象本身并不是数组,因此它没有shape属性:

>>> img.shape
AttributeError: 'Mat' object has no attribute 'shape'

仔细观察数组的shape属性,可以发现数组的第0轴的长度为图像的高度,第1轴的长度为图像的宽度,而第2轴的长度为图像的通道数。由strides属性值可知,每个通道的数据占用一个字节,而一个像素点占用3个字节,而一行数据占用512*3=1536个字节。因此图像数据在内存中是连续存储的。

下面的程序获得图像的第1行到第3行,第5列到第9列,第0通道的数据,所得到的数组a和Mat对象共享图像数据:

>>> a = img[1:4,5:10,0]
>>> a.strides # 由于a和img共享图像数据,因此第0轴的stride仍然是1536个字节
(1536, 3)
>>> a
array([[109, 108, 107, 109, 107],
       [108, 107, 106, 110, 107],
       [106, 106, 106, 113, 108]], dtype=uint8)

修改a[0,0]将同时修改img[1,5,0],它们在内存中的地址是相同的:

>>> a[0,0] = 200 # a和img共享图像数据
>>> img[1,5,0]
200

使用下标语法也可以直接设置Mat对象中的数据。例如下面将第0和1通道的数据都设置为0:

>>> img[:,:,0] = 0
>>> img[:,:,1] = 0
>>> cv.imshow("demo1", img)

由于在使用imread()读入的Mat对象中,三个通道分别与蓝绿红三个颜色相对应,上面的程序将蓝绿通道的数值都设置为0,因此显示一幅全是红色的图像。

实际上在OpenCV内部每个通道并没有固定对应某种颜色,只是在用imshow()、imread()和imwrite()等函数时,才将通道按照蓝绿红的顺序进行输入和输出。

我们也可以使用matplotlib的imshow()绘制图像,但是由于它要求图像的三个通道的存储顺序为红绿蓝,因此需要将其第2轴进行反向:

>>> img = cv.imread("lena.jpg")
>>> import pylab as pl
>>> pl.imshow(img[:,:,::-1]) # 第2轴反向
>>> pl.show()

另外,使用Mat对象的ndarray属性也可以获得数组:

>>> img.ndarray.shape
(393, 512, 3)

Mat.ndarray实际上是一个Property属性,每次读取它时都会调用某个函数得到一个新的数组。下面通过id()查看img.ndarray的内存地址,两次结果不相同,表示它们是不同的数组。但是请注意,这样两个数组的数据存储区是相同的,都和原始的Mat对象img共享图像数据:

>>> img.__class__.ndarray
<property object at 0x01C47AE0>
>>> id(img.ndarray)
49696480
>>> id(img.ndarray) # 两次通过ndarray获得的数组不是同一个数组
49770224

也可以使用asMat()函数从数组创建Mat对象,这样我们就能用OpenCV的图像处理功能对NumPy数组进行处理。下面的程序演示了这一过程,其运行效果如【图:使用Laplacian算子对从NumPy产生的图像进行边缘检测】所示。

12-opencv/opencv_numpy2mat.py

将数组转换为Mat对象并进行图像处理

import pyopencv as cv
import numpy as np

y, x = np.ogrid[-1:1:250j,-1:1:250j]
z = np.sin(10*np.sqrt(x*x+y*y))*0.5 + 0.5 #
np.round(z, decimals=1, out=z) #

img = cv.asMat(z) #

cv.namedWindow("demo1")
cv.imshow("demo1", img)

img2 = cv.Mat() #
cv.Laplacian(img, img2, img.depth(), ksize=3) #

cv.namedWindow("demo2")
cv.imshow("demo2", img2)
cv.waitKey(0)
/tech/static/books/scipy/_images//opencv_numpy2mat.png

使用Laplacian算子对从NumPy产生的图像进行边缘检测

❶首先数组z是二元函数\frac{1}{2} \sin(10 \sqrt{x^2+y^2}) + \frac{1}{2}在(-1,-1)到(1,1)的网格上的计算结果,它的取值范围是0到1。❷为了后续的图像处理函数能够正常工作,这里对数组z中的数值保留小数点后一位的精度。❸使用asMat()将数组z转换为图像img,图像img将和数组z共享图像数据,读者可以自行验证。

❹为了进行图像处理,先创建一个空图像img2,当将空图像传递给某个OpenCV的图像处理函数时,OpenCV会自动为其分配内存以保存结果。如果img2不是空图像,并且它的大小、通道数和数值类型都满足图像处理函数的输出要求时,将不会分配额外的内存,而是直接把结果保存进它的图像数据区。

❺调用Laplacian()对img进行边缘检测,并将结果保存进img2。在本例中,图像的像素类型为单通道的双精度浮点数,因此它是一幅灰度图,其中0表示黑色,1表示白色。

在使用asMat()进行转换时,它会首先尝试将数组的最后一个轴转换为图像的通道,因此用下面的程序不能将一个4*3的二维数组转换为单通道的4*3的Mat对象:

>>> a = np.zeros((4,3))
>>> b = cv.asMat(a)

转换后的结果是一个1*4的三通道Mat对象:

>>> b.size()
Size2i(width=4, height=1)
>>> b.channels()
3

如果设置asMat()的force_single_channel参数为True,则强制产生单通道的Mat对象:

>>> cv.asMat(a,force_single_channel=True).size()
Size2i(width=3, height=4)

除了Mat对象之外,OpenCV还提供了MatND对象表示多维数组。下面调用asMatND()将三维数组转换成MatND对象,并通过dims属性查看其维数:

>>> c = cv.asMatND(np.zeros((10,20,30)))
>>> c.dims
3

MatND对象使用size和step属性保存每个轴的长度和每个轴的字节偏移量,它们和NumPy数组的shape和strides属性类似。但是这两个属性都是长度固定为32的整数数组。因此我们需要使用dims属性获取其中所需的部分:

>>> c.size # 得到一个长度固定为32的整数数组对象
<pyopencv.cxcore_h_ext.__array_1_int_32 object at 0x04A4C688>

这个数组对象可以使用正整数下标获取其中的元素,或者使用len()获取其长度,除此之外没有提供其它的元素的存取方法。因此为了访问方便我们可以调用list()将其转换为列表:

>>> list(c.size)
[10, 20, 30, 0, 0, 0, 0, ...]
>>> list(c.size)[:c.dims]
[10, 20, 30]
>>> list(c.step)[:c.dims]
[4800, 240, 8]

和Mat对象一样,MatND对象也可以很方便地转换为数组:

>>> c[:].shape
(10, 20, 30)
>>> c[:].strides
(4800, 240, 8)
>>> c[:].dtype
dtype('float64')

像素点类型

图像中的每个像素点可能有多个通道,例如用单通道可以表示灰度图像,而用红绿蓝三个通道[1]表示彩色图像、用四个通道表示带透明度(alpha)的彩色图像。而每个通道的值可以是不同的数值类型,例如通常的图像用8位无符号整数表示,而医学图像可能会用16位整数表示图像数据。因此像素点的类型由通道数和通道中每个数值的类型决定。

用Mat对象的type()方法可以获得图像的像素点的类型,而channels()方法可以获得像素点的通道数,depth()方法可以获得表示通道数据的数值类型。type()和depth()返回的都是一个整数序号,每个整数所表示的类型都在OpenCV的头文件“cxtypes.h”中使用“#define”定义。下面截取其中的一部分:

#define CV_8U   0
#define CV_8S   1
#define CV_16U  2
#define CV_16S  3
// ...省略...
#define CV_8UC1 CV_MAKETYPE(CV_8U,1)
#define CV_8UC2 CV_MAKETYPE(CV_8U,2)
#define CV_8UC3 CV_MAKETYPE(CV_8U,3)

在PyOpenCV中,可以通过模块中的全局变量获得这些常数值。数值类型名由三个部分组成:

  • 固定部分“CV_”
  • 数值的比特数:8、16、32、64
  • 一个描述类型的字母:“U”表示无符号整数、“S”表示符号整数、“F”表示浮点数

像素类型则在数值类型的基础上,添加“C1”、“C2”、“C3”、“C4”等,分别表示1到4个通道的像素类型。因此“CV_16U”表示16位的无符号整数,而“CV_8UC3”表示三个通道的8位无符号整数:

>>> cv.CV_16U
2
>>> cv.CV_8UC3
16

下面创建一个宽10、高20像素的图像,每个像素点是三个通道的无符号8位整数,因此将其转换为NumPy数组将得到一个三维数组,其形状为(20,10,3),并且数值类型为uint8。当图像只有一个通道时,则数组为二维数组。

>>> m = cv.Mat(cv.Size(10,20), cv.CV_8UC3)
>>> m[:].shape
(20, 10, 3)
>>> m[:].dtype
dtype('uint8')

Footnotes

[1]三个通道的图像不一定是储存RGB(红绿蓝)三种颜色的数据,也可以是HSV(色相、饱和度和明度)等其它表示颜色的数据。

其它数据类型

在OpenCV中使用C++的泛型模板定义各种数据类型,每种类型的模板都可以根据一些模板参数创建多种实际的数据类型。因为模板所创建的类型需要经过C++编译之后才能使用,所以在PyOpenCV中我们无法直接使用这些泛型模板。为了解决这个问题,PyOpenCV对各种模板所创建的实际的数据类型进行了包装,下面我们具体看看这些数据类型。

Point类型表示二维或者三维空间中的点的坐标,而坐标值可以是整数、单精度浮点数或者双精度浮点数,因此一共有6种Point类型。下面我们用IPython的自动完成显示这些Point类型:

>>> cv.Point # 按Tab键进行自动补全
cv.Point   cv.Point2d cv.Point2f cv.Point2i cv.Point3d cv.Point3f cv.Point3i

这里cv.Point是cv.Point2i的别名,后缀“2i”表示它是二维的,并且坐标值使用32位整数表示。此外“3”表示三维,“f”表示单精度浮点数,“d”表示双精度浮点数。

在IPython下输入“cv.Point2i?”可以看到__init__()所支持的各种参数:

>>> cv.Point2i?
...
__init__( (object)arg1, (object)_x, (object)_y) -> None :
C++ signature :
    void __init__(_object*,int,int)
...

根据上面所显示的参数类型,我们可以用两个整数创建Point2i对象:

>>> cv.Point2i(3, 4)
Point2i(x=3, y=4)

PyOpenCV的类型检测十分严格,如果使用类型不兼容的值就会出错,例如下面用浮点数做参数会抛出ArgumentError异常:

>>> cv.Point2i(3.0, 4.0)
ArgumentError ...

甚至使用NumPy库中的数值类型也会出错。这一点要特别注意:

>>> t = np.array([3,4])
>>> type(t[0]) # 由于t[0]是一个NumPy中的int32类型的整数
<type 'numpy.int32'>
>>> cv.Point2i(t[0], t[1]) # 因此用t[0]做参数会出现类型错误
ArgumentError ...

我们需要对数组元素进行类型转换才能使用它的值创建Point2i对象:

>>> cv.Point2i(int(t[0]), int(t[1]))
Point2i(x=3, y=4)
>>> cv.Point2i(*t.tolist())
Point2i(x=3, y=4)

在后面的实例程序中我们经常需要用数组中的结果创建PyOpenCV中的对象,这时要特别注意类型转换问题。

和Mat对象一样,Point对象可以和数组相互转换。使用asPoint*()可以将数组转换成对应类型的Point对象:

>>> p = cv.asPoint3f(np.array([1,2,3],dtype=np.float32)) # 数组转换成Point对象
>>> p
Point3f(x=1.0, y=2.0, z=3.0)
>>> p[:].dtype # 通过[:]下标获得NumPy数组
dtype('float32')
>>> p[:] = 10, 20, 30 # 所获得的NumPy数组是视图
>>> p
Point3f(x=10.0, y=20.0, z=30.0)

Size类型表示二维空间上的大小,一共有两种Size类型:Size2i和Size2f。用asSize2i()和asSize2f()可以将数组转换为相应的Size类型。它们的用法和Point类型相同,请读者自行在IPython下测试。

Rect类型表示二维空间上的矩形,它只支持整数类型。

Vec类型可以用来表示较短的一维数值数组。根据其长度和数值类型它多种版本,它们都有对应的函数从数组进行转换。

>>> cv.Vec # 按Tab自动完成
cv.Vec2b cv.Vec2i cv.Vec3b cv.Vec3i cv.Vec4b cv.Vec4i cv.Vec6d
cv.Vec2d cv.Vec2s cv.Vec3d cv.Vec3s cv.Vec4d cv.Vec4s cv.Vec6f
cv.Vec2f cv.Vec2w cv.Vec3f cv.Vec3w cv.Vec4f cv.Vec4w

后缀中的数字表示所创建的数组的长度,而最后的字符表示其数据类型,除了前面介绍的“i”、“f”和“d”之外,“b”表示8位无符号整数,“s”表示16位符号整数,“w”表示16位无符号整数。

Vector类型

在C++中Vector是可动态分配内存的一维数组模板,用它可以创建各种不同元素类型的动态数组类型。PyOpenCV对OpenCV中所用到的所有Vector模板所创建的数组类型进行了包装:

>>> cv.vector_ # 按Tab自动完成
[[省略]]
cv.vector_Point2i                cv.vector_int
cv.vector_Point3d                cv.vector_int16
cv.vector_Point3f                cv.vector_int64
cv.vector_Point3i                cv.vector_int8
cv.vector_Ptr_Mat                cv.vector_long
[[省略]]

这些类型中大部分都可以通过asvector_*()函数将数组转换成对应的类型对象:

>>> cv.asvector_ # 按Tab自动完成
[[省略]]
cv.asvector_Point2i         cv.asvector_Vec6f
cv.asvector_Point3d         cv.asvector_float32
[[省略]]

下面我们通过一个例子学习Vector类型的用法。

用vector_Point2i对象可以表示二维平面上的一组点,可以用asvector_Point2i()将一个shape为(N,2)的整数数组转换为vector_Point2i对象:

>>> a = np.arange(6).reshape(-1,2)
>>> p = cv.asvector_Point2i(a)
>>> p
vector_Point2i(len=3, [Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=4, y=5)])

Vector对象的用法和列表类似,可以使用切片下标或者动态地改变其大小:

>>> p[:2]
vector_Point2i(len=2, [Point2i(x=0, y=1), Point2i(x=2, y=3)])
>>> del p[-1]
>>> p.append( cv.Point2i(10,10) )
vector_Point2i(len=3, [Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=10, y=10)])

tolist()方法可以将其转换成列表:

>>> l = p.tolist()
>>> l
[Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=10, y=10)]

也可以用上面的列表创建vector_Point2i对象:

>>> cv.vector_Point2i(l)
vector_Point2i(len=3, [Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=10, y=10)])

vector_Point2i的初始化方法实际上会调用vector_Point2i类的fromlist()创建对象:

>>> cv.vector_Point2i.fromlist(l)
vector_Point2i(len=3, [Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=10, y=10)])

可以通过Vector类的elem_type属性获得其元素的类型:

>>> cv.vector_Point2i.elem_type()
<class 'pyopencv.cxcore_hpp_point_ext.Point2i'>

最后,vector_vector_Point2i类型是一个元素类型为vector_Point2i的Vector对象。和前面的方法类似,可以用下面的语句创建它的对象:

>>> cv.vector_vector_Point2i([p, p[:2]])
vector_vector_Point2i(len=2,
[vector_Point2i(len=3, [Point2i(x=0, y=1), Point2i(x=2, y=3), Point2i(x=10, y=10)]),
 vector_Point2i(len=2, [Point2i(x=0, y=1), Point2i(x=2, y=3)])])

在图像上绘图

虽然绘图功能不是OpenCV的重点,但是为了在图像上做必要的标识,OpenCV提供了一些简单的绘图功能。例如line()可以在图像上绘制线段。让我们先看看它的文档帮助,在IPython中输入:

>>> cv.line?
[[省略]]
line( (Mat)img, (Point2i)pt1, (Point2i)pt2, (Scalar)color
[, (object)thickness=1 [, (object)lineType=8 [, (object)shift=0]]])
 -> None

img参数的类型是Mat,它是绘图函数的目标图像。pt1和pt2参数的类型为Point2i,分别表示线段的起点和终点。color参数的类型为Scalar,通过它设置线段的颜色。最后用“[]”括起来的部分是关键字参数,其中thickness设置线段的粗细,而lineType设置线段的绘制方法:4表示4连通,8表示8连通,cv.CV_AA(或者16)表示反锯齿。

文档帮助中还列出了对应的C++函数的调用参数,请读者自行对照理解。

Scalar是一个由4个双精度浮点数构成的类型,line()会将直线所通过的像素的每个通道值都设置为color参数中对应的值。Scalar对象也可以使用CV_RGB()创建:

>>> cv.CV_RGB(255, 128, 0) # 三个参数分别为红绿蓝的分量
Scalar([   0.  128.  255.    0.])

由上面的结果可知Scalar中四个浮点数分别表示:蓝、绿、红和透明度。虽然颜色值有四个通道,但是绘图函数并不能利用透明度信息和图像上已有的像素数据进行颜色混合,它只是用color参数的值覆盖图像的像素值。

为了绘制半透明的图形,可以先在一个全黑的图像上进行绘图,然后将目标图像和绘图图像进行混合。下面的程序演示了这一过程,其结果如【图:使用图像混合绘制半透明的直线】所示。

12-opencv/opencv_draw.py

使用图像混合绘制半透明的直线

import pyopencv as cv

img = cv.imread("lena.jpg")
img2 = cv.Mat(img.size(), cv.CV_8UC4)

w, h = img.size().width, img.size().height

def blend(img, img2):
    """
    混合两幅图像, 其中img2有4个通道
    """
    #使用alpha通道计算img2的混和值
    b = img2[:,:,3:] / 255.0     #
    a = 1 - b # img的混合值

    #混合两幅图像
    img[:,:,:3] *= a  #
    img[:,:,:3] += b * img2[:,:,:3]

img2[:] = 0
for i in xrange(0, w, w/10):
    cv.line(img2, cv.Point(i,0), cv.Point(i, h),  #
        cv.Scalar(0, 0, 255, i*255/w), 5)

blend(img, img2) #

img2[:] = 0
for i in xrange(0, h, h/10):
    cv.line(img2, cv.Point(0,i), cv.Point(w, i),
        cv.Scalar(0, 255, 0, i*255/h), 5)

blend(img, img2)

cv.namedWindow("Draw Demo")
cv.imshow("Draw Demo", img)
cv.waitKey(0)
/tech/static/books/scipy/_images//opencv_draw.png

使用图像混合绘制半透明的直线

blend()对两幅图像进行混合,❶先用img2的第四通道计算出两个颜色混合用的数组a和b。❷使用a和b将img2中的颜色混合进img中。为了减少不必要的内存分配,程序中将混合公式的计算分为两步。混合运算公式如下:

img的RGB通道 = img的RGB通道*a + img2的RGB通道*b

❸在全黑的图像img2上绘制竖条直线,请注意在循环中我们改变第四通道的值,使得每条直线都有不同的透明度。❹最后调用blend()将img2混合进img。

除了line()之外,OpenCV还提供了circle()、ellipse()、rectangle()、polylines()、putText()等绘图函数。请读者自行研究它们的用法。

內容目录

上一个主题

OpenCV-图像处理和计算机视觉

下一个主题

图像处理

本页

loading...