瀏覽代碼

Merge branch 'master' into ammod

Dimitri Korsch 3 年之前
父節點
當前提交
d2c1e05e8c
共有 33 個文件被更改,包括 1408 次插入116 次删除
  1. 964 0
      labels/lepiforum_version_7/Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv
  2. 62 0
      labels/lepiforum_version_7/Provider.py
  3. 26 0
      labels/lepiforum_version_7/RowWrapper.py
  4. 11 0
      labels/lepiforum_version_7/configuration1.json
  5. 11 0
      labels/lepiforum_version_7/configuration2.json
  6. 11 0
      labels/lepiforum_version_7/configuration3.json
  7. 11 0
      labels/lepiforum_version_7/configuration4.json
  8. 1 1
      pycs/database/Collection.py
  9. 19 4
      pycs/database/Database.py
  10. 7 1
      pycs/database/File.py
  11. 32 0
      pycs/database/LabelProvider.py
  12. 38 5
      pycs/database/Project.py
  13. 27 9
      pycs/database/discovery/LabelProviderDiscovery.py
  14. 1 0
      pycs/frontend/WebServer.py
  15. 2 2
      pycs/frontend/endpoints/data/UploadFile.py
  16. 13 6
      pycs/frontend/endpoints/pipelines/FitModel.py
  17. 28 9
      pycs/frontend/endpoints/pipelines/PredictModel.py
  18. 2 3
      pycs/frontend/endpoints/projects/ExecuteLabelProvider.py
  19. 2 2
      pycs/frontend/endpoints/results/GetProjectResults.py
  20. 15 2
      pycs/frontend/notifications/NotificationList.py
  21. 5 5
      pycs/interfaces/LabelProvider.py
  22. 10 0
      pycs/interfaces/MediaBoundingBox.py
  23. 12 7
      pycs/interfaces/MediaFile.py
  24. 12 2
      pycs/interfaces/MediaFileList.py
  25. 9 0
      pycs/interfaces/MediaImageLabel.py
  26. 4 0
      pycs/interfaces/MediaLabel.py
  27. 4 4
      pycs/interfaces/MediaStorage.py
  28. 2 2
      pycs/jobs/JobRunner.py
  29. 0 28
      pycs/util/LabelProviderUtil.py
  30. 4 0
      pycs/util/PipelineCache.py
  31. 20 4
      webui/src/components/projects/project-creation-window.vue
  32. 28 17
      webui/src/components/projects/project-labels-window.vue
  33. 15 3
      webui/src/components/projects/settings/general-settings.vue

File diff suppressed because it is too large
+ 964 - 0
labels/lepiforum_version_7/Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv


+ 62 - 0
labels/lepiforum_version_7/Provider.py

@@ -0,0 +1,62 @@
+import csv
+import typing
+from os import path
+
+from pycs.interfaces.LabelProvider import LabelProvider
+from .RowWrapper import RowWrapper
+
+
+class Provider(LabelProvider):
+    def __init__(self, root_folder, configuration):
+        self.csv_path = path.join(root_folder, configuration['filename'])
+        self.csv_minimum_rarity = configuration['minimumRarity']
+        self.csv_all_hierarchy_levels = configuration['includeAllHierarchyLevels']
+
+    def close(self):
+        pass
+
+    def get_labels(self) -> typing.List[dict]:
+        result = []
+
+        with open(self.csv_path, mode='r', newline='', encoding='utf8') as csv_file:
+            # skip first line which contains column names
+            csv_file.readline()
+
+            # read csv line by line
+            reader = csv.reader(csv_file, delimiter='\t')
+            entries = list(map(RowWrapper, reader))
+
+        # filter
+        if self.csv_minimum_rarity is not None:
+            entries = filter(lambda row: row.rarity_is_larger_than(self.csv_minimum_rarity),
+                             entries)
+
+        # create result set
+        for entry in entries:
+            entry = entry.__dict__
+            parent_reference = None
+
+            # add hierarchy
+            if self.csv_all_hierarchy_levels:
+                hierarchy_levels = ('superfamily', 'family', 'subfamily', 'tribe', 'genus')
+            else:
+                hierarchy_levels = ('family', 'genus')
+
+            for tax in hierarchy_levels:
+                if entry[tax] is not None:
+                    reference, name = entry[tax].lower(), entry[tax]
+                    result.append(self.create_label(reference, name, parent_reference))
+
+                    parent_reference = reference
+
+            # add element
+            if entry['kr_number'].isnumeric():
+                name = f'{entry["genus"]} {entry["species"]} ({entry["kr_number"]})'
+                reference = entry['kr_number']
+            else:
+                name = f'{entry["genus"]} {entry["species"]}'
+                reference = name.lower()
+
+            result.append(self.create_label(reference, name, parent_reference))
+
+        return result

