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

More robust I/O tests, sample meshes, bug fixes and changes to USD importers #718

Merged
merged 1 commit into from
May 5, 2023
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: 1 addition & 1 deletion docs/notes/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ For an exhaustive check, install testing dependencies and run tests as follows:
$ pip install -r tools/ci_requirements.txt
$ export CI='true' # on Linux
$ set CI='true' # on Windows
$ pytest tests/python/
$ pytest --import-mode=importlib -s tests/python/

.. Note::
These tests rely on CUDA operations and will fail if you installed on CPU only, where not all functionality is available.
218 changes: 179 additions & 39 deletions kaolin/io/usd/mesh.py

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions kaolin/io/usd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ def get_scene_paths(file_path_or_stage, scene_path_regex=None, prim_types=None,
Path to usd file (\*.usd, \*.usda) or :class:`Usd.Stage`.
scene_path_regex (str, optional):
Optional regular expression used to select returned scene paths.
prim_types (list of str, optional):
Optional list of valid USD Prim types used to select scene paths.
prim_types (list of str, str, optional):
Optional list of valid USD Prim types used to select scene paths, or a single USD Prim type string.
conditional (function path: Bool): Custom conditionals to check

Returns:
Expand All @@ -78,6 +78,8 @@ def get_scene_paths(file_path_or_stage, scene_path_regex=None, prim_types=None,
if scene_path_regex is None:
scene_path_regex = '.*'
if prim_types is not None:
if isinstance(prim_types, str):
prim_types = [prim_types]
prim_types = [pt.lower() for pt in prim_types]

scene_paths = []
Expand Down
100 changes: 85 additions & 15 deletions kaolin/utils/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import copy
import collections
import functools
import logging
import numpy as np
import torch

Expand Down Expand Up @@ -68,6 +70,10 @@ def check_tensor(tensor, shape=None, dtype=None, device=None, throw=True):
if a dimension is set at ``None`` then it's not verified.
dtype (torch.dtype, optional): the expected dtype.
device (torch.device, optional): the expected device.
throw (bool): if true (default), will throw if checks fail

Return:
(bool) True if checks pass
"""
if shape is not None:
if len(shape) != tensor.ndim:
Expand Down Expand Up @@ -117,7 +123,7 @@ def check_packed_tensor(tensor, total_numel=None, last_dim=None, dtype=None, dev
return False
return True

def check_padded_tensor(tensor, padding_value=None, shape_per_tensor=None,
def check_padded_tensor(tensor, padding_value=None, shape_per_tensor=None,
batch_size=None, max_shape=None, last_dim=None,
dtype=None, device=None, throw=True):
"""Check if :ref:`padded tensor<padded>` is valid given set of criteria.
Expand All @@ -135,7 +141,7 @@ def check_padded_tensor(tensor, padding_value=None, shape_per_tensor=None,

Return:
(bool): status of the check.
"""
"""
if not check_tensor(tensor, dtype=dtype, device=device, throw=throw):
return False
if shape_per_tensor is not None:
Expand Down Expand Up @@ -274,48 +280,112 @@ def _get_details_str():
(_get_stats_str() if print_stats else ''),
(_get_details_str() if detailed else '')))

def contained_torch_equal(elem, other):
"""Check for equality of two objects potentially containing tensors.
def contained_torch_equal(elem, other, approximate=False, **allclose_args):
"""Check for equality (or allclose if approximate) of two objects potentially containing tensors.

:func:`torch.equal` do not support data structure like dictionary / arrays
and `==` is ambiguous on :class:`torch.Tensor`.
This class will try to apply recursion through :class:`collections.abc.Mapping`,
:class:`collections.abc.Sequence`, :func:`torch.equal` if the objects are `torch.Tensor`,
of else `==` operator.

Args:
elem (object): The first object
other (object): The other object to compare to ``elem``
elem (object, dict, list, tuple): The first object
other (object, dict, list, tuple): The other object to compare to ``elem``
approximate (bool): if requested will use allclose for comparison instead (default=False)
allclose_args: arguments to `torch.allclose` if approximate comparison requested

Return (bool): the comparison result
"""
elem_type = type(elem)
if elem_type != type(other):
return False

def _tensor_compare(a, b):
if not approximate:
return torch.equal(a, b)
else:
return torch.allclose(a, b, **allclose_args)

def _number_compare(a, b):
return _tensor_compare(torch.tensor([a]), torch.tensor([b]))

recursive_args = copy.copy(allclose_args)
recursive_args['approximate'] = approximate

if isinstance(elem, torch.Tensor):
return torch.equal(elem, other)
return _tensor_compare(elem, other)
elif isinstance(elem, str):
return elem == other
elif isinstance(elem, float):
return _number_compare(elem, other)
elif isinstance(elem, collections.abc.Mapping):
if elem.keys() != other.keys():
return False
return all(contained_torch_equal(elem[key], other[key]) for key in elem)
return all(contained_torch_equal(elem[key], other[key], **recursive_args) for key in elem)
elif isinstance(elem, tuple) and hasattr(elem, '_fields'): # namedtuple
if set(elem._fields()) != set(other._fields()):
if set(elem._fields) != set(other._fields):
return False
return all(contained_torch_equal(
getattr(elem, f), getattr(other, f)) for f in elem._fields()
getattr(elem, f), getattr(other, f), **recursive_args) for f in elem._fields
)
elif isinstance(elem, collections.abc.Sequence):
if len(elem) != len(other):
return False
return all(contained_torch_equal(a, b) for a, b in zip(elem, other))
return all(contained_torch_equal(a, b, **recursive_args) for a, b in zip(elem, other))
else:
return elem == other

def check_allclose(tensor, other, rtol=1e-5, atol=1e-8, equal_nan=False):
if not torch.allclose(tensor, other, atol, rtol, equal_nan):
diff_idx = torch.where(~torch.isclose(tensor, other, atol, rtol, equal_nan))
if not torch.allclose(tensor, other, atol=atol, rtol=rtol, equal_nan=equal_nan):
diff_idx = torch.where(~torch.isclose(tensor, other, atol=atol, rtol=rtol, equal_nan=equal_nan))
raise ValueError(f"Tensors are not close on indices {diff_idx}:",
f"Example values: {tensor[diff_idx][:10]} vs {other[diff_idx][:10]}.")

def check_tensor_attribute_shapes(container, throw=True, **attribute_info):
"""Checks shape on all specified attributes of the container.

Args:
container (dict, tuple, object): container with named attributes to be tested
throw (bool): if true (default), will throw error on first check that fails
attribute_info: named attribute=shape values, where shape can be list or tuple (see `check_tensor`)

Return:
(bool) True if checks pass
"""
def _get_item(container, attr):
if isinstance(container, collections.abc.Mapping):
return container[attr]
else:
return getattr(container, attr)

success = True
for k, shape in attribute_info.items():
val = _get_item(container, k)
if not check_tensor(val, shape=shape, throw=False):
success = False
message = f'Attribute {k} has shape {val.shape} (expected {shape})'
if throw:
raise ValueError(message)
else:
logging.error(message)
return success


def print_namedtuple_attributes(ntuple, name='', **tensor_info_args):
print_dict_attributes(ntuple._asdict(), name=name, **tensor_info_args)


def print_dict_attributes(in_dict, name='', **tensor_info_args):
name_info = '' if len(name) == 0 else f' of {name}'
print(f'\nAttributes{name_info}:')
for k, v in in_dict.items():
if torch.is_tensor(v):
tinfo = tensor_info(v, **tensor_info_args)
elif isinstance(v, (str, int, float)):
tinfo = v
elif isinstance(v, collections.abc.Sequence):
tinfo = f'{type(v)} of length {len(v)}'
else:
tinfo = type(v)
print(f' {k}: {tinfo}')
89 changes: 89 additions & 0 deletions sample_data/meshes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Sample Meshes

This directory contains sample meshes that might be useful across the project, in both
tests and examples. These are simple test cases for stress-testing and prototyping
and are not meant to be high-quality 3D examples.

## Flat-shaded Ico Sphere

**Filenames**: [ico_flat.obj](ico_flat.obj), [ico_flat.usda](ico_flat.usda).

**Source**: this mesh is an ico sphere that was authored in Blender with flat shading.

**Formats**: was exported as both `obj` and `usda` (ascii) from Blender.

**Sanity checks**: displays correctly as `obj` and `usda` imported into Blender3.1 and as `usda` imported into [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/).

<img width="30%" style="padding:0 20px 0 0; float: left" src="renders/ico_flat.jpg">

**Attributes** of the single mesh in this file:
* Vertices: 42
* Faces: 80 triangles
* Materials: one simple material applied to all faces
* Normals: for flat shading (same normal for all vertices of a face)
* UVs

<div style="clear:both"></div>

## Smooth-shaded Ico Sphere

**Filenames**: [ico_smooth.obj](ico_smooth.obj), [ico_smooth.usda](ico_smooth.usda).

**Source**: this mesh is an ico sphere that was authored in Blender with smooth shading.

**Formats**: was exported as both `obj` and `usda` (ascii) from Blender.

**Sanity checks**: displays correctly as `obj` and `usda` imported into Blender3.1 and as `usda` imported into [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/).

<img width="30%" style="padding:0 20px 0 0; float: left" src="renders/ico_smooth.jpg">

**Attributes** of the single mesh in this file:
* Vertices: 42
* Faces: 80 triangles
* Materials: one simple material applied to all faces
* Normals: for smooth shading
* UVs

<div style="clear:both"></div>

## Textured Fox

**Filenames:** [fox.obj](fox.obj), [fox.usdc](fox.usdc).

**Source:** this file is a simiplified version of TurboSquid ID 1073771, which NVIDIA has licensed with distribution rights. *By using this TurboSquid model, you agree that you will only use the content for research purposes only. You may not redistribute this model.*

**Formats**: the original format is `obj`, which was converted to `usdc` (binary) using Blender exporter.

**Sanity checks**: displays correctly as `obj` and `usdc` imported into Blender3.1 and as `usdc` imported into [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/).

<img width="30%" style="padding:0 20px 0 0; float: left" src="renders/fox.jpg">

**Attributes** of the single mesh in this file:
* Vertices: 5002
* Faces: 10000 triangles
* Materials: one simple material with texture applied to all faces
* Normals: for smooth shading
* UVs.

<div style="clear:both"></div>

## Multi-Material Pizza

**Filenames:** [pizza.obj](pizza.obj), [pizza.usda](pizza.usda).

**Source:** this file was authored in Blender with pizza texture taken from a [royalty-free photo by Rene Strgar](https://www.pexels.com/photo/italian-style-pizza-13814644/).

**Formats**: was exported as both `obj` and `usda` (ascii) from Blender.

**Sanity checks**: displays correctly as `obj` (not usd) imported into Blender3.1 and as `usda` imported into [NVIDIA Omniverse](https://www.nvidia.com/en-us/omniverse/).

<img width="30%" style="padding:0 20px 0 0; float: left" src="renders/pizza.jpg">

**Attributes** of the single mesh in this file:
* Vertices: 482
* Faces: 960 triangles
* Materials: one simple material and one texture material applied to groups of faces
* Normals: for smooth shading
* UVs

<div style="clear:both"></div>
13 changes: 13 additions & 0 deletions sample_data/meshes/fox.mtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Blender MTL File: 'None'
# Material Count: 1

newmtl material_0
Ns 0.000000
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 1.000000 1.000000 1.000000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd textures/fox.jpg
Loading