6
0
Jelajahi Sumber

move ResultAsCrop to GetCroppedFile

Eric Tröbs 3 tahun lalu
induk
melakukan
e3817e48b9

+ 5 - 13
pycs/frontend/WebServer.py

@@ -12,6 +12,7 @@ from pycs.frontend.endpoints.ListLabelProviders import ListLabelProviders
 from pycs.frontend.endpoints.ListModels import ListModels
 from pycs.frontend.endpoints.ListProjects import ListProjects
 from pycs.frontend.endpoints.additional.FolderInformation import FolderInformation
+from pycs.frontend.endpoints.data.GetCroppedFile import GetCroppedFile
 from pycs.frontend.endpoints.data.GetFile import GetFile
 from pycs.frontend.endpoints.data.GetPreviousAndNextFile import GetPreviousAndNextFile
 from pycs.frontend.endpoints.data.GetResizedFile import GetResizedFile
@@ -38,7 +39,6 @@ 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.CreateResult import CreateResult
-from pycs.frontend.endpoints.results.ResultAsCrop import ResultAsCrop
 from pycs.frontend.endpoints.results.EditResultData import EditResultData
 from pycs.frontend.endpoints.results.EditResultLabel import EditResultLabel
 from pycs.frontend.endpoints.results.GetProjectResults import GetProjectResults
@@ -205,6 +205,10 @@ class WebServer:
             '/data/<int:file_id>/<resolution>',
             view_func=GetResizedFile.as_view('get_resized_file', database)
         )
+        self.__flask.add_url_rule(
+            '/data/<int:file_id>/<resolution>/<crop_box>',
+            view_func=GetCroppedFile.as_view('crop_result', database)
+        )
         self.__flask.add_url_rule(
             '/data/<int:file_id>/previous_next',
             view_func=GetPreviousAndNextFile.as_view('get_previous_and_next_file', database)
@@ -235,18 +239,6 @@ class WebServer:
             '/results/<int:result_id>/confirm',
             view_func=ConfirmResult.as_view('confirm_result', database, notifications)
         )
