6
0

annotated-image.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. <template>
  2. <div class="annotated-image" v-on="events">
  3. <img v-if="data.type === 'image'"
  4. alt="media" ref="image" draggable="false"
  5. :src="mediaUrl" :srcset="mediaUrlSet" :sizes="mediaUrlSizes"
  6. v-on:load="resizeEvent" v-on:loadedmetadata="resizeEvent" v-on:loadeddata="resizeEvent"/>
  7. <template v-if="data.type === 'video'">
  8. <video ref="image" draggable="false"
  9. preload="auto" :src="mediaUrl"
  10. v-on:touchstart.prevent v-on:touchmove.prevent v-on:touchend.prevent
  11. v-on:mousedown.prevent v-on:mousemove.prevent v-on:mouseup.prevent
  12. v-on:click.prevent v-on:dragstart.prevent
  13. v-on:load="resizeEvent" v-on:canplay="resizeEvent" v-on:loadeddata="resizeEvent"
  14. v-on:timeupdate="videoProgress">
  15. </video>
  16. <video-control :video="video" :data="data"
  17. v-on:touchstart.prevent.stop v-on:touchmove.prevent.stop v-on:touchend.prevent.stop
  18. v-on:mousedown.prevent.stop v-on:mousemove.prevent.stop v-on:mouseup.prevent.stop
  19. v-on:click.prevent.stop v-on:dragstart.prevent.stop
  20. @play="videoPlay" @jump="videoJump"/>
  21. </template>
  22. <!-- <div style="position: absolute; top: 0.5rem; left: 0.5rem">{{ extremeClicking }}</div> -->
  23. <annotation-box v-if="current"
  24. :image="image"
  25. :supports="project.model.supports"
  26. :position="current"/>
  27. <annotation-box v-for="(result, index) in predictions"
  28. :type="result.origin"
  29. :key="index"
  30. :id="result.id"
  31. :image="image"
  32. :position="result"
  33. :socket="socket"
  34. :box-url="mediaUrl + '/' + result.id"
  35. :labels="project.labels"
  36. :supports="project.model.supports"
  37. :selected="selectedBoundingBox !== false && index === selectedBoundingBox"
  38. @move="move"
  39. @resize="resize"
  40. @update="update"/>
  41. </div>
  42. </template>
  43. <script>
  44. import AnnotationBox from "@/components/media/annotation-box";
  45. import VideoControl from "@/components/media/video-control";
  46. export default {
  47. name: "annotated-image",
  48. components: {VideoControl, AnnotationBox},
  49. props: ['data', 'project', 'socket', 'filter', 'extremeClicking'],
  50. mounted: function () {
  51. window.addEventListener('resize', this.resizeEvent);
  52. this.resizeEvent();
  53. // TODO dirty workaround
  54. // this works around a bug which is probably caused by partly loaded
  55. // videos and their received dimensions
  56. this.watcher = setInterval(this.resizeEvent, 400);
  57. },
  58. created: function () {
  59. window.addEventListener('keypress', this.keypressEvent);
  60. },
  61. destroyed() {
  62. window.removeEventListener('resize', this.resizeEvent);
  63. window.removeEventListener('keypress', this.keypressEvent);
  64. clearInterval(this.watcher);
  65. },
  66. watch: {
  67. data: function () {
  68. this.current = false;
  69. this.video = {
  70. frame: 0,
  71. play: false
  72. };
  73. this.resizeEvent();
  74. },
  75. extremeClicking: function (newValue) {
  76. if (!newValue && this.current) {
  77. this.release();
  78. }
  79. }
  80. },
  81. data: function () {
  82. return {
  83. sizes: [600, 800, 1200, 1600, 2000, 3000],
  84. watcher: 0,
  85. image: {
  86. left: 0,
  87. top: 0,
  88. width: 0,
  89. height: 0
  90. },
  91. video: {
  92. frame: 0,
  93. play: false
  94. },
  95. start: false,
  96. fixed: false,
  97. current: false,
  98. callback: false,
  99. selectedBoundingBox: false
  100. }
  101. },
  102. computed: {
  103. mediaUrl: function () {
  104. return this.socket.media(this.project.id, this.data.id);
  105. },
  106. mediaUrlSet: function () {
  107. return this.sizes.map(e => this.mediaUrl + '/' + e + ' ' + e + 'w').join(',');
  108. },
  109. mediaUrlSizes: function () {
  110. return this.sizes.map(e => '(max-width: ' + e + 'px) ' + e + 'px').join(',');
  111. },
  112. predictions: function () {
  113. const predictions = Object.keys(this.data.predictionResults)
  114. .map(k => this.data.predictionResults[k])
  115. .filter(k => 'x' in k)
  116. .filter(k => !this.filter || k.origin === this.filter);
  117. if (this.data.type === 'video')
  118. return predictions.filter(k => k.frame === this.video.frame);
  119. else
  120. return predictions;
  121. },
  122. events: function () {
  123. if (this.project.model.supports.includes('bounding-boxes')
  124. || this.project.model.supports.includes('labeled-bounding-boxes')) {
  125. return {
  126. 'touchstart': this.press,
  127. 'touchmove': this.track,
  128. 'touchend': this.release,
  129. 'mousedown': this.press,
  130. 'mousemove': this.track,
  131. 'mouseup': this.release,
  132. 'dragstart': e => e.stopPropagation()
  133. }
  134. } else {
  135. return {}
  136. }
  137. }
  138. },
  139. methods: {
  140. resizeEvent: function () {
  141. if (!this.$refs.image)
  142. return;
  143. const element = this.$refs.image.getBoundingClientRect();
  144. const parent = this.$refs.image.parentElement.getBoundingClientRect();
  145. this.image.left = element.x - parent.x;
  146. this.image.top = element.y - parent.y;
  147. this.image.width = element.width;
  148. this.image.height = element.height;
  149. },
  150. keypressEvent: function (event) {
  151. if (event.key === 'w') {
  152. if (this.selectedBoundingBox === false || this.selectedBoundingBox === this.predictions.length - 1)
  153. this.selectedBoundingBox = -1;
  154. this.selectedBoundingBox += 1;
  155. } else if (event.key === 's') {
  156. if (this.selectedBoundingBox === false || this.selectedBoundingBox === 0)
  157. this.selectedBoundingBox = this.predictions.length;
  158. this.selectedBoundingBox -= 1;
  159. }
  160. },
  161. update: function () {
  162. this.selectedBoundingBox = false;
  163. this.$emit('update', true);
  164. },
  165. videoPlay: function (value) {
  166. this.video.play = value;
  167. if (value)
  168. this.$refs.image.play();
  169. else
  170. this.$refs.image.pause();
  171. },
  172. videoJump: function (value) {
  173. value = Math.max(0, Math.min(this.data.frameCount, value));
  174. const diff = value - this.video.frame;
  175. this.$refs.image.currentTime += diff / this.data.fps;
  176. },
  177. videoProgress: function (event) {
  178. this.video.frame = Math.floor(event.target.currentTime * this.data.fps);
  179. },
  180. get: function (event) {
  181. if ('clientX' in event)
  182. return event;
  183. if ('touches' in event && event.touches.length > 0)
  184. return event.touches[0];
  185. if ('changedTouches' in event && event.changedTouches.length > 0)
  186. return event.changedTouches[0];
  187. },
  188. getX: function (event) {
  189. const bcr = this.$refs.image.getBoundingClientRect();
  190. return (this.get(event).clientX - bcr.left) / bcr.width;
  191. },
  192. getY: function (event) {
  193. const bcr = this.$refs.image.getBoundingClientRect();
  194. return (this.get(event).clientY - bcr.top) / bcr.height;
  195. },
  196. buildRectangle: function (x1, y1, x2, y2) {
  197. let lx, hx, ly, hy;
  198. if (this.fixed && 'offsetX' in this.fixed && 'offsetY' in this.fixed) {
  199. lx = Math.max(x2 + this.fixed.offsetX, 0);
  200. hx = Math.min(x2 + this.fixed.offsetX + this.fixed.w, 1);
  201. ly = Math.max(y2 + this.fixed.offsetY, 0);
  202. hy = Math.min(y2 + this.fixed.offsetY + this.fixed.h, 1);
  203. } else {
  204. lx = this.fixed && 'lx' in this.fixed ? this.fixed.lx : Math.max(Math.min(x1, x2), 0);
  205. hx = this.fixed && 'hx' in this.fixed ? this.fixed.hx : Math.min(Math.max(x1, x2), 1);
  206. ly = this.fixed && 'ly' in this.fixed ? this.fixed.ly : Math.max(Math.min(y1, y2), 0);
  207. hy = this.fixed && 'hy' in this.fixed ? this.fixed.hy : Math.min(Math.max(y1, y2), 1);
  208. }
  209. const rectangle = {
  210. x: lx,
  211. y: ly,
  212. w: hx - lx,
  213. h: hy - ly
  214. }
  215. if (this.data.type === 'video')
  216. rectangle.frame = this.video.frame;
  217. return rectangle;
  218. },
  219. press: function (event) {
  220. const x = this.getX(event);
  221. const y = this.getY(event);
  222. if (this.extremeClicking && this.start) {
  223. if (this.current) {
  224. const minX = Math.min(x, this.current.x);
  225. const maxX = Math.max(x, this.current.x + this.current.w);
  226. const minY = Math.min(y, this.current.y);
  227. const maxY = Math.max(y, this.current.y + this.current.h);
  228. this.current = this.buildRectangle(minX, minY, maxX, maxY);
  229. } else {
  230. this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
  231. }
  232. } else {
  233. this.start = {
  234. x: x,
  235. y: y
  236. };
  237. }
  238. },
  239. track: function (event) {
  240. if (this.start && !this.extremeClicking) {
  241. const x = this.getX(event);
  242. const y = this.getY(event);
  243. this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
  244. }
  245. },
  246. release: function () {
  247. if (this.start && !this.extremeClicking) {
  248. if (this.callback)
  249. this.callback(this.current);
  250. else
  251. this.socket.post(this.mediaUrl, this.current);
  252. this.start = false;
  253. this.fixed = false;
  254. this.current = false;
  255. this.callback = false;
  256. this.update();
  257. }
  258. },
  259. move: function (event, position, callback) {
  260. this.callback = callback;
  261. const x = this.getX(event);
  262. const y = this.getY(event);
  263. this.start = {x: position.x, y: position.y};
  264. this.fixed = {w: position.w, h: position.h, offsetX: position.x - x, offsetY: position.y - y};
  265. },
  266. resize: function (position, mode, callback) {
  267. this.callback = callback;
  268. switch (mode) {
  269. case 'nw':
  270. this.start = {x: position.x + position.w, y: position.y + position.h};
  271. this.fixed = false;
  272. break;
  273. case 'ne':
  274. this.start = {x: position.x, y: position.y + position.h};
  275. this.fixed = false;
  276. break;
  277. case 'sw':
  278. this.start = {x: position.x + position.w, y: position.y};
  279. this.fixed = false;
  280. break;
  281. case 'se':
  282. this.start = {x: position.x, y: position.y};
  283. this.fixed = false;
  284. break;
  285. case 'nn':
  286. this.start = {x: position.x, y: position.y + position.h};
  287. this.fixed = {lx: position.x, hx: position.x + position.w};
  288. break;
  289. case 'ww':
  290. this.start = {x: position.x + position.w, y: position.y};
  291. this.fixed = {ly: position.y, hy: position.y + position.h};
  292. break;
  293. case 'ee':
  294. this.start = {x: position.x, y: position.y};
  295. this.fixed = {ly: position.y, hy: position.y + position.h};
  296. break;
  297. case 'ss':
  298. this.start = {x: position.x, y: position.y};
  299. this.fixed = {lx: position.x, hx: position.x + position.w};
  300. break;
  301. }
  302. }
  303. }
  304. }
  305. </script>
  306. <style scoped>
  307. img,
  308. video {
  309. max-width: 100%;
  310. max-height: 100%;
  311. }
  312. .video-control {
  313. position: absolute;
  314. bottom: 0;
  315. }
  316. </style>