大家好,我是石璞东。
在我书的第六章中有一个关于MNIST手写数字的例子,当数据集加载完成之后,用户可以在<canvas/>
上输入手写数字,点击「预测」按钮之后,浏览器会弹出经模型预测之后的结果;在我书的第九章和第十章中,分别有关于目标检测和人体姿态检测的案例,当关键点的得分符合一定要求时,会通过<canvas/>
将关键部分绘制出来,请看效果:
图1 - MNIST手写数字案例
<br/>
图2 - 人体姿态检测案例
<br/>
图3 - 目标检测案例
<br/>
接下来,我们将在本文中详细讲解一下上述三个模块均用到的一个技术<canvas/>
。
参考资料
- MDN Web Docs:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API
- 《JavaScript高级程序设计(第三版)》
- 《HTML5权威指南》
1.canvas概述
HTML5最受欢迎的功能就是<canvas>
元素,Canvas API
提供了一个通过JavaScript
和HTML
的<canvas>
元素来绘制图形的方式,可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面,该API
主要聚焦于2D
图形,我们也可以使用WebGL API
绘制硬件加速的2D
和3D
图形。
2.基本用法
使用<canvas>
大概可以分为两个步骤:
- 在
HTML
中定义<canvas>
标签; - 获取
<canvas>
对象的上下文并判断getContext()
方法是否存在;
了解了使用<canvas>
的大概步骤之后,我们首先在页面中定义一个<canvas>
标签,请看代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas示例</title>
</head>
<body>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
</body>
</html>
该标签有两个属性——width
和height
,当没有设置width
和height
的值时,<canvas>
标签会初始化大小为300*150px
的矩形,我们也可以通过CSS
去设置<canvas>
元素的大小,但在绘制时图像会伸缩以适应它的尺寸框架,可能会出现扭曲等情况,所以推荐大家使用width
和height
属性明确的规定宽度和高度,接下来,请看效果展示:
图4 - 画布展示
<br/>
在页面中定义了<canvas>
标签之后,我们需要获取该标签的绘图上下文,请看示例代码:
var canvas = document.getElementById("canvas")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
ctx.fillStyle = "orange"
ctx.fillRect(10,10,100,100)
}
上述代码中,我们首先通过 document.getElementById()
方法获取页面中的<canvas>
标签,并通过getContext()
方法获取渲染上下文和它的绘画功能,接着我们通过ctx.fillStyle()
和ctx.fillRect()
方法在<canvas>
中绘制了一个左上角(x
,y
)坐标为(10,10)
且大小为100*100px
的绿色正方形,请看效果展示:
图5 - 绘制矩形
<br/>
3.2D上下文
使用2D
绘图上下文提供的方法可以绘制简单的2D
图形,比如矩形、弧线和路径。2D
上下文的坐标开始于<canvas>
元素的左上角,原点坐标是(0,0)
,所有坐标值都是基于这个原点计算,x
值越大表示越靠右,y
值越大表示越靠下。
图6 - canvas绘制规则
<br/>
3.1 填充和描边
2D上下文的两种基本绘图操作是填充和描边。填充就是用指定的样式(颜色、渐变或图像)填充图形;描边就是只在图形的边缘画线。大多数2D
上下文操作都会细分为填充和描边两个操作,其操作结果取决于fillStyle
属性和strokeStyle
属性,其默认值均为#000000
,请看代码示例:
var canvas = document.getElementById("canvas")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
ctx.fillRect(10,10,100,100)
ctx.strokeRect(120,10,100,100)
}
上述代码中,我们通过ctx.fillRect()
方法和ctx.strokeRect()
方法分别绘制了两个矩形,其中通过ctx.fillRect()
方法绘制的为填充矩形;通过ctx.strokeRect()
方法绘制的为描边矩形,其样式均采用默认样式,请看演示效果:
图7 - 案例展示
<br/>
3.2 绘制矩形
canvas只支持两种形式的图形绘制:矩形和路径,矩形是唯一一种可以直接在2D
上下文中绘制的形状,它提供了三种方法绘制矩形,请看详细介绍:
1.fillRext(x,y,width,height)
该方法用于绘制一个填充的矩形,可以通过fillStyle
属性决定当前矩形的填充样式;
2.strokeRect(x,y,width,height)
该方法用于绘制一个矩形的边框;
3.clearRect(x,y,width,height)
该方法用于清除指定矩形区域,让清除部分完全透明;
上述提供的三个方法中均包含相同的参数,x
与y
指定了在<canvas>
画布上所绘制矩形左上角(相对于原点)的坐标,width
和height
设置矩形的尺寸,请看演示案例:
var canvas = document.getElementById("canvas")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
ctx.fillRect(100,100,300,300)
ctx.clearRect(150,150,200,200)
ctx.strokeRect(200,200,100,100)
}
上述代码中,我们在大小为500*500px
的画布中绘制了三个矩形,其中第一个矩形左上角坐标为(100,100)
,大小为300*300
;第二个矩形左上角坐标为(150,150)
,大小为200*200
;第三个矩形左上角坐标为(200,200)
,大小为100*100
,请看效果演示:
图8 - 案例展示
<br/>
3.3 绘制路径
要绘制路径,首先必须调用beginPath()
方法,表示要开始绘制新的路径,然后调用以下方法来绘制路径,请看方法介绍:
1.arc(x,y,radius,startAngle,endAngle,anticlockwise)
该方法会绘制一个以(x,y)
为圆心,以radius
为半径的圆弧,从startAngle
开始到endAngle
结束,其中,startAngle
表示圆弧的起始点(单位:弧度),endAngle
表示圆弧的终点(单位:弧度),我们还可以指定anticlockwise
的参数值来规定绘制圆弧的方向(默认为顺时针);
2.arcTo(x1,y1,x2,y2,radius)
该方法根据给定的控制点(其中x1,y1
表示第一个控制点的坐标,x2,y2
表示第二个控制点的坐标,)和半径画一段圆弧,再以直线连接两个控制点;
3.lineTo(x,y)
该方法绘制一条从当前位置到指定x
以及y
位置的直线;
4.moveTo(x,y)
该方法将笔触移动到指定的坐标x
以及y
上,当<canvas>
初始化或者beginPath()
调用之后,我们通常会使用moveTo()
函数设置起点;
5.rect(x,y,width,height)
该方法绘制一个左上角坐标为(x,y)
,宽高为width
和height
的矩形;
创建了路径之后,我们可以调用closePath()
方法绘制一条连接到路径起点的线条;也可以调用fill()
方法并指定fillStyle
属性完成填充;另外,也可以通过stroke()
方法对路径描边,并通过strokeStyle
指定其样式;
接下来,请看演示案例:
var canvas = document.getElementById("canvas")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
//绘制一个圆形
ctx.beginPath()
ctx.moveTo(250,150)
ctx.arc(250,150,10,0,2*Math.PI)
ctx.fillStyle = "aqua"
ctx.fill()
ctx.closePath()
//绘制一条线
ctx.beginPath()
ctx.moveTo(150,200)
ctx.lineTo(350,200)
ctx.lineWidth = 10
ctx.strokeStyle = "blue"
ctx.stroke()
ctx.closePath()
//绘制一个矩形
ctx.beginPath()
ctx.moveTo(150,250)
ctx.rect(150,250,200,200)
ctx.fillStyle = "green"
ctx.fill()
ctx.closePath()
}
上述代码中我们分别绘制了圆形、直线、矩形,请看效果展示:
图9 - 案例展示
<br/>
3.4 绘制文本
canvas提供了两种方法来渲染文本,请看详细介绍:
1.fillText(text,x,y,[,maxWidth])
该方法在指定的(x,y)
位置填充指定的文本,其中maxWidth
表示绘制的最大宽度,为可选参数;
2.strokeText(text,x,y,[,maxWidth])
该方法在指定的(x,y)
位置绘制文本边框,其中maxWidth
表示绘制的最大宽度,为可选参数;
我们可以通过设置font
、textAlign
、textBaseline
和direction
的值来对文本进行渲染,请看详细介绍:
- font:表示文本样式、大小及字体,默认字体是
10px sans-serif
; - textAlign:表示文本对齐方式,其值包括
left
(文本左对齐)、right
(文本右对齐)、center
(文本居中对齐)、start
(文本对齐界线开始的地方,即左对齐指本地从左向右,右对齐指本地从右向左)、end
(文本对齐界线结束的地方,即左对齐指本地从左向右,右对齐指本地从右向左),默认值为start
; - textBaseline:表示文本的基线,其值包括
top
(文本基线在文本块的顶部)、hanging
(文本基线是悬挂基线)、middle
(文本基线在文本块的中间)、alphabetic
(文本基线是标准的字母基线)、ideographic
、bottom
,默认值是alphabetic
; - direction:此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀;
接下来,请看演示案例:
var canvas = document.getElementById("canvas")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
ctx.font = "bold 30px Arial"
ctx.textAlign = "center"
ctx.textBaseline = 'middle'
ctx.fillText("石璞东",100,100)
ctx.strokeText("石璞东",200,100)
}
上述代码中我们通过ctx.fillText()
方法和ctx.strokeText()
方法绘制了字符串石璞东
,并指定了其字体样式、对齐方式等,请看效果演示:
图10 - 案例展示
<br/>
3.5 绘制图像
可以用drawImage()
方法在画布上绘制图像,该方法需要三个、五个或九个参数,请看参数解释:
1.drawImage(image, x, y)
其中,image
是image
或者canvas
对象,x
和y
是其在目标canvas
里的起始坐标;
2.drawImage(image, x, y, width, height)
该方法多了两个参数:width
和height
,这两个参数用来控制当向canvas
画入时应该缩放的大小;
3.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
第一个参数和其它的是相同的,都是一个图像或者另一个canvas
的引用,前4个是定义图像源的切片位置和大小,后4个则是定义切片的目标显示位置和大小,如图所示:
图11 - 图解参数
<br/>
接下来,请看演示案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas示例</title>
<style>
img{
width: 250px;
height: 250px;
}
</style>
</head>
<body>
<img src="author.jpg" id="image">
<button id="btn">绘制图像</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas")
var imgEle = document.getElementById("image")
var oclear = document.getElementById("clear")
var obtn = document.getElementById("btn")
var num = 0;
if (canvas.getContext){
var ctx = canvas.getContext("2d");
obtn.onclick = () => {
if(num == 0){
ctx.drawImage(imgEle,20,20)
}else if (num == 1){
ctx.drawImage(imgEle,20,20,100,100)
}else{
ctx.drawImage(imgEle,20,20,300,300,10,10,200,200)
}
}
oclear.onclick = () => {
ctx.clearRect(0,0,500,500)
num >= 2 ? num=0:num++;
}
}
</script>
</body>
</html>
上述代码中,我们分别指定了当ctx.drawImage()
方法分别有3个、5个、9个参数时绘制图像的效果,请看效果展示:
图12 - 案例展示
<br/>
读者在每次绘制完成当前状态下的图像之后,需要点击「清除画布」按钮清除当前画布。
实际上,我们还可以将video
元素作为drawImage()
方法的图像来源,请看代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas示例</title>
</head>
<body>
<video src="Lenet.mp4" id="video" controls width="360" height="240" preload="auto"></video>
<button id="btn">绘制图像</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas")
var videoEle = document.getElementById("video")
var oclear = document.getElementById("clear")
var obtn = document.getElementById("btn")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
obtn.onclick = () => {
ctx.drawImage(videoEle,0,0,360,360)
}
oclear.onclick = () => {
ctx.clearRect(0,0,500,500)
}
}
</script>
</body>
</html>
上述代码中,我们可以通过点击「绘制图像」按钮将当前帧图像绘制在canvas
中;我们还可以在视频中绘制一个矩形,请看代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>canvas示例</title>
</head>
<body>
<video src="Lenet.mp4" id="video" controls width="360" height="240" preload="auto"></video>
<button id="btn">开始预测</button>
<button id="clear">清除画布</button>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<script>
var canvas = document.getElementById("canvas")
var videoEle = document.getElementById("video")
var oclear = document.getElementById("clear")
var obtn = document.getElementById("btn")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
var width = 180;
var height = 150;
ctx.lineWidth = 5;
ctx.strokeStyle = "aqua"
obtn.onclick = () => {
ctx.drawImage(videoEle,0,0,360,360)
ctx.strokeRect(30,90,width,height)
ctx.font = "20px bold Arial"
ctx.fillStyle ="blue"
ctx.fillText("computer",100,85)
}
oclear.onclick = () => {
ctx.clearRect(0,0,500,500)
}
}
</script>
</body>
</html>
上述代码中,我们在视频中绘制了一个矩形,并用矩形框框出了视频在0:01
秒时电脑出现的位置,请看效果演示:
图13 - 案例展示
<br/>
4.你画我猜(MNIST手写数字版)
讲解完成了<canvas>
的基础知识之后,我们来看看本书中用到的<canvas>
的相关内容,在你画我猜(MNIST手写数字版)
这个案例中,我们先在页面中定义一个<canvas>
标签,当用户按下鼠标并拖动时,可以在画布上画出拖动的轨迹,请看代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>你画我猜(MNIST手写数字版)canvas示例</title>
</head>
<body>
<canvas width="500" height="500" style="border: 1px solid red" id="canvas"></canvas>
<button id="clear">清除画布</button>
<script>
var canvas = document.getElementById("canvas")
var oclear = document.getElementById("clear")
if (canvas.getContext){
var ctx = canvas.getContext("2d");
canvas.onmousemove = (e) => {
if(e.buttons == 1){
ctx.fillStyle = "black"
ctx.fillRect(e.offsetX,e.offsetY,5,5)
}
}
oclear.onclick = () => {
ctx.clearRect(0,0,500,500)
}
}
</script>
</body>
</html>
请看效果演示:
图13 - 案例展示
<br/>
5.绘制关键点(人体姿态检测、目标识别)
在本书关于目标检测的案例中,我们需要使用手机的摄像头,并实时的框选出摄像头所采集的每一帧数据中的物体,如图3所示,其中涉及到的canvas
相关的知识请参考3.5小节,这里不在赘述。
6.文章最后
以上就是本文的所有内容,小伙伴们学会了嘛?快去实践一下吧!更多详情请关注我的更多开源作品:
1. 微信公众号(hahaCoder)
图14 - 微信公众号
<br/>
2. 微信小程序(hahaAI)
图15 - 微信小程序
<br/>
3. Github
链接地址:https://github.com/TURBO1002