6
0
Prechádzať zdrojové kódy

Resolve "individual manager"

Eric Tröbs 4 rokov pred
rodič
commit
e256a2790b

+ 38 - 0
pycs/frontend/WebServer.py

@@ -229,6 +229,44 @@ class WebServer:
             # return default success response
             # return default success response
             return response()
             return response()
 
 
+        @self.__flask.route('/projects/<project_identifier>/labels', methods=['POST'])
+        def create_label(project_identifier):
+            # abort if project id is not valid
+            if project_identifier not in app_status['projects'].keys():
+                return make_response('project does not exist', 500)
+
+            project = app_status['projects'][project_identifier]
+
+            # add result
+            result = request.get_json(force=True)
+            if result:
+                project.add_label(result['name'])
+
+            # return default success response
+            return response()
+
+        @self.__flask.route('/projects/<project_identifier>/labels/<label_identifier>', methods=['POST'])
+        def edit_label(project_identifier, label_identifier):
+            # abort if project id is not valid
+            if project_identifier not in app_status['projects'].keys():
+                return make_response('project does not exist', 500)
+
+            project = app_status['projects'][project_identifier]
+
+            # parse post data
+            result = request.get_json(force=True)
+            if result:
+                # remove label
+                if 'delete' in result.keys():
+                    project.remove_label(label_identifier)
+
+                # update label
+                else:
+                    project.update_label(label_identifier, result['name'])
+
+            # return default success response
+            return response()
+
         # finally start web server
         # finally start web server
         host = app_status['settings']['frontend']['host']
         host = app_status['settings']['frontend']['host']
         port = app_status['settings']['frontend']['port']
         port = app_status['settings']['frontend']['port']

+ 21 - 1
pycs/projects/Project.py

@@ -1,13 +1,18 @@
+from os import path
 from uuid import uuid1
 from uuid import uuid1
 
 
 from pycs.observable import ObservableDict
 from pycs.observable import ObservableDict
 from pycs.projects.MediaFile import MediaFile
 from pycs.projects.MediaFile import MediaFile
 from pycs.util.RecursiveDictionary import set_recursive
 from pycs.util.RecursiveDictionary import set_recursive
-from os import path
 
 
 
 
 class Project(ObservableDict):
 class Project(ObservableDict):
     def __init__(self, obj: dict, parent):
     def __init__(self, obj: dict, parent):
+        # ensure all required object keys are available
+        for key in ['data', 'labels', 'jobs']:
+            if key not in obj.keys():
+                obj[key] = {}
+
         # save data as MediaFile objects
         # save data as MediaFile objects
         for key in obj['data'].keys():
         for key in obj['data'].keys():
             obj['data'][key] = MediaFile(obj['data'][key], self)
             obj['data'][key] = MediaFile(obj['data'][key], self)
@@ -24,3 +29,18 @@ class Project(ObservableDict):
     def add_media_file(self, file):
     def add_media_file(self, file):
         file = MediaFile(file, self)
         file = MediaFile(file, self)
         self['data'][file['id']] = file
         self['data'][file['id']] = file
+
+    def add_label(self, name):
+        label_uuid = str(uuid1())
+        self['labels'][label_uuid] = {
+            'id': label_uuid,
+            'name': name
+        }
+
+    def update_label(self, identifier, name):
+        if identifier in self['labels']:
+            self['labels'][identifier]['name'] = name
+
+    def remove_label(self, identifier):
+        if identifier in self['labels']:
+            del self['labels'][identifier]

+ 1 - 0
pycs/projects/ProjectManager.py

@@ -48,6 +48,7 @@ class ProjectManager(ObservableDict):
                 'model-distribution': model
                 'model-distribution': model
             },
             },
             'data': {},
             'data': {},
+            'labels': {},
             'jobs': {}
             'jobs': {}
         }, self)
         }, self)
 
 

+ 6 - 0
webui/src/App.vue

@@ -36,6 +36,10 @@
                                  :current-project="currentProject"
                                  :current-project="currentProject"
                                  :socket="socket"/>
                                  :socket="socket"/>
 
 
