123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- <template>
- <div class="annotated-image" ref="root">
- <template v-if="src">
- <options-bar :file="current"
- :interaction="interaction"
- @interaction="interaction = $event"
- :filter="filter"
- @filter="filter = $event"
- :label="label"
- @label="label = $event"
- :labels="labels"
- :zoomBox="zoomBox"
- :infoBox="infoBox !== false"
- @infoBox="infoBox = $event; interaction=false"
- @unzoom="zoomBox=false; interaction=false"
- @prevzoom="$refs.overlay.prevZoom()"
- @nextzoom="$refs.overlay.nextZoom()"/>
- <div class="media">
- <!-- image -->
- <img v-if="current.type === 'image'"
- ref="media" :src="src" alt="media"
- :style="cropPosition"
- v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change"
- v-on:transitionend="resize">
- <!-- video -->
- <template v-if="current.type === 'video'">
- <div class="video-player">
- <video ref="media" :src="src"
- v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change" v-on:canplay="change"
- v-on:timeupdate="videoProgress"/>
- </div>
- <video-control :file="current"
- :video="video"
- :results="results"
- @play="videoPlay"
- @jump="videoJump"/>
- </template>
- <!-- overlay -->
- <annotation-overlay ref="overlay"
- :file="current"
- :position="overlayPosition"
- :size="image"
- :interaction="interaction"
- :filter="filter"
- :label="label"
- :video="video"
- :results="results"
- :labels="labels"
- :crop="infoBox"
- @crop="infoBox = $event"
- :zoom="zoomBox"
- @zoom="zoomBox = $event"/>
- </div>
- <cropped-image v-if="infoBox !== false"
- :labels="labels"
- :file="current"
- :box="infoBox"
- @close="infoBox=false"/>
- </template>
- </div>
- </template>
- <script>
- import AnnotationOverlay from "@/components/media/annotation-overlay";
- import VideoControl from "@/components/media/video-control";
- import OptionsBar from "@/components/media/options-bar";
- import CroppedImage from "@/components/media/cropped-image";
- export default {
- name: "annotated-image",
- components: {OptionsBar, VideoControl, AnnotationOverlay, CroppedImage},
- props: ['current'],
- mounted: function () {
- // add resize listener
- window.addEventListener('resize', this.resize);
- this.interval = setInterval(this.resize, 1000);
- this.resize();
- // add result listener
- this.$root.socket.on('create-result', this.createResult);
- this.$root.socket.on('edit-result', this.editResult);
- this.$root.socket.on('remove-result', this.removeResult);
- // add label listener
- this.getLabels();
- this.$root.socket.on('connect', this.getLabels);
- this.$root.socket.on('create-label', this.addLabelToList);
- this.$root.socket.on('remove-label', this.removeLabelFromList);
- this.$root.socket.on('edit-label', this.editLabelInList);
- // get model
- this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
- .then(response => response.json())
- .then(model => {
- this.supported.labeledImages = model.supports.includes('labeled-images');
- this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
- this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
- || model.supports.includes('bounding-boxes');
- });
- },
- destroyed: function () {
- window.removeEventListener('resize', this.resize);
- clearInterval(this.interval);
- this.$root.socket.off('create-result', this.createResult);
- this.$root.socket.off('edit-result', this.editResult);
- this.$root.socket.off('remove-result', this.removeResult);
- this.$root.socket.off('connect', this.getLabels);
- this.$root.socket.off('create-label', this.addLabelToList);
- this.$root.socket.off('remove-label', this.removeLabelFromList);
- this.$root.socket.off('edit-label', this.editLabelInList);
- },
- data: function () {
- return {
- interval: false,
- container: {
- top: 0,
- left: 0,
- width: 0,
- height: 0,
- },
- image: {
- top: 0,
- left: 0,
- width: 0,
- height: 0
- },
- video: {
- frame: 0,
- play: false
- },
- infoBox: false,
- zoomBox: false,
- supported: {
- labeledImages: false,
- boundingBoxes: false,
- labeledBoundingBoxes: false,
- },
- interaction: 'draw-box',
- filter: ['user', 'pipeline'],
- label: false,
- results: [],
- labels: []
- }
- },
- computed: {
- src: function () {
- if (!this.container.width || !this.container.height)
- return '';
- if (this.current.type === 'video')
- return this.$root.socket.media(this.current);
- const width = Math.ceil(this.container.width / 400) * 400;
- const height = Math.ceil(this.container.height / 400) * 400;
- return this.$root.socket.media(this.current, width, height);
- },
- overlayPosition: function () {
- return {
- top: (this.image.top - this.container.top) + 'px',
- left: (this.image.left - this.container.left) + 'px',
- width: this.image.width + 'px',
- height: this.image.height + 'px'
- }
- },
- cropPosition: function () {
- if (!this.zoomBox)
- return {
- transform: ``,
- };
- const posX = 0.5 - (this.zoomBox.x + this.zoomBox.w / 2);
- const posY = 0.5 - (this.zoomBox.y + this.zoomBox.h / 2);
- const factor = 0.75 / Math.max(this.zoomBox.w, this.zoomBox.h);
- // use a transition to use the transitionend event to recalculate box positions
- return {
- transform: `scale(${factor}) translateX(${posX * 100}%) translateY(${posY * 100}%)`
- };
- }
- },
- methods: {
- resize: function () {
- const rect = this.$refs.root.getBoundingClientRect();
- this.container.top = rect.top;
- this.container.left = rect.left;
- this.container.width = rect.width;
- this.container.height = rect.height;
- this.change();
- },
- change: function () {
- if (!this.$refs.media)
- return;
- const rect = this.$refs.media.getBoundingClientRect();
- this.image.top = rect.top;
- this.image.left = rect.left;
- this.image.width = rect.width;
- this.image.height = rect.height;
- },
- videoProgress: function (event) {
- this.video.frame = Math.floor(event.target.currentTime * this.current.fps);
- },
- videoPlay: function (value) {
- this.video.play = value;
- if (value)
- this.$refs.media.play();
- else
- this.$refs.media.pause();
- },
- videoJump: function (value) {
- // calculate difference
- value = Math.max(0, Math.min(this.current.frames, value));
- const diff = value - this.video.frame;
- // set timestamp
- this.$refs.media.currentTime += diff / this.current.fps;
- // get timestamp offset and correct if needed
- const currentFrame = this.$refs.media.currentTime * this.current.fps;
- if (diff < 0 && currentFrame < value) {
- this.$refs.media.currentTime += 0.5 / this.current.fps;
- } else if (diff > 0 && currentFrame > value + 0.5) {
- this.$refs.media.currentTime -= 0.5 / this.current.fps;
- }
- },
- createResult: function (result) {
- if (result['file_id'] !== this.current.identifier)
- return;
- for (let r of this.results)
- if (r.identifier === result.identifier)
- return;
- this.results.push(result);
- },
- removeResult: function (result) {
- for (let i = 0; i < this.results.length; i++) {
- if (this.results[i].identifier === result.identifier) {
- this.results.splice(i, 1);
- return;
- }
- }
- },
- editResult: function (result) {
- if (this.infoBox && result.identifier === this.infoBox.identifier)
- this.infoBox = result;
- for (let i = 0; i < this.results.length; i++) {
- if (this.results[i].identifier === result.identifier) {
- this.$set(this.results, i, result);
- return;
- }
- }
- },
- getLabels: function () {
- this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels`)
- .then(response => response.json())
- .then(labels => {
- this.labels = [];
- labels.forEach(this.addLabelToList);
- });
- },
- addLabelToList: function (label) {
- if (label['project_id'] !== this.$root.project.identifier)
- return;
- for (let l of this.labels)
- if (l.identifier === label.identifier)
- return;
- this.labels.push(label);
- },
- removeLabelFromList: function (label) {
- for (let i = 0; i < this.labels.length; i++) {
- if (this.labels[i].identifier === label.identifier) {
- this.labels.splice(i, 1);
- return;
- }
- }
- },
- editLabelInList: function (label) {
- for (let i = 0; i < this.labels.length; i++) {
- if (this.labels[i].identifier === label.identifier) {
- this.$set(this.labels, i, label);
- return;
- }
- }
- }
- },
- watch: {
- current: {
- immediate: true,
- handler: function (newVal) {
- this.video.play = false;
- this.video.frame = 0;
- this.zoomBox = false;
- this.$root.socket.get(`/data/${newVal.identifier}/results`)
- .then(response => response.json())
- .then(results => {
- this.results = results;
- });
- }
- },
- infoBox: function () {
- setTimeout(this.resize, 1);
- }
- }
- }
- </script>
- <style scoped>
- .annotated-image {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- overflow: hidden;
- }
- .options-bar {
- height: 100%;
- }
- .media {
- width: 100%;
- height: 100%;
- flex-grow: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- overflow: hidden;
- }
- .video-player {
- width: 100%;
- height: 100%;
- flex-grow: 1;
- overflow-y: auto;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- img, video {
- max-width: 100%;
- max-height: 100%;
- transition: transform 0.01s;
- }
- </style>
|