技术笔记

OpenCV 教程(1):图片、视频基本操作

从零开始学习 OpenCV,覆盖图片读写、视频处理、绘图函数、鼠标事件等基础操作。

关键结论

  • OpenCV 默认以 BGR 顺序读写彩色图像,和 Matplotlib 的 RGB 显示习惯不同。
  • 先掌握读写、显示、像素与几何操作,后续滤波、分割与识别才有可靠输入。
  • 视频处理本质是逐帧读取;读取失败时应优先检查文件路径、摄像头权限和帧状态。

这篇文章解决什么问题?

这是 OpenCV 入门系列的第一篇,集中处理“图像怎么读进来、怎么看、怎么改、视频怎么逐帧处理”这些基础问题。代码以 Python + OpenCV 为例,适合已经会一点 Python、准备进入计算机视觉实践的读者。

学习时建议先跑通图片读写和颜色通道,再练习绘制、鼠标事件与简单几何变换。遇到显示颜色不对时,优先检查 BGR 与 RGB 的转换,而不是直接修改像素值。

运行环境与公共工具

下面的导入、窗口退出和图片展示函数会在全文重复使用。先准备好它们,后面的每一节只关注一个图像处理概念;这样阅读代码时不会被样板逻辑打断。

OpenCV 图片基本操作

读取图像

使用:'cv2.imread(文件名, 标记)' 可选的标记如下:

- cv2.IMREAD_COLOR 总是读取到三通道数组中、
- cv2.IMREAD_GRAYSCALE 读取到单通道数组中,灰度模式、
- cv2.IMREAD_ANYCOLOR 读取的通道由具体的文件决定,最高3通道、
- cv2.IMREAD_ANYDEPTH 允许读取超过深度超过8位的图像、
- cv2.IMREAD_UNCHANGED 完全按照文件的原有形式进行读取、

默认情况下是 3 通道颜色模式,OpenCV 的颜色通道排列方式为 BGR,而传统的排列方式为 RGB,如下图 BGR 方式排列:

![img](/blog/opencv/69d4185bly1fs0d0915atj20jg03a0so.jpg)

![png](/blog/opencv/output_8_0.png)

显示图片

使用 cv2.imshow() 显示图像,

- cv2.waitKey() 等待键盘输入,为毫秒级
- cv2.destroyAllWindows() 可以轻易删除任何我们建立的窗口,括号内输入想删除的窗口名

cv2.namedWindow('image',cv2.WINDOW_NORMAL) 可以在展示图片前先常见一个窗口,并对窗口设置参数

保存图片

使用 cv2.imwrite(文件名, 文件数据, flags) 保存成功返回 true, 可选的 flag 值与读取图片类似,有 :IMWRITE_JPEG_CHROMA_QUALITY 等。

![png](/blog/opencv/output_13_0.png)

OpenCV 视频操作

读取视频

对视频的捕获可以使用 videoCapture() ,传入 0 表示使用计算机内置的摄像头,传入文件名则读取视频。

- isOpened() 用于判断摄像头或者视频已经打开, 若是没有打开就需要使用 cap.open() 来打开
- get(属性 ID) 获取视频的相关信息,可算的参数值为 0 - 18,也可以使用具体的属性名称,在3.0中需要把 CV_ 去掉:CAP_PROP_POS_MSECCAP_PROP_POS_FRAMESCAP_PROP_POS_AVI_RATIOCAP_PROP_FRAME_WIDTHCAP_PROP_FRAME_HEIGHTCAP_PROP_FPSCAP_PROP_FOURCCCAP_PROP_FRAME_COUNTCAP_PROP_FORMATCAP_PROP_MODECAP_PROP_BRIGHTNESSCAP_PROP_CONTRASTCAP_PROP_SATURATIONCAP_PROP_HUECAP_PROP_GAINCAP_PROP_EXPOSURECAP_PROP_CONVERT_RGBCAP_PROP_WHITE_BALANCECAP_PROP_RECTIFICATION
- set(属性 ID, value) 重新设置属性的值

保存视频

保存视频使用 VideoWriter() 其含有一下几个参数:

