实现一个简单的个人图片检测标注工具
项目背景
最近有一个简单的图片检测任务,需要对目标图片进行简单的多点标注。网上一些开源的项目,例如labelme什么的由于各式各样的原因,都没能配置成功。加上由于项目本省不是很复杂,故萌发了自己基于python+opencv搭建一个简单标注工具的想法。最终效果如下:
前期准备
- python 3.0 up
- opencv 4.0 (低版本的opencv也可以适用,具体参见相关opencv文档)
任务整理
基本任务:
- 完成对目标图像的点标注
- 记录标注点的坐标
基本流程:
- 使用opencv读取图片,并且显示
- 在显示的窗口上通过鼠标点击方式标注关键点,并且记录
- 标注完成后读取下一张图片,重复标注过程
- 所有标注完成后将图片关键点记录输出为文件
补充功能
- [-] 通过右键点击方式取消标注点,并且在图片上显示(未完全实现)
实现细节:
1. 实现通过鼠标点击控制opencv窗口
opencv 里面有特定函数 cv2.setMouseCallback() 来完成鼠标控制。具体使用方式可以参考其官方文档,不再细数(我也没看细节)。重点关注一下其实现方式。
具体应用时,首先需要定义一个响应函数,名字可以自取,如 on_EVENT_MOUSE, 其输入参数是固定的:event,x,y,flags,param。其中event是鼠标的操作方式,x,y是鼠标当前位置,flags在本应用中没有使用到,就不细述了,param可以用来传递参数。本函数没有返回值。
而在主函数中,通过上文说到 cv2.setMouseCallback() 来调用这个相应函数进行对鼠标点击的相应,其具体代码如下:
def on_EVENT_MOUSE(event,x,y,flags,param):
外部需要读取的内容 = param
if event == cv2.EVENT_LBUTTONDOWN:
点击鼠标左键后做什么
if event == cv2.EVENT_LBUTTONDOWN:
点击鼠标右键后做什么
...
def main(**kargs):
...
cv2.setMouseCallback("image",on_EVENT_MOUSE,(*待传入的内容))
# "image" 是当前窗口的名称,on_EVENT_MOUSE 是响应函数的名称,后面跟待传入的内容
...
2. 具体实现点击内容
-
左键单击后,需要完成的工作有两个:
- 记录现有位置
- 在窗口上有所表示
我通过全局变量完成第一个目标。即最开始定义了一个全局变量points,并且在响应函数和main函数中开头定义 global points,即可在函数中操作此变量,具体实现如下:
points = []
def on_EVENT_MOUSE(event,x,y,flags,param):
global points
if event == cv2.EVENT_LBUTTONDOWN:
points.append([x,y]) # 将鼠标当前位置存到points里面
...
def main(**kargs):
global points
use_points(points)
第二个目标实现比较简单,可以通过调用cv2的画图函数完成。注意一点,需要重新显示一下图片,并且使用同样的窗口名。
- 右键单击后,希望实现功能:
- 删除标注点
- 窗口上删除标注
第一点是先比较容易,通过 pop方式即可将points最后标注的点删除。第二点暂时没有发现好的实现方式。本文通过将原本标记为黑色的部分标记为白色实现,胜在相应迅速,但效果不是很好。之后可以尝试其他开发方法。
3. 一些其他注意事项
-
窗口大小调整:
使用默认cv2.imshow的话,图片会按照实际尺寸打开。一般而言,手机照相机出来的图片都会比较大。不进行修改的话,屏幕是很难放下的。所以建议对窗口尺寸进行设置
-
存储形式
现在为了方便起见,且由于个人使用原因,固定每张图输入八点坐标,且在整体txt上面按照 x1,y1,x2,y2 … x8,y8的顺序存储。后续待优化此处。
完整代码:
git地址, 顺手点个star也是极好的
import cv2
import numpy as np
import os
points = []
def on_EVENT_MOUSE(event, x, y, flags, param):
global points
img = param
if event == cv2.EVENT_LBUTTONDOWN:
xy = "%d,%d" % (x, y)
points.append([x,y])
cv2.circle(img, (x, y), 10, (255, 0, 0), thickness = -1)
cv2.putText(img, xy, (x, y), cv2.FONT_HERSHEY_PLAIN,
10.0, (0,0,0), thickness = 10)
cv2.imshow("image", img)
if event == cv2.EVENT_RBUTTONDOWN:
x1,y1 = points.pop()
xy = "%d,%d" % (x1, y1)
cv2.circle(img, (x1, y1), 10, (255, 255, 255), thickness = -1)
cv2.putText(img, xy, (x1, y1), cv2.FONT_HERSHEY_PLAIN,
10.0, (255,255,255), thickness = 10)
cv2.imshow("image", img)
def main(path):
global points
for name in os.listdir(images_path):
image_name = os.path.join(images_path, name)
image = cv2.imread(image_name)
h,w,_ = image.shape
cv2.namedWindow("image",cv2.WINDOW_NORMAL)
if h>w:
cv2.resizeWindow('image',720,960)
else:
cv2.resizeWindow("image",960,720)
cv2.setMouseCallback("image",on_EVENT_MOUSE,image)
while(1):
cv2.imshow("image", image)
if cv2.waitKey(0)&0xFF == 27:
break
cv2.destroyAllWindows()
if len(points) != 8:
break
print(points)
with open("label.txt",'a',encoding='utf8') as record:
str1 = '{}{}'.format(name[:-3],'\t')
for x,y in points:
str1 += '{} {} '.format(x,y)
str1 += '\n'
record.write(str1)
points = []
if __name__ == '__main__':
images_path = './images'
main(images_path)