Browse Source

Resolve "full-image labels"

Eric Tröbs 4 years ago
parent
commit
abfc6591f6

+ 5 - 0
pycs/frontend/WebServer.py

@@ -198,6 +198,11 @@ class WebServer:
             if result:
                 if 'delete' in result:
                     project.remove_media_file(file_identifier)
+                elif 'x' not in result:
+                    if result['label']:
+                        target_object.add_global_result(result)
+                    else:
+                        target_object.remove_global_result()
                 else:
                     target_object.add_result(result)
 

+ 13 - 0
pycs/projects/MediaFile.py

@@ -22,6 +22,19 @@ class MediaFile(ObservableDict):
     def get_file(self):
         return self.__get_file(self['id'])
 
+    def add_global_result(self, result, origin='user'):
+        self.remove_global_result()
+        self.add_result(result, origin)
+
+    def remove_global_result(self):
+        delete = []
+        for result_id in self['predictionResults']:
+            if 'x' not in self['predictionResults'][result_id]:
+                delete.append(result_id)
+
+        for result_id in delete:
+            del self['predictionResults'][result_id]
+
     def add_result(self, result, origin='user'):
         result['id'] = str(uuid1())
         result['origin'] = origin

+ 3 - 1
webui/src/components/media/annotated-image.vue

@@ -73,7 +73,9 @@ export default {
       return this.sizes.map(e => '(max-width: ' + e + 'px) ' + e + 'px').join(',');
     },
     predictions: function () {
-      return Object.keys(this.data.predictionResults).map(k => this.data.predictionResults[k]);
+      return Object.keys(this.data.predictionResults)
+          .map(k => this.data.predictionResults[k])
+          .filter(k => 'x' in k);
     },
     events: function () {
       return {

+ 4 - 1
webui/src/components/media/annotated-media-view.vue

@@ -9,7 +9,10 @@
                      @previous="previous" @next="next"
                      @predict="predict"
                      :control="showMediaSelector"
-                     @control="showMediaSelector = $event"/>
+                     @control="showMediaSelector = $event"
+                     :project="currentProject"
+                     :data="currentMedia"
+                     :socket="socket"/>
     </div>
 
     <div class="selector" v-if="showMediaSelector">

+ 79 - 8
webui/src/components/media/media-control.vue

@@ -1,5 +1,12 @@
 <template>
   <div class="media-control">
+    <button-input type="transparent"
+                  style="color: var(--on_error)"
+                  :class="{disabled: !hasNext}"
+                  @click="$emit('control', !control)">
+      {{ control ? '&#9660;' : '&#9650;' }}
+    </button-input>
+
     <button-input type="transparent"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasPrevious}"
@@ -26,12 +33,26 @@
       &gt;
     </button-input>
 
-    <button-input type="transparent"
-                  style="color: var(--on_error)"
-                  :class="{disabled: !hasNext}"
-                  @click="$emit('control', !control)">
-      {{ control ? '&#9660;' : '&#9650;' }}
+    <button-input type="transparent">
+      <img alt="label" src="@/assets/icons/tag.svg"
+           :class="{tagged: currentLabel}"
+           @touchstart.stop @mousedown.stop
+           @click.stop="showLabelSelection = true">
     </button-input>
+
+    <select v-if="showLabelSelection"
+            @touchstart.stop @mousedown.stop
+            @change="labelSelf"
+            @focusout="showLabelSelection = false">
+      <option value="">None</option>
+
+      <option v-for="label in labelList"
+              :key="label.id"
+              :value="label.id"
+              :selected="label === currentLabel">
+        {{ label.name }}
+      </option>
+    </select>
   </div>
 
 </template>
@@ -43,15 +64,48 @@ import ButtonRow from "@/components/base/button-row";
 export default {
   name: "media-control",
   components: {ButtonRow, ButtonInput},
-  props: ['hasPrevious', 'hasNext', 'control'],
+  props: ['hasPrevious', 'hasNext', 'control', 'data', 'project', 'socket'],
+  data: function () {
+    return {
+      showLabelSelection: false
+    }
+  },
+  computed: {
+    mediaUrl: function () {
+      return this.socket.media(this.project.id, this.data.id);
+    },
+    labelList: function () {
+      return Object.keys(this.project.labels).map(key => this.project.labels[key]);
+    },
+    currentLabel: function () {
+      const predictions = Object.keys(this.data.predictionResults).map(k => this.data.predictionResults[k]);
+      for (let result of predictions) {
+        if (!('x' in result || 'y' in result || 'w' in result || 'h' in result)) {
+          return this.project.labels[result.label];
+        }
+      }
+
+      return false;
+    }
+  },
   methods: {
-    previous: function() {
+    previous: function () {
       if (this.hasPrevious)
         this.$emit('previous', null);
     },
-    next: function() {
+    next: function () {
       if (this.hasNext)
         this.$emit('next', null);
+    },
+    labelSelf: function (event) {
+      const options = event.target.options;
+      const option = options[options.selectedIndex];
+      const value = option.value;
+
+      this.socket.post(this.mediaUrl, {
+        label: value ? value : false
+      });
+      this.showLabelSelection = false;
     }
   }
 }
@@ -59,12 +113,29 @@ export default {
 
 <style scoped>
 .media-control {
+  position: relative;
   display: flex;
   justify-content: center;
   align-items: center;
+  margin: 0 0.5rem;
 }
 
 .disabled {
   opacity: 0.4;
 }
+
+img {
+  filter: invert(1);
+  opacity: 0.4;
+}
+
+img.tagged {
+  opacity: 1;
+}
+
+select {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
 </style>