基于Vue + fabric.js的图片标注组件搭建

7,744

需求收集

做这个组件的初衷,是基于AI组的标注识别,传送一张图片以及图片上的一些坐标,返回对应的识别结果,前端要做的就是基于一张图片,在图片上绘制出相应的标注框,并将标注框对应的坐标以及宽高传送给后端进行识别,这是最基础的需求。在图片上进行绘制,首先想到的是用canvascancas强大的功能能让我们在图片上为所欲为,原生的canvasapi众多且繁杂,上手不易,fabric是一个基于canvas的强大的框架,提供一种类似面向对象的方法来编写canva,在原生canvas之上提供了交互式对象模型,通过简洁的api就可以在画布上进行丰富的操作。因此选择fabric来作为基础框架。

fabric.js介绍

fabric是基于canvas进行的api封装,可以实现绘制矩形、圆、椭圆、文本等一些基础图形,同时支持画笔自定义图形,fabric的优点在于它对生成的canvas画布进行了良好的封装,包括对画布以及画布上的对象进行调整,监听画布和对象的各种事件,使得画布交互逻辑变得简单易上手。fabric官网详细地列出了fabric的各种参数以及api,由于Fabric.js是国外的框架,文档为全英文,且相关示例少,所以建议配合源码使用

功能

构建画布

此处参考github.com/EmilyZhang1…

  1. 根据图片生成基础画布

首先组件从外部接收图片链接

props:{
    imgData: String  // 图片链接
}

watch监听imageData变化,并生成画布

watch:{
    imageData(val){
        if(val){
            this.fabricCanvas() // 生成画布
         }
     }
}

fabricCanvas事件主要是初始化fabric,并将图片设置成画布的背景图片,以便后续在画布上添加标注框

<template>
    <div id="canvax-box">
        <canvas id="label-canvas" :width="width" :height="height">
    </div>
</template>
<script>
 export default{
     methods:{
         fabricCanvas(){
             if(this.fabricObj){ // 如果画布已经存在,清空画布重新绘制
                 this.fabricObj.clear()
             } else {
                 this.fabricObj = new fabric.Canvas('lavel-canvas',{
                 // 此处设置画布的初始属性
                 uniformScaling: false, // 等比例缩放
                 enableRetinaScaling: false, 
                 selection: false // 禁止组选择
                 }
             }
             let Shape
             const image = new Image()
             image.src = this.imageData
             image.setAttribute('crossOrigin','anonymous') // 允许跨域访问
             image.onload = () => {
                 // 将canvas的width和height设置成图片的原始width,height
                 this.width = image.width 
                 this.height = image.height
                 this.fabricObj.setWidth(this.width)
                 this.fabricObj.setHeight(this.height)
                 // 将图片放置在外部容器中
                 let boxWidth = document.getElementById('canvas-box').offsetWidth
                 let boxHeight = document.getElementById('canvas-box').offsetHeight
                 let scaleX = boxWidth / image.width
                 let scaleY = boxHeight / image.height
                 // 确定缩放因子
                 this.scale = scaleX > scaleY ? scaleX : scaleY
                 document.querySelector('.canvas-container').style.width = this.width * this.scale + 'px'
                 document.querySelector('.canvas-container').style.height = this.height * this.scale + 'px'
                 document.querySelector('#label-canvas').style.width = this.width * this.scale + 'px'
                 document.querySelector('#label-canvas').style.height = this.height * this.scale + 'px'
                 document.querySelector('.upper-canvas').style.width = this.width * this.scale + 'px'
                 document.querySelector('.upper-canvas').style.height = this.height * this.scale + 'px'
                 
                 Shape = new fabric.Image(image)
                 this.fabric.setBackgroundImage(Shape,
                     this.fabricObj.renderAll.bind(this.fabricObj),
                     {
                         opaity: 1,
                         angle: 0
                     }
                 )
                 this.$nextTick(()=>{
                     this.fabricObj.renderAll() // 重新渲染画布
                 })
             }
         }
     }
 }
</script>
  1. 监听画布事件 fabric提供了一系列的事件帮助我们来很好的对画布进行各种操作

image.png 此次主要用到以下几个事件

watch:{
 imageData(val){
     if(val){
         this.fabricCanvas() // 生成画布
         this.fabricObjEvent() // 监听画布事件
      }
  }
}

image.png

画布操作

标注画框

标注画框主要用到的是上述中的mouse:down:画笔落下;mouse:move画框;mouse:up画笔抬起事件

image.png

image.png

image.png

image.png

image.png

image.png

调整画框

在调整画框之前,首先要将画布设置为可选择 image.png 如果想要修改画框的默认选中样式,可修改画框的对应参数即可

image.png 调整画框主要用到上述的object:moving:对象移动;object:modified:对象调整;

handleObjectMoving(){

// 阻止对象移动到画布外面
      let padding = 0; // 内容距离画布的空白宽度,主动设置
      var obj = e.target;
      if (obj.currentHeight > obj.canvas.height - padding * 2 ||
        obj.currentWidth > obj.canvas.width - padding * 2) {
        return;
      }
      obj.setCoords();
      if (obj.getBoundingRect().top < padding || obj.getBoundingRect().left < padding) {
        obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top + padding);
        obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left + padding);
      }
      if (obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height - padding || obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width - padding) {
        obj.top = Math.min(
          obj.top,
          obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top - padding
        );
        obj.left = Math.min(
          obj.left,
          obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left - padding
        );
      }

}
handleObjectModified(e){
this.$emit('objectModified',e.target)
}

选中画框

画框被选中时,可抛出选中事件

// rect setRect()方法中生成的画框
rect.on('selected',(e)=>{
    this.$emit('objectSelected', e.target)
})

删除画框

调用fabric的remove事件即可

this.fabricObj.remove(item)

清空所有画框

clearAllMark(){
    const objects = this.fabricObj.getObjects()
    for(let i in objects){
        this.fabricObj.remove(i)
    }
    this.$emit('clearAllMark')
}

根据坐标生成画框

  1. 生成单个画框

image.png

  1. 批量生成

image.png

预览

此处参考github.com/Dark2017/vu…

使用css的transform来对画布进行放大缩小和拖拽操作

image.png

放大缩小

  1. 放大

image.png 2. 缩小

image.png 3. 还原

image.png

拖拽

image.png