Browse Source

Resolve "save unmanaged data out of project file"

Eric Tröbs 4 years ago
parent
commit
cf6c7e446c

+ 62 - 21
pycs/frontend/WebServer.py

@@ -32,9 +32,11 @@ class WebServer:
             self.__flask = Flask(__name__)
             self.__app = socketio.WSGIApp(self.__sio, self.__flask, static_files=static_files)
 
-            def response():
-                rsp = make_response()
-                return rsp
+            def response(data=None):
+                if data is None:
+                    return make_response()
+                else:
+                    return make_response(data)
         else:
             print('development build')
 
@@ -42,8 +44,12 @@ class WebServer:
             self.__flask = Flask(__name__)
             self.__app = socketio.WSGIApp(self.__sio, self.__flask)
 
-            def response():
-                rsp = make_response()
+            def response(data=None):
+                if data is None:
+                    rsp = make_response()
+                else:
+                    rsp = make_response(data)
+
                 rsp.headers['Access-Control-Allow-Origin'] = '*'
                 return rsp
 
@@ -152,6 +158,27 @@ class WebServer:
             # return default success response
             return response()
 
+        @self.__flask.route('/projects/<project_identifier>/unmanaged/<int:file_number>', methods=['GET'])
+        def get_unmanaged_file(project_identifier, file_number):
+            # 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]
+
+            # abort if file id is not valid
+            if file_number >= len(project.unmanaged_files_keys):
+                return make_response('file does not exist', 500)
+
+            file_identifier = project.unmanaged_files_keys[file_number]
+            if file_identifier not in project.unmanaged_files_keys:
+                return make_response('file does not exist', 500)
+
+            target_object = project.unmanaged_files[file_identifier]
+
+            # return element data
+            return response(target_object.get_data())
+
         @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>', defaults={'size': None}, methods=['GET'])
         @self.__flask.route('/projects/<project_identifier>/data/<file_identifier>/<size>', methods=['GET'])
         def get_file(project_identifier, file_identifier, size):
@@ -162,11 +189,10 @@ class WebServer:
             project = app_status['projects'][project_identifier]
 
             # abort if file id is not valid
-            if file_identifier not in project['data'].keys():
+            target_object = project.get_media_file(file_identifier)
+            if target_object is None:
                 return make_response('file does not exist', 500)
 
-            target_object = project['data'][file_identifier]
-
             # resize image to requested size
             if size is not None:
                 target_object = target_object.resize(size)
@@ -187,11 +213,10 @@ class WebServer:
             project = app_status['projects'][project_identifier]
 
             # abort if file id is not valid
-            if file_identifier not in project['data'].keys():
+            target_object = project.get_media_file(file_identifier)
+            if target_object is None:
                 return make_response('file does not exist', 500)
 
-            target_object = project['data'][file_identifier]
-
             # add result
             result = request.get_json(force=True)
             if result:
@@ -219,11 +244,10 @@ class WebServer:
             project = app_status['projects'][project_identifier]
 
             # abort if file id is not valid
-            if file_identifier not in project['data'].keys():
+            target_object = project.get_media_file(file_identifier)
+            if target_object is None:
                 return make_response('file does not exist', 500)
 
-            target_object = project['data'][file_identifier]
-
             # parse post data
             result = request.get_json(force=True)
             if result:
@@ -287,19 +311,36 @@ class WebServer:
             # create export
             result = []
 
-            for data_key in project['data']:
-                data_obj = project['data'][data_key]
+            def mk_obj(type, name, extension, predictions):
                 data_res = {
-                    'type': data_obj['type'],
-                    'filename': data_obj['name'] + data_obj['extension'],
+                    'type': type,
+                    'filename': name + extension,
                     'predictions': []
                 }
 
-                for result_key in data_obj['predictionResults']:
-                    result_obj = data_obj['predictionResults'][result_key]
+                for result_key in predictions:
+                    result_obj = predictions[result_key]
                     data_res['predictions'].append(result_obj)
 
-                result.append(data_res)
+                return data_res
+
+            for data_key in project['data']:
+                data_obj = project['data'][data_key]
+                result.append(mk_obj(
+                    data_obj['type'],
+                    data_obj['name'],
+                    data_obj['extension'],
+                    data_obj['predictionResults']
+                ))
+
+            for data_key in project.unmanaged_files:
+                data_obj = project.unmanaged_files[data_key].get_data()
+                result.append(mk_obj(
+                    data_obj['type'],
+                    data_obj['id'],
+                    data_obj['extension'],
+                    data_obj['predictionResults']
+                ))
 
             # send to user
             rsp = make_response(dumps(result))

+ 54 - 25
pycs/projects/Project.py

