Browse Source

added file filtering: "all files", "with annotations", and "without"

Dimitri Korsch 3 years ago
parent
commit
ac9f2216f8

+ 20 - 10
pycs/database/File.py

@@ -134,27 +134,37 @@ class File(NamedBaseModel):
         collection = Collection.query.filter_by(reference=collection_reference).one()
         self.collection_id = collection.id
 
-    def _get_another_file(self, *query) -> T.Optional[File]:
+    def _get_another_file(self, *query, with_annotations=None) -> T.Optional[File]:
         """
         get the first file matching the query ordered by descending id
 
         :return: another file or None
         """
-        return File.query.filter(File.project_id == self.project_id, *query)
+        result = File.query.filter(File.project_id == self.project_id, *query)
 
-    def next(self) -> T.Optional[File]:
+        if with_annotations is None:
+            return result
+
+        annot_query = File.results.any()
+
+        if with_annotations == False:
+            annot_query = ~annot_query
+
+        return result.filter(annot_query)
+
+    def next(self, **kwargs) -> T.Optional[File]:
         """
         get the successor of this file
 
         :return: another file or None
         """
 
-        res = self._get_another_file(File.path > self.path)\
+        res = self._get_another_file(File.path > self.path, **kwargs)\
             .order_by(File.path)
         return res.first()
 
 
-    def previous(self) -> T.Optional[File]:
+    def previous(self, **kwargs) -> T.Optional[File]:
         """
         get the predecessor of this file
 
@@ -162,23 +172,23 @@ class File(NamedBaseModel):
         """
 
         # pylint: disable=no-member
-        res = self._get_another_file(File.path < self.path)\
+        res = self._get_another_file(File.path < self.path, **kwargs)\
             .order_by(File.path.desc())
         return res.first()
 
 
-    def next_in_collection(self) -> T.Optional[File]:
+    def next_in_collection(self, **kwargs) -> T.Optional[File]:
         """
         get the predecessor of this file
 
         :return: another file or None
         """
         return self._get_another_file(
-            File.path > self.path, File.collection_id == self.collection_id)\
+            File.path > self.path, File.collection_id == self.collection_id, **kwargs)\
             .order_by(File.path).first()
 
 
-    def previous_in_collection(self) -> T.Optional[File]:
+    def previous_in_collection(self, **kwargs) -> T.Optional[File]:
         """
         get the predecessor of this file
 
@@ -187,7 +197,7 @@ class File(NamedBaseModel):
 
         # pylint: disable=no-member
         return self._get_another_file(
-            File.path < self.path, File.collection_id == self.collection_id)\
+            File.path < self.path, File.collection_id == self.collection_id, **kwargs)\
             .order_by(File.path.desc()).first()
 
 

+ 9 - 1
pycs/database/Project.py

@@ -292,7 +292,8 @@ class Project(NamedBaseModel):
 
         return file, is_new
 
-    def get_files(self, *filters, offset: int = 0, limit: int = -1) -> T.List[File]:
+    def get_files(self, *filters, offset: int = 0, limit: int = -1,
+                  with_annotations: T.Optional[bool] = None) -> T.List[File]:
         """
         get an iterator of files associated with this project
 
