Source code for facelift.transform

# -*- encoding: utf-8 -*-
# Copyright (c) 2020 Stephen Bunn <stephen@bunn.io>
# ISC License <https://choosealicense.com/licenses/isc>

"""Contains some common necessary frame transformation helper methods.

These transformation methods are useful for optimizing face detection in frames.
Typically face detection takes much longer the more pixels there are to consider.
Therefore, using :func:`~scale` or :func:`~resize` will help you speed up detection.

These helper transforms can be composed together to produce apply multiple operations on
a single frame.
For example, if we wanted to first downscale by half and then rotate a frame by 90
degrees, we could do something like the following:

.. code-block:: python

    from facelift.transform import rotate, scale
    transformed_frame = rotate(scale(frame, 0.5), 90)

Attributes:
    DEFAULT_INTERPOLATION (int):
        The default type of interpolation to use in transforms that require an
        interpolation method. Defaults to ``cv2.INTER_AREA``.
"""

import warnings
from typing import Optional, Tuple

import cv2
import numpy

from .types import Frame

DEFAULT_INTERPOLATION: int = cv2.INTER_AREA


[docs]def copy(frame: Frame) -> Frame: """Copy the given frame to a new location in memory. Examples: >>> from facelift.transform import copy >>> copied_frame = copy(frame) >>> assert frame == copied_frame >>> assert frame is not copied_frame Args: frame (:attr:`~.types.Frame`): The frame to copy Returns: :attr:`~.types.Frame`: An exact copy of the given frame """ return frame.copy()
[docs]def scale( frame: Frame, factor: float, interpolation: int = DEFAULT_INTERPOLATION ) -> Frame: """Scale a given frame down or up depending on the given scale factor. Examples: Downscaling a frame can be performed with a ``scale`` factor >0 and <1. For example, scaling a frame to half of its original size would require a scale factor of 0.5. >>> from facelift.transform import scale >>> assert frame.shape[:1] == [512, 512] >>> downscaled_frame = scale(frame, 0.5) >>> assert downscaled_frame.shape[:1] == [256, 256] Upscaling a frame with this method is **very** naive and suboptimal. However, any value >1 will result in a upscaled frame. For example, scaling a frame to double its original size would require a scale factor of 2. >>> from facelift.transform import scale >>> assert frame.shape[:1] == [512, 512] >>> upscaled_frame = scale(frame, 2) >>> assert upscaled_frame.shape[:1] == [1024, 1024] Following this logic, a scale factor of 1 would result in absolutely no change to the given frame. .. warning:: This transformation will return the **exact same frame instance** as the one provided through the ``frame`` parameter in the following cases: 1. If a factor of exactly ``1`` is given. In this case the scale operation would result in no change. 2. The given frame has factor less than ``1`` a width or height of 1px. In this case we are attempting to scale down the given frame and we cannot scale down the frame any further without producing a 0px frame. Args: frame (:attr:`~.types.Frame`): The frame to scale factor (float): The factor to scale the given frame interpolation (Optional[int], optional): The type of interpolation to use in the scale operation. Defaults to :attr:`~DEFAULT_INTERPOLATION`. Raises: ValueError: When the given scale factor is not positive Returns: :attr:`~.types.Frame`: The newly scaled frame """ if factor <= 0: raise ValueError( f"Factor should be a positive floating point, received {factor!r}" ) if factor == 1: return frame height, width, *_ = frame.shape if factor < 1 and (height == 1 or width == 1): return frame return cv2.resize( src=frame, dsize=None, fx=factor, fy=factor, interpolation=interpolation, )
[docs]def resize( frame: Frame, width: Optional[int] = None, height: Optional[int] = None, lock_aspect: bool = True, interpolation: int = DEFAULT_INTERPOLATION, ) -> Frame: """Resize a given frame to a given width and/or height. * If both width and height are given, the frame will be resized accordingly. * If only one of width or height is given, the frame will be resized according to the provided dimension (either width or height). * As long as ``lock_aspect`` is truthy, the unprovided dimension will be adjusted to maintain the original aspect-ratio of the frame. * If ``lock_aspect`` is falsy, the resize operation will only scale the provided dimension while keeping the original size of the unprovided dimension. Examples: Resize a frame's width while keeping the height relative: >>> from facelift.transform import resize >>> assert frame.shape[:1] == [512, 512] >>> resized_frame = resize(frame, width=256, lock_aspect=True) >>> assert resized_frame.shape[:1] == [256, 256] Resize a frame's width while keeping the original height: >>> from facelift.transform import resize >>> assert frame.shape[:1] == [512, 512] >>> resized_frame = resize(frame, width=256, lock_aspect=False) >>> assert resized_frame.shape[:1] == [512, 256] Resize both a frame's width and height: >>> from facelift.transform import resize >>> assert frame.shape[:1] == [512, 512] >>> resized_frame = resize(frame, width=256, height=128) >>> assert resized_frame.shape[:1] == [128, 256] Args: frame (:attr:`~.types.Frame`): The frame to resize width (Optional[int], optional): The exact width to resize the frame to. height (Optional[int], optional): The exact height to resize the frame to. lock_aspect (bool, optional): Whether to keep the width and height relative when only given one value. Defaults to True. interpolation (int, optional): The type of interpolation to use in the resize operation. Defaults to :attr:`~DEFAULT_INTERPOLATION`. Returns: :attr:`~.types.Frame`: The newly resized frame """ if width == 0 or height == 0: raise ValueError("Cannot resize frame to a width or height of 0") if width is None and height is None: return frame if width and height: return cv2.resize( src=frame, dsize=(width, height), fx=None, fy=None, interpolation=interpolation, ) frame_height, frame_width, *_ = frame.shape if not lock_aspect: return cv2.resize( src=frame, dsize=(width or frame_width, height or frame_height), fx=None, fy=None, interpolation=interpolation, ) if height is not None: relative_width = int(frame_width * (height / float(frame_height))) or 1 return cv2.resize( src=frame, dsize=(relative_width, height), fx=None, fy=None, interpolation=interpolation, ) elif width is not None: relative_height = int(frame_height * (width / float(frame_width))) or 1 return cv2.resize( src=frame, dsize=(width, relative_height), fx=None, fy=None, interpolation=interpolation, ) return frame # pragma: no cover
[docs]def rotate( frame: Frame, degrees: int, interpolation: int = DEFAULT_INTERPOLATION ) -> Frame: """Rotate a frame while keeping the whole frame visible. Examples: >>> from facelift.transform import rotate >>> rotated_90 = rotate(frame, 90) >>> rotated_neg_90 = rotate(frame, -90) .. warning:: This transform typically will produce larger frames since we are producing a rotated frame while keeping the original frame completely visible. This means if we do a perfect 45 degree rotation on a 512x512 frame we will produce a 724x724 frame since the 512x512 frame is now on a angle that requires a larger container. Be cautious when using rotation. Most of the time you do not need to rotate on any angles other than 90, 180, and 270 for decent face detection. However, this isn't *always* true. Args: frame (:attr:`~.types.Frame`): The frame to rotate degrees (int): The number of degrees to rotate the given frame interpolation (int, optional): The type of interpolation to use in the produced rotation matrix. Defaults to :attr:`~DEFAULT_INTERPOLATION`. Returns: :attr:`~.types.Frame`: The newly rotated frame """ if abs(degrees) in (0, 360): return frame frame_height, frame_width, *_ = frame.shape center_x, center_y = frame_width / 2, frame_height / 2 rotation_matrix = cv2.getRotationMatrix2D( center=(center_x, center_y), angle=-degrees, scale=1.0 ) cos = numpy.abs(rotation_matrix[0, 0]) sin = numpy.abs(rotation_matrix[0, 1]) new_width = int((frame_height * sin) + (frame_width * cos)) new_height = int((frame_height * cos) + (frame_width * sin)) rotation_matrix[0, 2] += (new_width / 2) - center_x rotation_matrix[1, 2] += (new_height / 2) - center_y return cv2.warpAffine( src=frame, M=rotation_matrix, dsize=(new_width, new_height), flags=interpolation, )
[docs]def crop(frame: Frame, start: Tuple[int, int], end: Tuple[int, int]) -> Frame: """Crop the given frame between two top-left to bottom-right points. Examples: Crop a frame from the first pixel to the center pixel. >>> from facelift.transform import crop >>> assert frame.shape[:1] == [512, 512] >>> cropped_frame = crop(frame, (0, 0), (256, 256)) >>> assert cropped_frame.shape[:1] == [256, 256] Args: frame (:attr:`~.types.Frame`): The frame to crop start (Tuple[int, int]): The top-left point to start the crop at end (Tuple[int, int]): The bottom-right point to end the crop at Raises: ValueError: When the given starting crop point appears after the given ending crop point Returns: :attr:`~.types.Frame`: The newly cropped frame """ left, top = start right, bottom = end if right <= left or bottom <= top: raise ValueError( "Starting crop point cannot be greater than the ending crop point, " f"(start={start}, end={end})" ) width = right - left height = bottom - top return frame[top : top + height, left : left + width]
[docs]def translate( frame: Frame, delta_x: Optional[int] = None, delta_y: Optional[int] = None, interpolation: int = DEFAULT_INTERPOLATION, ) -> Frame: """Translate the given frame a specific distance away from its origin. Examples: >>> from facelift.transform import translate >>> translated_neg_90_x_frame = translate(frame, delta_x=-90) .. important:: This translation retains the original size of the given frame. So a 512x512 frame translated 90px on the x-axis will still be 512x512 and space where the frame use to take up will be essentially nulled out. Args: frame (:attr:`~.types.Frame`): The frame to translate delta_x (Optional[int], optional): The pixel distance to translate the frame on the x-axis. delta_y (Optional[int], optional): The pixel distance to translate the frame on the y-axis. interpolation (int, optional): The type of interpolation to use during the translation. Defaults to :attr:`~DEFAULT_INTERPOLATION`. Returns: :attr:`~.types.Frame`: The newly translated frame """ if delta_x is None and delta_y is None: return frame translation_matrix = numpy.float32([[1, 0, delta_x or 0], [0, 1, delta_y or 0]]) frame_height, frame_width, *_ = frame.shape return cv2.warpAffine( src=frame, M=translation_matrix, dsize=(frame_width, frame_height), flags=interpolation, )
[docs]def flip(frame: Frame, x_axis: bool = False, y_axis: bool = False) -> Frame: """Flip the given frame over either or both the x and y axis. Examples: >>> from facelift.transform import flip >>> vertically_flipped_frame = flip(frame, x_axis=True) >>> horizontally_flipped_frame = flip(frame, y_axis=True) >>> inverted_frame = flip(frame, x_axis=True, y_axis=True) Args: frame (:attr:`~.types.Frame`): The frame to flip x_axis (bool, optional): Flag indicating the frame should be flipped vertically. Defaults to False. y_axis (bool, optional): Flag indicating the frame should be flipped horizontally. Defaults to False. Returns: :attr:`~.types.Frame`: The newly flipped frame """ if not x_axis and not y_axis: return frame if x_axis and y_axis: flip_code = -1 elif y_axis: flip_code = 0 else: flip_code = 1 return cv2.flip(src=frame, flipCode=flip_code)
[docs]def adjust( frame: Frame, brightness: Optional[int] = None, sharpness: Optional[float] = None, ) -> Frame: """Adjust the brightness or sharpness of a frame. Examples: >>> from facelift.transform import adjust >>> sharper_frame = adjust(frame, sharpness=1.4) >>> brighter_frame = adjust(frame, brightness=10) >>> sharper_and_brighter_frame = adjust(frame, sharpness=1.4, brightness=10) Args: frame (:attr:`~.types.Frame`): The frame to adjust brightness (Optional[int], optional): The new brightness of the frame (can be negative, default is 0). Defaults to 0. sharpness (Optional[float], optional): The new sharpness of the frame (0.0 is black, default is 1.0). Defaults to 1.0. Returns: :attr:`~.types.Frame`: The newly adjusted frame """ if brightness is None and sharpness is None: return frame if brightness is None: brightness = 0 if sharpness is None: sharpness = 1.0 return cv2.convertScaleAbs(src=frame, alpha=sharpness, beta=brightness)
[docs]def grayscale(frame: Frame) -> Frame: """Convert the given frame to grayscale. This helper is useful *sometimes* for classification as color doesn't matter as much during face encoding. Examples: >>> from facelift.transform import grayscale >>> grayscale_frame = grayscale(bgr_frame) Args: frame (:attr:`~.types.Frame`): The BGR frame to convert to grayscale Returns: :attr:`~.types.Frame`: The newly grayscaled frame """ return cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2GRAY)
[docs]def rgb(frame: Frame) -> Frame: """Convert the given frame to RGB. This helper transform is typically needed when working with other image processing libraries such as `pillow <https://pillow.readthedocs.io/en/stable/>`_ as they work in RGB coordinates while OpenCV works in BGR coordinates. Examples: >>> from facelift.transform import rgb >>> rgb_frame = rgb(bgr_frame) Args: frame (:attr:`~.types.Frame`): The BGR frame to convert to RGB Returns: :attr:`~.types.Frame`: The new RGB frame """ return cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2RGB)