From e8f59fa3495ea80dbc274851d441217707e13209 Mon Sep 17 00:00:00 2001 From: Daniel Ecer Date: Tue, 23 Mar 2021 21:34:27 +0000 Subject: [PATCH 1/2] added fill source and filter --- README.md | 2 + ...eo-bodypix-replace-background-template.yml | 8 +++ layered_vision/filters/fill.py | 54 +++++++++++++++++++ layered_vision/filters/warp_perspective.py | 2 + layered_vision/sources/fill.py | 41 ++++++++++++++ layered_vision/utils/colors.py | 22 ++++++++ 6 files changed, 129 insertions(+) create mode 100644 layered_vision/filters/fill.py create mode 100644 layered_vision/sources/fill.py create mode 100644 layered_vision/utils/colors.py diff --git a/README.md b/README.md index abf098f..990dbf1 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ The following inputs are currently supported: | image | Static image (e.g. `.png`) | | video | Video (e.g. `.mp4`) | | webcam | Linux Webcam (`/dev/videoN`) | +| fill | Fills a new image with a color | | youtube | YouTube stream (e.g. `https://youtu.be/f0cGgOv3l4c`, see [example config](https://github.com/de-code/layered-vision/tree/develop/example-config/display-video-bodypix-replace-background-youtube.yml)) | | mss | Screen capture using [mss](https://python-mss.readthedocs.io/index.html) (see [example config](https://github.com/de-code/layered-vision/tree/develop/example-config/display-video-bodypix-replace-background-mss.yml)) | @@ -159,6 +160,7 @@ The following filters are currently supported: | `bilateral` | Applies a [bilateral filter](https://en.wikipedia.org/wiki/Bilateral_filter), using `d`, `sigma_color` and `sigma_space` parameters. | | `motion_blur` | Adds a motion blur to the image or channel. That could be used to make an alpha mask move more slowly | | `pixelate` | Pixelates the input. | +| `fill` | Fills the input or a selected channel with a color / value. e.g. with `color` set to `blue` | | `invert` | Inverts the input. e.g. `black` to `white` | | `multiply` | Multiplies the input with a constant value. e.g. to adjust the `alpha` channel | | `warp_perspective` | Warps the perspective of the input image given a list of `target_points`. e.g. to display it in a corner of the output image | diff --git a/example-config/display-video-bodypix-replace-background-template.yml b/example-config/display-video-bodypix-replace-background-template.yml index d1ba9b2..a00df74 100644 --- a/example-config/display-video-bodypix-replace-background-template.yml +++ b/example-config/display-video-bodypix-replace-background-template.yml @@ -18,6 +18,14 @@ layers: input_path: "https://www.dropbox.com/s/oqftndbs29g8ekd/carnival-rides-operating-in-an-amusement-park-3031943-360p.mp4?dl=1" repeat: true preload: true + - layers: + - id: bg_fill + input_path: "fill:" + color: blue + repeat: true + resize_like_id: in + no_source: true + enabled: false - layers: - id: bg2 enabled: false diff --git a/layered_vision/filters/fill.py b/layered_vision/filters/fill.py new file mode 100644 index 0000000..e285bde --- /dev/null +++ b/layered_vision/filters/fill.py @@ -0,0 +1,54 @@ +import logging +from typing import NamedTuple, Optional + +import numpy as np + +from layered_vision.utils.image import ImageArray +from layered_vision.filters.api import AbstractOptionalChannelFilter +from layered_vision.config import LayerConfig +from layered_vision.utils.colors import get_color_numpy_array + + +LOGGER = logging.getLogger(__name__) + + +class FillFilter(AbstractOptionalChannelFilter): + class Config(NamedTuple): + color: Optional[np.ndarray] + value: Optional[int] + + def __init__(self, layer_config: LayerConfig, **kwargs): + super().__init__(layer_config, **kwargs) + self.fill_config = self.parse_fill_config(layer_config) + + def parse_fill_config(self, layer_config: LayerConfig) -> Config: + color = layer_config.get('color') + color_value: Optional[np.ndarray] = get_color_numpy_array(color) + value = layer_config.get_int('value') + config = FillFilter.Config( + color=color_value, + value=value + ) + LOGGER.info('fill config: %s', config) + assert config.color is not None or config.value is not None + return config + + def on_config_changed(self, layer_config: LayerConfig): + super().on_config_changed(layer_config) + self.fill_config = self.parse_fill_config(layer_config) + + def do_channel_filter(self, image_array: ImageArray) -> ImageArray: + LOGGER.debug('fill, image_array dtype: %s', image_array.dtype) + config = self.fill_config + assert config.color + image_shape = image_array.shape + color_channels = image_shape[-1] + if color_channels == 1: + return np.full_like(image_array, config.value) + return np.full( + (image_shape[0], image_shape[1], len(config.color)), + config.color + ) + + +FILTER_CLASS = FillFilter diff --git a/layered_vision/filters/warp_perspective.py b/layered_vision/filters/warp_perspective.py index 55492ff..03003e7 100644 --- a/layered_vision/filters/warp_perspective.py +++ b/layered_vision/filters/warp_perspective.py @@ -68,6 +68,8 @@ def do_channel_filter(self, image_array: ImageArray) -> ImageArray: image_array, np.full_like(image_array[:, :, 0], 255), )) + if np.issubdtype(image_array.dtype, np.integer): + image_array = image_array.astype(np.float) return cv2.warpPerspective( image_array, transformation_matrix, diff --git a/layered_vision/sources/fill.py b/layered_vision/sources/fill.py new file mode 100644 index 0000000..7ded734 --- /dev/null +++ b/layered_vision/sources/fill.py @@ -0,0 +1,41 @@ +import logging +from contextlib import contextmanager +from itertools import cycle +from typing import Iterable, Iterator + +import numpy as np + +from layered_vision.utils.image import ImageArray, ImageSize +from layered_vision.sources.api import T_ImageSourceFactory, T_ImageSource +from layered_vision.utils.colors import get_color_numpy_array + + +LOGGER = logging.getLogger(__name__) + + +@contextmanager +def get_fill_image_source( + path: str, # pylint: disable=unused-argument + *args, + image_size: ImageSize = None, + repeat: bool = False, + color, + **_ +) -> Iterator[Iterable[ImageArray]]: + color_value = get_color_numpy_array(color) + if color_value is None: + raise RuntimeError('color required') + if image_size is None: + raise RuntimeError('image size required') + LOGGER.info('fill input: color=%r (image_size=%s)', color_value, image_size) + image_array = np.full( + (image_size.height, image_size.width, len(color_value)), + color_value + ) + image_array_iterable: T_ImageSource = [image_array] + if repeat: + image_array_iterable = cycle(image_array_iterable) + yield image_array_iterable + + +IMAGE_SOURCE_FACTORY: T_ImageSourceFactory = get_fill_image_source diff --git a/layered_vision/utils/colors.py b/layered_vision/utils/colors.py new file mode 100644 index 0000000..c277227 --- /dev/null +++ b/layered_vision/utils/colors.py @@ -0,0 +1,22 @@ +from typing import Optional + +import numpy as np + + +NAMED_COLORS = { + 'black': (0, 0, 0), + 'white': (255, 255, 255), + 'red': (255, 0, 0), + 'green': (0, 255, 0), + 'blue': (0, 0, 255) +} + + +def get_color_numpy_array(color) -> Optional[np.ndarray]: + if color: + if color in NAMED_COLORS: + return np.asarray(NAMED_COLORS[color]) + if isinstance(color, (tuple, list,)): + return np.asarray(color) + raise ValueError('unsupported color value type: %r (%r)' % (type(color), color)) + return None From 122531c6659a7e6336472ceb2727fd0ebf7098f2 Mon Sep 17 00:00:00 2001 From: Daniel Ecer Date: Tue, 23 Mar 2021 21:38:46 +0000 Subject: [PATCH 2/2] linting --- layered_vision/utils/image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/layered_vision/utils/image.py b/layered_vision/utils/image.py index d195317..40a7636 100644 --- a/layered_vision/utils/image.py +++ b/layered_vision/utils/image.py @@ -19,6 +19,9 @@ class SimpleImageArray: def __getitem__(self, *args) -> Union['SimpleImageArray', int, float]: pass + def astype(self, dtype: Any) -> 'SimpleImageArray': + pass + ImageArray = Union[np.ndarray, SimpleImageArray]