背景
最近在搞点机器学习相关的内容,大致是上传一个图片,然后机器识别特殊部分,通过返回的坐标,在图片上画框。
一说到画框这种东西,不用说肯定是使用 canvas 来画了,但是如何利用坐标来画正确的框,这其中就有一些坑要踩了。
下面容我来讲解一下在 react 中如果通过坐标来使用 canvas 画框。
准备
首先后端返回的是一个数组,每个 object 包含的是每个点的位置,表示经过机器识别之后的需要画框的位置。
类似于下面这种:
1 2 3 4
| [ { x: 0.7, y: 0.6 }, { x: 0.9, y: 0.8 } ]
|
写到这里,有人肯定问我,画框不是需要四个点吗,这才两个点,简直在逗我。
其实事情不是这样的,这两个点是对角线点,只要确定了对角线的点,就能画框了,你可以细想一下看看是不是这么回事。
所以我们准备好了后端的 API 返回的数据结构,就可以准备解构数据并且利用画框了。
开始画框
这里我们准备开始编写组件的渲染元素:
1 2 3 4
| <div> <canvas width={300} height={300} /> <img src={Img} style={{ display: "none" }} /> </div>
|
这里我们在 canvas 之后设定了一个 img 元素,然后设定它 display 为 none, 目的是在图片加载完成之后再绘制 canvas,这里我们分别为 canvas 和 img 加上 ref,以便获取其真实 dom 的宽高,下面我们开始在组件加载完成之后(componentDidMount)绘制长方框。
这里先贴出代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const canvas = this.refs.canvas; const ctx = canvas.getContext("2d"); const img = this.refs.image; img.onload = () => { const scale = img.width / 300; const scaledHeight = img.height / scale; ctx.drawImage(img, 0, 0, 300, scaledHeight); this.boundingBox.map(({ normalizedVertices }) => { const firstPoint = { x: normalizedVertices[0].x * 300, y: normalizedVertices[0].y * scaledHeight }; const rectSize = { w: normalizedVertices[1].x * 300 - normalizedVertices[0].x * 300, h: normalizedVertices[1].y * scaledHeight - normalizedVertices[0].y * scaledHeight }; ctx.strokeStyle = "red"; ctx.rect(firstPoint.x, firstPoint.y, rectSize.w, rectSize.h); ctx.stroke(); }); }
|
前两行获取到 canvas 元素的真实引用,然后在 img onload 的方法中进行画框,首先我们需要考虑到的是图片本身也是有宽高的,比如图片高宽为 500 400, 而我们的 canvas 画布宽为 300 200, 所以我们要确定合适的宽高比例,后端的坐标也是根据原始的图片进行计算返回的坐标,所以如果我们如果不对图片缩放比例进行计算,就会出现画框不正确,没有画到正确的位置。
所以 scale 变量表示当前图片的对应于 canvas 画布(宽 300 高 200)所需要的缩放比例,然后再用原始图片的高度除以缩放比例得到经过缩放之后图片在 canvas 画布上的高。
再调用 canvas 的 drawImage API 将缩放后的图片画到 canvas 上。
图片已经绘制在 canvas 上了,并且保持了正确的缩放比例,下面可以开始画框了。
遍历 boundingBox 取出每一个点的位置, 然后绘制第一个点的位置,因为 x 和 y 是 0 ~ 1 的数值,表示在原始图片的位置点,所以我们分别乘以当前图片在画布的宽高,就可以得到第一个点的位置,然后再通过第二个点的位置减去第一个点的得到长方形的宽高,还是我们在最开始的时候说的,两个对角点确定一个框。
最后定义 strokeStyle 为 red, 这样比较显眼, 然后调用 canvas 的 API rect 进行画框,最后调用 stroke 绘制.
这样我们就可以画出正确的框了。成品如下:

总结
网上有很多类似的处理 canvas 的包,但最后经过调研发现都被深度封装过,对很多场景不是很适用,所以只好调用原始的 canvs 在 react 中绘制,这个问题最 tricky 的地方就是如何确定图片缩放比例的问题,我前期也是踩了很多坑才正确绘制成功。别看最后核心思想就十几行代码,但实际操作起来还是很打脑壳的。hhhh
下面附上完整组件的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import React from "react"; import Img from "./group.png";
class DrawRect extends React.Component { constructor(props) { super(props); this.boundingBox = this.props.boundingBox || []; }
componentDidMount() { const canvas = this.refs.canvas; const ctx = canvas.getContext("2d"); const img = this.refs.image; img.onload = () => { const scale = img.width / 300; const imgHei = img.height / scale; ctx.drawImage(img, 0, 0, 300, imgHei); this.boundingBox.map(({ normalizedVertices }) => { const firstPoint = { x: normalizedVertices[0].x * 300, y: normalizedVertices[0].y * imgHei }; const rectSize = { w: normalizedVertices[1].x * 300 - normalizedVertices[0].x * 300, h: normalizedVertices[1].y * imgHei - normalizedVertices[0].y * imgHei }; ctx.strokeStyle = "red"; ctx.rect(firstPoint.x, firstPoint.y, rectSize.w, rectSize.h); ctx.stroke(); }); }; } render() { return ( <div> <canvas ref="canvas" width={300} height={200} /> <img ref="image" src={Img} style={{ display: "none" }} /> </div> ); } } export default DrawRect;
|