@@ -300,6 +301,13 @@ class Project(NamedBaseModel):
         :param limit: file limit
         :return: iterator of files
         """
+        if with_annotations is not None:
+            annot_query = File.results.any()
+
+            if with_annotations == False:
+                annot_query = ~annot_query
+
+            filters = filters + (annot_query,)
 
         return self.files.filter(*filters).order_by(File.path).offset(offset).limit(limit)
 

+ 13 - 4
pycs/frontend/endpoints/data/GetPreviousAndNextFile.py

@@ -1,4 +1,5 @@
 from flask import jsonify
+from flask import request
 from flask.views import View
 
 from pycs.database.File import File
@@ -16,13 +17,21 @@ class GetPreviousAndNextFile(View):
         # get file from database
         file = File.get_or_404(file_id)
 
+
+        with_annotations = request.args.get("only_with_annotations")
+
+        kwargs = dict(with_annotations=None)
+
+        if with_annotations is not None:
+            kwargs["with_annotations"] = with_annotations == "1"
+
         # get previous and next
         result = {
             'current': file,
-            'previous': file.previous(),
-            'next': file.next(),
-            'previousInCollection': file.previous_in_collection(),
-            'nextInCollection': file.next_in_collection()
+            'previous': file.previous(**kwargs),
+            'next': file.next(**kwargs),
+            'previousInCollection': file.previous_in_collection(**kwargs),
+            'nextInCollection': file.next_in_collection(**kwargs)
         }
 
         # return data

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

@@ -1,5 +1,6 @@
 from flask import abort
 from flask import jsonify
+from flask import request
 from flask.views import View
 
 from pycs.database.Project import Project
@@ -38,7 +39,16 @@ class ListProjectFiles(View):
 
         else:
             count = project.files.count()
-            files = project.get_files(offset=start, limit=length).all()
+
+
+            with_annotations = request.args.get("only_with_annotations")
+
+            kwargs = dict(with_annotations=None)
+
+            if with_annotations is not None:
+                kwargs["with_annotations"] = with_annotations == "1"
+
+            files = project.get_files(offset=start, limit=length, **kwargs).all()
 
         # return files
         return jsonify({

+ 45 - 6
webui/src/components/media/media-control.vue

@@ -2,11 +2,12 @@
   <div class="media-control">
     <button-input ref="previousPage"
                   type="transparent"
-                  title="previous page (W)"
+                  title="previous page (Y)"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasPreviousPage}"
                   @click="$emit('previousPage', true)">
-      &lt;&lt;
+      <img alt="next" :class="{disabled: !hasPreviousPage}" src="@/assets/icons/chevron-left.svg">
+      <img alt="next" :class="{disabled: !hasPreviousPage}" src="@/assets/icons/chevron-left.svg">
     </button-input>
 
     <button-input ref="previousElement"
@@ -15,7 +16,8 @@
                   style="color: var(--on_error)"
                   :class="{disabled: !hasPreviousElement}"
                   @click="$emit('previousElement', true)">
-      &lt;
+
+      <img alt="next" :class="{disabled: !hasPreviousElement}" src="@/assets/icons/chevron-left.svg">
     </button-input>
 
     <select v-if="collections.length > 0"
@@ -30,22 +32,30 @@
       </option>
     </select>
 
+    <select v-else @change="only_annotations">
+      <option>all images</option>
+      <option>images with annotations</option>
+      <option>images without annotations</option>
+    </select>
+
     <button-input ref="nextElement"
                   type="transparent"
                   title="next element (D)"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasNextElement}"
                   @click="$emit('nextElement', true)">
-      &gt;
+      <img alt="next" :class="{disabled: !hasNextElement}" src="@/assets/icons/chevron-right.svg">
     </button-input>
 
     <button-input ref="nextPage"
                   type="transparent"
-                  title="next page (S)"
+                  title="next page (C)"
                   style="color: var(--on_error)"
                   :class="{disabled: !hasNextPage}"
                   @click="$emit('nextPage', true)">
-      &gt;&gt;
+      <img alt="next" :class="{disabled: !hasNextPage}" src="@/assets/icons/chevron-right.svg">
+      <img alt="next" :class="{disabled: !hasNextPage}" src="@/assets/icons/chevron-right.svg">
+
     </button-input>
   </div>
 </template>
@@ -114,6 +124,27 @@ export default {
           this.$emit('filter', select.options[select.selectedIndex].value);
           break;
       }
+    },
+
+    only_annotations: function(e) {
+      const select = e.target;
+
+      switch (select.selectedIndex) {
+          // no filter
+        case 0:
+          this.$emit('only_annotations', null);
+          break;
+
+          // only with annotations
+        case 1:
+          this.$emit('only_annotations', true);
+          break;
+
+          // only without annotations
+        default:
+          this.$emit('only_annotations', false);
+          break;
+      }
     }
   }
 }
@@ -139,6 +170,14 @@ export default {
   opacity: 0.4;
 }
 
+img {
+  filter: invert(1);
+}
+img.disabled {
+  filter: invert(1);
+  background-color: transparent;
+}
+
 select {
   flex-grow: 1;
   max-width: 15rem;

+ 33 - 2
webui/src/components/media/paginated-media.vue

@@ -39,7 +39,15 @@
 <script>
 export default {
   name: "paginated-media",
-  props: ['rows', 'width', 'inline', 'deletable', 'current', 'filter'],
+  props: [
+    'rows',
+    'width',
+    'inline',
+    'deletable',
+    'current',
+    'filter',
+    'only_annotations'
+  ],
   mounted: function () {
     window.addEventListener('resize', this.resize);
     window.addEventListener('wheel', this.scroll);
@@ -170,6 +178,14 @@ export default {
       else
         url = `/projects/${this.$root.project.identifier}/data/${this.filter}/${offset}/${limit}`;
 
+      if (this.only_annotations === true)
+        url = `${url}?only_with_annotations=1`
+      else if (this.only_annotations === false)
+        url = `${url}?only_with_annotations=0`
+
+      // for null or undefined, do not change the URL
+
+
       // call endpoint
       this.$root.socket.get(url)
           .then(response => response.json())
@@ -224,8 +240,15 @@ export default {
       // find current in list
       this.findCurrent();
 
+      let url = `/data/${this.current.identifier}/previous_next`;
+
+      if (this.only_annotations === true)
+        url = `${url}?only_with_annotations=1`
+      else if (this.only_annotations === false)
+        url = `${url}?only_with_annotations=0`
+
       // receive previous and next element
-      this.$root.socket.get(`/data/${this.current.identifier}/previous_next`)
+      this.$root.socket.get(url)
           .then(response => response.json())
           .then(data => {
             if (this.filter === undefined || this.filter === false) {
@@ -247,6 +270,14 @@ export default {
         else
           this.$emit('click', this.images[0]);
       });
+    },
+    only_annotations: function() {
+      this.get(() => {
+        if (this.images.length === 0)
+          this.$emit('click', false);
+        else
+          this.$emit('click', this.images[0]);
+      });
     }
   }
 }

+ 6 - 2
webui/src/components/projects/project-data-view-window.vue

@@ -14,12 +14,15 @@
                    @nextPage="$refs.media.nextPage()"
                    @previousElement="$refs.media.prevElement()"
                    @nextElement="$refs.media.nextElement()"
-                   @filter="filter=$event"/>
+                   @filter="filter=$event"
+                   @only_annotations="only_annotations=$event"
+                   />
 
     <paginated-media ref="media"
                      rows="1" width="100" :inline="true"
                      :current="current"
                      :filter="filter"
+                     :only_annotations="only_annotations"
                      @click="current=$event"
                      @hasPreviousPage="hasPreviousPage=$event"
                      @hasNextPage="hasNextPage=$event"
@@ -43,7 +46,8 @@ export default {
       hasNextPage: false,
       hasPreviousElement: false,
       hasNextElement: false,
-      filter: false
+      filter: false,
+      only_annotations: null,
     }
   }
 }