跳转至

项目实战-自制绘图板

概要

本期,阿凯从易到难, 实现了四个版本的绘图板程序。在这节, 阿凯提高了作业的难度, 让你做出功能丰富的绘图板出来。

keywords 绘图板 Painter 鼠标事件监听

1. 实战:绘图板v1-圆形点点

我们首先设定一个鼠标回调事件,

我们需要指定窗口名称windowName , 只在这个指定窗口下触发事件.

然后我们需要指定一下,当Mouse事件触发时, 对应的回调函数onMouse

# 设置鼠标事件回调
cv2.setMouseCallback(windowName,onMouse)  

使用举例

# 设置鼠标事件回调
cv2.setMouseCallback('image',draw_circle)  

当每次鼠标事件产生的时候, 例如鼠标移动鼠标点击等, 就会触发draw_circle这个函数。

上文提及的三个onMouse 函数你都可以尝试一下哦。 尤其是第三个。

2.1 绘图板v1源码

diy-painter-v1.py

'''
鼠标 每次双击,触发回调函数, 在点击处绘制一个圆圈

'''
import cv2  
import numpy as np  

# 鼠标回调函数  
# x, y 都是相对于窗口内的图像的位置

def draw_circle(event,x,y,flags,param): 
    # 判断事件是否为 Left Button Double Clicck 
    if event == cv2.EVENT_LBUTTONDBLCLK:  
        cv2.circle(img,(x,y),20,(255,0,0),-1)  

# 创建一个黑色图像,并绑定窗口和鼠标回调函数  
img = np.zeros((512,512,3), np.uint8)  
cv2.namedWindow('image')
# 设置鼠标事件回调
cv2.setMouseCallback('image',draw_circle)  

while(True):  
    cv2.imshow('image',img)  
    if cv2.waitKey(20) == ord('q'):  
        break  
cv2.destroyAllWindows()

# 保存图片
cv2.imwrite("MousePaint01.png",  img)

2.2 阿凯的灵魂画作01

20180126MousePaint01.png

3. 实战:绘图板v2-线条绘制

有了单个圆圈的绘制, 我们想一下, 如何才能绘制一条曲线呢?

如果我移动鼠标的时候,一直绘制圆圈, 鼠标慢慢移动,这个轨迹不就是一个有宽度的曲线嘛~~

关键点在于, 我们需要判断,鼠标是否按下, 以此来判定是否需要绘制图片。

3.1 绘图板v2源码

我们先来看一下,不用flags参数我们需要如何实现。

我们使用isMouseLBDown 这个布尔值,记录当前鼠标的状态。

diy-painter-v2.py

'''
 鼠标按下绘制线条

'''
import cv2  
import numpy as np  

# 鼠标回调函数  
# x, y 都是相对于窗口内的图像的位置
isMouseLBDown = False

def draw_circle(event,x,y,flags,param): 
    # 判断事件是否为 Left Button Double Clicck 
    print(event)
    global isMouseLBDown

    if event == cv2.EVENT_LBUTTONDOWN:
        # 检测到鼠标左键按下
        print("mouse down")
        isMouseLBDown = True
        cv2.circle(img,(x,y),5,(255,0,0),-1) 
    elif event == cv2.EVENT_LBUTTONUP:
        # 检测到鼠标左键抬起
        isMouseLBDown = False
        print("mouse up")
    elif event == cv2.EVENT_MOUSEMOVE:
        if isMouseLBDown:
            print("drawing")
            cv2.circle(img,(x,y),5,(255,0,0),-1)

# 创建一个黑色图像,并绑定窗口和鼠标回调函数  
img = np.zeros((512,512,3), np.uint8)  
cv2.namedWindow('image')
# 设置鼠标事件回调
cv2.setMouseCallback('image',draw_circle)  

while(True):  
    cv2.imshow('image',img)  
    if cv2.waitKey(1) == ord('q'):  
        break  
cv2.destroyAllWindows()

cv2.imwrite("MousePaint02.png",  img)

当然, 我们有了flags参数, EVENT_FLAG_LBUTTON 本身就可以判定鼠标左键是否按下, 所以改写一下:

