Canvas实现标注多边形与图像切割

我又重新实现了一版依赖Airglass.js库的基于Canvas技术的功能包括多边形区域标注以及图像切割的实验项目,这篇想法记录开发过程。先放一张演示动图,囊括多边形标注与图像切割的演示。

这是Canvas标注多边形区域与切割图像的实验项目入口

多边形标注与图像切割的演示

加载待标注图像后

实际应用场景中,根据需要加载特定图像。在本实验项目中,为了体现对多样性的尊重,缓解什么疲劳,每次刷新都会加载不同的图像作为示例图像。我特意从自己的相册中选取了几张有东西可标注的照片。因为我选择的图片的宽、高不固定,因此我需要将图片们按一定规则缩放,以适配画布大小。

目前实例化一个Airglass对象可以指定四个参数。一是作为Glass放置容器的DOM元素,二是画布的样式宽度,三是画布的样式高度,最后是和渲染清晰度有关的设备像素比。

按照我的预期设想,固定画布的样式高度,等到图像加载完毕后确定画布的样式宽度。所以初始化Airglass是在图像加载完之后。得到的两张已加载图像img1和img2。

let agHeight = 280;
let DPR = window.devicePixelRatio;
let img1ResizeWidth = img1.width / img1.height * agHeight;
let img2ResizeWidth = img2.width / img2.height * agHeight;
ag = new airglass.Airglass({
  element: document.querySelector('#wrap'),
  width: img1ResizeWidth + img2ResizeWidth,
  height: agHeight,
  DPR: DPR
});
imageRenderer = ag.addRenderer();
polygonRenderer = ag.addRenderer();
controllerRenderer = ag.addRenderer();

关于缩放图像以适配画布这一块的算法,其实和角度与弧度之间的转换算法很类似。图像的原始宽度做分子,原始高度做分母,就得到了图像等比缩放的宽、高比例值。使用这个比值与一个数相乘,就得到了那个数按照这个比值算出的对应分子的数值。

等比缩放图像的算法公式

准备第2个Airglass

该实验项目用到了两个Airglass实例。如上方演示动图所示,左侧的Airglass实例用于显示待标注图像、标注多边形,右侧的Airglass实例用于呈现图像的标注结果。

clipAg = new airglass.Airglass({
  element: document.querySelector('#clip'),
  width: img1ResizeWidth + img2ResizeWidth,
  height: agHeight,
  DPR: DPR
});

按照我的想法,另一个Airglass的大小和第一个的一样。第一个Airglass的大小在加载完待标注图像后初始化已经获得了。直接使用现成的画布宽度与高度数值初始化第2个Airglass。

canvas图像标注结果与切割

第二个Airglass存在的必要性在于呈现图像的标注结果。至于何为标注结果,多边形控制点在图像上的坐标是标注结果,将标注区域作为图像的切割依据以得到切割后的图像也是标注结果。

如果要获得多边形各控制点在图像上的位置坐标,不要忘了图像是经过缩放后渲染到画布上的,如果没有缩放图像而是按原始图像大小直接渲染到画布上,则不需要考虑按比例值反求出各控制点在图像上的真实位置坐标。

let Polygon = airglass.extend(airglass.Renderable, {
  _constructor: function (params) {
    this.path = null;
    this.points = params.points || [];
  },
  updatePath: function () {
    let path = new Path2D;
    for (let i = 0; i < this.points.length; i++) {
      let point = this.points[i];
      if (i == 0) {
        path.moveTo(point.x, point.y);
        continue;
      }
      path.lineTo(point.x, point.y);
    }
    this.path = path;
  },
  addPoint: function (point) {
    this.points.push(point);
  },
  draw: function (ctx) {
    ctx.strokeStyle = this.stroke;
    ctx.lineWidth = this.line;
    ctx.fillStyle = this.fill;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.fill(this.path);
    ctx.stroke(this.path);
  }
})

我专门定义了Ploygon类,表示多边形。它存储至少三个点,因为至少需要三个点才能连成一个最少顶点的面。在Three.js中,最小面也是由3个顶点组成。

触摸与拖拽

最开始Airglass内置了基础图像,比如圆形、矩形和多边形。在我实际应用Airglass的过程中发现基础的图形不能满足变化多样的需要。我删掉了Airglass中的基础图形,保留下Renderable和Effect这样的抽象类,方便扩展出多种多样的形状和丰富的效果。

我也将Airglass中关于事件处理方面的代码保留了下来,其中就包括触摸事件。Airglass制作的界面技能使用鼠标交互,也能使用手指在触摸屏上交互。我将所有事件名称统一按他们在触摸屏上的名称称呼。比如mousedown和touchstart在Airglass中是一种事件类型,但有些是鼠标特有的事件类型我也保留了下来,比如mousemove。

Airglass的实例能订阅这些主要的事件,我常常在一个函数中就能把所有的代码逻辑写清楚,只需判断事件类型即可。

ag.subscribe(event => {
  if(event.type == 'touchstart'){
    touchstart: {
      /* 处理鼠标按下或手指在触摸屏上按下时的逻辑 */
    }
  }
  
  if(event.type == 'touchmove'){
    touchmove: {
      /* 处理鼠标按下并移动或手指在触摸屏上滑动时的逻辑 */
    }
  }
  
  /* 其他事件类型的处理 */
})
请使用Github账号登录留言