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.

286 lines
8.3 KiB

  1. <template>
  2. <v-stage ref="stageRef" :config="{ ...configStage, dragBoundFunc }" @mousedown="onMouseDown">
  3. <v-layer>
  4. <base-image :image-url="imageUrl" @loaded="imageLoaded" />
  5. </v-layer>
  6. <v-layer>
  7. <v-editing-region
  8. v-if="!!editingPolygon"
  9. :polygon="editingPolygon"
  10. :color="editingPolygon.getColor(labels)"
  11. :max-width="imageSize.width"
  12. :max-height="imageSize.height"
  13. :scale="scale"
  14. @drag-end-polygon="translatePolygon"
  15. @mouse-over-start-point="onMouseOverStartPoint"
  16. @mouse-out-start-point="onMouseOutStartPoint"
  17. @drag-point="movePoint"
  18. @double-click-point="removePoint"
  19. />
  20. </v-layer>
  21. <v-layer>
  22. <v-region
  23. v-for="(polygon, index) in readonlyPolygons"
  24. :key="`polygon-${index}`"
  25. :polygon="polygon"
  26. :highlight-id="highlightId"
  27. :is-selected="polygon.id === selectedPolygon"
  28. :selected-point="selectedPoint"
  29. :max-width="imageSize.width"
  30. :max-height="imageSize.height"
  31. :scale="scale"
  32. :color="polygon.getColor(labels)"
  33. @click-point="updateSelectedPoint"
  34. @click-line="insertPoint"
  35. @click-polygon="updateSelectedPolygon"
  36. @drag-end-polygon="translatePolygon"
  37. @drag-end-point="movePoint"
  38. @double-click-point="removePoint"
  39. />
  40. </v-layer>
  41. </v-stage>
  42. </template>
  43. <script lang="ts">
  44. import Konva from 'konva'
  45. import type { PropType } from 'vue'
  46. import Vue from 'vue'
  47. import VEditingRegion from './VEditingRegion.vue'
  48. import VRegion from './VRegion.vue'
  49. import BaseImage from '@/components/tasks/image/BaseImage.vue'
  50. import Polygon from '@/domain/models/tasks/segmentation/Polygon'
  51. import PolygonProps from '@/domain/models/tasks/segmentation/PolygonProps'
  52. import LabelProps from '@/domain/models/tasks/shared/LabelProps'
  53. import { transform } from '@/domain/models/tasks/shared/Scaler'
  54. export default Vue.extend({
  55. name: 'VSegmentation',
  56. components: {
  57. VEditingRegion,
  58. VRegion,
  59. BaseImage
  60. },
  61. props: {
  62. imageUrl: {
  63. type: String,
  64. required: true
  65. },
  66. labels: {
  67. type: Array as () => LabelProps[],
  68. required: true
  69. },
  70. polygons: {
  71. type: Array as () => PolygonProps[],
  72. required: true
  73. },
  74. selectedLabel: {
  75. type: Object as PropType<LabelProps | undefined>,
  76. default: undefined
  77. },
  78. scale: {
  79. type: Number,
  80. default: 1
  81. },
  82. highlightId: {
  83. type: String,
  84. required: false,
  85. default: 'uuid'
  86. }
  87. },
  88. data() {
  89. return {
  90. imageSize: {
  91. width: 0,
  92. height: 0
  93. },
  94. editingPolygon: null as Polygon | null,
  95. isMouseOverStartPoint: false,
  96. selectedPolygon: null as string | null,
  97. selectedPoint: -1,
  98. configStage: {
  99. width: window.innerWidth,
  100. height: window.innerHeight,
  101. draggable: true
  102. },
  103. stage: {} as Konva.Stage
  104. }
  105. },
  106. computed: {
  107. readonlyPolygons() {
  108. return this.polygons.map((p: PolygonProps) => new Polygon(p.label, p.points, p.id))
  109. }
  110. },
  111. watch: {
  112. scale() {
  113. this.setZoom()
  114. }
  115. },
  116. mounted() {
  117. document.addEventListener('keydown', this.removePointOrPolygon)
  118. window.addEventListener('resize', this.setZoom)
  119. this.stage = (this.$refs.stageRef as unknown as Konva.StageConfig).getNode()
  120. },
  121. beforeDestroy() {
  122. document.removeEventListener('keydown', this.removePointOrPolygon)
  123. window.removeEventListener('resize', this.setZoom)
  124. },
  125. methods: {
  126. onMouseDown(e: Konva.KonvaEventObject<MouseEvent>) {
  127. if (e.target instanceof Konva.Image) {
  128. // while new polygon is creating, prevent to select polygon.
  129. this.selectedPolygon = null
  130. }
  131. if (!this.selectedLabel) {
  132. return
  133. }
  134. // prevent multiple event.
  135. if (e.target instanceof HTMLCanvasElement) {
  136. return
  137. }
  138. // prevent to create circle on Polygon.
  139. if (e.target instanceof Konva.Line) {
  140. return
  141. }
  142. if (e.target instanceof Konva.Circle) {
  143. if (
  144. this.isMouseOverStartPoint &&
  145. this.editingPolygon &&
  146. this.editingPolygon.canBeClosed()
  147. ) {
  148. this.$emit('add-polygon', this.editingPolygon.toProps())
  149. this.editingPolygon = null
  150. }
  151. return
  152. }
  153. const pos = this.stage.getPointerPosition()!
  154. const { x: stageX = 0, y: stageY = 0 } = this.stage.attrs
  155. pos.x = transform(pos.x, stageX, this.scale)
  156. pos.y = transform(pos.y, stageY, this.scale)
  157. if (!this.editingPolygon) {
  158. this.editingPolygon = new Polygon(this.selectedLabel.id, [pos.x, pos.y])
  159. } else {
  160. this.editingPolygon.addPoint(pos.x, pos.y)
  161. }
  162. },
  163. onMouseOverStartPoint() {
  164. this.isMouseOverStartPoint = true
  165. },
  166. onMouseOutStartPoint() {
  167. this.isMouseOverStartPoint = false
  168. },
  169. updateSelectedPolygon(polygon: Polygon) {
  170. this.selectedPoint = -1
  171. if (this.selectedPolygon === polygon.id) {
  172. this.selectedPolygon = null
  173. } else {
  174. this.selectedPolygon = polygon.id
  175. }
  176. this.$emit('select-polygon', this.selectedPolygon)
  177. },
  178. updateSelectedPoint(point: number) {
  179. if (this.selectedPoint === point) {
  180. this.selectedPoint = -1
  181. } else {
  182. this.selectedPoint = point
  183. }
  184. },
  185. insertPoint(polygon: Polygon, index: number, x: number, y: number) {
  186. polygon.insertPoint(x, y, index)
  187. this.$emit('update-polygon', polygon.toProps())
  188. },
  189. translatePolygon(polygon: Polygon, dx: number, dy: number) {
  190. polygon.translate(dx, dy)
  191. this.$emit('update-polygon', polygon.toProps())
  192. },
  193. movePoint(polygon: Polygon, index: number, x: number, y: number) {
  194. polygon.movePoint(index, x, y)
  195. this.$emit('update-polygon', polygon.toProps())
  196. },
  197. removePoint(polygon: Polygon, index: number) {
  198. polygon.removePoint(index)
  199. this.$emit('update-polygon', polygon.toProps())
  200. },
  201. removePointOrPolygon(e: KeyboardEvent) {
  202. if (e.key === 'Backspace' || e.key === 'Delete') {
  203. if (this.selectedPoint !== -1) {
  204. const polygon = this.readonlyPolygons.find((p) => p.id === this.selectedPolygon)
  205. this.removePoint(polygon!, this.selectedPoint)
  206. this.selectedPoint = -1
  207. return
  208. }
  209. if (this.selectedPolygon !== null) {
  210. this.$emit('delete-polygon', this.selectedPolygon)
  211. this.selectedPolygon = null
  212. }
  213. }
  214. },
  215. dragBoundFunc(pos: { x: number; y: number }) {
  216. const { stageX = 0, stageY = 0 } = this.stage.attrs
  217. let x = pos.x - stageX
  218. let y = pos.y - stageY
  219. const paddingX = this.imageSize.width * this.scale - this.configStage.width
  220. const paddingY = this.imageSize.height * this.scale - this.configStage.height
  221. if (paddingX + x < 0) x = -paddingX
  222. if (paddingY + y < 0) y = -paddingY
  223. if (this.configStage.width + paddingX + x > this.imageSize.width * this.scale) x = 0
  224. if (this.configStage.height + paddingY + y > this.imageSize.height * this.scale) y = 0
  225. x += stageX
  226. y += stageY
  227. return { x, y }
  228. },
  229. imageLoaded(width: number, height: number) {
  230. const maxScale = this.$el.clientWidth / width
  231. const imageIsSmallerThanContainer = maxScale > 1
  232. this.imageSize.width = width
  233. this.imageSize.height = height
  234. if (imageIsSmallerThanContainer) {
  235. this.configStage.width = width
  236. this.configStage.height = height
  237. this.stage.scale({ x: 1, y: 1 })
  238. this.$emit('update-scale', 1)
  239. } else {
  240. this.configStage.width = width * maxScale
  241. this.configStage.height = height * maxScale
  242. this.stage.scale({ x: maxScale, y: maxScale })
  243. this.$emit('update-scale', maxScale)
  244. }
  245. this.stage.draw()
  246. },
  247. setZoom() {
  248. if (this.scale < 0) {
  249. return
  250. }
  251. const maxScale = this.$el.clientWidth / this.imageSize.width
  252. this.stage.scale({ x: this.scale, y: this.scale })
  253. if (this.scale <= maxScale) {
  254. this.configStage.width = this.imageSize.width * this.scale
  255. } else {
  256. this.configStage.width = this.imageSize.width * maxScale
  257. }
  258. this.configStage.height = this.imageSize.height * this.scale
  259. this.$el.setAttribute('style', `min-height: ${this.configStage.height}px`)
  260. }
  261. }
  262. })
  263. </script>