+ 26 - 0
labels/lepiforum_version_7/RowWrapper.py

@@ -0,0 +1,26 @@
+class RowWrapper:
+    def __init__(self, row: list):
+        self.local_occurrence = self.__empty_to_none(row[0])
+        self.rarity = self.__empty_to_none(row[1])
+        self.superfamily = self.__empty_to_none(row[2])
+        self.family = self.__empty_to_default(row[3], self.superfamily)
+        self.subfamily = self.__empty_to_default(row[4], self.family)
+        self.tribe = self.__empty_to_default(row[5], self.subfamily)
+        self.kr_number = self.__empty_to_none(row[9])
+        self.genus = self.__empty_to_default(row[10], self.tribe)
+        self.species = self.__empty_to_none(row[11])
+
+    def rarity_is_larger_than(self, limit: int):
+        return self.rarity is not None and self.rarity.isnumeric() and limit < int(self.rarity)
+
+    @staticmethod
+    def __empty_to_none(val: str):
+        return val if val.strip() else None
+
+    @staticmethod
+    def __empty_to_default(val: str, default: str):
+        val = RowWrapper.__empty_to_none(val)
+        if val is not None:
+            return val
+
+        return default

+ 11 - 0
labels/lepiforum_version_7/configuration1.json

@@ -0,0 +1,11 @@
+{
+  "name": "Lepiforum Version 7 Hierarchic / most common, reduced hierarchy depth",
+  "description": "Stand: 01.01.2020, bearbeitet GBrehm",
+  "code": {
+    "module": "Provider",
+    "class": "Provider"
+  },
+  "filename": "Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv",
+  "minimumRarity": 0,
+  "includeAllHierarchyLevels": false
+}

+ 11 - 0
labels/lepiforum_version_7/configuration2.json

@@ -0,0 +1,11 @@
+{
+  "name": "Lepiforum Version 7 Hierarchic / reduced hierarchy depth",
+  "description": "Stand: 01.01.2020, bearbeitet GBrehm",
+  "code": {
+    "module": "Provider",
+    "class": "Provider"
+  },
+  "filename": "Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv",
+  "minimumRarity": null,
+  "includeAllHierarchyLevels": false
+}

+ 11 - 0
labels/lepiforum_version_7/configuration3.json

@@ -0,0 +1,11 @@
+{
+  "name": "Lepiforum Version 7 Hierarchic / most common",
+  "description": "Stand: 01.01.2020, bearbeitet GBrehm",
+  "code": {
+    "module": "Provider",
+    "class": "Provider"
+  },
+  "filename": "Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv",
+  "minimumRarity": 0,
+  "includeAllHierarchyLevels": true
+}

+ 11 - 0
labels/lepiforum_version_7/configuration4.json

@@ -0,0 +1,11 @@
+{
+  "name": "Lepiforum Version 7 Hierarchic",
+  "description": "Stand: 01.01.2020, bearbeitet GBrehm",
+  "code": {
+    "module": "Provider",
+    "class": "Provider"
+  },
+  "filename": "Lepiforums-Europaliste_Schmetterlinge_Version_7_Stand_2020_01_01_bearbeitet_GBrehm.csv",
+  "minimumRarity": null,
+  "includeAllHierarchyLevels": true
+}

+ 1 - 1
pycs/database/Collection.py

@@ -18,7 +18,7 @@ class Collection:
         self.name = row[3]
         self.description = row[4]
         self.position = row[5]
