【python第三方库】opencv库常用方法和属性

文章目录


参考链接:
https://www.cnblogs.com/shizhengwen/p/8719062.html
https://blog.csdn.net/Vici__/article/details/100714822
仅作记录学习~

一、opencv总览

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。 OpenCV用C++语言编写,它的主要接口也是C++语言,但是依然保留了大量的C语言接口。

在计算机视觉项目的开发中,OpenCV作为较大众的开源库,拥有了丰富的常用图像处理函数库,采用C/C++语言编写,可以运行在Linux/Windows/Mac等操作系统上,能够快速的实现一些图像处理和识别的任务。此外,OpenCV还提供了Java、python、cuda等的使用接口、机器学习的基础算法调用,从而使得图像处理和图像分析变得更加易于上手,让开发人员更多的精力花在算法的设计上。

应用领域

1、计算机视觉领域方向

  1. 人机互动
  2. 物体识别
  3. 图像分割
  4. 人脸识别
  5. 动作识别
  6. 运动跟踪
  7. 机器人
  8. 运动分析
  9. 机器视觉
  10. 结构分析
  11. 汽车安全驾驶

2、计算机操作底层技术

  1. 图像数据的操作: 分配、释放、复制、设置和转换。( 图像是视频的输入输出I/O ,文件与摄像头的输入、图像和视频文件输出)。
  2. 矩阵和向量的操作以及线性代数的算法程序:矩阵积、解方程、特征值以及奇异值等。
  3. 各种动态数据结构:列表、队列、集合、树、图等。
  4. 基本的数字图像处理:滤波、边缘检测、角点检测、采样与差值、色彩转换、形态操作、直方图、图像金字塔等。
  5. 结构分析:连接部件、轮廓处理、距离变换、各自距计算、模板匹配、Hough变换、多边形逼近、直线拟合、椭圆拟合、Delaunay 三角划分等。
  6. 摄像头定标:发现与跟踪定标模式、定标、基本矩阵估计、齐次矩阵估计、立体对应。
  7. 运动分析:光流、运动分割、跟踪。
  8. 目标识别:特征法、隐马尔可夫模型:HMM。
  9. 基本的GUI:图像与视频显示、键盘和鼠标事件处理、滚动条。
  10. 图像标注:线、二次曲线、多边形、画文字。

二、常用操作

安装

pip install opencv-python
pip install opencv-python==3.3.0.10 -i https://pypi.doubanio.com/simple

1. 图片加载、显示和保存

  • cv2.imread(filename, flags) :读取加载图片
  • cv2.imshow(winname, mat) : 显示图片
  • cv2.waitKey() : 等待图片的关闭。0是等待按键关闭,>0是等待多少ms自动关闭
  • cv2.imwrite(filename, img) : 保存图片
import cv2
 
 
# 读取图片,第二个参数为False时,显示为灰度图像,True为原图
img = cv2.imread(filename="cat.jpg", flags=False)
 
# 显示图片,第一个参数为图片的标题
cv2.imshow(winname="image title", mat=img)
 
# 等待图片的关闭,不写这句图片会一闪而过
cv2.waitKey()
 
# 保存图片
cv2.imwrite("Grey_img.jpg", img)

关于读图的函数 下面是详细介绍

def imread(filename: Any, flags: Any = None) -> None

默认是flag=1,按BGR彩图3通道格式读入 数据深度在 0~2552^8),通道格式为(W,H,C)
- filename:图片的绝对路径或者相对路径。 ps:路径中不能出现中文!
- flags:图像的通道和色彩信息(默认值为1),即彩色图片。
		- flags = -1, 8/16/32深度,原通道;
		- flags = 08位深度,1通道;
		- flags = 18位深度,3通道;
		- flags = 2, 原深度, 1通道;
		- flags = 3, 原深度, 3通道;
		- flags = 48位深度,3通道;

代码示例
path = r'C:xx\0038551.png' # 单通道 16bit 图
	img1 = cv2.imread(path, -1) # 按原通道 原图像深度bit 读取
	print(img1.shape, np.max(img1), np.min(img1))
	#print(img1)

	img2 = cv2.imread(path, 0) # 一律按单通道 8bit读取
	print(img2.shape, np.max(img2), np.min(img2))
	#print(img2)

	img3 = cv2.imread(path, 1) # 一律按3通道,8bit读取
	print(img3.shape, np.max(img3), np.min(img3))
输出如下
(512, 1664) 4095 1087
(512, 1664) 15 4
(512, 1664, 3) 15 4

2. 图像显示窗口创建与销毁

  • cv2.namedWindow(winname, 属性):创建一个窗口
  • cv2.destroyWindow(winname):销毁某个窗口
  • cv2.destroyAllWindows():销毁所有窗口

winname作为窗口的唯一标识,如果想使用指定窗口显示目标图像,需要让cv2.imshow(winname)中的winname与窗口的winname需要保持一致

窗口创建时可以添加的属性:

  • cv2.WINDOW_NORMAL:窗口大小可以改变(同cv2.WINDOW_GUI_NORMAL)
  • cv2.WINDOW_AUTOSIZE:窗口大小不能改变
  • cv2.WINDOW_FREERATIO:窗口大小自适应比例
  • cv2.WINDOW_KEEPRATIO:窗口大小保持比例
  • cv2.WINDOW_GUI_EXPANDED:显示色彩变成暗色
  • cv2.WINDOW_FULLSCREEN:全屏显示
  • cv2.WINDOW_OPENGL:支持OpenGL的窗口
img = cv2.imread("cat.jpg")
 
# 第二个参数为窗口属性
cv2.namedWindow(winname="title", cv2.WINDOW_NORMAL)
 
# 如果图片显示想使用上面的窗口,必须保证winname一致
cv2.imshow(winname="title", img)
 
cv2.waitKey()
 
# 销毁
cv2.destroyWindow("title")
# 销毁所有窗口:cv2.destroyAllWindows()

3. 图片的常用属性的获取

  • img.shape:打印图片的高、宽和通道数(当图片为灰度图像时,颜色通道数为1,不显示)

  • img.size:打印图片的像素数目

  • img.dtype:打印图片的格式

注意:这几个是图片的属性,并不是调用的函数,所以后面没有‘ () ’。

import cv2
 
img = cv2.imread("cat.jpg")
imgGrey = cv2.imread("cat.jpg", False)
 
print(img.shape)
print(imgGrey.shape)
 
#输出:
#(280, 300, 3)
#(280, 300)
 
print(img.size)
print(img.dtype)
 
#输出:
# 252000
# uint8

4. 生成指定大小的矩形区域(ROI)

  1. 一个图片img,它的某个像素点可以用 img[x, y, c] 表示(x,y为坐标,c为通道数)
  2. 同理,这个图片的某个矩形区域可以表示为:img[x1:x2, y1:y2, c](相当于截下一块矩形,左上角坐标为(x1, y1),右下角坐标为(x2, y2))
  3. 其中 c 一般取值为0,1,2(BGR)代表第几个颜色通道,可以省略不写 img[x, y] 代表所有通道。

实例一:生成一个大小为(300,400)颜色通道为3的红色图片

import cv2
import numpy as np
 
imgzero = np.zeros(shape=(300, 400, 3), dtype=np.uint8)
 
imgzero[:, :] = (0, 0, 255) # (B, G, R)
cv2.imshow("imgzero",imgzero)
cv2.waitKey()

实例二:从一张图片上截取一个矩形区域

import cv2
import numpy as np
 
