6
0
فهرست منبع

added proper right-click event to an annotation-box component: label selection; rewritten event handling of the box events (deletion, confirmation, update, etc.)

Dimitri Korsch 3 سال پیش
والد
کامیت
798e63a872

+ 138 - 40
webui/src/components/media/annotated-image.vue

@@ -6,9 +6,9 @@
                    @interaction="interaction = $event"
                    :filter="filter"
                    @filter="filter = $event"
-                   :label="label"
-                   @label="label = $event"
                    :labels="labels"
+                   :labelsEnabled="labelsEnabled"
+                   @labelSelector="openLabelSelector()"
                    :zoomBox="zoomBox"
                    :infoBox="infoBox !== false"
                    @infoBox="infoBox = $event; interaction=false"
@@ -16,6 +16,12 @@
                    @prevzoom="$refs.overlay.prevZoom()"
                    @nextzoom="$refs.overlay.nextZoom()"/>
 
+      <label-selector v-if="labelSelector"
+                      :labels="labels"
+                      @close="closeLabelSelector()"
+                      @label="labelSelected($event);"
+                      />
+
       <div class="media">
         <h3>{{current.path}}</h3>
         <div class="mode-tooltip">{{modeTooltip()}}</div>
@@ -54,6 +60,11 @@
                             :results="results"
                             :labels="labels"
                             :crop="infoBox"
+                            @labelSelector="openLabelSelector($event)"
+                            @labelBox="labelBox"
+                            @removeBox="removeBox"
+                            @confirmBox="confirmBox"
+                            @updateBox="updateBox"
                             @crop="infoBox = $event"
                             :zoom="zoomBox"
                             @zoom="zoomBox = $event"/>
@@ -63,7 +74,10 @@
                      :labels="labels"
                      :file="current"
                      :box="infoBox"
-                     @close="interaction=defaultInteraction; infoBox=false"/>
+                     @close="
+                      interaction=defaultInteraction;
+                      infoBox=false"
+                    />
     </template>
   </div>
 </template>