- filename – 输出文件的文字.
- fourcc – 压缩帧使用的四字编码,如 VideoWriter::fourcc('P','I','M','1') 是 MPEG-1 编码, VideoWriter::fourcc('M','J','P','G') 是 motion-jpeg 编码等.完整编码列表再此页面 Video Codecs by FOURCC, 常用的有 Fedora : DIVX , XVID , MJPG , X264 , WMV1 , WMV2 ; XVID是最好的,MJPG是高尺寸视频,X264得到小尺寸视频. Windows : DIVX. Mac: MP4V.
- fps – 视频的帧率
- frameSize – 帧的大小.
- isColor – If it is not zero, the encoder will expect and encode color frames, otherwise it will work with grayscale frames (the flag is currently supported on Windows only).

The constructors/functions initialize video writers. On Linux FFMPEG is used to write videos; on Windows FFMPEG or VFW is used; on MacOSX QTKit is used.

需要注意的是,设置的保存尺寸一定要和原始帧的大小一致,否则无法保存成视频。

【参考】

- 翻转示例:opencv: cv2.flip 图像翻转 进行 数据增强
- 编码示例:OpenCV Video Writer on Mac OS X
- 保存视频示例:Record video with Python 3 + OpenCV 3 on OSX

OpenCV 中的绘图函数

OpenCV 中有以下几种绘图函数:

- cv2.line() 绘制线
- cv2.cicle() 绘制原型
- cv2.rectangle() 绘制矩形
- cv2.ellipse() 绘制椭圆
- cv2.putText() 添加文字

以上函数共有的参数如下:

- img 你想要绘制的图形的那副图像
- plt1、plt2 起止点(元组)
- color 形状的颜色,以RGB为例,需要传入的元组,例(255,0,0)代表蓝色,对于灰度图只需传入灰度值
- thickness 线条的粗细,如果给一个闭合图形设置为-1,那么这个图形就会被填充,默认值为1
- linetype 线条的类型,8连接,抗锯齿等。默认是8连接。cv2.LINE_AA为抗锯齿

画线

![png](/blog/opencv/output_25_0.png)

画矩形

需要告诉函数左上角顶点和右下角顶点的坐标

![png](/blog/opencv/output_27_0.png)

画圆形

需要指定圆心坐标和半径大小,可以在上面矩形中画个圆

![png](/blog/opencv/output_29_0.png)

画椭圆

参数为:

- img 用于作画的图片,
- center 椭圆中心点的位置,
- axes 椭圆轴的长度,分为横轴与纵轴,用一个元组表示,
- angle 以度为单位的旋转角度,以原点为中心顺时针旋转此角度,
- startAngle 开始角度,以横轴右向为起点做顺时针旋转,
- endAngle结束角度,
- color[, thickness[, lineType[, shift]]]

![png](/blog/opencv/output_31_0.png)

绘制多边形

需要指定每个顶点的坐标,构建一个大小相等于行数 × 1 × 2 的数组,行数就是点的数目,这个数组必须为int32。

cv2.polylines() 可以用来画很多条线。只把想画的线放在一 个列中将个列传给函数就可以了。每条线会独立绘制。会比用 cv2.line() 一条一条的绘制快一些。

![png](/blog/opencv/output_33_0.png)

添加文字

需要设置,文字内容,绘制的位置,字体类型、大小、颜色、粗细、类型等,这里推荐linetype=cv2.LINE_AA。

目前 OpenCV 不支持添加中文,想要添加中文可以将图片转成 PIL 格式,在 PIL 格式上添加文字之后在转成 OpenCV 格式。

![png](/blog/opencv/output_35_0.png)

图片进阶操作

获取并修改像素值

读取一副图像,根据像素的 行 和 列 的坐标获取它的像素值,对于RGB图像而言,返回RGB的值,对于灰度图则返回灰度值

numpy 是经过优化了的进行快速矩阵运算的包,所以不推荐逐个获取像素值并修改能矩阵运算就不要用循环。例如前5行的后3列,用 numpy 的array.item() 和 array.itemset() 会更好。但是返回是标量,如果想获得所有RGB的值,需要使用 array.item()分割他们。
更好的方法是

查看图像的属性

属性的查看可以使用如下的方法:

- img.shape: 可以获得图像的形状,返回值是一个包含行数,列数,通道数的元组.如果图像是灰度图,返回值仅有行数和列数,所以通过检查返回值可以判断是灰度图还是彩色图
- img.size: 可以返回图像的像素数目
- img.dtype: 返回图像的数据类型,在debug时很重要,因为OpenCV-Python代码中经常出现数据类型的不一致

