flatpoints 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. flatpoints-1.1.0/Method.md +78 -0
  2. {flatpoints-1.0.0 → flatpoints-1.1.0}/PKG-INFO +10 -5
  3. {flatpoints-1.0.0 → flatpoints-1.1.0}/README.md +10 -5
  4. {flatpoints-1.0.0 → flatpoints-1.1.0}/pyproject.toml +1 -1
  5. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/__init__.py +1 -1
  6. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/main.py +19 -7
  7. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/mesh.py +26 -0
  8. flatpoints-1.1.0/src/flatpoints/resampler.py +79 -0
  9. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/viz.py +1 -1
  10. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/PKG-INFO +10 -5
  11. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/SOURCES.txt +1 -0
  12. flatpoints-1.0.0/Method.md +0 -49
  13. {flatpoints-1.0.0 → flatpoints-1.1.0}/LICENSE +0 -0
  14. {flatpoints-1.0.0 → flatpoints-1.1.0}/LICENSE-rectangular-conformal-map +0 -0
  15. {flatpoints-1.0.0 → flatpoints-1.1.0}/MANIFEST.in +0 -0
  16. {flatpoints-1.0.0 → flatpoints-1.1.0}/setup.cfg +0 -0
  17. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/compressor.py +0 -0
  18. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/core.py +0 -0
  19. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/data/seed.npy +0 -0
  20. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/datasets.py +0 -0
  21. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/examples.py +0 -0
  22. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/rcm.py +0 -0
  23. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/utils.py +0 -0
  24. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/dependency_links.txt +0 -0
  25. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/requires.txt +0 -0
  26. {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/top_level.txt +0 -0
@@ -0,0 +1,78 @@
1
+ # Method
2
+
3
+ flatpoints takes a point cloud sampled on a surface and returns one or two square images:
4
+ each point of the cloud on its own pixel, neighbour pixels holding neighbour points.
5
+
6
+ This is the *geometry image* of Gu, Gortler & Hoppe (2002), with one difference: no
7
+ resampling. The pixels hold the input points themselves, so `flat.pixels` gives the exact
8
+ point <-> pixel correspondence.
9
+
10
+ The flattening pipeline:
11
+
12
+ ```
13
+ points ──► mesh, cut into disks ──► conformal map onto the square ──► grid assignment ──► image
14
+ ```
15
+
16
+ The grid assignment (gridification) is the heart of the method. It matches the points to the
17
+ cells of a square grid, one to one (one point = one pixel), which gives a simple data
18
+ structure. By construction, it makes the final mapping roughly area-preserving: each pixel
19
+ covers the area of its point, the same for every point if the cloud is sampled uniformly.
20
+ Gridification is performed with the [gridpoints](https://pypi.org/project/gridpoints/) Python package.
21
+
22
+
23
+ ## 1. Mesh and cut
24
+
25
+ A square is a topological disk, so the surface must first be cut into disks, the faces.
26
+
27
+ - **Mesh**: screened Poisson reconstruction (open3d) of the point cloud, from normals
28
+ estimated and oriented on the points; only the largest connected piece is kept. Poisson
29
+ always closes the surface, so for an open surface (`single_face=True`) the parts far
30
+ from the points are trimmed.
31
+ - **Cut**: a closed surface is split into two faces, left and right of the median of the
32
+ points along `normal_axis`; an open surface is a single face. Faces grow triangle by
33
+ triangle and only accept triangles that keep them a disk, so handles and holes are cut
34
+ open: their triangles belong to no face.
35
+ - **Points**: each point is projected onto the nearest triangle of a face, and takes that
36
+ face.
37
+
38
+ ## 2. Conformal map
39
+
40
+ Each face is mapped onto the unit square by a rectangular conformal map (Meng, Choi & Lui
41
+ 2016). The four corners of the square go to the boundary vertices closest to the corners
42
+ of the face seen along `normal_axis`. Each point takes the conformal uv of its projection,
43
+ interpolated in its triangle.
44
+
45
+ ## 3. Grid
46
+
47
+ The points of each face are assigned to the cells of an n x n grid, each point to its own
48
+ cell, by finding a spatially consistant assignement from the conformal uv coordinates of
49
+ the points to cells of a grid, with one point matching one cell, up to a few padding cells
50
+ in the corners in case the number of points itself is not a square number, which are kept
51
+ empty (because the grid has exactly n^2 cells) and filled a posteriori with duplicate points.
52
+ The resulting one-to-one matching
53
+
54
+ ```
55
+ point -> pixel = (face, row, column)
56
+ ```
57
+
58
+ is the output of flatpoints.
59
+
60
+ ## Limits
61
+
62
+ - **Cuts**: points on either side of a cut are neighbours on the surface but not in the
63
+ images. This applies to the left / right cut of a closed surface, and to the extra cuts
64
+ of handles and holes (e.g. a torus).
65
+ - **Tears**: the grid cannot follow the uv exactly, so a few adjacent pixels hold points
66
+ several spacings apart (on the bunny, about 3% of the pairs of adjacent pixels are more
67
+ than 3 local spacings apart, the local spacing being the mean distance of a point to its
68
+ 6 nearest neighbours).
69
+ - **Meshing**: the surface is unknown and reconstructed from the points, so the quality of
70
+ the flattening depends on that of the Poisson mesh (sparse or noisy clouds, thin parts).
71
+
72
+ ## References
73
+
74
+ X. Gu, S. J. Gortler and H. Hoppe, "Geometry Images", SIGGRAPH 2002.
75
+ [hhoppe.com/proj/gim](https://hhoppe.com/proj/gim/)
76
+
77
+ T. W. Meng, G. P.-T. Choi and L. M. Lui, "TEMPO: Feature-Endowed Teichmüller Extremal
78
+ Mappings of Point Clouds", SIAM J. Imaging Sciences, 9(4), 2016.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatpoints
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Flatten a point cloud into square images where neighbour pixels are neighbour points
5
5
  Author: Armand de Cacqueray
6
6
  License-Expression: MIT AND Apache-2.0
@@ -112,6 +112,7 @@ z = xyz[:, -1] #(N, *C) any data related to the point cloud, e.g. the z coordina
112
112
  # ==== Build the flat object ============
113
113
 
114
114
  flat = fp.Flat(xyz) # Apply the flattening processing of the surface
115
+ # flat = fp.Flat(xyz, mesh=(vertices, triangles)) # giving your mesh as additional input (optional).
115
116
 
116
117
  # ==== Use it: pixels and images ========
117
118
 
@@ -158,16 +159,20 @@ cloud. So one can sample points in the unit square and transport them onto the s
158
159
  bilinear interpolation of the xyz image, (row, col) → xyz.
159
160
 
160
161
  By default, `flat.resample` uses a hyperuniform 2D seed (blue noise), so the new points are
161
- fairly evenly spread over the surface.
162
+ fairly evenly spread over the surface. If method = "ot" is used, the initial resampling is
163
+ optimised further using Optimal Transport.
162
164
 
163
165
  ```python
164
- sampler = flat.resample(n_points=512) # n new points in total
166
+ sampler = flat.resample(n_points=512, method="ot") # n new points in total
165
167
  new = sampler.points # (512, 3)
166
168
  sampler.plot() # before / after
167
- other = flat.resample(n_points=512, seed=1) # a new resampling, with different seed
168
169
  ```
170
+
169
171
  **To be honest:** if you only need a subset of the input points, farthest point sampling
170
- (FPS) are more uniform than flatpoints (figure below), this is mainly an exemple usecase to show how to use the images to transport properties (like good spatial repartition) from the simple unit square to an arbitrary surface.
172
+ (FPS) is roughly as uniform as flatpoints (fps has slightly smaller worst case hole and
173
+ flatpoints.resample slitghly lower ot loss), this is mainly an example usecase to show how
174
+ to use the images to transport properties like good spatial repartition from the simple unit
175
+ square to an arbitrary surface.
171
176
 
172
177
 
173
178
  ![hyperuniform resampling: comparison](https://raw.githubusercontent.com/neighborhood-grid/flatpoints/main/plots/resampling/bunny_comparison.png)
@@ -67,6 +67,7 @@ z = xyz[:, -1] #(N, *C) any data related to the point cloud, e.g. the z coordina
67
67
  # ==== Build the flat object ============
68
68
 
69
69
  flat = fp.Flat(xyz) # Apply the flattening processing of the surface
70
+ # flat = fp.Flat(xyz, mesh=(vertices, triangles)) # giving your mesh as additional input (optional).
70
71
 
71
72
  # ==== Use it: pixels and images ========
72
73
 
@@ -113,16 +114,20 @@ cloud. So one can sample points in the unit square and transport them onto the s
113
114
  bilinear interpolation of the xyz image, (row, col) → xyz.
114
115
 
115
116
  By default, `flat.resample` uses a hyperuniform 2D seed (blue noise), so the new points are
116
- fairly evenly spread over the surface.
117
+ fairly evenly spread over the surface. If method = "ot" is used, the initial resampling is
118
+ optimised further using Optimal Transport.
117
119
 
118
120
  ```python
119
- sampler = flat.resample(n_points=512) # n new points in total
121
+ sampler = flat.resample(n_points=512, method="ot") # n new points in total
120
122
  new = sampler.points # (512, 3)
121
123
  sampler.plot() # before / after
122
- other = flat.resample(n_points=512, seed=1) # a new resampling, with different seed
123
124
  ```
125
+
124
126
  **To be honest:** if you only need a subset of the input points, farthest point sampling
125
- (FPS) are more uniform than flatpoints (figure below), this is mainly an exemple usecase to show how to use the images to transport properties (like good spatial repartition) from the simple unit square to an arbitrary surface.
127
+ (FPS) is roughly as uniform as flatpoints (fps has slightly smaller worst case hole and
128
+ flatpoints.resample slitghly lower ot loss), this is mainly an example usecase to show how
129
+ to use the images to transport properties like good spatial repartition from the simple unit
130
+ square to an arbitrary surface.
126
131
 
127
132
 
128
133
  ![hyperuniform resampling: comparison](https://raw.githubusercontent.com/neighborhood-grid/flatpoints/main/plots/resampling/bunny_comparison.png)
@@ -202,4 +207,4 @@ The images rely on the rectangular conformal map of:
202
207
  year = {2016},
203
208
  doi = {10.1137/15M1049117}
204
209
  }
205
- ```
210
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flatpoints"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Flatten a point cloud into square images where neighbour pixels are neighbour points"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = "MIT AND Apache-2.0" # MIT, except src/flatpoints/rcm.py (Apache-2.0)
@@ -5,4 +5,4 @@ from .main import Compressor, Flat, Resampler
5
5
 
6
6
  __all__ = ["Flat", "Resampler", "Compressor", "load", "datasets",
7
7
  "exemple_flat", "exemple_sampler", "exemple_compression"]
8
- __version__ = "1.0.0"
8
+ __version__ = "1.1.0"
@@ -9,7 +9,8 @@ from scipy.ndimage import gaussian_filter, map_coordinates
9
9
  from scipy.spatial import cKDTree
10
10
 
11
11
  from .core import conformal_uv, image_cells
12
- from .mesh import TRIM, closest_on_mesh, disk_cut, hemisphere_cut, poisson_mesh, submesh
12
+ from .mesh import TRIM, closest_on_mesh, disk_cut, given_mesh, hemisphere_cut, poisson_mesh, submesh
13
+ from .resampler import lloyd_ot
13
14
  from .utils import axes, fill_empty, Chart, Corners, Mesh, _window, _seeds, ON_SURFACE
14
15
 
15
16
 
@@ -53,6 +54,9 @@ class Flat(Viz):
53
54
 
54
55
  Args:
55
56
  points: (N, 3) points sampled on a surface.
57
+ mesh: optional mesh of the surface, used instead of the Poisson reconstruction:
58
+ (vertices (V, 3), triangles (F, 3)), or any object with `vertices` and
59
+ `faces` (or `triangles`), e.g. trimesh, open3d. The points are projected on it.
56
60
  single_face: True for an open surface (one image), False for a closed one (two
57
61
  images, the surface cut in left and right halves).
58
62
  normal_axis: "x", "y" or "z". Orients the images (rows follow z, or y if
@@ -73,17 +77,19 @@ class Flat(Viz):
73
77
  new = flat.resample(512).points # (512, 3)
74
78
  """
75
79
 
76
- def __init__(self, points, single_face=False, normal_axis="x", verbose=False):
80
+ def __init__(self, points, single_face=False, normal_axis="x", verbose=False, mesh=None):
77
81
  self.points = P = np.asarray(points, float)
78
82
  self.normal_axis, self.single_face = normal_axis, single_face
79
83
  normal, horiz, vert = axes(normal_axis)
80
84
 
81
85
  # 1. Mesh, cut into charts
86
+ if mesh is not None:
87
+ V, F = given_mesh(mesh)
88
+ else:
89
+ V, F = poisson_mesh(P, trim=TRIM if single_face else None)
82
90
  if single_face:
83
- V, F = poisson_mesh(P, trim=TRIM)
84
91
  side = disk_cut(V, F)
85
92
  else:
86
- V, F = poisson_mesh(P)
87
93
  side = hemisphere_cut(V, F, normal, np.median(P[:, normal]))
88
94
  self._mesh = Mesh(V, F, side)
89
95
  if verbose:
@@ -129,9 +135,9 @@ class Flat(Viz):
129
135
 
130
136
  self.corners = Corners(np.concatenate(pad_idxs), np.concatenate(pad_pixels))
131
137
 
132
- def resample(self, n_points=512, seed=42, sigma=0.0):
138
+ def resample(self, n_points=512, seed=42, sigma=0.0, method="seed", n_iter=10):
133
139
  """`n_points` new points evenly spread on the surface, see `Resampler`."""
134
- return Resampler(self, n_points, seed, sigma)
140
+ return Resampler(self, n_points, seed, sigma, method, n_iter)
135
141
 
136
142
  def compress(self, factor=0.8, method="auto"):
137
143
  """Compress the points, `factor` (e.g. 0.8) of the storage saved, with `method`
@@ -173,13 +179,15 @@ class Resampler:
173
179
  seed: int (shift of the default hyperuniform seed), (M, 2) points in [0, 1)^2, or
174
180
  {face name: (M, 2)}.
175
181
  sigma: Gaussian smoothing of the images before lifting, in pixels.
182
+ method: "seed" (the seed lifted to 3D) or "ot" (then `n_iter` Lloyd iterations of
183
+ optimal transport onto the input cloud, see `resampler.py`).
176
184
 
177
185
  Attributes:
178
186
  points: (n_points, 3); faces: (n_points,); pixels: (n_points, 3) float
179
187
  (face, row, col).
180
188
  """
181
189
 
182
- def __init__(self, flat, n_points=512, seed=42, sigma=0.0):
190
+ def __init__(self, flat, n_points=512, seed=42, sigma=0.0, method="seed", n_iter=10):
183
191
  self.flat, self.n_points, self.sigma = flat, n_points, sigma
184
192
  names = ("main",) if flat.single_face else ("left", "right")
185
193
  if n_points < len(names):
@@ -210,6 +218,10 @@ class Resampler:
210
218
  self.points = np.concatenate(points)
211
219
  self.pixels = np.concatenate(pixels)
212
220
  self.faces = self.pixels[:, 0].astype(int)
221
+ if method == "ot":
222
+ self.points, self.pixels = lloyd_ot(flat, self.points, self.pixels, n_iter)
223
+ elif method != "seed":
224
+ raise ValueError('method must be "seed" or "ot"')
213
225
 
214
226
  def plot(self, ax=None, point_size=1.0, **kwargs):
215
227
  """Before (input cloud, left) / after (new points on its mesh, right), in 3D.
@@ -37,6 +37,32 @@ def poisson_mesh(points, depth=POISSON_DEPTH, trim=None):
37
37
  spacing = tree.query(points, k=7)[0][:, 1:].mean(1)
38
38
  d, i = tree.query(np.asarray(mesh.vertices))
39
39
  mesh.remove_vertices_by_mask(d > trim * spacing[i])
40
+ return clean(mesh)
41
+
42
+
43
+ def given_mesh(mesh):
44
+ """(V, F) of a mesh given by the user, in the standard format: (vertices (V, 3),
45
+ triangles (F, 3)), or any object with `vertices` and `faces` (or `triangles`), e.g.
46
+ trimesh, open3d. Cleaned like the Poisson mesh, duplicated vertices merged first."""
47
+ import open3d as o3d
48
+ if isinstance(mesh, (tuple, list)):
49
+ V, F = mesh
50
+ else:
51
+ V = mesh.vertices # faces first: trimesh.triangles are (F, 3, 3) coordinates
52
+ F = mesh.faces if hasattr(mesh, "faces") else mesh.triangles
53
+ V, F = np.asarray(V, float), np.asarray(F)
54
+ if V.ndim != 2 or V.shape[1] != 3 or F.ndim != 2 or F.shape[1] != 3:
55
+ raise ValueError("mesh: (vertices (V, 3), triangles (F, 3)), got "
56
+ f"{V.shape} and {F.shape}")
57
+ mesh = o3d.geometry.TriangleMesh(o3d.utility.Vector3dVector(V),
58
+ o3d.utility.Vector3iVector(F.astype(np.int32)))
59
+ mesh.remove_duplicated_vertices()
60
+ return clean(mesh)
61
+
62
+
63
+ def clean(mesh):
64
+ """(V, F) of the open3d `mesh` without degenerate triangles nor non-manifold edges,
65
+ its largest piece only."""
40
66
  mesh.remove_degenerate_triangles()
41
67
  mesh.remove_non_manifold_edges()
42
68
  piece, sizes, _ = mesh.cluster_connected_triangles()
@@ -0,0 +1,79 @@
1
+ """`Resampler(..., method="ot")`: the seed, then Lloyd iterations of optimal transport.
2
+
3
+ Each new point receives N / M input points (exact masses), by optimal transport between
4
+ the points, with the true cost ||x - y||^2 in 3D, then moves to the barycentre of its input
5
+ points. No mesh is used (it is only a reconstruction, it can miss thin parts): at the end,
6
+ each barycentre (slightly inside the surface where it curves) is projected on the tangent
7
+ plane of its nearest input point, fitted on the input points around it.
8
+
9
+ The images make the transport sparse: an input point may only go to its K nearest new
10
+ points on the images (N K pairs, instead of N M), and no farther than REACH times the
11
+ spacing of the new points in 3D (across a tear, a candidate can be near on the image and
12
+ far on the surface).
13
+
14
+ The transport is solved by its dual (a power diagram): input point j goes to the
15
+ candidate i minimising ||x_j - y_i||^2 - w_i, and the weight w_i rises if cell i has too
16
+ few points, falls if it has too many. numpy only; the weights are kept from one Lloyd
17
+ iteration to the next, so a few dual steps suffice. Cells hold N / M points up to a few.
18
+ """
19
+ import numpy as np
20
+ from scipy.spatial import cKDTree
21
+
22
+ K = 8 # candidate new points of an input point, nearest on the images
23
+ REACH = 2.0 # ... and closer than REACH x the spacing of the new points in 3D
24
+ DUAL_STEPS = 30 # dual steps per Lloyd iteration
25
+ DUAL_RATE = 1.2 # dual step, x the typical cost / (N / M)
26
+ K_PLANE = 10 # input points fitting a tangent plane
27
+
28
+
29
+ def lloyd_ot(flat, points, pixels, n_iter=10):
30
+ """`n_iter` Lloyd iterations of sparse optimal transport from the new `points` (M, 3),
31
+ at `pixels` (M, 3) float (face, row, col) on the images of `flat`. Faces do not change.
32
+ Returns (points (M, 3), pixels (M, 3) float (face, row, col))."""
33
+ P, ip = flat.points, flat.pixels
34
+ points = np.asarray(points, float).copy()
35
+ pixels = np.asarray(pixels, float).copy()
36
+ face = pixels[:, 0].astype(int)
37
+ weights = np.zeros(len(points))
38
+ for _ in range(n_iter):
39
+ reach2 = REACH ** 2 * np.median(cKDTree(points).query(points, k=2)[0][:, 1]) ** 2
40
+ for f in range(flat.n_faces):
41
+ J, I = np.flatnonzero(ip[:, 0] == f), np.flatnonzero(face == f)
42
+ if len(I) == 0:
43
+ continue
44
+ k = min(K, len(I))
45
+ _, nb = cKDTree(pixels[I, 1:]).query(ip[J, 1:].astype(float), k=k)
46
+ nb = nb.reshape(len(J), k)
47
+ cost = ((P[J, None, :] - points[I][nb]) ** 2).sum(-1)
48
+ far = cost > reach2
49
+ far[np.arange(len(J)), cost.argmin(1)] = False # always one candidate
50
+ cost[far] = np.inf
51
+
52
+ # transport: dual ascent on the weights of the cells
53
+ share = len(J) / len(I)
54
+ rate = DUAL_RATE * np.median(cost.min(1)) / share
55
+ w = weights[I]
56
+ for _ in range(DUAL_STEPS):
57
+ owner = nb[np.arange(len(J)), np.argmin(cost - w[nb], 1)]
58
+ count = np.bincount(owner, minlength=len(I))
59
+ w += rate * (share - count)
60
+ weights[I] = w
61
+
62
+ # Lloyd: barycentre of each cell (an empty cell keeps its point)
63
+ full = np.flatnonzero(count)
64
+ for x, out, cols in ((P[J], points, slice(None)),
65
+ (ip[J, 1:].astype(float), pixels, slice(1, 3))):
66
+ sums = np.stack([np.bincount(owner, c, len(I)) for c in x.T], 1)
67
+ out[I[full], cols] = sums[full] / count[full, None]
68
+ return tangent_projection(P, points), pixels
69
+
70
+
71
+ def tangent_projection(P, Q):
72
+ """`Q` (M, 3) projected on the tangent plane of their nearest point of `P`, fitted
73
+ (PCA) on its K_PLANE nearest points."""
74
+ tree = cKDTree(P)
75
+ _, i = tree.query(Q)
76
+ _, around = tree.query(P[i], k=min(K_PLANE, len(P)))
77
+ X = P[around] - P[around].mean(1, keepdims=True)
78
+ normal = np.linalg.eigh(np.einsum("mki,mkj->mij", X, X))[1][:, :, 0]
79
+ return Q - ((Q - P[i]) * normal).sum(1, keepdims=True) * normal
@@ -89,7 +89,7 @@ def plot_resampler(sampler, ax=None, point_size=1.0, light=0.45, radius=None):
89
89
  if len(mine): # shade of a point: the one of the nearest quad
90
90
  _, near = cKDTree(_quads(g["xyz"]).mean(2).reshape(-1, 3)).query(mine)
91
91
  layers.append(disc_layer(mine @ proj.T, rgb(mine, flat.points),
92
- shade.ravel()[near], radius, push=radius, sides=10))
92
+ shade.ravel()[near], radius, push=3 * radius, sides=10))
93
93
  render(ax, layers)
94
94
  return ax.figure
95
95
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatpoints
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Flatten a point cloud into square images where neighbour pixels are neighbour points
5
5
  Author: Armand de Cacqueray
6
6
  License-Expression: MIT AND Apache-2.0
@@ -112,6 +112,7 @@ z = xyz[:, -1] #(N, *C) any data related to the point cloud, e.g. the z coordina
112
112
  # ==== Build the flat object ============
113
113
 
114
114
  flat = fp.Flat(xyz) # Apply the flattening processing of the surface
115
+ # flat = fp.Flat(xyz, mesh=(vertices, triangles)) # giving your mesh as additional input (optional).
115
116
 
116
117
  # ==== Use it: pixels and images ========
117
118
 
@@ -158,16 +159,20 @@ cloud. So one can sample points in the unit square and transport them onto the s
158
159
  bilinear interpolation of the xyz image, (row, col) → xyz.
159
160
 
160
161
  By default, `flat.resample` uses a hyperuniform 2D seed (blue noise), so the new points are
161
- fairly evenly spread over the surface.
162
+ fairly evenly spread over the surface. If method = "ot" is used, the initial resampling is
163
+ optimised further using Optimal Transport.
162
164
 
163
165
  ```python
164
- sampler = flat.resample(n_points=512) # n new points in total
166
+ sampler = flat.resample(n_points=512, method="ot") # n new points in total
165
167
  new = sampler.points # (512, 3)
166
168
  sampler.plot() # before / after
167
- other = flat.resample(n_points=512, seed=1) # a new resampling, with different seed
168
169
  ```
170
+
169
171
  **To be honest:** if you only need a subset of the input points, farthest point sampling
170
- (FPS) are more uniform than flatpoints (figure below), this is mainly an exemple usecase to show how to use the images to transport properties (like good spatial repartition) from the simple unit square to an arbitrary surface.
172
+ (FPS) is roughly as uniform as flatpoints (fps has slightly smaller worst case hole and
173
+ flatpoints.resample slitghly lower ot loss), this is mainly an example usecase to show how
174
+ to use the images to transport properties like good spatial repartition from the simple unit
175
+ square to an arbitrary surface.
171
176
 
172
177
 
173
178
  ![hyperuniform resampling: comparison](https://raw.githubusercontent.com/neighborhood-grid/flatpoints/main/plots/resampling/bunny_comparison.png)
@@ -12,6 +12,7 @@ src/flatpoints/examples.py
12
12
  src/flatpoints/main.py
13
13
  src/flatpoints/mesh.py
14
14
  src/flatpoints/rcm.py
15
+ src/flatpoints/resampler.py
15
16
  src/flatpoints/utils.py
16
17
  src/flatpoints/viz.py
17
18
  src/flatpoints.egg-info/PKG-INFO
@@ -1,49 +0,0 @@
1
- # Method
2
-
3
- flatpoints takes a point cloud sampled on a surface and returns one or two square images: each point of the cloud on its own pixel, neighbour pixels holding neighbour points.
4
-
5
- ```
6
- points ──► mesh, cut into disks ──► map onto a square (conformal) ──► gridify (conformal to equal area) ──► image
7
- ```
8
-
9
- This is the *geometry image* of Gu, Gortler & Hoppe (2002), with one difference: no
10
- resampling. The pixels are the input points themselves, so `flat.pixels` gives the exact
11
- point <-> pixel correspondence.
12
-
13
- ## 1. Mesh and cut
14
-
15
- A square is a topological disk, so the surface must first be cut into disks, the faces.
16
-
17
- - **Mesh**: mesh the point cloud using screened Poisson reconstruction (open3d)
18
- - **Cut**: a closed surface is split into two faces, left and right of the median along
19
- `normal_axis`; an open surface is a single face. Faces grow triangle by triangle and
20
- only accept triangles that keep them a disk, so handles and holes are left cut open.
21
- - **Points**: each point is projected onto the mesh and takes the face of its triangle.
22
-
23
- ## 2. Conformal map
24
-
25
- Each face is mapped onto the unit square by a rectangular conformal map (Meng, Choi & Lui
26
- 2016). Each point takes the uv_conformal of its projection on the mesh.
27
-
28
- ## 3. Grid
29
-
30
- The points of each face are sorted onto an n x n grid by their uv_conformal (gridpoints). The resulting one to one matching
31
- ```
32
- point -> pixel = face, row, column
33
- ```
34
- is the final output of the flatpoints. Due to the one to one assignement, the flattening is by construction an area preserving transformation.
35
-
36
- ## Limits
37
-
38
- - **Cuts**: points on either side of a cut are neighbours on the surface but not in the images. This apply to the left / right cut of the single_face = False pipeline, but also on further topological closedness of the surface (e.g. for a torus), which will also be cut.
39
- - **Tears**: the grid cannot follow the uv exactly, so a few adjacent pixels hold points
40
- several spacings apart (2.7% beyond 3 spacings on the bunny).
41
- - **Meshing**: since flatpoints operate on point cloud with unkown surface, the overal precision of the flattening will depend on the quality of the meshing performed by open3d.
42
-
43
- ## References
44
-
45
- X. Gu, S. J. Gortler and H. Hoppe, "Geometry Images", SIGGRAPH 2002.
46
- [hhoppe.com/proj/gim](https://hhoppe.com/proj/gim/)
47
-
48
- T. W. Meng, G. P.-T. Choi and L. M. Lui, "TEMPO: Feature-Endowed Teichmüller Extremal
49
- Mappings of Point Clouds", SIAM J. Imaging Sciences, 9(4), 2016.
File without changes
File without changes
File without changes