img = cv2.imread("cat.jpg")
# 输出(50,100)上的像素值
num = img[50, 100]
print(num)
 
# 截取部分区域并显示
region = img[50:100, 50:100]
cv2.imshow("img", region)
cv2.waitKey()

5、图片颜色通道的分离与合并

  • cv2.split(m):将图片m分离为三个颜色通道

  • cv2.merge(mv):将三个颜色通道合并为一张图片

import cv2
 
img = cv2.imread("cat.jpg")
 
b, g, r = cv2.split(img)
 
merge = cv2.merge([b, g, r])

6、两张图片相加,改变对比度和亮度

  • cv2.add(src1, src2):普通相加

  • cv2.addWeighted(src1, alpha, src2, w2,beta):带权相加
    - src1:第一张图片
    - alpha:第一张图片权重
    - src2:第二张图片
    - beta:第二张图片权重
    - gamma:图1与图2作和后添加的数值。
    - dst:输出图片

import cv2
 
img1 = cv2.imread("cat.jpg")
img2 = cv2.imread("dog.jpg")
 
add1 = cv2.add(img1,img2)
 
add2 = cv2.addWeighted(img1, 0.5, img2, 0.5, 3)
 
cv2.imshow("add1", add1)
cv2.imshow("add2", add2)
cv2.waitKey()

cv2.addWeighted(src1, alpha, src2, w2,beta)可以改变图像的对比度和亮度。
通过改变alpha的值改变对比度,beta控制亮度。

# 改变对比度和亮度
def contrast_brightness_demo(img, c, b):
    h, w, ch = img.shape
    blank = np.zeros([h, w, ch], img.dtype)
    dst = cv2.addWeighted(img, c, blank, 1-c , b)
    cv2.imshow("contrast_brightness_demo", dst)

7、像素运算(1)加减乘除

对两张相同大小的图像进行加减乘除,cv2.imread()读取的图像,其实相当于获取了一个多维数组,每一个像素值就是数组坐标下的值。那么像素的基本运算就相当于是数组之间的运算

def add_demo(m1, m2):
    dst = cv2.add(m1, m2)
    cv2.imshow("add", dst)
 
def subtract_demo(m1, m2):
    dst = cv2.subtract(m1, m2)
    cv2.imshow("subtract", dst)
 
def multiply_demo(m1, m2):
    dst = cv2.multiply(m1, m2)
    cv2.imshow("multiply", dst)
 
def divide_demo(m1, m2):
    dst = cv2.divide(m1, m2)
    cv2.imshow("divide", dst)

8、像素运算(2)均值&方差

def demo(img):
    # 均值
    M1 = cv2.mean(img)
    print(M1)
    # 均值和方差
    M1, dev1 = cv2.meanStdDev(img)
    print(M1)
    print(dev1)

9、像素运算(3)逻辑运算——与、或、非、异或

其中非运算就是对图像进行颜色反转

def logic_demo(m1, m2):
    dst = cv2.bitwise_and(m1, m2)
    cv2.imshow("bitwise_and", dst)
    dst = cv2.bitwise_or(m1, m2) 
    cv2.imshow("bitwise_or", dst)
    dst = cv2.bitwise_not(m1, m2)
    cv2.imshow("bitwise_not", dst)
    dst = cv2.bitwise_xor(m1, m2)
    cv2.imshow("bitwise_xor", dst)

10、计算执行时间

  • cv2.getTickCount() :用于返回从操作系统启动到当前所经的计时周期数;
  • cv2.getTickFrequency():用于返回CPU的频率,也就是一秒内重复的次数。

时间(s) = 总次数Count / 一秒内重复的次数Frequency
时间(ms) = 1000 *总次数Count / 一秒内重复的次数Frequency

t1 = cv2.getTickCount()
function()   # 待测试的函数
t2 = cv2.getTickCount()
time = (t2 - t1) / cv2.getTickFrequency()
print("time : %s ms" % (time * 1000))

11、泛洪填充(需要4、ROI的知识)

Windows自带的画图中有一个工具(油桶形状的),看下右图中,白色的背景,你用黑色画一个菱形,然后用这个油桶工具点一下菱形内部,就可以把菱形内部染成红色。
你在菱形内部用鼠标点击的那一下,点在了一个像素点上,我们知道这个像素点是白色的,那么油桶在染色的时候,就在这个原像素点的周围寻找相同的像素(白色像素),然后把和原像素点相同的像素都染成红色。(就像是从原像素点360度无死角发散寻找)
那么它什么时候结束染色呢?当它遇到和原像素点的像素不同的点时,就会中止这个方向的寻找。(也就是遇到了我们画的那个黑色边框)

同理在OpenCV里,提供了这样的函数

def floodFill( image, mask, seedPoint, newVal, loDiff=None, upDiff=None, flags=None)

  • image, 原图像
  • mask, 掩码,单通道8位图像,比image的高度多2个像素,宽度多2个像素。
  • seedPoint, 起始点(原像素点,相当于鼠标点击的那个像素点)
  • newVal, 在重绘区域像素的新值(RBG值,相当于上图指定的红色)
  • loDiff=None, 像素值的下限差值(最多比原像素点低多少)
  • upDiff=None, 像素值的上限差值(最多比原像素点高多少)
  • flags
    • FLOODFILL_FIXED_RANGE – 改变图像,泛洪填充
    • FLOODFILL_MASK_ONLY – 不改变图像,只填充遮罩层本身,忽略新的颜色值参数
def fill_color_demo():
    copyImg = img.copy()
    h, w = img.shape[:2]
    mask = np.zeros([h+2, w+2], np.uint8)
    cv2.floodFill(copyImg, mask, (100, 200), (0, 255, 0), (100, 100, 100), (50, 50, 50), cv2.FLOODFILL_FIXED_RANGE)
    cv2.imshow("fill_color_demo", copyImg)
 
def fill_binary_demo():
    img2 = np.zeros([400, 400, 3], np.uint8)
    img2[100:300, 100:300, :] = 255
    mask = np.ones([402, 402], np.uint8)
    mask[101:301, 101:301] = 0
 
    cv2.floodFill(img2, mask, (200, 200), (0, 0, 255), cv2.FLOODFILL_MASK_ONLY)
    cv2.imshow("fill_binary_demo", img2)

12、彩色空间转换

cv2.cvtColor

  • 原型:cvtColor(src,code,dst=None,dstCn=None)

  • 作用:将一幅图像从一个色彩空间转换到另一个色彩空间

  • 参数:code,转换的色彩空间

