浏览代码

added possibility to confirm all results for an image

Dimitri Korsch 3 年之前
父节点
当前提交
00df7d1ab2

+ 8 - 1
pycs/frontend/WebServer.py

@@ -45,6 +45,7 @@ from pycs.frontend.endpoints.projects.ListProjectCollections import ListProjectC
 from pycs.frontend.endpoints.projects.ListProjectFiles import ListProjectFiles
 from pycs.frontend.endpoints.projects.RemoveProject import RemoveProject
 from pycs.frontend.endpoints.results.ConfirmResult import ConfirmResult
+from pycs.frontend.endpoints.results.ConfirmAllResults import ConfirmAllResults
 from pycs.frontend.endpoints.results.CopyResults import CopyResults
 from pycs.frontend.endpoints.results.CreateResult import CreateResult
 from pycs.frontend.endpoints.results.EditResultData import EditResultData
@@ -289,7 +290,13 @@ class WebServer:
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/copy_results',
-            view_func=CopyResults.as_view('copy_results', self.notifications)
+            view_func=self.htpasswd.required( CopyResults.as_view('copy_results',
+                self.notifications) )
+        )
+        self.app.add_url_rule(
+            '/data/<int:file_id>/confirm_all',
+            view_func=self.htpasswd.required( ConfirmAllResults.as_view('confirm_all',
+                self.notifications) )
         )
         self.app.add_url_rule(
             '/data/<int:file_id>/reset',

+ 35 - 0
pycs/frontend/endpoints/results/ConfirmAllResults.py

@@ -0,0 +1,35 @@
+from flask import abort
+from flask import make_response
+from flask import request
+from flask.views import View
+
+from pycs.database.File import File
+from pycs.frontend.notifications.NotificationManager import NotificationManager
+
+
+class ConfirmAllResults(View):
+    """
+    confirm a result (change its origin to user)
+    """
+    # pylint: disable=arguments-differ
+    methods = ['POST']
+
+    def __init__(self, nm: NotificationManager):
+        # pylint: disable=invalid-name
+        self.nm = nm
+
+    def dispatch_request(self, user: str, file_id: int):
+        # find file
+        file = File.get_or_404(file_id)
+
+        # extract request data
+        data = request.get_json(force=True)
+
+        if not data.get('confirm', False):
+            return abort(400, "confirm flag is missing")
+
+        for result in file.results:
+            result.confirm(user)
+            self.nm.edit_result(result)
+
+        return make_response()

+ 69 - 0
webui/src/assets/icons/check-all.svg

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 16 16"
+   width="16"
+   height="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="check_all.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1135"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="14.75"
+     inkscape:cx="8"
+     inkscape:cy="8"
+     inkscape:window-x="1200"
+     inkscape:window-y="536"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <g
+     id="g875"
+     transform="translate(0.00792471,-0.35699187)">
+    <path
+       style="fill-rule:evenodd"
+       inkscape:connector-curvature="0"
+       id="path2"
+       d="m 13.78,4.2019913 a 0.75,0.75 0 0 1 0,1.06 L 6.53,12.511991 a 0.75,0.75 0 0 1 -1.06,0 L 2.22,9.2619913 a 0.75,0.75 0 0 1 1.06,-1.06 L 6,10.921991 12.72,4.2019913 a 0.75,0.75 0 0 1 1.06,0 z" />
+    <path
+       id="path2-6"
+       d="m 13.746104,6.9318645 a 0.75,0.75 0 0 1 0,1.06 L 6.4961017,15.241865 a 0.75,0.75 0 0 1 -1.0600001,0 l -3.25,-3.25 a 0.75,0.75 0 0 1 1.0599999,-1.06 l 2.7200001,2.72 6.7200024,-6.7200005 a 0.75,0.75 0 0 1 1.06,0 z"
+       style="fill-rule:evenodd"
+       inkscape:connector-curvature="0" />
+    <path
+       id="path2-6-7"
+       d="m 13.77098,1.4721187 a 0.75,0.75 0 0 1 0,1.0599999 L 6.5209771,9.7821182 a 0.75,0.75 0 0 1 -1.06,0 L 2.2109772,6.532119 a 0.75,0.75 0 0 1 1.06,-1.0600006 L 5.9909771,8.1921182 12.71098,1.4721187 a 0.75,0.75 0 0 1 1.06,0 z"
+       style="fill-rule:evenodd"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>

+ 57 - 42
webui/src/components/media/annotated-image.vue

@@ -12,6 +12,8 @@
                    :infoBox="infoBox !== false"
                    @infoBox="infoBox = $event; interaction=false"
                    @unzoom="zoomBox=false; interaction=false"
+                   @predict="predictImage"
+                   @confirmAll="confirmAll"
                    @prevzoom="$refs.overlay.prevZoom()"
                    @nextzoom="$refs.overlay.nextZoom()"/>
 
@@ -23,14 +25,16 @@
 
       <div class="media">
         <h3>{{current.path}}</h3>
-        <div class="mode-tooltip">{{modeTooltip()}}</div>
+        <div class="mode-tooltip">{{modeTooltip}}</div>
 
         <!-- image -->
         <img v-if="current.type === 'image'"
              ref="media" :src="src" alt="media"
              :style="cropPosition"
-             v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change"
-             v-on:transitionend="resize">
+             @load="change"
+             @loadedmetadata="change"
+             @loadeddata="change"
+             @transitionend="resize">
 
         <!-- video -->
         <template v-if="current.type === 'video'">
@@ -227,6 +231,41 @@ export default {
       return this.model
           && (this.supported.labeledImages || this.supported.labeledBoundingBoxes);
     },
+
+    modeTooltip: function(){
+      let tip = "Current mode";
+      switch (this.interaction) {
+        case "draw-box":
+          return `${tip}: draw bounding-box`;
+
+        case "estimate-box":
+          return `${tip}: estimate bounding-box`;
+
+        case "extreme-clicking":
+          return `${tip}: extreme clicking`;
+
+        case "move-box":
+          return `${tip}: move or resize`;
+
+        case "label-box":
+          if (this.label.identifier === null)
+            return `${tip}: remove tag`;
+          else
+            return `${tip}: tag as "${this.label.name}"`;
+
+        case "confirm-box":
+          return `${tip}: confirm`;
+
+        case "remove-box":
+          return `${tip}: remove`;
+
+        case "info-box":
+          return `${tip}: bounding-box info`;
+
+        default:
+          return "";
+      }
+    },
   },
   methods: {
 
@@ -253,7 +292,6 @@ export default {
     },
 
     labelBox: function(box_id, label_id) {
-      console.log(box_id, label_id);
       this.$root.socket.post(`/results/${box_id}/label`,
         {label: label_id});
     },
@@ -274,11 +312,24 @@ export default {
     },
 
     estimateBox: function(file_id, coordinates) {
-      this.$root.socket.post(`/data/${file_id}/estimate`, coordinates);
+      this.$root.socket.post(`/data/${file_id}/estimate`,
+        coordinates);
+    },
+
+    predictImage: function(file_id) {
+      this.$root.socket.post(`/data/${file_id}/predict`,
+        {predict: true});
     },
 
     predictBox: function(file_id, box_id) {
-      this.$root.socket.post(`/data/${file_id}/${box_id}/predict_bounding_box`, {predict: true});
+      this.$root.socket.post(`/data/${file_id}/${box_id}/predict_bounding_box`,
+        {predict: true});
+    },
+
+    // confirm all bboxes of the image
+    confirmAll: function(file_id) {
+      this.$root.socket.post(`/data/${file_id}/confirm_all`,
+        {confirm: true});
     },
 
     // confirm either a bbox or image result
@@ -292,42 +343,6 @@ export default {
         {remove: true});
     },
 