+        <project-labels-window v-if="window.content === 'labels'"
+                               :current-project="currentProject"
+                               :socket="socket"/>
+
         <project-data-view-window v-if="window.content === 'view_data'"
         <project-data-view-window v-if="window.content === 'view_data'"
                                   :current-project="currentProject"
                                   :current-project="currentProject"
                                   :status="status"
                                   :status="status"
@@ -57,10 +61,12 @@ import AboutWindow from "@/components/other/about-window";
 import ProjectDataAddWindow from "@/components/projects/project-data-add-window";
 import ProjectDataAddWindow from "@/components/projects/project-data-add-window";
 import ProjectDataViewWindow from "@/components/projects/project-data-view-window";
 import ProjectDataViewWindow from "@/components/projects/project-data-view-window";
 import LoadingOverlay from "@/components/other/loading-overlay";
 import LoadingOverlay from "@/components/other/loading-overlay";
+import ProjectLabelsWindow from "@/components/projects/project-labels-window";
 
 
 export default {
 export default {
   name: 'App',
   name: 'App',
   components: {
   components: {
+    ProjectLabelsWindow,
     LoadingOverlay,
     LoadingOverlay,
     ProjectDataAddWindow,
     ProjectDataAddWindow,
     ProjectDataViewWindow,
     ProjectDataViewWindow,

+ 4 - 0
webui/src/assets/icons/tag.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path fill-rule="evenodd"
+          d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path>
+</svg>

+ 4 - 0
webui/src/assets/icons/x-circle.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
+    <path fill-rule="evenodd"
+          d="M3.404 12.596a6.5 6.5 0 119.192-9.192 6.5 6.5 0 01-9.192 9.192zM2.344 2.343a8 8 0 1011.313 11.314A8 8 0 002.343 2.343zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path>
+</svg>

+ 19 - 3
webui/src/components/base/text-input.vue

@@ -6,7 +6,9 @@
              :placeholder="placeholder"
              :placeholder="placeholder"
              :class="{error: error}"
              :class="{error: error}"
              v-bind:value="value"
              v-bind:value="value"
-             v-on:input="input">
+             v-on:input="input"
+             v-on:change="change"
+             v-on:keypress="keypress">
     </label>
     </label>
   </div>
   </div>
 </template>
 </template>
@@ -16,9 +18,23 @@ export default {
   name: "text-input",
   name: "text-input",
   props: ['value', 'placeholder', 'error'],
   props: ['value', 'placeholder', 'error'],
   methods: {
   methods: {
-    input: function(event) {
+    clear: function () {
       this.$emit('clearError', null);
       this.$emit('clearError', null);
-      this.$emit('input', event.target.value)
+    },
+    input: function (event) {
+      this.clear();
+      this.$emit('input', event.target.value);
+    },
+    change: function (event) {
+      this.clear();
+      this.$emit('change', event.target.value);
+    },
+    keypress: function (event) {
+      if (event.keyCode !== 13 && event.keyCode !== 10)
+        return;
+
+      this.clear();
+      this.$emit('enter', event.target.value)
     }
     }
   }
   }
 }
 }

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

@@ -17,6 +17,7 @@
                     :position="result"
                     :position="result"
                     :socket="socket"
                     :socket="socket"
                     :box-url="mediaUrl + '/' + result.id"
                     :box-url="mediaUrl + '/' + result.id"
+                    :labels="project.labels"
                     @move="move"
                     @move="move"
                     @resize="resize"/>
                     @resize="resize"/>
   </div>
   </div>

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

@@ -7,11 +7,35 @@
 
 
     <div class="buttons">
     <div class="buttons">
       <template v-if="!immutable">
       <template v-if="!immutable">
-        <img v-if="confirmationButton" alt="confirm" src="@/assets/icons/check.svg" @click="confirmSelf">
-        <img alt="delete" src="@/assets/icons/trash.svg" @click="deleteSelf">
+        <div v-if="position.label" class="label-text">{{ currentLabel }}</div>
+
+        <img v-if="confirmationButton"
+             alt="confirm" src="@/assets/icons/check.svg"
+             @click.stop.prevent="confirmSelf"
+             @touchstart.stop.prevent="confirmSelf">
+        <img v-if="labelButton"
+             alt="label" src="@/assets/icons/tag.svg"
+             @click.stop.prevent="showLabelSelection = true"
+             @touchstart.stop.prevent="showLabelSelection = true">
+        <img alt="delete" src="@/assets/icons/trash.svg"
+             @click.stop.prevent="deleteSelf"
+             @touchstart.stop.prevent="deleteSelf">
       </template>
       </template>
     </div>
     </div>
 
 
+    <select v-if="!immutable && 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">
+        {{ label.name }}
+      </option>
+    </select>
+
     <div class="ee" @mousedown.stop.prevent="resizeSelf('ee')" @touchstart.stop.prevent="resizeSelf('ee')"></div>
     <div class="ee" @mousedown.stop.prevent="resizeSelf('ee')" @touchstart.stop.prevent="resizeSelf('ee')"></div>
     <div class="sw" @mousedown.stop.prevent="resizeSelf('sw')" @touchstart.stop.prevent="resizeSelf('sw')"></div>
     <div class="sw" @mousedown.stop.prevent="resizeSelf('sw')" @touchstart.stop.prevent="resizeSelf('sw')"></div>
     <div class="ss" @mousedown.stop.prevent="resizeSelf('ss')" @touchstart.stop.prevent="resizeSelf('ss')"></div>
     <div class="ss" @mousedown.stop.prevent="resizeSelf('ss')" @touchstart.stop.prevent="resizeSelf('ss')"></div>
@@ -22,7 +46,12 @@
 <script>
 <script>
 export default {
 export default {
   name: "annotation-box",
   name: "annotation-box",
-  props: ['type', 'image', 'position', 'socket', 'boxUrl'],
+  props: ['type', 'image', 'position', 'socket', 'boxUrl', 'labels'],
+  data: function () {
+    return {
+      showLabelSelection: false
+    }
+  },
   computed: {
   computed: {
     immutable: function () {
     immutable: function () {
       return !this.type;
       return !this.type;
@@ -40,6 +69,15 @@ export default {
     confirmationButton: function () {
     confirmationButton: function () {
       return this.type === 'pipeline';
       return this.type === 'pipeline';
     },
     },
+    labelButton: function () {
+      return this.type !== 'pipeline';
+    },
+    labelList: function () {
+      return Object.keys(this.labels).map(key => this.labels[key]);
+    },
+    currentLabel: function () {
+      return this.labels[this.position.label].name;
+    },
     style: function () {
     style: function () {
       const left = this.image.left;
       const left = this.image.left;
       const top = this.image.top;
       const top = this.image.top;
@@ -59,8 +97,21 @@ export default {
     confirmSelf: function () {
     confirmSelf: function () {
       this.updateSelf(this.position);
       this.updateSelf(this.position);
     },
     },
+    labelSelf: function (event) {
+      const options = event.target.options;
+      const option = options[options.selectedIndex];
+      const value = option.value;
+
+      if (value)
+        this.position.label = value;
+      else
+        delete this.position.label;
+
+      this.updateSelf(this.position);
+      this.showLabelSelection = false;
+    },
     deleteSelf: function () {
     deleteSelf: function () {
-      this.socket.post(this.boxUrl, {delete: true, id: this.id});
+      this.socket.post(this.boxUrl, {delete: true});
     },
     },
     moveSelf: function (event) {
     moveSelf: function (event) {
       this.$emit('move', event, this.position, this.updateSelf);
       this.$emit('move', event, this.position, this.updateSelf);
@@ -69,6 +120,9 @@ export default {
       this.$emit('resize', this.position, mode, this.updateSelf);
       this.$emit('resize', this.position, mode, this.updateSelf);
     },
     },
     updateSelf: function (value) {
     updateSelf: function (value) {
+      if ('label' in this.position && !('label' in value))
+        value.label = this.position.label;
+
       this.socket.post(this.boxUrl, value);
       this.socket.post(this.boxUrl, value);
     }
     }
   }
   }
@@ -88,6 +142,7 @@ export default {
   display: flex;
   display: flex;
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
+  position: relative;
 }
 }
 
 
 img {
 img {
@@ -111,4 +166,19 @@ img {
 .nw, .se {
 .nw, .se {
   cursor: nwse-resize;
   cursor: nwse-resize;
 }
 }
+
+select {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.label-text {
+  position: absolute;
+  bottom: 0;
+  color: whitesmoke;
+  font-size: 80%;
+}
 </style>
 </style>

+ 117 - 0
webui/src/components/projects/project-labels-window.vue

@@ -0,0 +1,117 @@
+<template>
+  <div class="project-labels-window">
+    <div class="label" v-for="label in labels" :key="label.id">
+      <h2>
+        <img alt="delete" src="@/assets/icons/x-circle.svg" @click="removeLabel(label.id)">
+        {{ label.name }}
+      </h2>
+
+      <media-selector v-if="labeledImages[label.id].length > 0"
+                      :project-id="currentProject.id"
+                      :media="labeledImages[label.id]"
+                      current="-1"
+                      :socket="socket"></media-selector>
+      <div v-else
+           class="no-elements">
+        No labeled images found...
+      </div>
+    </div>
+
+    <div class="label">
+      <text-input placeholder="New Label"
+                  v-model="createLabelValue"
+                  @change="createLabel"
+                  @enter="createLabel"/>
+    </div>
+  </div>
+</template>
+
+<script>
+import TextInput from "@/components/base/text-input";
+import MediaSelector from "@/components/media/media-selector";
+
+export default {
+  name: "project-labels-window",
+  components: {MediaSelector, TextInput},
+  props: ['currentProject', 'socket'],
+  data: function () {
+    return {
+      createLabelValue: ''
+    }
+  },
+  computed: {
+    labels: function () {
+      let labels = Object.keys(this.currentProject.labels)
+          .map(e => this.currentProject.labels[e]);
+      labels.sort((a, b) => a.name < b.name ? -1 : +1);
+
+      return labels;
+    },
+    labeledImages: function () {
+      const result = {};
+
+      for (let label of this.labels)
+        result[label.id] = [];
+
+      for (let image of Object.values(this.currentProject.data)) {
+        for (let prediction of Object.values(image['predictionResults'])) {
+          if ('label' in prediction && !result[prediction.label].includes(image)) {
+            result[prediction.label].push(image);
+          }
+        }
+      }
+
+      return result;
+    }
+  },
+  methods: {
+    createLabel: function (value) {
+      if (!this.createLabelValue)
+        return;
+
+      this.socket.post('/projects/' + this.currentProject.id + '/labels', {name: value});
+      this.createLabelValue = '';
+    },
+    removeLabel: function (id) {
+      this.socket.post('/projects/' + this.currentProject.id + '/labels/' + id, {delete: true});
+    }
+  }
+}
+</script>
+
+<style scoped>
+.project-labels-window {
+  padding: 1rem;
+}
+
+.label {
+  margin-bottom: 1rem;
+}
+
+h2 {
+  display: flex;
+  align-items: center;
+  margin: 0;
+}
+
+img {
+  width: 1.5rem;
+  margin-right: 0.75rem;
+}
+
+/deep/ .element {
+  border: none;
+}
+
+/deep/ .element:not(:first-child) {
+  margin-left: 0.1rem;
+}
+
+/deep/ .element:not(:last-child) {
+  margin-right: 0.1rem;
+}
+
+.no-elements {
+  margin-top: 0.5rem;
+}
+</style>

+ 1 - 1
webui/src/components/projects/project-open-window.vue

@@ -127,7 +127,7 @@ h2 {
   cursor: pointer;
   cursor: pointer;
 }
 }
 
 
-.description {
+h2, .description {
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;

+ 7 - 0
webui/src/components/window/side-navigation-bar.vue

@@ -28,6 +28,13 @@
         <span>Add Data</span>
         <span>Add Data</span>
       </div>
       </div>
 
 
+      <div class="item"
+           :class="{active: window.content === 'labels', inactive: !currentProject}"
+           @click="ifProjectIsOpened(show, 'labels')">
+        <img src="@/assets/icons/tag.svg">
+        <span>Labels</span>
+      </div>
+
       <div class="item"
       <div class="item"
            :class="{active: window.content === 'view_data', inactive: !currentProject || !mediaAvailable}"
            :class="{active: window.content === 'view_data', inactive: !currentProject || !mediaAvailable}"
            @click="ifProjectIsOpened(show, 'view_data')">
            @click="ifProjectIsOpened(show, 'view_data')">