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

Feature/attribute_to_image #15

Merged
merged 16 commits into from
May 8, 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
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