123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- <template>
- <div class="annotated-image" v-on="events">
- <img v-if="data.type === 'image'"
- alt="media" ref="image" draggable="false"
- :src="mediaUrl" :srcset="mediaUrlSet" :sizes="mediaUrlSizes"
- v-on:load="resizeEvent" v-on:loadedmetadata="resizeEvent" v-on:loadeddata="resizeEvent"/>
- <template v-if="data.type === 'video'">
- <video ref="image" draggable="false"
- preload="auto" :src="mediaUrl"
- v-on:touchstart.prevent v-on:touchmove.prevent v-on:touchend.prevent
- v-on:mousedown.prevent v-on:mousemove.prevent v-on:mouseup.prevent
- v-on:click.prevent v-on:dragstart.prevent
- v-on:load="resizeEvent" v-on:canplay="resizeEvent" v-on:loadeddata="resizeEvent"
- v-on:timeupdate="videoProgress">
- </video>
- <video-control :video="video" :data="data"
- v-on:touchstart.prevent.stop v-on:touchmove.prevent.stop v-on:touchend.prevent.stop
- v-on:mousedown.prevent.stop v-on:mousemove.prevent.stop v-on:mouseup.prevent.stop
- v-on:click.prevent.stop v-on:dragstart.prevent.stop
- @play="videoPlay" @jump="videoJump"/>
- </template>
- <!-- <div style="position: absolute; top: 0.5rem; left: 0.5rem">{{ extremeClicking }}</div> -->
- <annotation-box v-if="current"
- :image="image"
- :supports="project.model.supports"
- :position="current"/>
- <annotation-box v-for="(result, index) in predictions"
- :type="result.origin"
- :key="index"
- :id="result.id"
- :image="image"
- :position="result"
- :socket="socket"
- :box-url="mediaUrl + '/' + result.id"
- :labels="project.labels"
- :supports="project.model.supports"
- :selected="selectedBoundingBox !== false && index === selectedBoundingBox"
- @move="move"
- @resize="resize"
- @update="update"/>
- </div>
- </template>
- <script>
- import AnnotationBox from "@/components/media/annotation-box";
- import VideoControl from "@/components/media/video-control";
- export default {
- name: "annotated-image",
- components: {VideoControl, AnnotationBox},
- props: ['data', 'project', 'socket', 'filter', 'extremeClicking'],
- mounted: function () {
- window.addEventListener('resize', this.resizeEvent);
- this.resizeEvent();
- // TODO dirty workaround
- // this works around a bug which is probably caused by partly loaded
- // videos and their received dimensions
- this.watcher = setInterval(this.resizeEvent, 400);
- },
- created: function () {
- window.addEventListener('keypress', this.keypressEvent);
- },
- destroyed() {
- window.removeEventListener('resize', this.resizeEvent);
- window.removeEventListener('keypress', this.keypressEvent);
- clearInterval(this.watcher);
- },
- watch: {
- data: function () {
- this.current = false;
- this.video = {
- frame: 0,
- play: false
- };
- this.resizeEvent();
- },
- extremeClicking: function (newValue) {
- if (!newValue && this.current) {
- this.release();
- }
- }
- },
- data: function () {
- return {
- sizes: [600, 800, 1200, 1600, 2000, 3000],
- watcher: 0,
- image: {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- },
- video: {
- frame: 0,
- play: false
- },
- start: false,
- fixed: false,
- current: false,
- callback: false,
- selectedBoundingBox: false
- }
- },
- computed: {
- mediaUrl: function () {
- return this.socket.media(this.project.id, this.data.id);
- },
- mediaUrlSet: function () {
- return this.sizes.map(e => this.mediaUrl + '/' + e + ' ' + e + 'w').join(',');
- },
- mediaUrlSizes: function () {
- return this.sizes.map(e => '(max-width: ' + e + 'px) ' + e + 'px').join(',');
- },
- predictions: function () {
- const predictions = Object.keys(this.data.predictionResults)
- .map(k => this.data.predictionResults[k])
- .filter(k => 'x' in k)
- .filter(k => !this.filter || k.origin === this.filter);
- if (this.data.type === 'video')
- return predictions.filter(k => k.frame === this.video.frame);
- else
- return predictions;
- },
- events: function () {
- if (this.project.model.supports.includes('bounding-boxes')
- || this.project.model.supports.includes('labeled-bounding-boxes')) {
- return {
- 'touchstart': this.press,
- 'touchmove': this.track,
- 'touchend': this.release,
- 'mousedown': this.press,
- 'mousemove': this.track,
- 'mouseup': this.release,
- 'dragstart': e => e.stopPropagation()
- }
- } else {
- return {}
- }
- }
- },
- methods: {
- resizeEvent: function () {
- if (!this.$refs.image)
- return;
- const element = this.$refs.image.getBoundingClientRect();
- const parent = this.$refs.image.parentElement.getBoundingClientRect();
- this.image.left = element.x - parent.x;
- this.image.top = element.y - parent.y;
- this.image.width = element.width;
- this.image.height = element.height;
- },
- keypressEvent: function (event) {
- if (event.key === 'w') {
- if (this.selectedBoundingBox === false || this.selectedBoundingBox === this.predictions.length - 1)
- this.selectedBoundingBox = -1;
- this.selectedBoundingBox += 1;
- } else if (event.key === 's') {
- if (this.selectedBoundingBox === false || this.selectedBoundingBox === 0)
- this.selectedBoundingBox = this.predictions.length;
- this.selectedBoundingBox -= 1;
- } else if (event.key === 'v') {
- navigator.clipboard.readText().then(text => {
- const obj = JSON.parse(text);
- if (obj.type === 'bounding-box' || obj.type === 'labeled-bounding-box') {
- if (this.data.type === 'video')
- obj.frame = this.video.frame;
- this.socket.post(this.mediaUrl, obj).then(this.update);
- console.log('paste', obj);
- }
- });
- }
- },
- update: function () {
- this.selectedBoundingBox = false;
- this.$emit('update', true);
- },
- videoPlay: function (value) {
- this.video.play = value;
- if (value)
- this.$refs.image.play();
- else
- this.$refs.image.pause();
- },
- videoJump: function (value) {
- // calculate difference
- value = Math.max(0, Math.min(this.data.frameCount, value));
- const diff = value - this.video.frame;
- // set timestamp
- this.$refs.image.currentTime += diff / this.data.fps;
- // get timestamp offset and correct if needed
- const currentFrame = this.$refs.image.currentTime * this.data.fps;
- if (diff < 0 && currentFrame < value) {
- this.$refs.image.currentTime += 0.5 / this.data.fps;
- } else if (diff > 0 && currentFrame > value + 0.5) {
- this.$refs.image.currentTime -= 0.5 / this.data.fps;
- }
- },
- videoProgress: function (event) {
- this.video.frame = Math.floor(event.target.currentTime * this.data.fps);
- },
- get: function (event) {
- if ('clientX' in event)
- return event;
- if ('touches' in event && event.touches.length > 0)
- return event.touches[0];
- if ('changedTouches' in event && event.changedTouches.length > 0)
- return event.changedTouches[0];
- },
- getX: function (event) {
- const bcr = this.$refs.image.getBoundingClientRect();
- return (this.get(event).clientX - bcr.left) / bcr.width;
- },
- getY: function (event) {
- const bcr = this.$refs.image.getBoundingClientRect();
- return (this.get(event).clientY - bcr.top) / bcr.height;
- },
- buildRectangle: function (x1, y1, x2, y2) {
- let lx, hx, ly, hy;
- if (this.fixed && 'offsetX' in this.fixed && 'offsetY' in this.fixed) {
- lx = Math.max(x2 + this.fixed.offsetX, 0);
- hx = Math.min(x2 + this.fixed.offsetX + this.fixed.w, 1);
- ly = Math.max(y2 + this.fixed.offsetY, 0);
- hy = Math.min(y2 + this.fixed.offsetY + this.fixed.h, 1);
- } else {
- lx = this.fixed && 'lx' in this.fixed ? this.fixed.lx : Math.max(Math.min(x1, x2), 0);
- hx = this.fixed && 'hx' in this.fixed ? this.fixed.hx : Math.min(Math.max(x1, x2), 1);
- ly = this.fixed && 'ly' in this.fixed ? this.fixed.ly : Math.max(Math.min(y1, y2), 0);
- hy = this.fixed && 'hy' in this.fixed ? this.fixed.hy : Math.min(Math.max(y1, y2), 1);
- }
- const rectangle = {
- x: lx,
- y: ly,
- w: hx - lx,
- h: hy - ly
- }
- if (this.data.type === 'video')
- rectangle.frame = this.video.frame;
- return rectangle;
- },
- press: function (event) {
- const x = this.getX(event);
- const y = this.getY(event);
- if (this.extremeClicking && this.start) {
- if (this.current) {
- const minX = Math.min(x, this.current.x);
- const maxX = Math.max(x, this.current.x + this.current.w);
- const minY = Math.min(y, this.current.y);
- const maxY = Math.max(y, this.current.y + this.current.h);
- this.current = this.buildRectangle(minX, minY, maxX, maxY);
- } else {
- this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
- }
- } else {
- this.start = {
- x: x,
- y: y
- };
- }
- },
- track: function (event) {
- if (this.start && !this.extremeClicking) {
- const x = this.getX(event);
- const y = this.getY(event);
- this.current = this.buildRectangle(this.start.x, this.start.y, x, y);
- }
- },
- release: function () {
- if (this.start && !this.extremeClicking) {
- if (this.callback) {
- this.callback(this.current);
- this.update();
- } else {
- this.socket.post(this.mediaUrl, this.current)
- .then(this.update);
- }
- this.start = false;
- this.fixed = false;
- this.current = false;
- this.callback = false;
- }
- },
- move: function (event, position, callback) {
- this.callback = callback;
- const x = this.getX(event);
- const y = this.getY(event);
- this.start = {x: position.x, y: position.y};
- this.fixed = {w: position.w, h: position.h, offsetX: position.x - x, offsetY: position.y - y};
- },
- resize: function (position, mode, callback) {
- this.callback = callback;
- switch (mode) {
- case 'nw':
- this.start = {x: position.x + position.w, y: position.y + position.h};
- this.fixed = false;
- break;
- case 'ne':
- this.start = {x: position.x, y: position.y + position.h};
- this.fixed = false;
- break;
- case 'sw':
- this.start = {x: position.x + position.w, y: position.y};
- this.fixed = false;
- break;
- case 'se':
- this.start = {x: position.x, y: position.y};
- this.fixed = false;
- break;
- case 'nn':
- this.start = {x: position.x, y: position.y + position.h};
- this.fixed = {lx: position.x, hx: position.x + position.w};
- break;
- case 'ww':
- this.start = {x: position.x + position.w, y: position.y};
- this.fixed = {ly: position.y, hy: position.y + position.h};
- break;
- case 'ee':
- this.start = {x: position.x, y: position.y};
- this.fixed = {ly: position.y, hy: position.y + position.h};
- break;
- case 'ss':
- this.start = {x: position.x, y: position.y};
- this.fixed = {lx: position.x, hx: position.x + position.w};
- break;
- }
- }
- }
- }
- </script>
- <style scoped>
- img,
- video {
- max-width: 100%;
- max-height: 100%;
- }
- .video-control {
- position: absolute;
- bottom: 0;
- }
- </style>
|