Browse Source

Merge branch 'master' into ammod

Dimitri Korsch 3 years ago
parent
commit
d2c1e05e8c
33 changed files with 1408 additions and 116 deletions
  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.name = row[3]
         self.description = row[4]
         self.description = row[4]
         self.position = row[5]
         self.position = row[5]
-        self.autoselect = False if row[6] == 0 else True
+        self.autoselect = row[6] > 0
 
 
     def set_name(self, name: str):
     def set_name(self, name: str):
         """
         """

+ 19 - 4
pycs/database/Database.py

@@ -46,10 +46,12 @@ class Database:
                     ''')
                     ''')
                     cursor.execute('''
                     cursor.execute('''
                         CREATE TABLE IF NOT EXISTS label_providers (
                         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)
                 discover_label_providers(self.con)
 
 
     def close(self):
     def close(self):
+        """
+        close database file
+        """
         self.con.close()
         self.con.close()
 
 
     def copy(self):
     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)
         return Database(self.path, initialization=False, discovery=False)
 
 
     def commit(self):
     def commit(self):
+        """
+        commit changes
+        """
         self.con.commit()
         self.con.commit()
 
 
     def __enter__(self):
     def __enter__(self):

+ 7 - 1
pycs/database/File.py

@@ -160,7 +160,7 @@ class File:
                     LIMIT 1
                     LIMIT 1
                 ''', (self.identifier, self.project_id))
                 ''', (self.identifier, self.project_id))
             else:
             else:
