6
0
Эх сурвалжийг харах

Additional color for bounding boxes, fixed pure prediction for bounding boxes

Bounding boxes created by other users are now displayed in purple.
Executing the pipeline for single bounding boxes resulted in the created labels not being saved. That is now fixed.
The annotation box for cropped images is no longer misleading for user annotations for which no username was specified.
blunk 3 жил өмнө
parent
commit
7e3ce6cd6a
34 өөрчлөгдсөн 91 нэмэгдсэн , 24 устгасан
  1. 16 0
      pycs/database/File.py
  2. 1 0
      pycs/frontend/endpoints/ListJobs.py
  3. 1 0
      pycs/frontend/endpoints/ListLabelProviders.py
  4. 1 0
      pycs/frontend/endpoints/ListModels.py
  5. 1 0
      pycs/frontend/endpoints/ListProjects.py
  6. 1 0
      pycs/frontend/endpoints/data/RemoveFile.py
  7. 1 0
      pycs/frontend/endpoints/data/UploadFile.py
  8. 1 0
      pycs/frontend/endpoints/jobs/RemoveJob.py
  9. 1 0
      pycs/frontend/endpoints/labels/CreateLabel.py
  10. 1 0
      pycs/frontend/endpoints/labels/EditLabelName.py
  11. 1 0
      pycs/frontend/endpoints/labels/EditLabelParent.py
  12. 1 0
      pycs/frontend/endpoints/labels/ListLabelTree.py
  13. 1 0
      pycs/frontend/endpoints/labels/ListLabels.py
  14. 1 0
      pycs/frontend/endpoints/labels/RemoveLabel.py
  15. 1 0
      pycs/frontend/endpoints/pipelines/FitModel.py
  16. 5 4
      pycs/frontend/endpoints/pipelines/PredictBoundingBox.py
  17. 1 0
      pycs/frontend/endpoints/pipelines/PredictFile.py
  18. 13 12
      pycs/frontend/endpoints/pipelines/PredictModel.py
  19. 1 0
      pycs/frontend/endpoints/projects/CreateProject.py
  20. 1 0
      pycs/frontend/endpoints/projects/EditProjectDescription.py
  21. 1 0
      pycs/frontend/endpoints/projects/EditProjectName.py
  22. 1 0
      pycs/frontend/endpoints/projects/ExecuteExternalStorage.py
  23. 1 0
      pycs/frontend/endpoints/projects/ExecuteLabelProvider.py
  24. 1 0
      pycs/frontend/endpoints/projects/GetProjectModel.py
  25. 1 0
      pycs/frontend/endpoints/projects/ListProjectCollections.py
  26. 2 1
      pycs/frontend/endpoints/projects/ListProjectFiles.py
  27. 1 0
      pycs/frontend/endpoints/projects/RemoveProject.py
  28. 1 0
      pycs/frontend/endpoints/results/GetProjectResults.py
  29. 1 0
      pycs/frontend/endpoints/results/GetResults.py
  30. 1 0
      pycs/frontend/endpoints/results/RemoveResult.py
  31. 1 0
      pycs/frontend/endpoints/results/ResetResults.py
  32. 21 2
      pycs/interfaces/MediaFile.py
  33. 4 2
      webui/src/components/media/annotation-box.vue
  34. 3 3
      webui/src/components/media/cropped-image.vue

+ 16 - 0
pycs/database/File.py

@@ -226,6 +226,22 @@ class File(NamedBaseModel):
 
         return result
 
