OpenCV的图形用户界面(Graphical User Interface, GUI)和绘图等相关功能也是很有用的功能,无论是可视化,图像调试还是我们这节要实现的标注任务,都可以有所帮助。
窗口循环
OpenCV显示一幅图片的函数是cv2.imshow(),第一个参数是显示图片的窗口名称,第二个参数是图片的array。不过如果直接执行这个函数的话,什么都不会发生,因为这个函数得配合cv2.waitKey()一起使用。cv2.waitKey()指定当前的窗口显示要持续的毫秒数,比如cv2.waitKey(1000)就是显示一秒,然后窗口就关闭了。比较特殊的是cv2.waitKey(0),并不是显示0毫秒的意思,而是一直显示,直到有键盘上的按键被按下,或者鼠标点击了窗口的小叉子才关闭。cv2.waitKey()的默认参数就是0,所以对于图像展示的场景,cv2.waitKey()或者cv2.waitKey(0)是最常用的。
cv2.waitKey()参数不为零的时候则可以和循环结合产生动态画面。Python的itertools模块中的cycle函数可以把一个可遍历结构编程一个无限循环的迭代器。cv2.waitKey()返回的就是键盘上出发的按键。对于字母就是ascii码,特殊按键比如上下左右等,则对应特殊的值,其实这就是键盘事件的最基本用法。
鼠标和键盘事件
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)
通过这个程序我们能获取一些常用特殊按键的值。
需要注意的是在不同的操作系统里这些值可能是不一样的。鼠标事件比起键盘事件稍微复杂一点点,需要定义一个回调函数,然后把回调函数和一个指定名称的窗口绑定,这样只要鼠标位于画面区域内的事件就都能捕捉到。把下面这段代码插入到上段代码的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)
标注小工具
基本思路是对要标注的图像建立一个窗口循环,然后每次循环的时候对图像进行一次拷贝。鼠标在画面上画框的操作,以及已经画好的框的相关信息在全局变量中保存,并且在每个循环中根据这些信息,在拷贝的图像上再画一遍,然后显示这份拷贝的图像。
基于这种实现思路,使用上我们采用一个尽量简化的设计:
- 输入是一个文件夹,下面包含了所有要标注物体框的图片。如果图片中标注了物体,则生成一个相同名称加额外后缀名的文件保存标注信息。
- 标注的方式是按下鼠标左键选择物体框的左上角,松开鼠标左键选择物体框的右下角,鼠标右键删除上一个标注好的物体框。所有待标注物体的类别,和标注框颜色由用户自定义,如果没有定义则默认只标注一种物体,定义该物体名称叫“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的文本文件。把上段代码的内容放进去,就定义了小山头的框为黄色,骏马的框为青色,以及红色的屌丝。