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.
- flatpoints-1.1.0/Method.md +78 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/PKG-INFO +10 -5
- {flatpoints-1.0.0 → flatpoints-1.1.0}/README.md +10 -5
- {flatpoints-1.0.0 → flatpoints-1.1.0}/pyproject.toml +1 -1
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/__init__.py +1 -1
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/main.py +19 -7
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/mesh.py +26 -0
- flatpoints-1.1.0/src/flatpoints/resampler.py +79 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/viz.py +1 -1
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/PKG-INFO +10 -5
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/SOURCES.txt +1 -0
- flatpoints-1.0.0/Method.md +0 -49
- {flatpoints-1.0.0 → flatpoints-1.1.0}/LICENSE +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/LICENSE-rectangular-conformal-map +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/MANIFEST.in +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/setup.cfg +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/compressor.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/core.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/data/seed.npy +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/datasets.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/examples.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/rcm.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints/utils.py +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/dependency_links.txt +0 -0
- {flatpoints-1.0.0 → flatpoints-1.1.0}/src/flatpoints.egg-info/requires.txt +0 -0
- {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.
|
|
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)
|
|
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
|

|
|
@@ -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)
|
|
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
|

|
|
@@ -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.
|
|
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)
|
|
@@ -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.
|
|
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)
|
|
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
|

|
flatpoints-1.0.0/Method.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|