+    def remove_result(self, result_id: int) -> T.List[Result]:
+        """
+            Remove the result with the given id.
+
+            :param result_id: id of the result to delete
+            :return: list of result objects
+        """
+
+        results = Result.query.filter(
+            Result.file_id == self.id,
+            Result.id == result_id)
+
+        _results = [r.serialize() for r in results.all()]
+        results.delete()
+
+        return _results
 
     def remove_results(self, origin='pipeline') -> T.List[Result]:
         """

+ 1 - 0
pycs/frontend/endpoints/ListJobs.py

@@ -16,4 +16,5 @@ class ListJobs(View):
         self.jobs = jobs
 
     def dispatch_request(self, user: str):
+        # pylint: disable=unused-argument
         return jsonify(self.jobs.list())

+ 1 - 0
pycs/frontend/endpoints/ListLabelProviders.py

@@ -13,4 +13,5 @@ class ListLabelProviders(View):
 
 
     def dispatch_request(self, user: str):
+        # pylint: disable=unused-argument
         return jsonify(LabelProvider.query.all())

+ 1 - 0
pycs/frontend/endpoints/ListModels.py

@@ -13,4 +13,5 @@ class ListModels(View):
 
 
     def dispatch_request(self, user: str):
+        # pylint: disable=unused-argument
         return jsonify(Model.query.all())

+ 1 - 0
pycs/frontend/endpoints/ListProjects.py

@@ -13,4 +13,5 @@ class ListProjects(View):
 
 
     def dispatch_request(self, user: str):
+        # pylint: disable=unused-argument
         return jsonify(Project.query.all())

+ 1 - 0
pycs/frontend/endpoints/data/RemoveFile.py

@@ -18,6 +18,7 @@ class RemoveFile(View):
         self.nm = nm
 
     def dispatch_request(self, user: str, file_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/data/UploadFile.py

@@ -29,6 +29,7 @@ class UploadFile(View):
         self.size = None
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # find project
         project = Project.get_or_404(project_id)
 

+ 1 - 0
pycs/frontend/endpoints/jobs/RemoveJob.py

@@ -18,6 +18,7 @@ class RemoveJob(View):
         self.jobs = jobs
 
     def dispatch_request(self, user: str, job_id):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/labels/CreateLabel.py

@@ -21,6 +21,7 @@ class CreateLabel(View):
 
 
     def dispatch_request(self, user: str, project_id):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
         name = data.get('name')

+ 1 - 0
pycs/frontend/endpoints/labels/EditLabelName.py

@@ -20,6 +20,7 @@ class EditLabelName(View):
 
 
     def dispatch_request(self, user: str, project_id: int, label_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
         name = data.get('name')

+ 1 - 0
pycs/frontend/endpoints/labels/EditLabelParent.py

@@ -20,6 +20,7 @@ class EditLabelParent(View):
 
 
     def dispatch_request(self, user: str, project_id: int, label_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
         parent = data.get('parent')

+ 1 - 0
pycs/frontend/endpoints/labels/ListLabelTree.py

@@ -13,6 +13,7 @@ class ListLabelTree(View):
 
 
     def dispatch_request(self, user: str, project_id):
+        # pylint: disable=unused-argument
         # find project
         project = Project.get_or_404(project_id)
 

+ 1 - 0
pycs/frontend/endpoints/labels/ListLabels.py

@@ -13,6 +13,7 @@ class ListLabels(View):
 
 
     def dispatch_request(self, user: str, project_id):
+        # pylint: disable=unused-argument
         # find project
         project = Project.get_or_404(project_id)
 

+ 1 - 0
pycs/frontend/endpoints/labels/RemoveLabel.py

@@ -20,6 +20,7 @@ class RemoveLabel(View):
         self.nm = nm
 
     def dispatch_request(self, user: str, project_id: int, label_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/pipelines/FitModel.py

@@ -23,6 +23,7 @@ class FitModel(View):
         self.pipelines = pipelines
 
     def dispatch_request(self, user: str, project_id):
+        # pylint: disable=unused-argument
         project = Project.get_or_404(project_id)
 
         # extract request data

+ 5 - 4
pycs/frontend/endpoints/pipelines/PredictBoundingBox.py

@@ -28,9 +28,10 @@ class PredictBoundingBox(View):
 
     def dispatch_request(self, user: str, file_id, bbox_id):
         # find file and result (=bounding box)
-        # We need the result to get (x,y,w,h)
+        # We will later need the result to get (x,y,w,h). Here, we just check
+        # whether the result is valid.
         file = File.get_or_404(file_id)
-        result = Result.get_or_404(bbox_id)
+        Result.get_or_404(bbox_id)
 
         # extract request data
         data = request.get_json(force=True)
@@ -51,8 +52,8 @@ class PredictBoundingBox(View):
                           f'{project.id}/model-interaction',
                           Predict.load_and_pure_inference,
                           self.pipelines, notifications, self.nm,
-                          project.id, [file.id], {file.id: [result]},
-                          progress=Predict.progress)
+                          project.id, [file.id], {file.id: [bbox_id]},
+                          user, progress=Predict.progress)
 
         except JobGroupBusyException:
             abort(400, "File prediction is already running")

+ 1 - 0
pycs/frontend/endpoints/pipelines/PredictFile.py

@@ -26,6 +26,7 @@ class PredictFile(View):
         self.pipelines = pipelines
 
     def dispatch_request(self, user: str, file_id):
+        # pylint: disable=unused-argument
         # find file
         file = File.get_or_404(file_id)
 

+ 13 - 12
pycs/frontend/endpoints/pipelines/PredictModel.py

@@ -33,6 +33,7 @@ class PredictModel(View):
         self.pipelines = pipelines
 
     def dispatch_request(self, user: str, project_id):
+        # pylint: disable=unused-argument
         project = Project.get_or_404(project_id)
 
         # extract request data
@@ -129,7 +130,7 @@ class PredictModel(View):
                          notifications: NotificationList,
                          notification_manager: NotificationManager,
                          project_id: int, file_filter: List[int],
-                         result_filter: dict[int, List[Result]]):
+                         bbox_id_filter: dict[int, List[int]], user: str):
         """
         load the pipeline and call the execute function
 
