OpenCV中cv2函数有哪些用法?

2026-04-02 13:141阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计5900个文字,预计阅读时间需要24分钟。

OpenCV中cv2函数有哪些用法?

1. 主要函数1:cv2.imread() 读取图像 参数1:要读取的图像文件名 参数2:读取模式(可选,默认为读取为彩色图像)

1、主要函数1、cv2.imread()读入图片共两个参数第一个参数为要读入的图片文件名第二个参数为如何读取图片读入图片共两个参数第一个参数为要读入的图片文件名第二个参数为如何读取图片包括

cv2.IMREAD_COLOR读入一副彩色图片cv2.IMREAD_GRAYSCALE以灰度模式读入图片cv2.IMREAD_UNCHANGED读入一幅图片并包括其alpha通道。

PS调用opencv就算图像的路径是错的OpenCV 也不会提醒你的但是当你使用命

令print(img)时得到的结果是None。

2、cv2.imshow()创建一个窗口显示图片共两个参数第一个参数表示窗口名字可以创建多个窗口中但是每个窗口不能重名第二个参数是读入的图片窗口大小自动调整为图片大小。

3、cv2.waitKey()键盘绑定函数共一个参数表示等待毫秒数将等待特定的几毫秒看键盘是否有输入返回值为ASCII值。如果其参数为0则表示无限期的等待键盘输入。

4、cv2.destroyAllWindows()删除建立的全部窗口。

5、cv2.destroyWindows()删除指定的窗口。

6、cv2.imwrite(fileimgnum)保存图片共3个参数第一个为保存文件名第二个为读入图片可选的第三个参数它针对特定的格式对于JPEG其表示的是图像的质量用0 - 100的整数表示默认95;对于png ,第三个参数表示的是压缩级别。默认为3.。

注意:

cv2.IMWRITE_JPEG_QUALITY类型为 long ,必须转换成 intcv2.IMWRITE_PNG_COMPRESSION, 从0到9 压缩级别越高图像越小。

cv2.imwrite(1.png,img, [int( cv2.IMWRITE_JPEG_QUALITY), 95])cv2.imwrite(1.png,img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])

cv2.imread()

使用opencv和caffe的伙伴们可能会有一个疑问那就是对于同时读取图片的cv2.imread()和caffe.io.loadimage两个函数有什么差别

1、cv2.imread()接口读图像读进来直接是BGR 格式数据格式在 0~255通道格式为(W,H,C)