@@ -8,6 +8,8 @@ from eventlet import spawn_after
 from pycs.observable import ObservableDict
 from pycs.pipeline.PipelineManager import PipelineManager
 from pycs.projects.ImageFile import ImageFile
+from pycs.projects.UnmanagedImageFile import UnmanagedImageFile
+from pycs.projects.UnmanagedVideoFile import UnmanagedVideoFile
 from pycs.projects.VideoFile import VideoFile
 from pycs.util.RecursiveDictionary import set_recursive
 
@@ -19,6 +21,9 @@ class Project(ObservableDict):
         self.pipeline_manager = None
         self.quit_pipeline_thread = None
 
+        self.unmanaged_files_keys = []
+        self.unmanaged_files = {}
+
         # ensure all required object keys are available
         for key in ['data', 'labels', 'jobs']:
             if key not in obj.keys():
@@ -32,18 +37,31 @@ class Project(ObservableDict):
 
             obj['model'] = model
 
+        # save data as MediaFile objects
+        if obj['unmanaged'] is None:
+            for key in obj['data'].keys():
+                obj['data'][key] = self.create_media_file(obj['data'][key])
+
         # handle unmanaged files
-        if obj['unmanaged'] is not None:
+        else:
+            prev = None
             for file in listdir(obj['unmanaged']):
-                if file not in obj['data'].keys():
-                    name, ext = splitext(file)
-                    uuid = name
+                uuid, ext = splitext(file)
 
-                    obj['data'][uuid] = self.create_media_file_dict(uuid, name, ext, 0, 0)
+                next = {
+                    'id': uuid,
+                    'extension': ext
+                }
+                next = self.create_media_file(next, unmanaged=True)
 
-        # save data as MediaFile objects
-        for key in obj['data'].keys():
-            obj['data'][key] = self.create_media_file(obj['data'][key], self)
+                if prev is not None:
+                    next.prev(prev)
+                    prev.next(next)
+
+                prev = next
+
+                self.unmanaged_files_keys.append(uuid)
+                self.unmanaged_files[uuid] = next
 
         # initialize super
         super().__init__(obj, parent)
@@ -63,35 +81,46 @@ class Project(ObservableDict):
     def update_properties(self, update):
         set_recursive(update, self)
 
-    def new_media_file_path(self):
-        return path.join('projects', self['id'], 'data'), str(uuid1())
+    def get_media_file(self, identifier):
+        if self['unmanaged']:
+            if identifier not in self.unmanaged_files_keys:
+                return None
 
-    @staticmethod
-    def create_media_file_dict(uuid, name, extension, size, created):
-        return {
-            'id': uuid,
-            'name': name,
-            'extension': extension,
-            'size': size,
-            'created': created
-        }
+            return self.unmanaged_files[identifier]
+        else:
+            if identifier not in self['data'].keys():
+                return None
+
+            return self['data'][identifier]
 
-    def create_media_file(self, file, project=None):
-        if project is None:
-            project = self
+    def new_media_file_path(self):
+        return path.join('projects', self['id'], 'data'), str(uuid1())
 
+    def create_media_file(self, file, unmanaged=False):
         # TODO check file extension
         # TODO determine type
         # TODO filter supported types
         if file['extension'] in ['.jpg', '.png']:
-            return ImageFile(file, project)
+            if unmanaged:
+                return UnmanagedImageFile(file, self)
+            else:
+                return ImageFile(file, self)
         if file['extension'] in ['.mp4']:
-            return VideoFile(file, project)
+            if unmanaged:
+                return UnmanagedVideoFile(file, self)
+            else:
+                return VideoFile(file, self)
 
         raise NotImplementedError
 
     def add_media_file(self, uuid, name, extension, size, created):
-        file = self.create_media_file_dict(uuid, name, extension, size, created)
+        file = {
+            'id': uuid,
+            'name': name,
+            'extension': extension,
+            'size': size,
+            'created': created
+        }
         self['data'][file['id']] = self.create_media_file(file)
 
     def remove_media_file(self, file_id):

+ 97 - 0
pycs/projects/UnmanagedImageFile.py

