Bläddra i källkod

feat: 图片标注功能1.0、修改树型控件显示问题

Rmengdi 10 månader sedan
förälder
incheckning
b3ad23013f

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 615 - 25
package-lock.json


+ 2 - 0
package.json

@@ -40,6 +40,7 @@
     "echarts": "^5.4.3",
     "echarts-liquidfill": "^3.1.0",
     "element-plus": "^2.4.3",
+    "fabric": "^5.3.0",
     "file-saver": "^2.0.5",
     "image-conversion": "^2.1.1",
     "js-cookie": "^3.0.5",
@@ -57,6 +58,7 @@
     "vue": "^3.3.11",
     "vue-cropper": "^1.1.2",
     "vue-i18n": "^9.6.4",
+    "vue-konva": "^3.0.2",
     "vue-router": "^4.2.5",
     "vue-types": "^5.1.1",
     "vuedraggable": "^4.1.0"

+ 18 - 3
src/routers/modules/routerData.json

@@ -25,7 +25,7 @@
         "title": "创建任务",
         "link": "",
         "full": false,
-        "affix": true,
+        "affix": false,
         "noCache": true,
         "activeMenu": "/index"
       }
@@ -40,7 +40,7 @@
         "title": "日志",
         "link": "",
         "full": false,
-        "affix": true,
+        "affix": false,
         "noCache": true,
         "activeMenu": "/index"
       }
@@ -55,7 +55,22 @@
         "title": "结果",
         "link": "",
         "full": false,
