You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

347 lines
10 KiB

<template>
<v-stage
ref="stageRef"
:config="{ ...configStage, dragBoundFunc }"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onMouseMove"
>
<v-layer>
<base-image :image-url="imageUrl" @loaded="imageLoaded" />
</v-layer>
<v-layer>
<v-rectangle
v-for="rect in annotationsToDraw"
:key="rect.id"
:rect="rect"
:color="rect.getColor(labels)"
:highlight-id="highlightId"
:max-width="imageSize.width"
:max-height="imageSize.height"
:scale="scale"
@dragend="onDragEnd"
@transformend="handleTransformEnd"
/>
<v-transformer
ref="transformerRef"
:config="{
rotateEnabled: false,
flipEnabled: false,
keepRatio: false,
boundBoxFunc
}"
/>
</v-layer>
</v-stage>
</template>
<script lang="ts">
import Konva from 'konva'
import { Box } from 'konva/lib/shapes/Transformer.d'
import type { PropType } from 'vue'
import Vue from 'vue'
import VRectangle from './VRectangle.vue'
import BaseImage from '@/components/tasks/image/BaseImage.vue'
import Rectangle from '@/domain/models/tasks/boundingbox/Rectangle'
import RectangleProps from '@/domain/models/tasks/boundingbox/RectangleProps'
import LabelProps from '@/domain/models/tasks/shared/LabelProps'
import { inverseTransform, transform } from '@/domain/models/tasks/shared/Scaler'
export default Vue.extend({
name: 'VBoundingBox',
components: {
BaseImage,
VRectangle
},
props: {
imageUrl: {
type: String,
required: true
},
labels: {
type: Array as PropType<LabelProps[]>,
required: true
},
rectangles: {
type: Array as PropType<RectangleProps[]>,
required: true
},
selectedLabel: {
type: Object as PropType<LabelProps | undefined>,
default: undefined
},
scale: {
type: Number,
required: true
},
highlightId: {
type: String,
required: false,
default: 'uuid'
}
},
data() {
return {
selectedRectangle: null as string | null,
newRectangle: null as Rectangle | null,
imageSize: {
width: 0,
height: 0
},
configStage: {
width: window.innerWidth,
height: window.innerHeight,
draggable: true
},
stage: {} as Konva.Stage
}
},
computed: {
annotations(): Rectangle[] {
return this.rectangles.map((r) => new Rectangle(r.label, r.x, r.y, r.width, r.height, r.id))
},
transformer(): Konva.Transformer {
return (this.$refs.transformerRef as unknown as Konva.TransformerConfig).getNode()
},
annotationsToDraw(): Rectangle[] {
if (this.newRectangle) {
return this.annotations.concat(this.newRectangle)
}
return this.annotations
}
},
watch: {
scale() {
this.setZoom()
},
imageUrl() {
this.selectedRectangle = null
this.updateTransformer()
}
},
mounted() {
document.addEventListener('keydown', this.removeRectangle)
window.addEventListener('resize', this.setZoom)
this.stage = (this.$refs.stageRef as unknown as Konva.StageConfig).getNode()
},
beforeDestroy() {
document.removeEventListener('keydown', this.removeRectangle)
window.removeEventListener('resize', this.setZoom)
},
methods: {
updateTransformer() {
// here we need to manually attach or detach Transformer node
const selectedNode = this.stage.findOne(`#${this.selectedRectangle}`)
// do nothing if selected node is already attached
if (selectedNode === this.transformer.getNode()) {
return
}
if (selectedNode) {
// attach to another node
this.transformer.nodes([selectedNode])
} else {
// remove transformer
this.transformer.nodes([])
}
this.$emit('select-rectangle', this.selectedRectangle)
},
boundBoxFunc(_: Box, newBoundBox: Box) {
const box = { ...newBoundBox }
const { x: stageX = 0, y: stageY = 0 } = this.stage.attrs
box.x = transform(box.x, stageX, this.scale)
box.y = transform(box.y, stageY, this.scale)
box.width = transform(box.width, 0, this.scale)
box.height = transform(box.height, 0, this.scale)
if (box.x < 0) {
box.width += box.x
box.x = 0
}
if (box.y < 0) {
box.height += box.y
box.y = 0
}
if (box.x + box.width > this.imageSize.width) box.width = this.imageSize.width - box.x
if (box.y + box.height > this.imageSize.height) box.height = this.imageSize.height - box.y
box.x = inverseTransform(box.x, stageX, this.scale)
box.y = inverseTransform(box.y, stageY, this.scale)
box.width = inverseTransform(box.width, 0, this.scale)
box.height = inverseTransform(box.height, 0, this.scale)
return box
},
onMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
if (e.target instanceof Konva.Image) {
// while new polygon is creating, prevent to select polygon.
const clickedOutsideOfRectangle = !!this.selectedRectangle
this.selectedRectangle = null
this.updateTransformer()
if (clickedOutsideOfRectangle) {
return
}
}
// prevent multiple event.
if (e.target instanceof HTMLCanvasElement) {
return
}
// clicked on transformer - do nothing
const clickedOnTransformer = e.target.getParent().className === 'Transformer'
if (clickedOnTransformer) {
return
}
// prevent to create circle on Polygon.
if (e.target instanceof Konva.Rect) {
const rectId = e.target.id()
const rect = this.annotations.find((r) => r.id === rectId)
if (rect && !this.selectedRectangle) {
this.selectedRectangle = rectId
} else {
this.selectedRectangle = null
}
this.updateTransformer()
return
}
if (!this.newRectangle && !!this.selectedLabel) {
this.configStage.draggable = false
const pos = this.stage.getPointerPosition()!
const { x: stageX = 0, y: stageY = 0 } = this.stage.attrs
pos.x = transform(pos.x, stageX, this.scale)
pos.y = transform(pos.y, stageY, this.scale)
this.newRectangle = new Rectangle(this.selectedLabel.id, pos.x, pos.y, 0, 0)
}
},
onMouseUp() {
if (this.newRectangle && this.newRectangle.exists()) {
const pos = this.stage.getPointerPosition()!
const { x: stageX = 0, y: stageY = 0 } = this.stage.attrs
pos.x = transform(pos.x, stageX, this.scale)
pos.y = transform(pos.y, stageY, this.scale)
let x = this.newRectangle.x
let y = this.newRectangle.y
let width = pos.x - x
let height = pos.y - y
if (width < 0) {
x += width
width = -width
}
if (height < 0) {
y += height
height = -height
}
const annotationToAdd = this.newRectangle.transform(x, y, width, height)
this.newRectangle = null
this.configStage.draggable = true
this.$emit('add-rectangle', annotationToAdd.toProps())
}
},
onMouseMove() {
if (this.newRectangle) {
const sx = this.newRectangle.x
const sy = this.newRectangle.y
const pos = this.stage.getPointerPosition()!
const { x: stageX = 0, y: stageY = 0 } = this.stage.attrs
pos.x = transform(pos.x, stageX, this.scale)
pos.y = transform(pos.y, stageY, this.scale)
this.newRectangle = this.newRectangle.transform(sx, sy, pos.x - sx, pos.y - sy)
}
},
handleTransformEnd(e: Konva.KonvaEventObject<MouseEvent>) {
// shape is transformed, let us save new attrs back to the node
// find element in our state
const rect = this.annotations.find((r) => r.id === this.selectedRectangle)
// update the state
if (rect) {
const x = e.target.x()
const y = e.target.y()
const width = rect.width * e.target.scaleX()
const height = rect.height * e.target.scaleY()
const newRect = rect.transform(x, y, width, height)
e.target.scaleX(1)
e.target.scaleY(1)
this.$emit('update-rectangle', newRect.toProps())
}
},
onDragEnd(rect: Rectangle) {
this.$emit('update-rectangle', rect.toProps())
},
removeRectangle(e: KeyboardEvent) {
if (e.key === 'Backspace' || e.key === 'Delete') {
if (this.selectedRectangle !== null) {
this.$emit('delete-rectangle', this.selectedRectangle)
this.selectedRectangle = null
this.updateTransformer()
}
}
},
dragBoundFunc(pos: { x: number; y: number }) {
const { stageX = 0, stageY = 0 } = this.stage.attrs
let x = pos.x - stageX
let y = pos.y - stageY
const paddingX = this.imageSize.width * this.scale - this.configStage.width
const paddingY = this.imageSize.height * this.scale - this.configStage.height
if (paddingX + x < 0) x = -paddingX
if (paddingY + y < 0) y = -paddingY
if (this.configStage.width + paddingX + x > this.imageSize.width * this.scale) x = 0
if (this.configStage.height + paddingY + y > this.imageSize.height * this.scale) y = 0
x += stageX
y += stageY
return { x, y }
},
imageLoaded(width: number, height: number) {
const maxScale = this.$el.clientWidth / width
const imageIsSmallerThanContainer = maxScale > 1
this.imageSize.width = width
this.imageSize.height = height
if (imageIsSmallerThanContainer) {
this.configStage.width = width
this.configStage.height = height
this.stage.scale({ x: 1, y: 1 })
this.$emit('update-scale', 1)
} else {
this.configStage.width = width * maxScale
this.configStage.height = height * maxScale
this.stage.scale({ x: maxScale, y: maxScale })
this.$emit('update-scale', maxScale)
}
this.stage.draw()
},
setZoom() {
if (this.scale < 0) {
return
}
const maxScale = this.$el.clientWidth / this.imageSize.width
this.stage.scale({ x: this.scale, y: this.scale })
if (this.scale <= maxScale) {
this.configStage.width = this.imageSize.width * this.scale
} else {
this.configStage.width = this.imageSize.width * maxScale
}
this.configStage.height = this.imageSize.height * this.scale
this.$el.setAttribute('style', `min-height: ${this.configStage.height}px`)
}
}
})
</script>