ROI 操作

ROI 即 Region Of Interest,对感兴趣的区域操作。操作的实现都是通过 numpy索引来实现。

【参考】

- 设置图片ROI(OpenCV学习笔记之二)

![png](/blog/opencv/output_45_0.png)

![png](/blog/opencv/output_46_0.png)

![png](/blog/opencv/output_47_0.png)

通道拆分

有时需要对RGB三个通道分别操作,这就需要拆分RGB为单个通道。有时需要把独立的通道的图片合成一个RGB。

使用 split 可进行通道拆分, 但 OpenCV 拆出来的通道为 B、G、R。cv2.split() 是比较耗时的操作,能用numpy就尽量使用。假如想使所有红色通道值都为0,不必拆分再赋值,可以使用numpy索引,img[:,:,2]=0 , 这样更快.

使用 merge 可进行通道合并,注意合并的顺序。

【参考】

- 小强学Python+OpenCV之-1.4.5通道拆分(cv2.split)及合并(cv2.merge)

![png](/blog/opencv/output_49_0.png)

![png](/blog/opencv/output_51_0.png)

图片扩边(填充)

想为图像周围建一个边可以使用 cv2.copyMakeBorder() 函数。这经常在卷积运算或0填充时被用到。具体参数如下:

- src输入图像
- top,bottom,left,right: 对应边界的像素数目
- borderType要添加哪种类型的边界:cv2.BORDER_CONSTANT: 添加有颜色的常数值边界,还需要下一个参数(value)cv2.BORDER_REFLIECT: 边界元素的镜像。例如:fedcba | abcdefgh | hgfedcbcv2.BORDER_101或者cv2.BORDER_DEFAULT: 跟上面一样,但稍作改动,例如:gfedcb | abcdefgh | gfedcbacv2.BORDER_REPLICATE: 复后一个元素。例如: aaaaaa| abcdefgh|hhhhhhhcv2.BORDER_WRAP: 不知怎么了, 就像样: cdefgh| abcdefgh | abcdefgvalue: 边界颜色

![png](/blog/opencv/output_53_1.png)

图像上的运算

图像加法运算

加法运算使用 cv2.add(), 也可以直接使用 numpy,res=img1+img2.OpenCV 中的加法与 Numpy 的加法有所不同的。 OpenCV 的加法是一种饱和操作,而 Numpy 的加法是一种模操作。 例如,对于 100+200,使用 cv2.add() 函数得到的是 255,而使用 Numpy 的加法得到的是 300%256 . OpenCV的结果会更好,so尽量使用OpenCV中的函数

两幅图像的大小,类型必须一致,或者第二个图像可以是一个简单的标量值。在大小不一致的情况下可以使用 ROI 进行操作。

【参考】

- 图像的算术运算和逻辑运算

![png](/blog/opencv/output_56_0.png)

图像混合

次处是使用 cv2.addWeighted() 做权重相加。

这也是加法,不同的是两幅图像的权重不同,这会给人一种混合或者透明的感觉。图像混合的计算公式如下:

g(x) = (1−α)f0(x)+αf1(x)

通过修改α的值(0-->1),可以实现很酷的混合。例:将两幅图像混合,第一幅权重为0.7 . 第二幅权重为0.3。函数cv2.addWeighed()可以按下面的公式对图片进行混合。dst = α·img1 + β·img2+γ 。这里γ的取值为0.

![png](/blog/opencv/output_58_0.png)

通过 ROI 混合图像

![png](/blog/opencv/output_60_1.png)

图像的位运算

位运算包括:and、or、not、xor 四种。与数学中的概念类似,and 表示两者相同的部分,or 表示两者叠加,not 是取反的意思,xor 表示相同的部分取反。

【参考】

- Python二进制表示和位操作

![png](/blog/opencv/output_62_0.png)

![png](/blog/opencv/output_63_1.png)

![png](/blog/opencv/output_64_0.png)

颜色空间转换

颜色空间装换

在 OpenCV 中有 超过150 种进行颜色空间转换的方法。但是你以后就会发现我们经常用到的也就两种:BGR ↔ Gray 和 BGR ↔ HSV。

我们用到的函数是 cv2.cvtColor(input_image,flag),其中 flag 就是转换类型。 对于BGR↔Gray的转换,我们使用的flag就是cv2.COLOR_BGR2GRAY。 同样对于 BGR↔HSV 的转换我们用的 flag 就是 cv2.COLOR_BGR2HSV。