-        self.autoselect = False if row[6] == 0 else True
+        self.autoselect = row[6] > 0
 
     def set_name(self, name: str):
         """

+ 19 - 4
pycs/database/Database.py

@@ -46,10 +46,12 @@ class Database:
                     ''')
                     cursor.execute('''
                         CREATE TABLE IF NOT EXISTS label_providers (
-                            id          INTEGER PRIMARY KEY,
-                            name        TEXT                NOT NULL,
-                            description TEXT,
-                            root_folder TEXT                NOT NULL UNIQUE
+                            id                 INTEGER PRIMARY KEY,
+                            name               TEXT                NOT NULL,
+                            description        TEXT,
+                            root_folder        TEXT                NOT NULL,
+                            configuration_file TEXT                NOT NULL,
+                            UNIQUE(root_folder, configuration_file)
                         )
                     ''')
 
@@ -140,12 +142,25 @@ class Database:
                 discover_label_providers(self.con)
 
     def close(self):
+        """
+        close database file
+        """
         self.con.close()
 
     def copy(self):
+        """
+        Create a copy of this database object. This can be used to access the database
+        from another thread. Table initialization and model and label provider discovery is
+        disabled to speedup this function.
+
+        :return: Database
+        """
         return Database(self.path, initialization=False, discovery=False)
 
     def commit(self):
+        """
+        commit changes
+        """
         self.con.commit()
 
     def __enter__(self):

+ 7 - 1
pycs/database/File.py

@@ -160,7 +160,7 @@ class File:
                     LIMIT 1
                 ''', (self.identifier, self.project_id))
             else:
-                cursor.execute(''' 
+                cursor.execute('''
                     SELECT * FROM files
                     WHERE id > ? AND project = ? AND collection = ?
                     ORDER BY id ASC
@@ -226,6 +226,12 @@ class File:
             return self.result(cursor.lastrowid)
 
     def remove_results(self, origin='pipeline') -> List[Result]:
+        """
+        remove all results with the specified origin
+
+        :param origin: either 'pipeline' or 'user'
+        :return: list of removed results
+        """
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
                 SELECT * FROM results WHERE file = ? AND origin = ?

+ 32 - 0
pycs/database/LabelProvider.py

@@ -1,3 +1,9 @@
+import json
+from os import path
+
+from pycs.interfaces.LabelProvider import LabelProvider as LabelProviderInterface
+
+
 class LabelProvider:
     """
     database class for label providers
@@ -10,3 +16,29 @@ class LabelProvider:
         self.name = row[1]
         self.description = row[2]
         self.root_folder = row[3]
+        self.configuration_file = row[4]
+
+    @property
+    def configuration_path(self):
+        return path.join(self.root_folder, self.configuration_file)
+
+    def load(self) -> LabelProviderInterface:
+        """
+        load configuration.json and create an instance from the included code object
+
+        :return: LabelProvider instance
+        """
+        # load configuration.json
+        with open(self.configuration_path, 'r') as configuration_file:
+            configuration = json.load(configuration_file)
+
+        # load code
+        code_path = path.join(self.root_folder, configuration['code']['module'])
+        module_name = code_path.replace('/', '.').replace('\\', '.')
+        class_name = configuration['code']['class']
+
+        imported_module = __import__(module_name, fromlist=[class_name])
+        class_attr = getattr(imported_module, class_name)
+
+        # return instance
+        return class_attr(self.root_folder, configuration)

+ 38 - 5
pycs/database/Project.py

@@ -1,7 +1,7 @@
 from contextlib import closing
 from os.path import join
 from time import time
-from typing import List, Optional, Tuple, Iterator
+from typing import List, Optional, Tuple, Iterator, Union
 
 from pycs.database.Collection import Collection
 from pycs.database.File import File
@@ -77,26 +77,48 @@ class Project:
 
             return None
 
+    def label_by_reference(self, reference: str) -> Optional[Label]:
+        """
+        get a label using its reference string
+
+        :param reference: reference string
+        :return: label
+        """
+        with closing(self.database.con.cursor()) as cursor:
+            cursor.execute('SELECT * FROM labels WHERE reference = ? AND project = ?',
+                           (reference, self.identifier))
+            row = cursor.fetchone()
+
+            if row is not None:
+                return Label(self.database, row)
+
+            return None
+
     def create_label(self, name: str, reference: str = None,
-                     parent_id: int = None) -> Tuple[Optional[Label], bool]:
+                     parent: Union[Label, int, str] = None) -> Tuple[Optional[Label], bool]:
         """
         create a label for this project. If there is already a label with the same reference
         in the database its name is updated.
 
         :param name: label name
         :param reference: label reference
-        :param parent_id: parent's identifier
+        :param parent: either parent identifier, parent reference string or `Label` object
         :return: created or edited label, insert
         """
         created = int(time())
 