# 色彩空间转换
def color_space_demo(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cv2.imshow("gray", gray)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    cv2.imshow("hsv", hsv)
    yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
    cv2.imshow("yuv", yuv)
    ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
    cv2.imshow("ycrcb", ycrcb)

在这里插入图片描述

13、cv2.inRange(hsv, lower_hsv, upper_hsv)

利用cv2.inRange函数设阈值,这里注意用的颜色空间是hsv。

HSV:HSV颜色空间是孟塞尔彩色空间的简化形式,是一种基于感知的颜色模型。它将彩色信号分为3种属性:色调(Hue,H),饱和度(Saturation,S),亮度(Value,V)

  • 色调表示从一个物体反射过来的或透过物体的光波长,也就是说,色调是由颜色的名称来辨别的,如红、黄、蓝;
  • 亮度是颜色的明暗程度;
  • 饱和度是颜色的深浅,如深红、浅红。

HSV颜色空间反映了人观察色彩的方式,具有两个显著的特点:

  • 亮度分量与图像的彩色信息无关
  • “色调”和“饱和度”分量与人感受颜色的方式是紧密相连的

可以根据右表来确定lower_hsv, upper_hsv的取值。
在这里插入图片描述

# 颜色追踪
def extrace_object_demo():
    capture = cv2.VideoCapture("testvideo.mp4")
    while(True):
        ret, frame = capture.read()
        if ret == False:
            break
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        lower_hsv = np.array([0, 0, 0])
        upper_hsv = np.array([180, 255, 46])
        mask = cv2.inRange(hsv, lower_hsv, upper_hsv)
        dst = cv2.bitwise_and(frame, frame, mask=mask)
        cv2.imshow("video", frame)
        cv2.imshow("video", dst)
        c = cv2.waitKey(40)
        if c == 27:
            break

14、均值模糊、中值模糊,高斯模糊,双边模糊

附一篇博客:真正搞懂均值模糊、中值模糊、高斯模糊、双边模糊
模糊操作基本原理

- 基于离散卷积
- 定义好每个卷积核
- 不同卷积核得到不同的卷积效果
- 模糊是卷积的一种表象 

cv2.blur

  • 原型:blur(src,ksize,dst=None,anchor=None,borderType=None)

  • 作用:对图像进行算术平均值模糊

  • 参数:ksize,卷积核的大小。dst,若填入dst,则将图像写入到dst矩阵。

cv2.medianBlur

  • 原型:mediaBlur(src,ksize,dst=None)

  • 作用:对图像进行中值模糊

def blur_demo(img):
    # 均值模糊
    dst = cv2.blur(img, (5, 5)) # 5*5的卷积核
    cv2.imshow("dst", dst)
 
    # 中值模糊,可以去噪音
    dst = cv2.medianBlur(img, 5)
 
    # 自定义
    kernel = np.ones([5, 5], np.float32) / 25
    dst = cv2.filter2D(img, -1, kernel)
 
    # 锐化(特定的卷积核)
    kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]], np.float32)
    dst = cv2.filter2D(img, -1, kernel)

cv2.GaussianBlur

  • 原型:GaussianBlur(src, ksize, sigmaX, dst=None, sigmaY=None, borderType=None)

  • 作用:对图像进行高斯模糊

  • 参数:sigmaX,X方向上的方差,一般设为0让系统自动计算。

def Gauss_blur():
    img = np.array([[14, 15, 16], [24, 25, 26], [34, 35, 36]], dtype=np.float32)
    blur = cv2.GaussianBlur(img, (3, 3), 1.5)
    print(blur)
 
 
Gauss_blur()
 
# output:
[[20.771631 21.156027 21.540426]
 [24.615604 25.       25.3844  ]
 [28.45958  28.843975 29.228374]]

cv2.bilateralFilter

  • 原型:bilateralFilter(src, d, sigmaColor, sigmaSpace, dst=None, borderType=None)
  • 作用:对图像进行双边模糊
  • 参数:
    • int d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
    • double sigmaColor: 颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有越宽广的颜色会被混合到一起,产生较大的半相等颜色区域。 (这个参数可以理解为值域核的)
    • double sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着越远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace无关,否则d正比于sigmaSpace. (这个参数可以理解为空间域核的)
def bilateralFilter_demo(img):
    dst = cv2.bilateralFilter(img, 0, 100, 150)
    cv2.imshow("bilateralFilter", dst)
 
bi_demo(img)

15、二值化(cv2.threshold)

二值化就是把图像的像素转变为0或者255,只有这两个像素值。

  • 原型:threshold(src,thresh,maxval,type,dst=None)

  • 作用:将图像的每个像素点进行二值化

  • 参数:thresh,阈值(最小值)。maxval,二值化的最大取值。
    type,二值化类型,一般设为0,也可以取以下的值:
    在这里插入图片描述

  • 返回值:计算过后的阈值值和二值化后的图像(如果dst是None)

# 全局二值化
def threshold_demo():
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
    cv2.ADAPTIVE
    print("threshold value : %s\n" % ret)
    cv2.imshow("binary_global", binary)
 
threshold_demo()

函数:adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)

参数:

  • maxValue:阈值的最大值;

  • adaptiveMethod:指定自适应阈值算法。可选择
    ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_C两种。(自适应阈值化计算大概过程是为每一个象素点单独计算的阈值,即每个像素点的阈值都是不同的,就是将该像素点周围blockSize*blockSize区域内的像素加权平均,然后减去一个常数C,从而得到该点的阈值。)。

    • ADAPTIVE_THRESH_MEAN_C:为局部邻域块的平均值。该算法是先求出块中的均值,再减去常数C。
    • ADAPTIVE_THRESH_GAUSSIAN_C:为局部邻域块的高斯加权和。该算法是在区域中(x,y)周围的像素根 据高斯函数按照他们离中心点的距离进行加权计算, 再减去常数C。
  • thresholdType:指定阈值类型。可选择THRESH_BINARY或者THRESH_BINARY_INV两种。(即二进制阈值或反二进制阈值)。

  • blockSize:表示邻域块大小,用来计算区域阈值,奇数,一般选择为3、5、7…等。

  • C:表示与算法有关的参数,它是一个从均值或加权均值提取的常数,可以是负数。(具体见下面的解释)。

# 局部二值化
def local_threshold_demo():
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 25, 10)
    #print("threshold value : %s\n" % ret)
    cv2.imshow("binary_local", binary)

16、图像直方图

图像直方图详解——定义、计算、均衡、比较、反射投影

17、模板匹配

模板匹配,就是在整个图像区域发现与给定子图像匹配的小块区域,需要模板图像T和待检测图像-源图像S;

工作方法:在待检测的图像上,从左到右,从上倒下计算模板图像与重叠子图像匹配度,匹配度越大,两者相同的可能性越大。

函数:matchTemplate(image, templ, method, result=None, mask=None)

参数:

  • image:源图像S;
  • templ:模板图像T,一般是源图像S中的一小块;
  • method:模板匹配算法(cv.TM_SQDIFF_NORMED最小时最相似,其他最大时最相似)
    在这里插入图片描述
import cv2
import numpy as np
from matplotlib import pyplot as plt
 
 
def template_demo():
    tpl = cv2.imread("sample.jpg")
    target = cv2.imread("target.jpg")
    cv2.imshow("tpl", tpl)
    cv2.imshow("target", target)
    methods = [cv2.TM_SQDIFF_NORMED, cv2.TM_CCORR_NORMED, cv2.TM_CCOEFF_NORMED]  # 三种模板匹配方法
    th, tw = tpl.shape[:2]
    
    for md in methods:
        print(md)
        result = cv2.matchTemplate(target, tpl, md)  # 得到匹配结果
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
        if md == cv2.TM_SQDIFF_NORMED:  # cv.TM_SQDIFF_NORMED最小时最相似,其他最大时最相似
            tl = min_loc
        else:
            tl = max_loc
 
        br = (tl[0] + tw, tl[1] + th)
        cv2.rectangle(target, tl, br, (0, 0, 255), 2)  # tl为左上角坐标,br为右下角坐标,从而画出矩形
        cv2.imshow("match-"+np.str(md), target)
 
template_demo()
cv2.waitKey(0)
cv2.destroyAllWindows()

在这里插入图片描述

18、图像金字塔(上采样和下采样)

图像缩小(先高斯模糊,再降采样,需要一次次重复,不能一次到底)

图像扩大(先扩大,再卷积或者使用拉普拉斯金字塔)

推荐博客:OpenCV—图像金字塔原理

