From df10367f3edc28cc28a5e8e5e774909e45c57fc7 Mon Sep 17 00:00:00 2001 From: Daniel-Ionut Rancea <117300186+TechyDaniel@users.noreply.github.com> Date: Fri, 24 Nov 2023 07:13:56 -0800 Subject: [PATCH] feat: update uv_to_texture_space function (#5) * feat: Implement UV to texture space conversion - Add function to convert UV coordinates to texture space pixel coordinates. - Ensure correct handling of edge cases including wrapping of out-of-range UVs. - Include comprehensive tests to validate functionality across various scenarios. --------- Co-authored-by: Olaf Haag <83961384+Olaf-Wolf3D@users.noreply.github.com> --- src/readyplayerme/meshops/mesh.py | 38 +++++++++++++- src/readyplayerme/meshops/types.py | 3 ++ tests/readyplayerme/meshops/unit/test_mesh.py | 51 ++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/readyplayerme/meshops/mesh.py b/src/readyplayerme/meshops/mesh.py index 2ef70cc..fd456e1 100644 --- a/src/readyplayerme/meshops/mesh.py +++ b/src/readyplayerme/meshops/mesh.py @@ -6,7 +6,7 @@ import trimesh from scipy.spatial import cKDTree -from readyplayerme.meshops.types import Edges, IndexGroups, Indices, Mesh, Vertices +from readyplayerme.meshops.types import Edges, IndexGroups, Indices, Mesh, PixelCoord, UVs, Vertices def read_mesh(filename: str | Path) -> Mesh: @@ -92,3 +92,39 @@ def get_overlapping_vertices( processed.update(neighbors) return grouped_indices + + +def uv_to_image_coords( + uvs: UVs, + width: int, + height: int, + indices: Indices | None = None, +) -> PixelCoord: + """Convert UV coordinates to image space coordinates. + + :param uvs: UV coordinates. + :param indices: Optional subset of UV indices for which to retrieve pixel coordinates. + :param width: Width of the image in pixels. + :param height: Height of the image in pixels. + :return: Coordinates in image space given the input width and height. + """ + try: + selected_uvs = uvs if indices is None else uvs[indices] + except IndexError as error: + msg = f"Index {np.where(indices>=len(uvs))[0]} is out of bounds for UVs with shape {uvs.shape}." # type: ignore + raise IndexError(msg) from error + if not len(selected_uvs): + return np.empty((0, 2), dtype=np.uint16) + + # Wrap UV coordinates within the range [0, 1]. + wrapped_uvs = np.mod(selected_uvs, 1) + + # With wrapping, we keep the max 1 as 1 and not transpose into the next space. + wrapped_uvs[selected_uvs == 1] = 1 + + # Convert UV coordinates to texture space (pixel coordinates) + img_coords = np.empty((len(selected_uvs), 2), dtype=np.uint16) + img_coords[:, 0] = (wrapped_uvs[:, 0] * (width - 0.5)).astype(np.uint16) + img_coords[:, 1] = ((1 - wrapped_uvs[:, 1]) * (height - 0.5)).astype(np.uint16) + + return img_coords diff --git a/src/readyplayerme/meshops/types.py b/src/readyplayerme/meshops/types.py index d4189f7..e6ac9db 100644 --- a/src/readyplayerme/meshops/types.py +++ b/src/readyplayerme/meshops/types.py @@ -11,6 +11,9 @@ Faces: TypeAlias = npt.NDArray[np.int32] | npt.NDArray[np.int64] # Shape (f, 3) IndexGroups: TypeAlias = list[npt.NDArray[np.uint32]] +UVs: TypeAlias = npt.NDArray[np.float32] | npt.NDArray[np.float64] # Shape (i, 2) +PixelCoord: TypeAlias = npt.NDArray[np.uint16] # Shape (i, 2) + class Mesh(Protocol): """Structural type for a mesh class. diff --git a/tests/readyplayerme/meshops/unit/test_mesh.py b/tests/readyplayerme/meshops/unit/test_mesh.py index 1d09b17..569ce68 100644 --- a/tests/readyplayerme/meshops/unit/test_mesh.py +++ b/tests/readyplayerme/meshops/unit/test_mesh.py @@ -8,7 +8,7 @@ import pytest from readyplayerme.meshops import mesh -from readyplayerme.meshops.types import IndexGroups, Indices, Mesh, Vertices +from readyplayerme.meshops.types import IndexGroups, Indices, Mesh, PixelCoord, UVs, Vertices class TestReadMesh: @@ -119,3 +119,52 @@ def test_get_overlapping_vertices_error_handling(indices): vertices = np.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]) with pytest.raises(IndexError): mesh.get_overlapping_vertices(vertices, indices) + + +@pytest.mark.parametrize( + "uvs, width, height, indices, expected", + [ + # Simple UV conversion with specific indices + (np.array([[0.5, 0.5], [0.25, 0.75]]), 100, 100, np.array([0]), np.array([[49, 49]])), + # Full range UV conversion without specific indices + (np.array([[0.0, 0.0], [1.0, 1.0]]), 200, 200, None, np.array([[0, 199], [199, 0]])), + # Near 0 and 1 values + ( + np.array([[0.0001, 0.9999], [0.9999, 0.0001]]), + 200, + 200, + np.array([0, 1]), + np.array([[0, 0], [199, 199]]), + ), + # Empty indices + (np.array([[0.5, 0.5], [0.25, 0.75]]), 50, 50, np.array([], dtype=np.uint8), np.empty((0, 2))), # FixMe + # UV coordinates out of range (non square tex - negative values) + (np.array([[-0.5, 1.5], [1.0, -1.0]]), 10, 100, np.array([0, 1]), np.array([[4, 49], [9, 99]])), + # UV coordinates out of range (wrapped - negative values) + (np.array([[-0.25, 1.5], [-2.0, -1.0]]), 100, 100, np.array([0, 1]), np.array([[74, 49], [0, 99]])), + # UV coordinates for non square + (np.array([[0.5, 0.5], [0.25, 0.75]]), 124, 10024, np.array([0]), np.array([[61, 5011]])), + # 1px image + (np.array([[0.5, 0.5], [-1, 1], [0, 0]]), 1, 1, np.array([0, 1, 2]), np.array([[0, 0], [0, 0], [0, 0]])), + # 0 px image + (np.array([[0.5, 0.5], [0.25, 0.75]]), 0, 0, np.array([0]), np.array([[0, 0]])), + ], +) +def test_uv_to_image_coords(uvs: UVs, width: int, height: int, indices: Indices, expected: PixelCoord): + """Test the uv_to_texture_space function returns the correct texture space coordinates.""" + image_space_coords = mesh.uv_to_image_coords(uvs, width, height, indices) + assert np.array_equal(image_space_coords, expected), "Image space coordinates do not match expected values." + + +@pytest.mark.parametrize( + "uvs, width, height, indices", + [ + (np.array([[0.5, 0.5], [0.25, 0.75]]), 100, 100, np.array([0, 1, 2])), + # No UV coord + (np.array([]), 1, 1, np.array([0, 1, 2])), + ], +) +def test_uv_to_image_coords_exceptions(uvs: UVs, width: int, height: int, indices: Indices): + """Test the uv_to_image_space function raises expected exceptions.""" + with pytest.raises(IndexError): + mesh.uv_to_image_coords(uvs, width, height, indices)