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

added one-click bounding box annotation creation

Dimitri Korsch 3 жил өмнө
parent
commit
91dc7d985f

+ 1 - 0
.gitignore

@@ -47,3 +47,4 @@ dist/
 *.sqlite3-journal
 
 output*.json
+settings.local.json

+ 4 - 2
pycs/__init__.py

@@ -17,8 +17,10 @@ from sqlalchemy.engine import Engine
 
 from pycs.util.JSONEncoder import JSONEncoder
 
-print('=== Loading settings ===')
-with open('settings.json', encoding='utf8') as file:
+settings_file = os.environ.get("PYCS_SETTINGS", "settings.json")
+
+print(f'=== Loading settings from "{settings_file}" ===')
+with open(settings_file, encoding='utf8') as file:
     settings = munchify(json.load(file))
 
 # create projects folder

+ 1 - 1
pycs/database/Project.py

@@ -107,7 +107,7 @@ class Project(NamedBaseModel):
         """
         return self.labels.filter(Label.reference == reference).one_or_none()
 
-    def file(self, identifier: int) -> T.Optional[Label]:
+    def file(self, identifier: int) -> T.Optional[File]:
         """
         get a file using its unique identifier
 

+ 8 - 1
pycs/frontend/WebServer.py

@@ -9,8 +9,8 @@ import socketio
 
 from flask import send_from_directory
 
-from pycs.database.Model import Model
 from pycs.database.LabelProvider import LabelProvider
+from pycs.database.Model import Model
 from pycs.frontend.endpoints.ListJobs import ListJobs
 from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders
 from pycs.frontend.endpoints.ListModels import ListModels
@@ -29,6 +29,7 @@ from pycs.frontend.endpoints.labels.EditLabelParent import EditLabelParent
 from pycs.frontend.endpoints.labels.ListLabelTree import ListLabelTree
 from pycs.frontend.endpoints.labels.ListLabels import ListLabels
 from pycs.frontend.endpoints.labels.RemoveLabel import RemoveLabel
+from pycs.frontend.endpoints.pipelines.EstimateBoundingBox import EstimateBoundingBox
 from pycs.frontend.endpoints.pipelines.FitModel import FitModel
 from pycs.frontend.endpoints.pipelines.PredictBoundingBox import PredictBoundingBox
 from pycs.frontend.endpoints.pipelines.PredictFile import PredictFile
@@ -341,6 +342,12 @@ class WebServer:
                                           self.jobs, self.pipelines)
         )
 
+        self.app.add_url_rule(
+            '/data/<int:file_id>/estimate',
+            view_func=EstimateBoundingBox.as_view('estimate_result', self.notifications,
+                                          self.jobs)
+        )
+
     def run(self):
         """ start web server """
         self.pipelines.start()

+ 147 - 0
pycs/frontend/endpoints/pipelines/EstimateBoundingBox.py