-        self.__flask.add_url_rule(
-            '/results/<int:result_id>/crop',
-            view_func=ResultAsCrop.as_view('crop_result', database)
-        )
-        self.__flask.add_url_rule(
-            '/results/<int:result_id>/crop/<int:max_width>',
-            view_func=ResultAsCrop.as_view('crop_result_resized_by_width', database)
-        )
-        self.__flask.add_url_rule(
-            '/results/<int:result_id>/crop/<int:max_width>x<int:max_height>',
-            view_func=ResultAsCrop.as_view('crop_result_resized', database)
-        )
         self.__flask.add_url_rule(
             '/results/<int:result_id>/label',
             view_func=EditResultLabel.as_view('edit_result_label', database, notifications)

+ 52 - 0
pycs/frontend/endpoints/data/GetCroppedFile.py

@@ -0,0 +1,52 @@
+import os
+import re
+
+from eventlet import tpool
+from flask import abort
+from flask import send_from_directory
+from flask.views import View
+
+from pycs.database.Database import Database
+from pycs.util.FileOperations import crop_file
+
+
+class GetCroppedFile(View):
+    """
+    return the image crop defined by the result.
+    """
+    # pylint: disable=arguments-differ
+    methods = ['GET']
+
+    def __init__(self, db: Database):
+        # pylint: disable=invalid-name
+        self.db = db
+
+    def dispatch_request(self, file_id: int, resolution: str, crop_box: str):
+        # get file from database
+        file = self.db.file(file_id)
+        if file is None:
+            return abort(404)
+
+        project = file.project()
+
+        if not os.path.exists(file.absolute_path):
+            abort(404, "File not found!")
+
+        # extract desired crop
+        resolution = re.split(r'[^0-9]', resolution)
+        max_width = int(resolution[0])
+        max_height = int(resolution[1]) if len(resolution) > 1 else 2 ** 24
+
+        crop_box = re.split(r'[^0-9.]', crop_box)
+        crop_x = float(crop_box[0])
+        crop_y = float(crop_box[1]) if len(crop_box) > 1 else 0
+        crop_w = float(crop_box[2]) if len(crop_box) > 2 else 1 - crop_x
+        crop_h = float(crop_box[3]) if len(crop_box) > 3 else 1 - crop_y
+
+        # crop file
+        file_directory, file_name = tpool.execute(crop_file, file, project.root_folder,
+                                                  crop_x, crop_y, crop_w, crop_h,
+                                                  max_width, max_height)
+
+        # send to client
+        return send_from_directory(file_directory, file_name)

+ 5 - 3
pycs/frontend/endpoints/data/GetResizedFile.py

@@ -37,7 +37,9 @@ class GetResizedFile(View):
         max_width = int(resolution[0])
         max_height = int(resolution[1]) if len(resolution) > 1 else 2 ** 24
 
-        # send data
-        file_directory, file_name = tpool.execute(resize_file, file,
-                                                  project.root_folder, max_width, max_height)
+        # resize file
+        file_directory, file_name = tpool.execute(resize_file, file, project.root_folder,
+                                                  max_width, max_height)
+
+        # send to client
         return send_from_directory(file_directory, file_name)

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

@@ -1,67 +0,0 @@
-import os
-
-from flask import abort
-from flask import send_from_directory
-from flask.views import View
-
-from pycs.database.Database import Database
-from pycs.util.FileOperations import crop_file
-
-
-class ResultAsCrop(View):
-    """
-    return the image crop defined by the result.
-    """
-    # pylint: disable=arguments-differ
-    methods = ['GET']
-
-    def __init__(self, db: Database):
-        # pylint: disable=invalid-name
-        self.db = db
-
-    def dispatch_request(self, result_id: int, max_width: int = 2**24, max_height: int = 2**24):
-        # find result
-        result = self.db.result(result_id)
-
-        if result is None:
-            abort(404)
-
-        if result.type != "bounding-box":
-            abort(400, f"The type of the queried result was not \"bounding-box\"! It was {result.type}")
-
-        file = result.file()
-
-        if file.type != "image":
-            abort(400, f"Currently only supporting images!")
-
-
-        data = result.data
-
-        if 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
-
-        project = file.project()
-
-        crop_path, crop_fname = crop_file(file, project.root_folder, x, y, w, h)
-
-        parts = os.path.splitext(crop_fname)
-
-        crop_new_fname = f"{parts[0]}_{max_width}_{max_height}.{parts[1]}"
-
-        resized = 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)

+ 107 - 59
pycs/util/FileOperations.py

@@ -1,5 +1,4 @@
 import os
-from typing import Optional
 from typing import Tuple
 
 import cv2
@@ -47,61 +46,58 @@ def file_info(data_folder: str, file_name: str, file_ext: str):
     return file_type, frames, fps
 
 