+        if isinstance(parent, str):
+            parent = self.label_by_reference(parent)
+        if isinstance(parent, Label):
+            parent = parent.identifier
+
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
                 INSERT INTO labels (project, parent, created, reference, name)
                 VALUES (?, ?, ?, ?, ?)
                 ON CONFLICT (project, reference) DO
                 UPDATE SET parent = ?, name = ?
-            ''', (self.identifier, parent_id, created, reference, name, parent_id, name))
+            ''', (self.identifier, parent, created, reference, name, parent, name))
 
             # lastrowid is 0 if on conflict clause applies.
             # If this is the case we do an extra query to receive the row id.
@@ -165,7 +187,18 @@ class Project:
                           name: str,
                           description: str,
                           position: int,
-                          autoselect: bool):
+                          autoselect: bool) -> Tuple[Collection, bool]:
+        """
+        create a new collection associated with this project
+
+        :param reference: collection reference string
+        :param name: collection name
+        :param description: collection description
+        :param position: position in menus
+        :param autoselect: automatically select this collection on session load
+
+        :return: collection object, insert
+        """
         autoselect = 1 if autoselect else 0
 
         with closing(self.database.con.cursor()) as cursor:

+ 27 - 9
pycs/database/discovery/LabelProviderDiscovery.py

@@ -1,7 +1,26 @@
+import re
 from contextlib import closing
 from glob import glob
 from json import load
-from os import path
+from os import path, listdir
+
+
+def __find_files():
+    # list folders in labels/
+    for folder in glob('labels/*'):
+        # list files
+        for filename in listdir(folder):
+            file_path = path.join(folder, filename)
+
+            # filter configuration files
+            if not path.isfile(file_path):
+                continue
+
+            if not re.match(r'^configuration(\d+)?\.json$', filename):
+                continue
+
+            # yield element
+            yield folder, filename, file_path
 
 
 def discover(database):
@@ -12,10 +31,9 @@ def discover(database):
     :return:
     """
     with closing(database.cursor()) as cursor:
-        # list folders in labels/
-        for folder in glob('labels/*'):
-            # load distribution.json
-            with open(path.join(folder, 'configuration.json'), 'r') as file:
+        for folder, configuration_file, configuration_path in __find_files():
+            # load configuration file
+            with open(configuration_path, 'r') as file:
                 label = load(file)
 
             # extract data
@@ -24,8 +42,8 @@ def discover(database):
 
             # save to database
             cursor.execute('''
-                INSERT INTO label_providers (name, description, root_folder)
-                VALUES (?, ?, ?)
-                ON CONFLICT (root_folder)
+                INSERT INTO label_providers (name, description, root_folder, configuration_file)
+                VALUES (?, ?, ?, ?)
+                ON CONFLICT (root_folder, configuration_file)
                 DO UPDATE SET name = ?, description = ?
-            ''', (name, description, folder, name, description))
+            ''', (name, description, folder, configuration_file, name, description))

+ 1 - 0
pycs/frontend/WebServer.py

@@ -55,6 +55,7 @@ class WebServer:
     """
 
     # pylint: disable=line-too-long
+    # pylint: disable=too-many-statements
     def __init__(self, settings: dict, database: Database, jobs: JobRunner, pipelines: PipelineCache):
         # initialize web server
         if exists('webui/index.html'):

+ 2 - 2
pycs/frontend/endpoints/data/UploadFile.py

@@ -56,8 +56,8 @@ class UploadFile(View):
         try:
             ftype, frames, fps = tpool.execute(file_info,
                                                self.data_folder, self.file_id, self.file_extension)
-        except ValueError as e:
-            return abort(400, str(e))
+        except ValueError as exception:
+            return abort(400, str(exception))
 
         # add to project files
         with self.db:

+ 13 - 6
pycs/frontend/endpoints/pipelines/FitModel.py

@@ -47,15 +47,22 @@ class FitModel(View):
 
     @staticmethod
     def load_and_fit(database: Database, pipelines: PipelineCache, project_id: int):
