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.
 
 
 
 
 
 

285 lines
8.3 KiB

<template>
<v-stage ref="stageRef" :config="{ ...configStage, dragBoundFunc }" @mousedown="onMouseDown">
<v-layer>
<base-image :image-url="imageUrl" @loaded="imageLoaded" />
</v-layer>
<v-layer>
<v-editing-region
v-if="!!editingPolygon"
:polygon="editingPolygon"
:color="editingPolygon.getColor(labels)"
:max-width="imageSize.width"
:max-height="imageSize.height"
:scale="scale"
@drag-end-polygon="translatePolygon"
@mouse-over-start-point="onMouseOverStartPoint"
@mouse-out-start-point="onMouseOutStartPoint"
@drag-point="movePoint"
@double-click-point="removePoint"
/>
</v-layer>
<v-layer>
<v-region
v-for="(polygon, index) in readonlyPolygons"
:key="`polygon-${index}`"
:polygon="polygon"
:highlight-id="highlightId"
:is-selected="polygon.id === selectedPolygon"
:selected-point="selectedPoint"
:max-width="imageSize.width"
:max-height="imageSize.height"
:scale="scale"
:color="polygon.getColor(labels)"
@click-point="updateSelectedPoint"
@click-line="insertPoint"
@click-polygon="updateSelectedPolygon"
@drag-end-polygon="translatePolygon"
@drag-end-point="movePoint"
@double-click-point="removePoint"
/>
</v-layer>
</v-stage>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import Konva from 'konva'
import Polygon from '@/domain/models/tasks/segmentation/Polygon'
import PolygonProps from '@/domain/models/tasks/segmentation/PolygonProps'
import LabelProps from '@/domain/models/tasks/shared/LabelProps'
import BaseImage from '@/components/tasks/image/BaseImage.vue'
import { transform } from '@/domain/models/tasks/shared/Scaler'
import VEditingRegion from './VEditingRegion.vue'
import VRegion from './VRegion.vue'
export default Vue.extend({
name: 'VSegmentation',
components: {
VEditingRegion,
VRegion,
BaseImage
},
props: {
imageUrl: {
type: String,
required: true
},
labels: {
type: Array as () => LabelProps[],
required: true
},
polygons: {
type: Array as () => PolygonProps[],
required: true
},
selectedLabel: {
type: Object as PropType<LabelProps | undefined>,
default: undefined
},
scale: {
type: Number,
default: 1
},
highlightId: {
type: String,
required: false,
default: 'uuid'
}
},
data() {
return {
imageSize: {
width: 0,
height: 0
},
editingPolygon: null as Polygon | null,
isMouseOverStartPoint: false,
selectedPolygon: null as string | null,
selectedPoint: -1,
configStage: {
width: window.innerWidth,
height: window.innerHeight,
draggable: true
},
stage: {} as Konva.Stage
}
},
computed: {
readonlyPolygons() {
return this.polygons.map((p: PolygonProps) => new Polygon(p.label, p.points, p.id))
}
},
watch: {
scale() {
this.setZoom()
}
},
mounted() {
document.addEventListener('keydown', this.removePointOrPolygon)
window.addEventListener('resize', this.setZoom)
this.stage = (this.$refs.stageRef as unknown as Konva.StageConfig).getNode()
},
beforeDestroy() {
document.removeEventListener('keydown', this.removePointOrPolygon)
window.removeEventListener('resize', this.setZoom)
},
methods: {
onMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
if (e.target instanceof Konva.Image) {
// while new polygon is creating, prevent to select polygon.
this.selectedPolygon = null
}
if (!this.selectedLabel) {
return
}
// prevent multiple event.
if (e.target instanceof HTMLCanvasElement) {
return
}
// prevent to create circle on Polygon.
if (e.target instanceof Konva.Line) {
return
}
if (e.target instanceof Konva.Circle) {
if (
this.isMouseOverStartPoint &&
this.editingPolygon &&
this.editingPolygon.canBeClosed()
) {
this.$emit('add-polygon', this.editingPolygon.toProps())
this.editingPolygon = null
}
return
}
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)
if (!this.editingPolygon) {
this.editingPolygon = new Polygon(this.selectedLabel.id, [pos.x, pos.y])
} else {
this.editingPolygon.addPoint(pos.x, pos.y)
}
},
onMouseOverStartPoint() {
this.isMouseOverStartPoint = true
},
onMouseOutStartPoint() {
this.isMouseOverStartPoint = false
},
updateSelectedPolygon(polygon: Polygon) {
this.selectedPoint = -1
if (this.selectedPolygon === polygon.id) {
this.selectedPolygon = null
} else {
this.selectedPolygon = polygon.id
}
this.$emit('select-polygon', this.selectedPolygon)
},
updateSelectedPoint(point: number) {
if (this.selectedPoint === point) {
this.selectedPoint = -1
} else {
this.selectedPoint = point
}
},
insertPoint(polygon: Polygon, index: number, x: number, y: number) {
polygon.insertPoint(x, y, index)
this.$emit('update-polygon', polygon.toProps())
},
translatePolygon(polygon: Polygon, dx: number, dy: number) {
polygon.translate(dx, dy)
this.$emit('update-polygon', polygon.toProps())
},
movePoint(polygon: Polygon, index: number, x: number, y: number) {
polygon.movePoint(index, x, y)
this.$emit('update-polygon', polygon.toProps())
},
removePoint(polygon: Polygon, index: number) {
polygon.removePoint(index)
this.$emit('update-polygon', polygon.toProps())
},
removePointOrPolygon(e: KeyboardEvent) {
if (e.key === 'Backspace' || e.key === 'Delete') {
if (this.selectedPoint !== -1) {
const polygon = this.readonlyPolygons.find((p) => p.id === this.selectedPolygon)
this.removePoint(polygon!, this.selectedPoint)
this.selectedPoint = -1
return
}
if (this.selectedPolygon !== null) {
this.$emit('delete-polygon', this.selectedPolygon)
this.selectedPolygon = null
}
}
},
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>