@@ -0,0 +1,147 @@
+import cv2
+import uuid
+import numpy as np
+import typing as T
+
+from flask import abort
+from flask import make_response
+from flask import request
+from flask.views import View
+
+from pycs import db
+from pycs.database.File import File
+from pycs.database.Result import Result
+from pycs.frontend.notifications.NotificationManager import NotificationManager
+from pycs.jobs.JobGroupBusyException import JobGroupBusyException
+from pycs.jobs.JobRunner import JobRunner
+
+class EstimateBoundingBox(View):
+    """
+    create a result for a file
+    """
+    # pylint: disable=arguments-differ
+    methods = ['POST']
+
+    def __init__(self, nm: NotificationManager, jobs: JobRunner,):
+        # pylint: disable=invalid-name
+        self.nm = nm
+        self.jobs = jobs
+
+    def dispatch_request(self, file_id: int):
+
+        file = File.get_or_404(file_id)
+        request_data = request.get_json(force=True)
+        if 'x' not in request_data or 'y' not in request_data:
+            abort(400, "coordinates for the estimation are missing")
+
+        x,y = map(request_data.get, "xy")
+
+        # get project
+        project = file.project
+        try:
+            rnd = str(uuid.uuid4())[:10]
+            self.jobs.run(project,
+                          "Estimation",
+                          f'{project.name} (create predictions)',
+                          f"{project.id}/estimation/{rnd}",
+                          estimate,
+                          file.id, x, y,
+                          result=self.nm.create_result
+                          )
+
+        except JobGroupBusyException:
+            abort(400, "Job is already running!")
+
+        return make_response()
+
+
+def estimate(file_id: int, x: float, y: float) -> Result:
+    file = File.query.get(file_id)
+
+    im = cv2.imread(file.absolute_path, cv2.IMREAD_GRAYSCALE)
+
+    h, w = im.shape
+    pos = int(x * w), int(y * h)
+    x0, y0, x1, y1 = detect(im, pos,
+                            window_size=1000,
+                            pixel_delta=50,
+                            enlarge=1e-2,
+                           )
+
+    data = dict(
+       x=x0 / w,
+       y=y0 / h,
+       w=(x1-x0) / w,
+       h=(y1-y0) / h
+    )
+
+    return file.create_result('pipeline', 'bounding-box', label=None, data=data)
+
+def detect(im: np.ndarray,
+           pos: T.Tuple[int, int],
+           window_size: int = 1000,
+           pixel_delta: int = 0,
+           enlarge: float = -1) -> T.Tuple[int, int, int, int]:
+    # im = blur(im, 3)
+    x, y = pos
+    pixel = im[y, x]
+
+    min_pix, max_pix = pixel - pixel_delta, pixel + pixel_delta
+
+    mask = np.logical_and(min_pix < im, im < max_pix).astype(np.float32)
+    # mask = open_close(mask)
+    # mask = blur(mask)
+
+    pad = window_size // 2
+    mask = np.pad(mask, pad, mode="constant")
+    window = mask[y: y + window_size, x: x + window_size]
+
+    sum_x, sum_y = window.sum(axis=0), window.sum(axis=1)
+
+    enlarge = int(enlarge * max(im.shape))
+    (x0, x1), (y0, y1) = get_borders(sum_x, enlarge), get_borders(sum_y, enlarge)
+
+    x0 = max(x + x0 - pad, 0)
+    y0 = max(y + y0 - pad, 0)
+
+    x1 = min(x + x1 - pad, im.shape[1])
+    y1 = min(y + y1 - pad, im.shape[0])
+
+    return x0, y0, x1, y1
+
+def get_borders(arr, enlarge: int, eps=5e-1):
+    mid = len(arr) // 2
+
+    arr0, arr1 = arr[:mid], arr[mid:]
+
+    thresh = arr[mid] * eps
+
+    lowers = np.where(arr0 < thresh)[0]
+    lower = 0 if len(lowers) == 0 else lowers[-1]
+
+    uppers = np.where(arr1 < thresh)[0]
+    upper = arr1.argmin() if len(uppers) == 0 else uppers[0]
+
+    # since the second half starts after the first
+    upper = len(arr0) + upper
+
+    if enlarge > 0:
+        lower = max(lower - enlarge, 0)
+        upper = min(upper + enlarge, len(arr)-1)
+
+    return int(lower), int(upper)
+
+
+"""
+def blur(im, sigma=5):
+    from skimage import filters
+    return filters.gaussian(im, sigma=sigma, preserve_range=True)
+
+def open_close(im, kernel_size=3):
+
+    kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
+
+    im = cv2.morphologyEx(im, cv2.MORPH_OPEN, kernel)
+    im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel)
+    return im
+"""

+ 8 - 0
webui/src/components/media/annotation-overlay.vue

@@ -199,6 +199,14 @@ export default {
       if (this.interaction === 'extreme-clicking')
         return;
 
+
+      if (this.interaction === 'estimate-box'){
+        const coordinates = this.getEventCoordinates(event);
+        this.$root.socket.post(`/data/${this.file.identifier}/estimate`, coordinates);
+        return;
+      }
+
+
       if (this.current) {
         if (this.callback)
           this.callback(this.current);

+ 12 - 1
webui/src/components/media/options-bar.vue

@@ -8,6 +8,15 @@
       <img alt="draw bounding box" src="@/assets/icons/screen-full.svg">
     </div>
 
+
+    <div ref="estimate_box"
+         class="image"
+         title="estimate bounding box (M)"
+         :class="{active: interaction === 'estimate-box'}"
+         @click="$emit('interaction', 'estimate-box')">
+      <img alt="estimate bounding box" src="@/assets/icons/paper-airplane.svg">
+    </div>
+
     <div ref="extreme_clicking"
          class="image"
          title="extreme clicking (E)"
@@ -18,7 +27,6 @@
       <img v-else
            alt="extreme clicking" src="@/assets/icons/flame.svg">
     </div>
-
     <div class="spacer"/>
 
     <div ref="move_box"
@@ -200,6 +208,9 @@ export default {
         case 'q':
           this.$refs.draw_box.click();
           break;
+        case 'w':
+          this.$refs.estimate_box.click();
+          break;
         case 'e':
           this.$refs.extreme_clicking.click();
           break;

+ 2 - 1
webui/src/components/projects/project-open-window.vue

@@ -25,7 +25,8 @@
         </div>
 
         <div v-if="sortedProjects.length === 0"
-             class="project">
+          @click="create = true"
+          class="project">
           <h2>There are no projects available.</h2>
           <div class="description">Please use the button at the bottom of the page to create a new one.</div>
         </div>