Skip to content

Commit

Permalink
feat(image): attribute_to_image (#15)
Browse files Browse the repository at this point in the history
a generalized solution to turn arbitrary color attributes into images using a UV layout. Used by object space normal maps, and position maps, and can potentially be used for morphTargets, skin weights etc.
  • Loading branch information
Olaf-Wolf3D authored May 8, 2024
1 parent f7b6e60 commit 4a6fe8e
Show file tree
Hide file tree
Showing 17 changed files with 1,124 additions and 235 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Convert line endings to LF.
* text=auto
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# Following people will be requested for review when someone opens a pull request.
* @TechyDaniel @Olaf-Wolf3D
* @Olaf-Wolf3D
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ repos:
# Code style and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.285
rev: v0.4.3
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

- repo: https://github.com/psf/black
rev: 23.7.0
rev: 24.4.2
hooks:
- id: black
args: [
Expand All @@ -42,7 +42,7 @@ repos:
args: ["--verbose", "2"] # override the .docstr.yaml to see less output

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.5.1'
rev: 'v1.10.0'
hooks:
- id: mypy
additional_dependencies: [numpy, pytest]
Expand Down
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,55 @@ 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
## Features

### Position Map

```python
from readyplayerme.meshops.draw.image import get_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
### Object Space Normal Maps

```python
from readyplayerme.meshops.draw.image import get_obj_space_normal_map
```

Object space normal maps encode the surface normals of a mesh as color values in an image using the UV layout.
Similar to position maps, the conversion from normals to colors is lossy.
They also can be used as control signals for various tasks such as texture synthesis.

### Any Vertex Attribute to Image

```python
from readyplayerme.meshops.draw.image import get_vertex_attribute_image
```

This function allows you to convert any vertex attribute of a mesh that can be represented as a color to an image.

### UV Island Mask

```python
from readyplayerme.meshops.draw.image import create_mask
```

This function creates a black and white mask image from the mesh's UV layout.

### UV Seams Transitioning

```python
from readyplayerme.meshops.draw.image import blend_uv_seams
```

UV seams are splits in a triangle mesh that, however, is supposed to represent a continuous surface across these splits.
These splits are necessary to allow the mesh to flatten out cleanly in UV space and have as little distortion in the texture projection as possible.
The UV seams are splits between UV islands for which the projection onto the mesh should appear as seamless as possible.

### Goal
#### Goal

- identify UV splits in a mesh
- mitigate the mismatch between image content when transitioning over a UV split
32 changes: 17 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ test = [
"pyinstrument",
]
lint = [
"black>=23.3.0",
"mypy>=1.5.1",
"ruff>=0.0.285",
"black==24.4.2",
"mypy==1.10.0",
"ruff==0.4.3",
]
dev = [
"readyplayerme.meshops[test, lint]",
Expand Down Expand Up @@ -73,7 +73,7 @@ post-install-commands = [
]

[tool.hatch.envs.default.scripts]
install-precommit = "pre-commit install -t pre-commit -t commit-msg -t pre-push"
install-precommit = "pre-commit install --overwrite -t pre-commit -t commit-msg -t pre-push"
test = "pytest {args:tests}"
test-cov = "coverage run -m pytest {args:tests}"
cov-report = [
Expand All @@ -87,7 +87,7 @@ cov = [
]
typing = "mypy --install-types --non-interactive {args:src/readyplayerme/meshops tests}"
style = [
"ruff {args:.}",
"ruff check {args:.}",
"black --check --diff {args:.}",
]
fmt = [
Expand Down Expand Up @@ -124,13 +124,16 @@ python = ["3.10", "3.11"]
#addopts="-n auto"

[tool.black]
target-versions = ["py310", "py311"]
target-version = ["py310", "py311"]
line-length = 120
skip-string-normalization = false

[tool.ruff]
target-version = "py311"
line-length = 120
builtins = ["_"]

[tool.ruff.lint]
select = [
"A",
"ANN",
Expand Down Expand Up @@ -178,7 +181,6 @@ ignore = [
# Don't require documentation for every function parameter and magic methods.
"D417", "D102", "D105", "D107", "D100"
]
builtins = ["_"]
unfixable = [
# Don't touch unused imports
"F401",
Expand Down Expand Up @@ -208,32 +210,32 @@ exclude = [
"venv",
]

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["readyplayerme"]

[tool.ruff.flake8-tidy-imports]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"

[tool.ruff.flake8-quotes]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"

[tool.ruff.flake8-annotations]
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
allow-star-arg-any = true
ignore-fully-untyped = true
suppress-none-returning = true

[tool.ruff.flake8-unused-arguments]
[tool.ruff.lint.flake8-unused-arguments]
ignore-variadic-names = true

[tool.ruff.pycodestyle]
[tool.ruff.lint.pycodestyle]
ignore-overlong-task-comments = true

[tool.ruff.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "pep257"
ignore-decorators = ["typing.overload"]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
# Tests can use magic values, assertions, and relative imports
"tests/**/*" = ["PLR2004", "S101", "TID252"]

Expand Down
2 changes: 1 addition & 1 deletion src/readyplayerme/meshops/__about__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Ready Player Me <[email protected]>
#
# SPDX-License-Identifier: MIT
__version__ = "0.0.1"
__version__ = "0.1.0"
1 change: 1 addition & 0 deletions src/readyplayerme/meshops/draw/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Module for drawing operations on images."""

from readyplayerme.meshops.draw.color import * # noqa: F403
from readyplayerme.meshops.draw.image import * # noqa: F403
58 changes: 54 additions & 4 deletions src/readyplayerme/meshops/draw/color.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""Functions to deal with colors and color modes."""

from typing import Any

import numpy as np
import numpy.typing as npt
import skimage

from readyplayerme.meshops.types import Color, ColorMode, Image, IndexGroups

Expand Down Expand Up @@ -33,19 +38,19 @@ def get_image_color_mode(image: Image) -> ColorMode:
raise ValueError(error_msg)


def get_color_array_color_mode(color_array: Color) -> ColorMode:
def get_color_mode(color: Color) -> ColorMode:
"""
Determine the color mode of a color array.
:param color_array: An array representing colors.
:param color: 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]
n_channels = color.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:
match color.ndim, n_channels:
case 1, _:
return ColorMode.GRAYSCALE
case 2, 1:
Expand Down Expand Up @@ -117,3 +122,48 @@ def interpolate_values(start: Color, end: Color, num_steps: int) -> Color:
return start + t * (end - start)
else:
return start[None, :] + t[:, None] * (end - start)


def attribute_to_color(attribute: npt.NDArray[Any], *, normalize_per_channel: bool = False) -> Color:
"""Convert an attribute to color values.
If necessary, remap to [0, 255] range as uint8.
:param attribute: The attribute to turn into color values.
:param normalize_per_channel: Whether to remap each channel separately or across all channels.
Only if the attribute is not already uint8.
:return: The attribute as uint8 color values.
"""
attribute = np.nan_to_num(attribute)
if attribute.ndim == 0:
msg = "Attribute has 0 dimensions. Must at least be a scalar (1 dimension)."
raise ValueError(msg)
if attribute.size > 1: # Do not squeeze a scalar, as it will get 0 dimensions.
attribute = np.squeeze(attribute)
# If the attribute has 2 values, like UV coordinates, add a third value to make it RGB.
dims_2d = 2
if attribute.ndim == dims_2d and attribute.shape[1] == dims_2d:
attribute = np.pad(attribute, ((0, 0), (0, 1)), mode="constant", constant_values=0)
# Now check if we're actually dealing with colors.
try:
_ = get_color_mode(attribute)
except ValueError:
raise
# Normalize the attribute.
if attribute.dtype != np.uint8:
axis = 0 if normalize_per_channel else None
# If the attribute range is below 1, keep 1 as the maximum. Also avoids division by 0.
attribute_range = np.maximum(np.absolute(attribute).max(axis=axis, keepdims=True), 1)
colors = attribute / attribute_range
# If the minimum is less than 0, shift the array into the [0, 1] range.
column_mask = colors.min(axis=axis) < 0
if column_mask.any():
if axis is None:
colors = (colors + 1) * 0.5
elif axis == 0:
colors[:, column_mask] = (colors[:, column_mask] + 1) * 0.5
colors = skimage.util.img_as_ubyte(colors)
else:
colors = attribute

return colors
Loading

0 comments on commit 4a6fe8e

Please sign in to comment.