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

需求收集

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

fabric.js介绍

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

功能

构建画布

  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

预览

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

image.png

放大缩小

  1. 放大
image.png
  1. 缩小
image.png
  1. 还原
image.png

拖拽

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容