6
0
ソースを参照

Merge branch '144-zoom-function' into 'master'

Resolve "zoom function"

Closes #144

See merge request troebs/pycs!131
Eric Tröbs 3 年 前
コミット
d0c1234d55

+ 5 - 0
webui/src/assets/icons/codescan.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path d="M8.47 4.97a.75.75 0 000 1.06L9.94 7.5 8.47 8.97a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM6.53 6.03a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 101.06-1.06L5.06 7.5l1.47-1.47z"></path>
+    <path fill-rule="evenodd"
+          d="M12.246 13.307a7.5 7.5 0 111.06-1.06l2.474 2.473a.75.75 0 11-1.06 1.06l-2.474-2.473zM1.5 7.5a6 6 0 1110.386 4.094.75.75 0 00-.292.293A6 6 0 011.5 7.5z"></path>
+</svg>

+ 36 - 4
webui/src/components/media/annotated-image.vue

@@ -8,13 +8,19 @@
                    @filter="filter = $event"
                    :label="label"
                    @label="label = $event"
-                   :labels="labels"/>
+                   :labels="labels"
+                   :zoomBox="zoomBox"
+                   @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"
-             v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change">
+             :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'">
@@ -31,7 +37,8 @@
         </template>
 
         <!-- overlay -->
-        <annotation-overlay :file="current"
+        <annotation-overlay ref="overlay"
+                            :file="current"
                             :position="overlayPosition"
                             :size="image"
                             :interaction="interaction"
@@ -39,7 +46,9 @@
                             :label="label"
                             :video="video"
                             :results="results"
-                            :labels="labels"/>
+                            :labels="labels"
+                            :zoom="zoomBox"
+                            @zoom="zoomBox = $event"/>
       </div>
     </template>
   </div>
@@ -114,6 +123,7 @@ export default {
         frame: 0,
         play: false
       },
