Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(image): position map #14

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)