-                cursor.execute(''' 
+                cursor.execute('''
                     SELECT * FROM files
                     SELECT * FROM files
                     WHERE id > ? AND project = ? AND collection = ?
                     WHERE id > ? AND project = ? AND collection = ?
                     ORDER BY id ASC
                     ORDER BY id ASC
@@ -226,6 +226,12 @@ class File:
             return self.result(cursor.lastrowid)
             return self.result(cursor.lastrowid)
 
 
     def remove_results(self, origin='pipeline') -> List[Result]:
     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:
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
             cursor.execute('''
                 SELECT * FROM results WHERE file = ? AND origin = ?
                 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:
 class LabelProvider:
     """
     """
     database class for label providers
     database class for label providers
@@ -10,3 +16,29 @@ class LabelProvider:
         self.name = row[1]
         self.name = row[1]
         self.description = row[2]
         self.description = row[2]
         self.root_folder = row[3]
         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 contextlib import closing
 from os.path import join
 from os.path import join
 from time import time
 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.Collection import Collection
 from pycs.database.File import File
 from pycs.database.File import File
@@ -77,26 +77,48 @@ class Project:
 
 
             return None
             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,
     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
         create a label for this project. If there is already a label with the same reference
         in the database its name is updated.
         in the database its name is updated.
 
 
         :param name: label name
         :param name: label name
         :param reference: label reference
         :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
         :return: created or edited label, insert
         """
         """
         created = int(time())
         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:
         with closing(self.database.con.cursor()) as cursor:
             cursor.execute('''
             cursor.execute('''
                 INSERT INTO labels (project, parent, created, reference, name)
                 INSERT INTO labels (project, parent, created, reference, name)
                 VALUES (?, ?, ?, ?, ?)
                 VALUES (?, ?, ?, ?, ?)
                 ON CONFLICT (project, reference) DO
                 ON CONFLICT (project, reference) DO
                 UPDATE SET parent = ?, name = ?
                 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.
             # lastrowid is 0 if on conflict clause applies.
             # If this is the case we do an extra query to receive the row id.
             # If this is the case we do an extra query to receive the row id.
@@ -165,7 +187,18 @@ class Project:
                           name: str,
                           name: str,
                           description: str,
                           description: str,
                           position: int,
                           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
         autoselect = 1 if autoselect else 0
 
 
         with closing(self.database.con.cursor()) as cursor:
         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 contextlib import closing
 from glob import glob
 from glob import glob
 from json import load
 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):
 def discover(database):
@@ -12,10 +31,9 @@ def discover(database):
     :return:
     :return:
     """
     """
     with closing(database.cursor()) as cursor:
     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)
                 label = load(file)
 
 
             # extract data
             # extract data
@@ -24,8 +42,8 @@ def discover(database):
 
 
             # save to database
             # save to database
             cursor.execute('''
             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 = ?
                 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=line-too-long
+    # pylint: disable=too-many-statements
     def __init__(self, settings: dict, database: Database, jobs: JobRunner, pipelines: PipelineCache):
     def __init__(self, settings: dict, database: Database, jobs: JobRunner, pipelines: PipelineCache):
         # initialize web server
         # initialize web server
         if exists('webui/index.html'):
         if exists('webui/index.html'):

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

@@ -56,8 +56,8 @@ class UploadFile(View):
         try:
         try:
             ftype, frames, fps = tpool.execute(file_info,
             ftype, frames, fps = tpool.execute(file_info,
                                                self.data_folder, self.file_id, self.file_extension)
                                                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
         # add to project files
         with self.db:
         with self.db:

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

@@ -47,15 +47,22 @@ class FitModel(View):
 
 
     @staticmethod
     @staticmethod
     def load_and_fit(database: Database, pipelines: PipelineCache, project_id: int):
     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
         pipeline = None
 
 
         # create new database instance
         # create new database instance
         try:
         try:
-            db = database.copy()
-            project = db.project(project_id)
+            database_copy = database.copy()
+            project = database_copy.project(project_id)
             model = project.model()
             model = project.model()
-            storage = MediaStorage(db, project_id)
+            storage = MediaStorage(database_copy, project_id)
 
 
             # load pipeline
             # load pipeline
             try:
             try:
@@ -67,5 +74,5 @@ class FitModel(View):
                 if pipeline is not None:
                 if pipeline is not None:
                     pipelines.free_instance(model.root_folder)
                     pipelines.free_instance(model.root_folder)
         finally:
         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 import make_response, request, abort
 from flask.views import View
 from flask.views import View
 
 
 from pycs.database.Database import Database
 from pycs.database.Database import Database
+from pycs.database.File import File
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationList import NotificationList
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.frontend.notifications.NotificationManager import NotificationManager
 from pycs.interfaces.MediaFile import MediaFile
 from pycs.interfaces.MediaFile import MediaFile
@@ -59,16 +60,27 @@ class PredictModel(View):
 
 
     @staticmethod
     @staticmethod
     def load_and_predict(database: Database, pipelines: PipelineCache,
     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
         pipeline = None
 
 
         # create new database instance
         # create new database instance
         try:
         try:
-            db = database.copy()
-            project = db.project(project_id)
+            database_copy = database.copy()
+            project = database_copy.project(project_id)
             model = project.model()
             model = project.model()
-            storage = MediaStorage(db, project_id, notifications)
+            storage = MediaStorage(database_copy, project_id, notifications)
 
 
             # create a list of MediaFile
             # create a list of MediaFile
             if isinstance(file_filter, str):
             if isinstance(file_filter, str):
@@ -99,7 +111,7 @@ class PredictModel(View):
                     pipeline.execute(storage, file)
                     pipeline.execute(storage, file)
 
 
                     # commit changes and yield progress
                     # commit changes and yield progress
-                    db.commit()
+                    database_copy.commit()
                     yield index / length, notifications
                     yield index / length, notifications
 
 
                     index += 1
                     index += 1
@@ -107,10 +119,17 @@ class PredictModel(View):
                 if pipeline is not None:
                 if pipeline is not None:
                     pipelines.free_instance(model.root_folder)
                     pipelines.free_instance(model.root_folder)
         finally:
         finally:
-            if db is not None:
-                db.close()
+            if database_copy is not None:
+                database_copy.close()
 
 
     @staticmethod
     @staticmethod
     def progress(progress: float, notifications: NotificationList):
     def progress(progress: float, notifications: NotificationList):
+        """
+        fire notifications from the correct thread
+
+        :param progress: [0, 1]
+        :param notifications: Notificationlist
+        :return: progress
+        """
         notifications.fire()
         notifications.fire()
         return progress
         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.frontend.notifications.NotificationManager import NotificationManager
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobGroupBusyException import JobGroupBusyException
 from pycs.jobs.JobRunner import JobRunner
 from pycs.jobs.JobRunner import JobRunner
-from pycs.util.LabelProviderUtil import load_from_root_folder as load_label_provider
 
 
 
 
 class ExecuteLabelProvider(View):
 class ExecuteLabelProvider(View):
@@ -68,7 +67,7 @@ class ExecuteLabelProvider(View):
         # pylint: disable=invalid-name
         # pylint: disable=invalid-name
         # receive loads and executes the given label provider
         # receive loads and executes the given label provider
         def receive():
         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()
                 provided_labels = label_provider_impl.get_labels()
                 return provided_labels
                 return provided_labels
 
 
@@ -76,7 +75,7 @@ class ExecuteLabelProvider(View):
         def result(provided_labels):
         def result(provided_labels):
             with db:
             with db:
                 for label in provided_labels:
                 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'])
                                                                  label['parent'])
 
 
                     if insert:
                     if insert:

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

@@ -23,8 +23,8 @@ class GetProjectResults(View):
             return abort(404)
             return abort(404)
 
 
         # map media files to a dict
         # 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 result
         return jsonify(files)
         return jsonify(files)

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

@@ -2,14 +2,27 @@ from pycs.frontend.notifications.NotificationManager import NotificationManager
 
 
 
 
 class NotificationList:
 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.__list = []
-        self.nm = nm
+        self.notifications = notifications
 
 
     def add(self, fun: callable, *params):
     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))
         self.__list.append((fun, *params))
 
 
     def fire(self):
     def fire(self):
+        """
+        fire all stored events and reset the list
+        """
         for fun, *params in self.__list:
         for fun, *params in self.__list:
             fun(*params)
             fun(*params)
 
 

+ 5 - 5
pycs/interfaces/LabelProvider.py

@@ -32,17 +32,17 @@ class LabelProvider:
         raise NotImplementedError
         raise NotImplementedError
 
 
     @staticmethod
     @staticmethod
-    def create_label(identifier, name, parent_identifier=None):
+    def create_label(reference: str, name: str, parent_reference=None):
         """
         """
         create a label result
         create a label result
 
 
-        :param identifier: label identifier
+        :param reference: label reference string
         :param name: label name
         :param name: label name
-        :param parent_identifier: parent's identifier
+        :param parent_reference: parent's reference string
         :return:
         :return:
         """
         """
         return {
         return {
-            'id': identifier,
+            'reference': reference,
             'name': name,
             '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:
 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):
     def __init__(self, result: Result):
         self.x = result.data['x']
         self.x = result.data['x']
         self.y = result.data['y']
         self.y = result.data['y']
@@ -11,4 +16,9 @@ class MediaBoundingBox:
         self.frame = result.data['frame'] if 'frame' in result.data else None
         self.frame = result.data['frame'] if 'frame' in result.data else None
 
 
     def serialize(self) -> dict:
     def serialize(self) -> dict:
+        """
+        serialize all object properties to a dict
+
+        :return: dict
+        """
         return dict({'type': 'bounding-box'}, **self.__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
         :param reference: use None to remove this file's collection
         """
         """
         self.__file.set_collection_by_reference(reference)
         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):
     def set_image_label(self, label: Union[int, MediaLabel], frame: int = None):
         """
         """
@@ -53,7 +53,7 @@ class MediaFile:
             data = None
             data = None
 
 
         created = self.__file.create_result('pipeline', 'labeled-image', label, data)
         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,
     def add_bounding_box(self, x: float, y: float, w: float, h: float,
                          label: Union[int, MediaLabel] = None, frame: int = None):
                          label: Union[int, MediaLabel] = None, frame: int = None):
@@ -80,22 +80,22 @@ class MediaFile:
             label = label.identifier
             label = label.identifier
 
 
         created = self.__file.create_result('pipeline', 'bounding-box', label, result)
         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):
     def remove_predictions(self):
         """
         """
         remove and return all predictions added from pipelines
         remove and return all predictions added from pipelines
         """
         """
         removed = self.__file.remove_results(origin='pipeline')
         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 __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
         def map_r(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:
         def map_r(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:
             if result.type == 'labeled-image':
             if result.type == 'labeled-image':
                 return MediaImageLabel(result)
                 return MediaImageLabel(result)
-            else:
-                return MediaBoundingBox(result)
+
+            return MediaBoundingBox(result)
 
 
         return list(map(map_r,
         return list(map(map_r,
                         filter(lambda r: r.origin == origin,
                         filter(lambda r: r.origin == origin,
@@ -118,6 +118,11 @@ class MediaFile:
         return self.__get_results('pipeline')
         return self.__get_results('pipeline')
 
 
     def serialize(self) -> dict:
     def serialize(self) -> dict:
+        """
+        serialize all object properties to a dict
+
+        :return: dict
+        """
         return {
         return {
             'type': self.type,
             'type': self.type,
             'size': self.size,
             'size': self.size,

+ 12 - 2
pycs/interfaces/MediaFileList.py

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

+ 9 - 0
pycs/interfaces/MediaImageLabel.py

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

+ 4 - 4
pycs/interfaces/MediaStorage.py

@@ -30,13 +30,13 @@ class MediaStorage:
         result = []
         result = []
 
 
         for label in label_list:
         for label in label_list:
-            ml = label_dict[label.identifier]
+            medial_label = label_dict[label.identifier]
 
 
             if label.parent_id is not None:
             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
         return result
 
 

+ 2 - 2
pycs/jobs/JobRunner.py

@@ -218,8 +218,8 @@ class JobRunner:
                     result_event.send(result)
                     result_event.send(result)
 
 
             # save exceptions to show in ui
             # 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
             # remove from group dict
             if group is not None:
             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:
 class PipelineCache:
+    """
+    Store initialized pipelines and call `close` after `CLOSE_TIMER` if they are not requested
+    another time.
+    """
     CLOSE_TIMER = 120
     CLOSE_TIMER = 120
 
 
     def __init__(self, jobs: JobRunner):
     def __init__(self, jobs: JobRunner):

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

@@ -191,11 +191,9 @@ export default {
       return false;
       return false;
     },
     },
     availableLabels: function () {
     availableLabels: function () {
-      let result = [{
-        name: 'None',
-        value: null
-      }];
+      let result = [];
 
 
+      // add label providers
       for (let label of this.labels) {
       for (let label of this.labels) {
         result.push({
         result.push({
           name: label.name,
           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;
       return result;
     }
     }
   },
   },

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

@@ -6,21 +6,26 @@
       Labels
       Labels
     </h1>
     </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>
   </div>
 </template>
 </template>
 
 
@@ -29,10 +34,11 @@ import TextInput from "@/components/base/text-input";
 import ButtonInput from "@/components/base/button-input";
 import ButtonInput from "@/components/base/button-input";
 import InputGroup from "@/components/base/input-group";
 import InputGroup from "@/components/base/input-group";
 import LabelTreeView from "@/components/other/LabelTreeView";
 import LabelTreeView from "@/components/other/LabelTreeView";
+import LoadingIcon from "@/components/base/loading-icon";
 
 
 export default {
 export default {
   name: "project-labels-window",
   name: "project-labels-window",
-  components: {LabelTreeView, InputGroup, ButtonInput, TextInput},
+  components: {LoadingIcon, LabelTreeView, InputGroup, ButtonInput, TextInput},
   created: function () {
   created: function () {
     // get labels
     // get labels
     this.getLabels();
     this.getLabels();
@@ -51,7 +57,7 @@ export default {
   },
   },
   data: function () {
   data: function () {
     return {
     return {
-      labels: [],
+      labels: false,
       createLabelValue: '',
       createLabelValue: '',
       target: false
       target: false
     }
     }
@@ -171,6 +177,11 @@ h1.target {
   text-decoration: underline;
   text-decoration: underline;
 }
 }
 
 
+.loading-icon {
+  width: 4rem;
+  height: 4rem;
+}
+
 /*
 /*
 /deep/ .element {
 /deep/ .element {
   border: none;
   border: none;

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

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

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