【OpenCV中文手册学习-4】基于OpenCV-Python的鼠标绘图

文章目录

1. 前言

本文给出一个鼠标绘图的综合程序。程序由两部分组成:一是参数设置,二是画板显示。参数设置包括画笔颜色、画笔大小和绘制图形。绘制图像可以在直线、矩形、圆形、曲线中进行选择。画布默认是黑色的,初始画笔默认是白色的,绘制图形默认为直线。画笔大小默认为1px,最大画笔大小为5px。如果画笔大小设置为0,代表以填充方式绘制图形。
【OpenCV中文手册学习-4】基于OpenCV-Python的鼠标绘图

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 鼠标监听事件

添加鼠标监听事件步骤

  1. 定义鼠标事件回调函数(即发生鼠标事件后调用的函数)
  2. 将第一步定义的函数绑定一个窗口
# 鼠标回调函数
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 主循环

  1. 以全局变量的形式保存绘图参数
    • 与轨迹栏有关的绘图参数包括:画笔颜色、画笔大小、绘图模式
    • 与画布有关的绘图参数包括:画布(图像)、直线或矩形或圆的绘制起始点坐标、直线或矩形或圆上一次绘图的终点(防止线框模式重复绘图);
  2. 每1ms重新显示一次画布;
  3. 每1ms获取一次轨迹栏中的值对轨迹栏有关绘图参数进行更新;
  4. 如果用户按下 Esc 键将退出主循环;
  5. 画布中图形的绘制是在鼠标监听线程中完成的,主循环只负责画布的显示
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为半径的小圆,这些小圆连起来就是曲线。

按照上面的绘图逻辑,在以填充模式绘制图形时没有问题,但是若以线框方式绘制图形时,会在鼠标移动过程中画出很多重复图形,如下图所示。

【OpenCV中文手册学习-4】基于OpenCV-Python的鼠标绘图
为了解决上述问题,本程序在鼠标事件回调函数中记录了上一次绘制终点(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. 程序功能演示

【OpenCV中文手册学习-4】基于OpenCV-Python的鼠标绘图

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()
上一篇:改变element 的表格样式的滚动条


下一篇:利用iframe 监听页面宽度变化