+      zoomBox: false,
       supported: {
         labeledImages: false,
         boundingBoxes: false,
@@ -145,6 +155,21 @@ export default {
         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: {
@@ -264,6 +289,8 @@ export default {
         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 => {
@@ -284,6 +311,8 @@ export default {
   flex-direction: row;
   justify-content: center;
   align-items: center;
+
+  overflow: hidden;
 }
 
 .options-bar {
@@ -299,6 +328,8 @@ export default {
   flex-direction: column;
   justify-content: center;
   align-items: center;
+
+  overflow: hidden;
 }
 
 .video-player {
@@ -315,5 +346,6 @@ export default {
 img, video {
   max-width: 100%;
   max-height: 100%;
+  transition: transform 0.01s;
 }
 </style>

+ 12 - 4
webui/src/components/media/annotation-box.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="annotation-box"
        :style="style"
-       :class="{draggable: draggable}"
+       :class="{draggable: draggable, shine: shine}"
        @mousedown.prevent="click" @touchstart.prevent="click">
     <template v-if="draggable">
       <div class="nw" @mousedown.prevent.stop="resize('nw')" @touchstart.prevent.stop="resize('nw')"/>
@@ -14,8 +14,7 @@
       <div class="se" @mousedown.prevent.stop="resize('se')" @touchstart.prevent.stop="resize('se')"/>
     </template>
 
-    <div v-if="labelName"
-         class="label">
+    <div v-if="labelName" class="label">
       {{ labelName }}
     </div>
   </div>
@@ -24,7 +23,7 @@
 <script>
 export default {
   name: "annotation-box",
-  props: ['box', 'position', 'draggable', 'deletable', 'taggable', 'confirmable', 'labels'],
+  props: ['box', 'position', 'draggable', 'deletable', 'taggable', 'confirmable', 'zoomable', 'labels', 'shine'],
   computed: {
     labelName: function () {
       if (!this.box || !this.box.label)
@@ -87,6 +86,10 @@ export default {
         });
         event.stopPropagation();
       }
+      if (this.zoomable) {
+        this.$emit('zoom', this.position);
+        event.stopPropagation();
+      }
     },
     resize: function (mode) {
       this.$emit('resize', mode, this.position, this.update);
@@ -106,6 +109,11 @@ export default {
   border: 1px solid transparent;
 }
 
+.annotation-box.shine {
+  background: none !important;
+  border-width: 5px;
+}
+
 .label {
   position: absolute;
   bottom: 0.25rem;

+ 28 - 4
webui/src/components/media/annotation-overlay.vue

@@ -4,7 +4,7 @@
        @mousedown="press" @mousemove="track" @mouseup="release"
        @dragstart.stop>
 
-    <annotation-box v-for="box in boundingBoxes"
+    <annotation-box v-for="(box, index) in boundingBoxes"
                     v-bind:key="box.identifier"
                     :box="box"
                     :position="box.data"
@@ -12,9 +12,12 @@
                     :deletable="interaction === 'remove-box'"
                     :taggable="interaction === 'label-box' ? label : false"
                     :confirmable="interaction === 'confirm-box'"
+                    :zoomable="interaction === 'zoom-box'"
                     :labels="labels"
+                    :shine="zoom"
                     @move="move"
-                    @resize="resize"/>
+                    @resize="resize"
+                    @zoom="zoomBox(index, $event)"/>
 
     <annotation-box v-if="current"
                     :position="current"/>
@@ -36,14 +39,15 @@ import AnnotationBox from "@/components/media/annotation-box";
 export default {
   name: "annotation-overlay",
   components: {AnnotationBox},
-  props: ['file', 'position', 'size', 'interaction', 'filter', 'label', 'video', 'results', 'labels'],
+  props: ['file', 'position', 'size', 'interaction', 'filter', 'label', 'video', 'results', 'labels', 'zoom'],
   data: function () {
     return {
       callback: false,
       start: false,
       fixed: false,
       current: false,
-      mousedown: false
+      mousedown: false,
+      zoomed: false
     }
   },
   computed: {
@@ -250,6 +254,26 @@ export default {
           this.fixed = {lx: position.x, hx: position.x + position.w};
           break;
       }
+    },
+    zoomBox: function (index) {
+      if (this.boundingBoxes.length === 0) {
+        this.zoomed = false;
+        return;
+      }
+      if (index < 0) {
+        index = this.boundingBoxes.length - 1;
+      } else if (index >= this.boundingBoxes.length) {
+        index = 0;
+      }
+
+      this.zoomed = index;
+      this.$emit('zoom', this.boundingBoxes[index].data);
+    },
+    prevZoom: function () {
+      this.zoomBox(this.zoomed - 1);
+    },
+    nextZoom: function () {
+      this.zoomBox(this.zoomed + 1);
     }
   },
   watch: {

+ 36 - 7
webui/src/components/media/options-bar.vue

@@ -61,9 +61,33 @@
 
     <div class="spacer"/>
 
+    <div ref="zoom_annotation"
+         class="image"
+         title="zoom bounding box (X)"
+         :class="{active: interaction === 'zoom-box' || zoomBox}"
+         @click="$emit(zoomBox ? 'unzoom' : 'interaction', 'zoom-box')">
+      <img alt="zoom bounding box" src="@/assets/icons/codescan.svg">
+    </div>
+
+    <div ref="zoom_prev_annotation"
+         class="image"
+         title="zoom previous bounding box (C)"
+         @click="$emit('prevzoom', true)">
+      <img alt="zoom previous bounding box" src="@/assets/icons/chevron-left.svg">
+    </div>
+
+    <div ref="zoom_next_annotation"
+         class="image"
+         title="zoom next bounding box (V)"
+         @click="$emit('nextzoom', true)">
+      <img alt="zoom next bounding box" src="@/assets/icons/chevron-right.svg">
+    </div>
+
+    <div class="spacer"/>
+
     <div ref="show_user_annotations"
          class="image"
-         title="show user annotations (X)"
+         title="show user annotations"
          :class="{active: filter && filter.includes('user')}"
          @click="invertFilter('user')">
       <img alt="show user annotations" src="@/assets/icons/smiley.svg">
@@ -71,7 +95,7 @@
 
     <div ref="show_pipeline_annotations"
          class="image"
-         title="show pipeline annotations (C)"
+         title="show pipeline annotations"
          :class="{active: filter && filter.includes('pipeline')}"
          @click="invertFilter('pipeline')">
       <img alt="show pipeline annotations" src="@/assets/icons/cpu.svg">
@@ -95,7 +119,7 @@ import LabelSelector from "@/components/media/label-selector";
 export default {
   name: "options-bar",
   components: {LabelSelector},
-  props: ['file', 'interaction', 'filter', 'label', 'labels'],
+  props: ['file', 'interaction', 'filter', 'label', 'labels', 'zoomBox'],
   created: function () {
     // get data
     this.getJobs();
@@ -179,14 +203,17 @@ export default {
         case 'g':
           this.$refs.confirm_box.click();
           break;
+        case 'b':
+          this.$refs.create_predictions.click();
+          break;
         case 'x':
-          this.$refs.show_user_annotations.click();
+          this.$refs.zoom_annotation.click();
           break;
         case 'c':
-          this.$refs.show_pipeline_annotations.click();
+          this.$refs.zoom_prev_annotation.click();
           break;
         case 'v':
-          this.$refs.create_predictions.click();
+          this.$refs.zoom_next_annotation.click();
           break;
       }
     },
@@ -227,7 +254,9 @@ export default {
 
 <style scoped>
 .options-bar {
-  background-color: rgba(0, 0, 0, 0.25);
+  /* background-color: rgba(0, 0, 0, 0.25); */
+  background-color: #858585;
+  z-index: 1;
 }
 
 .spacer {