import cv2
import numpy as np
 
 
# 图像金字塔和拉普拉斯金字塔(L1 = g1 - expand(g2)):reduce:高斯模糊+降采样,expand:扩大+卷积
# PyrDown降采样,PyrUp还原
def pyramid_demo(image):
    level = 4
    temp = image.copy()
    pyramid_images = []
 
    for i in range(level):
        dst = cv2.pyrDown(temp)
        pyramid_images.append(dst)
        cv2.imshow("pyramid_down_"+str(i+1), dst)
        temp = dst.copy()
    return pyramid_images
 
 
def laplace_demo(image):  # 注意:图片必须是满足2^n这种分辨率
    pyramid_images = pyramid_demo(image)
    level = len(pyramid_images)
 
    for i in range(level-1, -1, -1):
        if i-1 < 0:
            expand  = cv2.pyrUp(pyramid_images[i], dstsize=image.shape[:2])
            lpls = cv2.subtract(image, expand)
            cv2.imshow("laplace_demo"+str(i), lpls)
        else:
            expand = cv2.pyrUp(pyramid_images[i], dstsize=pyramid_images[i-1].shape[:2])
            lpls = cv2.subtract(pyramid_images[i-1], expand)
            cv2.imshow("laplace_demo"+str(i), lpls)
 
 
src = cv2.imread("img1.jpg") # 图像必须是2^n * 2^m的
cv2.imshow("demo", src)
#pyramid_demo(src)
laplace_demo(src)
cv2.waitKey(0)
cv2.destroyAllWindows()

19、图像梯度/各种滤波器

图像梯度其实就是对图像进行求导,图像也是一个函数(离散的),这里其实就是用特定的滤波器来进行卷积操作

cv2.Sobel

Sobel算子是高斯平滑和微分操作的结合体,所以他的抗噪声能力很好。他计算的是一阶导数,可以自己定义x方向或者y方向

卷积因子:
在这里插入图片描述
原型: Sobel(src,ddepth,dx,dy,dst=None,ksize=None,scale=None,delta=None,borderType=None)

  • 作用:对图像进行Sobel算子计算。检测出其边缘。

  • 参数:dx,x方向上的导数阶数;dy,y方向上的导数阶数。

import cv2 as cv
import numpy as np
 
 
def sobel_demo(image):
    grad_x = cv2.Sobel(image, cv2.CV_32F, 1, 0)  # 采用Scharr边缘更突出
    grad_y = cv2.Sobel(image, cv2.CV_32F, 0, 1)
 
    gradx = cv2.convertScaleAbs(grad_x)  # 由于算完的图像有正有负,所以对其取绝对值
    grady = cv2.convertScaleAbs(grad_y)
 
    # 计算两个图像的权值和,dst = src1*alpha + src2*beta + gamma
    gradxy = cv2.addWeighted(gradx, 0.5, grady, 0.5, 0)
 
    cv2.imshow("gradx", gradx)
    cv2.imshow("grady", grady)
    cv2.imshow("gradient", gradxy)
 
 
src = cv.imread("../images/lena.jpg")
cv.imshow("lena",src)
sobel_demo(src)
cv.waitKey(0)
cv.destroyAllWindows()

cv2.scharr

原型:Scharr(src, ddepth, dx, dy, dst=None, scale=None, delta=None, borderType=None, /)
Sobel的优化版在使用3*3卷积核时这个优于Sobel,其它尺寸的卷积核用Sobel就行
在这里插入图片描述

import cv2 as cv
import numpy as np
 
 
def scharr_demo(image):
    grad_x = cv2.Scharr(image, cv2.CV_32F, 1, 0)  # 采用Scharr边缘更突出
    grad_y = cv2.Scharr(image, cv2.CV_32F, 0, 1)
 
    gradx = cv2.convertScaleAbs(grad_x)  # 由于算完的图像有正有负,所以对其取绝对值
    grady = cv2.convertScaleAbs(grad_y)
 
    # 计算两个图像的权值和,dst = src1*alpha + src2*beta + gamma
    gradxy = cv2.addWeighted(gradx, 0.5, grady, 0.5, 0)
 
    cv2.imshow("gradx", gradx)
    cv2.imshow("grady", grady)
    cv2.imshow("gradient", gradxy)
 
 
src = cv.imread("../images/lena.jpg")
cv.imshow("lena",src)
scharr_demo(src)
cv.waitKey(0)
cv.destroyAllWindows()

cv2.Laplacian

Laplacian算子是个二阶微分。下面两个卷积核,靠上的是4邻域的,靠下的是8邻域的。函数默认为8邻域。
在这里插入图片描述
原型:Laplacian(src,ddepth,dst=None,ksize=None,scale=None,delta=None,borderType=None)

  • 作用:检测图像边缘。

  • 参数:ddepth,图像位深度,对于灰度图来说,其值为:cv2.CV_8U。ksize,希望使用的卷积核的大小。scale,是缩放导数的比例常数。

import cv2 as cv
import numpy as np
 
 
def laplace_demo(image):  # 二阶导数,边缘更细
    dst = cv2.Laplacian(image,cv2.CV_32F)
    lpls = cv2.convertScaleAbs(dst)
    cv2.imshow("laplace_demo", lpls)
 
 
src = cv.imread("../images/lena.jpg")
cv.imshow("lena",src)
laplace_demo(src)
cv.waitKey(0)
cv.destroyAllWindows()

20. VideoCapture 类(视频读取)

cv2.VideoCapture()

  • 原型:VideoCapture(*args,**kwargs)

  • 作用:初始化VideoCapture类并利用构造函数读入该视频的当前帧。

  • 参数:一般仅填入一个,即文件名。如果填入整数,则打开对应的捕获设备ID(多个相机)。若为0,则打开默认摄像头。

VideoCapture.get()

  • 原型:VideoCapture.get(self,propld)

  • 作用:返回该视频的propld所指定的属性

  • 参数:propld,为需要读取的视频属性参数位,一般以cv2.CAP_PROP_ 开头
    在这里插入图片描述

VideoCapture.isOpened()

  • 参数:无

  • 作用:判断设备/文件是否读取成功,若成功,返回True

VideoCapture.release()

  • 参数:无

  • 作用:关闭文件/摄像头

VideoCapture.read()

  • 参数:无

  • 返回值:bool,numpy.array

作用:读取该文件/摄像头的下一帧,成功与否由bool返回值决定,返回的帧矩阵为第二个参数

代码

  1. 从相机设备读取:cv2.VideoCapture(Index)——Index默认为0,可以根据相机数目增加,cap.read()返回布尔值,最后记得释放捕获
import numpy as np
import cv2
 
cap = cv2.VideoCapture(0)
ret = cap.set(3,320)####设置捕获窗口大小
ret = cap.set(4,240)
 
while(cap.isOpened()):
    ret, frame = cap.read()
 
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)####彩色图像用BGR2RGB
 
    cv2.imshow('frame',gray)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
 
cap.release()
cv2.destroyAllWindows()
  1. 从视频文件捕捉:VideoCapture(filename):
import numpy as np
import cv2
 
cap = cv2.VideoCapture('vtest.avi')
 
while(cap.isOpened()):
    ret, frame = cap.read()##ret返回布尔量
 
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
    cv2.imshow('frame',gray)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
 
cap.release()
cv2.destroyAllWindows()

21. VideoWriter类(视频保存)

