Browse Source

added a prototype of a cropped info box

Dimitri Korsch 3 năm trước cách đây
mục cha
commit
5e25aeb8a0

+ 10 - 2
pycs/database/File.py

@@ -1,15 +1,16 @@
 from __future__ import annotations
 
 import json
+import os
 import typing as T
 
 from contextlib import closing
 from datetime import datetime
 
 from pycs import db
-from pycs.database.Result import Result
-from pycs.database.Label import Label
 from pycs.database.Collection import Collection
+from pycs.database.Label import Label
+from pycs.database.Result import Result
 from pycs.database.base import NamedBaseModel
 
 
@@ -63,6 +64,13 @@ class File(NamedBaseModel):
     def filename(self):
         return f"{self.name}{self.extension}"
 
+    @property
+    def absolute_path(self):
+        if os.path.isabs(self.path):
+            return self.path
+
+        return os.path.join(os.getcwd(), self.path)
+
     def set_collection(self, id: T.Optional[int]):
         """
         set this file's collection

+ 10 - 1
pycs/database/base.py

@@ -1,6 +1,7 @@
-import typing as T
 import datetime
+import typing as T
 
+from flask import abort
 from sqlalchemy_serializer import SerializerMixin
 
 from pycs import app
@@ -67,6 +68,14 @@ class BaseModel(db.Model, ModelSerializer):
 
         return obj, is_new
 
+    @classmethod
+    def get_or_404(cls, obj_id: int) -> T.Any:
+        obj = cls.query.get(obj_id)
+        if obj is None:
+            abort(404, f"{cls.__name__} with ID {obj_id} could not be found!")
+
+        return obj
+
     def commit(self):
         db.session.commit()
 

+ 13 - 0
pycs/frontend/WebServer.py

@@ -43,6 +43,7 @@ from pycs.frontend.endpoints.projects.GetProjectModel import GetProjectModel
 from pycs.frontend.endpoints.projects.ListFiles import ListFiles
 from pycs.frontend.endpoints.projects.RemoveProject import RemoveProject
 from pycs.frontend.endpoints.results.ConfirmResult import ConfirmResult
+from pycs.frontend.endpoints.results.ResultAsCrop import ResultAsCrop
 from pycs.frontend.endpoints.results.CreateResult import CreateResult
 from pycs.frontend.endpoints.results.EditResultData import EditResultData
 from pycs.frontend.endpoints.results.EditResultLabel import EditResultLabel
@@ -287,6 +288,18 @@ class WebServer:
             '/results/<int:result_id>/confirm',
             view_func=ConfirmResult.as_view('confirm_result')
         )
+        self.app.add_url_rule(
+            '/results/<int:result_id>/crop',
+            view_func=ResultAsCrop.as_view('crop_result')
+        )
+        self.app.add_url_rule(
+            '/results/<int:result_id>/crop/<int:max_width>',
+            view_func=ResultAsCrop.as_view('crop_result_resized_by_width')
+        )
+        self.app.add_url_rule(
+            '/results/<int:result_id>/crop/<int:max_width>x<int:max_height>',
+            view_func=ResultAsCrop.as_view('crop_result_resized')
+        )
         self.app.add_url_rule(
             '/results/<int:result_id>/label',
             view_func=EditResultLabel.as_view('edit_result_label')

+ 6 - 103
pycs/frontend/endpoints/data/GetResizedFile.py

@@ -1,6 +1,5 @@
 import cv2
 import os
-import re
 
 from PIL import Image
 from eventlet import tpool
@@ -8,6 +7,7 @@ from flask import abort
 from flask import send_from_directory
 from flask.views import View
 
+from pycs.util import file_ops
 from pycs.database.File import File
 from pycs.database.Project import Project
 
@@ -23,111 +23,14 @@ class GetResizedFile(View):
         # get file from database
         file = File.query.get(file_id)
         if file is None:
-            abort(404, "file object not found")
+            abort(404, "File object not found")
 
-        if not os.path.exists(file.path):
-            abort(404, "image not found!")
+        if not os.path.exists(file.absolute_path):
+            abort(404, "File not found!")
 
         project = file.project
 
         # send data
-        file_directory, file_name = tpool.execute(resize_file,
-                                                  project.id, file.id, max_width, max_height)
+        file_directory, file_name = tpool.execute(file_ops.resize_file,
+                                                  file.id, max_width, max_height)
         return send_from_directory(file_directory, file_name)
-
-def resize_file(project_id, file_id, max_width, max_height):
-    """
-    If file type equals video this function extracts a thumbnail first. It calls resize_image
-    to resize and returns the resized files directory and name.
-
-    :param project: associated project
-    :param file: file object
-    :param max_width: maximum image or thumbnail width
-    :param max_height: maximum image or thumbnail height
-    :return: resized file directory, resized file name
-    """
-    file = File.query.get(file_id)
-    project = Project.query.get(project_id)
-
-    # get absolute path
-    if os.path.isabs(file.path):
-        abs_file_path = file.path
-    else:
-        abs_file_path = os.path.join(os.getcwd(), file.path)
-
-    # extract video thumbnail
-    if file.type == 'video':
-        abs_target_path = os.path.join(os.getcwd(), project.root_folder, 'temp', f'{file.uuid}.jpg')
-        create_thumbnail(abs_file_path, abs_target_path)
-
-        abs_file_path = abs_target_path
-
-    # resize image file
-    abs_target_path = os.path.join(os.getcwd(), project.root_folder,
-                                'temp', f'{file.uuid}_{max_width}_{max_height}.jpg')
-    result = resize_image(abs_file_path, abs_target_path, max_width, max_height)
-
-    # return path
-    if result is not None:
-        return os.path.split(abs_target_path)
-
-    return os.path.split(abs_file_path)
-
-def resize_image(file_path, target_path, max_width, max_height):
-    """
-    resize an image so width < max_width and height < max_height
-
-    :param file_path: path to source file
-    :param target_path: path to target file
-    :param max_width: maximum image width
-    :param max_height: maximum image height
-    :return:
-    """
-    # return if file exists
-    if os.path.exists(target_path):
-        return True
-
-    # load full size image
-    image = Image.open(file_path)
-    img_width, img_height = image.size
-
-    # abort if file is smaller than desired
-    if img_width < max_width and img_height < max_height:
-        return None
-
-    # calculate target size
-    target_width = int(max_width)
-    target_height = int(max_width * img_height / img_width)
-
-    if target_height > max_height:
-        target_height = int(max_height)
-        target_width = int(max_height * img_width / img_height)
-
-    # resize image
-    resized_image = image.resize((target_width, target_height))
-
-    # save to file
-    resized_image.save(target_path, quality=80)
-    return True
-
-def create_thumbnail(file_path, target_path):
-    """
-    extract a thumbnail from a video
-
-    :param file_path: path to source file
-    :param target_path: path to target file
-    :return:
-    """
-    # return if file exists
-    if os.path.exists(target_path):
-        return
-
-    # load video
-    video = cv2.VideoCapture(file_path)
-
-    # create thumbnail
-    _, image = video.read()
-    cv2.imwrite(target_path, image)
-
-    # close video file
-    video.release()

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

@@ -1,4 +1,4 @@
-from flask import abort, jsonify
+from flask import jsonify
 from flask.views import View
 
 from pycs.database.Project import Project
@@ -14,9 +14,7 @@ class GetProjectResults(View):
 
     def dispatch_request(self, project_id: int):
         # get project from database
-        project = Project.query.get(project_id)
-        if project is None:
-            return abort(404)
+        project = Project.get_or_404(project_id)
 
         # map media files to a dict
         storage = MediaStorage(project.id)

+ 57 - 0
pycs/frontend/endpoints/results/ResultAsCrop.py

@@ -0,0 +1,57 @@
+import os
+
+from flask import abort
+from flask import send_from_directory
+from flask.views import View
+
+from pycs.database.Result import Result
+from pycs.util import file_ops
+
+
+class ResultAsCrop(View):
+    """
+    return the image crop defined by the result.
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+
+    def dispatch_request(self, result_id: int, max_width: int = 2**24, max_height: int = 2**24):
+
+        # find result
+        result = Result.get_or_404(result_id)
+
+        if result.file.type != "image":
+            abort(400, f"Currently only supporting images!")
+
+        if result.type != "bounding-box":
+            abort(400, f"The type of the queried result was not \"bounding-box\"! It was {result.type}")
+
+        data = result.data
+
+        if result.data is None:
+            abort(400, "The data of the result was None!")
+
+        xywh = [data.get(attr, -1) for attr in "xywh"]
+        if -1 in xywh:
+            abort(400, f"The data of the result is not correct: {data}!")
+
+        x, y, w, h = xywh
+
+        crop_path, crop_fname = file_ops.crop_file(result.file.id, x, y, w, h)
+
+        parts = os.path.splitext(crop_fname)
+
+        crop_new_fname = f"{parts[0]}_{max_width}_{max_height}.{parts[1]}"
+
+        resized = file_ops.resize_image(
+            os.path.join(crop_path, crop_fname),
+            os.path.join(crop_path, crop_new_fname),
+            max_width,
+            max_height
+        )
+
+        if resized:
+            crop_fname = crop_new_fname
+
+        return send_from_directory(crop_path, crop_fname)