@@ -73,11 +87,59 @@ 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";
+import LabelSelector from "@/components/media/label-selector";
 
 export default {
   name: "annotated-image",
-  components: {OptionsBar, VideoControl, AnnotationOverlay, CroppedImage},
-  props: ['current'],
+  components: {
+    OptionsBar,
+    VideoControl,
+    AnnotationOverlay,
+    CroppedImage,
+    LabelSelector
+  },
+  props: [
+    'current'
+  ],
+
+  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',
+      defaultInteraction: 'draw-box',
+      filter: ['user', 'pipeline'],
+      label: false,
+      labelSelector: false,
+      boxTolabel: null,
+      model: null,
+      results: [],
+      labels: []
+    }
+  },
   mounted: function () {
     // add resize listener
     window.addEventListener('resize', this.resize);
@@ -100,6 +162,7 @@ export default {
     this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
       .then(response => response.json())
       .then(model => {
+        this.model = model;
         this.supported.labeledImages = model.supports.includes('labeled-images');
         this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
         this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
@@ -119,40 +182,6 @@ export default {
     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',
-      defaultInteraction: 'draw-box',
-      filter: ['user', 'pipeline'],
-      label: false,
-      results: [],
-      labels: []
-    }
-  },
   computed: {
     src: function () {
       if (!this.container.width || !this.container.height)
@@ -187,10 +216,54 @@ export default {
       return {
         transform: `scale(${factor}) translateX(${posX * 100}%) translateY(${posY * 100}%)`
       };
-    }
+    },
+    labelsEnabled: function() {
+
+      return this.model
+          && (this.supported.labeledImages || this.supported.labeledBoundingBoxes);
+    },
   },
   methods: {
 
+    openLabelSelector: function(box) {
+      if (box === null || box === undefined)
+        this.boxTolabel = null
+      else
+        this.boxTolabel = box;
+
+      this.labelSelector = true;
+    },
+
+    closeLabelSelector: function() {
+      this.boxTolabel = null;
+      this.labelSelector = false;
+    },
+
+    labelSelected: function(label) {
+      this.interaction = 'label-box';
+      this.label = label;
+
+      if (this.boxTolabel !== null)
+        this.labelBox(this.boxTolabel.identifier, label.identifier);
+    },
+
+    labelBox: function(box_id, label_id) {
+      this.$root.socket.post(`/results/${box_id}/label`,
+        {label: label_id});
+    },
+    removeBox: function(box_id,) {
+      this.$root.socket.post(`/results/${box_id}/remove`,
+        {remove: true});
+    },
+    confirmBox: function(box_id) {
+      this.$root.socket.post(`/results/${box_id}/confirm`,
+        {confirm: true});
+    },
+    updateBox: function(box_id, data) {
+      this.$root.socket.post(`/results/${box_id}/data`,
+        {data: data});
+    },
+
     modeTooltip: function(){
       let tip = "Current mode";
       switch (this.interaction) {
@@ -369,6 +442,7 @@ export default {
   align-items: center;
 
   overflow: hidden;
+
 }
 
 .options-bar {
@@ -417,4 +491,28 @@ h3 {
   margin: 0.5em;
 }
 
+
+.label-selector {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  z-index: 101;
+
+  width: 30rem;
+  max-width: 90vw;
+  max-height: 90%;
+  padding-bottom: 0;
+
+  animation: label-selector-animation 0.3s ease-out forwards;
+}
+
+@keyframes label-selector-animation {
+  from {
+    transform: translateX(-50%) translateY(-100%);
+  }
+  to {
+    transform: translateX(-50%) translateY(0%);
+  }
+}
+
 </style>

+ 13 - 17
webui/src/components/media/annotation-box.vue

@@ -126,7 +126,7 @@ export default {
 
       if (this.deletable) {
         // TODO then / error
-        this.$root.socket.post(`/results/${this.box.identifier}/remove`, {remove: true});
+        this.$emit("removeBox", this.box.identifier)
       }
       else if (this.draggable) {
         // TODO then / error
@@ -134,22 +134,18 @@ export default {
       }
       else if (this.taggable) {
         // TODO then / error
-        this.$root.socket.post(`/results/${this.box.identifier}/label`, {
-          label: this.taggable.identifier
-        });
-        this.$emit('crop', this.box);
+        this.$emit("labelBox", this.box.identifier, this.taggable.identifier)
+        this.selectMe();
       }
       else if (this.confirmable) {
         // TODO then / error
-        this.$root.socket.post(`/results/${this.box.identifier}/confirm`, {
-          confirm: true
-        });
+        this.$emit("confirmBox", this.box.identifier)
       }
       else if (this.zoomable) {
         this.$emit('zoom', this.position);
       }
       else if (this.croppable) {
-        this.$emit('crop', this.box);
+        this.selectMe();
       } else{
         return
       }
@@ -157,11 +153,9 @@ export default {
       event.stopPropagation();
     },
 
-    singleRightClick(event) {
-      console.debug("single right click", event);
-      this.$emit('interaction', "label-box");
-      this.$emit('crop', this.box);
-
+    singleRightClick() {
+      this.selectMe();
+      this.$emit('labelSelector', this.box);
     },
 
     singleClick(event) {
@@ -188,6 +182,10 @@ export default {
       }
     },
 
+    selectMe(){
+      this.$emit('crop', this.box);
+    },
+
     click: function (event) {
       this.clickCounter++;
 
@@ -207,9 +205,7 @@ export default {
       this.$emit('resize', mode, this.position, this.update);
     },
     update: function (value) {
-      this.$root.socket.post(`/results/${this.box.identifier}/data`, {
-        data: value
-      });
+      this.$emit("updateBox", this.box.identifier, value)
     }
   }
 }

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

@@ -1,7 +1,12 @@
 <template>
-  <div class="annotation-overlay" :style="position"
-       @touchstart="press" @touchmove="track" @touchend="release"
-       @mousedown.left="press" @mousemove.left="track" @mouseup.left="release"
+  <div class="annotation-overlay"
+       :style="position"
+       @touchstart="press"
+       @touchmove="track"
+       @touchend="release"
+       @mousedown.left="press"
+       @mousemove.left="track"
+       @mouseup.left="release"
        @dragstart.stop>
 
     <annotation-box v-for="(box, index) in boundingBoxes"
@@ -21,6 +26,11 @@
                     @resize="resize"
                     @crop="$emit('crop', $event)"
                     @interaction="$emit('interaction', $event)"
+                    @labelSelector="$emit('labelSelector', $event)"
+                    @labelBox="labelBox"
+                    @removeBox="removeBox"
+                    @confirmBox="confirmBox"
+                    @updateBox="updateBox"
                     @zoom="zoomBox(index, $event)"/>
 
     <annotation-box v-if="current"
@@ -34,6 +44,7 @@
          }">
       {{ imageLabelObject.name }}
     </div>
+
   </div>
 </template>
 
@@ -300,7 +311,19 @@ export default {
     },
     nextZoom: function () {
       this.zoomBox(this.zoomed + 1);
-    }
+    },
+    labelBox: function (box_id, label_id) {
+      this.$emit('labelBox', box_id, label_id);
+    },
+    removeBox: function (box_id) {
+      this.$emit('removeBox', box_id);
+    },
+    confirmBox: function (box_id) {
+      this.$emit('confirmBox', box_id);
+    },
+    updateBox: function (box_id, data) {
+      this.$emit('updateBox', box_id, data);
+    },
   },
   watch: {
     interaction: function (newVal, oldVal) {

+ 8 - 45
webui/src/components/media/options-bar.vue

@@ -42,15 +42,10 @@
          class="image"
          title="label bounding box (T)"
          :class="{active: interaction === 'label-box'}"
-         @click="labelSelector=true">
-      <img alt="extreme clicking" src="@/assets/icons/tag.svg">
+         @click="$emit('labelSelector')">
+      <img alt="label bounding box" src="@/assets/icons/tag.svg">
     </div>
 
-    <label-selector v-if="labelSelector"
-                    :labels="labels"
-                    @close="labelSelector=false"
-                    @label="$emit('interaction', 'label-box'); $emit('label', $event)"/>
-
     <div ref="confirm_box"
          class="image"
          title="confirm bounding box (G)"
@@ -132,20 +127,19 @@
 </template>
 
 <script>
-import LabelSelector from "@/components/media/label-selector";
 
 export default {
   name: "options-bar",
-  components: {LabelSelector},
-  props: ['file', 'interaction', 'filter', 'label', 'labels', 'infoBox', 'zoomBox'],
+  props: [
+    'file',
+    'interaction',
+    'filter',
+    'labelsEnabled',
+  ],
   created: function () {
     // get data
     this.getJobs();
 
-    this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
-      .then(response => response.json())
-      .then(model => this.model = model);
-
     // subscribe to changes
     this.$root.socket.on('connect', this.getJobs);
     this.$root.socket.on('create-job', this.addJob);
@@ -166,20 +160,11 @@ export default {
   data: function () {
     return {
       jobs: [],
-      labelSelector: false,
-      model: null
     }
   },
   computed: {
     isPredictionRunning: function () {
       return this.jobs.filter(j => !j.finished && j.type === 'Model Interaction').length > 0;
-    },
-    labelsEnabled: function () {
-      return this.model
-          && (
-              this.model.supports.includes('labeled-images')
-              || this.model.supports.includes('labeled-bounding-boxes')
-          );
     }
   },
   methods: {
@@ -304,26 +289,4 @@ img {
   filter: invert(1);
 }
 
-.label-selector {
-  position: absolute;
-  top: 0;
-  left: 50%;
-  z-index: 101;
-
-  width: 30rem;
-  max-width: 90vw;
-  max-height: 100%;
-  padding-bottom: 0;
-
-  animation: label-selector-animation 0.5s ease-out forwards;
-}
-
-@keyframes label-selector-animation {
-  from {
-    transform: translateX(-50%) translateY(-100%);
-  }
-  to {
-    transform: translateX(-50%) translateY(0%);
-  }
-}
 </style>