-        db = None
+        """
+        load the pipeline and call the fit function
+
+        :param database: database object
+        :param pipelines: pipeline cache
+        :param project_id: project id
+        """
+        database_copy = None
         pipeline = None
 
         # create new database instance
         try:
-            db = database.copy()
-            project = db.project(project_id)
+            database_copy = database.copy()
+            project = database_copy.project(project_id)
             model = project.model()
-            storage = MediaStorage(db, project_id)
+            storage = MediaStorage(database_copy, project_id)
 
             # load pipeline
             try:
@@ -67,5 +74,5 @@ class FitModel(View):
                 if pipeline is not None:
                     pipelines.free_instance(model.root_folder)
         finally:
-            if db is not None:
-                db.close()
+            if database_copy is not None:
+                database_copy.close()

+ 28 - 9
pycs/frontend/endpoints/pipelines/PredictModel.py

@@ -1,9 +1,10 @@
-from typing import Any
+from typing import Union, List
 
 from flask import make_response, request, abort
 from flask.views import View
 
 from pycs.database.Database import Database
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.interfaces.MediaFile import MediaFile
@@ -59,16 +60,27 @@ class PredictModel(View):
 
     @staticmethod
     def load_and_predict(database: Database, pipelines: PipelineCache,
-                         notifications: NotificationList, project_id: int, file_filter: Any):
-        db = None
+                         notifications: NotificationList,
+                         project_id: int, file_filter: Union[str, List[File]]):
+        """
+        load the pipeline and call the execute function
+
+        :param database: database object
+        :param pipelines: pipeline cache
+        :param notifications: notification object
+        :param project_id: project id
+        :param file_filter: list of files or 'new' / 'all'
+        :return:
+        """
+        database_copy = None
         pipeline = None
 
         # create new database instance
         try:
-            db = database.copy()
-            project = db.project(project_id)
+            database_copy = database.copy()
+            project = database_copy.project(project_id)
             model = project.model()
-            storage = MediaStorage(db, project_id, notifications)
+            storage = MediaStorage(database_copy, project_id, notifications)
 
             # create a list of MediaFile
             if isinstance(file_filter, str):
@@ -99,7 +111,7 @@ class PredictModel(View):
                     pipeline.execute(storage, file)
 
                     # commit changes and yield progress
-                    db.commit()
+                    database_copy.commit()
                     yield index / length, notifications
 
                     index += 1
@@ -107,10 +119,17 @@ class PredictModel(View):
                 if pipeline is not None:
                     pipelines.free_instance(model.root_folder)
         finally:
-            if db is not None:
-                db.close()
+            if database_copy is not None:
+                database_copy.close()
 
     @staticmethod
     def progress(progress: float, notifications: NotificationList):
+        """
+        fire notifications from the correct thread
+
+        :param progress: [0, 1]
+        :param notifications: Notificationlist
+        :return: progress
+        """
         notifications.fire()
         return progress

+ 2 - 3
pycs/frontend/endpoints/projects/ExecuteLabelProvider.py

@@ -9,7 +9,6 @@ from pycs.database.Project import Project
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
-from pycs.util.LabelProviderUtil import load_from_root_folder as load_label_provider
 
 
 class ExecuteLabelProvider(View):
@@ -68,7 +67,7 @@ class ExecuteLabelProvider(View):
         # pylint: disable=invalid-name
         # receive loads and executes the given label provider
         def receive():
-            with closing(load_label_provider(label_provider.root_folder)) as label_provider_impl:
+            with closing(label_provider.load()) as label_provider_impl:
                 provided_labels = label_provider_impl.get_labels()
                 return provided_labels
 
@@ -76,7 +75,7 @@ class ExecuteLabelProvider(View):
         def result(provided_labels):
             with db:
                 for label in provided_labels:
