annotated-image.vue 10 KB

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