文章目录
1. 前言
本文给出一个鼠标绘图的综合程序。程序由两部分组成:一是参数设置,二是画板显示。参数设置包括画笔颜色、画笔大小和绘制图形。绘制图像可以在直线、矩形、圆形、曲线中进行选择。画布默认是黑色的,初始画笔默认是白色的,绘制图形默认为直线。画笔大小默认为1px,最大画笔大小为5px。如果画笔大小设置为0,代表以填充方式绘制图形。
2. 程序分析
2.1 轨迹栏
2.1.1 函数原型
createTrackbar(trackbarName,windowName,value,count,onChange)
- trackbarName : 轨迹栏名称
- windowName : 轨迹栏所在窗口名称
- value : 轨迹栏初始值
- count : 轨迹栏取值个数,轨迹栏取值从0开始逐1递增
- onChange : 轨迹栏值发生改变时的回调函数
2.1.2 创建窗口
轨迹栏必须存在一个窗口中,所以要先创建一个窗口才能添加轨迹栏
cv.namedWindow('image')
2.1.3 添加轨迹栏
# 轨迹栏发生变化时的回调函数
def nothing(x):
pass
# 创建颜色变化的轨迹栏
cv.createTrackbar('R','image',255,255,nothing)
cv.createTrackbar('G','image',255,255,nothing)
cv.createTrackbar('B','image',255,255,nothing)
# 根据names生成选项
def get_choice_str(names):
values = np.arange(0,len(names))
choices = dict(zip(names,values))
choice_str = [f'{key}:{value}' for (key,value) in choices.items()]
choice_str = '\n'.join(choice_str)
return choice_str
# 创建绘制方式选择轨迹栏
shape_choice_str = get_choice_str(['line',"rectangle","circle","curve"])
cv.createTrackbar(shape_choice_str,'image',0,3,nothing)
# 创建填充方式选择轨迹栏
size_choice_str = "pen size(0 is solid)"
cv.createTrackbar(size_choice_str,'image',1,5,nothing)
2.2 画布
# 创建黑色画布
img = np.zeros((300,512,3), np.uint8)
创建画布后不用程序员手动添加到窗口中,当调用 imshow 函数时,画布会自动添加到窗口中。
2.3 鼠标监听事件
添加鼠标监听事件步骤
- 定义鼠标事件回调函数(即发生鼠标事件后调用的函数)
- 将第一步定义的函数绑定一个窗口
# 鼠标回调函数
def mouse_call(event,x,y,flags,param):
# 鼠标左键按下
if event == cv.EVENT_LBUTTONDOWN:
# do something1...
pass
# 鼠标移动
elif event == cv.EVENT_MOUSEMOVE:
# do something2...
pass
# 鼠标左键抬起
elif event == cv.EVENT_LBUTTONUP:
# do something3...
pass
# 监听鼠标事件
# image 为窗口名称
cv.setMouseCallback('image',mouse_call)
2.4 主循环
- 以全局变量的形式保存绘图参数
- 与轨迹栏有关的绘图参数包括:画笔颜色、画笔大小、绘图模式
- 与画布有关的绘图参数包括:画布(图像)、直线或矩形或圆的绘制起始点坐标、直线或矩形或圆上一次绘图的终点(防止线框模式重复绘图);
- 每1ms重新显示一次画布;
- 每1ms获取一次轨迹栏中的值对轨迹栏有关绘图参数进行更新;
- 如果用户按下 Esc 键将退出主循环;
- 画布中图形的绘制是在鼠标监听线程中完成的,主循环只负责画布的显示
while True:
cv.imshow('image',img)
k = cv.waitKey(1)
if k == 27:
break
r = cv.getTrackbarPos('R','image')
g = cv.getTrackbarPos('G','image')
b = cv.getTrackbarPos('B','image')
mode = cv.getTrackbarPos(shape_choice_str,'image')
pen_size = cv.getTrackbarPos(size_choice_str,'image')
cv.destroyAllWindows()
2.5 绘图逻辑
- 绘制直线:鼠标左键按下时记录直线起点
(ix,iy)
,在鼠标移动时获取当前鼠标坐标(x,y)
作为直线终点,然后利用line
函数绘制直线; - 绘制矩形:鼠标左键按下时记录矩形左上角坐标
(ix,iy)
,在鼠标移动时获取当前鼠标坐标(x,y)
作为矩形右下角坐标,然后利用rectangle
函数绘制矩形; - 绘制圆形:鼠标左键按下时记录矩形圆上一点坐标
(ix,iy)
,在鼠标移动时获取当前鼠标坐标(x,y)
作为圆心坐标,然后利用circle
函数绘制圆形; - 绘制曲线:在鼠标移动时获取当前鼠标坐标
(x,y)
作为圆心坐标,绘制以画笔大小pen_size
为半径的小圆,这些小圆连起来就是曲线。
按照上面的绘图逻辑,在以填充模式绘制图形时没有问题,但是若以线框方式绘制图形时,会在鼠标移动过程中画出很多重复图形,如下图所示。
为了解决上述问题,本程序在鼠标事件回调函数中记录了上一次绘制终点(px,py)
这个变量。对于非填充模式的绘制,在绘制前,先用背景色将上一次绘制图形进行一次覆盖。以直线为例给出下列代码。
bg_color = (0,0,0)
if pen_size >= 1:
cv.line(img,(ix,iy),(px,py),bg_color,pen_size)
cv.line(img,(ix,iy),(x,y),color,pen_size)
绘图逻辑完整代码
# 全局变量
drawing = False # 如果按下鼠标,则为真
mode = 3 # 默认为曲线模式
pen_size = 0 # 默认为实心
ix,iy = 0,0 # 直线或矩形的起始点坐标/圆的圆心
px,py = 0,0 # 直线、矩形或圆上一次绘图的终点(防止线框模式重复绘图)
img = None # 图像
b,g,r = 255,255,255 # 画笔初始颜色
# 绘制图像
def draw(mode,color,pen_size,ix,iy,x,y,px,py):
global img
bg_color = (0,0,0)
if mode == 0:
if pen_size >= 1:
cv.line(img,(ix,iy),(px,py),bg_color,pen_size)
cv.line(img,(ix,iy),(x,y),color,pen_size)
elif mode == 1:
if pen_size >= 1:
cv.rectangle(img,(ix,iy),(px,py),bg_color,pen_size)
cv.rectangle(img,(ix,iy),(x,y),color,pen_size)
elif mode == 2:
if pen_size >= 1:
radius = int(((px-ix)**2 + (py-iy)**2)**0.5)
cv.circle(img,(px,py),radius,bg_color,pen_size)
radius = int(((x-ix)**2 + (y-iy)**2)**0.5)
cv.circle(img,(x,y),radius,color,pen_size)
elif mode == 3:
cv.circle(img,(x,y),pen_size,color,pen_size)
# 鼠标回调函数
def mouse_call(event,x,y,flags,param):
global ix,iy,px,py,drawing,mode,pen_size,img,b,g,r
if event == cv.EVENT_LBUTTONDOWN:
drawing = True
ix,iy = x,y
elif event == cv.EVENT_MOUSEMOVE:
if drawing == True:
brush = pen_size
if pen_size <= 0:
brush = -1
draw(mode,(b,g,r),brush,ix,iy,x,y,px,py)
elif event == cv.EVENT_LBUTTONUP:
drawing = False
brush = pen_size
if pen_size <= 0:
brush = -1
draw(mode,(b,g,r),brush,ix,iy,x,y,px,py)
px,py = x,y
3. 程序功能演示
4. 程序完整代码
import cv2 as cv
import numpy as np
drawing = False # 如果按下鼠标,则为真
mode = 3 # 默认为曲线模式
pen_size = 0 # 默认为实心
ix,iy = 0,0 # 直线或矩形的起始点坐标/圆的圆心
px,py = 0,0 # 直线、矩形或圆上一次绘图的终点(防止线框模式重复绘图)
img = None # 图像
b,g,r = 255,255,255 # 画笔初始颜色
# 根据names生成选项
def get_choice_str(names):
values = np.arange(0,len(names))
choices = dict(zip(names,values))
choice_str = [f'{key}:{value}' for (key,value) in choices.items()]
choice_str = '\n'.join(choice_str)
return choice_str
# 轨迹栏发生变化时的回调函数
def nothing(x):
pass
# 绘制图像
def draw(mode,color,pen_size,ix,iy,x,y,px,py):
global img
bg_color = (0,0,0)
if mode == 0:
if pen_size >= 1:
cv.line(img,(ix,iy),(px,py),bg_color,pen_size)
cv.line(img,(ix,iy),(x,y),color,pen_size)
elif mode == 1:
if pen_size >= 1:
cv.rectangle(img,(ix,iy),(px,py),bg_color,pen_size)
cv.rectangle(img,(ix,iy),(x,y),color,pen_size)
elif mode == 2:
if pen_size >= 1:
radius = int(((px-ix)**2 + (py-iy)**2)**0.5)
cv.circle(img,(px,py),radius,bg_color,pen_size)
radius = int(((x-ix)**2 + (y-iy)**2)**0.5)
cv.circle(img,(x,y),radius,color,pen_size)
elif mode == 3:
cv.circle(img,(x,y),pen_size,color,pen_size)
# 鼠标回调函数
def mouse_call(event,x,y,flags,param):
global ix,iy,px,py,drawing,mode,pen_size,img,b,g,r
if event == cv.EVENT_LBUTTONDOWN:
drawing = True
ix,iy = x,y
elif event == cv.EVENT_MOUSEMOVE:
if drawing == True:
brush = pen_size
if pen_size <= 0:
brush = -1
draw(mode,(b,g,r),brush,ix,iy,x,y,px,py)
elif event == cv.EVENT_LBUTTONUP:
drawing = False
brush = pen_size
if pen_size <= 0:
brush = -1
draw(mode,(b,g,r),brush,ix,iy,x,y,px,py)
px,py = x,y
# 绘图程序
def test():
global mode,pen_size,img,b,g,r
# 创建一个窗口
cv.namedWindow('image')
# 创建颜色变化的轨迹栏
cv.createTrackbar('R','image',255,255,nothing)
cv.createTrackbar('G','image',255,255,nothing)
cv.createTrackbar('B','image',255,255,nothing)
# 创建绘制方式选择轨迹栏
shape_choice_str = get_choice_str(['line',"rectangle","circle","curve"])
cv.createTrackbar(shape_choice_str,'image',0,3,nothing)
# 创建填充方式选择轨迹栏
size_choice_str = "pen size(0 is solid)"
cv.createTrackbar(size_choice_str,'image',1,5,nothing)
# 创建黑色画布
img = np.zeros((300,512,3), np.uint8)
# 监听鼠标事件
cv.setMouseCallback('image',mouse_call)
while True:
cv.imshow('image',img)
k = cv.waitKey(1)
if k == 27:
break
r = cv.getTrackbarPos('R','image')
g = cv.getTrackbarPos('G','image')
b = cv.getTrackbarPos('B','image')
mode = cv.getTrackbarPos(shape_choice_str,'image')
pen_size = cv.getTrackbarPos(size_choice_str,'image')
cv.destroyAllWindows()
if __name__ == "__main__":
test()