圖像标注
-
-
-
- 需求
- 打點式實作思路
- 矩形框實作思路
- 右鍵彈出删除和添加備注菜單
- 删除選中内容
- 更新備注
-
-
項目中碰到一個需要對圖像标注的功能,查了好幾個插件,但是感覺用起來有點頗為複雜而且和自己需要的功能不完全一緻,于是就自己用canvas寫了一個簡單的:
需求
- 兩種标注方式,一種是打點式标注,一種是矩形框标注
- 打點式:單擊圖像打點,把每一個點用線連接配接起來,最後輕按兩下結束,形成一個閉環,并把所有坐标點記錄下來傳給後端
- 矩形框:和其他标注工具一樣,滑鼠随意拖動,形成一個矩形框,并把四個角的坐标點記錄下來傳給後端
- 在繪制的内容上右鍵高亮選中該内容并彈出菜單,可以删除,可以更新備注
- 使用打點式标注,滑鼠變畫筆;使用矩形框标注,滑鼠變十字
打點式實作思路
- 建立canvas 并繪制和圖檔一樣大的背景
- 使用
事件擷取點選的坐标,用click
方法繪制一個直徑4像素的小矩形,作為點(也可以做圓形)fillRect
- 把每次點選的坐标存儲起來,點下一個坐标時,使用
方法連線moveTo,lineTo
- 輕按兩下時把最後一個坐标和第一個連起來
注意: 輕按兩下事件會觸發兩次單擊事件,需要處理一下,網上找的方法:
click(){
clearTimeout(timer)
timer = setTimeout(()=>{
// xxx
},200)
}
dbclick(){
clearTimeout(timer)
// xxxx
}
打點式标注部分代碼
this.ctx.drawImage(this.image,0,0,this.w,this.h)
// 繪制矩形小點
this.ctx.fillStyle="#FF0000";
this.ctx.fillRect(e.offsetX - 2, e.offsetY - 2, 4, 4);
//連線 tempPointArr是儲存坐标點的數組,輕按兩下形成閉環後清空
this.ctx.beginPath()
this.ctx.strokeStyle= '#FF0000'
this.ctx.moveTo(this.tempPointArr[this.tempPointArr.length - 2].x,this.tempPointArr[this.tempPointArr.length - 2].y);
this.ctx.lineTo(this.tempPointArr[this.tempPointArr.length - 1].x,this.tempPointArr[this.tempPointArr.length - 1].y);
this.ctx.stroke();
矩形框實作思路
- 使用
mousedown
mousemove
來完成mouseup
- 在
事件擷取開始的坐标mousedown
-
事件使用mousemove
畫矩形,但要注意先清空,再畫新的,否則會導緻有很多矩形框strokeRect
-
事件清空開始坐标,完成一個矩形框的繪制mouseup
滑鼠按下時記錄目前canvas 内容,在每一次滑鼠拖動時先還原之前幹淨的canvas内容,再畫新的矩形,達到先清空再畫新矩形的目的
重點代碼:
mousedown(){
this.imgData = this.ctx.getImageData(0,0,this.w,this.h)
}
mousemove(){
this.ctx.putImageData(this.imgData,0,0)
}
矩形框标注部分代碼
mousedown(){
this.startPoint = {
x:e.offsetX,
y:e.offsetY
}
this.imgData = this.ctx.getImageData(0,0,this.w,this.h)
}
mousemove(){
this.ctx.putImageData(this.imgData,0,0)
this.ctx.beginPath()
this.ctx.fillStyle = "rgba(255,0,0,0.1)";
this.ctx.strokeStyle = '#FF0000';
this.ctx.strokeRect(this.startPoint.x,this.startPoint.y,e.offsetX - this.startPoint.x,e.offsetY - this.startPoint.y);
}
mouseup(){
this.startPoint = null
}
右鍵彈出删除和添加備注菜單
在有内容的地方才彈出,沒有内容的地方不彈
主要使用坐标點去判斷在不在某一個範圍内,之前把每一個繪制的内容都存儲成數組
思路:右鍵的坐标點必須滿足大于某個x點,小于另一個x點,大于某個y點,小于另一個y點,則表示在這個内容區域内
部分代碼
const index = this.pointData.findIndex((item)=>{
let leftX = false
let rightX = false
let topY = false
let bottomY = false
item.point.forEach(p=>{
if(e.offsetX > p.x){
leftX = true
}
if(e.offsetX < p.x){
rightX = true
}
if(e.offsetY < p.y){
bottomY = true
}
if(e.offsetY > p.y){
topY = true
}
})
return leftX && rightX && topY && bottomY
})
if(index > -1){
/**
xxxxxx
*/
this.fillRect(this.pointData[index].point) //給這個區域填充背景色,表示高亮
}
删除選中内容
思路:使用橡皮擦模拟實作删除效果,canvas下面要有多一張背景圖,否則的話會直接擦成白色;
使用完後再使用其他功能切記要把
屬性從
globalCompositeOperation
改回預設值
destination-out
source-over
部分代碼
this.ctx.globalCompositeOperation = 'destination-out' //重點
/**
...重新繪制一次要删除的内容
*/
this.ctx.globalCompositeOperation = 'source-over'
更新備注
使用
fillText
方法添加文本
在添加新的文本前先清除掉之前寫的文本,主要使用
方法
clearRect
部分代碼
let textW = this.ctx.measureText(this.textObj[this.selectedIndex]).width
this.ctx.clearRect(this.pointData[this.selectedIndex].point[0].x + 5,this.pointData[this.selectedIndex].point[0].y - 8 - 12 ,textW,13)
this.ctx.font="12px Arial"
this.ctx.fillStyle ='#FF0000'
this.ctx.fillText(this.note,this.pointData[this.selectedIndex].point[0].x + 5,this.pointData[this.selectedIndex].point[0].y - 8)
大緻思路到此為止
多次使用
geiImageData
putImageData
來實作儲存某一步的内容,再顯示;
也可以利用這個實作撤銷的功能
如果需要完整的代碼可以去git 上下載下傳一下,也希望有大佬可以幫忙優化一下代碼,感謝~~~
最後附上位址:
https://gitee.com/cuijinrong/project-gather/blob/master/src/components/marker.vue