annotated-image.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <template>
  2. <div class="annotated-image" ref="root">
  3. <template v-if="src">
  4. <options-bar :file="current"
  5. :interaction="interaction"
  6. @interaction="interaction = $event"
  7. :filter="filter"
  8. @filter="filter = $event"
  9. :label="label"
  10. @label="label = $event"
  11. :labels="labels"
  12. :zoomBox="zoomBox"
  13. :infoBox="infoBox !== false"
  14. @infoBox="infoBox = $event; interaction=false"
  15. @unzoom="zoomBox=false; interaction=false"
  16. @prevzoom="$refs.overlay.prevZoom()"
  17. @nextzoom="$refs.overlay.nextZoom()"/>
  18. <div class="media">
  19. <!-- image -->
  20. <img v-if="current.type === 'image'"
  21. ref="media" :src="src" alt="media"
  22. :style="cropPosition"
  23. v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change"
  24. v-on:transitionend="resize">
  25. <!-- video -->
  26. <template v-if="current.type === 'video'">
  27. <div class="video-player">
  28. <video ref="media" :src="src"
  29. v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change" v-on:canplay="change"
  30. v-on:timeupdate="videoProgress"/>
  31. </div>
  32. <video-control :file="current"
  33. :video="video"
  34. :results="results"
  35. @play="videoPlay"
  36. @jump="videoJump"/>
  37. </template>
  38. <!-- overlay -->
  39. <annotation-overlay ref="overlay"
  40. :file="current"
  41. :position="overlayPosition"
  42. :size="image"
  43. :interaction="interaction"
  44. :filter="filter"
  45. :label="label"
  46. :video="video"
  47. :results="results"
  48. :labels="labels"
  49. :crop="infoBox"
  50. @crop="infoBox = $event"
  51. :zoom="zoomBox"
  52. @zoom="zoomBox = $event"/>
  53. </div>
  54. <cropped-image v-if="infoBox !== false"
  55. :labels="labels"
  56. :file="current"
  57. :box="infoBox"
  58. @close="infoBox=false"/>
  59. </template>
  60. </div>
  61. </template>
  62. <script>
  63. import AnnotationOverlay from "@/components/media/annotation-overlay";
  64. import VideoControl from "@/components/media/video-control";
  65. import OptionsBar from "@/components/media/options-bar";
  66. import CroppedImage from "@/components/media/cropped-image";
  67. export default {
  68. name: "annotated-image",
  69. components: {OptionsBar, VideoControl, AnnotationOverlay, CroppedImage},
  70. props: ['current'],
  71. mounted: function () {
  72. // add resize listener
  73. window.addEventListener('resize', this.resize);
  74. this.interval = setInterval(this.resize, 1000);
  75. this.resize();
  76. // add result listener
  77. this.$root.socket.on('create-result', this.createResult);
  78. this.$root.socket.on('edit-result', this.editResult);
  79. this.$root.socket.on('remove-result', this.removeResult);
  80. // add label listener
  81. this.getLabels();
  82. this.$root.socket.on('connect', this.getLabels);
  83. this.$root.socket.on('create-label', this.addLabelToList);
  84. this.$root.socket.on('remove-label', this.removeLabelFromList);
  85. this.$root.socket.on('edit-label', this.editLabelInList);
  86. // get model
  87. this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
  88. .then(response => response.json())
  89. .then(model => {
  90. this.supported.labeledImages = model.supports.includes('labeled-images');
  91. this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
  92. this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
  93. || model.supports.includes('bounding-boxes');
  94. });
  95. },
  96. destroyed: function () {
  97. window.removeEventListener('resize', this.resize);
  98. clearInterval(this.interval);
  99. this.$root.socket.off('create-result', this.createResult);
  100. this.$root.socket.off('edit-result', this.editResult);
  101. this.$root.socket.off('remove-result', this.removeResult);
  102. this.$root.socket.off('connect', this.getLabels);
  103. this.$root.socket.off('create-label', this.addLabelToList);
  104. this.$root.socket.off('remove-label', this.removeLabelFromList);
  105. this.$root.socket.off('edit-label', this.editLabelInList);
  106. },
  107. data: function () {
  108. return {
  109. interval: false,
  110. container: {
  111. top: 0,
  112. left: 0,
  113. width: 0,
  114. height: 0,
  115. },
  116. image: {
  117. top: 0,
  118. left: 0,
  119. width: 0,
  120. height: 0
  121. },
  122. video: {
  123. frame: 0,
  124. play: false
  125. },
  126. infoBox: false,
  127. zoomBox: false,
  128. supported: {
  129. labeledImages: false,
  130. boundingBoxes: false,
  131. labeledBoundingBoxes: false,
  132. },
  133. interaction: 'draw-box',
  134. filter: ['user', 'pipeline'],
  135. label: false,
  136. results: [],
  137. labels: []
  138. }
  139. },
  140. computed: {
  141. src: function () {
  142. if (!this.container.width || !this.container.height)
  143. return '';
  144. if (this.current.type === 'video')
  145. return this.$root.socket.media(this.current);
  146. const width = Math.ceil(this.container.width / 400) * 400;
  147. const height = Math.ceil(this.container.height / 400) * 400;
  148. return this.$root.socket.media(this.current, width, height);
  149. },
  150. overlayPosition: function () {
  151. return {
  152. top: (this.image.top - this.container.top) + 'px',
  153. left: (this.image.left - this.container.left) + 'px',
  154. width: this.image.width + 'px',
  155. height: this.image.height + 'px'
  156. }
  157. },
  158. cropPosition: function () {
  159. if (!this.zoomBox)
  160. return {
  161. transform: ``,
  162. };
  163. const posX = 0.5 - (this.zoomBox.x + this.zoomBox.w / 2);
  164. const posY = 0.5 - (this.zoomBox.y + this.zoomBox.h / 2);
  165. const factor = 0.75 / Math.max(this.zoomBox.w, this.zoomBox.h);
  166. // use a transition to use the transitionend event to recalculate box positions
  167. return {
  168. transform: `scale(${factor}) translateX(${posX * 100}%) translateY(${posY * 100}%)`
  169. };
  170. }
  171. },
  172. methods: {
  173. resize: function () {
  174. const rect = this.$refs.root.getBoundingClientRect();
  175. this.container.top = rect.top;
  176. this.container.left = rect.left;
  177. this.container.width = rect.width;
  178. this.container.height = rect.height;
  179. this.change();
  180. },
  181. change: function () {
  182. if (!this.$refs.media)
  183. return;
  184. const rect = this.$refs.media.getBoundingClientRect();
  185. this.image.top = rect.top;
  186. this.image.left = rect.left;
  187. this.image.width = rect.width;
  188. this.image.height = rect.height;
  189. },
  190. videoProgress: function (event) {
  191. this.video.frame = Math.floor(event.target.currentTime * this.current.fps);
  192. },
  193. videoPlay: function (value) {
  194. this.video.play = value;
  195. if (value)
  196. this.$refs.media.play();
  197. else
  198. this.$refs.media.pause();
  199. },
  200. videoJump: function (value) {
  201. // calculate difference
  202. value = Math.max(0, Math.min(this.current.frames, value));
  203. const diff = value - this.video.frame;
  204. // set timestamp
  205. this.$refs.media.currentTime += diff / this.current.fps;
  206. // get timestamp offset and correct if needed
  207. const currentFrame = this.$refs.media.currentTime * this.current.fps;
  208. if (diff < 0 && currentFrame < value) {
  209. this.$refs.media.currentTime += 0.5 / this.current.fps;
  210. } else if (diff > 0 && currentFrame > value + 0.5) {
  211. this.$refs.media.currentTime -= 0.5 / this.current.fps;
  212. }
  213. },
  214. createResult: function (result) {
  215. if (result['file_id'] !== this.current.identifier)
  216. return;
  217. for (let r of this.results)
  218. if (r.identifier === result.identifier)
  219. return;
  220. this.results.push(result);
  221. },
  222. removeResult: function (result) {
  223. for (let i = 0; i < this.results.length; i++) {
  224. if (this.results[i].identifier === result.identifier) {
  225. this.results.splice(i, 1);
  226. return;
  227. }
  228. }
  229. },
  230. editResult: function (result) {
  231. if (this.infoBox && result.identifier === this.infoBox.identifier)
  232. this.infoBox = result;
  233. for (let i = 0; i < this.results.length; i++) {
  234. if (this.results[i].identifier === result.identifier) {
  235. this.$set(this.results, i, result);
  236. return;
  237. }
  238. }
  239. },
  240. getLabels: function () {
  241. this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels`)
  242. .then(response => response.json())
  243. .then(labels => {
  244. this.labels = [];
  245. labels.forEach(this.addLabelToList);
  246. });
  247. },
  248. addLabelToList: function (label) {
  249. if (label['project_id'] !== this.$root.project.identifier)
  250. return;
  251. for (let l of this.labels)
  252. if (l.identifier === label.identifier)
  253. return;
  254. this.labels.push(label);
  255. },
  256. removeLabelFromList: function (label) {
  257. for (let i = 0; i < this.labels.length; i++) {
  258. if (this.labels[i].identifier === label.identifier) {
  259. this.labels.splice(i, 1);
  260. return;
  261. }
  262. }
  263. },
  264. editLabelInList: function (label) {
  265. for (let i = 0; i < this.labels.length; i++) {
  266. if (this.labels[i].identifier === label.identifier) {
  267. this.$set(this.labels, i, label);
  268. return;
  269. }
  270. }
  271. }
  272. },
  273. watch: {
  274. current: {
  275. immediate: true,
  276. handler: function (newVal) {
  277. this.video.play = false;
  278. this.video.frame = 0;
  279. this.zoomBox = false;
  280. this.$root.socket.get(`/data/${newVal.identifier}/results`)
  281. .then(response => response.json())
  282. .then(results => {
  283. this.results = results;
  284. });
  285. }
  286. },
  287. infoBox: function () {
  288. setTimeout(this.resize, 1);
  289. }
  290. }
  291. }
  292. </script>
  293. <style scoped>
  294. .annotated-image {
  295. width: 100%;
  296. height: 100%;
  297. display: flex;
  298. flex-direction: row;
  299. justify-content: center;
  300. align-items: center;
  301. overflow: hidden;
  302. }
  303. .options-bar {
  304. height: 100%;
  305. }
  306. .media {
  307. width: 100%;
  308. height: 100%;
  309. flex-grow: 1;
  310. display: flex;
  311. flex-direction: column;
  312. justify-content: center;
  313. align-items: center;
  314. overflow: hidden;
  315. }
  316. .video-player {
  317. width: 100%;
  318. height: 100%;
  319. flex-grow: 1;
  320. overflow-y: auto;
  321. display: flex;
  322. justify-content: center;
  323. align-items: center;
  324. }
  325. img, video {
  326. max-width: 100%;
  327. max-height: 100%;
  328. transition: transform 0.01s;
  329. }
  330. </style>