diff --git a/requirements.txt b/requirements.txt index cb1a66eaf..cdc4294fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ termcolor >= 1.1.0 trimesh >= 2.37.22 # Required by trimesh. networkx + diff --git a/tensorflow_graphics/projects/point_convolutions/.flake8 b/tensorflow_graphics/projects/point_convolutions/.flake8 new file mode 100644 index 000000000..f4e3292ee --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/.flake8 @@ -0,0 +1,32 @@ +[flake8] +inline-quotes = double +max-line-length = 79 +max-complexity = 10 +exclude = .git, + .tox, + .pytest_cache, + __pycache__, + tensorflow_graphics/projects/* + tensorflow_graphics/submodules/* +ignore = C901, + E101, + E111, + E114, + E121, + E125, + E126, + E129, + E221, + E265, + E271, + E305, + E306, + #E501, + E502, + E731, + E741, + F401, + F812, + W191, + W503, + W504, \ No newline at end of file diff --git a/tensorflow_graphics/projects/point_convolutions/LICENSE b/tensorflow_graphics/projects/point_convolutions/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tensorflow_graphics/projects/point_convolutions/README.md b/tensorflow_graphics/projects/point_convolutions/README.md new file mode 100644 index 000000000..83db3fe66 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/README.md @@ -0,0 +1,70 @@ +# TensorFlow Graphics Point Cloud Convolutions + +This module contains a python TensorFlow module `pylib` and a custom ops package in `tfg_custom_ops`. +While it is possible to run without the custom ops package, it is strongly advised to install it for performance and memory efficiency. + +## Content + +This code contains all necessary operations to perform point cloud convolutions + +1. Datastructure + - Point cloud class for batches of arbitrary sized point clouds. + - Memory efficient regular grid data structure +2. Point cloud operations + - Neighborhood computation + - Point density estimation + - Spatial sampling + - Poisson Disk sampling + - Cell average sampling +3. Convolution kernels + - Kernel Point Convolutions + - linear interpolation + - gaussian interpolation + - deformable points with regularization loss as in [KPConv](https://arxiv.org/abs/1904.08889) + - MLP + - multiple MLPs as in [MCConv](https://arxiv.org/abs/1806.01759) + - single MLP as in [PointConv](https://arxiv.org/abs/1811.07246) +4. Feature aggregation inside receptive fields + - Monte-Carlo integration with pdf + - Constant summation +5. Easy to use classes for building models + - `PointCloud` class + - `PointHierarchy` for sequential downsampling of point clouds + - layer classes + - `MCConv` + - `PointConv` + - `KPConv` + - `Conv1x1` + +## Installation + +Precompiled versions of the custom ops package are provided in `custom_ops/pkg_builds/tf_*` for the latest TensorFlow versions. +For compilation instructions see the [README](custom_ops/README.md) in the `custom_ops` folder. + +To install it run the following command (replace `VERSION` with your installed TensorFlow version, e.g. `2.3.0`) +```bash + pip install custom_ops/tf_VERSION/*.whl +``` + +## Tutorials + +Check out the Colab notebooks for an introduction to the code + +- [Introduction](pylib/notebooks/Introduction.ipynb) +- [Classification on ModelNet40](pylib/notebooks/ModelNet40.ipynb) + +## Unit tests + +Unit tests can be evaluated using + +```bash + pip install -r pytest_requirements.txt + pytest pylib/ +``` + +These include tests of the custom ops if they are installed. + +## Additional Information + +You may use this software under the +[Apache 2.0 License](https://github.com/schellmi42/tensorflow_graphics_point_clouds/blob/master/LICENSE). \ No newline at end of file diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py new file mode 100755 index 000000000..73eba35d2 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific + +from pylib import pc +# from pylib import io diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py new file mode 100755 index 000000000..e08cf279a --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/PointCloud.py @@ -0,0 +1,281 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Class to represent a point cloud.""" + +import tensorflow as tf +from tensorflow_graphics.geometry.convolution.utils import \ + flatten_batch_to_2d, unflatten_2d_to_batch + +from pylib.pc.utils import check_valid_point_cloud_input + + +class _AABB: + """Axis aligned bounding box of a point cloud. + + Args: + Pointcloud: A 'PointCloud' instance from which to compute the + axis aligned bounding box. + + """ + + def __init__(self, point_cloud, name=None): + + self._batch_size = point_cloud._batch_size + self._batch_shape = point_cloud._batch_shape + self.point_cloud_ = point_cloud + + self._aabb_min = tf.math.unsorted_segment_min( + data=point_cloud._points, segment_ids=point_cloud._batch_ids, + num_segments=self._batch_size) - 1e-9 + self._aabb_max = tf.math.unsorted_segment_max( + data=point_cloud._points, segment_ids=point_cloud._batch_ids, + num_segments=self._batch_size) + 1e-9 + + def get_diameter(self, ord='euclidean', name=None): + """ Returns the diameter of the bounding box. + + Note: + In the following, A1 to An are optional batch dimensions. + + Args: + ord: Order of the norm. Supported values are `'euclidean'`, + `1`, `2`, `np.inf` and any positive real number yielding the + corresponding p-norm. Default is `'euclidean'`. (optional) + Return: + diam: A `float` 'Tensor' of shape `[A1, ..., An]`, diameters of the + bounding boxes + + """ + + diam = tf.linalg.norm(self._aabb_max - self._aabb_min, ord=ord, axis=-1) + if self._batch_shape is None: + return diam + else: + return tf.reshape(diam, self._batch_shape) + + +class PointCloud: + """ Class to represent point clouds. + + Note: + In the following, A1 to An are optional batch dimensions. + + Args: + points: A float `Tensor` either of shape `[N, D]` or of shape + `[A1, .., An, V, D]`, possibly padded as indicated by `sizes`. + Represents the point coordinates. + batch_ids: An `int` `Tensor` of shape `[N]` associated with the points. + Is required if `points` is of shape `[N, D]`. + sizes: An `int` `Tensor` of shape `[A1, ..., An]` indicating the + true input sizes in case of padding (`sizes=None` indicates no padding) + Note that `sizes[A1, ..., An] <= V` or `sum(sizes) == N`. + batch_size: An `int`, the size of the batch. + """ + + def __init__(self, + points, + batch_ids=None, + batch_size=None, + sizes=None, + name=None): + points = tf.convert_to_tensor(value=points, dtype=tf.float32) + if sizes is not None: + sizes = tf.convert_to_tensor(value=sizes, dtype=tf.int32) + if batch_ids is not None: + batch_ids = tf.convert_to_tensor(value=batch_ids, dtype=tf.int32) + if batch_size is not None: + self._batch_size = tf.convert_to_tensor(value=batch_size, dtype=tf.int32) + else: + self._batch_size = None + + check_valid_point_cloud_input(points, sizes, batch_ids) + + self._sizes = sizes + # compatibility batch size as CPU int for graph mode + self._batch_size_numpy = batch_size + self._batch_ids = batch_ids + self._dimension = tf.gather(tf.shape(points), tf.rank(points) - 1) + self._batch_shape = None + self._unflatten = None + self._aabb = None + + if points.shape.ndims > 2: + self._init_from_padded(points) + else: + self._init_from_segmented(points) + + if self._batch_size_numpy is None: + self._batch_size_numpy = self._batch_size + + #Sort the points based on the batch ids in incremental order. + self._sorted_indices_batch = tf.argsort(self._batch_ids) + + def _init_from_padded(self, points): + """converting padded `Tensor` of shape `[A1, ..., An, V, D]` into a 2D + `Tensor` of shape `[N,D]` with segmentation ids. + """ + self._batch_shape = tf.shape(points)[:-2] + if self._batch_size is None: + self._batch_size = tf.reduce_prod(self._batch_shape) + if self._sizes is None: + self._sizes = tf.constant( + value=tf.shape(points)[-2], shape=self._batch_shape) + self._get_segment_id = tf.reshape( + tf.range(0, self._batch_size), self._batch_shape) + self._points, self._unflatten = flatten_batch_to_2d(points, self._sizes) + self._batch_ids = tf.repeat( + tf.range(0, self._batch_size), + repeats=tf.reshape(self._sizes, [-1])) + + def _init_from_segmented(self, points): + """if input is already 2D `Tensor` with segmentation ids or given sizes. + """ + if self._batch_ids is None: + if self._batch_size is None: + self._batch_size = tf.reduce_prod(self._sizes.shape) + self._batch_ids = tf.repeat(tf.range(0, self._batch_size), self._sizes) + if self._batch_size is None: + self._batch_size = tf.reduce_max(self._batch_ids) + 1 + self._points = points + + def get_points(self, id=None, max_num_points=None, name=None): + """ Returns the points. + + Note: + In the following, A1 to An are optional batch dimensions. + + If called withoud specifying 'id' returns the points in padded format + `[A1, ..., An, V, D]` + + Args: + id: An `int`, index of point cloud in the batch, if `None` returns all + max_num_points: An `int`, specifies the 'V' dimension the method returns, + by default uses maximum of 'sizes'. `max_rows >= max(sizes)` + + Return: + A `float` `Tensor` + of shape `[Ni, D]`, if 'id' was given + or + of shape `[A1, ..., An, V, D]`, zero padded, if no `id` was given. + + """ + if id is not None: + if not isinstance(id, int): + slice = self._get_segment_id + for slice_id in id: + slice = slice[slice_id] + id = slice + if id > self._batch_size: + raise IndexError('batch index out of range') + return self._points[self._batch_ids == id] + else: + return self.get_unflatten(max_num_points=max_num_points)(self._points) + + def get_sizes(self, name=None): + """ Returns the sizes of the point clouds in the batch. + + Note: + In the following, A1 to An are optional batch dimensions. + Use this instead of accessing 'self._sizes', + if the class was constructed using segmented input the '_sizes' is + created in this method. + + Returns: + `Tensor` of shape `[A1, .., An]`. + + """ + if self._sizes is None: + _ids, _, self._sizes = tf.unique_with_counts( + self._batch_ids) + _ids_sorted = tf.argsort(_ids) + self._sizes = tf.gather(self._sizes, _ids_sorted) + if self._batch_shape is not None: + self._sizes = tf.reshape(self._sizes, self._batch_shape) + return self._sizes + + def get_unflatten(self, max_num_points=None, name=None): + """ Returns the method to unflatten the segmented points. + + Use this instead of accessing 'self._unflatten', + if the class was constructed using segmented input the '_unflatten' method + is created in this method. + + Note: + In the following, A1 to An are optional batch dimensions. + + Args: + max_num_points: An `int`, specifies the 'V' dimension the method returns, + by default uses maximum of 'sizes'. `max_rows >= max(sizes)` + Returns: + A method to unflatten the segmented points, which returns a `Tensor` of + shape `[A1,...,An,V,D]`, zero padded. + + Raises: + ValueError: When trying to unflatten unsorted points. + + """ + if self._unflatten is None: + self._unflatten = lambda data: unflatten_2d_to_batch( + data=tf.gather(data, self._sorted_indices_batch), + sizes=self.get_sizes(), + max_rows=max_num_points) + return self._unflatten + + def get_AABB(self) -> _AABB: + """ Returns the axis aligned bounding box of the point cloud. + + Use this instead of accessing `self._aabb`, as the bounding box + is initialized with tthe first call of his method. + + Returns: + A `AABB` instance + + """ + if self._aabb is None: + self._aabb = _AABB(point_cloud=self) + return self._aabb + + def set_batch_shape(self, batch_shape, name=None): + """ Function to change the batch shape + + Use this to set a batch shape instead of using 'self._batch_shape' to + also change dependent variables. + + Note: + In the following, A1 to An are optional batch dimensions. + + Args: + batch_shape: A 1D `int` `Tensor` `[A1,...,An]`. + + Raises: + ValueError: if shape does not sum up to batch size. + + """ + if batch_shape is not None: + batch_shape = tf.convert_to_tensor(value=batch_shape, dtype=tf.int32) + tf.assert_equal( + tf.reduce_prod(batch_shape), self._batch_size, + f'Incompatible batch size. Must be {self._batch_size} \ + but is {tf.reduce_prod(batch_shape)}') + # if tf.reduce_prod(batch_shape) != self._batch_size: + # raise ValueError( + # f'Incompatible batch size. Must be {self._batch_size} \ + # but is {tf.reduce_prod(batch_shape)}') + self._batch_shape = batch_shape + self._get_segment_id = tf.reshape( + tf.range(0, self._batch_size), self._batch_shape) + if self._sizes is not None: + self._sizes = tf.reshape(self._sizes, self._batch_shape) + else: + self._batch_shape = None diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py new file mode 100755 index 000000000..26a7badc3 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Point cloud module.""" + +from .PointCloud import _AABB as AABB +from .PointCloud import PointCloud +''' +from .Grid import Grid +from .Neighborhood import Neighborhood +from .Neighborhood import KDEMode +from .sampling import poisson_disk_sampling, cell_average_sampling +from .sampling import sample +from .PointHierarchy import PointHierarchy + +from pylib.pc import layers +from pylib.pc import custom_ops +''' \ No newline at end of file diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py new file mode 100644 index 000000000..26540aa8e --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py new file mode 100644 index 000000000..c7635badd --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/aabb_test.py @@ -0,0 +1,72 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Class to test bounding box""" + +import os +import sys +import numpy as np +import tensorflow as tf +from absl.testing import parameterized +from tensorflow_graphics.util import test_case + +from pylib.pc import PointCloud, AABB +from pylib.pc.tests import utils + + +class AABB_test(test_case.TestCase): + + @parameterized.parameters( + (1, 1000, 3), + (8, 1000, 2), + (32, 1000, 4) + ) + def test_aabb_min_max(self, batch_size, num_points, dimension): + points, batch_ids = utils._create_random_point_cloud_segmented( + batch_size, num_points, dimension) + aabb_max_numpy = np.empty([batch_size, dimension]) + aabb_min_numpy = np.empty([batch_size, dimension]) + for i in range(batch_size): + aabb_max_numpy[i] = np.amax(points[batch_ids == i], axis=0) + aabb_min_numpy[i] = np.amin(points[batch_ids == i], axis=0) + + aabb_tf = PointCloud(points, batch_ids=batch_ids, + batch_size=batch_size).get_AABB() + + self.assertAllClose(aabb_max_numpy, aabb_tf._aabb_max) + self.assertAllClose(aabb_min_numpy, aabb_tf._aabb_min) + + @parameterized.parameters( + ([1], 1000, 3), + ([4, 4], 1000, 2), + ([1, 2, 3], 100, 4) + ) + def test_aabb_diameter(self, batch_shape, max_num_points, dimension): + points, sizes = utils._create_random_point_cloud_padded( + max_num_points, batch_shape, dimension) + batch_size = np.prod(batch_shape) + diameter_numpy = np.empty(batch_size) + points_flat = np.reshape(points, [batch_size, max_num_points, dimension]) + sizes_flat = np.reshape(sizes, [batch_size]) + for i in range(batch_size): + curr_pts = points_flat[i][:sizes_flat[i]] + diag = np.amax(curr_pts, axis=0) - np.amin(curr_pts, axis=0) + diameter_numpy[i] = np.linalg.norm(diag) + diameter_numpy = np.reshape(diameter_numpy, batch_shape) + + aabb_tf = PointCloud(points, sizes=sizes).get_AABB() + diameter_tf = aabb_tf.get_diameter() + self.assertAllClose(diameter_numpy, diameter_tf) + +if __name__ == '__main__': + test_case.main() diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py new file mode 100644 index 000000000..e71d27ff3 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/point_cloud_test.py @@ -0,0 +1,109 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific +"""Class to test point clouds""" + +import os +import sys +import numpy as np +import tensorflow as tf +from absl.testing import parameterized +from tensorflow_graphics.util import test_case + +from pylib.pc import PointCloud +from pylib.pc.tests import utils + + +class PointCloudTest(test_case.TestCase): + + @parameterized.parameters( + ([32], 100, 3), + ([5, 2], 100, 2), + ([2, 3, 4], 100, 4) + ) + def test_flatten_unflatten_padded(self, batch_shape, num_points, dimension): + batch_size = np.prod(batch_shape) + points, sizes = utils._create_random_point_cloud_padded( + num_points, batch_shape, dimension=dimension) + point_cloud = PointCloud(points, sizes=sizes) + retrieved_points = point_cloud.get_points().numpy() + self.assertAllEqual(points.shape, retrieved_points.shape) + points = points.reshape([batch_size, num_points, dimension]) + retrieved_points = retrieved_points.reshape( + [batch_size, num_points, dimension]) + sizes = sizes.reshape([batch_size]) + for i in range(batch_size): + self.assertAllClose(points[i, :sizes[i]], retrieved_points[i, :sizes[i]]) + self.assertTrue(np.all(retrieved_points[i, sizes[i]:] == 0)) + + @parameterized.parameters( + (100, 32, [8, 4]), + (100, 16, [2, 2, 2, 2]) + ) + def test_construction_methods(self, max_num_points, batch_size, batch_shape): + points, sizes = utils._create_random_point_cloud_padded( + max_num_points, batch_shape) + num_points = np.sum(sizes) + + sizes_flat = sizes.reshape([batch_size]) + points_flat = points.reshape([batch_size, max_num_points, 3]) + batch_ids = np.repeat(np.arange(0, batch_size), sizes_flat) + + points_seg = np.empty([num_points, 3]) + cur_id = 0 + for pts, size in zip(points_flat, sizes_flat): + points_seg[cur_id:cur_id + size] = pts[:size] + cur_id += size + + pc_from_padded = PointCloud(points, sizes=sizes) + self.assertAllEqual(batch_ids, pc_from_padded._batch_ids) + self.assertAllClose(points_seg, pc_from_padded._points) + + pc_from_ids = PointCloud(points_seg, batch_ids) + pc_from_ids.set_batch_shape(batch_shape) + + pc_from_sizes = PointCloud(points_seg, sizes=sizes_flat) + pc_from_sizes.set_batch_shape(batch_shape) + self.assertAllEqual(batch_ids, pc_from_sizes._batch_ids) + + points_from_padded = pc_from_padded.get_points( + max_num_points=max_num_points) + points_from_ids = pc_from_ids.get_points( + max_num_points=max_num_points) + points_from_sizes = pc_from_sizes.get_points( + max_num_points=max_num_points) + + self.assertAllEqual(points_from_padded, points_from_ids) + self.assertAllEqual(points_from_ids, points_from_sizes) + self.assertAllEqual(points_from_sizes, points_from_padded) + + @parameterized.parameters( + (1000, + ['Invalid input! Point tensor is of dimension 1 \ + but should be at least 2!', + 'Missing input! Either sizes or batch_ids must be given.', + 'Invalid sizes! Sizes of points and batch_ids are not equal.']) + ) + def test_exceptions_raised_at_construction(self, num_points, msgs): + points = np.random.rand(num_points) + batch_ids = np.zeros(num_points) + with self.assertRaisesRegex(ValueError, msgs[0]): + _ = PointCloud(points, batch_ids) + points = np.random.rand(num_points, 3) + with self.assertRaisesRegexp(ValueError, msgs[1]): + _ = PointCloud(points) + with self.assertRaisesRegexp(AssertionError, msgs[2]): + _ = PointCloud(points, batch_ids[1:]) + + +if __name__ == '__main__': + test_case.main() diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py new file mode 100644 index 000000000..e7ec1e1e9 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/tests/utils.py @@ -0,0 +1,82 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""helper functions for unit tests""" + +import tensorflow as tf +import numpy as np + + +def _create_random_point_cloud_segmented(batch_size, + num_points, + dimension=3, + sizes=None, + scale=1, + clean_aabb=False, + equal_sized_batches=False): + points = np.random.uniform(0, scale, [num_points, dimension]) + if sizes is None: + if not equal_sized_batches: + batch_ids = np.random.randint(0, batch_size, num_points) + batch_ids[:batch_size] = np.arange(0, batch_size) + else: + batch_ids = np.repeat(np.arange(0, batch_size), num_points // batch_size) + # batch_ids = np.sort(batch_ids) + else: + sizes = np.array(sizes, dtype=int) + batch_ids = np.repeat(np.arange(0, batch_size), sizes) + if clean_aabb: + # adds points such that the aabb is [0,0,0] [1,1,1]*scale + # to prevent rounding errors + points = np.concatenate( + (points, scale * np.ones([batch_size, dimension]) - 1e-9, + 1e-9 + np.zeros([batch_size, dimension]))) + batch_ids = np.concatenate( + (batch_ids, np.arange(0, batch_size), np.arange(0, batch_size))) + return points, batch_ids + + +def _create_random_point_cloud_padded(max_num_points, + batch_shape, + dimension=3, + sizes=None, + scale=1): + batch_size = np.prod(batch_shape) + points = np.random.uniform( + 0, scale, [max_num_points * batch_size, dimension]) + points = points.reshape(batch_shape + [max_num_points, dimension]) + if sizes is None: + sizes = np.random.randint(1, max_num_points, batch_shape) + return points, sizes + +''' +def _create_uniform_distributed_point_cloud_2D(num_points_sqrt, + scale=1, + flat=False): + ticks = np.linspace(0, scale, num=num_points_sqrt) + points = np.array(np.meshgrid(ticks, ticks)).T + if flat: + points = points.reshape(-1, 2) + return points + + +def _create_uniform_distributed_point_cloud_3D(num_points_root, + bb_min=0, + bb_max=1, + flat=False): + ticks = np.linspace(bb_min, bb_max, num=num_points_root, endpoint=False) + points = np.array(np.meshgrid(ticks, ticks, ticks)).T + if flat: + points = points.reshape(-1, 3) + return points +''' \ No newline at end of file diff --git a/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py b/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py new file mode 100644 index 000000000..972222310 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pylib/pc/utils.py @@ -0,0 +1,115 @@ +# Copyright 2020 The TensorFlow Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +''' helper functions for point clouds ''' + +import tensorflow as tf +from tensorflow_graphics.geometry.convolution.utils import flatten_batch_to_2d +from pylib.pc import PointCloud + + +def check_valid_point_cloud_input(points, sizes, batch_ids): + """Checks that the inputs to the constructor of class 'PointCloud' are valid. + + Args: + points: A `float` `Tensor` of shape `[N, D]` or `[A1, ..., An, V, D]`. + sizes: An `int` `Tensor` of shape `[A1, ..., An]` or `None`. + batch_ids: An `int` `Tensor` of shape `[N]` or `None`. + + Raises: + Value Error: If input dimensions are invalid or no valid segmentation + is given. + + """ + + if points.shape.ndims == 2 and sizes is None and batch_ids is None: + raise ValueError('Missing input! Either sizes or batch_ids must be given.') + if points.shape.ndims == 1: + raise ValueError( + 'Invalid input! Point tensor is of dimension 1 \ + but should be at least 2!') + if points.shape.ndims == 2 and batch_ids is not None: + if points.shape[0] != batch_ids.shape[0]: + raise AssertionError('Invalid sizes! Sizes of points and batch_ids are' + + ' not equal.') + +''' +def check_valid_point_hierarchy_input(point_cloud, cell_sizes, pool_mode): + """ Checks that inputs to the constructor of class 'PontHierarchy' are valid. + + Args: + point_cloud: A 'PointCloud' instance. + cell_sizes: A `list` of `float` `Tensors`. + pool_mode: An `int`. + + Raises: + TypeError: if input is of invalid type + ValueError: if pool_mode is invalid, or cell_sizes dimension are invalid + or non-positive + + """ + if not isinstance(point_cloud, (PointCloud)): + raise TypeError('Input must be instance of class PointCloud') + if pool_mode not in [0, 1]: + raise ValueError('Unknown pooling mode.') + for curr_cell_sizes in cell_sizes: + if any(curr_cell_sizes <= 0): + raise ValueError('cell size must be positive.') + if not curr_cell_sizes.shape[0] in [1, point_cloud.dimension_]: + raise ValueError( + 'Invalid number of cell sizes for point cloud' +\ + f'dimension. Must be 1 or {point_cloud.dimension_} but is' +\ + f'{curr_cell_sizes.shape[0]}.') + + +def _flatten_features(features, point_cloud: PointCloud): + """ Converts features of shape `[A1, ..., An, C]` to shape `[N, C]`. + + Args: + features: A `Tensor`. + point_cloud: A `PointCloud` instance. + + Returns: + A `Tensor` of shape `[N, C]`. + + """ + if features.shape.ndims > 2: + sizes = point_cloud.get_sizes() + features, _ = flatten_batch_to_2d(features, sizes) + sorting = tf.math.invert_permutation(point_cloud._sorted_indices_batch) + features = tf.gather(features, sorting) + else: + tf.assert_equal(tf.shape(features)[0], tf.shape(point_cloud._points)[0]) + tf.assert_equal(tf.rank(features), 2) + return features + + +def cast_to_num_dims(values, num_dims, dtype=tf.float32): + """ Converts an input to the specified `dtype` and repeats it `num_dims` + times. + + Args: + values: Must be convertible to a `Tensor` of shape `[], [1]` or + `[num_dims]`. + dtype: A `tf.dtype`. + + Returns: + A `dtype` `Tensor` of shape `[num_dims]`. + + """ + values = tf.cast(tf.convert_to_tensor(value=values), + dtype=dtype) + if values.shape == [] or values.shape[0] == 1: + values = tf.repeat(values, num_dims) + return values +''' diff --git a/tensorflow_graphics/projects/point_convolutions/pytest.ini b/tensorflow_graphics/projects/point_convolutions/pytest.ini new file mode 100644 index 000000000..82972425f --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +norecursedirs = tensorflow_graphics/rendering/opengl/tests tensorflow_graphics/submodules +python_files = *_test.py diff --git a/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt b/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt new file mode 100644 index 000000000..418e8a444 --- /dev/null +++ b/tensorflow_graphics/projects/point_convolutions/pytest_requirements.txt @@ -0,0 +1,3 @@ +pytest +sklearn +trimesh \ No newline at end of file