-def crop_file(file: File, project_root: str, x: float, y: float, w: float, h: float) -> str:
+def resize_file(file: File, project_root: str, max_width: int, max_height: int) -> Tuple[str, 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.
+    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 project_root: project root folder path
-    :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
+    :param max_width: maximum image or thumbnail width
+    :param max_height: maximum image or thumbnail height
+    :return: resized file directory, resized file name
     """
+    abs_file_path = file.absolute_path
 
-    # TODO in browser cache
-    # TODO not possible for video files
-
-    image = Image.open(file.absolute_path)
-    width, height = image.size
-
-    crop_width = int(width * w)
-    crop_height = int(height * h)
+    # extract video thumbnail
+    if file.type == 'video':
+        abs_target_path = os.path.join(os.getcwd(), project_root, 'temp', f'{file.uuid}.jpg')
+        create_thumbnail(abs_file_path, abs_target_path)
 
-    x0 = int(width * x)
-    y0 = int(height * y)
-    x1 = x0 + crop_width
-    y1 = y0 + crop_height
+        abs_file_path = abs_target_path
 
-    target_path = os.path.join(
-        os.getcwd(),
-        project_root,
-        'temp',
-        f'{file.uuid}_{x0}-{y0}_{x1}-{y1}.jpg',
-    )
+    # resize image file
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{max_width}_{max_height}.jpg')
+    result = resize_image(abs_file_path, abs_target_path, max_width, max_height)
 
-    if not os.path.exists(target_path):
-        crop = image.crop((x0, y0, x0 + crop_width, y0 + crop_height))
-        crop.save(target_path, quality=DEFAULT_JPEG_QUALITY)
+    # return path
+    if result is not None:
+        return os.path.split(abs_target_path)
 
-    return os.path.split(target_path)
+    return os.path.split(abs_file_path)
 
 
-def resize_file(file: File, project_root: str, max_width: int, max_height: int) -> Tuple[str, str]:
+def crop_file(file: File, project_root: str,
+              x: float, y: float, w: float, h: float,
+              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.
+    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 project_root: project root folder path
     :param max_width: maximum image or thumbnail width
     :param max_height: maximum image or thumbnail height
-    :return: resized file directory, resized file name
-    """
+    :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
+    """
     abs_file_path = file.absolute_path
 
     # extract video thumbnail
@@ -111,28 +107,63 @@ def resize_file(file: File, project_root: str, max_width: int, max_height: int)
 
         abs_file_path = abs_target_path
 
-    # resize image file
-    abs_target_path = os.path.join(os.getcwd(), project_root,
-                                   'temp', f'{file.uuid}_{max_width}_{max_height}.jpg')
+    # crop image file
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{x}_{y}_{w}_{h}.jpg')
+    result = crop_image(abs_file_path, abs_target_path, x, y, w, h)
+
+    if result:
+        abs_file_path = abs_target_path
+
+    # resize image
+    abs_target_path = os.path.join(os.getcwd(),
+                                   project_root,
+                                   'temp',
+                                   f'{file.uuid}_{max_width}_{max_height}_{x}_{y}_{w}_{h}.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)
+    if result:
+        abs_file_path = abs_target_path
 
+    # return image
     return os.path.split(abs_file_path)
 
 
-def resize_image(file_path: str, target_path: str,
-                 max_width: int, max_height: int) -> Optional[bool]:
+def create_thumbnail(file_path: str, target_path: str) -> None:
+    """
+    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()
+
+
+def resize_image(file_path: str, target_path: str, max_width: int, max_height: int) -> bool:
     """
-    resize an image so width < max_width and height < max_height
+    Resize an image so width < `max_width` and height < `max_height` applies.
+    If the image is already smaller than the given dimensions no new file is stored.
 
     :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: `True` if a resize operation was performed or the target file already exists
     """
     # return if file exists
     if os.path.exists(target_path):
@@ -144,7 +175,7 @@ def resize_image(file_path: str, target_path: str,
 
     # abort if file is smaller than desired
     if img_width < max_width and img_height < max_height:
-        return None
+        return False
 
     # calculate target size
     target_width = int(max_width)
@@ -162,24 +193,41 @@ def resize_image(file_path: str, target_path: str,
     return True
 
 
-def create_thumbnail(file_path: str, target_path: str) -> None:
+def crop_image(file_path: str, target_path: str, x: float, y: float, w: float, h: float) -> bool:
     """
-    extract a thumbnail from a video
+    Crop an image with the given coordinates, width and height.
+    If however no crop is applied no new file is stored.
 
     :param file_path: path to source file
     :param target_path: path to target file
-    :return:
+    :param x: crop x position (normalized)
+    :param y: crop y position (normalized)
+    :param w: crop width (normalized)
+    :param h: crop height (normalized)
+    :return: `True` if a crop operation was performed or the target file already exists
     """
     # return if file exists
     if os.path.exists(target_path):
-        return
+        return True
 
-    # load video
-    video = cv2.VideoCapture(file_path)
+    # abort if no crop would be applied
+    if x <= 0 and y <= 0 and w >= 1 and h >= 1:
+        return False
 
-    # create thumbnail
-    _, image = video.read()
-    cv2.imwrite(target_path, image)
+    # load full size image
+    image = Image.open(file_path)
+    img_width, img_height = image.size
 
-    # close video file
-    video.release()
+    # calculate absolute crop position
+    crop_x1 = int(img_width * x)
+    crop_y1 = int(img_height * y)
+    crop_x2 = min(int(img_width * w) + crop_x1, img_width)
+    crop_y2 = min(int(img_height * h) + crop_y1, img_height)
+
+    # crop image
+    print(crop_x1, crop_y1, crop_x2, crop_y2)
+    cropped_image = image.crop((crop_x1, crop_y1, crop_x2, crop_y2))
+
+    # save to file
+    cropped_image.save(target_path, quality=DEFAULT_JPEG_QUALITY)
+    return True