annotation-overlay.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <template>
  2. <div class="annotation-overlay" :style="position"
  3. @touchstart="press" @touchmove="track" @touchend="release"
  4. @mousedown="press" @mousemove="track" @mouseup="release"
  5. @dragstart.stop>
  6. <annotation-box v-for="(box, index) in boundingBoxes"
  7. v-bind:key="box.identifier"
  8. ref="box"
  9. :box="box"
  10. :position="box.data"
  11. :draggable="interaction === 'move-box'"
  12. :deletable="interaction === 'remove-box'"
  13. :taggable="interaction === 'label-box' ? label : false"
  14. :confirmable="interaction === 'confirm-box'"
  15. :zoomable="interaction === 'zoom-box'"
  16. :croppable="interaction === 'info-box'"
  17. :labels="labels"
  18. :shine="crop && box.identifier === crop.identifier"
  19. @move="move"
  20. @resize="resize"
  21. @crop="$emit('crop', $event)"
  22. @zoom="zoomBox(index, $event)"/>
  23. <annotation-box v-if="current"
  24. :position="current"/>
  25. <div v-if="imageLabelResult"
  26. class="image-label"
  27. :class="{
  28. user: imageLabelResult.origin === 'user',
  29. pipeline: imageLabelResult.origin === 'pipeline'
  30. }">
  31. {{ imageLabelObject.name }}
  32. </div>
  33. </div>
  34. </template>
  35. <script>
  36. import AnnotationBox from "@/components/media/annotation-box";
  37. export default {
  38. name: "annotation-overlay",
  39. components: {AnnotationBox},
  40. props: [
  41. 'file',
  42. 'position',
  43. 'size',
  44. 'interaction',
  45. 'filter',
  46. 'label',
  47. 'video',
  48. 'results',
  49. 'labels',
  50. 'crop',
  51. 'zoom'
  52. ],
  53. data: function () {
  54. return {
  55. callback: false,
  56. start: false,
  57. fixed: false,
  58. current: false,
  59. mousedown: false,
  60. zoomed: false
  61. }
  62. },
  63. computed: {
  64. boundingBoxes: function () {
  65. return this.results.filter(b => {
  66. return b.type === 'bounding-box' && this.filter.includes(b.origin)
  67. && (!('frame' in b.data) || b.data.frame === this.video.frame)
  68. });
  69. },
  70. imageLabelResult: function () {
  71. for (let result of this.results)
  72. if (result.type === 'labeled-image')
  73. return result;
  74. return false;
  75. },
  76. imageLabelObject: function () {
  77. if (!this.imageLabelResult)
  78. return false;
  79. for (let label of this.labels)
  80. if (label.identifier === this.imageLabelResult.label)
  81. return label;
  82. return 'unknown label';
  83. }
  84. },
  85. methods: {
  86. getEventCoordinates: function (event) {
  87. if (!('clientX' in event)) {
  88. if ('touches' in event && event.touches.length > 0)
  89. event = event.touches[0];
  90. else if ('changedTouches' in event && event.changedTouches.length > 0)
  91. event = event.changedTouches[0];
  92. }
  93. return {
  94. x: Math.max(0, Math.min(1, (event.clientX - this.size.left) / this.size.width)),
  95. y: Math.max(0, Math.min(1, (event.clientY - this.size.top) / this.size.height))
  96. }
  97. },
  98. buildRectangle: function (start, end) {
  99. let lx, hx, ly, hy;
  100. if (this.fixed && 'offsetX' in this.fixed && 'offsetY' in this.fixed) {
  101. lx = Math.max(end.x + this.fixed.offsetX, 0);
  102. hx = Math.min(end.x + this.fixed.offsetX + this.fixed.w, 1);
  103. ly = Math.max(end.y + this.fixed.offsetY, 0);
  104. hy = Math.min(end.y + this.fixed.offsetY + this.fixed.h, 1);
  105. } else {
  106. lx = this.fixed && 'lx' in this.fixed ? this.fixed.lx : Math.max(Math.min(start.x, end.x), 0);
  107. hx = this.fixed && 'hx' in this.fixed ? this.fixed.hx : Math.min(Math.max(start.x, end.x), 1);
  108. ly = this.fixed && 'ly' in this.fixed ? this.fixed.ly : Math.max(Math.min(start.y, end.y), 0);
  109. hy = this.fixed && 'hy' in this.fixed ? this.fixed.hy : Math.min(Math.max(start.y, end.y), 1);
  110. }
  111. if (this.file.type === 'image') {
  112. return {
  113. x: lx,
  114. y: ly,
  115. w: hx - lx,
  116. h: hy - ly
  117. }
  118. } else {
  119. return {
  120. x: lx,
  121. y: ly,
  122. w: hx - lx,
  123. h: hy - ly,
  124. frame: this.video.frame
  125. }
  126. }
  127. },
  128. press: function (event) {
  129. this.mousedown = true;
  130. if (this.interaction === 'draw-box') {
  131. this.start = this.getEventCoordinates(event);
  132. }
  133. if (this.interaction === 'extreme-clicking') {
  134. const coordinates = this.getEventCoordinates(event);
  135. if (!this.start) {
  136. this.start = coordinates;
  137. } else if (!this.current) {
  138. this.current = this.buildRectangle(this.start, coordinates);
  139. } else {
  140. this.current = this.buildRectangle({
  141. x: Math.min(coordinates.x, this.current.x),
  142. y: Math.min(coordinates.y, this.current.y)
  143. }, {
  144. x: Math.max(coordinates.x, this.current.x + this.current.w),
  145. y: Math.max(coordinates.y, this.current.y + this.current.h)
  146. });
  147. }
  148. }
  149. if (this.interaction === 'label-box') {
  150. this.$root.socket.post(`/data/${this.file.identifier}/results`, {
  151. type: 'labeled-image',
  152. label: this.label.identifier
  153. });
  154. }
  155. if (this.interaction === 'confirm-box') {
  156. if (this.imageLabelResult) {
  157. this.$root.socket.post(`/results/${this.imageLabelResult.identifier}/confirm`, {
  158. confirm: true
  159. });
  160. }
  161. }
  162. if (this.interaction === 'remove-box') {
  163. if (this.imageLabelResult) {
  164. this.$root.socket.post(`/results/${this.imageLabelResult.identifier}/remove`, {
  165. remove: true
  166. });
  167. }
  168. }
  169. },
  170. track: function (event) {
  171. if (this.interaction === 'extreme-clicking') {
  172. if (this.mousedown)
  173. this.press(event);
  174. return;
  175. }
  176. if (this.start) {
  177. const coordinates = this.getEventCoordinates(event);
  178. this.current = this.buildRectangle(this.start, coordinates);
  179. }
  180. },
  181. release: function () {
  182. this.mousedown = false;
  183. if (this.interaction === 'extreme-clicking')
  184. return;
  185. if (this.current) {
  186. if (this.callback)
  187. this.callback(this.current);
  188. else
  189. // TODO then / error
  190. this.$root.socket.post(`/data/${this.file.identifier}/results`, {
  191. type: 'bounding-box',
  192. data: this.current
  193. });
  194. }
  195. this.callback = false;
  196. this.start = false;
  197. this.fixed = false;
  198. this.current = false;
  199. },
  200. move: function (event, position, callback) {
  201. if (this.interaction !== 'move-box')
  202. return;
  203. this.callback = callback;
  204. const coordinates = this.getEventCoordinates(event);
  205. this.start = position;
  206. this.fixed = {
  207. w: position.w,
  208. h: position.h,
  209. offsetX: position.x - coordinates.x,
  210. offsetY: position.y - coordinates.y
  211. };
  212. },
  213. resize: function (mode, position, callback) {
  214. if (this.interaction !== 'move-box')
  215. return;
  216. this.callback = callback;
  217. switch (mode) {
  218. case 'nw':
  219. this.start = {x: position.x + position.w, y: position.y + position.h};
  220. this.fixed = false;
  221. break;
  222. case 'ne':
  223. this.start = {x: position.x, y: position.y + position.h};
  224. this.fixed = false;
  225. break;
  226. case 'sw':
  227. this.start = {x: position.x + position.w, y: position.y};
  228. this.fixed = false;
  229. break;
  230. case 'se':
  231. this.start = {x: position.x, y: position.y};
  232. this.fixed = false;
  233. break;
  234. case 'nn':
  235. this.start = {x: position.x, y: position.y + position.h};
  236. this.fixed = {lx: position.x, hx: position.x + position.w};
  237. break;
  238. case 'ww':
  239. this.start = {x: position.x + position.w, y: position.y};
  240. this.fixed = {ly: position.y, hy: position.y + position.h};
  241. break;
  242. case 'ee':
  243. this.start = {x: position.x, y: position.y};
  244. this.fixed = {ly: position.y, hy: position.y + position.h};
  245. break;
  246. case 'ss':
  247. this.start = {x: position.x, y: position.y};
  248. this.fixed = {lx: position.x, hx: position.x + position.w};
  249. break;
  250. }
  251. },
  252. zoomBox: function (index) {
  253. if (this.boundingBoxes.length === 0) {
  254. this.zoomed = false;
  255. return;
  256. }
  257. if (index < 0) {
  258. index = this.boundingBoxes.length - 1;
  259. } else if (index >= this.boundingBoxes.length) {
  260. index = 0;
  261. }
  262. this.zoomed = index;
  263. this.$emit('zoom', this.boundingBoxes[index].data);
  264. },
  265. prevZoom: function () {
  266. this.zoomBox(this.zoomed - 1);
  267. },
  268. nextZoom: function () {
  269. this.zoomBox(this.zoomed + 1);
  270. }
  271. },
  272. watch: {
  273. interaction: function (newVal, oldVal) {
  274. if (oldVal === 'extreme-clicking')
  275. this.release();
  276. }
  277. }
  278. }
  279. </script>
  280. <style scoped>
  281. .annotation-overlay {
  282. position: absolute;
  283. }
  284. .image-label {
  285. position: absolute;
  286. right: 0;
  287. bottom: 0;
  288. padding: 0.7rem 1rem;
  289. color: whitesmoke;
  290. border-top-left-radius: 1rem;
  291. }
  292. .image-label.user {
  293. background-color: rgba(255, 0, 0, 0.4);
  294. }
  295. .image-label.pipeline {
  296. background-color: rgba(0, 0, 255, 0.4);
  297. }
  298. </style>