在 OpenCV 的 HSV 格式中,H(色彩/色度)的取值范围是 [0,179], S(饱和度)的取值范围 [0,255],V(亮度)的取值范围 [0,255]。但是不同的软件使用的值可能不同。所以当你拿 OpenCV 的 HSV 值与其他软件的 HSV 值对比时,一定要记得归一化。

你可以下的命令得到所有可用的 flag:

![png](/blog/opencv/output_68_1.png)

物体追踪 HSV

现在我们知怎样将一幅图像从 BGR 换到 HSV 了,我们可以利用 一点来提取带有某个特定色的物体。在 HSV 颜色空间中要比在 BGR 空间中更容易表示一个特定颜色。在我们的程序中,我们提取的是一个红色的苹果 ,可以使用 cv2.inRange() 来选定。此过程分为以下几个步奏:

- 将图像换到 HSV 空间
- 设置 HSV 阀值到蓝色范围。
- 获取红色物体,当然我们可以做其他任何我们想做的事,比如:在红色物体周围画一个圈。

追踪物体轮廓,以后可以找物体中心,然后跟踪物体,可以在摄像头前挥挥手就可以画图等一些有趣的事。

【参考】

- 【OpenCV】HSV颜色识别-HSV基本颜色分量范围

几何变化

扩展缩放

只是改变图像的尺寸大小,cv2.resize() 可以实现这个功能。在缩放时推荐 cv2.INTER_AREA,在拓展时推荐 cv2.INTER_CUBIC(慢)和cv2.INTER_LINEAR。默认情况下所有改变图像尺寸大小的操作使用的是插值法都是 cv2.INTER_LINEAR。

cv2.resize()有以下几个参数:

- src: 原始的图片
- dsize: 目标的大小,要改变成多大的图片
- fx: 沿着横轴缩放比例
- fy: 沿着纵轴缩放比例
- interpolation: 选用的插值方式

![png](/blog/opencv/output_73_1.png)

平移变换

如果想要沿(x,y)方向移动,移动的距离为(x,y)可以以下面方式构建移动矩阵,M:[[1,0,x],[0,1,y]]。可以使用Numpy数组构建矩阵,数据类型是np.float32,然后传给函数 cv2.warpAffine() , 其参数如下:

- src: 输入图片
- M: 变换矩阵,移动的距离
- dsize: 输出图片的大小,可以从原始输入中获取,但顺序为(宽,高)
- borderValue:背景的颜色
【参考】
- Python-OpenCv之图像基本处理-平移,缩放,旋转,翻转,裁剪及仿射变换

![png](/blog/opencv/output_75_0.png)

旋转

对一个图像旋转角度θ,需要使用下面的旋转矩阵。

