Skip to content

Commit

Permalink
feat(image): position map (#14)
Browse files Browse the repository at this point in the history
* feat(image): position map

get position map as image from mesh with padding, unit tests.

Task: TECHART-359
  • Loading branch information
Olaf-Wolf3D authored Jan 5, 2024
1 parent d4b2594 commit f7b6e60
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ A collection of tools for dealing with mesh related data.

You'll find setup instruction of this project in the [CONTRIBUTING.md](https://github.com/readyplayerme/meshops/blob/main/CONTRIBUTING.md) file.

## Position Map

Position maps encode locations on the surface of a mesh as color values in an image using the UV layout.
Since 8-bit colors are positive values only and capped at 255, the linear transformation of the positions into the color space is lossy and not invertible, meaning that the original positions cannot be recovered from the color values.
However, these maps can be utilized as control signals for various tasks such as texture synthesis and for shader effects.

## UV Seams Transitioning

UV seams are splits in a triangle mesh that, however, is supposed to represent a continuous surface across these splits.
Expand Down
35 changes: 34 additions & 1 deletion src/readyplayerme/meshops/draw/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np
import numpy.typing as npt
import skimage
from scipy.ndimage import gaussian_filter
from scipy.ndimage import gaussian_filter, maximum_filter

from readyplayerme.meshops import mesh as mops
from readyplayerme.meshops.draw.color import (
Expand Down Expand Up @@ -308,3 +308,36 @@ def blend_uv_seams(mesh: mops.Mesh, image: Image) -> Image:

# Blend in average vertex color at the UV seams.
return blend_images(image, raster_image, blurred_mask).astype(np.uint8)


def get_position_map(width: int, height: int, mesh: mops.Mesh, padding: int = 4) -> Image:
"""Get a position map from the given mesh.
The positions are normalized and then mapped to the 8-bit range [0, 255].
:param width: Width of the position map in pixels.
:param height: Height of the position map in pixels.
:param mesh: The mesh to use for creating the position map.
:param padding: Padding in pixels to add around the UV shells. Default 4.
:return: The position map as uint8.
"""
positions = mesh.vertices
# Since 8-bit images only store positive numbers up to 255, first normalize the positions to the range [0, 1].
positions = positions - positions.min(axis=0)
positions = positions / positions.max(axis=0)
# Map the normalized positions to the 8-bit color format.
positions = (positions * 255).astype(np.uint8)
# Create an image from the positions.
image_coords = mops.uv_to_image_coords(mesh.uv_coords, width, height)
triangle_coords = mops.get_faces_image_coords(mesh.faces, mesh.uv_coords, width, height)
pos_map = create_nan_image(width, height, ColorMode.RGB)
pos_map = rasterize(pos_map, mesh.edges, image_coords, positions, inplace=True)
# Constrain colored pixels to the UV shells.
mask = create_mask(width, height, triangle_coords).astype(bool)[:, :, np.newaxis].repeat(3, axis=2)
pos_map *= mask
# Add padding around the UV shells.
if padding > 0:
padded = maximum_filter(pos_map, size=padding, axes=[0, 1])
pos_map[~mask] = padded[~mask]

return pos_map.astype(np.uint8)
247 changes: 247 additions & 0 deletions tests/readyplayerme/meshops/unit/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,3 +702,250 @@ def test_blend_uv_seams(mock_mesh: Mesh, image: Image, expected_output: Image):
pytest.skip("Skipping test of RGB & RGBA for debugging purposes.")
output = draw.blend_uv_seams(mock_mesh, image)
np.testing.assert_array_equal(output, expected_output)


@pytest.mark.parametrize(
"mock_mesh, padding, expected_output",
[
# No Padding
(
"mock_mesh",
0,
np.array(
[
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [255, 0, 0], [0, 0, 0], [0, 0, 0]],
[
[0, 0, 0],
[51, 25, 0],
[102, 50, 0],
[153, 76, 0],
[204, 101, 0],
[255, 127, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 51],
[0, 85, 85],
[0, 170, 0],
[63, 180, 0],
[127, 191, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 102],
[0, 170, 170],
[0, 212, 85],
[0, 255, 0],
[255, 255, 0],
[255, 191, 0],
[255, 127, 0],
[0, 0, 0],
],
[
[0, 0, 153],
[0, 127, 204],
[0, 255, 255],
[127, 191, 255],
[255, 191, 127],
[255, 127, 127],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 204],
[0, 127, 255],
[127, 63, 255],
[191, 95, 255],
[255, 127, 255],
[255, 63, 127],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 255],
[63, 31, 255],
[127, 63, 255],
[191, 63, 255],
[255, 63, 255],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [255, 0, 255], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
],
dtype=np.uint8,
),
),
# Negative Padding
(
"mock_mesh",
-4,
np.array(
[
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [255, 0, 0], [0, 0, 0], [0, 0, 0]],
[
[0, 0, 0],
[51, 25, 0],
[102, 50, 0],
[153, 76, 0],
[204, 101, 0],
[255, 127, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 51],
[0, 85, 85],
[0, 170, 0],
[63, 180, 0],
[127, 191, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 102],
[0, 170, 170],
[0, 212, 85],
[0, 255, 0],
[255, 255, 0],
[255, 191, 0],
[255, 127, 0],
[0, 0, 0],
],
[
[0, 0, 153],
[0, 127, 204],
[0, 255, 255],
[127, 191, 255],
[255, 191, 127],
[255, 127, 127],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 204],
[0, 127, 255],
[127, 63, 255],
[191, 95, 255],
[255, 127, 255],
[255, 63, 127],
[0, 0, 0],
[0, 0, 0],
],
[
[0, 0, 255],
[63, 31, 255],
[127, 63, 255],
[191, 63, 255],
[255, 63, 255],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
],
[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [255, 0, 255], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
],
dtype=np.uint8,
),
),
# Positive Padding
(
"mock_mesh",
4,
np.array(
[
[
[51, 25, 0],
[102, 50, 0],
[153, 76, 0],
[204, 101, 0],
[255, 127, 0],
[255, 0, 0],
[255, 127, 0],
[255, 127, 0],
],
[
[0, 0, 0],
[51, 25, 0],
[102, 50, 0],
[153, 76, 0],
[204, 101, 0],
[255, 127, 0],
[255, 191, 0],
[255, 127, 0],
],
[
[0, 0, 51],
[0, 85, 85],
[0, 170, 0],
[63, 180, 0],
[127, 191, 0],
[255, 255, 0],
[255, 255, 0],
[255, 191, 0],
],
[
[0, 0, 102],
[0, 170, 170],
[0, 212, 85],
[0, 255, 0],
[255, 255, 0],
[255, 191, 0],
[255, 127, 0],
[255, 191, 127],
],
[
[0, 0, 153],
[0, 127, 204],
[0, 255, 255],
[127, 191, 255],
[255, 191, 127],
[255, 127, 127],
[255, 255, 255],
[255, 191, 127],
],
[
[0, 0, 204],
[0, 127, 255],
[127, 63, 255],
[191, 95, 255],
[255, 127, 255],
[255, 63, 127],
[255, 255, 255],
[255, 191, 127],
],
[
[0, 0, 255],
[63, 31, 255],
[127, 63, 255],
[191, 63, 255],
[255, 63, 255],
[255, 191, 255],
[255, 191, 255],
[255, 127, 127],
],
[
[63, 127, 255],
[127, 127, 255],
[191, 127, 255],
[255, 127, 255],
[255, 0, 255],
[255, 127, 255],
[255, 127, 255],
[255, 63, 127],
],
],
dtype=np.uint8,
),
),
],
indirect=["mock_mesh"],
)
def test_get_position_map(mock_mesh: Mesh, padding: int, expected_output: Image):
"""Test the get_position_map function with valid inputs."""
width = height = 8
output = draw.get_position_map(width, height, mock_mesh, padding=padding)
np.testing.assert_array_equal(output, expected_output)

0 comments on commit f7b6e60

Please sign in to comment.