6
0

annotated-image.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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. } else if (event.key === 'v') {
  160. navigator.clipboard.readText().then(text => {
  161. const obj = JSON.parse(text);
  162. if (obj.type === 'bounding-box' || obj.type === 'labeled-bounding-box') {
  163. if (this.data.type === 'video')
  164. obj.frame = this.video.frame;
  165. this.socket.post(this.mediaUrl, obj).then(this.update);
  166. console.log('paste', obj);
  167. }
  168. });
  169. }
  170. },
  171. update: function () {
  172. this.selectedBoundingBox = false;
  173. this.$emit('update', true);
  174. },
  175. videoPlay: function (value) {
  176. this.video.play = value;
  177. if (value)
  178. this.$refs.image.play();
  179. else
  180. this.$refs.image.pause();
  181. },
  182. videoJump: function (value) {
  183. // calculate difference
  184. value = Math.max(0, Math.min(this.data.frameCount, value));
  185. const diff = value - this.video.frame;
  186. // set timestamp
  187. this.$refs.image.currentTime += diff / this.data.fps;
  188. // get timestamp offset and correct if needed
  189. const currentFrame = this.$refs.image.currentTime * this.data.fps;
  190. if (diff < 0 && currentFrame < value) {
  191. this.$refs.image.currentTime += 0.5 / this.data.fps;
  192. } else if (diff > 0 && currentFrame > value + 0.5) {
  193. this.$refs.image.currentTime -= 0.5 / this.data.fps;
  194. }
  195. },
  196. videoProgress: function (event) {
  197. this.video.frame = Math.floor(event.target.currentTime * this.data.fps);
  198. },
  199. get: function (event) {
  200. if ('clientX' in event)
  201. return event;
  202. if ('touches' in event && event.touches.length > 0)
  203. return event.touches[0];
  204. if ('changedTouches' in event && event.changedTouches.length > 0)
  205. return event.changedTouches[0];
  206. },
  207. getX: function (event) {
  208. const bcr = this.$refs.image.getBoundingClientRect();
  209. return (this.get(event).clientX - bcr.left) / bcr.width;
  210. },
  211. getY: function (event) {
  212. const bcr = this.$refs.image.getBoundingClientRect();
  213. return (this.get(event).clientY - bcr.top) / bcr.height;
  214. },
  215. buildRectangle: function (x1, y1, x2, y2) {
  216. let lx, hx, ly, hy;
  217. if (this.fixed && 'offsetX' in this.fixed && 'offsetY' in this.fixed) {
  218. lx = Math.max(x2 + this.fixed.offsetX, 0);
  219. hx = Math.min(x2 + this.fixed.offsetX + this.fixed.w, 1);
  220. ly = Math.max(y2 + this.fixed.offsetY, 0);
  221. hy = Math.min(y2 + this.fixed.offsetY + this.fixed.h, 1);
  222. } else {
  223. lx = this.fixed && 'lx' in this.fixed ? this.fixed.lx : Math.max(Math.min(x1, x2), 0);
  224. hx = this.fixed && 'hx' in this.fixed ? this.fixed.hx : Math.min(Math.max(x1, x2), 1);
  225. ly = this.fixed && 'ly' in this.fixed ? this.fixed.ly : Math.max(Math.min(y1, y2), 0);
  226. hy = this.fixed && 'hy' in this.fixed ? this.fixed.hy : Math.min(Math.max(y1, y2), 1);
  227. }
  228. const rectangle = {
  229. x: lx,
  230. y: ly,
  231. w: hx - lx,
  232. h: hy - ly
  233. }
  234. if (this.data.type === 'video')
  235. rectangle.frame = this.video.frame;
  236. return rectangle;
  237. },
  238. press: function (event) {
  239. const x = this.getX(event);
  240. const y = this.getY(event);
  241. if (this.extremeClicking && this.start) {
  242. if (this.current) {
  243. const minX = Math.min(x, this.current.x);
  244. const maxX = Math.max(x, this.current.x + this.current.w);
  245. const minY = Math.min(y, this.current.y);
  246. const maxY = Math.max(y, this.current.y + this.current.h);
  247. this.current = this.buildRectangle(minX, minY, maxX, maxY);
  248. } else {
  249. this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
  250. }
  251. } else {
  252. this.start = {
  253. x: x,
  254. y: y
  255. };
  256. }
  257. },
  258. track: function (event) {
  259. if (this.start && !this.extremeClicking) {
  260. const x = this.getX(event);
  261. const y = this.getY(event);
  262. this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
  263. }
  264. },
  265. release: function () {
  266. if (this.start && !this.extremeClicking) {
  267. if (this.callback) {
  268. this.callback(this.current);
  269. this.update();
  270. } else {
  271. this.socket.post(this.mediaUrl, this.current)
  272. .then(this.update);
  273. }
  274. this.start = false;
  275. this.fixed = false;
  276. this.current = false;
  277. this.callback = false;
  278. }
  279. },
  280. move: function (event, position, callback) {
  281. this.callback = callback;
  282. const x = this.getX(event);
  283. const y = this.getY(event);
  284. this.start = {x: position.x, y: position.y};
  285. this.fixed = {w: position.w, h: position.h, offsetX: position.x - x, offsetY: position.y - y};
  286. },
  287. resize: function (position, mode, callback) {
  288. this.callback = callback;
  289. switch (mode) {
  290. case 'nw':
  291. this.start = {x: position.x + position.w, y: position.y + position.h};
  292. this.fixed = false;
  293. break;
  294. case 'ne':
  295. this.start = {x: position.x, y: position.y + position.h};
  296. this.fixed = false;
  297. break;
  298. case 'sw':
  299. this.start = {x: position.x + position.w, y: position.y};
  300. this.fixed = false;
  301. break;
  302. case 'se':
  303. this.start = {x: position.x, y: position.y};
  304. this.fixed = false;
  305. break;
  306. case 'nn':
  307. this.start = {x: position.x, y: position.y + position.h};
  308. this.fixed = {lx: position.x, hx: position.x + position.w};
  309. break;
  310. case 'ww':
  311. this.start = {x: position.x + position.w, y: position.y};
  312. this.fixed = {ly: position.y, hy: position.y + position.h};
  313. break;
  314. case 'ee':
  315. this.start = {x: position.x, y: position.y};
  316. this.fixed = {ly: position.y, hy: position.y + position.h};
  317. break;
  318. case 'ss':
  319. this.start = {x: position.x, y: position.y};
  320. this.fixed = {lx: position.x, hx: position.x + position.w};
  321. break;
  322. }
  323. }
  324. }
  325. }
  326. </script>
  327. <style scoped>
  328. img,
  329. video {
  330. max-width: 100%;
  331. max-height: 100%;
  332. }
  333. .video-control {
  334. position: absolute;
  335. bottom: 0;
  336. }
  337. </style>