背景

最近在搞点机器学习相关的内容,大致是上传一个图片,然后机器识别特殊部分,通过返回的坐标,在图片上画框。
一说到画框这种东西,不用说肯定是使用 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;