1. 安装fabric库
npm install fabric
2. 需要进行图片框选的react的js文件如下:
import React, { useEffect, useRef, useState } from 'react'
const [imgWrapWidth, setImgWrapWidth] = useState(0)
const [imgWrapHeight, setImgWrapHeight] = useState(0)
const [imgWidth, setImgWidth] = useState(0)
const [imgHeight, setImgHeight] = useState(0)
const canvasWrapRef = useRef(null)
const nativeImgRef = useRef(null)
const canvasImgRef = useRef(null)
const initImgDraw = async () => {
const imgWidth = nativeImgRef.current.naturalWidth
const imgHeight = nativeImgRef.current.naturalHeight
const wrapWidth = canvasWrapRef.current.offsetWidth
const wrapHeight = (wrapWidth * imgHeight) / imgWidth
setImgWidth(imgWidth)
setImgHeight(imgHeight)
setImgWrapWidth(wrapWidth)
setImgWrapHeight(wrapHeight)
canvasImgRef.current =new fabric.Canvas('canvasImg', {
width: wrapWidth,
height: wrapHeight,
selection: true,
selectionColor: 'transparent',
selectionBorderColor: '#00B7EE',
skipTargetFind: false, // 允许选中
strokeWidth: 2,
})
const backImage = await getBase64Image(store.currentMarkedInfo?.img_detail, wrapWidth, wrapHeight)
canvasImgRef.current.setBackgroundImage(
backImage,
canvasImgRef.current.renderAll.bind(canvasImgRef.current),
)
canvasImgRef.current.on('mouse:down', canvasMouseDown)
canvasImgRef.current.on('mouse:up', canvasMouseUp)
}
const getBase64Image = (src, width, height) => {
return new Promise(resolve => {
const img = new Image()
img.crossOrigin = ''
img.src = src
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height)
const ext = img.src.substring(img.src.lastIndexOf('.') + 1).toLowerCase()
const dataURL = canvas.toDataURL('image/' + ext)
resolve(dataURL)
}
})
}
const canvasMouseDown = (e) => {
activeObject = canvasImgRef.current.getActiveObject()
Object.assign(downPoint, JSON.parse(JSON.stringify(e.absolutePointer)))
setDownPoint(downPoint)
if (activeObject) return
}
const canvasMouseUp = (e) => {
Object.assign(upPoint, JSON.parse(JSON.stringify(e.absolutePointer)))
setUpPoint(upPoint)
if (activeObject) return
createRect()
}
const createRect = (color) => {
if (JSON.stringify(downPoint) === JSON.stringify(upPoint)) {
return
}
let top = Math.min(downPoint.y, upPoint.y)
let left = Math.min(downPoint.x, upPoint.x)
let width = Math.abs(downPoint.x - upPoint.x)
let height = Math.abs(downPoint.y - upPoint.y)
let storkColor = color ? color : store.colorList[Object.keys(historyObject).length]
// 矩形对象
const rect = new fabric.Rect({
top,
left,
width,
height,
fill: 'transparent',
stroke: storkColor,
strokeWidth: 2
})
historyObject[Object.keys(historyObject).length] = rect
setHistoryObject(historyObject)
canvasImgRef.current.add(rect)
if (!color) {
historyLabel[Object.keys(historyLabel).length] = {label: ''}
setHistoryLabel(historyLabel)
store.currentEditRectId = historyObject.length
store.changeIsMarking(true)
}
}
const changeRectColor = () => {
abortDraw('isReDraw')
const labelKeys = Object.keys(historyLabel)
const currentLabel = historyLabel[labelKeys[labelKeys.length - 1]]?.label
const index = store.colorSet.indexOf(currentLabel)
createLabeledRect(store.colorList[index], currentLabel)
}
const createLabeledRect = (color, currentLabel) => {
if (JSON.stringify(downPoint) === JSON.stringify(upPoint)) {
return
}
let top = Math.min(downPoint.y, upPoint.y)
let left = Math.min(downPoint.x, upPoint.x)
let width = Math.abs(downPoint.x - upPoint.x)
let height = Math.abs(downPoint.y - upPoint.y)
let storkColor = color ? color : store.colorList[Object.keys(historyObject).length]
// 矩形对象
const labeledRect = new LabeledRect({
top,
left,
width,
height,
fill: 'transparent',
stroke: storkColor,
strokeWidth: 2,
label: currentLabel,
fontColor: storkColor
})
historyObject[Object.keys(historyObject).length] = labeledRect
setHistoryObject(historyObject)
canvasImgRef.current.add(labeledRect)
}
const abortDraw = (isReDraw) => {
const keyList = Object.keys(historyObject)
if (keyList.length) {
canvasImgRef.current.remove(historyObject[keyList.length - 1])
delete historyObject[keyList.length - 1]
setHistoryObject(historyObject)
if (isReDraw !== 'isReDraw') {
const currentLabel = JSON.parse(JSON.stringify(historyLabel[keyList.length - 1]))?.label
delete historyLabel[keyList.length - 1]
setHistoryLabel(historyLabel)
store.changeLabelList('pop', {label: currentLabel, opt_id: currentLabel})
}
}
}
const saveDraw = async () => {
const rectList = canvasImgRef.current.toJSON(['canvasImg'])
if (!rectList.objects.length) return
const imgData = canvasImgRef.current.toDataURL('image/png', ['canvasImg'])
const rectLabelList = rectList.objects.map((item, index) => {
const idx = store.colorSet.indexOf(historyLabel[index].label)
return {
color: store.colorList[idx],
label_name: historyLabel[index].label,
x: (item.left + 0.5 * item.width) / imgWrapWidth,
y: (item.top + 0.5 * item.height) / imgWrapHeight,
w: item.width / imgWrapWidth,
h: item.height / imgWrapHeight
}
})
const requetData = {
img_name: store.currentMarkedInfo.img_detail,
base64_img: imgData,
width: imgWidth,
height: imgHeight,
label_list: rectLabelList
}
await store.fetchToImgMark(requetData)
store.changeCurrentMarkedInfo({})
store.changeModelShow(false)
if (store.currentMarkTab === 1) {
store.setUnMarkedQuery({
page: 1
})
store.fetchUnMarkedList()
} else {
store.setMarkedQuery({
page: 1
})
store.fetchMarkedList()
}
}
3. 需要进行框选的store文件如下:
import { observable, computed } from 'mobx'
import { http } from 'libs'
import { message } from 'antd'
import history from 'libs/history'
class Store {
@observable isModelShow = false
@observable currentMarkedInfo = {}
@observable labelList = {}
@observable isMarking = false
@observable eidtContentInfo = {}
@observable currentEditRectId = ''
@observable colorList = [
"#FFC353",
"#0000FF",
"#FC8746",
"#00F4C3",
"#00B7EE",
"#238E23",
"#C71585",
"#EA5F61",
"#5F9EA0",
"#00FFFF",
"#FFA500",
"#FF8C00",
"#FF0000",
"#FF1493",
"#4B0082",
"#6A5ACD",
"#1E90FF",
"#F0E68C",
"#8B008B",
"#8B4513",
"#D2B48C",
"#F5DEB3",
"#808000",
"#556B2F",
"#90EE90",
"#008B8B",
]
@observable colorSet = []
changeIsOperatedUnMarked = (val) => {
this.isOperatedUnMarked = val
}
changeUnMarkedPage = (page) => {
this.unMarkedQuery.page = page
}
changeMarkedPage = (page) => {
this.markedQuery.page = page
}
changeCurrentMarkedInfo = (markedInfo) => {
this.currentMarkedInfo = markedInfo
}
changeModelShow = (val) => {
this.isModelShow = val
}
changeIsMarking = (val) => [ this.isMarking = val ]
changeLabelList = (opt ,val) => {
switch (opt) {
case 'push':
this.labelList[val.label] ? this.labelList[val.label].push({...val}) : this.labelList[val.label] = [{...val}]
if (this.colorSet.indexOf(val.label) === -1) {
this.colorSet.push(val.label)
}
break
case 'pop':
if (this.labelList[val.label]) {
this.labelList[val.label].forEach((item, index) => {
if (item.opt_id === val.opt_id) {
if (JSON.parse(JSON.stringify(this.labelList[val.label])).length === 1) {
const currentIndex = this.colorSet.indexOf(val.label)
this.colorSet.splice(currentIndex, currentIndex + 1)
}
this.labelList[val.label].splice(index, index + 1)
}
})
}
break
case 'delete':
this.labelList = {}
this.colorSet = []
break
default:
}
}
}
export default new Store()
4. 用到的labelRect.js如下:
import { fabric } from 'fabric'
const LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
this.set('label', options.label || '')
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
})
},
_render: function(ctx, options) {
this.callSuper('_render', ctx)
ctx.font = '18px Helvetica'
ctx.fillStyle = this.fontColor
ctx.fillText(this.label, -this.width/2, -this.height/2 - 5)
}
})
export default LabeledRect
最终效果

主要实现思路
- 先根据图片尺寸绘制fabric画布;
- 通过fabric创建矩形对象;
- 创建完矩形对象之后给矩形对象编辑一个标签,自定义,这里是(New);
- 编辑确认后再擦除当前绘制的矩形框,绘制带标签的矩形框;
- 注意管理好矩形框列表和标签列表,一一对应;
- 最后转化成自己想要的json对象;