diy-painter-v3.py

'''
 鼠标按下绘制线条

'''
import cv2  
import numpy as np  

# 鼠标回调函数  
# x, y 都是相对于窗口内的图像的位置

def draw_circle(event,x,y,flags,param): 

    if flags == cv2.EVENT_FLAG_LBUTTON:
        cv2.circle(img,(x,y),5,(255,0,0),-1)

# 创建一个黑色图像,并绑定窗口和鼠标回调函数  
img = np.zeros((512,512,3), np.uint8)  
cv2.namedWindow('image')
# 设置鼠标事件回调
cv2.setMouseCallback('image',draw_circle)  

while(True):  
    cv2.imshow('image',img)  
    if cv2.waitKey(1) == ord('q'):  
        break  
cv2.destroyAllWindows()

cv2.imwrite("MousePaint03.png",  img)

看, 是不是通过flags参数, 大大减少了你的代码复杂度。

我猜你的内心独白可能是这样的:“”哦,原来flags是这么用的。“

3.2 阿凯的灵魂画作02

20180126MousePaint02.png

3.3 存在问题

  1. 如果鼠标运动快了, 就会变成散点, 不连续。

  2. 只支持单个颜色, 不美丽。

  3. 不支持调节笔刷粗细。

4. 实战:绘图板v3-彩色线条笔触调节

4.1 绘图板v4源码

diy-painter-v4.py

'''
 鼠标按下绘制线条 可以调整线条粗细 变换颜色
 线条也更流畅
'''
import cv2  
import numpy as np  

isMouseLBDown = False # 判断鼠标是否摁下的标志
circleColor = (0, 0, 0) # 画笔的颜色
circleRadius = 5 # 画笔的粗细
lastPoint = (0, 0)

# 鼠标回调函数  绘制图像 
# x, y 都是相对于窗口内的图像的位置
def draw_circle(event,x,y,flags,param): 
    # 判断事件是否为 Left Button Double Clicck 
    # print(event)
    global img
    global isMouseLBDown
    global color
    global lastPoint

    if event == cv2.EVENT_LBUTTONDOWN:
        # 检测到鼠标左键按下
        # print("mouse down")
        isMouseLBDown = True
        cv2.circle(img,(x,y), int(circleRadius/2), circleColor,-1)
        lastPoint = (x, y)
    elif event == cv2.EVENT_LBUTTONUP:
        # 检测到鼠标左键抬起
        isMouseLBDown = False
        # print("mouse up")
    elif event == cv2.EVENT_MOUSEMOVE:
        if isMouseLBDown:
            # print("drawing")
            cv2.line(img, pt1=lastPoint, pt2=(x, y), color=circleColor, thickness=circleRadius)
            lastPoint = (x, y)
            # cv2.circle(img,(x,y), circleRadius, circleColor,-1)

# 更新颜色
def updateCircleColor(x):
    global circleColor
    global colorPreviewImg

    r = cv2.getTrackbarPos('Channel_Red','image')
    g = cv2.getTrackbarPos('Channel_Green','image')
    b = cv2.getTrackbarPos('Channel_Blue','image')

    circleColor = (b, g, r)
    colorPreviewImg[:] = circleColor

# 更新画笔的宽度
def updateCircleRadius(x):
    global circleRadius
    global radiusPreview

    circleRadius = cv2.getTrackbarPos('Circle_Radius', 'image')
    # 重置画布
    radiusPreview[:] = (255, 255, 255)
    # 绘制圆形
    cv2.circle(radiusPreview, center=(50, 50), radius=int(circleRadius / 2), color=(0, 0, 0), thickness=-1)

# 创建一个画布,并绑定窗口和鼠标回调函数

img = np.ones((512,512,3), np.uint8)
img[:] = (255, 255, 255)

# 用于预览画笔的颜色
colorPreviewImg = np.ones((100, 100, 3), np.uint8)
colorPreviewImg[:] = (0,  0, 0)
# 用于预览画笔的粗细
radiusPreview = np.ones((100, 100, 3), np.uint8)
radiusPreview[:] = (255, 255, 255)