-
-    modeTooltip: function(){
-      let tip = "Current mode";
-      switch (this.interaction) {
-        case "draw-box":
-          return `${tip}: draw bounding-box`;
-
-        case "estimate-box":
-          return `${tip}: estimate bounding-box`;
-
-        case "extreme-clicking":
-          return `${tip}: extreme clicking`;
-
-        case "move-box":
-          return `${tip}: move or resize`;
-
-        case "label-box":
-          if (this.label.identifier === null)
-            return `${tip}: remove tag`;
-          else
-            return `${tip}: tag as "${this.label.name}"`;
-
-        case "confirm-box":
-          return `${tip}: confirm`;
-
-        case "remove-box":
-          return `${tip}: remove`;
-
-        case "info-box":
-          return `${tip}: bounding-box info`;
-
-        default:
-          return "";
-      }
-    },
-
     resize: function () {
       const rect = this.$refs.root.getBoundingClientRect();
 

+ 42 - 25
webui/src/components/media/options-bar.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="options-bar">
     <div ref="draw_box"
-         class="image"
+         class="icon"
          title="draw bounding box (Q)"
          :class="{active: interaction === 'draw-box'}"
          @click="$emit('interaction', 'draw-box')">
@@ -10,7 +10,7 @@
 
 
     <div ref="estimate_box"
-         class="image"
+         class="icon"
          title="estimate bounding box (W)"
          :class="{active: interaction === 'estimate-box'}"
          @click="$emit('interaction', 'estimate-box')">
@@ -18,7 +18,7 @@
     </div>
 
     <div ref="extreme_clicking"
-         class="image"
+         class="icon"
          title="extreme clicking (E)"
          :class="{active: interaction === 'extreme-clicking'}"
          @click="$emit('interaction', interaction === 'extreme-clicking' ? 'draw-box' : 'extreme-clicking')">
@@ -30,7 +30,7 @@
     <div class="spacer"/>
 
     <div ref="move_box"
-         class="image"
+         class="icon"
          title="move and resize bounding box (R)"
          :class="{active: interaction === 'move-box'}"
          @click="$emit('interaction', 'move-box')">
@@ -39,7 +39,7 @@
 
     <div v-if="labelsEnabled"
          ref="labels"
-         class="image"
+         class="icon"
          title="label bounding box (T)"
          :class="{active: interaction === 'label-box'}"
          @click="$emit('labelSelector')">
@@ -47,15 +47,23 @@
     </div>
 
     <div ref="confirm_box"
-         class="image"
+         class="icon"
          title="confirm bounding box (G)"
          :class="{active: interaction === 'confirm-box'}"
          @click="$emit('interaction', 'confirm-box')">
       <img alt="confirm bounding box" src="@/assets/icons/check-circle.svg">
     </div>
 
+    <div ref="confirm_all"
+         class="icon"
+         title="confirm all bounding boxes (H)"
+         :class="{active: interaction === 'confirm-box'}"
+         @click="confirmAll">
+      <img alt="confirm all bounding boxes" src="@/assets/icons/check-all.svg">
+    </div>
+
     <div ref="remove_box"
-         class="image"
+         class="icon"
          title="remove bounding box (F)"
          :class="{active: interaction === 'remove-box'}"
          @click="$emit('interaction', 'remove-box')">
@@ -65,7 +73,7 @@
     <div class="spacer"/>
 
     <div ref="crop_info"
-         class="image"
+         class="icon"
          title="Show info of a bounding box (I)"
          :class="{active: interaction === 'info-box'}"
          @click="$emit('interaction', 'info-box')">
@@ -74,7 +82,7 @@
 
     <!--
     <div ref="zoom_annotation"
-         class="image"
+         class="icon"
          title="zoom bounding box (X)"
          :class="{active: interaction === 'zoom-box' || zoomBox}"
          @click="$emit(zoomBox ? 'unzoom' : 'interaction', 'zoom-box')">
@@ -82,14 +90,14 @@
     </div>
 
     <div ref="zoom_prev_annotation"
-         class="image"
+         class="icon"
          title="zoom previous bounding box (C)"
          @click="$emit('prevzoom', true)">
       <img alt="zoom previous bounding box" src="@/assets/icons/chevron-left.svg">
     </div>
 
     <div ref="zoom_next_annotation"
-         class="image"
+         class="icon"
          title="zoom next bounding box (V)"
          @click="$emit('nextzoom', true)">
       <img alt="zoom next bounding box" src="@/assets/icons/chevron-right.svg">
@@ -99,7 +107,7 @@
     <div class="spacer"/>
 
     <div ref="show_user_annotations"
-         class="image"
+         class="icon"
          title="show user annotations"
          :class="{active: filter && filter.includes('user')}"
          @click="invertFilter('user')">
@@ -107,7 +115,7 @@
     </div>
 
     <div ref="show_pipeline_annotations"
-         class="image"
+         class="icon"
          title="show pipeline annotations"
          :class="{active: filter && filter.includes('pipeline')}"
          @click="invertFilter('pipeline')">
@@ -117,7 +125,7 @@
     <div class="spacer"/>
 
     <div ref="create_predictions"
-         class="image"
+         class="icon"
          title="create predictions (B)"
          :class="{active: isPredictionRunning}"
          @click="predict">
@@ -177,14 +185,17 @@ export default {
       else
         this.$emit('filter', [...this.filter, name]);
     },
+
     predict: function () {
-      if (!this.isPredictionRunning) {
-        // TODO then / error
-        this.$root.socket.post(`/data/${this.file.identifier}/predict`, {
-          predict: true
-        });
-      }
+      if (this.isPredictionRunning)
+        return
+      this.$emit("predict", this.file.identifier);
     },
+
+    confirmAll: function() {
+      this.$emit("confirmAll", this.file.identifier);
+    },
+
     keypressEvent: function (event) {
       switch (event.key) {
         case 'r':
@@ -209,7 +220,10 @@ export default {
         case 'g':
           this.$refs.confirm_box.click();
           break;
-        case 'b':
+        case 'h':
+          this.$refs.confirm_all.click();
+          break;
+        case 'p':
           this.$refs.create_predictions.click();
           break;
         case 'i':
@@ -260,10 +274,10 @@ export default {
 }
 
 .spacer {
-  border-top: 1px solid rgba(255, 255, 255, 0.25);
+  border-top: 2px solid rgba(255, 255, 255, 0.25);
 }
 
-.image {
+.icon {
   display: flex;
   justify-content: center;
   align-items: center;
@@ -278,9 +292,12 @@ export default {
   height: 1.6rem;
 }
 
-.image.active {
+.icon.active {
   background-color: rgba(0, 0, 0, 0.2);
-  box-shadow: inset 0 0 5px 0 rgba(0, 0, 0, 0.2);
+}
+
+.icon:hover {
+  background-color: rgba(0, 0, 0, 0.3);
 }
 
 img {