2、caffe.io.load_image()读进来是RGB格式和 0~1(float

因此cv2.imread()读取的数据需要进过转换后才和caffe.io.load_image()相同例如

image caffe.io.load_image(examples/images/cat.jpg)

image1cv2.imread(examples/images/cat.jpg) image1cv2.cvtColor(image1,cv2.COLOR_BGR2RGB) image1image1/255.

经过转换后cv2.imread读取的image1和caffe.io.load_image()读取到的image格式相同。

所以在进行特征提取之前要在transformer中设置transformer.set_raw_scale(‘data’,255)(缩放至0~255 以及transformer.set_channel_swap(‘data’,(2,1,0)(将RGB变换到BGR。

调用caffe model进行特征提取分类时要注意区分image用何种方法读进来。


cv2.imread不能正常读取gif格式图片

Python中cv2模块的imread函数可以正常读取’jpg’,png’格式的图片但是不能处理’gif’图片。可以改用imageio模块来处理。

import cv2import imageiodef readImg(im_fn):im cv2.imread(im_fn)if im is None :print({} cv2.imread failed.format(im_fn))tmp imageio.mimread(im_fn)if tmp is not None:imt np.array(tmp)imt imt[0]im imt[:,:,0:3]return im

help(imageio.mimread)


关于cv2.imread()读取图像为BGR问题

opencv读取图像为b,g,r方法,比如

img cv2.imread("xx.jpg") cv2.imshow("xx",img)

展示的结果是正常的:

但是此时读取到的img已经为bgr方式了,如果我们再用其他使用rgb方式读取的函数进行读取时就会出错,比如我用plt对图像进行显示,效果如下:

因为plt函数是rgb方式读取的,所以会出错.这时我们可以手动改变img的通道顺序,如下:

b,g,r cv2.split(img) img_rgb cv2.merge([r,g,b]) plt.figure() plt.imshow(img_rgb) plt.show()

这时img_rgb就是rgb顺序的了.那么这时再用cv2.imshow()显示出来,rgb错误:

简单实例

1、以下面的图片为例 显示结果 显示结果 5、配合画图

import numpy as npimport cv2np.set_printoptions(thresholdnan)# 创建一个宽512高512的黑色画布RGB(0,0,0)即黑色imgnp.zeros((512,512,3),np.uint8)# 画直线,图片对象起始坐标(x轴,y轴)结束坐标颜色宽度cv2.line(img,(0,0),(311,511),(255,0,0),10)# 画矩形图片对象左上角坐标右下角坐标颜色宽度cv2.rectangle(img,(30,166),(130,266),(0,255,0),3)# 画圆形图片对象中心点坐标半径大小颜色宽度cv2.circle(img,(222,222),50,(255.111,111),-1)# 画椭圆形图片对象中心点坐标长短轴顺时针旋转度数开始角度(右长轴表0度上短轴表270度)颜色宽度cv2.ellipse(img,(333,333),(50,20),0,0,150,(255,222,222),-1)# 画多边形指定各个点坐标,array必须是int32类型ptsnp.array([[10,5],[20,30],[70,20],[50,10]], np.int32)# -1表示该纬度靠后面的纬度自动计算出来实际上是4pts pts.reshape((-1,1,2,))# print(pts)# 画多条线False表不闭合True表示闭合闭合即多边形cv2.polylines(img,[pts],True,(255,255,0),5)#写字,字体选择fontcv2.FONT_HERSHEY_SCRIPT_COMPLEX# 图片对象要写的内容左边距字的底部到画布上端的距离字体大小颜色粗细cv2.putText(img,"OpenCV",(10,400),font,3.5,(255,255,255),2)acv2.imwrite("picture.jpg",img)cv2.imshow("picture",img)cv2.waitKey(0)cv2.destroyAllWindows()

6.1、图像的表示

前面章节已经提到过了单通道的灰度图像在计算机中的表示就是一个8位无符号整形的矩阵。在OpenCV的C代码中表示图像有个专门的结构叫做cv::Mat不过在Python-OpenCV中因为已经有了numpy这种强大的基础工具所以这个矩阵就用numpy的array表示。如果是多通道情况最常见的就是红绿蓝RGB三通道则第一个维度是高度第二个维度是高度第三个维度是通道比如图6-1a是一幅3×3图像在计算机中表示的例子

不管是RGB还是BGR都是高度×宽度×通道数H×W×C的表达方式而在深度学习中因为要对不同通道应用卷积所以用的是另一种方式C×H×W就是把每个通道都单独表达成一个二维矩阵如图6-1c所示。

6.2、基本图像处理

6.2.1、存取图像

读图像用cv2.imread()可以按照不同模式读取一般最常用到的是读取单通道灰度图或者直接默认读取多通道。存图像用cv2.imwrite()注意存的时候是没有单通道这一说的根据保存文件名的后缀和当前的array维度OpenCV自动判断存的通道另外压缩格式还可以指定存储质量来看代码例子

import cv2# 读取一张400x600分辨率的图像color_img cv2.imread(test_400x600.jpg)print(color_img.shape)# 直接读取单通道gray_img cv2.imread(test_400x600.jpg, cv2.IMREAD_GRAYSCALE)print(gray_img.shape)# 把单通道图片保存后再读取仍然是3通道相当于把单通道值复制到3个通道保存cv2.imwrite(test_grayscale.jpg, gray_img)reload_grayscale cv2.imread(test_grayscale.jpg)print(reload_grayscale.shape)# cv2.IMWRITE_JPEG_QUALITY指定jpg质量范围0到100默认95越高画质越好文件越大cv2.imwrite(test_imwrite.jpg, color_img, (cv2.IMWRITE_JPEG_QUALITY, 80))# cv2.IMWRITE_PNG_COMPRESSION指定png质量范围0到9默认3越高文件越小画质越差cv2.imwrite(test_imwrite.png, color_img, (cv2.IMWRITE_PNG_COMPRESSION, 5))

6.2.2、 缩放裁剪和补边

缩放通过cv2.resize()实现裁剪则是利用array自身的下标截取实现此外OpenCV还可以给图像补边这样能对一幅图像的形状和感兴趣区域实现各种操作。下面的例子中读取一幅400×600分辨率的图片并执行一些基础的操作

import cv2# 读取一张四川大录古藏寨的照片img cv2.imread(tiger_tibet_village.jpg)# 缩放成200x200的方形图像img_200x200 cv2.resize(img, (200, 200))# 不直接指定缩放后大小通过fx和fy指定缩放比例0.5则长宽都为原来一半# 等效于img_200x300 cv2.resize(img, (300, 200))注意指定大小的格式是(宽度,高度)# 插值方法默认是cv2.INTER_LINEAR这里指定为最近邻插值img_200x300 cv2.resize(img, (0, 0), fx0.5, fy0.5, interpolationcv2.INTER_NEAREST)# 在上张图片的基础上上下各贴50像素的黑边生成300x300的图像img_300x300 cv2.copyMakeBorder(img, 50, 50, 0, 0, cv2.BORDER_CONSTANT, value(0, 0, 0))# 对照片中树的部分进行剪裁patch_tree img[20:150, -180:-50]cv2.imwrite(cropped_tree.jpg, patch_tree)cv2.imwrite(resized_200x200.jpg, img_200x200)cv2.imwrite(resized_200x300.jpg, img_200x300)cv2.imwrite(bordered_300x300.jpg, img_300x300)

6.2.3、色调明暗直方图和Gamma曲线

除了区域图像本身的属性操作也非常多比如可以通过HSV空间对色调和明暗进行调节。HSV空间是由美国的图形学专家A. R. Smith提出的一种颜色空间HSV分别是色调Hue饱和度Saturation和明度Value。在HSV空间中进行调节就避免了直接在RGB空间中调节是还需要考虑三个通道的相关性。OpenCV中H的取值是[0, 180)其他两个通道的取值都是[0, 256)下面例子接着上面例子代码通过HSV空间对图像进行调整

# 通过cv2.cvtColor把图像从BGR转换到HSVimg_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# H空间中绿色比黄色的值高一点所以给每个像素15黄色的树叶就会变绿turn_green_hsv img_hsv.copy()turn_green_hsv[:, :, 0] (turn_green_hsv[:, :, 0]15) % 180turn_green_img cv2.cvtColor(turn_green_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(turn_green.jpg, turn_green_img)# 减小饱和度会让图像损失鲜艳变得更灰colorless_hsv img_hsv.copy()colorless_hsv[:, :, 1] 0.5 * colorless_hsv[:, :, 1]colorless_img cv2.cvtColor(colorless_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(colorless.jpg, colorless_img)# 减小明度为原来一半darker_hsv img_hsv.copy()darker_hsv[:, :, 2] 0.5 * darker_hsv[:, :, 2]darker_img cv2.cvtColor(darker_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(darker.jpg, darker_img)

无论是HSV还是RGB我们都较难一眼就对像素中值的分布有细致的了解这时候就需要直方图。如果直方图中的成分过于靠近0或者255可能就出现了暗部细节不足或者亮部细节丢失的情况。背景里的暗部细节是非常弱的。这个时候一个常用方法是考虑用Gamma变换来提升暗部细节。Gamma变换是矫正相机直接成像和人眼感受图像差别的一种常用手段简单来说就是通过非线性变换让图像从对曝光强度的线性响应变得更接近人眼感受到的响应。具体的定义和实现还是接着上面代码中读取的图片执行计算直方图和Gamma变换的代码如下

import numpy as np# 分通道计算每个通道的直方图hist_b cv2.calcHist([img], [0], None, [256], [0, 256])hist_g cv2.calcHist([img], [1], None, [256], [0, 256])hist_r cv2.calcHist([img], [2], None, [256], [0, 256])# 定义Gamma矫正的函数def gamma_trans(img, gamma):# 具体做法是先归一化到1然后gamma作为指数值求出新的像素值再还原gamma_table [np.power(x/255.0, gamma)*255.0 for x in range(256)]gamma_table np.round(np.array(gamma_table)).astype(np.uint8)# 实现这个映射用的是OpenCV的查表函数return cv2.LUT(img, gamma_table)# 执行Gamma矫正小于1的值让暗部细节大量提升同时亮部细节少量提升img_corrected gamma_trans(img, 0.5)cv2.imwrite(gamma_corrected.jpg, img_corrected)# 分通道计算Gamma矫正后的直方图hist_b_corrected cv2.calcHist([img_corrected], [0], None, [256], [0, 256])hist_g_corrected cv2.calcHist([img_corrected], [1], None, [256], [0, 256])hist_r_corrected cv2.calcHist([img_corrected], [2], None, [256], [0, 256])# 将直方图进行可视化import matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfig plt.figure()pix_hists [[hist_b, hist_g, hist_r],[hist_b_corrected, hist_g_corrected, hist_r_corrected]]pix_vals range(256)for sub_plt, pix_hist in zip([121, 122], pix_hists):ax fig.add_subplot(sub_plt, projection3d)for c, z, channel_hist in zip([b, g, r], [20, 10, 0], pix_hist):cs [c] * 256ax.bar(pix_vals, channel_hist, zsz, zdiry, colorcs, alpha0.618, edgecolornone, lw0)ax.set_xlabel(Pixel Values)ax.set_xlim([0, 256])ax.set_ylabel(Channels)ax.set_zlabel(Counts)plt.show()

上面三段代码的结果统一放在下图中 代码实现的操作示意在下图中

这个例子实现了延时摄影的功能把程序打开并将摄像头对准一些缓慢变化的画面比如桌上缓慢蒸发的水或者正在生长的小草就能制作出有趣的延时摄影作品。比如下面这个链接中的图片就是用这段程序生成的

images.cnitblog.com/blog2015/609274/201503/251904209276278.gif

程序的结构非常清晰简单注释里也写清楚了每一步所以流程就不解释了。需要提一下的有两点一个是VideoWriter中的一个函数cv2.VideoWriter_fourcc()。这个函数指定了视频编码的格式比如例子中用的是MP42也就是MPEG-4更多编码方式可以在下面的地址查询

Video Codecs by FOURCC

还有一个是KeyboardInterrupt这是一个常用的异常用来获取用户CtrlC的中止捕获这个异常后直接结束循环并释放VideoCapture和VideoWriter的资源使已经捕获好的部分视频可以顺利生成。

从视频中截取帧也是处理视频时常见的任务下面代码实现的是遍历一个指定文件夹下的所有视频并按照指定的间隔进行截屏并保存

import cv2import osimport sys# 第一个输入参数是包含视频片段的路径input_path sys.argv[1]# 第二个输入参数是设定每隔多少帧截取一帧frame_interval int(sys.argv[2])# 列出文件夹下所有的视频文件filenames os.listdir(input_path)# 获取文件夹名称video_prefix input_path.split(os.sep)[-1]# 建立一个新的文件夹名称为原文件夹名称后加上_framesframe_path {}_frames.format(input_path)if not os.path.exists(frame_path):os.mkdir(frame_path)# 初始化一个VideoCapture对象cap cv2.VideoCapture()# 遍历所有文件for filename in filenames:filepath os.sep.join([input_path, filename])# VideoCapture::open函数可以从文件获取视频cap.open(filepath)# 获取视频帧数n_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 同样为了避免视频头几帧质量低下黑屏或者无关等for i in range(42):cap.read()for i in range(n_frames):ret, frame cap.read()# 每隔frame_interval帧进行一次截屏操作if i % frame_interval 0:imagename {}_{}_{:0>6d}.jpg.format(video_prefix, filename.split(.)[0], i)imagepath os.sep.join([frame_path, imagename])print(exported {}!.format(imagepath))cv2.imwrite(imagepath, frame)# 执行结束释放资源cap.release()

7、 用OpenCV实现数据增加小工具

到目前我们已经熟悉了numpy中的随机模块多进程调用和OpenCV的基本操作基于这些基础本节将从思路到代码一步步实现一个最基本的数据增加小工具。

第三章和第四章都提到过数据增加data augmentation作为一种深度学习中的常用手段数据增加对模型的泛化性和准确性都有帮助。数据增加的具体使用方式一般有两种一种是实时增加比如在Caffe中加入数据扰动层每次图像都先经过扰动操作再去训练这样训练经过几代epoch之后就等效于数据增加。还有一种是更加直接简单一些的就是在训练之前就通过图像处理手段对数据样本进行扰动和增加也就是本节要实现的。

这个例子中将包含三种基本类型的扰动随机裁剪随机旋转和随机颜色/明暗。

7.1 随机裁剪

AlexNet中已经讲过了随机裁剪的基本思路我们的小例子中打算更进一步在裁剪的时候考虑图像宽高比的扰动。在绝大多数用于分类的图片中样本进入网络前都是要变为统一大小所以宽高比扰动相当于对物体的横向和纵向进行了缩放这样除了物体的位置扰动又多出了一项扰动。只要变化范围控制合适目标物体始终在画面内这种扰动是有助于提升泛化性能的。实现这种裁剪的思路如下图所示

执行这段代码得到如下窗口

cv2.waitKey()参数不为零的时候则可以和循环结合产生动态画面比如在6.2.4的延时小例子中我们把延时摄影保存下来的所有图像放到一个叫做frames的文件夹下。下面代码从frames的文件夹下读取所有图片并以24的帧率在窗口中显示成动画

OpenCV中cv2函数有哪些用法?

import osfrom itertools import cycleimport cv2# 列出frames文件夹下的所有图片filenames os.listdir(frames)# 通过itertools.cycle生成一个无限循环的迭代器每次迭代都输出下一张图像对象img_iter cycle([cv2.imread(os.sep.join([frames, x])) for x in filenames])key 0while key 27:cv2.imshow(Animation, next(img_iter))key cv2.waitKey(42)

在这个例子中我们采用了Python的itertools模块中的cycle函数这个函数可以把一个可遍历结构编程一个无限循环的迭代器。另外从这个例子中我们还发现cv2.waitKey()返回的就是键盘上出发的按键。对于字母就是ascii码特殊按键比如上下左右等则对应特殊的值其实这就是键盘事件的最基本用法。

8.2 鼠标和键盘事件

因为GUI总是交互的所以鼠标和键盘事件基本使用必不可少上节已经提到了cv2.waitKey()就是获取键盘消息的最基本方法。比如下面这段循环代码就能够获取键盘上按下的按键并在终端输出

while key ! 27:cv2.imshow(Honeymoon Island, img)key cv2.waitKey()# 如果获取的键值小于256则作为ascii码输出对应字符否则直接输出值msg {} is pressed.format(chr(key) if key <256 else key)print(msg)

通过这个程序我们能获取一些常用特殊按键的值比如在笔者用的机器上四个方向的按键和删除键对应的值如下

  • 上↑65362

  • 下↓65364

  • 左←65361

  • 右→65363

  • 删除Delete65535

需要注意的是在不同的操作系统里这些值可能是不一样的。鼠标事件比起键盘事件稍微复杂一点点需要定义一个回调函数然后把回调函数和一个指定名称的窗口绑定这样只要鼠标位于画面区域内的事件就都能捕捉到。把下面这段代码插入到上段代码的while之前就能获取当前鼠标的位置和动作并输出

# 定义鼠标事件回调函数def on_mouse(event, x, y, flags, param):# 鼠标左键按下抬起双击if event cv2.EVENT_LBUTTONDOWN:print(Left button down at ({}, {}).format(x, y))elif event cv2.EVENT_LBUTTONUP:print(Left button up at ({}, {}).format(x, y))elif event cv2.EVENT_LBUTTONDBLCLK:print(Left button double clicked at ({}, {}).format(x, y))# 鼠标右键按下抬起双击elif event cv2.EVENT_RBUTTONDOWN:print(Right button down at ({}, {}).format(x, y))elif event cv2.EVENT_RBUTTONUP:print(Right button up at ({}, {}).format(x, y))elif event cv2.EVENT_RBUTTONDBLCLK:print(Right button double clicked at ({}, {}).format(x, y))# 鼠标中/滚轮键如果有的话按下抬起双击elif event cv2.EVENT_MBUTTONDOWN:print(Middle button down at ({}, {}).format(x, y))elif event cv2.EVENT_MBUTTONUP:print(Middle button up at ({}, {}).format(x, y))elif event cv2.EVENT_MBUTTONDBLCLK:print(Middle button double clicked at ({}, {}).format(x, y))# 鼠标移动elif event cv2.EVENT_MOUSEMOVE:print(Moving at ({}, {}).format(x, y))# 为指定的窗口绑定自定义的回调函数cv2.namedWindow(Honeymoon Island)cv2.setMouseCallback(Honeymoon Island, on_mouse)

8.3 代码物体检测标注的小工具

基于上面两小节的基本使用就能和OpenCV的基本绘图功能就能实现一个超级简单的物体框标注小工具了。基本思路是对要标注的图像建立一个窗口循环然后每次循环的时候对图像进行一次拷贝。鼠标在画面上画框的操作以及已经画好的框的相关信息在全局变量中保存并且在每个循环中根据这些信息在拷贝的图像上再画一遍然后显示这份拷贝的图像。

基于这种实现思路使用上我们采用一个尽量简化的设计

  • 输入是一个文件夹下面包含了所有要标注物体框的图片。如果图片中标注了物体则生成一个相同名称加额外后缀名的文件保存标注信息。

  • 标注的方式是按下鼠标左键选择物体框的左上角松开鼠标左键选择物体框的右下角鼠标右键删除上一个标注好的物体框。所有待标注物体的类别和标注框颜色由用户自定义如果没有定义则默认只标注一种物体定义该物体名称叫“Object”。

  • 方向键的←和→用来遍历图片↑和↓用来选择当前要标注的物体Delete键删除一张图片和对应的标注信息。

每张图片的标注信息以及自定义标注物体和颜色的信息用一个元组表示第一个元素是物体名字第二个元素是代表BGR颜色的tuple或者是代表标注框坐标的元组。对于这种并不复杂复杂的数据结构我们直接利用Python的repr()函数把数据结构保存成机器可读的字符串放到文件里读取的时候用eval()函数就能直接获得数据。这样的方便之处在于不需要单独写个格式解析器。如果需要可以在此基础上再编写一个转换工具就能够转换成常见的Pascal VOC的标注格式或是其他的自定义格式。

在这些思路和设计下我们定义标注信息文件的格式的例子如下

(Hill, ((221, 163), (741, 291)))(Horse, ((465, 430), (613, 570)))

元组中第一项是物体名称第二项是标注框左上角和右下角的坐标。这里之所以不把标注信息的数据直接用pickle保存是因为数据本身不会很复杂直接保存还有更好的可读性。自定义标注物体和对应标注框颜色的格式也类似不过更简单些因为括号可以不写具体如下

Horse, (255, 255, 0)Hill, (0, 255, 255)DiaoSi, (0, 0, 255)

第一项是物体名称第二项是物体框的颜色。使用的时候把自己定义好的内容放到一个文本里然后保存成和待标注文件夹同名后缀名为labels的文件。比如我们在一个叫samples的文件夹下放上一些草原的照片然后自定义一个samples.labels的文本文件。把上段代码的内容放进去就定义了小山头的框为黄色骏马的框为青色以及红色的屌丝。基于以上标注小工具的代码如下

import osimport cv2# tkinter是Python内置的简单GUI库实现一些比如打开文件夹确认删除等操作十分方便from tkFileDialog import askdirectoryfrom tkMessageBox import askyesno# 定义标注窗口的默认名称WINDOW_NAME Simple Bounding Box Labeling Tool# 定义画面刷新的大概帧率是否能达到取决于电脑性能FPS 24# 定义支持的图像格式SUPPOTED_FORMATS [jpg, jpeg, png]# 定义默认物体框的名字为Object颜色蓝色当没有用户自定义物体时用默认物体DEFAULT_COLOR {Object: (255, 0, 0)}# 定义灰色用于信息显示的背景和未定义物体框的显示COLOR_GRAY (192, 192, 192)# 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息BAR_HEIGHT 16# 上下左右ESC及删除键对应的cv.waitKey()的返回值# 注意这个值根据操作系统不同有不同可以通过6.4.2中的代码获取KEY_UP 65362KEY_DOWN 65364KEY_LEFT 65361KEY_RIGHT 65363KEY_ESC 27KEY_DELETE 65535# 空键用于默认循环KEY_EMPTY 0get_bbox_name {}.bbox.format# 定义物体框标注工具类class SimpleBBoxLabeling:def __init__(self, data_dir, fpsFPS, window_nameNone):self._data_dir data_dirself.fps fpsself.window_name window_name if window_name else WINDOW_NAME#pt0是正在画的左上角坐标pt1是鼠标所在坐标self._pt0 Noneself._pt1 None# 表明当前是否正在画框的状态标记self._drawing False# 当前标注物体的名称self._cur_label None# 当前图像对应的所有已标注框self._bboxes []# 如果有用户自定义的标注信息则读取否则用默认的物体和颜色label_path {}.labels.format(self._data_dir)self.label_colors DEFAULT_COLOR if not os.path.exists(label_path) else self.load_labels(label_path)# 获取已经标注的文件列表和还未标注的文件列表imagefiles [x for x in os.listdir(self._data_dir) if x[x.rfind(.) 1:].lower() in SUPPOTED_FORMATS]labeled [x for x in imagefiles if os.path.exists(get_bbox_name(x))]to_be_labeled [x for x in imagefiles if x not in labeled]# 每次打开一个文件夹都自动从还未标注的第一张开始self._filelist labeled to_be_labeledself._index len(labeled)if self._index > len(self._filelist) - 1:self._index len(self._filelist) - 1# 鼠标回调函数def _mouse_ops(self, event, x, y, flags, param):# 按下左键时坐标为左上角同时表明开始画框改变drawing标记为Trueif event cv2.EVENT_LBUTTONDOWN:self._drawing Trueself._pt0 (x, y)# 左键抬起表明当前框画完了坐标记为右下角并保存同时改变drawing标记为Falseelif event cv2.EVENT_LBUTTONUP:self._drawing Falseself._pt1 (x, y)self._bboxes.append((self._cur_label, (self._pt0, self._pt1)))# 实时更新右下角坐标方便画框elif event cv2.EVENT_MOUSEMOVE:self._pt1 (x, y)# 鼠标右键删除最近画好的框elif event cv2.EVENT_RBUTTONUP:if self._bboxes:self._bboxes.pop()# 清除所有标注框和当前状态def _clean_bbox(self):self._pt0 Noneself._pt1 Noneself._drawing Falseself._bboxes []# 画标注框和当前信息的函数def _draw_bbox(self, img):# 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息h, w img.shape[:2]canvas cv2.copyMakeBorder(img, 0, BAR_HEIGHT, 0, 0, cv2.BORDER_CONSTANT, valueCOLOR_GRAY)# 正在标注的物体信息如果鼠标左键已经按下则显示两个点坐标否则显示当前待标注物体的名称label_msg {}: {}, {}.format(self._cur_label, self._pt0, self._pt1) \if self._drawing \else Current label: {}.format(self._cur_label)# 显示当前文件名文件个数信息msg {}/{}: {} | {}.format(self._index 1, len(self._filelist), self._filelist[self._index], label_msg)cv2.putText(canvas, msg, (1, h12),cv2.FONT_HERSHEY_SIMPLEX,0.5, (0, 0, 0), 1)# 画出已经标好的框和对应名字for label, (bpt0, bpt1) in self._bboxes:label_color self.label_colors[label] if label in self.label_colors else COLOR_GRAYcv2.rectangle(canvas, bpt0, bpt1, label_color, thickness2)cv2.putText(canvas, label, (bpt0[0]3, bpt0[1]15),cv2.FONT_HERSHEY_SIMPLEX,0.5, label_color, 2)# 画正在标注的框和对应名字if self._drawing:label_color self.label_colors[self._cur_label] if self._cur_label in self.label_colors else COLOR_GRAYif self._pt1[0] > self._pt0[0] and self._pt1[1] > self._pt0[1]:cv2.rectangle(canvas, self._pt0, self._pt1, label_color, thickness2)cv2.putText(canvas, self._cur_label, (self._pt0[0] 3, self._pt0[1] 15),cv2.FONT_HERSHEY_SIMPLEX,0.5, label_color, 2)return canvas# 利用repr()导出标注框数据到文件staticmethoddef export_bbox(filepath, bboxes):if bboxes:with open(filepath, w) as f:for bbox in bboxes:line repr(bbox) \nf.write(line)elif os.path.exists(filepath):os.remove(filepath)# 利用eval()读取标注框字符串到数据staticmethoddef load_bbox(filepath):bboxes []with open(filepath, r) as f:line f.readline().rstrip()while line:bboxes.append(eval(line))line f.readline().rstrip()return bboxes# 利用eval()读取物体及对应颜色信息到数据staticmethoddef load_labels(filepath):label_colors {}with open(filepath, r) as f:line f.readline().rstrip()while line:label, color eval(line)label_colors[label] colorline f.readline().rstrip()return label_colors# 读取图像文件和对应标注框信息如果有的话staticmethoddef load_sample(filepath):img cv2.imread(filepath)bbox_filepath get_bbox_name(filepath)bboxes []if os.path.exists(bbox_filepath):bboxes SimpleBBoxLabeling.load_bbox(bbox_filepath)return img, bboxes# 导出当前标注框信息并清空def _export_n_clean_bbox(self):bbox_filepath os.sep.join([self._data_dir, get_bbox_name(self._filelist[self._index])])self.export_bbox(bbox_filepath, self._bboxes)self._clean_bbox()# 删除当前样本和对应的标注框信息def _delete_current_sample(self):filename self._filelist[self._index]filepath os.sep.join([self._data_dir, filename])if os.path.exists(filepath):os.remove(filepath)filepath get_bbox_name(filepath)if os.path.exists(filepath):os.remove(filepath)self._filelist.pop(self._index)print({} is deleted!.format(filename))# 开始OpenCV窗口循环的方法定义了程序的主逻辑def start(self):# 之前标注的文件名用于程序判断是否需要执行一次图像读取last_filename # 标注物体在列表中的下标label_index 0# 所有标注物体名称的列表labels self.label_colors.keys()# 待标注物体的种类数n_labels len(labels)# 定义窗口和鼠标回调cv2.namedWindow(self.window_name)cv2.setMouseCallback(self.window_name, self._mouse_ops)key KEY_EMPTY# 定义每次循环的持续时间delay int(1000 / FPS)# 只要没有按下Esc键就持续循环while key ! KEY_ESC:# 上下键用于选择当前标注物体if key KEY_UP:if label_index 0:passelse:label_index - 1elif key KEY_DOWN:if label_index n_labels - 1:passelse:label_index 1# 左右键切换当前标注的图片elif key KEY_LEFT:# 已经到了第一张图片的话就不需要清空上一张if self._index > 0:self._export_n_clean_bbox()self._index - 1if self._index <0:self._index 0elif key KEY_RIGHT:# 已经到了最后一张图片的话就不需要清空上一张if self._index len(self._filelist) - 1:self._index len(self._filelist) - 1# 删除当前图片和对应标注信息elif key KEY_DELETE:if askyesno(Delete Sample, Are you sure?):self._delete_current_sample()key KEY_EMPTYcontinue# 如果键盘操作执行了换图片则重新读取更新图片filename self._filelist[self._index]if filename ! last_filename:filepath os.sep.join([self._data_dir, filename])img, self._bboxes self.load_sample(filepath)# 更新当前标注物体名称self._cur_label labels[label_index]# 把标注和相关信息画在图片上并显示指定的时间canvas self._draw_bbox(img)cv2.imshow(self.window_name, canvas)key cv2.waitKey(delay)# 当前文件名就是下次循环的老文件名last_filename filenameprint(Finished!)cv2.destroyAllWindows()# 如果退出程序需要对当前进行保存self.export_bbox(os.sep.join([self._data_dir, get_bbox_name(filename)]), self._bboxes)print(Labels updated!)if __name__ __main__:dir_with_images askdirectory(titleWhere are the images?)labeling_task SimpleBBoxLabeling(dir_with_images)labeling_task.start()

需要注意的是几个比较通用且独立的方法前加上了一句staticmethod表明是个静态方法。执行这个程序并选择samples文件夹标注时的画面如下图


参考 www.cnblogs.com/xiaowuyi/p/4214271.html www.2cto.com/kf/201806/757475.html blog.csdn.net/weixin_39943271/article/details/79086131 blog.csdn.net/chuigedaqiqiu/article/details/80017510 blog.csdn.net/liu13364876993/article/details/79867061 www.cnblogs.com/shizhengwen/p/8719062.html

本文共计5900个文字,预计阅读时间需要24分钟。

OpenCV中cv2函数有哪些用法?

1. 主要函数1:cv2.imread() 读取图像 参数1:要读取的图像文件名 参数2:读取模式(可选,默认为读取为彩色图像)

1、主要函数1、cv2.imread()读入图片共两个参数第一个参数为要读入的图片文件名第二个参数为如何读取图片读入图片共两个参数第一个参数为要读入的图片文件名第二个参数为如何读取图片包括

cv2.IMREAD_COLOR读入一副彩色图片cv2.IMREAD_GRAYSCALE以灰度模式读入图片cv2.IMREAD_UNCHANGED读入一幅图片并包括其alpha通道。

PS调用opencv就算图像的路径是错的OpenCV 也不会提醒你的但是当你使用命

令print(img)时得到的结果是None。

2、cv2.imshow()创建一个窗口显示图片共两个参数第一个参数表示窗口名字可以创建多个窗口中但是每个窗口不能重名第二个参数是读入的图片窗口大小自动调整为图片大小。

3、cv2.waitKey()键盘绑定函数共一个参数表示等待毫秒数将等待特定的几毫秒看键盘是否有输入返回值为ASCII值。如果其参数为0则表示无限期的等待键盘输入。

4、cv2.destroyAllWindows()删除建立的全部窗口。

5、cv2.destroyWindows()删除指定的窗口。

6、cv2.imwrite(fileimgnum)保存图片共3个参数第一个为保存文件名第二个为读入图片可选的第三个参数它针对特定的格式对于JPEG其表示的是图像的质量用0 - 100的整数表示默认95;对于png ,第三个参数表示的是压缩级别。默认为3.。

注意:

cv2.IMWRITE_JPEG_QUALITY类型为 long ,必须转换成 intcv2.IMWRITE_PNG_COMPRESSION, 从0到9 压缩级别越高图像越小。

cv2.imwrite(1.png,img, [int( cv2.IMWRITE_JPEG_QUALITY), 95])cv2.imwrite(1.png,img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])

cv2.imread()

使用opencv和caffe的伙伴们可能会有一个疑问那就是对于同时读取图片的cv2.imread()和caffe.io.loadimage两个函数有什么差别

1、cv2.imread()接口读图像读进来直接是BGR 格式数据格式在 0~255通道格式为(W,H,C)

2、caffe.io.load_image()读进来是RGB格式和 0~1(float

因此cv2.imread()读取的数据需要进过转换后才和caffe.io.load_image()相同例如

image caffe.io.load_image(examples/images/cat.jpg)

image1cv2.imread(examples/images/cat.jpg) image1cv2.cvtColor(image1,cv2.COLOR_BGR2RGB) image1image1/255.

经过转换后cv2.imread读取的image1和caffe.io.load_image()读取到的image格式相同。

所以在进行特征提取之前要在transformer中设置transformer.set_raw_scale(‘data’,255)(缩放至0~255 以及transformer.set_channel_swap(‘data’,(2,1,0)(将RGB变换到BGR。

调用caffe model进行特征提取分类时要注意区分image用何种方法读进来。


cv2.imread不能正常读取gif格式图片

Python中cv2模块的imread函数可以正常读取’jpg’,png’格式的图片但是不能处理’gif’图片。可以改用imageio模块来处理。

import cv2import imageiodef readImg(im_fn):im cv2.imread(im_fn)if im is None :print({} cv2.imread failed.format(im_fn))tmp imageio.mimread(im_fn)if tmp is not None:imt np.array(tmp)imt imt[0]im imt[:,:,0:3]return im

help(imageio.mimread)


关于cv2.imread()读取图像为BGR问题

opencv读取图像为b,g,r方法,比如

img cv2.imread("xx.jpg") cv2.imshow("xx",img)

展示的结果是正常的:

但是此时读取到的img已经为bgr方式了,如果我们再用其他使用rgb方式读取的函数进行读取时就会出错,比如我用plt对图像进行显示,效果如下:

因为plt函数是rgb方式读取的,所以会出错.这时我们可以手动改变img的通道顺序,如下:

b,g,r cv2.split(img) img_rgb cv2.merge([r,g,b]) plt.figure() plt.imshow(img_rgb) plt.show()

这时img_rgb就是rgb顺序的了.那么这时再用cv2.imshow()显示出来,rgb错误:

简单实例

1、以下面的图片为例 显示结果 显示结果 5、配合画图

import numpy as npimport cv2np.set_printoptions(thresholdnan)# 创建一个宽512高512的黑色画布RGB(0,0,0)即黑色imgnp.zeros((512,512,3),np.uint8)# 画直线,图片对象起始坐标(x轴,y轴)结束坐标颜色宽度cv2.line(img,(0,0),(311,511),(255,0,0),10)# 画矩形图片对象左上角坐标右下角坐标颜色宽度cv2.rectangle(img,(30,166),(130,266),(0,255,0),3)# 画圆形图片对象中心点坐标半径大小颜色宽度cv2.circle(img,(222,222),50,(255.111,111),-1)# 画椭圆形图片对象中心点坐标长短轴顺时针旋转度数开始角度(右长轴表0度上短轴表270度)颜色宽度cv2.ellipse(img,(333,333),(50,20),0,0,150,(255,222,222),-1)# 画多边形指定各个点坐标,array必须是int32类型ptsnp.array([[10,5],[20,30],[70,20],[50,10]], np.int32)# -1表示该纬度靠后面的纬度自动计算出来实际上是4pts pts.reshape((-1,1,2,))# print(pts)# 画多条线False表不闭合True表示闭合闭合即多边形cv2.polylines(img,[pts],True,(255,255,0),5)#写字,字体选择fontcv2.FONT_HERSHEY_SCRIPT_COMPLEX# 图片对象要写的内容左边距字的底部到画布上端的距离字体大小颜色粗细cv2.putText(img,"OpenCV",(10,400),font,3.5,(255,255,255),2)acv2.imwrite("picture.jpg",img)cv2.imshow("picture",img)cv2.waitKey(0)cv2.destroyAllWindows()

6.1、图像的表示

前面章节已经提到过了单通道的灰度图像在计算机中的表示就是一个8位无符号整形的矩阵。在OpenCV的C代码中表示图像有个专门的结构叫做cv::Mat不过在Python-OpenCV中因为已经有了numpy这种强大的基础工具所以这个矩阵就用numpy的array表示。如果是多通道情况最常见的就是红绿蓝RGB三通道则第一个维度是高度第二个维度是高度第三个维度是通道比如图6-1a是一幅3×3图像在计算机中表示的例子

不管是RGB还是BGR都是高度×宽度×通道数H×W×C的表达方式而在深度学习中因为要对不同通道应用卷积所以用的是另一种方式C×H×W就是把每个通道都单独表达成一个二维矩阵如图6-1c所示。

6.2、基本图像处理

6.2.1、存取图像

读图像用cv2.imread()可以按照不同模式读取一般最常用到的是读取单通道灰度图或者直接默认读取多通道。存图像用cv2.imwrite()注意存的时候是没有单通道这一说的根据保存文件名的后缀和当前的array维度OpenCV自动判断存的通道另外压缩格式还可以指定存储质量来看代码例子

import cv2# 读取一张400x600分辨率的图像color_img cv2.imread(test_400x600.jpg)print(color_img.shape)# 直接读取单通道gray_img cv2.imread(test_400x600.jpg, cv2.IMREAD_GRAYSCALE)print(gray_img.shape)# 把单通道图片保存后再读取仍然是3通道相当于把单通道值复制到3个通道保存cv2.imwrite(test_grayscale.jpg, gray_img)reload_grayscale cv2.imread(test_grayscale.jpg)print(reload_grayscale.shape)# cv2.IMWRITE_JPEG_QUALITY指定jpg质量范围0到100默认95越高画质越好文件越大cv2.imwrite(test_imwrite.jpg, color_img, (cv2.IMWRITE_JPEG_QUALITY, 80))# cv2.IMWRITE_PNG_COMPRESSION指定png质量范围0到9默认3越高文件越小画质越差cv2.imwrite(test_imwrite.png, color_img, (cv2.IMWRITE_PNG_COMPRESSION, 5))

6.2.2、 缩放裁剪和补边

缩放通过cv2.resize()实现裁剪则是利用array自身的下标截取实现此外OpenCV还可以给图像补边这样能对一幅图像的形状和感兴趣区域实现各种操作。下面的例子中读取一幅400×600分辨率的图片并执行一些基础的操作

import cv2# 读取一张四川大录古藏寨的照片img cv2.imread(tiger_tibet_village.jpg)# 缩放成200x200的方形图像img_200x200 cv2.resize(img, (200, 200))# 不直接指定缩放后大小通过fx和fy指定缩放比例0.5则长宽都为原来一半# 等效于img_200x300 cv2.resize(img, (300, 200))注意指定大小的格式是(宽度,高度)# 插值方法默认是cv2.INTER_LINEAR这里指定为最近邻插值img_200x300 cv2.resize(img, (0, 0), fx0.5, fy0.5, interpolationcv2.INTER_NEAREST)# 在上张图片的基础上上下各贴50像素的黑边生成300x300的图像img_300x300 cv2.copyMakeBorder(img, 50, 50, 0, 0, cv2.BORDER_CONSTANT, value(0, 0, 0))# 对照片中树的部分进行剪裁patch_tree img[20:150, -180:-50]cv2.imwrite(cropped_tree.jpg, patch_tree)cv2.imwrite(resized_200x200.jpg, img_200x200)cv2.imwrite(resized_200x300.jpg, img_200x300)cv2.imwrite(bordered_300x300.jpg, img_300x300)

6.2.3、色调明暗直方图和Gamma曲线

除了区域图像本身的属性操作也非常多比如可以通过HSV空间对色调和明暗进行调节。HSV空间是由美国的图形学专家A. R. Smith提出的一种颜色空间HSV分别是色调Hue饱和度Saturation和明度Value。在HSV空间中进行调节就避免了直接在RGB空间中调节是还需要考虑三个通道的相关性。OpenCV中H的取值是[0, 180)其他两个通道的取值都是[0, 256)下面例子接着上面例子代码通过HSV空间对图像进行调整

# 通过cv2.cvtColor把图像从BGR转换到HSVimg_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# H空间中绿色比黄色的值高一点所以给每个像素15黄色的树叶就会变绿turn_green_hsv img_hsv.copy()turn_green_hsv[:, :, 0] (turn_green_hsv[:, :, 0]15) % 180turn_green_img cv2.cvtColor(turn_green_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(turn_green.jpg, turn_green_img)# 减小饱和度会让图像损失鲜艳变得更灰colorless_hsv img_hsv.copy()colorless_hsv[:, :, 1] 0.5 * colorless_hsv[:, :, 1]colorless_img cv2.cvtColor(colorless_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(colorless.jpg, colorless_img)# 减小明度为原来一半darker_hsv img_hsv.copy()darker_hsv[:, :, 2] 0.5 * darker_hsv[:, :, 2]darker_img cv2.cvtColor(darker_hsv, cv2.COLOR_HSV2BGR)cv2.imwrite(darker.jpg, darker_img)

无论是HSV还是RGB我们都较难一眼就对像素中值的分布有细致的了解这时候就需要直方图。如果直方图中的成分过于靠近0或者255可能就出现了暗部细节不足或者亮部细节丢失的情况。背景里的暗部细节是非常弱的。这个时候一个常用方法是考虑用Gamma变换来提升暗部细节。Gamma变换是矫正相机直接成像和人眼感受图像差别的一种常用手段简单来说就是通过非线性变换让图像从对曝光强度的线性响应变得更接近人眼感受到的响应。具体的定义和实现还是接着上面代码中读取的图片执行计算直方图和Gamma变换的代码如下

import numpy as np# 分通道计算每个通道的直方图hist_b cv2.calcHist([img], [0], None, [256], [0, 256])hist_g cv2.calcHist([img], [1], None, [256], [0, 256])hist_r cv2.calcHist([img], [2], None, [256], [0, 256])# 定义Gamma矫正的函数def gamma_trans(img, gamma):# 具体做法是先归一化到1然后gamma作为指数值求出新的像素值再还原gamma_table [np.power(x/255.0, gamma)*255.0 for x in range(256)]gamma_table np.round(np.array(gamma_table)).astype(np.uint8)# 实现这个映射用的是OpenCV的查表函数return cv2.LUT(img, gamma_table)# 执行Gamma矫正小于1的值让暗部细节大量提升同时亮部细节少量提升img_corrected gamma_trans(img, 0.5)cv2.imwrite(gamma_corrected.jpg, img_corrected)# 分通道计算Gamma矫正后的直方图hist_b_corrected cv2.calcHist([img_corrected], [0], None, [256], [0, 256])hist_g_corrected cv2.calcHist([img_corrected], [1], None, [256], [0, 256])hist_r_corrected cv2.calcHist([img_corrected], [2], None, [256], [0, 256])# 将直方图进行可视化import matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfig plt.figure()pix_hists [[hist_b, hist_g, hist_r],[hist_b_corrected, hist_g_corrected, hist_r_corrected]]pix_vals range(256)for sub_plt, pix_hist in zip([121, 122], pix_hists):ax fig.add_subplot(sub_plt, projection3d)for c, z, channel_hist in zip([b, g, r], [20, 10, 0], pix_hist):cs [c] * 256ax.bar(pix_vals, channel_hist, zsz, zdiry, colorcs, alpha0.618, edgecolornone, lw0)ax.set_xlabel(Pixel Values)ax.set_xlim([0, 256])ax.set_ylabel(Channels)ax.set_zlabel(Counts)plt.show()

上面三段代码的结果统一放在下图中 代码实现的操作示意在下图中

这个例子实现了延时摄影的功能把程序打开并将摄像头对准一些缓慢变化的画面比如桌上缓慢蒸发的水或者正在生长的小草就能制作出有趣的延时摄影作品。比如下面这个链接中的图片就是用这段程序生成的

images.cnitblog.com/blog2015/609274/201503/251904209276278.gif

程序的结构非常清晰简单注释里也写清楚了每一步所以流程就不解释了。需要提一下的有两点一个是VideoWriter中的一个函数cv2.VideoWriter_fourcc()。这个函数指定了视频编码的格式比如例子中用的是MP42也就是MPEG-4更多编码方式可以在下面的地址查询

Video Codecs by FOURCC

还有一个是KeyboardInterrupt这是一个常用的异常用来获取用户CtrlC的中止捕获这个异常后直接结束循环并释放VideoCapture和VideoWriter的资源使已经捕获好的部分视频可以顺利生成。

从视频中截取帧也是处理视频时常见的任务下面代码实现的是遍历一个指定文件夹下的所有视频并按照指定的间隔进行截屏并保存

import cv2import osimport sys# 第一个输入参数是包含视频片段的路径input_path sys.argv[1]# 第二个输入参数是设定每隔多少帧截取一帧frame_interval int(sys.argv[2])# 列出文件夹下所有的视频文件filenames os.listdir(input_path)# 获取文件夹名称video_prefix input_path.split(os.sep)[-1]# 建立一个新的文件夹名称为原文件夹名称后加上_framesframe_path {}_frames.format(input_path)if not os.path.exists(frame_path):os.mkdir(frame_path)# 初始化一个VideoCapture对象cap cv2.VideoCapture()# 遍历所有文件for filename in filenames:filepath os.sep.join([input_path, filename])# VideoCapture::open函数可以从文件获取视频cap.open(filepath)# 获取视频帧数n_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT))# 同样为了避免视频头几帧质量低下黑屏或者无关等for i in range(42):cap.read()for i in range(n_frames):ret, frame cap.read()# 每隔frame_interval帧进行一次截屏操作if i % frame_interval 0:imagename {}_{}_{:0>6d}.jpg.format(video_prefix, filename.split(.)[0], i)imagepath os.sep.join([frame_path, imagename])print(exported {}!.format(imagepath))cv2.imwrite(imagepath, frame)# 执行结束释放资源cap.release()

7、 用OpenCV实现数据增加小工具

到目前我们已经熟悉了numpy中的随机模块多进程调用和OpenCV的基本操作基于这些基础本节将从思路到代码一步步实现一个最基本的数据增加小工具。

第三章和第四章都提到过数据增加data augmentation作为一种深度学习中的常用手段数据增加对模型的泛化性和准确性都有帮助。数据增加的具体使用方式一般有两种一种是实时增加比如在Caffe中加入数据扰动层每次图像都先经过扰动操作再去训练这样训练经过几代epoch之后就等效于数据增加。还有一种是更加直接简单一些的就是在训练之前就通过图像处理手段对数据样本进行扰动和增加也就是本节要实现的。

这个例子中将包含三种基本类型的扰动随机裁剪随机旋转和随机颜色/明暗。

7.1 随机裁剪

AlexNet中已经讲过了随机裁剪的基本思路我们的小例子中打算更进一步在裁剪的时候考虑图像宽高比的扰动。在绝大多数用于分类的图片中样本进入网络前都是要变为统一大小所以宽高比扰动相当于对物体的横向和纵向进行了缩放这样除了物体的位置扰动又多出了一项扰动。只要变化范围控制合适目标物体始终在画面内这种扰动是有助于提升泛化性能的。实现这种裁剪的思路如下图所示

执行这段代码得到如下窗口

cv2.waitKey()参数不为零的时候则可以和循环结合产生动态画面比如在6.2.4的延时小例子中我们把延时摄影保存下来的所有图像放到一个叫做frames的文件夹下。下面代码从frames的文件夹下读取所有图片并以24的帧率在窗口中显示成动画

OpenCV中cv2函数有哪些用法?

import osfrom itertools import cycleimport cv2# 列出frames文件夹下的所有图片filenames os.listdir(frames)# 通过itertools.cycle生成一个无限循环的迭代器每次迭代都输出下一张图像对象img_iter cycle([cv2.imread(os.sep.join([frames, x])) for x in filenames])key 0while key 27:cv2.imshow(Animation, next(img_iter))key cv2.waitKey(42)

在这个例子中我们采用了Python的itertools模块中的cycle函数这个函数可以把一个可遍历结构编程一个无限循环的迭代器。另外从这个例子中我们还发现cv2.waitKey()返回的就是键盘上出发的按键。对于字母就是ascii码特殊按键比如上下左右等则对应特殊的值其实这就是键盘事件的最基本用法。

8.2 鼠标和键盘事件

因为GUI总是交互的所以鼠标和键盘事件基本使用必不可少上节已经提到了cv2.waitKey()就是获取键盘消息的最基本方法。比如下面这段循环代码就能够获取键盘上按下的按键并在终端输出

while key ! 27:cv2.imshow(Honeymoon Island, img)key cv2.waitKey()# 如果获取的键值小于256则作为ascii码输出对应字符否则直接输出值msg {} is pressed.format(chr(key) if key <256 else key)print(msg)

通过这个程序我们能获取一些常用特殊按键的值比如在笔者用的机器上四个方向的按键和删除键对应的值如下

  • 上↑65362

  • 下↓65364

  • 左←65361

  • 右→65363

  • 删除Delete65535

需要注意的是在不同的操作系统里这些值可能是不一样的。鼠标事件比起键盘事件稍微复杂一点点需要定义一个回调函数然后把回调函数和一个指定名称的窗口绑定这样只要鼠标位于画面区域内的事件就都能捕捉到。把下面这段代码插入到上段代码的while之前就能获取当前鼠标的位置和动作并输出

# 定义鼠标事件回调函数def on_mouse(event, x, y, flags, param):# 鼠标左键按下抬起双击if event cv2.EVENT_LBUTTONDOWN:print(Left button down at ({}, {}).format(x, y))elif event cv2.EVENT_LBUTTONUP:print(Left button up at ({}, {}).format(x, y))elif event cv2.EVENT_LBUTTONDBLCLK:print(Left button double clicked at ({}, {}).format(x, y))# 鼠标右键按下抬起双击elif event cv2.EVENT_RBUTTONDOWN:print(Right button down at ({}, {}).format(x, y))elif event cv2.EVENT_RBUTTONUP:print(Right button up at ({}, {}).format(x, y))elif event cv2.EVENT_RBUTTONDBLCLK:print(Right button double clicked at ({}, {}).format(x, y))# 鼠标中/滚轮键如果有的话按下抬起双击elif event cv2.EVENT_MBUTTONDOWN:print(Middle button down at ({}, {}).format(x, y))elif event cv2.EVENT_MBUTTONUP:print(Middle button up at ({}, {}).format(x, y))elif event cv2.EVENT_MBUTTONDBLCLK:print(Middle button double clicked at ({}, {}).format(x, y))# 鼠标移动elif event cv2.EVENT_MOUSEMOVE:print(Moving at ({}, {}).format(x, y))# 为指定的窗口绑定自定义的回调函数cv2.namedWindow(Honeymoon Island)cv2.setMouseCallback(Honeymoon Island, on_mouse)

8.3 代码物体检测标注的小工具

基于上面两小节的基本使用就能和OpenCV的基本绘图功能就能实现一个超级简单的物体框标注小工具了。基本思路是对要标注的图像建立一个窗口循环然后每次循环的时候对图像进行一次拷贝。鼠标在画面上画框的操作以及已经画好的框的相关信息在全局变量中保存并且在每个循环中根据这些信息在拷贝的图像上再画一遍然后显示这份拷贝的图像。

基于这种实现思路使用上我们采用一个尽量简化的设计

  • 输入是一个文件夹下面包含了所有要标注物体框的图片。如果图片中标注了物体则生成一个相同名称加额外后缀名的文件保存标注信息。

  • 标注的方式是按下鼠标左键选择物体框的左上角松开鼠标左键选择物体框的右下角鼠标右键删除上一个标注好的物体框。所有待标注物体的类别和标注框颜色由用户自定义如果没有定义则默认只标注一种物体定义该物体名称叫“Object”。

  • 方向键的←和→用来遍历图片↑和↓用来选择当前要标注的物体Delete键删除一张图片和对应的标注信息。

每张图片的标注信息以及自定义标注物体和颜色的信息用一个元组表示第一个元素是物体名字第二个元素是代表BGR颜色的tuple或者是代表标注框坐标的元组。对于这种并不复杂复杂的数据结构我们直接利用Python的repr()函数把数据结构保存成机器可读的字符串放到文件里读取的时候用eval()函数就能直接获得数据。这样的方便之处在于不需要单独写个格式解析器。如果需要可以在此基础上再编写一个转换工具就能够转换成常见的Pascal VOC的标注格式或是其他的自定义格式。

在这些思路和设计下我们定义标注信息文件的格式的例子如下

(Hill, ((221, 163), (741, 291)))(Horse, ((465, 430), (613, 570)))

元组中第一项是物体名称第二项是标注框左上角和右下角的坐标。这里之所以不把标注信息的数据直接用pickle保存是因为数据本身不会很复杂直接保存还有更好的可读性。自定义标注物体和对应标注框颜色的格式也类似不过更简单些因为括号可以不写具体如下

Horse, (255, 255, 0)Hill, (0, 255, 255)DiaoSi, (0, 0, 255)

第一项是物体名称第二项是物体框的颜色。使用的时候把自己定义好的内容放到一个文本里然后保存成和待标注文件夹同名后缀名为labels的文件。比如我们在一个叫samples的文件夹下放上一些草原的照片然后自定义一个samples.labels的文本文件。把上段代码的内容放进去就定义了小山头的框为黄色骏马的框为青色以及红色的屌丝。基于以上标注小工具的代码如下

import osimport cv2# tkinter是Python内置的简单GUI库实现一些比如打开文件夹确认删除等操作十分方便from tkFileDialog import askdirectoryfrom tkMessageBox import askyesno# 定义标注窗口的默认名称WINDOW_NAME Simple Bounding Box Labeling Tool# 定义画面刷新的大概帧率是否能达到取决于电脑性能FPS 24# 定义支持的图像格式SUPPOTED_FORMATS [jpg, jpeg, png]# 定义默认物体框的名字为Object颜色蓝色当没有用户自定义物体时用默认物体DEFAULT_COLOR {Object: (255, 0, 0)}# 定义灰色用于信息显示的背景和未定义物体框的显示COLOR_GRAY (192, 192, 192)# 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息BAR_HEIGHT 16# 上下左右ESC及删除键对应的cv.waitKey()的返回值# 注意这个值根据操作系统不同有不同可以通过6.4.2中的代码获取KEY_UP 65362KEY_DOWN 65364KEY_LEFT 65361KEY_RIGHT 65363KEY_ESC 27KEY_DELETE 65535# 空键用于默认循环KEY_EMPTY 0get_bbox_name {}.bbox.format# 定义物体框标注工具类class SimpleBBoxLabeling:def __init__(self, data_dir, fpsFPS, window_nameNone):self._data_dir data_dirself.fps fpsself.window_name window_name if window_name else WINDOW_NAME#pt0是正在画的左上角坐标pt1是鼠标所在坐标self._pt0 Noneself._pt1 None# 表明当前是否正在画框的状态标记self._drawing False# 当前标注物体的名称self._cur_label None# 当前图像对应的所有已标注框self._bboxes []# 如果有用户自定义的标注信息则读取否则用默认的物体和颜色label_path {}.labels.format(self._data_dir)self.label_colors DEFAULT_COLOR if not os.path.exists(label_path) else self.load_labels(label_path)# 获取已经标注的文件列表和还未标注的文件列表imagefiles [x for x in os.listdir(self._data_dir) if x[x.rfind(.) 1:].lower() in SUPPOTED_FORMATS]labeled [x for x in imagefiles if os.path.exists(get_bbox_name(x))]to_be_labeled [x for x in imagefiles if x not in labeled]# 每次打开一个文件夹都自动从还未标注的第一张开始self._filelist labeled to_be_labeledself._index len(labeled)if self._index > len(self._filelist) - 1:self._index len(self._filelist) - 1# 鼠标回调函数def _mouse_ops(self, event, x, y, flags, param):# 按下左键时坐标为左上角同时表明开始画框改变drawing标记为Trueif event cv2.EVENT_LBUTTONDOWN:self._drawing Trueself._pt0 (x, y)# 左键抬起表明当前框画完了坐标记为右下角并保存同时改变drawing标记为Falseelif event cv2.EVENT_LBUTTONUP:self._drawing Falseself._pt1 (x, y)self._bboxes.append((self._cur_label, (self._pt0, self._pt1)))# 实时更新右下角坐标方便画框elif event cv2.EVENT_MOUSEMOVE:self._pt1 (x, y)# 鼠标右键删除最近画好的框elif event cv2.EVENT_RBUTTONUP:if self._bboxes:self._bboxes.pop()# 清除所有标注框和当前状态def _clean_bbox(self):self._pt0 Noneself._pt1 Noneself._drawing Falseself._bboxes []# 画标注框和当前信息的函数def _draw_bbox(self, img):# 在图像下方多出BAR_HEIGHT这么多像素的区域用于显示文件名和当前标注物体等信息h, w img.shape[:2]canvas cv2.copyMakeBorder(img, 0, BAR_HEIGHT, 0, 0, cv2.BORDER_CONSTANT, valueCOLOR_GRAY)# 正在标注的物体信息如果鼠标左键已经按下则显示两个点坐标否则显示当前待标注物体的名称label_msg {}: {}, {}.format(self._cur_label, self._pt0, self._pt1) \if self._drawing \else Current label: {}.format(self._cur_label)# 显示当前文件名文件个数信息msg {}/{}: {} | {}.format(self._index 1, len(self._filelist), self._filelist[self._index], label_msg)cv2.putText(canvas, msg, (1, h12),cv2.FONT_HERSHEY_SIMPLEX,0.5, (0, 0, 0), 1)# 画出已经标好的框和对应名字for label, (bpt0, bpt1) in self._bboxes:label_color self.label_colors[label] if label in self.label_colors else COLOR_GRAYcv2.rectangle(canvas, bpt0, bpt1, label_color, thickness2)cv2.putText(canvas, label, (bpt0[0]3, bpt0[1]15),cv2.FONT_HERSHEY_SIMPLEX,0.5, label_color, 2)# 画正在标注的框和对应名字if self._drawing:label_color self.label_colors[self._cur_label] if self._cur_label in self.label_colors else COLOR_GRAYif self._pt1[0] > self._pt0[0] and self._pt1[1] > self._pt0[1]:cv2.rectangle(canvas, self._pt0, self._pt1, label_color, thickness2)cv2.putText(canvas, self._cur_label, (self._pt0[0] 3, self._pt0[1] 15),cv2.FONT_HERSHEY_SIMPLEX,0.5, label_color, 2)return canvas# 利用repr()导出标注框数据到文件staticmethoddef export_bbox(filepath, bboxes):if bboxes:with open(filepath, w) as f:for bbox in bboxes:line repr(bbox) \nf.write(line)elif os.path.exists(filepath):os.remove(filepath)# 利用eval()读取标注框字符串到数据staticmethoddef load_bbox(filepath):bboxes []with open(filepath, r) as f:line f.readline().rstrip()while line:bboxes.append(eval(line))line f.readline().rstrip()return bboxes# 利用eval()读取物体及对应颜色信息到数据staticmethoddef load_labels(filepath):label_colors {}with open(filepath, r) as f:line f.readline().rstrip()while line:label, color eval(line)label_colors[label] colorline f.readline().rstrip()return label_colors# 读取图像文件和对应标注框信息如果有的话staticmethoddef load_sample(filepath):img cv2.imread(filepath)bbox_filepath get_bbox_name(filepath)bboxes []if os.path.exists(bbox_filepath):bboxes SimpleBBoxLabeling.load_bbox(bbox_filepath)return img, bboxes# 导出当前标注框信息并清空def _export_n_clean_bbox(self):bbox_filepath os.sep.join([self._data_dir, get_bbox_name(self._filelist[self._index])])self.export_bbox(bbox_filepath, self._bboxes)self._clean_bbox()# 删除当前样本和对应的标注框信息def _delete_current_sample(self):filename self._filelist[self._index]filepath os.sep.join([self._data_dir, filename])if os.path.exists(filepath):os.remove(filepath)filepath get_bbox_name(filepath)if os.path.exists(filepath):os.remove(filepath)self._filelist.pop(self._index)print({} is deleted!.format(filename))# 开始OpenCV窗口循环的方法定义了程序的主逻辑def start(self):# 之前标注的文件名用于程序判断是否需要执行一次图像读取last_filename # 标注物体在列表中的下标label_index 0# 所有标注物体名称的列表labels self.label_colors.keys()# 待标注物体的种类数n_labels len(labels)# 定义窗口和鼠标回调cv2.namedWindow(self.window_name)cv2.setMouseCallback(self.window_name, self._mouse_ops)key KEY_EMPTY# 定义每次循环的持续时间delay int(1000 / FPS)# 只要没有按下Esc键就持续循环while key ! KEY_ESC:# 上下键用于选择当前标注物体if key KEY_UP:if label_index 0:passelse:label_index - 1elif key KEY_DOWN:if label_index n_labels - 1:passelse:label_index 1# 左右键切换当前标注的图片elif key KEY_LEFT:# 已经到了第一张图片的话就不需要清空上一张if self._index > 0:self._export_n_clean_bbox()self._index - 1if self._index <0:self._index 0elif key KEY_RIGHT:# 已经到了最后一张图片的话就不需要清空上一张if self._index len(self._filelist) - 1:self._index len(self._filelist) - 1# 删除当前图片和对应标注信息elif key KEY_DELETE:if askyesno(Delete Sample, Are you sure?):self._delete_current_sample()key KEY_EMPTYcontinue# 如果键盘操作执行了换图片则重新读取更新图片filename self._filelist[self._index]if filename ! last_filename:filepath os.sep.join([self._data_dir, filename])img, self._bboxes self.load_sample(filepath)# 更新当前标注物体名称self._cur_label labels[label_index]# 把标注和相关信息画在图片上并显示指定的时间canvas self._draw_bbox(img)cv2.imshow(self.window_name, canvas)key cv2.waitKey(delay)# 当前文件名就是下次循环的老文件名last_filename filenameprint(Finished!)cv2.destroyAllWindows()# 如果退出程序需要对当前进行保存self.export_bbox(os.sep.join([self._data_dir, get_bbox_name(filename)]), self._bboxes)print(Labels updated!)if __name__ __main__:dir_with_images askdirectory(titleWhere are the images?)labeling_task SimpleBBoxLabeling(dir_with_images)labeling_task.start()

需要注意的是几个比较通用且独立的方法前加上了一句staticmethod表明是个静态方法。执行这个程序并选择samples文件夹标注时的画面如下图


参考 www.cnblogs.com/xiaowuyi/p/4214271.html www.2cto.com/kf/201806/757475.html blog.csdn.net/weixin_39943271/article/details/79086131 blog.csdn.net/chuigedaqiqiu/article/details/80017510 blog.csdn.net/liu13364876993/article/details/79867061 www.cnblogs.com/shizhengwen/p/8719062.html