# 用于展示绘图区域的窗口
cv2.namedWindow('image')
# 用于预览颜色的窗口
cv2.namedWindow('colorPreview')
# 用于预览画笔宽度的窗口
cv2.namedWindow('radiusPreview')

# 在window‘image’ 上创建一个滑动条,起名为Channel_XXX, 设定滑动范围为0-255, 
# onChange事件回调 啥也不做
cv2.createTrackbar('Channel_Red','image',0,255,updateCircleColor)
cv2.createTrackbar('Channel_Green','image',0,255,updateCircleColor)
cv2.createTrackbar('Channel_Blue','image',0,255,updateCircleColor)

cv2.createTrackbar('Circle_Radius','image',1,20,updateCircleRadius)
# 设置鼠标事件回调
cv2.setMouseCallback('image',draw_circle)  

while(True):
    cv2.imshow('colorPreview', colorPreviewImg)
    cv2.imshow('radiusPreview', radiusPreview)
    cv2.imshow('image',img)  
    if cv2.waitKey(1) == ord('q'):  
        break

cv2.destroyAllWindows()
cv2.imwrite("MousePaint04.png",  img)

4.2 阿凯的灵魂画作04

20180126MousePaint03.png

绘制了自己的灵魂画作之后, 记得发空间的时候@一下阿凯哦。

5. 作业CH2.4

5.1 作业1: 改写绘图板v1, 用滑稽脸替换圆形

20180126huaji.jpg

鼠标点击过的地方都替换成滑稽脸。

你可能需要去了解这么几个操作:

  • 缩放图片, 控制滑稽脸的尺寸

  • 图片的局部替换/赋值 (需要用到numpy的索引)

为了控制难度, 就矩形区域赋值吧,不抠滑稽脸。

如果还是有难度, 阿凯提供给你一个我缩小过的滑稽脸吧。代码也先提供吧, 开心比较重要

20180126smallhuaji.png

需要就直接下载吧, 注意修改一下后缀。

哈哈,坏坏的阿凯, 这次不会给你代码注释, 自己去官网看文档哦

Geometric Transformations of Images - 图像的几何变换

写个博客,做做笔记也是极好的。

import cv2
import numpy as np

def imgResize(img, ratio = 1):
    height, width = img.shape[:2]
    res = cv2.resize(img,(int(ratio*width), int(ratio*height)), interpolation = cv2.INTER_CUBIC)
    return res

huaji = cv2.imread("20180126huaji.jpg")
cv2.imwrite("smallhuaji.png",imgResize(huaji, 0.2))

5.2 作业2: 给绘图板添加绘制几何形状的功能

例如,你可以给画图板添加矩形绘制的函数,第一次点击的点为矩形的左上角的起点.

接下来鼠标拖动,动态绘制矩形,鼠标弹起,确定矩形,矩形保留在画布上.

你也可以添加绘制圆形的功能,初始点击的点为圆心,接下来鼠标向外拖动,半径放大,鼠标弹起完成圆形绘制.

5.3 作业3: 在图片的基础上涂鸦

这个作业没有解析,纯开放题目

如果图形化有困难,可以预先设定图片的路径。

通过对话框的形式,选择一张图片,将图片导入到画布,任意涂鸦,接下来保存为本地图片.

你可以结合其他的Python GUI组件实现此功能. 例如PyQT 例如Tkinter .

PyQt5 file dialog

OpenCV with Tkinter

你甚至可以再加上一个保存按钮, 可以选择保存路径。

哈哈, 还有, 还有, 撤销按钮,保存最近10步操作 (适合使用数据结构:栈)

5.4 作业4 实现SelectROI的功能

利用作业1同样的原理, 去选择一个矩形区域,并且将这个矩形区域的内容保存为另外一张图片, 并且打印矩形区域的tuple

(x, y, w, h)

矩形区域在原来图片的左上角坐标(x,y)

矩形区域的宽度w

矩形区域的高度h

6. Reference

How to Detect Mouse Clicks and Moves