@@ -0,0 +1,97 @@
+from json import load, dump
+from os.path import join, exists
+from uuid import uuid1
+
+from pycs.projects.ImageFile import ImageFile
+
+
+class UnmanagedImageFile(ImageFile):
+    def __init__(self, obj, project):
+        obj['prev'] = None
+        obj['next'] = None
+
+        super().__init__(obj, project, 'data')
+        del self['predictionResults']
+
+    def prev(self, value):
+        self['prev'] = value['id']
+
+    def next(self, value):
+        self['next'] = value['id']
+
+    @property
+    def data_path(self):
+        return join('projects', self.project['id'], 'data', self['id'] + '.json')
+
+    def __load(self):
+        if not exists(self.data_path):
+            return {}
+
+        with open(self.data_path, 'r') as f:
+            return load(f)
+
+    def __write(self, data):
+        with open(self.data_path, 'w') as f:
+            dump(data, f, indent=4)
+
+    def get_data(self):
+        copy = self.copy()
+        copy['predictionResults'] = self.__load()
+        return copy
+
+    def add_global_result(self, result, origin='user'):
+        self.remove_global_result()
+        self.add_result(result, origin)
+
+    def remove_global_result(self):
+        data = self.__load()
+
+        delete = []
+        for result_id in data:
+            if 'x' not in data[result_id]:
+                delete.append(result_id)
+
+        for result_id in delete:
+            del data[result_id]
+
+        self.__write(data)
+
+    def add_result(self, result, origin='user'):
+        data = self.__load()
+
+        result['id'] = str(uuid1())
+        result['origin'] = origin
+
+        data[result['id']] = result
+
+        self.__write(data)
+
+    def remove_result(self, identifier):
+        data = self.__load()
+
+        del data[identifier]
+
+        self.__write(data)
+
+    def remove_pipeline_results(self):
+        data = self.__load()
+
+        remove = list(filter(lambda k: data[k]['origin'] == 'pipeline', data.keys()))
+
+        for key in remove:
+            del data[key]
+
+        self.__write(data)
+
+    def remove_results(self):
+        self.__write({})
+
+    def update_result(self, identifier, result, origin='user'):
+        data = self.__load()
+
+        result['id'] = identifier
+        result['origin'] = origin
+
+        data[identifier] = result
+
+        self.__write(data)

+ 97 - 0
pycs/projects/UnmanagedVideoFile.py

@@ -0,0 +1,97 @@
+from json import load, dump
+from os.path import join, exists
+from uuid import uuid1
+
+from pycs.projects.VideoFile import VideoFile
+
+
+class UnmanagedVideoFile(VideoFile):
+    def __init__(self, obj, project):
+        obj['prev'] = None
+        obj['next'] = None
+
+        super().__init__(obj, project, 'data')
+        del self['predictionResults']
+
+    def prev(self, value):
+        self['prev'] = value['id']
+
+    def next(self, value):
+        self['next'] = value['id']
+
+    @property
+    def data_path(self):
+        return join('projects', self.project['id'], 'data', self['id'] + '.json')
+
+    def __load(self):
+        if not exists(self.data_path):
+            return {}
+
+        with open(self.data_path, 'r') as f:
+            return load(f)
+
+    def __write(self, data):
+        with open(self.data_path, 'w') as f:
+            dump(data, f, indent=4)
+
+    def get_data(self):
+        copy = self.copy()
+        copy['predictionResults'] = self.__load()
+        return copy
+
+    def add_global_result(self, result, origin='user'):
+        self.remove_global_result()
+        self.add_result(result, origin)
+
+    def remove_global_result(self):
+        data = self.__load()
+
+        delete = []
+        for result_id in data:
+            if 'x' not in data[result_id]:
+                delete.append(result_id)
+
+        for result_id in delete:
+            del data[result_id]
+
+        self.__write(data)
+
+    def add_result(self, result, origin='user'):
+        data = self.__load()
+
+        result['id'] = str(uuid1())
+        result['origin'] = origin
+
+        data[result['id']] = result
+
+        self.__write(data)
+
+    def remove_result(self, identifier):
+        data = self.__load()
+
+        del data[identifier]
+
+        self.__write(data)
+
+    def remove_pipeline_results(self):
+        data = self.__load()
+
+        remove = list(filter(lambda k: data[k]['origin'] == 'pipeline', data.keys()))
+
+        for key in remove:
+            del data[key]
+
+        self.__write(data)
+
+    def remove_results(self):
+        self.__write({})
+
+    def update_result(self, identifier, result, origin='user'):
+        data = self.__load()
+
+        result['id'] = identifier
+        result['origin'] = origin
+
+        data[identifier] = result
+
+        self.__write(data)

+ 8 - 0
webui/src/App.vue