![img](https://ws1.sinaimg.cn/large/69d4185bly1fs0qkxxs0wj208802rglf.jpg)

但OpenCVC允许在任意地方进行旋转,所以矩阵应该为

![img](https://ws1.sinaimg.cn/large/69d4185bly1fs0qlqu6h3j20cz038mx2.jpg)

其中 α = scale · cos θ. 为构建旋转矩阵,OpenCV提供了一个函数 cv2.getRotationMatrix2D 其参数如下:

- center: 旋转中心位置,在原始的图片中
- angle: 旋转的角度,以度为单位
- scale: 旋转缩放比例

![png](/blog/opencv/output_77_0.png)

仿射变换

仿射变换就是图像的线性变换加上平移,用一幅图表示,就是

![img](https://ws1.sinaimg.cn/large/69d4185bly1fs19tf2lwpj208c03qq2w.jpg)

由 image1 到 image2 的转换经过了三个操作

- 旋转 (线性变换)
- 缩放操作(线性变换)
- 平移 (向量加)

在仿射变换中,原图中所有平行线在结果图像中同样平行。为创建这个矩阵,需要从原图像中找到三个点以及他们在输出图像中的位置,然后cv2.getAffineTransForm() 会创建一个2X3的矩阵。最后这个矩阵会被传给函数 cv2.warpAffine()

【参考】

- OpenCV Python 学习笔记(三) 仿射变换
- Python+OpenCV教程7:图像几何变换

![png](/blog/opencv/output_80_0.png)

透视变换

透视变换绝对是一项很酷的功能。我们经常会用手机去拍身份证和文件,无论你怎么拍,貌似都拍不正或者有边框。如果你使用过手机上面一些扫描类软件,比如”扫描全能王“,”Office Lens“,它们能很好地矫正图片。这些软件就是应用透视变换实现的,跟仿射变换一样,我们不用知道它的具体原理。

透视变换后,原图中的直线依旧是直线。如下图,我们实现这个功能:

![image](/blog/opencv/69d4185bly1fs1kn81m4gj21j00g8qua.jpg)

上图,首先确定四个角的坐标,并确定变换后的宽高,然后对每个点进行变换,先确定一个变换的基点,然后通过宽高算出其他几个点应该在的位置。

对于视角变换,我们需要一个3x3变换矩阵。在变换前后直线还是直线。需要在原图上找到4个点,以及他们在输出图上对应的位置,这四个点中任意三个都不能共线,可以有函数 cv2.getPerspectiveTransform() 构建,然后这个矩阵传给函数 cv2.warpPerspective().

![png](/blog/opencv/output_83_0.png)

图像阈值

此处会用到 cv2.threshold, cv2.adaptiveThreshold 两个函数

固定阈值

当像素值高于阀值时,我们给这个像素赋予一个新值(可能是白色),否则我们给它赋予另外一种颜色(也许是黑色)。这个函数就是 cv2.threshold()参数如下:

- src: 原图像,原图像应该是灰度图。
- thresh: 阀值,
- maxval: 最大阈值,即超过阈值时被赋予的值
- type: 阈值类型, 包括以下五种cv2.THRESH_BINARYcv2.THRESH_BINARY_INVcv2.THRESH_TRUNCcv2.THRESH_TOZEROcv2.THRESH_TOZERO_INV

【参考】

- Python+OpenCV教程6:阈值分割

![png](/blog/opencv/output_86_0.png)

结合上图,可以看到五种方式的行为描述如下:

![img](/blog/opencv/69d4185bly1fs1m16f1rsj20nq0esq4b.jpg)

自适应阈值

根据图像上的每一个小区域计算与其对应的阀值。因此在同一幅图像上的不同区域采用的是不同的阀值,从而使我们能在亮度不同的情况下得到更好的结果。

这里需要使用 cv2.adaptiveThreshold 函数,参数如下:

- src: 原始图像,单通道,
- maxValue: 当满足阈值条件是为像素赋予的值,通常为255,
- adaptiveMethod: 区域阈值的计算方式, 可以选的有两个ADAPTIVE_THRESH_MEAN_C:小区域内取均值ADAPTIVE_THRESH_GAUSSIAN_C:小区域内加权求和,权重是个高斯核
- thresholdType: 阈值类型,即前面的五种,
- blockSize: 小块区域的面积,如 5×5,
- C : 常量,最终阈值等于小区域计算出的阈值再减去此值

![png](/blog/opencv/output_89_0.png)

Otsu阈值法

在前面固定阈值中,我们是随便选了一个阈值如127,那如何知道我们选的这个阈值效果好不好呢?答案是:不断尝试,所以这种方法在很多文献中都被称为经验阈值。Otsu阈值法就提供了一种自动高效的二值化方法 ,并且Otsu’s非常适合于图像灰度直方图具有双峰的情况,他会在双峰之间找到一个值作为阈值,对于非双峰图像,可能并不是很好用。那么经过Otsu’s得到的那个阈值就是函数 cv2.threshold 的第一个参数了。因为Otsu’s方法会产生一个阈值,那么函数 cv2.threshold 的的第二个参数(设置阈值)就是0了,并且在 cv2.threshold 的方法参数中还得加上语句 cv2.THRESH_OTSU。

【参考】

- Python+OpenCV教程番外篇4:Otsu阈值法

![png](/blog/opencv/output_91_0.png)

- OpenCV-Python中文教程
- 设置图片ROI(OpenCV学习笔记之二)
- 小强学Python+OpenCV之-1.4.5通道拆分(cv2.split)及合并(cv2.merge)
- 图像的算术运算和逻辑运算
- 【OpenCV】HSV颜色识别-HSV基本颜色分量范围
- Python-OpenCv之图像基本处理-平移,缩放,旋转,翻转,裁剪及仿射变换
- OpenCV Python 学习笔记(三) 仿射变换
- Python+OpenCV教程7:图像几何变换