Skip to content
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
53 changes: 53 additions & 0 deletions docarray/document/mixins/mesh.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from typing import TYPE_CHECKING

import numpy as np
Expand Down Expand Up @@ -40,3 +41,55 @@ def load_uri_to_point_cloud_tensor(
self.tensor = np.array(mesh.sample(samples))

return self

def load_uri_to_vertices_and_faces(self: 'T') -> 'T':
"""Convert a 3d mesh-like :attr:`.uri` into :attr:`.chunks` as vertices and faces

:return: itself after processed
"""

import trimesh
import urllib.parse
from docarray.document import Document

scheme = urllib.parse.urlparse(self.uri).scheme
loader = trimesh.load_remote if scheme in ['http', 'https'] else trimesh.load

mesh = loader(self.uri, force='mesh')

vertices = mesh.vertices.view(np.ndarray)
faces = mesh.faces.view(np.ndarray)

self.chunks = [
Document(name='vertices', tensor=vertices),
Document(name='faces', tensor=faces),
]
Comment thread
alaeddine-13 marked this conversation as resolved.

return self

def load_vertices_and_faces_to_point_cloud(self: 'T', samples: int) -> 'T':
"""Convert a 3d mesh of vertices and faces from :attr:`.chunks` into point cloud :attr:`.tensor`

:param samples: number of points to sample from the mesh
:return: itself after processed
"""
import trimesh

vertices = None
faces = None

for chunk in self.chunks:
if chunk.tags['name'] == 'vertices':
vertices = chunk.tensor
if chunk.tags['name'] == 'faces':
faces = chunk.tensor

if vertices is not None and faces is not None:
mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
self.tensor = np.array(mesh.sample(samples))
else:
raise AttributeError(
'Point cloud tensor can not be set, since vertices and faces chunk tensor have not been set.'
)

return self
48 changes: 42 additions & 6 deletions docs/datatypes/mesh/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,63 @@ This feature requires `trimesh`. You can install it via `pip install "docarray[f

A 3D mesh is the structural build of a 3D model consisting of polygons. Most 3D meshes are created via professional software packages, such as commercial suites like Unity, or the free open source Blender 3D.

## Point cloud
## Vertices and faces representation

Point cloud is a representation of a 3D mesh. It is made by repeated and uniformly sampling points within the 3D body. Comparing to the mesh representation, point cloud is a fixed size ndarray and hence easier for deep learning algorithms to handle. In DocArray, you can simply load a 3D mesh and convert it into a point cloud via:
A 3D mesh can be represented by its vertices and faces. Vertices are points in a 3D space, represented as a tensor of shape (n_points, 3). Faces are triangular surfaces that can be defined by three points in 3D space, corresponding to the three vertices of a triangle. Faces can be represented as a tensor of shape (n_faces, 3). Each number in that tensor refers to an index of a vertex in the tensor of vertices.

In DocArray, you can load a mesh and save its vertices and faces to a Document's `.chunks` as follows:

```python
from docarray import Document
doc = Document(uri='viking.glb').load_uri_to_point_cloud_tensor(1000)

print(doc.tensor.shape)
doc = Document(uri='viking.glb').load_uri_to_vertices_and_faces()

doc.summary()
```

```text
(1000, 3)
<Document ('id', 'chunks') at 7f907d786d6c11ec840a1e008a366d49>
└─ chunks
├─ <Document ('id', 'parent_id', 'granularity', 'tensor', 'tags') at 7f907ab26d6c11ec840a1e008a366d49>
└─ <Document ('id', 'parent_id', 'granularity', 'tensor', 'tags') at 7f907c106d6c11ec840a1e008a366d49>
```

The following pictures depict a 3D mesh and a point cloud with 1000 samples from that 3D mesh.
This stores the vertices and faces in `.tensor` of two separate sub-Documents in a Document's `.chunks`. Each sub-Document has a name assigned to it ('vertices' or 'faces'), which is saved in `.tags`:

```python
for chunk in doc.chunks:
print(f'chunk.tags = {chunk.tags}')
```

```text
chunk.tags = {'name': 'vertices'}
chunk.tags = {'name': 'faces'}
```

The following picture depicts a 3D mesh:

```{figure} 3dmesh-man.gif
:width: 50%
```

## Point cloud representation

A point cloud is a representation of a 3D mesh. It is made by repeatedly and uniformly sampling points within the 3D body. Compared to the mesh representation, the point cloud is a fixed size ndarray and hence easier for deep learning algorithms to handle. In DocArray, you can simply load a 3D mesh and convert it into a point cloud of size `samples` via:

```python
from docarray import Document

doc = Document(uri='viking.glb').load_uri_to_point_cloud_tensor(samples=1000)

print(doc.tensor.shape)
```

```text
(1000, 3)
```

The following picture depicts a point cloud with 1000 samples from the previously depicted 3D mesh.

```{figure} pointcloud-man.gif
:width: 50%
```
32 changes: 32 additions & 0 deletions tests/unit/document/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,35 @@ def test_glb_converters(uri, chunk_num):
doc.load_uri_to_point_cloud_tensor(2000, as_chunks=True)
assert len(doc.chunks) == chunk_num
assert doc.chunks[0].tensor.shape == (2000, 3)


@pytest.mark.parametrize('uri', [(os.path.join(cur_dir, 'toydata/test.glb'))])
def test_load_uri_to_vertices_and_faces(uri):
doc = Document(uri=uri)
doc.load_uri_to_vertices_and_faces()

assert len(doc.chunks) == 2
assert doc.chunks[0].tags['name'] == 'vertices'
assert doc.chunks[0].tensor.shape[1] == 3
assert doc.chunks[1].tags['name'] == 'faces'
assert doc.chunks[1].tensor.shape[1] == 3


@pytest.mark.parametrize('uri', [(os.path.join(cur_dir, 'toydata/test.glb'))])
def test_load_vertices_and_faces_to_point_cloud(uri):
doc = Document(uri=uri)
doc.load_uri_to_vertices_and_faces()
doc.load_vertices_and_faces_to_point_cloud(100)

assert doc.tensor.shape == (100, 3)
assert isinstance(doc.tensor, np.ndarray)


@pytest.mark.parametrize('uri', [(os.path.join(cur_dir, 'toydata/test.glb'))])
def test_load_to_point_cloud_without_vertices_faces_set_raise_warning(uri):
doc = Document(uri=uri)

with pytest.raises(
AttributeError, match='vertices and faces chunk tensor have not been set'
):
doc.load_vertices_and_faces_to_point_cloud(100)