@@ -90,6 +90,14 @@ export default {
         // initialize socket.io connection
         io: io(window.location.protocol + '//' + window.location.hostname + ':5000'),
         // http methods
+        get: function (name) {
+          if (!name.startsWith('http'))
+            name = this.baseurl + name;
+
+          return fetch(name, {
+            method: 'GET'
+          });
+        },
         post: function (name, value) {
           if (!name.startsWith('http'))
             name = this.baseurl + name;

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

@@ -40,7 +40,8 @@
                     :labels="project.labels"
                     :supports="project.model.supports"
                     @move="move"
-                    @resize="resize"/>
+                    @resize="resize"
+                    @update="$emit('update', true)"/>
   </div>
 </template>
 
@@ -141,6 +142,9 @@ export default {
   },
   methods: {
     resizeEvent: function () {
+      if (!this.$refs.image)
+        return;
+
       const element = this.$refs.image.getBoundingClientRect();
       const parent = this.$refs.image.parentElement.getBoundingClientRect();
 
@@ -249,6 +253,8 @@ export default {
         this.fixed = false;
         this.current = false;
         this.callback = false;
+
+        this.$emit('update', true);
       }
     },
     move: function (event, position, callback) {

+ 27 - 14
webui/src/components/media/annotated-media-view.vue

@@ -5,7 +5,8 @@
                        :project="currentProject"
                        :socket="socket"
                        :filter="filterValue"
-                       :extremeClicking="extremeClicking"/>
+                       :extremeClicking="extremeClicking"
+                       @update="update"/>
     </div>
 
     <div class="control">
@@ -20,7 +21,8 @@
                      :filter="filterValue"
                      @filter="filter"
                      :extremeClicking="extremeClicking"
-                     @extremeClicking="extremeClicking = $event"/>
+                     @extremeClicking="extremeClicking = $event"
+                     @update="update"/>
     </div>
 
     <div class="selector" v-if="showMediaSelector && !currentProject.unmanaged">
@@ -45,26 +47,23 @@ export default {
   data: function () {
     return {
       current: 0,
+      hasPrevious: false,
+      hasNext: false,
+      currentMedia: null,
       showMediaSelector: true,
       filterValue: '',
       extremeClicking: false
     };
   },
+  watch: {
+    current: {
+      immediate: true,
+      handler: 'update'
+    }
+  },
   computed: {
-    hasPrevious: function () {
-      return this.current > 0;
-    },
-    hasNext: function () {
-      return this.current < this.media.length - 1;
-    },
     media: function () {
       return Object.keys(this.currentProject.data).map(key => this.currentProject.data[key]);
-    },
-    currentMedia: function () {
-      if (this.current < this.media.length)
-        return this.media[this.current];
-      else
-        return false;
     }
   },
   methods: {
@@ -76,6 +75,20 @@ export default {
       if (this.hasNext)
         this.current += 1;
     },
+    update: async function (newVal) {
+      if (this.currentProject.unmanaged) {
+        const response = await this.socket.get('/projects/' + this.currentProject.id + '/unmanaged/' + this.current);
+        const data = await response.json();
+
+        this.hasPrevious = data.prev !== null;
+        this.hasNext = data.next !== null;
+        this.currentMedia = data;
+      } else {
+        this.hasPrevious = newVal > 0;
+        this.hasNext = newVal < this.media.length - 1;
+        this.currentMedia = newVal < this.media.length ? this.media[this.current] : null;
+      }
+    },
     predict: function () {
       this.socket.post('/projects/' + this.currentProject.id, {
         'predict': [this.currentMedia.id]

+ 2 - 0
webui/src/components/media/annotation-box.vue

@@ -122,6 +122,7 @@ export default {
     },
     deleteSelf: function () {
       this.socket.post(this.boxUrl, {delete: true});
+      this.$emit('update', true);
     },
     moveSelf: function (event) {
       this.$emit('move', event, this.position, this.updateSelf);
@@ -134,6 +135,7 @@ export default {
         value.label = this.position.label;
 
       this.socket.post(this.boxUrl, value);
+      this.$emit('update', true);
     }
   }
 }

+ 4 - 0
webui/src/components/media/media-control.vue

@@ -137,6 +137,8 @@ export default {
         label: value ? value : false
       });
       this.showLabelSelection = false;
+
+      this.$emit('update', true);
     },
     filterSelf: function (event) {
       this.$emit('filter', event.target.value);
@@ -145,6 +147,8 @@ export default {
       this.socket.post(this.mediaUrl, {
         reset: true
       });
+
+      this.$emit('update', true);
     }
   }
 }

+ 5 - 1
webui/src/components/window/side-navigation-bar.vue

@@ -84,7 +84,11 @@ export default {
       return this.status.settings.frontend.collapse;
     },
     mediaAvailable: function () {
-      return this.currentProject && 'data' in this.currentProject && Object.keys(this.currentProject.data).length > 0;
+      return this.currentProject
+          && (
+              this.currentProject.unmanaged
+              || ('data' in this.currentProject && Object.keys(this.currentProject.data).length > 0)
+          );
     },
     dataEnabled: function () {
       return this.currentProject && !this.currentProject.unmanaged;