@@ -139,7 +140,8 @@ class PredictModel(View):
         :param notification_manager: notification manager
         :param project_id: project id
         :param file_filter: list of file ids
-        :param result_filter: dict of file id and list of results to classify
+        :param bbox_id_filter: dict of file id and list of bbox_ids to classify
+        :param user: username of the user asking to predict the bounding box
         :return:
         """
         pipeline = None
@@ -149,32 +151,31 @@ class PredictModel(View):
         model_root = project.model.root_folder
         storage = MediaStorage(project_id, notifications)
 
-        # create a list of MediaFile
-        # Also convert dict to the same key type.
-        length = len(file_filter)
-
-
         # load pipeline
         try:
             pipeline = pipelines.load_from_root_folder(project_id, model_root)
 
             # iterate over media files
             index = 0
+            length = len(file_filter)
             for file_id in file_filter:
                 file = project.file(file_id)
                 file = MediaFile(file, notifications)
-                bounding_boxes = [MediaBoundingBox(result) for result in result_filter[file_id]]
+                bounding_boxes = [MediaBoundingBox(Result.get_or_404(bbox_id))
+                                for bbox_id in bbox_id_filter[file_id]]
 
                 # Perform inference.
                 bbox_labels = pipeline.pure_inference(storage, file, bounding_boxes)
 
                 # Add the labels determined in the inference process.
-                for i, result in enumerate(result_filter[file_id]):
-                    result.label_id = bbox_labels[i].identifier
-                    result.set_origin('user', origin_user=None, commit=True)
+                for i, bbox_id in enumerate(bbox_id_filter[file_id]):
+                    result = Result.get_or_404(bbox_id)
+                    result.set_label(bbox_labels[i].identifier, commit=True)
+                    result.set_origin('user', origin_user=user, commit=True)
                     notifications.add(notification_manager.edit_result, result)
 
-                # yield progress
+                # commit changes and yield progress
+                db.session.commit()
                 yield index / length, notifications
 
                 index += 1

+ 1 - 0
pycs/frontend/endpoints/projects/CreateProject.py

@@ -34,6 +34,7 @@ class CreateProject(View):
         self.jobs = jobs
 
     def dispatch_request(self, user: str):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/projects/EditProjectDescription.py

@@ -20,6 +20,7 @@ class EditProjectDescription(View):
 
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
         description = data.get('description')

+ 1 - 0
pycs/frontend/endpoints/projects/EditProjectName.py

@@ -20,6 +20,7 @@ class EditProjectName(View):
 
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
         name = data.get('name')

+ 1 - 0
pycs/frontend/endpoints/projects/ExecuteExternalStorage.py

@@ -28,6 +28,7 @@ class ExecuteExternalStorage(View):
         self.jobs = jobs
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/projects/ExecuteLabelProvider.py

@@ -26,6 +26,7 @@ class ExecuteLabelProvider(View):
         self.jobs = jobs
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         project = Project.get_or_404(project_id)
 
         # extract request data

+ 1 - 0
pycs/frontend/endpoints/projects/GetProjectModel.py

@@ -13,6 +13,7 @@ class GetProjectModel(View):
 
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # find project
         project = Project.get_or_404(project_id)
 

+ 1 - 0
pycs/frontend/endpoints/projects/ListProjectCollections.py

@@ -14,6 +14,7 @@ class ListProjectCollections(View):
 
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # find project
         project = Project.get_or_404(project_id)
 

+ 2 - 1
pycs/frontend/endpoints/projects/ListProjectFiles.py

@@ -14,11 +14,12 @@ class ListProjectFiles(View):
 
 
     def dispatch_request(self,
-                         user: str, 
+                         user: str,
                          project_id: int,
                          start: int = 0,
                          length: int = -1,
                          collection_id: int = None):
+        # pylint: disable=unused-argument
         # find project
 
         project = Project.get_or_404(project_id)

+ 1 - 0
pycs/frontend/endpoints/projects/RemoveProject.py

@@ -19,6 +19,7 @@ class RemoveProject(View):
         self.nm = nm
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # extract request data
         data = request.get_json(force=True)
 

+ 1 - 0
pycs/frontend/endpoints/results/GetProjectResults.py

@@ -14,6 +14,7 @@ class GetProjectResults(View):
 
 
     def dispatch_request(self, user: str, project_id: int):
+        # pylint: disable=unused-argument
         # get project from database
         project = Project.get_or_404(project_id)
 

+ 1 - 0
pycs/frontend/endpoints/results/GetResults.py

@@ -13,6 +13,7 @@ class GetResults(View):
 
 
     def dispatch_request(self, user: str, file_id: int):
+        # pylint: disable=unused-argument
         # get file from database
         file = File.get_or_404(file_id)
 

+ 1 - 0
pycs/frontend/endpoints/results/RemoveResult.py

@@ -19,6 +19,7 @@ class RemoveResult(View):
         self.nm = nm
 
     def dispatch_request(self, user: str, result_id: int):
+        # pylint: disable=unused-argument
         result = Result.get_or_404(result_id)
 
         # extract request data

+ 1 - 0
pycs/frontend/endpoints/results/ResetResults.py

@@ -19,6 +19,7 @@ class ResetResults(View):
         self.nm = nm
 
     def dispatch_request(self, user: str, file_id: int):
+        # pylint: disable=unused-argument
         file = File.get_or_404(file_id)
 
         # extract request data

+ 21 - 2
pycs/interfaces/MediaFile.py

@@ -55,7 +55,8 @@ class MediaFile:
         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):
+                         label: Union[int, MediaLabel] = None, frame: int = None,
+                         origin:str = None, origin_user: str = None) -> Result:
         """
         create a bounding-box result
 
