前端 Canvas 图像标记

  • Post author:
  • Post category:其他




前端 Canvas 图像标记

作者:

@ 很菜的小白在分享


日期:2021年12月22日



介绍

为什么要做图像标记?我们开发过程可能会涉及到需要在一张图片上做记号,例如:

人脸识别

,我们可能需要将识别到的人脸框选起来,类似我们手机相机对焦人脸的效果。本文主要是记录下本人在开发智能AI教育平台时,实现图像标记。



方案选择



方案一


html + css + js

(放弃)

这里我只能说它是一种方案,但不是最优的,其次扩展性不是很强 (大家肯定都想写出一劳永逸的code),如果后期需要对标记后的图片入库这类操作时,这种方案就无法实现了。



方案二


canvas


canvas 较方案一扩展性就很强了,它不仅足够灵活,而且可以实现复杂的效果,例如绘制不规则图形时,重要的是绘制的内容可以更好的适配大小,并且也支持导出。



实现



获取图片信息

这里主要是需要通过图片尺寸信息来动态生成 canvas 画布大小,动态的目的就是确保我们可以更好的适配绘制样式,下文会继续提到。

/**
 * @description: 获取识别后的图像
 * @param  {Object} options 配置对象
 * @param  {Function} cb 回调函数
 * @return {*}
 * options {
 *  url: 图像地址,
 *  types: 'features,check,checkName', // features: 特征点 check: 人脸检测 checkName: 人脸识别
 *  data: 图像数据
 * }
 */
export const getRecognitImage = (options, cb) => {
  const types = options.types

  // 获取图片类型
  let outType = options.url.substring(options.url.lastIndexOf('.') + 1)
  if (outType == 'jpg') outType = 'jpeg'

  // 创建图片对象
  const img = new Image()
  img.src = options.url + '?tamp=' + new Date().getTime() // 此处添加时间戳防止因为缓存导致跨域问题
  img.crossOrigin = 'Anonymous'
  img.onload = () => {
    // 获取图片大小
    const width = img.width
    const height = img.height
  }
}



动态生成 canvas并绘制图片


// 此处省略之前的代码
img.onload = () => {
    // 创建画布
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    
    // 设置画布大小与图片大小一致
    const width = img.width
    const height = img.height
    canvas.width = width
    canvas.height = height
    
    // 绘制图片
    ctx.drawImage(img, 0, 0, width, height);
    
    // 绘制特征点
    if (types.includes('features')) {
      drawPoint(ctx, canvas, options)
    }
    // 绘制人脸区域
    if (types.includes('check') || types.includes('checkName')) {
      drawRect(ctx, 'check', types.includes('checkName'), canvas, options)
    }
  }



根据坐标信息进行标记


数据格式

// 这里点的信息按照 上-右-下-左 的顺序存储
[
	{
		"points": [
			{
				"x": 10,
				"y": 10
			}, {
				"x": 30,
				"y": 10
			}, {
				"x": 30,
				"y": 60
			}, {
				"x": 10,
				"y": 60
			}
		]
	}
]

这里做了很多适配处理,目的就是为了绘制出来的图片效果更自然些



效果图

/**
 * @description: 绘制框
 * @param  {*}
 * @return {*}
 * @param {*} ctx 画布上下文
 * @param {*} type 绘制类型
 * @param {*} checkName 是否绘制名称
 * @param {*} canvas canvas 信息
 * @param {*} options 绘制信息
 */
function drawRect(ctx, check, checkName, canvas, options) {
  let canvasW = canvas.width
  let lineWidth = 2;
  
  // 依据图片大控制框线的宽度,适配框线的粗细
  if (canvasW > 1000) {
    lineWidth = canvasW.toString().substring(0, 1) * 3
  }
  
  options.data.bbox.forEach((item, index) => {
    ctx.strokeStyle = "#FF0000";
    ctx.lineWidth = lineWidth;
    ctx.beginPath()
    
    // 获取第一个点和最后一个点
	let first = item.points[0]
    let last = item.points[item.points.length-1]
    // 初始化画笔的位置
    ctx.moveTo(first .x, first .y)
    
    // 根据点位绘制图像
    for (let i = 0, len=item.points.length; i < len; i++) {
      let that = item.points[i]
      let next = item.points[i+1]
      ctx.lineTo(next.x, next.y)
    }
    
    ctx.stroke();
    ctx.fillStyle = "#FFFFFF";
    
    // 绘制带名称的情况
    if (checkName) {
      ctx.fillStyle = "#FF0000";

      let name = item.text
      if (name == 'no name') name = '未知'

      // 计算字体大小,用于适配
      let fontSize = (last.y - first.y) / (lineWidth*4)
      ctx.font = fontSize+'px bold'

      // 获取该字体大小下文本的宽度
      let textWidth = ctx.measureText(name)
      /*
        设置文本框的宽度
          expression: textWidth.width + 文本绘制的 X 位置
      */
      let rectWidth = textWidth.width + (first.x + lineWidth*2)
      // 设置最小宽度值为框的宽度
      if (rectWidth < Math.abs(item.points[1].x - first.x)) {
        rectWidth = Math.abs(item.points[1].x - first.x)
      }
      
      // 这里除 6 就是将框的高度分为6等分,将文本置于框底部
      ctx.fillRect(first.x, last.y - (last.y / 6), rectWidth, (last.y / 6))

      ctx.fillStyle = "#FFFFFF"
      ctx.textBaseline  = 'bottom'

      ctx.fillText(name, first.x + lineWidth*2, last.y-lineWidth)
      ctx.fillStyle = "#FFFFFF"
    }
  })
}

效果图

在项目中还进行了人脸特征点的绘制,点的绘制就更简单了,直接上代码

/**
 * @description: 绘制点
 * @param  {*}
 * @return {*}
 * @param {*} ctx 画布上下文
 */
function drawPoint(ctx, canvas, options) {
  let canvasW = canvas.width
  let rang = 3;
  
  // 根据 canvas 大小适配点的大小
  if (canvasW > 1000) {
    rang = canvasW.toString().substring(0, 1) * 3
  } else if (canvasW<400) {
    rang = 1
  }
  options.data.landmark.forEach(item => {
    item.forEach(val => {
      ctx.beginPath()
      ctx.arc(val[0], val[1], rang, 0, 2*Math.PI)
      ctx.fill()
    })
  })
}



生成图片

通过

canvas.toDataURL()

生成base64的图片

export const getRecognitImage = (options, cb) => {
  const types = options.types

  // 获取图片类型
  let outType = options.url.substring(options.url.lastIndexOf('.') + 1)
  if (outType == 'jpg') outType = 'jpeg'

  // 创建图片对象
  const img = new Image()
  img.src = options.url + '?tamp=' + new Date().getTime() // 此处添加时间戳防止因为缓存导致跨域问题
  img.crossOrigin = 'Anonymous'
  img.onload = () => {
    // 创建画布
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    ctx.save()
    // 设置画布大小为图片大小
    const width = img.width
    const height = img.height
    canvas.width = width
    canvas.height = height
    // 绘制图片
    ctx.drawImage(img, 0, 0, width, height);
    // 设置绘制样式为白色
    ctx.fillStyle = '#FFFFFF'
    if (types.includes('features')) {
      drawPoint(ctx, canvas, options)
    }
    if (types.includes('check') || types.includes('checkName')) {
      drawRect(ctx, 'check', types.includes('checkName'), canvas, options)
    }

    ctx.save()
    // 生成图片
    cb(canvas.toDataURL("image/" + outType, 1))
  }
}



(完)



版权声明:本文为weixin_43809685原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。