cv2.VideoWriter()

  • 原型:cv2.VideoWriter(filename, fourcc, fps, frameSize)

  • 参数:

    • 第一个,写入的视频文件名。文件路径,默认在pycharm目录下。
      也可以将文件路径写全,如:‘C:\Users\TC\PycharmProjects\pycharm\out.avi’,但需要注意转义字符 \ 使得路径出现问题,所以正确写法为,‘C:/Users/TC/PycharmProjects/pycharm/out.avi’或’C:\Users\TC\PycharmProjects\pycharm\out.avi’。

    • 第二个,视频编码格式,由cv2.VideoWriter_fourcc返回的视频制式特定代码,通常有XVID,MPEG等,见下图。

    • 第三个,该视频的帧率fps。

    • 第四个,一个tuple,为该视频的宽、高 (320,240)。
      在这里插入图片描述

VideoWriter.write()

  • 原型:VideoCapture.write(image)

  • 作用:将当前帧内容写入视频文件

  • 参数:image,写入的当前帧

代码

#!/usr/bin/env python
 
import numpy as np
import cv2
cap = cv2.VideoCapture(0)
i = 0
while( i < 18):
    i = i+1
    print(cap.get(i))
 
ret = cap.set(3,320)
ret = cap.set(4,240)
 
#output info
fourcc = cv2.VideoWriter_fourcc(*'XVID')	#视频编码格式
out = cv2.VideoWriter('output.avi', fourcc, 20.0, (320,240))
while(cap.isOpened()):
    ret, frame = cap.read()
    if ret == True:
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
        out.write(frame)
 
        cv2.imshow('image', gray)
        k = cv2.waitKey(1)
        if (k & 0xff == ord('q')):
            break
    else:
        break
 
cap.release()
out.release()
cv2.destroyAllWindows()

三、常用代码示例

1. 读入一副图像,按’s’键保存后退出,其它任意键则直接退出不保存

import cv2
img = cv2.imread('1.jpg',cv2.IMREAD_UNCHANGED)
cv2.imshow('image',img)
k = cv2.waitKey(0)
if k == ord('s'): # wait for 's' key to save and exit
    cv2.imwrite('1.png',img)
    cv2.destroyAllWindows()
else: 
    cv2.destroyAllWindows()

2. 读入一副图像,给图片加文本

import cv2

# img=cv2.imread('1.jpg',cv2.IMREAD_COLOR)
img=cv2.imread('1.png',cv2.IMREAD_COLOR)    # 打开文件
font = cv2.FONT_HERSHEY_DUPLEX  # 设置字体
# 图片对象、文本、像素、字体、字体大小、颜色、字体粗细
imgzi = cv2.putText(img, "zhengwen", (1100, 1164), font, 5.5, (0, 0, 0), 2,)
# cv2.imshow('lena',img)



cv2.imwrite('5.png',img)    # 写磁盘
cv2.destroyAllWindows()     # 毁掉所有窗口
cv2.destroyWindow(wname)    # 销毁指定窗口

3. opencv自定义画图

import numpy as np
import cv2

np.set_printoptions(threshold='nan')
# 创建一个宽512高512的黑色画布,RGB(0,0,0)即黑色
img=np.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类型
pts=np.array([[10,5],[20,30],[70,20],[50,10]], np.int32)
# -1表示该纬度靠后面的纬度自动计算出来,实际上是4

pts = pts.reshape((-1,1,2,))
# print(pts)
# 画多条线,False表不闭合,True表示闭合,闭合即多边形
cv2.polylines(img,[pts],True,(255,255,0),5)

#写字,字体选择
font=cv2.FONT_HERSHEY_SCRIPT_COMPLEX

# 图片对象,要写的内容,左边距,字的底部到画布上端的距离,字体,大小,颜色,粗细
cv2.putText(img,"OpenCV",(10,400),font,3.5,(255,255,255),2)

a=cv2.imwrite("picture.jpg",img)
cv2.imshow("picture",img)
cv2.waitKey(0)

cv2.destroyAllWindows()

4. 缩放,裁剪和补边

缩放通过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), fx=0.5, fy=0.5, 
                              interpolation=cv2.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)

5.色调,明暗,直方图和Gamma曲线

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

# 通过cv2.cvtColor把图像从BGR转换到HSV
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# H空间中,绿色比黄色的值高一点,所以给每个像素+15,黄色的树叶就会变绿
turn_green_hsv = img_hsv.copy()
turn_green_hsv[:, :, 0] = (turn_green_hsv[:, :, 0]+15) % 180
turn_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,可能就出现了暗部细节不足或者亮部细节丢失的情况。比如图6-2中,背景里的暗部细节是非常弱的。这个时候,一个常用方法是考虑用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 plt
from mpl_toolkits.mplot3d import Axes3D

fig = 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, projection='3d')
    for c, z, channel_hist in zip(['b', 'g', 'r'], [20, 10, 0], pix_hist):
        cs = [c] * 256
        ax.bar(pix_vals, channel_hist, zs=z, zdir='y', color=cs, alpha=0.618, edgecolor='none', lw=0)

    ax.set_xlabel('Pixel Values')
    ax.set_xlim([0, 256])
    ax.set_ylabel('Channels')
    ax.set_zlabel('Counts')

plt.show()

在这里插入图片描述
可以看到,Gamma变换后的暗部细节比起原图清楚了很多,并且从直方图来看,像素值也从集中在0附近变得散开了一些。

6. 图像的仿射变换

图像的仿射变换涉及到图像的形状位置角度的变化,是深度学习预处理中常到的功能,在此简单回顾一下。仿射变换具体到图像中的应用,主要是对图像的缩放,旋转,剪切,翻转和平移的组合。在OpenCV中,仿射变换的矩阵是一个2×3的矩阵,其中左边的2×2子矩阵是线性变换矩阵,右边的2×1的两项是平移项:
在这里插入图片描述
对于图像上的任一位置(x,y),仿射变换执行的是如下的操作:
在这里插入图片描述
需要注意的是,对于图像而言,宽度方向是x,高度方向是y,坐标的顺序和图像像素对应下标一致。所以原点的位置不是左下角而是右上角y的方向也不是向上,而是向下。在OpenCV中实现仿射变换是通过仿射变换矩阵和cv2.warpAffine()这个函数,还是通过代码来理解一下,例子中图片的分辨率为600×400:

import cv2
import numpy as np

# 读取一张斯里兰卡拍摄的大象照片
img = cv2.imread('lanka_safari.jpg')

# 沿着横纵轴放大1.6倍,然后平移(-150,-240),最后沿原图大小截取,等效于裁剪并放大
M_crop_elephant = np.array([
    [1.6, 0, -150],
    [0, 1.6, -240]
], dtype=np.float32)

img_elephant = cv2.warpAffine(img, M_crop_elephant, (400, 600))
cv2.imwrite('lanka_elephant.jpg', img_elephant)

# x轴的剪切变换,角度15°
theta = 15 * np.pi / 180
M_shear = np.array([
    [1, np.tan(theta), 0],
    [0, 1, 0]
], dtype=np.float32)

img_sheared = cv2.warpAffine(img, M_shear, (400, 600))
cv2.imwrite('lanka_safari_sheared.jpg', img_sheared)

# 顺时针旋转,角度15°
M_rotate = np.array([
    [np.cos(theta), -np.sin(theta), 0],
    [np.sin(theta), np.cos(theta), 0]
], dtype=np.float32)

img_rotated = cv2.warpAffine(img, M_rotate, (400, 600))
cv2.imwrite('lanka_safari_rotated.jpg', img_rotated)

# 某种变换,具体旋转+缩放+旋转组合可以通过SVD分解理解
M = np.array([
    [1, 1.5, -400],
    [0.5, 2, -100]
], dtype=np.float32)