-        "affix": true,
+        "affix": false,
+        "noCache": true,
+        "activeMenu": "/index"
+      }
+    },
+    {
+      "path": "/inferResult",
+      "name": "inferResult",
+      "component": "taais/homePage/inferResult",
+      "hidden": true,
+      "meta": {
+        "icon": "HomeFilled",
+        "title": "结果",
+        "link": "",
+        "full": false,
+        "affix": false,
         "noCache": true,
         "activeMenu": "/index"
       }

+ 18 - 0
src/typings/canvas.d.ts

@@ -0,0 +1,18 @@
+interface canvasPoint {
+  x: number
+  y: number
+  radius?: number
+  fill?: string
+  stroke?: string
+  strokeWidth?: number
+  left?: number
+  top?: number
+  selectable?: boolean
+  hasBorders?: boolean
+  hasControls?: boolean
+  evented?: boolean
+  originX?: string
+  originY?: string
+  id?: number
+  objectCaching?: boolean
+}

+ 106 - 0
src/utils/fabric.ts

@@ -0,0 +1,106 @@
+// 坐标点排序
+export const sortPoints = (aCoord: any) => {
+  let points = JSON.parse(JSON.stringify(aCoord))
+  // (tl tr) (bl,br)
+  const poiT: canvasPoint = linesIntersection(points[0] || points.tl, points[1] || points.tr, points[2] || points.br, points[3] || points.bl)
+  // (tl br) (tr,bl)
+  const poiB: canvasPoint = linesIntersection(points[0] || points.tl, points[3] || points.bl, points[2] || points.br, points[1] || points.tr)
+  if (poiT?.x != -1) {
+    // 第二个和第三个坐标互换
+    changePoint(points[1] || points.tr, points[2] || points.br)
+  } else if (poiB?.x != -1) {
+    // 第一个和第四个坐标互换
+    changePoint(points[0] || points.tl, points[3] || points.bl)
+    // 第二个和第三个坐标互换
+    changePoint(points[1] || points.tr, points[2] || points.br)
+    // 第一个和第二个坐标互换
+    changePoint(points[0] || points.tl, points[1] || points.tr)
+  }
+  return points
+}
+// 坐标点更换
+const changePoint = (poi1, poi2) => {
+  let temp = { x: poi1.x, y: poi1.y }
+  poi1.x = poi2.x
+  poi1.y = poi2.y
+
+  poi2.x = temp.x
+  poi2.y = temp.y
+}
+// 获取线段交点
+// 算法参考:https://www.jb51.net/article/90104.htm
+function linesIntersection(tl, tr, br, bl) {
+  let tn1 = tr.y - tl.y,
+    ty1 = tl.x - tr.x
+  let tn2 = br.y - bl.y,
+    ty2 = bl.x - br.x
+  let denominator = tn1 * ty2 - ty1 * tn2
+  if (denominator == 0) {
+    return { x: -1, y: -1 }
+  }
+  let distC_N2 = tn2 * bl.x + ty2 * bl.y
+  let distA_N2 = tn2 * tl.x + ty2 * tl.y - distC_N2
+  let distB_N2 = tn2 * tr.x + ty2 * tr.y - distC_N2
+
+  if (distA_N2 * distB_N2 >= 0) {
+    return { x: -1, y: -1 }
+  }
+  let distA_N1 = tn1 * tl.x + ty1 * tl.y
+  let distC_N1 = tn1 * bl.x + ty1 * bl.y - distA_N1
+  let distD_N1 = tn1 * br.x + ty1 * br.y - distA_N1
+  if (distC_N1 * distD_N1 >= 0) {
+    return { x: -1, y: -1 }
+  }
+  //计算交点坐标
+  let fraction = distA_N2 / denominator
+  let dx = fraction * ty1,
+    dy = -fraction * tn1
+  return { x: tl.x + dx, y: tl.y + dy }
+}
+
+export const useDrawArea = (
+  params = {
+    src: '',
+    width: 1920,
+    height: 1080,
+    area: ''
+  }
+) => {
+  const { src, width, height, area } = params
+  return new Promise((resolve, reject) => {
+    if (src && src != '' && area && area != '') {
+      let cvs = document.createElement('canvas')
+      let ctx: CanvasRenderingContext2D | null = cvs.getContext('2d')
+      let img = new Image()
+      img.setAttribute('crossOrigin', 'anonymous')
+      img.src = src
+      let points = area.split(',').map(item => {
+        return item.split(';').map(arss => Number(arss))
+      })
+      img.onload = () => {
+        cvs.width = width
+        cvs.height = height
+        if (ctx) {
+          ctx?.drawImage(img, 0, 0, width, height)
+          ctx.strokeStyle = '#ff0000'
+          ctx.lineWidth = 2
+          points.forEach(ar => {
+            ctx?.beginPath()
+            ctx?.moveTo(ar[0], ar[1])
+            ctx?.lineTo(ar[2], ar[3])
+            if (ar.length > 4) {
+              ctx?.lineTo(ar[4], ar[5])
+              ctx?.lineTo(ar[6], ar[7])
+            }
+            ctx?.closePath()
+            ctx?.stroke()
+          })
+        }
+        let url: string = cvs.toDataURL('image/png') || ''
+        resolve(url)
+      }
+    } else {
+      reject('')
+    }
+  })
+}

+ 90 - 0
src/views/demo/components/img-detect.vue

@@ -0,0 +1,90 @@
+<template>
+  <el-dialog class="modal-canvas" v-model="visible" title="标注" width="980px" height="700px">
+    <ImgMaker ref="imgMaker" v-if="visible" :src="cover" :area="area" :width="width" :height="height"></ImgMaker>
+    <div style=" margin-top: 10px;text-align: center">
+      <!-- <el-space> -->
+      <el-button type="primary" @click="onClearLast">撤销最后一次的操作</el-button>
+      <el-button type="primary" @click="onClearAll">清空所有</el-button>
+      <el-button type="primary" @click="onCancel">取消</el-button>
+      <el-button type="primary" @click="onSubmit">保存</el-button>
+      <!-- </a-space> -->
+    </div>
+  </el-dialog>
+</template>
+<script lang="ts" setup>
+import { reactive, ref, toRefs, defineProps, watchEffect } from 'vue'
+import ImgMaker from './img-maker.vue'
+const props = defineProps({
+  area: {
+    type: String,
+    default: ''
+  },
+  img: {
+    type: String,
+    default: ''
+  },
+  width: {
+    type: Number,
+    default: 1920
+  },
+  height: {
+    type: Number,
+    default: 1080
+  }
+})
+const emit = defineEmits(['success'])
+const imgMaker = ref()
+const state = reactive({
+  visible: false,
+  cover: ''
+})
+const { visible, cover } = toRefs(state)
+watchEffect(() => {
+  state.cover = props.img
+})
+
+const onClearAll = () => {
+  imgMaker.value.clearAll()
+}
+const onClearLast = () => {
+  imgMaker.value.clearObjLast()
+}
+const onCancel = () => {
+  onClearAll()
+  state.visible = false
+  imgMaker.value.drawType = null
+}
+const onSubmit = () => {
+  let points = imgMaker.value.getData()
+  // console.log('points', points)
+
+  let datas = []
+  if (points.length > 0) {
+    datas = points.map(point => {
+      if (point.br) {
+        let w = point.tr.x - point.tl.x
+        let h = point.bl.y - point.tl.y
+
+        const algoData = `${point.tl.x / 1920};${point.tl.y / 1080};${w / 1920};${h / 1080}`
+        // return `${point.tl.x};${point.tl.y};${point.tr.x};${point.tr.y};${point.br.x};${point.br.y};${point.bl.x};${point.bl.y}`
+        return algoData
+      } else {
+        let coors = ''
+        Object.keys(point).forEach(item => {
+          coors += `${point[item].x};${point[item].y};`
+        })
+        coors = coors.slice(0, -1)
+        return coors
+      }
+      // return `${point.tl.x};${point.tl.y};${point.tr.x};${point.tr.y}`
+    })
+  }
+  // console.log('datas', datas)
+  // 归一化
+
+  emit('success', datas)
+  onClearAll()
+  state.visible = false
+}
+defineExpose({ visible, cover })
+</script>

+ 645 - 0
src/views/demo/components/img-maker.vue

@@ -0,0 +1,645 @@
+<template>
+  <div>
+    <div style="margin-bottom: 10px">
+      <el-button plain type="primary" class="shape-border" @click="drawTypeChange('rectangle')">矩形</el-button>
+      <!-- <el-button plain type="primary" class="shape-border" @click="drawPolygon('polygon')">多边形</el-button> -->
+    </div>
+    <canvas id="canvas" :width="cWidth" :height="cHeight"></canvas>
+  </div>
+</template>
+<script lang="ts" setup>
+import { fabric } from 'fabric'
+import { reactive, watch, onMounted } from 'vue'
+// import { sortPoints } from '@/utils/fabric'
+
+const props = defineProps({
+  src: {
+    type: String,
+    default: ''
+  },
+  area: {
+    type: String,
+    default: ''
+  },
+  width: {
+    type: Number,
+    default: 1920
+  },
+  height: {
+    type: Number,
+    default: 1080
+  },
+  cWidth: {
+    type: Number,
+    default: 960
+  },
+  cHeight: {
+    type: Number,
+    default: 540
+  }
+})
+const state = reactive({
+  loading: true,
+  radio: 0.5,
+  realRadioX: 0.5,
+  realRadioY: 0.5,
+  imgPoint: { x: 0, y: 0 },
+  realPoint: { x: 0, y: 0 },
+  canvas: {} as any,
+  mouseFrom: { x: 0, y: 0 } as canvasPoint,
+  mouseTo: { x: 0, y: 0 } as canvasPoint,
+  // drawType: 'rectangle' as string, //当前绘制图像的种类
+  drawType: null as any, //当前绘制图像的种类
+  drawWidth: 1, //笔触宽度
+  color: '#E34F51', //画笔颜色
+  drawingObject: null as any, //当前绘制对象
+  moveCount: 1, //绘制移动计数器
+  doDrawing: false as boolean, // 绘制状态
+  rectPath: '' as string, //矩形绘制路径
+  //polygon 相关参数
+  polygonMode: false as boolean,
+  pointArray: [] as canvasPoint[],
+  lineArray: [] as canvasPoint[],
+  activeShape: false as any,
+  activeLine: '' as any,
+  line: {} as canvasPoint
+})
+watch(
+  () => state.drawType,
+  value => {
+    state.canvas.selection = !value
+  }
+)
+watch(
+  () => props.width,
+  value => {
+    state.canvas.setWidth(value)
+  }
+)
+watch(
+  () => props.height,
+  value => {
+    state.canvas.setHeight(value)
+  }
+)
+
+const loadInit = () => {
+  if (props.src == '') {
+    return
+  }
+  state.loading = true
+  state.canvas = new fabric.Canvas('canvas', {})
+  state.canvas.selectionColor = 'rgba(0,0,0,0.05)'
+  state.canvas.on('mouse:down', mousedown)
+  state.canvas.on('mouse:move', mousemove)
+  state.canvas.on('mouse:up', mouseup)
+  let imgElement = new Image()
+  imgElement.src = props.src
+  imgElement.onload = () => {
+    // 区域大小/图片原始大小 缩放比例
+    state.radio =
+      props.cWidth / imgElement.width > props.cHeight / imgElement.height ? props.cHeight / imgElement.height : props.cWidth / imgElement.width
+    // console.log('state.radio', state.radio)
+
+    // 屏幕分辨率/图片原始大小
+    state.realRadioX = props.width / imgElement.width
+    state.realRadioY = props.height / imgElement.height
+
+    state.imgPoint.x = Math.floor(imgElement.width / 2)
+    state.imgPoint.y = Math.floor(imgElement.height / 2)
+
+    state.realPoint.x = Math.floor(props.width / 2)
+    state.realPoint.y = Math.floor(props.height / 2)
+    let imgInstance = new fabric.Image(imgElement, {
+      selectable: false,
+      width: imgElement.width,
+      height: imgElement.height,
+      scaleX: state.radio,
+      scaleY: state.radio
+    })
+    state.canvas.add(imgInstance)
+    drawImage()
+    state.canvas.renderAll()
+    state.loading = false
+  }
+}
+const drawImage = () => {
+  if (props.area === '') {
+    clearAll()
+    return
+  }
+  let points = props.area.split(',').map(item => {
+    let areas = item.split(';')
+    let data = areas.map((ars, index) => {
+      let arp = 0
+      let ar = Number(ars)
+      if (index % 2 == 0) {
+        let dx = Math.abs(state.realPoint.x > ar ? state.realPoint.x - ar : state.realPoint.x + ar) / state.realRadioX
+        let rdx = Math.abs(state.imgPoint.x - dx)
+        arp = rdx
+      } else {
+        let dy = Math.abs(state.realPoint.y > ar ? state.realPoint.y - ar : state.realPoint.y + ar) / state.realRadioY
+        let rdy = Math.abs(state.imgPoint.y - dy)
+        arp = rdy
+      }
+      return Number(arp) * state.radio
+    })
+    return data
+  })
+  points.forEach(point => {
+    drawImageObj(point)
+  })
+}
+const drawImageObj = data => {
+  let path = 'M '
+  // debugger
+  let points = [] as any
+  let len = data.length / 2
+  for (let i = 0; i < len; i++) {
+    let idx = i * 2
+    points.push({ x: data[idx], y: data[idx + 1] })
+    path += `${data[idx]} ${data[idx + 1]} L `
+  }
+  let canvasObject = null as any
+  if (points[0]?.y === points[1]?.y && points[2]?.y === points[3]?.y && points[0]?.x === points[3]?.x && points[1]?.x - points[2]?.x) {
+    path = path.replace(/L\s$/g, 'z')
+    canvasObject = new fabric.Path(path, {
+      left: data[0],
+      top: data[1],
+      stroke: state.color,
+      selectable: false,
+      strokeWidth: state.drawWidth,
+      fill: 'rgba(255, 255, 255, 0)',
+      hasControls: false
+    })
+  } else {
+    canvasObject = new fabric.Polygon(points, {
+      stroke: state.color,
+      strokeWidth: state.drawWidth,
+      fill: 'rgba(255, 255, 255, 0)',
+      opacity: 1,
+      hasBorders: false,
+      hasControls: false,
+      evented: false
+    })
+  }
+  canvasObject['points'] = points
+  state.canvas.add(canvasObject)
+}
+const drawTypeChange = e => {
+  state.drawType = e
+  state.canvas.skipTargetFind = !!e
+  if (e == 'pen') {
+    // isDrawingMode为true 才可以自由绘画
+    state.canvas.isDrawingMode = true
+  } else {
+    state.canvas.isDrawingMode = false
+  }
+}
+// 鼠标按下时触发
+const mousedown = e => {
+  // 记录鼠标按下时的坐标
+  let xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY)
+
+  state.mouseFrom.x = xy.x
+  state.mouseFrom.y = xy.y
+  state.doDrawing = true
+  // 绘制多边形
+  if (state.drawType == 'polygon') {
+    state.canvas.skipTargetFind = false
+    try {
+      // 此段为判断是否闭合多边形,点击红点时闭合多边形
+      if (state.pointArray.length > 1) {
+        // e.target.id == this.pointArray[0].id 表示点击了初始红点
+        if (e.target && e.target.id == state.pointArray[0].id) {
+          generatePolygon()
+        }
+      }
+      //未点击红点则继续作画
+      if (state.polygonMode) {
+        addPoint(e)
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  }
+}
+// 鼠标松开执行
+const mouseup = e => {
+  let xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY)
+  state.mouseTo.x = xy.x
+  state.mouseTo.y = xy.y
+  state.drawingObject = null
+  state.moveCount = 1
+  if (state.drawType != 'polygon' && state.drawType != 'line') {
+    state.doDrawing = false
+  }
+  // 设置只允许绘制一个
+  // let canvasObj = state.canvas.getObjects();
+  // if(canvasObj.length >2){
+  //   state.canvas.remove(canvasObj[1])
+  // }
+}
+//鼠标移动过程中已经完成了绘制
+const mousemove = e => {
+  if (state.moveCount % 2 && !state.doDrawing) {
+    //减少绘制频率
+    return
+  }
+  state.moveCount++
+  let xy = e.pointer || transformMouse(e.e.offsetX, e.e.offsetY)
+  if (xy.y >= 0 && xy.x <= props.cWidth && xy.y >= 0 && xy.y <= props.cHeight) {
+    state.mouseTo.x = xy.x
+    state.mouseTo.y = xy.y
+    // 矩形
+    if (state.drawType == 'rectangle') {
+      if (state.mouseFrom.x < state.mouseTo.x && state.mouseFrom.y < state.mouseTo.y) {
+        drawing()
+      } else {
+        // clearAll();
+      }
+    }
+    if (state.drawType == 'polygon') {
+      if (state.activeLine && state.activeLine.class == 'line') {
+        let pointer = state.canvas.getPointer(e.e)
+        state.activeLine.set({ x2: pointer.x, y2: pointer.y })
+
+        let points = state.activeShape.get('points')
+        points[state.pointArray.length] = {
+          x: pointer.x,
+          y: pointer.y,
+          zIndex: 1
+        }
+        state.activeShape.set({
+          points: points
+        })
+        state.canvas.renderAll()
+      }
+      state.canvas.renderAll()
+    }
+  } else {
+    // clearAll();
+  }
+}
+// 绘制矩形
+const drawing = () => {
+  if (state.drawingObject) {
+    state.canvas.remove(state.drawingObject)
+  }
+  let canvasObject = null
+  let left = state.mouseFrom.x,
+    top = state.mouseFrom.y,
+    mouseFrom = state.mouseFrom,
+    mouseTo = state.mouseTo
+  let path =
+    'M ' +
+    mouseFrom.x +
+    ' ' +
+    mouseFrom.y +
+    ' L ' +
+    mouseTo.x +
+    ' ' +
+    mouseFrom.y +
+    ' L ' +
+    mouseTo.x +
+    ' ' +
+    mouseTo.y +
+    ' L ' +
+    mouseFrom.x +
+    ' ' +
+    mouseTo.y +
+    ' L ' +
+    mouseFrom.x +
+    ' ' +
+    mouseFrom.y +
+    ' z'
+  state.rectPath = path
+  canvasObject = new fabric.Path(path, {
+    left: left,
+    top: top,
+    stroke: state.color,
+    selectable: false,
+    strokeWidth: state.drawWidth,
+    fill: 'rgba(255, 255, 255, 0)',
+    hasControls: false
+  })
+  if (canvasObject) {
+    state.canvas.add(canvasObject)
+    state.drawingObject = canvasObject
+  }
+}
+// 绘制多边形开始,绘制多边形和其他图形不一样,需要单独处理
+// const drawPolygon = type => {
+//   state.drawType = type
+//   state.polygonMode = true
+//   //这里画的多边形,由顶点与线组成
+//   state.pointArray = [] // 顶点集合
+//   state.lineArray = [] //线集合
+//   state.canvas.isDrawingMode = false
+// }
+const addPoint = e => {
+  let random = Math.floor(Math.random() * 10000)
+  let id = new Date().getTime() + random
+  let circle = new fabric.Circle({
+    radius: 5,
+    fill: '#ffffff',
+    stroke: '#333333',
+    strokeWidth: 0.5,
+    left: (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
+    top: (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
+    selectable: false,
+    hasBorders: false,
+    hasControls: false,
+    originX: 'center',
+    originY: 'center',
+    id: id,
+    objectCaching: false
+  })
+  if (state.pointArray.length == 0) {
+    circle.set({
+      fill: '#00FFFF'
+    })
+  }
+  let points = [
+    (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
+    (e.pointer.y || e.e.layerY) / state.canvas.getZoom(),
+    (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
+    (e.pointer.y || e.e.layerY) / state.canvas.getZoom()
+  ]
+
+  state.line = new fabric.Line(points, {
+    strokeWidth: 2,
+    fill: '#999999',
+    stroke: '#999999',
+    class: 'line',
+    originX: 'center',
+    originY: 'center',
+    selectable: false,
+    hasBorders: false,
+    hasControls: false,
+    evented: false,
+
+    objectCaching: false
+  })
+  if (state.activeShape) {
+    let pos = state.canvas.getPointer(e.e)
+    let points = state.activeShape.get('points')
+    points.push({
+      x: pos.x,
+      y: pos.y
+    })
+    let polygon = new fabric.Polygon(points, {
+      stroke: '#333333',
+      strokeWidth: 1,
+      fill: '#cccccc',
+      opacity: 0.3,
+      selectable: false,
+      hasBorders: false,
+      hasControls: false,
+      evented: false,
+      objectCaching: false
+    })
+    state.canvas.remove(state.activeShape)
+    state.canvas.add(polygon)
+    state.activeShape = polygon
+    state.canvas.renderAll()
+  } else {
+    let polyPoint = [
+      {
+        x: (e.pointer.x || e.e.layerX) / state.canvas.getZoom(),
+        y: (e.pointer.y || e.e.layerY) / state.canvas.getZoom()
+      }
+    ]
+    let polygon = new fabric.Polygon(polyPoint, {
+      stroke: '#333333',
+      strokeWidth: 1,
+      fill: '#cccccc',
+      opacity: 0.3,
+      selectable: false,
+      hasBorders: false,
+      hasControls: false,
+      evented: false,
+      objectCaching: false
+    })
+    state.activeShape = polygon
+    state.canvas.add(polygon)
+  }
+  state.activeLine = state.line
+
+  state.pointArray.push(circle)
+  state.lineArray.push(state.line)
+  state.canvas.add(state.line)
+  state.canvas.add(circle)
+}
+// 绘制不规则
+const generatePolygon = () => {
+  // let points = clearPolygonLines()
+  // let polygon = new fabric.Polygon(sortPoints(points), {
+  //   stroke: state.color,
+  //   strokeWidth: state.drawWidth,
+  //   fill: 'rgba(255, 255, 255, 0)',
+  //   opacity: 1,
+  //   hasBorders: false,
+  //   hasControls: false,
+  //   evented: false
+  // })
+  // state.canvas.add(polygon)
+  let points = [{}]
+  console.log('state.pointArray', state.pointArray)
+
+  state.pointArray.map(point => {
+    points.push({
+      x: point.left,
+      y: point.top
+    })
+    state.canvas.remove(point)
+  })
+  state.lineArray.map(line => {
+    state.canvas.remove(line)
+  })
+  state.canvas.remove(state.activeShape).remove(state.activeLine)
+  let polygon = new fabric.Polygon(points, {
+    stroke: state.color,
+    strokeWidth: state.drawWidth,
+    fill: 'rgba(255, 255, 255, 0)',
+    opacity: 1,
+    hasBorders: true,
+    hasControls: false
+  })
+  state.canvas.add(polygon)
+  resetPolygon()
+}
+// 坐标转换
+const transformMouse = (mouseX, mouseY) => {
+  return { x: mouseX / 1, y: mouseY / 1 }
+}
+// 重置不规则四边形
+const resetPolygon = () => {
+  state.activeLine = null
+  state.activeShape = null
+  state.polygonMode = false
+  state.doDrawing = false
+  state.drawType = null
+}
+// 清除绘制四边形的四个坐标点
+const clearPolygonLines = () => {
+  let points = [{}]
+  state.pointArray.forEach(point => {
+    points.push({
+      x: point.left,
+      y: point.top
+    })
+    state.canvas.remove(point)
+  })
+  state.lineArray.forEach(line => {
+    state.canvas.remove(line)
+  })
+  state.canvas.remove(state.activeShape).remove(state.activeLine)
+  return points
+}
+// 撤销最后一次的操作
+const clearObjLast = () => {
+  let canvasObjs = state.canvas.getObjects()
+  let len = canvasObjs.length
+  if (len > 1) {
+    state.canvas.remove(canvasObjs[len - 1])
+  }
+}
+// 全部清除
+const clearAll = () => {
+  state.canvas.getObjects().forEach((element, index) => {
+    if (index > 0) {
+      state.canvas.remove(element)
+    }
+  })
+  clearPolygonLines()
+  resetPolygon()
+  state.drawType = 'rectangle'
+}
+const getPoint = pi => {
+  return Math.floor(pi / state.radio)
+}
+const getRealPoint = poi => {
+  let dx = Math.abs(state.imgPoint.x > poi.x ? state.imgPoint.x - poi.x : state.imgPoint.x + poi.x) * state.realRadioX
+  let dy = Math.abs(state.imgPoint.y > poi.y ? state.imgPoint.y - poi.y : state.imgPoint.y + poi.y) * state.realRadioY
+  let rdx = Math.abs(state.realPoint.x - dx)
+  let rdy = Math.abs(state.realPoint.y - dy)
+  let minX = Math.min(Math.floor(rdx), props.width)
+  let minY = Math.min(Math.floor(rdy), props.height)
+  return { x: minX, y: minY }
+}
+// 生成真实分辨率图片的坐标点
+const getData = () => {
+  let datas = [] as any
+  let marks = ['tl', 'tr', 'br', 'bl']
+  // if (state.lineArray.length > 0 && state.lineArray.length < 4) {
+  //   clearPolygonLines()
+  // }
+  state.canvas.getObjects().forEach((item, index) => {
+    if (index > 0) {
+      let aCoords = item.aCoords
+      let point = {}
+      if (item.points) {
+        item.points.forEach((item, idx) => {
+          // if (aCoords[marks[idx]]) {
+          //   aCoords[marks[idx]].x = item.x
+          //   aCoords[marks[idx]].y = item.y
+          // }
+          // if (idx > 0) {
+          point[idx] = getRealPoint({
+            x: item?.x,
+            y: item?.y
+          })
+          // }
+        })
+      } else {
+        marks.forEach(mark => {
+          let poi = {
+            x: getPoint(aCoords[mark].x),
+            y: getPoint(aCoords[mark].y)
+          }
+          point[mark] = getRealPoint(poi)
+        })
+      }
+
+      if (item.points && item.points.length === 2) {
+        delete point['br']
+        delete point['bl']
+      }
+      datas.push(point)
+    }
+  })
+  return datas
+}
+// 获取归一化数据比例
+const normalization = () => {
+  return {
+    realRadioX: state.realRadioX,
+    realRadioY: state.realRadioY
+  }
+}
+defineExpose({ drawType: state.drawType, clearObjLast, clearAll, getData, normalization })
+onMounted(() => {
+  loadInit()
+})
+</script>
+
+<style lang="scss" scoped>
+canvas {
+  border: 1px dashed black;
+}
+.loading-box {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 960px;
+  height: 540px;
+  font-size: 14px;
+  color: #ea5413;
+}
+.draw-btn-group {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  width: 960px;
+  margin-top: 10px;
+  .active {
+    .draw-rect {
+      background: #ff00ff;
+      border-color: #ff00ff;
+    }
+  }
+  .shape-box {
+    width: 120px;
+    text-align: left;
+  }
+  .shape-border {
+    display: block;
+    width: 80px;
+    height: 30px;
+    margin-right: 30px;
+    font-size: 12px;
+    text-align: center;
+  }
+  .shape-border-ti {
+    transform: skewX(-45deg);
+  }
+  .draw-icon {
+    display: inline-block;
+    width: 80px;
+    height: 30px;
+  }
+  .draw-rect {
+    width: 80px;
+    border-color: #333333;
+    border-style: solid;
+    border-width: 1px;
+  }
+  .draw-line {
+    position: relative;
+    top: -14px;
+    border-bottom: 2px solid #00ffff;
+  }
+}
+</style>

+ 65 - 6
src/views/demo/data/index.vue

@@ -1,6 +1,9 @@
 <template>
   <div class="table-box">
     <ProTable ref="proTable" :columns="columns" row-key="id" :request-api="listDataApi" :init-param="initParam">
+      <template #yuan="scope">
+        <el-image style="width: 100px" :src="getImageUrl(scope.row.url)" @click="markImg(scope.row)" />
+      </template>
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" v-auth="['demo:data:add']" :icon="CirclePlus" @click="openDialog(1, '数据新增')"> 新增 </el-button>
@@ -19,6 +22,7 @@
       </template>
       <!-- 表格操作 -->
       <template #operation="scope">
+        <!-- <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据标注', scope.row)"> 标注 </el-button> -->
         <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据编辑', scope.row)"> 编辑 </el-button>
         <el-button type="primary" link :icon="View" v-auth="['demo:data:query']" @click="openDialog(3, '数据查看', scope.row)"> 查看 </el-button>
         <el-button type="primary" link :icon="Delete" v-auth="['demo:data:remove']" @click="deleteData(scope.row)"> 删除 </el-button>
@@ -27,22 +31,23 @@
 
     <FormDialog ref="formDialogRef" />
     <ImportPicDataset ref="dialogRef" />
+    <ImgDetect ref="imgDetect" :img="cover" :area="area" :width="width" :height="height" @success="handleImgSuccess"></ImgDetect>
   </div>
 </template>
 
 <script setup lang="tsx" name="Data">
-import { ref, reactive } from 'vue'
+import { ref, reactive, toRefs } from 'vue'
 import { useHandleData } from '@/hooks/useHandleData'
 import { useDownload } from '@/hooks/useDownload'
-import { ElMessageBox } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import ProTable from '@/components/ProTable/index.vue'
-// import ImportExcel from '@/components/ImportExcel/index.vue'
 import FormDialog from '@/components/FormDialog/index.vue'
 import ImportPicDataset from '@/components/ImportPicDataset/index.vue'
-// import uploadImgs from '@/components/Upload/Imgs.vue'
-// import uploadImg from '@/components/Upload/Img.vue'
 import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
 import { Delete, EditPen, Download, Upload, View, CirclePlus } from '@element-plus/icons-vue'
+// import { fabric } from 'fabric'
+import { useDrawArea } from '@/utils/fabric'
+import ImgDetect from '../components/img-detect.vue'
 import {
   listDataApi,
   delDataApi,
@@ -53,9 +58,20 @@ import {
   exportDataApi,
   getDataApi
 } from '@/api/modules/demo/data'
+const imgDetect = ref()
+const state = reactive({
+  area: '' as string,
+  width: 1920 as number,
+  height: 1080 as number,
+  cover: ''
+})
+const { area, width, height, cover } = toRefs(state)
 
 // ProTable 实例
 const proTable = ref<ProTableInstance>()
+const getImageUrl = name => {
+  return new URL(name, import.meta.url).href
+}
 
 const initParam = reactive({ type: 1 })
 // 删除数据管理信息
@@ -64,6 +80,49 @@ const deleteData = async (params: any) => {
   proTable.value?.getTableList()
 }
 
+// 标注图片
+const markImg = data => {
+  state.cover = data.url
+  // area 代表后端的传来的标注数据
+  const area = []
+  if (state.cover != '') {
+    if (area.length != 0) {
+      handleImgSuccess(area)
+    }
+    imgDetect.value.visible = true
+  } else {
+    ElMessage.warning('缺失图像,暂时不能进行区域添加!')
+  }
+}
+const getList = () => {
+  useDrawArea({
+    src: state.cover,
+    width: state.width,
+    height: state.height,
+    area: state.area
+  })
+    .then(url => {
+      state.cover = url as string
+    })
+    .catch(error => {
+      console.log(error)
+    })
+}
+const handleImgSuccess = data => {
+  data.forEach(item => {
+    const area = item.split(';')
+    // try=tly blx=tlx brx=trx bry=bly
+    const tlx = Math.round(Number(area[0]) * 1920)
+    const tly = Math.round(Number(area[1]) * 1080)
+    const trx = tlx + Math.round(Number(area[2]) * 1920)
+    const bly = tly + Math.round(Number(area[3]) * 1080)
+    state.area += `${tlx};${tly};${trx};${tly};${trx};${bly};${tlx};${bly},`
+  })
+  // console.log('state.area', state.area)
+  state.area.slice(0, -1)
+  getList()
+}
+
 // 批量删除数据管理信息
 const batchDelete = async (ids: string[]) => {
   await useHandleData(delDataApi, ids, '删除所选数据信息')
@@ -114,7 +173,7 @@ const openDialog = async (type: number, title: string, row?: any) => {
 // 表格配置项
 const columns = reactive<ColumnProps<any>[]>([
   { type: 'selection', fixed: 'left', width: 70 },
-
+  { prop: 'yuan', label: '原图', width: 200 },
   {
     prop: 'name',
     label: '名称',

+ 31 - 59
src/views/taais/homePage/createTask.vue

@@ -6,6 +6,9 @@
         <h4 class="title2" v-if="pageIndex === 4">训练算法</h4>
         <div v-for="(item, index) in formItems" :key="index">
           <ProForm :items-options="item.items" :form-options="_options" :model="item.model" class="proform">
+            <template #tree="{ formModel }">
+              <el-tree style="max-width: 600px" v-model="formModel.tree" :props="defaultProps" :data="data1" show-checkbox />
+            </template>
             <template #transfer1="{ formModel }">
               <el-transfer filterable v-model="formModel.transfer1" :data="transferImg1">
                 <template #default="{ option }">
@@ -136,14 +139,13 @@ const _options: ComputedRef<ProForm.FormOptions> = computed(() => {
 
 let items: ProForm.ItemsOptions[] = [
   {
-    // formItemOptions: {
     label: '任务名称',
     prop: 'taskName',
     span: 12,
     show: () => {
       return pageIndex.value === 1 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -151,44 +153,36 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '任务选择',
-    prop: 'treeName',
+    prop: 'tree',
     show: () => {
       return pageIndex.value === 1 ? true : false
     },
-    // },
+
     compOptions: {
-      elTagName: 'tree',
-      enum: data1,
-      props: defaultProps,
-      style: 'max-width: 600px',
-      defaultExpandAll: true,
-      showCheckbox: true
+      elTagName: 'slot'
     }
   },
   {
-    // formItemOptions: {
     label: '选择训练数据',
     prop: 'transfer1',
     show: () => {
       return pageIndex.value === 2 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'slot',
       filterable: true
     }
   },
   {
-    // formItemOptions: {
     label: '增强算法',
     prop: 'enhanceAlgo',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumData,
@@ -198,28 +192,26 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '增强模型',
     prop: 'enhanceModel',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumDataModel
     }
   },
   {
-    // formItemOptions: {
     label: '参数1',
     prop: 'threeParameter1',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -227,14 +219,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数2',
     prop: 'threeParameter2',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -242,14 +233,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数3',
     prop: 'threeParameter3',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -257,14 +247,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数4',
     prop: 'threeParameter4',
     span: 14,
     show: () => {
       return pageIndex.value === 3 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -272,21 +261,19 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '训练算法',
     prop: 'trainAlgo',
     span: 14,
     show: () => {
       return pageIndex.value === 4 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumData
     }
   },
   {
-    // formItemOptions: {
     label: '预训练模型权重文件路径',
     prop: 'fourParameter1',
     labelWidth: 180,
@@ -294,7 +281,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 4 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -302,7 +289,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '模型结构配置文件路径',
     prop: 'fourParameter2',
     labelWidth: 180,
@@ -310,7 +296,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 4 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -318,7 +304,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '训练轮数',
     prop: 'fourParameter3',
     labelWidth: 180,
@@ -326,7 +311,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 4 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -334,7 +319,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '余弦学习率调度器',
     prop: 'fourParameter4',
     labelWidth: 180,
@@ -342,7 +326,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 4 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -350,27 +334,25 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '选择推理数据',
     prop: 'transfer2',
     show: () => {
       return pageIndex.value === 5 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'slot',
       filterable: true
     }
   },
   {
-    // formItemOptions: {
     label: '预处理算法',
     prop: 'pretreatmentAlgo',
     span: 14,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumData,
@@ -380,28 +362,26 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '预处理模型',
     prop: 'pretreatmentModel',
     span: 14,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumDataModel
     }
   },
   {
-    // formItemOptions: {
     label: '参数1',
     prop: 'sixParameter1',
     span: 12,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -409,14 +389,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数2',
     prop: 'sixParameter2',
     span: 12,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -424,14 +403,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数3',
     prop: 'sixParameter3',
     span: 12,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -439,14 +417,13 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '参数4',
     prop: 'sixParameter4',
     span: 12,
     show: () => {
       return pageIndex.value === 6 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -454,21 +431,19 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '推理算法',
     prop: 'inferAlgo',
     span: 14,
     show: () => {
       return pageIndex.value === 7 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'select',
       enum: enumData
     }
   },
   {
-    // formItemOptions: {
     label: '目标置信度阈值',
     prop: 'sevenParameter1',
     labelWidth: 180,
@@ -476,7 +451,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 7 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -484,7 +459,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '非极大值抑制的IoU阈值',
     prop: 'sevenParameter2',
     labelWidth: 180,
@@ -492,7 +466,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 7 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -500,7 +474,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '测试图片最大检测器数',
     prop: 'sevenParameter3',
     labelWidth: 180,
@@ -508,7 +481,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 7 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,
@@ -516,7 +489,6 @@ let items: ProForm.ItemsOptions[] = [
     }
   },
   {
-    // formItemOptions: {
     label: '使用半精度推理(FP16)',
     prop: 'sevenParameter4',
     labelWidth: 180,
@@ -524,7 +496,7 @@ let items: ProForm.ItemsOptions[] = [
     show: () => {
       return pageIndex.value === 7 ? true : false
     },
-    // },
+
     compOptions: {
       elTagName: 'input',
       clearable: true,

+ 2 - 2
src/views/taais/homePage/index.scss

@@ -75,7 +75,7 @@
   height: 520px;
   padding: 20px;
   margin: 0 auto;
-  margin-top: 100px;
+  margin-top: 50px;
   overflow: hidden;
   overflow-y: scroll;
 }
@@ -119,7 +119,7 @@
 
   width: 90%;
   padding: 3px 20px;
-  margin-top: 50px;
+  margin-top: 15px;
   .table {
     position: relative;
     width: 100%;

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 246 - 246
yarn.lock


Vissa filer visades inte eftersom för många filer har ändrats