+ 4 - 6
pycs/interfaces/MediaFile.py

@@ -1,5 +1,6 @@
-from os import path, getcwd
-from typing import Optional, List, Union
+from typing import List
+from typing import Optional
+from typing import Union
 
 from pycs.database.File import File
 from pycs.database.Result import Result
@@ -23,10 +24,7 @@ class MediaFile:
         self.frames = file.frames
         self.fps = file.fps
 
-        if path.isabs(file.path):
-            self.path = file.path
-        else:
-            self.path = path.join(getcwd(), file.path)
+        self.path = file.absolute_path
 
     def set_collection(self, reference: Optional[str]):
         """

+ 141 - 0
pycs/util/file_ops.py

@@ -0,0 +1,141 @@
+import cv2
+import os
+
+from PIL import Image
+
+from pycs.database.File import File
+
+def crop_file(file_id: int, x: float, y: float, w: float, h: float) -> str:
+    """
+    gets a file for the given file_id, crops the according image to the
+    bounding box and saves the crops in the temp folder of the project.
+
+    :param file: file object
+    :param x: relative x-coordinate of the top left corner
+    :param y: relative y-coordinate of the top left corner
+    :param w: relative width of the bounding box
+    :param h: relative height of the bounding box
+
+    :return: directory and file name of the cropped patch
+    """
+
+    file = File.query.get(file_id)
+    file_path = file.absolute_path
+    root_folder = file.project.root_folder
+
+    image = Image.open(file_path)
+    width, height = image.size
+
+    crop_width = int(width * w)
+    crop_height = int(height * h)
+
+    x0 = int(width * x)
+    y0 = int(height * y)
+    x1 = x0 + crop_width
+    y1 = y0 + crop_height
+
+    target_path = os.path.join(
+        os.getcwd(),
+        root_folder,
+        'temp',
+        f'{file.uuid}_{x0}-{y0}_{x1}-{y1}.jpg',
+    )
+
+    if not os.path.exists(target_path):
+        crop = image.crop((x0, y0, x0+crop_width, y0+crop_height))
+        crop.save(target_path, quality=80)
+
+    return os.path.split(target_path)
+
+
+def resize_file(file_id: int, max_width: int, max_height: int) -> tuple[str, str]:
+    """
+    If file type equals video this function extracts a thumbnail first. It calls resize_image
+    to resize and returns the resized files directory and name.
+
+    :param file: file object
+    :param max_width: maximum image or thumbnail width
+    :param max_height: maximum image or thumbnail height
+    :return: resized file directory, resized file name
+    """
+    file = File.query.get(file_id)
+    project = file.project
+
+    abs_file_path = file.absolute_path
+
+    # extract video thumbnail
+    if file.type == 'video':
+        abs_target_path = os.path.join(os.getcwd(), project.root_folder, 'temp', f'{file.uuid}.jpg')
+        create_thumbnail(abs_file_path, abs_target_path)
+
+        abs_file_path = abs_target_path
+
+    # resize image file
+    abs_target_path = os.path.join(os.getcwd(), project.root_folder,
+                                'temp', f'{file.uuid}_{max_width}_{max_height}.jpg')
+    result = resize_image(abs_file_path, abs_target_path, max_width, max_height)
+
+    # return path
+    if result is not None:
+        return os.path.split(abs_target_path)
+
+    return os.path.split(abs_file_path)
+
+def resize_image(file_path: str, target_path: str, max_width: int, max_height: int):
+    """
+    resize an image so width < max_width and height < max_height
+
+    :param file_path: path to source file
+    :param target_path: path to target file
+    :param max_width: maximum image width
+    :param max_height: maximum image height
+    :return:
+    """
+    # return if file exists
+    if os.path.exists(target_path):
+        return True
+
+    # load full size image
+    image = Image.open(file_path)
+    img_width, img_height = image.size
+
+    # abort if file is smaller than desired
+    if img_width < max_width and img_height < max_height:
+        return None
+
+    # calculate target size
+    target_width = int(max_width)
+    target_height = int(max_width * img_height / img_width)
+
+    if target_height > max_height:
+        target_height = int(max_height)
+        target_width = int(max_height * img_width / img_height)
+
+    # resize image
+    resized_image = image.resize((target_width, target_height))
+
+    # save to file
+    resized_image.save(target_path, quality=80)
+    return True
+
+def create_thumbnail(file_path: str, target_path: str):
+    """
+    extract a thumbnail from a video
+
+    :param file_path: path to source file
+    :param target_path: path to target file
+    :return:
+    """
+    # return if file exists
+    if os.path.exists(target_path):
+        return
+
+    # load video
+    video = cv2.VideoCapture(file_path)
+
+    # create thumbnail
+    _, image = video.read()
+    cv2.imwrite(target_path, image)
+
+    # close video file
+    video.release()

+ 2 - 0
webui/src/assets/style/main.css

@@ -1,5 +1,7 @@
 * {
     box-sizing: border-box;
+    margin: 0;
+    padding: 0;
 }
 
 html, body {

+ 4 - 2
webui/src/components/media/annotated-image.vue

@@ -54,8 +54,10 @@
       </div>
 
       <cropped-image ref="info"
-                     :file="current"
-      />
+                     :width="300"
+                     :margin="10"
+                     :labels="labels"
+                     />
     </template>
   </div>
 </template>

+ 56 - 11
webui/src/components/media/cropped-image.vue

@@ -1,7 +1,12 @@
 <template>
     <div v-if="visible" class="cropped-image">
-        <div v-if="crop">{{crop}}</div>
-        <div v-else>Select a crop to show</div>
+        <div class="image-container" :style="style">
+          <div v-if="box">
+            <h3>{{labelName}}</h3>
+            <img :src="src" ref="crop" alt="crop" />
+          </div>
+          <div v-else>Select a crop to show</div>
+        </div>
     </div>
 
 
@@ -10,10 +15,7 @@
 <script>
     export default {
         name: "cropped-image",
-        props: [
-            'file',
-            'crop',
-        ],
+        props: ["width", "margin", "labels"],
 
 
         // mounted: function() {}
@@ -22,29 +24,72 @@
         data: function() {
             return {
                 visible: false,
+                box: null,
             }
         },
 
+        computed: {
+          src: function () {
+            if (!this.box)
+              return ''
+            var width = this.width - 2 * this.margin /*padding*/;
+            return this.$root.socket.url(`/results/${this.box.id}/crop/${width}x${width}`)
+          },
+
+          style: function() {
+
+            return {
+              minWidth: `${this.width}px`,
+              margin: `${this.margin}px`,
+            }
+
+          },
+          labelName: function() {
+            if (!this.box)
+              return ''
+
+            if (this.box.label_id == null)
+              return 'Unknown'
+
+            var label = this.labels[this.box.label_id]
+            return label.name
+          },
+
+        },
+
         methods: {
             toggle: function () {
                 this.visible = !this.visible;
+
+                if (!this.visible)
+                  this.box = null;
             },
 
             show: function (box_element) {
-                console.log(box_element.position);
+              this.box = box_element.box;
             },
         },
-
     }
 </script>
 
 <style scoped>
 
 .cropped-image {
-    height: 100%;
+  height: 100%;
+
+  background-color: rgba(133, 133, 133, 0.7);
+}
+
+.image-container {
+  /*align-items: center;*/
+  justify-content: center;
+  text-align: center;
+  display: flex;
+}
 
-    background-color: rgba(133, 133, 133, 0.7);
-    padding: 10px;
+.image-container img {
+  border-width: 2px;
+  border-style: solid;
 }
 
 </style>