img_transformed = cv2.warpAffine(img, M, (400, 600))
cv2.imwrite('lanka_safari_transformed.jpg', img_transformed)

在这里插入图片描述

7. 制作延时摄影视频

视频中最常用的就是从视频设备采集图片或者视频,或者读取视频文件并从中采样。所以比较重要的也是两个模块,一个是VideoCapture,用于获取相机设备并捕获图像和视频,或是从文件中捕获。还有一个VideoWriter,用于生成视频。还是来看例子理解这两个功能的用法,首先是一个制作延时摄影视频的小例子:

import cv2
import time

interval = 60           # 捕获图像的间隔,单位:秒
num_frames = 500        # 捕获图像的总帧数
out_fps = 24            # 输出文件的帧率

# VideoCapture(0)表示打开默认的相机
cap = cv2.VideoCapture(0)

# 获取捕获的分辨率
size =(int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
       int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
       
# 设置要保存视频的编码,分辨率和帧率
video = cv2.VideoWriter(
    "time_lapse.avi", 
    cv2.VideoWriter_fourcc('M','P','4','2'), 
    out_fps, 
    size
)

# 对于一些低画质的摄像头,前面的帧可能不稳定,略过
for i in range(42):
    cap.read()

# 开始捕获,通过read()函数获取捕获的帧
try:
    for i in range(num_frames):
        _, frame = cap.read()
        video.write(frame)

        # 如果希望把每一帧也存成文件,比如制作GIF,则取消下面的注释
        # filename = '{:0>6d}.png'.format(i)
        # cv2.imwrite(filename, frame)

        print('Frame {} is captured.'.format(i))
        time.sleep(interval)
except KeyboardInterrupt:
    # 提前停止捕获
    print('Stopped! {}/{} frames captured!'.format(i, num_frames))

# 释放资源并写入视频文件
video.release()
cap.release()

这个例子实现了延时摄影的功能,把程序打开并将摄像头对准一些缓慢变化的画面,比如桌上缓慢蒸发的水,或者正在生长的小草,就能制作出有趣的延时摄影作品。
需要提一下的有两点:

  • 一个是VideoWriter中的一个函数cv2.VideoWriter_fourcc()。这个函数指定了视频编码的格式,比如例子中用的是MP42,也就是MPEG-4.
  • 还有一个是KeyboardInterrupt,这是一个常用的异常,用来获取用户Ctrl+C的中止,捕获这个异常后直接结束循环并释放VideoCapture和VideoWriter的资源,使已经捕获好的部分视频可以顺利生成。

8. 对制定视频截取帧(截屏)

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

import cv2
import os
import 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]

# 建立一个新的文件夹,名称为原文件夹名称后加上_frames
frame_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()

9. 数据增强小工具

随机裁剪、随机旋转、随机颜色和明暗。
做数据增加时如果样本量本身就不小,则处理起来可能会很耗费时间,所以可以考虑利用多进程并行处理。比如我们的例子中,设定使用场景是输入一个文件夹路径,该文件夹下包含了所有原始的数据样本。用户指定输出的文件夹和打算增加图片的总量。执行程序的时候,通过os.listdir()获取所有文件的路径,然后按照上一章讲过的多进程平均划分样本的办法,把文件尽可能均匀地分给不同进程,进行处理。

import numpy as np
import cv2

'''
定义裁剪函数,四个参数分别是:
左上角横坐标x0
左上角纵坐标y0
裁剪宽度w
裁剪高度h
'''
crop_image = lambda img, x0, y0, w, h: img[y0:y0+h, x0:x0+w]

'''
随机裁剪
area_ratio为裁剪画面占原画面的比例
hw_vari是扰动占原高宽比的比例范围
'''
def random_crop(img, area_ratio, hw_vari):
    h, w = img.shape[:2]
    hw_delta = np.random.uniform(-hw_vari, hw_vari)
    hw_mult = 1 + hw_delta
    
    # 下标进行裁剪,宽高必须是正整数
    w_crop = int(round(w*np.sqrt(area_ratio*hw_mult)))
    
    # 裁剪宽度不可超过原图可裁剪宽度
    if w_crop > w:
        w_crop = w
        
    h_crop = int(round(h*np.sqrt(area_ratio/hw_mult)))
    if h_crop > h:
        h_crop = h
    
    # 随机生成左上角的位置
    x0 = np.random.randint(0, w-w_crop+1)
    y0 = np.random.randint(0, h-h_crop+1)
    
    return crop_image(img, x0, y0, w_crop, h_crop)

'''
定义旋转函数:
angle是逆时针旋转的角度
crop是个布尔值,表明是否要裁剪去除黑边
'''
def rotate_image(img, angle, crop):
    h, w = img.shape[:2]
    
    # 旋转角度的周期是360°
    angle %= 360
    
    # 用OpenCV内置函数计算仿射矩阵
    M_rotate = cv2.getRotationMatrix2D((w/2, h/2), angle, 1)
    
    # 得到旋转后的图像
    img_rotated = cv2.warpAffine(img, M_rotate, (w, h))

    # 如果需要裁剪去除黑边
    if crop:
        # 对于裁剪角度的等效周期是180°
        angle_crop = angle % 180
        
        # 并且关于90°对称
        if angle_crop > 90:
            angle_crop = 180 - angle_crop
            
        # 转化角度为弧度
        theta = angle_crop * np.pi / 180.0
        
        # 计算高宽比
        hw_ratio = float(h) / float(w)
        
        # 计算裁剪边长系数的分子项
        tan_theta = np.tan(theta)
        numerator = np.cos(theta) + np.sin(theta) * tan_theta
        
        # 计算分母项中和宽高比相关的项
        r = hw_ratio if h > w else 1 / hw_ratio
        
        # 计算分母项
        denominator = r * tan_theta + 1
        
        # 计算最终的边长系数
        crop_mult = numerator / denominator
        
        # 得到裁剪区域
        w_crop = int(round(crop_mult*w))
        h_crop = int(round(crop_mult*h))
        x0 = int((w-w_crop)/2)
        y0 = int((h-h_crop)/2)

        img_rotated = crop_image(img_rotated, x0, y0, w_crop, h_crop)

    return img_rotated

'''
随机旋转
angle_vari是旋转角度的范围[-angle_vari, angle_vari)
p_crop是要进行去黑边裁剪的比例
'''
def random_rotate(img, angle_vari, p_crop):
    angle = np.random.uniform(-angle_vari, angle_vari)
    crop = False if np.random.random() > p_crop else True
    return rotate_image(img, angle, crop)

'''
定义hsv变换函数:
hue_delta是色调变化比例
sat_delta是饱和度变化比例
val_delta是明度变化比例
'''
def hsv_transform(img, hue_delta, sat_mult, val_mult):
    img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float)
    img_hsv[:, :, 0] = (img_hsv[:, :, 0] + hue_delta) % 180
    img_hsv[:, :, 1] *= sat_mult
    img_hsv[:, :, 2] *= val_mult
    img_hsv[img_hsv > 255] = 255
    return cv2.cvtColor(np.round(img_hsv).astype(np.uint8), cv2.COLOR_HSV2BGR)

'''
随机hsv变换
hue_vari是色调变化比例的范围
sat_vari是饱和度变化比例的范围
val_vari是明度变化比例的范围
'''
def random_hsv_transform(img, hue_vari, sat_vari, val_vari):
    hue_delta = np.random.randint(-hue_vari, hue_vari)
    sat_mult = 1 + np.random.uniform(-sat_vari, sat_vari)
    val_mult = 1 + np.random.uniform(-val_vari, val_vari)
    return hsv_transform(img, hue_delta, sat_mult, val_mult)

