-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: added image blending function and unit tests - Add tests for non-uniform mask values in blend_images. - Include RGB images blending tests. - Handle edge cases and exception scenarios. - Cover different image sizes and empty masks. * feat: add functions to get color mode of colors & images --------- Co-authored-by: Olaf Haag <[email protected]>
- Loading branch information
1 parent
98ee8b0
commit 9101a0d
Showing
4 changed files
with
271 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
from readyplayerme.meshops.types import Color, ColorMode | ||
from readyplayerme.meshops.types import Image as IMG_type | ||
|
||
|
||
def get_image_color_mode(image: IMG_type) -> ColorMode: | ||
""" | ||
Determine the color mode of an image. | ||
:param image: An image array. | ||
:return ColorMode:: Enum indicating the color mode of the image (GRAYSCALE, RGB, or RGBA). | ||
""" | ||
try: | ||
n_channels = image.shape[-1] | ||
except IndexError as error: | ||
error_msg = "Image has invalid shape: zero dimensions." | ||
raise ValueError(error_msg) from error | ||
match image.ndim, n_channels: | ||
case 2, _: | ||
return ColorMode.GRAYSCALE | ||
case 3, 3: | ||
return ColorMode.RGB | ||
case 3, 4: | ||
return ColorMode.RGBA | ||
case _: | ||
error_msg = "Invalid color mode for an image." | ||
raise ValueError(error_msg) | ||
|
||
|
||
def get_color_array_color_mode(color_array: Color) -> ColorMode: | ||
""" | ||
Determine the color mode of a color array. | ||
:param color_array: An array representing colors. | ||
:return ColorMode: Enum indicating the color mode of the color array (GRAYSCALE, RGB, or RGBA). | ||
""" | ||
try: | ||
n_channels = color_array.shape[-1] | ||
except IndexError as error: | ||
error_msg = "Color has invalid shape: zero dimensions." | ||
raise ValueError(error_msg) from error | ||
match color_array.ndim, n_channels: | ||
case 1, _: | ||
return ColorMode.GRAYSCALE | ||
case 2, 1: | ||
return ColorMode.GRAYSCALE | ||
case 2, 3: | ||
return ColorMode.RGB | ||
case 2, 4: | ||
return ColorMode.RGBA | ||
case _: | ||
msg = "Invalid dimensions for a color array." | ||
raise ValueError(msg) | ||
|
||
|
||
def blend_images(image1: IMG_type, image2: IMG_type, mask: IMG_type) -> IMG_type: | ||
""" | ||
Blend two images using a mask. | ||
This function performs a blending operation on two images using a mask. The mask determines the blending ratio | ||
at each pixel. The blending is done via a vectorized operation for efficiency. | ||
:param image1: The first image to blend. | ||
:param image2: The second image to blend. Must be the same shape as the first image. | ||
:param mask: The blending mask. Must be the same shape as the images. | ||
:return: The blended image. | ||
""" | ||
try: | ||
# Check if the error is due to shape mismatch | ||
if not (image1.shape == image2.shape): | ||
msg = "image1 and image2 must have the same shape." | ||
raise ValueError(msg) | ||
# Mask is reshaped to be able to perform the blending | ||
expected_dimensions = 2 | ||
if mask.ndim == expected_dimensions and image1.ndim == expected_dimensions + 1: | ||
mask = mask[:, :, None] | ||
# Perform the blending operation using vectorized NumPy operations | ||
blended_image = (1 - mask) * image1 + mask * image2 | ||
return blended_image | ||
except AttributeError as error: | ||
# Re-raise the original exception if it's not a shape mismatch | ||
msg = "Could not blend the two images with the given mask" | ||
raise AttributeError(msg) from error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
"""Unit tests for the image module.""" | ||
|
||
import numpy as np | ||
import pytest | ||
|
||
import readyplayerme.meshops.image as img | ||
from readyplayerme.meshops.types import ColorMode | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"image_1, image_2, mask, expected_output", | ||
[ | ||
# Basic blending with a uniform mask | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[0.5, 0.5], [0.5, 0.5]]), | ||
np.array([[0.5, 0.5], [0.5, 0.5]]), | ||
), | ||
# Blending with different mask values | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[0.2, 0.8], [0.3, 0.7]]), | ||
np.array([[0.2, 0.8], [0.3, 0.7]]), | ||
), | ||
# Mask with all zeros (full image_1) | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[0, 0], [0, 0]]), | ||
), | ||
# Mask with all ones (full image_2) | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[1, 1], [1, 1]]), | ||
), | ||
# Non uniform mask values: | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([[0, 1], [1, 0]]), | ||
np.array([[0, 1], [1, 0]]), | ||
), | ||
# Full RGB Image | ||
( | ||
np.zeros((2, 2, 3)), # Black RGB image | ||
np.ones((2, 2, 3)), # White RGB image | ||
np.full((2, 2), 0.5), # Uniform grayscale mask | ||
np.full((2, 2, 3), 0.5), # Expected output: gray RGB image | ||
), | ||
], | ||
) | ||
def test_blend_images(image_1, image_2, mask, expected_output): | ||
"""Test the blend_images function with various input scenarios.""" | ||
output = img.blend_images(image_1, image_2, mask) | ||
np.testing.assert_array_equal(output, expected_output) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"image_1, image_2, mask, expected_exception", | ||
[ | ||
# Shape mismatch | ||
( | ||
np.array([[0, 0]]), # Shape (1, 2) | ||
np.array([[1, 1], [1, 1], [1, 1]]), # Shape (3, 2) | ||
np.array([[0.5, 0.5]]), # Shape (1, 2), | ||
ValueError, | ||
), | ||
# Invalid input type | ||
("0, 0, 0", np.array([[1, 1], [1, 1]]), np.array([[0.5, 0.5], [0.5, 0.5]]), AttributeError), | ||
# Empty mask | ||
( | ||
np.array([[0, 0], [0, 0]]), | ||
np.array([[1, 1], [1, 1]]), | ||
np.array([]), | ||
ValueError, | ||
), | ||
], | ||
) | ||
def test_blend_images_should_fail(image_1, image_2, mask, expected_exception): | ||
"""Test the blend_images function with invalid input scenarios.""" | ||
with pytest.raises(expected_exception): | ||
img.blend_images(image_1, image_2, mask) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"image, expected_mode", | ||
[ | ||
# Grayscale image (2D array) | ||
(np.array([[0, 1], [1, 0]], dtype=np.float32), ColorMode.GRAYSCALE), | ||
# RGB image (3D array with shape (h, w, 3)) | ||
(np.random.rand(10, 10, 3).astype(np.float32), ColorMode.RGB), | ||
# RGBA image (3D array with shape (h, w, 4)) | ||
(np.random.rand(10, 10, 4).astype(np.float32), ColorMode.RGBA), | ||
], | ||
) | ||
def test_get_image_color_mode(image, expected_mode): | ||
"""Test the get_image_color_mode function with valid inputs.""" | ||
assert img.get_image_color_mode(image) == expected_mode | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"image", | ||
[ | ||
# Invalid image: 2D array with incorrect channel count | ||
np.random.rand(10, 10, 5).astype(np.float32), | ||
# Invalid image: 1D array | ||
np.array([1, 2, 3], dtype=np.float32), | ||
# Invalid image: 4D array | ||
np.random.rand(10, 10, 10, 3).astype(np.float32), | ||
], | ||
) | ||
def test_get_image_color_mode_should_fail(image): | ||
"""Test the get_image_color_mode function with invalid inputs.""" | ||
with pytest.raises(ValueError): | ||
img.get_image_color_mode(image) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"color_array, expected_mode", | ||
[ | ||
# Grayscale color array (1D array) | ||
(np.array([128, 255, 100], dtype=np.uint8), ColorMode.GRAYSCALE), | ||
# Grayscale color array (2D array with single channel) | ||
(np.array([[128], [255], [100]], dtype=np.uint8), ColorMode.GRAYSCALE), | ||
# RGB color array (2D array with shape (n, 3)) | ||
(np.array([[255, 0, 0], [0, 255, 0], [0, 0, 255]], dtype=np.uint8), ColorMode.RGB), | ||
# RGBA color array (2D array with shape (n, 4)) | ||
(np.array([[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255]], dtype=np.uint8), ColorMode.RGBA), | ||
], | ||
) | ||
def test_get_color_array_color_mode(color_array, expected_mode): | ||
"""Test the get_color_array_color_mode function with valid inputs.""" | ||
assert img.get_color_array_color_mode(color_array) == expected_mode | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"color_array", | ||
[ | ||
# Invalid color array: 2D array with incorrect channel count | ||
np.array([[1, 2, 3, 4, 5]], dtype=np.uint8), | ||
# Invalid color array: 3D array | ||
np.random.rand(10, 10, 3).astype(np.uint8), | ||
# Invalid color array: 0-dimensional array | ||
np.array(128, dtype=np.uint8), | ||
], | ||
) | ||
def test_get_color_array_color_mode_should_fail(color_array): | ||
"""Test the get_color_array_color_mode function with invalid inputs.""" | ||
with pytest.raises(ValueError): | ||
img.get_color_array_color_mode(color_array) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters