Repository: mikedh/trimesh
Pull request #266 (2018-12-02 04:38:48)

Support MTL and UV unwarping to vertex_colors for Wavefront
<img width="642" alt="screen shot 2018-12-02 at 4 36 59" src="https://user-images.githubusercontent.com/4310419/49335769-0b91ed80-f5ec-11e8-8248-2b6e9b695ef8.png">

```python
#!/usr/bin/env python

import pprint

import trimesh


mesh = trimesh.load('./fuze.obj')

print('mesh.metadata:')
pprint.pprint(mesh.metadata)

print('mesh.visual.vertex_colors:')
print(mesh.visual.vertex_colors)

mesh.show()
```

## Output of above script

```bash
% python load_fuze.py
mesh.metadata:
{'file_name': 'fuze.obj',
 'file_path': '/Users/wkentaro/Documents/xxx/python/src/trimesh/models/fuze.obj',
 'mtllibs': [{'filename': '/Users/wkentaro/Documents/xxx/python/src/trimesh/models/./fuze.o
bj.mtl',
              'map_Kd': 'fuze_uv.jpg',
              'newmtl': 'material_0'}],
 'processed': True,
 'vertex_texture': array([[0.629012, 0.417026],
       [0.629012, 0.442966],
       [0.661437, 0.404056],
       ...,
       [0.565363, 0.991709],
       [0.148287, 0.971874],
       [0.520901, 0.780556]])}
mesh.visual.vertex_colors:
[[153   5  41 255]
 [127 127 127 255]
 [103  29  46 255]
 ...
 [161  11  56 255]
 [158   3   9 255]
 [163 111   2 255]]
```

wkentaro: This is the result of a mesh with higher resolution.

<img width="638" alt="screen shot 2018-12-02 at 4 42 11" src="https://user-images.githubusercontent.com/4310419/49335798-ba362e00-f5ec-11e8-8d1e-5059aa5d4acb.png">

mikedh: Awesome! Texture is a bear, it would be great to incrementally start getting stuff into trimesh. I just moved around some files and refactored slightly with some of the ideas from #218 into the [`texture`](https://github.com/mikedh/trimesh/tree/texture) branch to collect texture changes. The layout is currently:

- visuals/resolvers.py
  - includes tools to resolve and read assets (like MTL files, texture images, etc). It's not always easy to figure out where the assets are relative to the file you're loading, and in the case of ZIP archives, cloud files, or http in load_remote, you need to handle it differently. 
- visuals/colors.py
  - has ColorVisuals object (moved and renamed trimesh/visual.py)
- visuals/texture.py
  - has TextureVisuals object (not implemented) and utility functions (like `uv_to_color`)

What do you think? @mmatl  @PyryM  @EpicWink any 'puts'?

I think your `_get_vertex_colors` is generally useful and doesn't have to be OBJ- specific. Would you mind moving it into `/visuals/texture.py` in the `texture` branch? I'd probably change the API for it to:

```
def uv_to_color(uv, image):
    """
    Get the color in a texture image.
	
    Parameters
    -------------
    uv : (n, 2) float
      UV coordinates on texture image
    image : PIL.Image
      Texture image

    Returns
    ----------
    colors : (n, 4) float
      RGBA color at each of the UV coordinates
    """
```

Then the question of "where the heck are the assets" is answered elsewhere. Thanks for the PR!
wkentaro: It sounds good. So what should I need to do exactly?
I just need to move `uv_to_color` to `visuals/texture.py`?
mikedh: Yeah that would be great then I'll merge it into texture. I was thinking of keeping `texture` out of master for a month or so until the MVP feature list in #218 is done 

Thanks!
Mike

Patch:
diff --git a/trimesh/io/wavefront.py b/trimesh/io/wavefront.py
index ed4c6641..7e563f0e 100644
--- a/trimesh/io/wavefront.py
+++ b/trimesh/io/wavefront.py
@@ -1,7 +1,51 @@
+import os.path
+
 import numpy as np
 
+try:
+    import PIL.Image as PIL_Image
+except ImportError:
+    PIL_Image = None
+
 from ..constants import log
 from .. import util
+from .. import visual
+
+
+def _get_vertex_colors(uv, mtllibs):
+    if PIL_Image is None:
+        log.warning('Pillow is required to load texture')
+        return
+
+    vertex_colors = np.zeros((len(uv), 4), dtype=np.uint8)
+    n_success = 0
+    for mtllib in mtllibs:
+        texture_file = os.path.join(
+            os.path.dirname(mtllib['filename']), mtllib['map_Kd']
+        )
+        try:
+            texture = PIL_Image.open(texture_file)
+        except IOError:
+            continue
+        vertex_colors += visual.uv_to_color(uv, texture)
+        n_success += 1
+    if n_success == 0:
+        return  # all fails
+    return vertex_colors
+
+
+def _parse_mtl(filename):
+    data = {'filename': filename,
+            'newmtl': None,
+            'map_Kd': None}
+    with open(filename) as f:
+        for line in f:
+            line_split = line.strip().split()
+            if not line_split:
+                continue
+            if line_split[0] in data:
+                data[line_split[0]] = line_split[1]
+    return data
 
 
 def load_wavefront(file_obj, **kwargs):
@@ -105,11 +149,22 @@ def load_wavefront(file_obj, **kwargs):
                     face_groups[start_f:] = idx
                 loaded['metadata']['face_groups'] = face_groups
 
+            if len(current['usemtl']) > 0:
+                current_mtllibs = [mtllibs[k] for k in current['usemtl']
+                                   if k in mtllibs]
+                vertex_colors = _get_vertex_colors(
+                    uv=loaded['metadata']['vertex_texture'],
+                    mtllibs=current_mtllibs,
+                )
+                loaded['vertex_colors'] = vertex_colors
+                loaded['metadata']['mtllibs'] = current_mtllibs
+
             # we're done, append the loaded mesh kwarg dict
             meshes.append(loaded)
 
     attribs = {k: [] for k in ['v', 'vt', 'vn']}
-    current = {k: [] for k in ['v', 'vt', 'vn', 'f', 'g']}
+    current = {k: [] for k in ['v', 'vt', 'vn', 'f', 'g', 'usemtl']}
+    mtllibs = {}
     # remap vertex indexes {str key: int index}
     remap = {}
     next_idx = 0
@@ -162,6 +217,14 @@ def load_wavefront(file_obj, **kwargs):
             # defining a new group
             group_idx += 1
             current['g'].append((group_idx, len(current['f']) // 3))
+        elif line_split[0] == 'mtllib':
+            mtl_file = os.path.join(os.path.dirname(file_obj.name),
+                                    line_split[1])
+            mtllib = _parse_mtl(mtl_file)
+            if mtllib.get('newmtl'):
+                mtllibs[mtllib['newmtl']] = mtllib
+        elif line_split[0] == 'usemtl':
+            current['usemtl'].append(line_split[1])
 
     if next_idx > 0:
         append_mesh()
diff --git a/trimesh/visual/__init__.py b/trimesh/visual/__init__.py
index 9c34d3b0..8cb122ba 100644
--- a/trimesh/visual/__init__.py
+++ b/trimesh/visual/__init__.py
@@ -13,6 +13,8 @@ from .color import (ColorVisuals,
                     DEFAULT_COLOR,
                     linear_color_map)
 
+from .texture import uv_to_color
+
 # explicitly list imports in __all__
 # as otherwise flake8 gets mad
 __all__ = [ColorVisuals,
@@ -21,4 +23,5 @@ __all__ = [ColorVisuals,
            create_visual,
            DEFAULT_COLOR,
            interpolate,
-           linear_color_map]
+           linear_color_map,
+           uv_to_color]
diff --git a/trimesh/visual/texture.py b/trimesh/visual/texture.py
new file mode 100644
index 00000000..20b8212d
--- /dev/null
+++ b/trimesh/visual/texture.py
@@ -0,0 +1,28 @@
+import numpy as np
+
+from .color import to_rgba
+
+
+def uv_to_color(uv, image):
+    """
+    Get the color in a texture image.
+
+    Parameters
+    -------------
+    uv : (n, 2) float
+      UV coordinates on texture image
+    image : PIL.Image
+      Texture image
+
+    Returns
+    ----------
+    colors : (n, 4) float
+      RGBA color at each of the UV coordinates
+    """
+    x = (uv[:, 0] * image.width).round().astype(int)
+    y = ((1 - uv[:, 1]) * image.height).round().astype(int)
+
+    image = np.asarray(image)
+    colors = image[y, x]
+    colors = to_rgba(colors)
+    return colors


Test patch:
diff --git a/tests/test_visual.py b/tests/test_visual.py
index 2975ec34..52d5de70 100644
--- a/tests/test_visual.py
+++ b/tests/test_visual.py
@@ -205,6 +205,19 @@ class VisualTest(g.unittest.TestCase):
         # every color should differ
         assert (colors[:-1] != colors[1:]).any(axis=1).all()
 
+    def test_uv_to_color(self):
+        try:
+            import PIL.Image
+        except ImportError:
+            return
+
+        n_vertices = 100
+        uv = g.np.array([[0.25, 0.2], [0.4, 0.5]], dtype=float)
+        texture = g.np.arange(96, dtype=g.np.uint8).reshape(8, 4, 3)
+        colors = g.trimesh.visual.uv_to_color(uv, PIL.Image.fromarray(texture))
+        colors_expected = [[75, 76, 77, 255], [54, 55, 56, 255]]
+
+        g.np.testing.assert_allclose(colors, colors_expected, rtol=0, atol=0)
 
 if __name__ == '__main__':
     g.trimesh.util.attach_to_log()