'''
定义gamma变换函数:
gamma就是Gamma
'''
def gamma_transform(img, 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)
    return cv2.LUT(img, gamma_table)

'''
随机gamma变换
gamma_vari是Gamma变化的范围[1/gamma_vari, gamma_vari)
'''
def random_gamma_transform(img, gamma_vari):
    log_gamma_vari = np.log(gamma_vari)
    alpha = np.random.uniform(-log_gamma_vari, log_gamma_vari)
    gamma = np.exp(alpha)
    return gamma_transform(img, gamma)

调用这些函数需要通过一个主程序。这个主程序里首先定义三个子模块,

  • 定义一个函数parse_arg()通过Python的argparse模块定义了各种输入参数和默认值。需要注意的是这里用argparse来输入所有参数是因为参数总量并不是特别多,如果增加了更多的扰动方法,更合适的参数输入方式可能是通过一个配置文件
  • 然后定义一个生成待处理图像列表的函数generate_image_list(),根据输入中要增加图片的数量和并行进程的数目尽可能均匀地为每个进程生成了需要处理的任务列表。
  • 执行随机扰动的代码定义在augment_images()中,这个函数是每个进程内进行实际处理的函数,执行顺序是镜像 --> 裁剪 --> 旋转 --> HSV --> Gamma。需要注意的是镜像 --> 裁剪,因为只是个演示例子,这未必是一个合适的顺序。最后定义一个main函数进行调用,代码如下:
import os
import argparse
import random
import math
from multiprocessing import Process
from multiprocessing import cpu_count

import cv2

# 导入image_augmentation.py为一个可调用模块
import image_augmentation as ia

# 利用Python的argparse模块读取输入输出和各种扰动参数
def parse_args():
    parser = argparse.ArgumentParser(
        description='A Simple Image Data Augmentation Tool',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument('input_dir',
                        help='Directory containing images')
    parser.add_argument('output_dir',
                        help='Directory for augmented images')
    parser.add_argument('num',
                        help='Number of images to be augmented',
                        type=int)

    parser.add_argument('--num_procs',
                        help='Number of processes for paralleled augmentation',
                        type=int, default=cpu_count())

    parser.add_argument('--p_mirror',
                        help='Ratio to mirror an image',
                        type=float, default=0.5)

    parser.add_argument('--p_crop',
                        help='Ratio to randomly crop an image',
                        type=float, default=1.0)
    parser.add_argument('--crop_size',
                        help='The ratio of cropped image size to original image size, in area',
                        type=float, default=0.8)
    parser.add_argument('--crop_hw_vari',
                        help='Variation of h/w ratio',
                        type=float, default=0.1)

    parser.add_argument('--p_rotate',
                        help='Ratio to randomly rotate an image',
                        type=float, default=1.0)
    parser.add_argument('--p_rotate_crop',
                        help='Ratio to crop out the empty part in a rotated image',
                        type=float, default=1.0)
    parser.add_argument('--rotate_angle_vari',
                        help='Variation range of rotate angle',
                        type=float, default=10.0)

    parser.add_argument('--p_hsv',
                        help='Ratio to randomly change gamma of an image',
                        type=float, default=1.0)
    parser.add_argument('--hue_vari',
                        help='Variation of hue',
                        type=int, default=10)
    parser.add_argument('--sat_vari',
                        help='Variation of saturation',
                        type=float, default=0.1)
    parser.add_argument('--val_vari',
                        help='Variation of value',
                        type=float, default=0.1)

    parser.add_argument('--p_gamma',
                        help='Ratio to randomly change gamma of an image',
                        type=float, default=1.0)
    parser.add_argument('--gamma_vari',
                        help='Variation of gamma',
                        type=float, default=2.0)

    args = parser.parse_args()
    args.input_dir = args.input_dir.rstrip('/')
    args.output_dir = args.output_dir.rstrip('/')

    return args

'''
根据进程数和要增加的目标图片数,
生成每个进程要处理的文件列表和每个文件要增加的数目
'''
def generate_image_list(args):
    # 获取所有文件名和文件总数
    filenames = os.listdir(args.input_dir)
    num_imgs = len(filenames)

    # 计算平均处理的数目并向下取整
    num_ave_aug = int(math.floor(args.num/num_imgs))
    
    # 剩下的部分不足平均分配到每一个文件,所以做成一个随机幸运列表
    # 对于幸运的文件就多增加一个,凑够指定的数目
    rem = args.num - num_ave_aug*num_imgs
    lucky_seq = [True]*rem + [False]*(num_imgs-rem)
    random.shuffle(lucky_seq)

    # 根据平均分配和幸运表策略,
    # 生成每个文件的全路径和对应要增加的数目并放到一个list里
    img_list = [
        (os.sep.join([args.input_dir, filename]), num_ave_aug+1 if lucky else num_ave_aug)
        for filename, lucky in zip(filenames, lucky_seq)
    ]
    
    # 文件可能大小不一,处理时间也不一样,
    # 所以随机打乱,尽可能保证处理时间均匀
    random.shuffle(img_list)

    # 生成每个进程的文件列表,
    # 尽可能均匀地划分每个进程要处理的数目
    length = float(num_imgs) / float(args.num_procs)
    indices = [int(round(i * length)) for i in range(args.num_procs + 1)]
    return [img_list[indices[i]:indices[i + 1]] for i in range(args.num_procs)]

# 每个进程内调用图像处理函数进行扰动的函数
def augment_images(filelist, args):
    # 遍历所有列表内的文件
    for filepath, n in filelist:
        img = cv2.imread(filepath)
        filename = filepath.split(os.sep)[-1]
        dot_pos = filename.rfind('.')
        
        # 获取文件名和后缀名
        imgname = filename[:dot_pos]
        ext = filename[dot_pos:]

        print('Augmenting {} ...'.format(filename))
        for i in range(n):
            img_varied = img.copy()
            
            # 扰动后文件名的前缀
            varied_imgname = '{}_{:0>3d}_'.format(imgname, i)
            
            # 按照比例随机对图像进行镜像
            if random.random() < args.p_mirror:
                # 利用numpy.fliplr(img_varied)也能实现
                img_varied = cv2.flip(img_varied, 1)
                varied_imgname += 'm'
            
            # 按照比例随机对图像进行裁剪
            if random.random() < args.p_crop:
                img_varied = ia.random_crop(
                    img_varied,
                    args.crop_size,
                    args.crop_hw_vari)
                varied_imgname += 'c'
            
            # 按照比例随机对图像进行旋转
            if random.random() < args.p_rotate:
                img_varied = ia.random_rotate(
                    img_varied,
                    args.rotate_angle_vari,
                    args.p_rotate_crop)
                varied_imgname += 'r'
            
            # 按照比例随机对图像进行HSV扰动
            if random.random() < args.p_hsv:
                img_varied = ia.random_hsv_transform(
                    img_varied,
                    args.hue_vari,
                    args.sat_vari,
                    args.val_vari)
                varied_imgname += 'h'
            
            # 按照比例随机对图像进行Gamma扰动
            if random.random() < args.p_gamma:
                img_varied = ia.random_gamma_transform(
                    img_varied,
                    args.gamma_vari)
                varied_imgname += 'g'
            
            # 生成扰动后的文件名并保存在指定的路径
            output_filepath = os.sep.join([
                args.output_dir,
                '{}{}'.format(varied_imgname, ext)])
            cv2.imwrite(output_filepath, img_varied)

# 主函数
def main():
    # 获取输入输出和变换选项
    args = parse_args()
    params_str = str(args)[10:-1]

    # 如果输出文件夹不存在,则建立文件夹
    if not os.path.exists(args.output_dir):
        os.mkdir(args.output_dir)

    print('Starting image data augmentation for {}\n'
          'with\n{}\n'.format(args.input_dir, params_str))

    # 生成每个进程要处理的列表
    sublists = generate_image_list(args)
    
    # 创建进程
    processes = [Process(target=augment_images, args=(x, args, )) for x in sublists]

    # 并行多进程处理
    for p in processes:
        p.start()

    for p in processes:
        p.join()

    print('\nDone!')

if __name__ == '__main__':
    main()

还有默认进程数用的是cpu_count()函数,这个获取的是cpu的核数。把这段代码保存为run_augmentation.py,然后在命令行输入:

python run_augmentation.py -h

或者

python run_augmentation.py --help

就能看到脚本的使用方法,每个参数的含义,还有默认值。接下里来执行一个图片增加任务:

python run_augmentation.py imagenet_samples more_samples 1000 --rotate_angle_vari 180 --p_rotate_crop 0.5

其中imagenet_samples为一些从imagenet图片url中随机下载的一些图片,–rotate_angle_vari设为180方便测试全方向的旋转,–p_rotate_crop设置为0.5,让旋转裁剪对一半图片生效。扰动增加后的1000张图片在more_samples文件夹下.

10. 用OpenCV实现数据标注小工具

除了对图像的处理,OpenCV的图形用户界面(Graphical User Interface, GUI)和绘图等相关功能也是很有用的功能,无论是可视化,图像调试还是我们这节要实现的标注任务,都可以有所帮助。这节先介绍OpenCV窗口的最基本使用和交互,然后基于这些基础和之前的知识实现一个用于物体检测任务标注的小工具。

OpenCV窗口循环

OpenCV显示一幅图片的函数是cv2.imshow(),第一个参数是显示图片的窗口名称,第二个参数是图片的array。不过如果直接执行这个函数的话,什么都不会发生,因为这个函数得配合cv2.waitKey()一起使用。cv2.waitKey()指定当前的窗口显示要持续的毫秒数,比如cv2.waitKey(1000)就是显示一秒,然后窗口就关闭了。比较特殊的是cv2.waitKey(0),并不是显示0毫秒的意思,而是一直显示,直到有键盘上的按键被按下,或者鼠标点击了窗口的小叉子才关闭。cv2.waitKey()的默认参数就是0,所以对于图像展示的场景,cv2.waitKey()或者cv2.waitKey(0)是最常用的:

import cv2

img = cv2.imread('Aitutaki.png')
cv2.imshow('Honeymoon Island', img)
cv2.waitKey()

cv2.waitKey()参数不为零的时候则可以和循环结合产生动态画面,比如在7中 的延时小例子中,我们把延时摄影保存下来的所有图像放到一个叫做frames的文件夹下。

下面代码从frames的文件夹下读取所有图片并以24的帧率在窗口中显示成动画:

import os
from itertools import cycle
import cv2

# 列出frames文件夹下的所有图片
filenames = os.listdir('frames')

# 通过itertools.cycle生成一个无限循环的迭代器,每次迭代都输出下一张图像对象
img_iter = cycle([cv2.imread(os.sep.join(['frames', x])) for x in filenames])

key = 0
while key & 0xFF != 27:
    cv2.imshow('Animation', next(img_iter))
    key = cv2.waitKey(42)

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

鼠标和键盘事件

因为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

  • 删除(Delete):65535

需要注意的是在不同的操作系统里这些值可能是不一样的。

鼠标事件比起键盘事件稍微复杂一点点,需要定义一个回调函数,然后把回调函数和一个指定名称的窗口绑定,这样只要鼠标位于画面区域内的事件就都能捕捉到

把下面这段代码插入到上段代码的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)

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

