Vue 图片矩形标记

2021-01-02  本文已影响0人  茧铭

本人是记录了一次功能开发,先上一下效果图

矩形标记.jpg

首先博主是一个Java后端的臭弟弟,前端用的不是特别好,因此此标记功能是改造了组件VMarker(https://vmarker.sagocloud.com/diy/)实现的一些自定义的功能,首先从后台获取要标记的图片,每次仅能标记一张是基本要求。在标记过程中可以点击上一张、下一张切换,切换会保留之前已经标记过的图片(存储在data中不支持页面刷新,可以根据需求存储会话或者本地)。
清空按钮可以清空当前图片已经标记过的;
隐藏则是能切换显示矩形框,隐藏之后可以查看原图片以便观察。
完成标记完成后,点击完成之后可以将保存的标记数据保存至后端。

1.安装VMarker

npm install vue-picture-bd-marker

2.页面制作

<el-row :gutter="3">
    <el-col :span="2">
      <div style="width: 100%;height: 300px;"></div>
      <div class="jungle_button">
        <el-button type="primary" @click='before' :disabled="pageData.currentPage === 0 || testForm.sign.length === 1">上一张</el-button> <br/>
      </div>
      <div class="jungle_button">
        <el-button type="primary" @click='turnNext' :disabled="pageData.currentPage === (testForm.sign.length - 1) || testForm.sign.length === 1">下一张</el-button> <br/>
      </div>
      <div class="jungle_button">
        <el-button type="danger" @click="clearAll">清空</el-button> <br/>
      </div>
      <div class="jungle_button">
        <el-button type="warning" @click="handleHiddenData">{{isHidden ? '显示' : '隐藏'}}</el-button>
      </div>
      <div class="jungle_button" v-show="pageData.currentPage === (testForm.sign.length - 1)">
        <el-button type="success" @click="submitForm">完成</el-button>
      </div>
    </el-col>
    <el-col :span="16">
      <el-form
        id="el-form"
        ref="testForm"
        label-position="left"
        label-width="30px"
      >
        <el-row style="width: 100%; height: 90%">
            <el-form-item>
              <h3 class="image_title"> 标注图片 ({{testForm.sign[this.pageData.currentPage].name}})</h3>
              <div ref="imageWrapper" class="signBox">
                <div class="sign-main">
                  <ui-marker ref="aiPanel-editor" class="ai-observer"
                             v-bind:uniqueKey="'dsadsahdjklsaj'"
                             :ratio="4/3"
                             @vmarker:selectOne="selectOne"
                             @vmarker:onUpdated="onUpdated"
                             @vmarker:onDrawOne="onDrawOne"
                             @vmarker:onReady="onReady"
                             @vmarker:onImageLoad="onImageLoad"
                             v-bind:readOnly="readOnly"
                             :imgUrl="currentInfo.currentBaseImage">
                  </ui-marker>
                </div>
              </div>
            </el-form-item>
        </el-row>
      </el-form>
    </el-col>
    <el-col :span="4">
      <div style="width: 100%;height: 300px;"></div>
      <div id="headBtnOuter">
        <div v-for="(item,index) in label" class="headBtn" :class="[index === selected.index ? 'current' : '']"
             :code="item.value" :name="item.name"
             minbdlength="0" limitbdnum="1" :key="index"
             currentcolor="#FF0000" style="border-left: 5px solid #2c62a3;border-color:#2c62a3" @click="handleTabClick(index, $event)">
          {{item.name}}  <!--<i class="el-icon-delete" @click="handleDeleteTab(index)"></i>-->
        </div>
      </div>
      <!--<div class="add_tab">
        <el-button @click="handleAddTab"> 新增标签 </el-button>
      </div>-->
      <el-dialog title="新增标签" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
        <el-form ref="addTabForm" label-position="left" label-width="120px" class="demo-form-inline">
          <el-form-item label="名称" prop="name">
            <el-input v-model="newTab.name" type="text" placeholder="请输入内容" maxlength="10" show-word-limit></el-input>
          </el-form-item>
          <el-form-item label="值" prop="value">
            <el-input v-model="newTab.value" type="text" placeholder="请输入内容" maxlength="100" show-word-limit></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="dialogVisible = false">取 消</el-button>
            <el-button type="primary" @click.stop="saveTab">确 定</el-button>
          </el-form-item>
        </el-form>
      </el-dialog>
    </el-col>
  </el-row>

3.data数据

data () {
    return {
      testForm: {         # 需要标记的图片的集合
        sign: []
      },
      currentInfo: {        # 当前图片的信息,包含图片原本的高矮胖瘦尺寸
        currentBaseImage: '',       
        rawW: 0,
        rawH: 0,
        currentW: 0,
        currentH: 0,
        checked: false,      # false表示当前图片还没有标记过
        data: []             # 表示图片矩形标记信息
      },
      pageData: {        # 辅助信息,记录图片的编号,判断是不是第一张或者最后一张等
        currentPage: 0
      },
      readOnly: false, 
      label: [],        # 右侧标签集合
      selected: {      # 当前选中的标签的信息
        name: '',
        value: '',
        index: 0
      },
      allInfo: [],     # 存储过程中的图片的矩形标记信息
      imageInfo: [],    # 存储图片原始信息
      marker: null,
      dialogVisible: false,   # dialog显示与否
      newTab: {               # 新增标签信息
        name: '',
        value: ''
      },
      isHidden: false,        # 切换隐藏功能
      searchDataDetail: {      # 获取图片信息的参数表单
        id: '',
        isAdmin: '',
        page_no: 0,
        page_size: 10,
        updateType: '',
        startKey: ''
      },
      labelArr: '',        # 初始化的标签信息
      dataId: null      # 与上面表单的id对应
    }
  },
  1. 函数
methods: {
    /**
     * 图片加载完成后回调, 记录图片当前的大小和原始大小 data={rawW,rawH,currentW,currentH}
     */
    onImageLoad (imageInfo) {
      if (!this.imageInfo[this.pageData.currentPage]) {
        this.imageInfo[this.pageData.currentPage] = imageInfo
      }
      console.info(this.imageInfo)
    },
    /**
     * 当控件准备完成后回调,参数为 uniqueKey
     */
    onReady () {
      // let markerDiv = document.getElementsByClassName('vmr-ai-panel')
      // let innnerDiv = markerDiv[0].firstChild
      // let markerImg = document.getElementsByClassName('vmr-ai-raw-image')
      // let markerMask = document.getElementsByClassName('vmr-ai-raw-image-mask')
      // markerDiv[0].setAttribute('style', 'position: relative; overflow: hidden; width: 60%; height: 60%;')
      // innnerDiv.setAttribute('style', 'position: relative; overflow: hidden;')
      // markerImg[0].setAttribute('style', 'display: block; position: absolute; user-select: none; width:700px; height: 500px;')
      // markerMask[0].setAttribute('style', 'user-select: none; position: absolute; cursor: crosshair; left: 0px; top: 0px;width:700px; height: 500px;')
    },
    /**
     * 画框后回调,data 和 uniqueKey先不用了
     */
    onDrawOne (data, uniqueKey) {
      if (!this.selected.name || !this.selected.value) {
        this.$message.info('请先设置标签')
        this.$refs['aiPanel-editor'].getMarker().clearData()
        return
      }
      let name = data.tagName === '请选择或添加新标签' ? this.selected.name : data.tagName
      let tagValue = data.tagName === '请选择或添加新标签' ? this.selected.value : data.tag
      this.$refs['aiPanel-editor'].getMarker().setTag({
        tagName: name,
        tag: tagValue
      })
    },
    /**
     * 当选中图片上的标注框时回调,参数为data【标注数据】, uniqueKey
     */
    selectOne (data, uniqueKey) {
    },
    /**
     * 当标注框位置或者标框属性发生改动时回调,参数为data【标注数据】, uniqueKey
     */
    onUpdated (data, uniqueKey) {
    },
    /**
     * 切换下一张图片
     */
    turnNext () {
      let marker = this.$refs['aiPanel-editor'].getMarker()
      this.saveCurrentPageData(marker)
      marker.clearData()
      if ((this.pageData.currentPage + 1) < this.testForm.sign.length) {
        this.pageData.currentPage += 1
      }
      this.currentInfo.currentBaseImage = this.testForm.sign[this.pageData.currentPage].src
      // 页数+1了之后,如果之前是有data的,就加载进去
      if (this.allInfo[this.pageData.currentPage]) {
        marker.renderData(this.allInfo[this.pageData.currentPage])
      }
    },
    /**
     * 清空当前marker的标记信息
     */
    clearAll () {
      this.$refs['aiPanel-editor'].getMarker().clearData()
      if (this.allInfo[this.pageData.currentPage]) {
        this.allInfo[this.pageData.currentPage] = null
      }
    },
    /**
     * 切换上一张图片
     */
    before () {
      let marker = this.$refs['aiPanel-editor'].getMarker()
      this.saveCurrentPageData(marker)
      marker.clearData()
      if (this.pageData.currentPage > 0) {
        this.pageData.currentPage -= 1
      }
      this.currentInfo.currentBaseImage = this.testForm.sign[this.pageData.currentPage].src
      // 页数-1了之后,如果之前是有data的,就加载进去
      if (this.allInfo[this.pageData.currentPage]) {
        marker.renderData(this.allInfo[this.pageData.currentPage])
      }
    },
    /**
     * click标签切换,切换当前图标
     * @param index
     * @param event
     */
    handleTabClick (index, event) {
      this.selected.index = index
      let el = event.currentTarget
      this.selected.name = el.getAttribute('name')
      this.selected.value = el.getAttribute('code')
    },
    /**
     * 删除标签:1.至少保留一个标签,不能删除最后一个  2.删除系统选中的,需要
     */
    handleDeleteTab (index) {
      if (this.label.length <= 1) {
        this.$message.warning('至少保留一个标签')
        return
      }
      this.label.splice(index, 1)
      if (index === this.selected.index) {
        this.selected.index = 0
        this.selectTab()
      }
    },
    handleAddTab () {
      this.newTab.name = ''
      this.newTab.value = ''
      this.dialogVisible = true
    },
    /**
     * 保存新增的标签
     */
    saveTab () {
      if (!this.newTab.name || !this.newTab.value) {
        this.$message.warning('标签名或标签值不能为空')
        return
      }
      for (let index in this.label) {
        let item = this.label[index]
        if (item.name === this.newTab.name || item.value === this.newTab.value) {
          this.$message.warning('标签名或标签值已存在,请重新输入')
          return
        }
      }
      this.label.push({ name: this.newTab.name, value: this.newTab.value })
      if (this.label.length === 1) {
        this.selected.name = this.label[0].name
        this.selected.value = this.label[0].value
      }
      this.dialogVisible = false
    },
    /**
     * 加载默认的标签
     */
    selectTab () {
      this.selected.name = this.label[this.selected.index].name
      this.selected.value = this.label[this.selected.index].value
    },
    /**
     * 切换隐藏
     */
    handleHiddenData () {
      let el = document.getElementsByClassName('vmr-ai-raw-image-mask')
      let isable = this.isHidden
      if (!isable) {
        el[0].style['display'] = 'none'
      } else {
        el[0].style['display'] = 'block'
      }
      this.isHidden = !isable
      console.info('this.isHidden : ' + this.isHidden)
    },
    /**
     * 完成标记,提交标记集合
     */
    submitForm () {
      this.saveCurrentPageData(null)
      let result = []
      for (let index in this.allInfo) {
        let labelArr = this.allInfo[index]
        let image = this.imageInfo[index]
        let otherInfo = this.testForm.sign[index]
        let param = {}
        param.src = otherInfo.src
        param.originalUrl = otherInfo.originalUrl
        param.name = otherInfo.name
        let size = {
          width: image.rawW,
          height: image.rawH
        }
        param.size = size
        let text = []
        if (labelArr && labelArr.length > 0) {
          labelArr.forEach(item => {
            let oneInfo = {}
            oneInfo.id = item.uuid
            oneInfo.type = item.tag
            oneInfo.name = item.tagName
            oneInfo.xmin = parseInt(item.position.x.substring(0, item.position.x.length - 1)) * size.width / 100
            oneInfo.ymin = parseInt(item.position.y.substring(0, item.position.y.length - 1)) * size.height / 100
            oneInfo.xmax = parseInt(item.position.x1.substring(0, item.position.x1.length - 1)) * size.width / 100
            oneInfo.ymax = parseInt(item.position.y1.substring(0, item.position.y1.length - 1)) * size.height / 100
            oneInfo.width = (oneInfo.xmax - oneInfo.xmin).toFixed(2)
            oneInfo.height = (oneInfo.ymax - oneInfo.ymin).toFixed(2)
            text.push(oneInfo)
          })
        }
        param.text = text
        result.push(param)
      }
      let req = {}
      req.labelInfoVOList = result
      req.dataId = this.dataId
      req.labelArr = this.labelArr
      completeLabel(req).then(res => {
        this.$router.back()
      })
    },
    /**
     * 保存当前页面的标记信息当data-allInfo
     * @param marker
     */
    saveCurrentPageData (marker) {
      if (!marker) {
        marker = this.$refs['aiPanel-editor'].getMarker()
      }
      this.allInfo[this.pageData.currentPage] = marker.getData()
    }
  },
  mounted () {
    /**
     * 刚刚切进来的时候,初始化内容
     */
    if (this.$route.query.dataId) {
      this.searchDataDetail.id = this.$route.query.dataId
      this.searchDataDetail.isAdmin = this.$route.query.ossType
      this.dataId = this.$route.query.dataId
    } else {
      this.$message.error('获取样本失败,请重新尝试')
      return
    }
    this.searchDataDetail.updateType = 'original'
    this.searchDataDetail.page_no = 0
    this.searchDataDetail.page_size = 100000
    // 初始化内容
    listOssData(this.searchDataDetail).then(res => {
      if (res.code === 100) {
        // 加载图片
        let dataList = res.result.data
        let formats = ['bmp', 'jpg', 'png', 'tif', 'gif', 'pcx', 'tga', 'exif', 'fpx', 'svg', 'psd', 'cdr', 'pcd', 'dxf', 'ufo', 'eps', 'ai', 'raw', 'WMF', 'webp']
        dataList.forEach(item => {
          let itemName = item.name
          let ending = itemName.substring(itemName.lastIndexOf('.') + 1)
          if (formats.indexOf(ending) > -1) {
            this.testForm.sign.push({ src: item.url, uuid: getUUid(12, 9), name: itemName, originalUrl: item.originalUrl })
          }
        })
        if (this.testForm.sign.length === 0) {
          this.$message.info('没有可以标记的内容')
        }
        this.allInfo = new Array(this.testForm.sign.length)
        this.imageInfo = new Array(this.testForm.sign.length)
        // 设置初始图片
        this.currentInfo.currentBaseImage = this.testForm.sign[this.pageData.currentPage].src
      }
    }).catch(error => {
      console.log(error)
    })
    /**
     * 初始化标签
     */
    detailData(this.$route.query.dataId).then(res => {
      if (res.result.labelClassify === 1) {
        this.labelArr = res.result.labelArr
        if (this.labelArr) {
          let labels = JSON.parse(this.labelArr)
          labels.forEach(item => {
            this.label.push(item)
          })
          this.selectTab()
        }
      }
    })
  },
  created () {}
上一篇下一篇

猜你喜欢

热点阅读