-                    created_label, insert = project.create_label(label['name'], label['id'],
+                    created_label, insert = project.create_label(label['name'], label['reference'],
                                                                  label['parent'])
 
                     if insert:

+ 2 - 2
pycs/frontend/endpoints/results/GetProjectResults.py

@@ -23,8 +23,8 @@ class GetProjectResults(View):
             return abort(404)
 
         # map media files to a dict
-        ms = MediaStorage(self.db, project.identifier, None)
-        files = list(map(lambda f: f.serialize(), ms.files().iter()))
+        storage = MediaStorage(self.db, project.identifier, None)
+        files = list(map(lambda f: f.serialize(), storage.files().iter()))
 
         # return result
         return jsonify(files)

+ 15 - 2
pycs/frontend/notifications/NotificationList.py

@@ -2,14 +2,27 @@ from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 class NotificationList:
-    def __init__(self, nm: NotificationManager):
+    """
+    stores notifications to fire them later in the correct thread
+    """
+
+    def __init__(self, notifications: NotificationManager):
         self.__list = []
-        self.nm = nm
+        self.notifications = notifications
 
     def add(self, fun: callable, *params):
+        """
+        add a function and parameters to be executed later
+
+        :param fun: function
+        :param params: parameters
+        """
         self.__list.append((fun, *params))
 
     def fire(self):
+        """
+        fire all stored events and reset the list
+        """
         for fun, *params in self.__list:
             fun(*params)
 

+ 5 - 5
pycs/interfaces/LabelProvider.py

@@ -32,17 +32,17 @@ class LabelProvider:
         raise NotImplementedError
 
     @staticmethod
-    def create_label(identifier, name, parent_identifier=None):
+    def create_label(reference: str, name: str, parent_reference=None):
         """
         create a label result
 
-        :param identifier: label identifier
+        :param reference: label reference string
         :param name: label name
-        :param parent_identifier: parent's identifier
+        :param parent_reference: parent's reference string
         :return:
         """
         return {
-            'id': identifier,
+            'reference': reference,
             'name': name,
-            'parent': parent_identifier
+            'parent': parent_reference
         }

+ 10 - 0
pycs/interfaces/MediaBoundingBox.py

@@ -2,6 +2,11 @@ from pycs.database.Result import Result
 
 
 class MediaBoundingBox:
+    """
+    A bounding box defined by it's upper left corner coordinates plus width and height. All those
+    values are normalized. A label and a frame (for videos) are optional.
+    """
+
     def __init__(self, result: Result):
         self.x = result.data['x']
         self.y = result.data['y']
@@ -11,4 +16,9 @@ class MediaBoundingBox:
         self.frame = result.data['frame'] if 'frame' in result.data else None
 
     def serialize(self) -> dict:
+        """
+        serialize all object properties to a dict
+
+        :return: dict
+        """
         return dict({'type': 'bounding-box'}, **self.__dict__)

+ 12 - 7
pycs/interfaces/MediaFile.py

@@ -35,7 +35,7 @@ class MediaFile:
         :param reference: use None to remove this file's collection
         """
         self.__file.set_collection_by_reference(reference)
-        self.__notifications.add(self.__notifications.nm.edit_file, self.__file)
+        self.__notifications.add(self.__notifications.notifications.edit_file, self.__file)
 
     def set_image_label(self, label: Union[int, MediaLabel], frame: int = None):
         """
@@ -53,7 +53,7 @@ class MediaFile:
             data = None
 
         created = self.__file.create_result('pipeline', 'labeled-image', label, data)
-        self.__notifications.add(self.__notifications.nm.create_result, created)
+        self.__notifications.add(self.__notifications.notifications.create_result, created)
 
     def add_bounding_box(self, x: float, y: float, w: float, h: float,
                          label: Union[int, MediaLabel] = None, frame: int = None):
@@ -80,22 +80,22 @@ class MediaFile:
             label = label.identifier
 
         created = self.__file.create_result('pipeline', 'bounding-box', label, result)
-        self.__notifications.add(self.__notifications.nm.create_result, created)
+        self.__notifications.add(self.__notifications.notifications.create_result, created)
 
     def remove_predictions(self):
         """
         remove and return all predictions added from pipelines
         """
         removed = self.__file.remove_results(origin='pipeline')
-        for r in removed:
-            self.__notifications.add(self.__notifications.nm.remove_result, r)
+        for result in removed:
+            self.__notifications.add(self.__notifications.notifications.remove_result, result)
 
     def __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
         def map_r(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:
             if result.type == 'labeled-image':
                 return MediaImageLabel(result)
-            else:
-                return MediaBoundingBox(result)
+
+            return MediaBoundingBox(result)
 
         return list(map(map_r,
                         filter(lambda r: r.origin == origin,
@@ -118,6 +118,11 @@ class MediaFile:
         return self.__get_results('pipeline')
 
     def serialize(self) -> dict:
+        """
+        serialize all object properties to a dict
+
+        :return: dict
+        """
         return {
             'type': self.type,
             'size': self.size,

+ 12 - 2
pycs/interfaces/MediaFileList.py

@@ -17,13 +17,23 @@ class MediaFileList:
         self.__collection = None
         self.__label = None
 
-    # TODO pydoc
     def filter_collection(self, collection_reference: str):
+        """
+        only include results matching the collection
+
+        :param collection_reference: reference string
+        :return: MediaFileList
+        """
         self.__collection = collection_reference
         return self
 
-    # TODO pydoc
     def filter_label(self, label: MediaLabel):
+        """
+        only include results matching the label
+
+        :param label: label
+        :return: MediaFileList
+        """
         self.__label = label
         return self
 

+ 9 - 0
pycs/interfaces/MediaImageLabel.py

@@ -2,9 +2,18 @@ from pycs.database.Result import Result
 
 
 class MediaImageLabel:
+    """
+    An image label with an optional frame index for videos.
+    """
+
     def __init__(self, result: Result):
         self.label = result.label
         self.frame = result.data['frame'] if 'frame' in result.data else None
 
     def serialize(self) -> dict:
+        """
+        serialize all object properties to a dict
+
+        :return: dict
+        """
         return dict({'type': 'image-label'}, **self.__dict__)

+ 4 - 0
pycs/interfaces/MediaLabel.py

@@ -2,6 +2,10 @@ from pycs.database.Label import Label
 
 
 class MediaLabel:
+    """
+    a label
+    """
+
     def __init__(self, label: Label):
         self.identifier = label.identifier
         self.parent = None

+ 4 - 4
pycs/interfaces/MediaStorage.py

@@ -30,13 +30,13 @@ class MediaStorage:
         result = []
 
         for label in label_list:
-            ml = label_dict[label.identifier]
+            medial_label = label_dict[label.identifier]
 
             if label.parent_id is not None:
-                ml.parent = label_dict[label.parent_id]
-                ml.parent.children.append(ml)
+                medial_label.parent = label_dict[label.parent_id]
+                medial_label.parent.children.append(medial_label)
 
-            result.append(ml)
+            result.append(medial_label)
 
         return result
 

+ 2 - 2
pycs/jobs/JobRunner.py

@@ -218,8 +218,8 @@ class JobRunner:
                     result_event.send(result)
 
             # save exceptions to show in ui
-            except Exception as e:
-                job.exception = f'{type(e).__name__} ({str(e)})'
+            except Exception as exception:
+                job.exception = f'{type(exception).__name__} ({str(exception)})'
 
             # remove from group dict
             if group is not None:

+ 0 - 28
pycs/util/LabelProviderUtil.py

@@ -1,28 +0,0 @@
-from json import load
-from os import path
-
-from pycs.interfaces.LabelProvider import LabelProvider
-
-
-def load_from_root_folder(root_folder: str) -> LabelProvider:
-    """
-    load configuration.json and create an instance from the included code object
-
-    :param root_folder: path to label provider root folder
-    :return: LabelProvider instance
-    """
-    # load configuration.json
-    configuration_path = path.join(root_folder, 'configuration.json')
-    with open(configuration_path, 'r') as configuration_file:
-        configuration = load(configuration_file)
-
-    # load code
-    code_path = path.join(root_folder, configuration['code']['module'])
-    module_name = code_path.replace('/', '.').replace('\\', '.')
-    class_name = configuration['code']['class']
-
-    imported_module = __import__(module_name, fromlist=[class_name])
-    class_attr = getattr(imported_module, class_name)
-
-    # return instance
-    return class_attr(root_folder, configuration)

+ 4 - 0
pycs/util/PipelineCache.py

@@ -11,6 +11,10 @@ from pycs.util.PipelineUtil import load_from_root_folder
 
 
 class PipelineCache:
+    """
+    Store initialized pipelines and call `close` after `CLOSE_TIMER` if they are not requested
+    another time.
+    """
     CLOSE_TIMER = 120
 
     def __init__(self, jobs: JobRunner):

+ 20 - 4
webui/src/components/projects/project-creation-window.vue

@@ -191,11 +191,9 @@ export default {
       return false;
     },
     availableLabels: function () {
-      let result = [{
-        name: 'None',
-        value: null
-      }];
+      let result = [];
 
+      // add label providers
       for (let label of this.labels) {
         result.push({
           name: label.name,
@@ -203,6 +201,24 @@ export default {
         });
       }
 
+      // sort
+      result.sort((a, b) => {
+        if (a.name.includes(b.name))
+          return +1;
+        if (b.name.includes(a.name))
+          return -1;
+        if (a.name > b.name)
+          return +1;
+        else
+          return -1;
+      });
+
+      // add `None` option
+      result.unshift({
+        name: 'None',
+        value: null
+      });
+
       return result;
     }
   },

+ 28 - 17
webui/src/components/projects/project-labels-window.vue

@@ -6,21 +6,26 @@
       Labels
     </h1>
 
-    <label-tree-view v-for="label in labelTree" :key="label.identifier"
-                     :label="label"
-                     :targetable="true"
-                     indent="2rem"/>
-
-    <div class="label">
-      <input-group>
-        <text-input placeholder="New Label"
-                    v-model="createLabelValue"
-                    @enter="createLabel"/>
-        <button-input type="primary" @click="createLabel">
-          create
-        </button-input>
-      </input-group>
-    </div>
+    <template v-if="labels !== false">
+      <label-tree-view v-for="label in labelTree" :key="label.identifier"
+                       :label="label"
+                       :targetable="true"
+                       indent="2rem"/>
+
+      <div class="label">
+        <input-group>
+          <text-input placeholder="New Label"
+                      v-model="createLabelValue"
+                      @enter="createLabel"/>
+          <button-input type="primary" @click="createLabel">
+            create
+          </button-input>
+        </input-group>
+      </div>
+    </template>
+    <template v-else>
+      <loading-icon/>
+    </template>
   </div>
 </template>
 
@@ -29,10 +34,11 @@ import TextInput from "@/components/base/text-input";
 import ButtonInput from "@/components/base/button-input";
 import InputGroup from "@/components/base/input-group";
 import LabelTreeView from "@/components/other/LabelTreeView";
+import LoadingIcon from "@/components/base/loading-icon";
 
 export default {
   name: "project-labels-window",
-  components: {LabelTreeView, InputGroup, ButtonInput, TextInput},
+  components: {LoadingIcon, LabelTreeView, InputGroup, ButtonInput, TextInput},
   created: function () {
     // get labels
     this.getLabels();
@@ -51,7 +57,7 @@ export default {
   },
   data: function () {
     return {
-      labels: [],
+      labels: false,
       createLabelValue: '',
       target: false
     }
@@ -171,6 +177,11 @@ h1.target {
   text-decoration: underline;
 }
 
+.loading-icon {
+  width: 4rem;
+  height: 4rem;
+}
+
 /*
 /deep/ .element {
   border: none;

+ 15 - 3
webui/src/components/projects/settings/general-settings.vue

@@ -1,11 +1,23 @@
 <template>
   <div class="general-settings">
     <text-input placeholder="Project ID"
-                :value="projectId"
+                :value="$root.project.identifier"
                 readonly="readonly">
       Project ID
     </text-input>
 
+    <text-input placeholder="Project Root"
+                :value="$root.project.root_folder"
+                readonly="readonly">
+      Project Root
+    </text-input>
+
+    <text-input placeholder="Data Root"
+                :value="$root.project.data_folder"
+                readonly="readonly">
+      Data Root
+    </text-input>
+
     <text-input placeholder="Name"
                 :value="name"
                 @change="sendName"
@@ -49,13 +61,13 @@ export default {
   name: "general-settings",
   components: {ButtonInput, ConfirmedButtonInput, TextareaInput, TextInput},
   created: function () {
-    this.projectId = this.$root.project.identifier;
     this.name = this.$root.project.name;
     this.description = this.$root.project.description;
+
+    console.log(this.$root.project);
   },
   data: function () {
     return {
-      projectId: '',
       name: '',
       nameError: false,
       description: '',

Some files were not shown because too many files changed in this diff