基于上面两小节的基本使用,就能和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 os
import cv2

# tkinter是Python内置的简单GUI库,实现一些比如打开文件夹,确认删除等操作十分方便
from tkFileDialog import askdirectory
from 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 = 65362
KEY_DOWN = 65364
KEY_LEFT = 65361
KEY_RIGHT = 65363
KEY_ESC = 27
KEY_DELETE = 65535

# 空键用于默认循环
KEY_EMPTY = 0

get_bbox_name = '{}.bbox'.format

# 定义物体框标注工具类
class SimpleBBoxLabeling:

    def __init__(self, data_dir, fps=FPS, window_name=None):
        self._data_dir = data_dir
        self.fps = fps
        self.window_name = window_name if window_name else WINDOW_NAME

        #pt0是正在画的左上角坐标,pt1是鼠标所在坐标
        self._pt0 = None
        self._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_labeled
        self._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标记为True
        if event == cv2.EVENT_LBUTTONDOWN:
            self._drawing = True
            self._pt0 = (x, y)

        # 左键抬起,表明当前框画完了,坐标记为右下角,并保存,同时改变drawing标记为False
        elif event == cv2.EVENT_LBUTTONUP:
            self._drawing = False
            self._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 = None
        self._pt1 = None
        self._drawing = False
        self._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, value=COLOR_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, h+12),
                    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_GRAY
            cv2.rectangle(canvas, bpt0, bpt1, label_color, thickness=2)
            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_GRAY
            if self._pt1[0] >= self._pt0[0] and self._pt1[1] >= self._pt0[1]:
                cv2.rectangle(canvas, self._pt0, self._pt1, label_color, thickness=2)
            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()导出标注框数据到文件
    @staticmethod
    def export_bbox(filepath, bboxes):
        if bboxes:
            with open(filepath, 'w') as f:
                for bbox in bboxes:
                    line = repr(bbox) + '\n'
                    f.write(line)
        elif os.path.exists(filepath):
            os.remove(filepath)

    # 利用eval()读取标注框字符串到数据
    @staticmethod
    def 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()读取物体及对应颜色信息到数据
    @staticmethod
    def load_labels(filepath):
        label_colors = {}
        with open(filepath, 'r') as f:
            line = f.readline().rstrip()
            while line:
                label, color = eval(line)
                label_colors[label] = color
                line = f.readline().rstrip()
        return label_colors

    # 读取图像文件和对应标注框信息(如果有的话)
    @staticmethod
    def 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:
                    pass
                else:
                    label_index -= 1

            elif key == KEY_DOWN:
                if label_index == n_labels - 1:
                    pass
                else:
                    label_index += 1

            # 左右键切换当前标注的图片
            elif key == KEY_LEFT:
                # 已经到了第一张图片的话就不需要清空上一张
                if self._index > 0:
                    self._export_n_clean_bbox()

                self._index -= 1
                if self._index < 0:
                    self._index = 0

            elif key == KEY_RIGHT:
                # 已经到了最后一张图片的话就不需要清空上一张
                if self._index < len(self._filelist) - 1:
                    self._export_n_clean_bbox()

                self._index += 1
                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_EMPTY
                    continue

            # 如果键盘操作执行了换图片,则重新读取,更新图片
            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 = filename

        print('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(title='Where are the images?')
    labeling_task = SimpleBBoxLabeling(dir_with_images)
    labeling_task.start()

需要注意的是几个比较通用且独立的方法前加上了一句@staticmethod,表明是个静态方法。

执行这个程序,并选择samples文件夹,标注时的画面如下图:
在这里插入图片描述

  • 13
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值