@@ -65,6 +66,9 @@ class MediaFile:
         :param h: relative height [0, 1]
         :param label: label
         :param frame: frame index (only set for videos)
+        :param origin: Either pipeline or user
+        :param origin_user: Username of the user that provided the bounding box
+        :return: Created Result
         """
         result = {
             'x': x,
@@ -78,10 +82,14 @@ class MediaFile:
         if label is not None and isinstance(label, MediaLabel):
             label = label.identifier
 
-        created = self.__file.create_result(origin='pipeline',
+        if origin is None:
+            origin = 'pipeline'
+        created = self.__file.create_result(origin=origin, origin_user=origin_user,
             result_type='bounding-box', label=label, data=result)
         self.__notifications.add(self.__notifications.notifications.create_result, created)
 
+        return created
+
     def remove_predictions(self):
         """
         remove and return all predictions added from pipelines
@@ -90,6 +98,17 @@ class MediaFile:
         for result in removed:
             self.__notifications.add(self.__notifications.notifications.remove_result, result)
 
+    def remove_result(self, result_id):
+        """
+        Removes the result with the given id.
+
+        :param result_id: id of the result to delete
+        """
+
+        removed = self.__file.remove_result(id=result_id)
+        for result in removed:
+            self.__notifications.add(self.__notifications.notifications.remove_result, result)
+
     def __get_results(self, origin: str) -> List[Union[MediaImageLabel, MediaBoundingBox]]:
 
         def result_to_media(result: Result) -> Union[MediaImageLabel, MediaBoundingBox]:

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

@@ -37,7 +37,6 @@ export default {
   ],
   computed: {
     labelName: function () {
-      console.log(this.box)
       if (!this.box || !this.box.label_id)
         return false;
       for (let label of this.labels) {
@@ -53,7 +52,10 @@ export default {
       if (this.box) {
         switch (this.box.origin) {
           case 'user':
-            color = '255, 0, 0';
+            color = '136, 0, 136';
+            if (this.box.origin_user === this.$root.socket.username) {
+              color = '255, 0, 0';
+            }
             break;
           case 'pipeline':
             color = '0, 0, 255';

+ 3 - 3
webui/src/components/media/cropped-image.vue

@@ -21,10 +21,10 @@
 
     <div v-if="src" class="image-container">
       <img alt="crop" :src="src"/>
-    </div>
 
-    <div v-if="this.box.origin === 'user'">
-      Annotated by: {{this.box.origin_user}}
+      <div v-if="this.box.origin === 'user' && this.box.origin_user !== null">
+        Annotated by: <span style="color:red">{{this.box.origin_user}}</span>
+      </div>
     </div>
 
     <div v-else>