texture3d 0.2.0__py3-none-any.whl
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.
- texture3d/__init__.py +31 -0
- texture3d/bake.py +159 -0
- texture3d/blur.py +54 -0
- texture3d/camera.py +108 -0
- texture3d/cli.py +74 -0
- texture3d/config.py +57 -0
- texture3d/io.py +52 -0
- texture3d/keyframes.py +42 -0
- texture3d/mapping/__init__.py +31 -0
- texture3d/mapping/atlas.py +246 -0
- texture3d/mapping/imageops.py +80 -0
- texture3d/mapping/labeling.py +298 -0
- texture3d/mapping/leveling.py +548 -0
- texture3d/mapping/mapper.py +114 -0
- texture3d/mapping/patches.py +287 -0
- texture3d/mapping/raster.py +185 -0
- texture3d/mapping/scoring.py +229 -0
- texture3d/mapping/topology.py +99 -0
- texture3d/mapping/types.py +199 -0
- texture3d-0.2.0.dist-info/METADATA +362 -0
- texture3d-0.2.0.dist-info/RECORD +24 -0
- texture3d-0.2.0.dist-info/WHEEL +5 -0
- texture3d-0.2.0.dist-info/entry_points.txt +2 -0
- texture3d-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Stage 4: connected same-view faces become patches with a rectangle in
|
|
2
|
+
their view, and the seam vertices between patches are catalogued."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Dict, List, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy.sparse import coo_matrix
|
|
11
|
+
from scipy.sparse.csgraph import connected_components
|
|
12
|
+
|
|
13
|
+
from texture3d.mapping import labeling
|
|
14
|
+
from texture3d.mapping.topology import Topology
|
|
15
|
+
from texture3d.mapping.types import NO_ID, FaceViews, Mesh, Rect, View
|
|
16
|
+
|
|
17
|
+
PATCH_BORDER = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TexturePatch:
|
|
22
|
+
#: View index, or NO_ID for the patch of untextured faces.
|
|
23
|
+
label: int
|
|
24
|
+
faces: np.ndarray
|
|
25
|
+
#: Bounding rectangle of the faces' projections in the view, with a
|
|
26
|
+
#: two-pixel border.
|
|
27
|
+
rect: Rect
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PatchSet:
|
|
32
|
+
#: The last patch holds the untextured faces.
|
|
33
|
+
patches: List[TexturePatch]
|
|
34
|
+
face_patch: np.ndarray
|
|
35
|
+
#: (F, 3, 2), relative to ``patches[face_patch[f]].rect``.
|
|
36
|
+
face_texcoords: np.ndarray
|
|
37
|
+
seam_edges: np.ndarray
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def patch_count(self) -> int:
|
|
41
|
+
return len(self.patches) - 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SeamData:
|
|
46
|
+
"""A mesh vertex where two or more patches meet, with, per patch, its
|
|
47
|
+
projection in that patch's view and the seam edges leaving it."""
|
|
48
|
+
|
|
49
|
+
#: Per seam vertex, the mesh vertex.
|
|
50
|
+
vertex: np.ndarray
|
|
51
|
+
#: CSR over seam vertices of (patch, projection) entries, in the order
|
|
52
|
+
#: the seam edges registered them.
|
|
53
|
+
entry_offsets: np.ndarray
|
|
54
|
+
entry_patch: np.ndarray
|
|
55
|
+
entry_proj: np.ndarray # (P, 2) float32, image coordinates of the view
|
|
56
|
+
#: CSR over entries of the seam edges leaving the vertex in that patch:
|
|
57
|
+
#: the seam vertex at the other end and the face along the edge.
|
|
58
|
+
edge_offsets: np.ndarray
|
|
59
|
+
edge_other: np.ndarray
|
|
60
|
+
edge_face: np.ndarray
|
|
61
|
+
#: (seam vertex, patch) -> entry index.
|
|
62
|
+
lookup: Dict[Tuple[int, int], int] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def count(self) -> int:
|
|
66
|
+
return len(self.vertex)
|
|
67
|
+
|
|
68
|
+
def entries_of(self, sv: int) -> range:
|
|
69
|
+
return range(int(self.entry_offsets[sv]), int(self.entry_offsets[sv + 1]))
|
|
70
|
+
|
|
71
|
+
def entry_count(self, sv: int) -> int:
|
|
72
|
+
return int(self.entry_offsets[sv + 1] - self.entry_offsets[sv])
|
|
73
|
+
|
|
74
|
+
def edges_of(self, entry: int) -> range:
|
|
75
|
+
return range(int(self.edge_offsets[entry]), int(self.edge_offsets[entry + 1]))
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def entry_sv(self) -> np.ndarray:
|
|
79
|
+
return np.repeat(np.arange(self.count), np.diff(self.entry_offsets))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build(mesh: Mesh, topology: Topology, face_views: FaceViews, outcome: labeling.Outcome, views: List[View]) -> PatchSet:
|
|
83
|
+
"""Connected components of same-view faces become patches, numbered in
|
|
84
|
+
order of their lowest face, untextured faces collected last."""
|
|
85
|
+
face_count = mesh.face_count
|
|
86
|
+
labels = outcome.labels
|
|
87
|
+
textured = labels != NO_ID
|
|
88
|
+
|
|
89
|
+
# Adjacent faces with the same view join one component. Faces with no
|
|
90
|
+
# view never join anything; they all end in the empty patch regardless.
|
|
91
|
+
ff = topology.face_faces
|
|
92
|
+
f_idx = np.broadcast_to(np.arange(face_count)[:, None], ff.shape)
|
|
93
|
+
same = (ff != NO_ID) & (f_idx < ff) & textured[f_idx] & (labels[f_idx] == labels[np.where(ff != NO_ID, ff, 0)])
|
|
94
|
+
graph = coo_matrix(
|
|
95
|
+
(np.ones(int(same.sum()), dtype=np.int8), (f_idx[same], ff[same])), shape=(face_count, face_count)
|
|
96
|
+
)
|
|
97
|
+
n_components, component = connected_components(graph, directed=False)
|
|
98
|
+
textured_components = np.unique(component[textured])
|
|
99
|
+
patch_of_component = np.full(n_components, -1, dtype=np.int64)
|
|
100
|
+
patch_of_component[textured_components] = np.arange(len(textured_components))
|
|
101
|
+
num_patches = len(textured_components)
|
|
102
|
+
face_patch = np.where(textured, patch_of_component[component], num_patches)
|
|
103
|
+
|
|
104
|
+
order = np.argsort(face_patch, kind="stable")
|
|
105
|
+
counts = np.bincount(face_patch, minlength=num_patches + 1)
|
|
106
|
+
starts = np.concatenate([[0], np.cumsum(counts)])
|
|
107
|
+
patches: List[TexturePatch] = []
|
|
108
|
+
for p in range(num_patches):
|
|
109
|
+
faces = order[starts[p] : starts[p + 1]]
|
|
110
|
+
patches.append(TexturePatch(label=int(labels[faces[0]]), faces=faces, rect=Rect(0, 0, 0, 0)))
|
|
111
|
+
empty_faces = order[starts[num_patches] : starts[num_patches + 1]]
|
|
112
|
+
patches.append(TexturePatch(label=NO_ID, faces=empty_faces, rect=Rect(0, 0, 5, 5)))
|
|
113
|
+
|
|
114
|
+
# Project every patch into its view, take the bounding rectangle with a
|
|
115
|
+
# two-pixel border, and make the coordinates relative.
|
|
116
|
+
texcoords = np.zeros((face_count, 3, 2), dtype=np.float32)
|
|
117
|
+
for v in np.unique(labels[textured]):
|
|
118
|
+
faces_v = np.flatnonzero(labels == v)
|
|
119
|
+
proj = views[int(v)].camera.project(mesh.positions)
|
|
120
|
+
texcoords[faces_v] = proj[mesh.faces[faces_v]]
|
|
121
|
+
textured_sorted = order[: starts[num_patches]]
|
|
122
|
+
if num_patches:
|
|
123
|
+
corners = texcoords[textured_sorted].reshape(-1, 2)
|
|
124
|
+
corner_starts = starts[:num_patches] * 3
|
|
125
|
+
lo = np.minimum.reduceat(corners, corner_starts, axis=0)
|
|
126
|
+
hi = np.maximum.reduceat(corners, corner_starts, axis=0)
|
|
127
|
+
rx = np.floor(lo[:, 0]).astype(np.int64) - PATCH_BORDER
|
|
128
|
+
ry = np.floor(lo[:, 1]).astype(np.int64) - PATCH_BORDER
|
|
129
|
+
rw = np.ceil(hi[:, 0] - lo[:, 0]).astype(np.int64) + PATCH_BORDER * 2
|
|
130
|
+
rh = np.ceil(hi[:, 1] - lo[:, 1]).astype(np.int64) + PATCH_BORDER * 2
|
|
131
|
+
for p in range(num_patches):
|
|
132
|
+
patches[p].rect = Rect(int(rx[p]), int(ry[p]), int(rw[p]), int(rh[p]))
|
|
133
|
+
offset = np.stack([rx, ry], axis=-1).astype(np.float32)
|
|
134
|
+
texcoords[textured] -= offset[face_patch[textured]][:, None, :]
|
|
135
|
+
texcoords[empty_faces] = np.float32(0.5)
|
|
136
|
+
return PatchSet(patches=patches, face_patch=face_patch, face_texcoords=texcoords, seam_edges=outcome.seam_edges)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_seam_vertices(mesh: Mesh, patch_set: PatchSet) -> SeamData:
|
|
140
|
+
"""Every seam edge registers its two vertices with the patches on both
|
|
141
|
+
sides, remembering the face and the other seam vertex the edge leads to."""
|
|
142
|
+
num_patches = patch_set.patch_count
|
|
143
|
+
edges = patch_set.seam_edges
|
|
144
|
+
faces = mesh.faces
|
|
145
|
+
tex = patch_set.face_texcoords
|
|
146
|
+
f0 = edges[:, 0]
|
|
147
|
+
f1 = edges[:, 1]
|
|
148
|
+
p0 = patch_set.face_patch[f0]
|
|
149
|
+
p1 = patch_set.face_patch[f1]
|
|
150
|
+
fa = faces[f0]
|
|
151
|
+
fb = faces[f1]
|
|
152
|
+
shared = fa[:, :, None] == fb[:, None, :] # (E, 3, 3)
|
|
153
|
+
in_f0 = shared.any(axis=2)
|
|
154
|
+
keep = (p0 != p1) & (in_f0.sum(axis=1) == 2)
|
|
155
|
+
e_idx = np.flatnonzero(keep)
|
|
156
|
+
slots0 = np.argsort(~in_f0[e_idx], axis=1, kind="stable")[:, :2]
|
|
157
|
+
slots1 = shared[e_idx][np.arange(len(e_idx))[:, None], slots0].argmax(axis=2)
|
|
158
|
+
vs = fa[e_idx][np.arange(len(e_idx))[:, None], slots0]
|
|
159
|
+
rect_x = np.array([p.rect.x for p in patch_set.patches], dtype=np.float32)
|
|
160
|
+
rect_y = np.array([p.rect.y for p in patch_set.patches], dtype=np.float32)
|
|
161
|
+
|
|
162
|
+
slot_of_vertex: Dict[int, int] = {}
|
|
163
|
+
sv_vertex: List[int] = []
|
|
164
|
+
sv_entries: List[List[int]] = []
|
|
165
|
+
entry_patch: List[int] = []
|
|
166
|
+
entry_proj: List[Tuple[float, float]] = []
|
|
167
|
+
entry_edges: List[List[Tuple[int, int]]] = []
|
|
168
|
+
lookup: Dict[Tuple[int, int], int] = {}
|
|
169
|
+
|
|
170
|
+
def seam_vertex(v: int) -> int:
|
|
171
|
+
s = slot_of_vertex.get(v)
|
|
172
|
+
if s is None:
|
|
173
|
+
s = len(sv_vertex)
|
|
174
|
+
slot_of_vertex[v] = s
|
|
175
|
+
sv_vertex.append(v)
|
|
176
|
+
sv_entries.append([])
|
|
177
|
+
return s
|
|
178
|
+
|
|
179
|
+
def entry(sv: int, patch: int) -> int:
|
|
180
|
+
e = lookup.get((sv, patch))
|
|
181
|
+
if e is None:
|
|
182
|
+
e = len(entry_patch)
|
|
183
|
+
lookup[(sv, patch)] = e
|
|
184
|
+
entry_patch.append(patch)
|
|
185
|
+
entry_proj.append((0.0, 0.0))
|
|
186
|
+
entry_edges.append([])
|
|
187
|
+
sv_entries[sv].append(e)
|
|
188
|
+
return e
|
|
189
|
+
|
|
190
|
+
for i, e in enumerate(e_idx):
|
|
191
|
+
v0, v1 = int(vs[i, 0]), int(vs[i, 1])
|
|
192
|
+
s0 = seam_vertex(v0)
|
|
193
|
+
s1 = seam_vertex(v1)
|
|
194
|
+
for patch, face, slots in ((int(p0[e]), int(f0[e]), slots0[i]), (int(p1[e]), int(f1[e]), slots1[i])):
|
|
195
|
+
if patch >= num_patches:
|
|
196
|
+
continue
|
|
197
|
+
ox, oy = float(rect_x[patch]), float(rect_y[patch])
|
|
198
|
+
a = entry(s0, patch)
|
|
199
|
+
entry_edges[a].append((s1, face))
|
|
200
|
+
t = tex[face, int(slots[0])]
|
|
201
|
+
entry_proj[a] = (float(t[0]) + ox, float(t[1]) + oy)
|
|
202
|
+
b = entry(s1, patch)
|
|
203
|
+
entry_edges[b].append((s0, face))
|
|
204
|
+
t = tex[face, int(slots[1])]
|
|
205
|
+
entry_proj[b] = (float(t[0]) + ox, float(t[1]) + oy)
|
|
206
|
+
|
|
207
|
+
# Flatten: entries per seam vertex in registration order, edges per entry.
|
|
208
|
+
entry_order = [e for entries in sv_entries for e in entries]
|
|
209
|
+
new_index = {old: new for new, old in enumerate(entry_order)}
|
|
210
|
+
entry_offsets = np.zeros(len(sv_vertex) + 1, dtype=np.int64)
|
|
211
|
+
np.cumsum([len(entries) for entries in sv_entries], out=entry_offsets[1:])
|
|
212
|
+
patch_arr = np.array([entry_patch[e] for e in entry_order], dtype=np.int64)
|
|
213
|
+
proj_arr = np.array([entry_proj[e] for e in entry_order], dtype=np.float32).reshape(-1, 2)
|
|
214
|
+
edge_counts = [len(entry_edges[e]) for e in entry_order]
|
|
215
|
+
edge_offsets = np.zeros(len(entry_order) + 1, dtype=np.int64)
|
|
216
|
+
np.cumsum(edge_counts, out=edge_offsets[1:])
|
|
217
|
+
edge_other = np.array([o for e in entry_order for o, _ in entry_edges[e]], dtype=np.int64)
|
|
218
|
+
edge_face = np.array([f for e in entry_order for _, f in entry_edges[e]], dtype=np.int64)
|
|
219
|
+
lookup_new = {key: new_index[old] for key, old in lookup.items()}
|
|
220
|
+
return SeamData(
|
|
221
|
+
vertex=np.array(sv_vertex, dtype=np.int64),
|
|
222
|
+
entry_offsets=entry_offsets,
|
|
223
|
+
entry_patch=patch_arr,
|
|
224
|
+
entry_proj=proj_arr,
|
|
225
|
+
edge_offsets=edge_offsets,
|
|
226
|
+
edge_other=edge_other,
|
|
227
|
+
edge_face=edge_face,
|
|
228
|
+
lookup=lookup_new,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def merge_contained(patch_set: PatchSet) -> None:
|
|
233
|
+
"""Fold a patch whose rectangle lies inside another in the same view.
|
|
234
|
+
|
|
235
|
+
Every patch joins the lowest-numbered surviving patch of its view whose
|
|
236
|
+
rectangle contains its own.
|
|
237
|
+
"""
|
|
238
|
+
patches = patch_set.patches
|
|
239
|
+
num_patches = patch_set.patch_count
|
|
240
|
+
if num_patches < 2:
|
|
241
|
+
return
|
|
242
|
+
labels = np.array([p.label for p in patches[:num_patches]])
|
|
243
|
+
x = np.array([p.rect.x for p in patches[:num_patches]])
|
|
244
|
+
y = np.array([p.rect.y for p in patches[:num_patches]])
|
|
245
|
+
w = np.array([p.rect.width for p in patches[:num_patches]])
|
|
246
|
+
h = np.array([p.rect.height for p in patches[:num_patches]])
|
|
247
|
+
target = np.arange(num_patches)
|
|
248
|
+
for label in np.unique(labels):
|
|
249
|
+
ids = np.flatnonzero(labels == label)
|
|
250
|
+
if len(ids) < 2:
|
|
251
|
+
continue
|
|
252
|
+
xi, yi, wi, hi = x[ids], y[ids], w[ids], h[ids]
|
|
253
|
+
# contained[j, i]: rect j lies inside rect i.
|
|
254
|
+
contained = (
|
|
255
|
+
(xi[:, None] >= xi[None, :])
|
|
256
|
+
& (yi[:, None] >= yi[None, :])
|
|
257
|
+
& (xi[:, None] + wi[:, None] <= xi[None, :] + wi[None, :])
|
|
258
|
+
& (yi[:, None] + hi[:, None] <= yi[None, :] + hi[None, :])
|
|
259
|
+
)
|
|
260
|
+
identical = contained & contained.T
|
|
261
|
+
np.fill_diagonal(contained, False)
|
|
262
|
+
# Of two identical rectangles the earlier absorbs the later.
|
|
263
|
+
later = np.arange(len(ids))[None, :] > np.arange(len(ids))[:, None]
|
|
264
|
+
contained &= ~(identical & later)
|
|
265
|
+
has = contained.any(axis=1)
|
|
266
|
+
first = np.argmax(contained, axis=1)
|
|
267
|
+
target[ids[has]] = ids[first[has]]
|
|
268
|
+
# Follow chains: a patch absorbed into one that is itself absorbed ends in
|
|
269
|
+
# the final absorber, with the offsets adding up.
|
|
270
|
+
for _ in range(num_patches):
|
|
271
|
+
nxt = target[target]
|
|
272
|
+
if np.array_equal(nxt, target):
|
|
273
|
+
break
|
|
274
|
+
target = nxt
|
|
275
|
+
merged = target != np.arange(num_patches)
|
|
276
|
+
if not merged.any():
|
|
277
|
+
return
|
|
278
|
+
for j in np.flatnonzero(merged):
|
|
279
|
+
i = int(target[j])
|
|
280
|
+
small, big = patches[j], patches[i]
|
|
281
|
+
offset = np.array([small.rect.x - big.rect.x, small.rect.y - big.rect.y], dtype=np.float32)
|
|
282
|
+
patch_set.face_texcoords[small.faces] += offset
|
|
283
|
+
big.faces = np.concatenate([big.faces, small.faces])
|
|
284
|
+
survivors = [patches[p] for p in range(num_patches) if not merged[p]] + [patches[num_patches]]
|
|
285
|
+
patch_set.patches = survivors
|
|
286
|
+
for p, patch in enumerate(survivors):
|
|
287
|
+
patch_set.face_patch[patch.faces] = p
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Rasterisers: barycentric for view face maps and colour-adjustment maps,
|
|
2
|
+
fixed-point for the local leveling mask, and Bresenham lines for the seams."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Iterator, Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from texture3d.mapping.types import round2int
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def edge(x0: np.ndarray, x1: np.ndarray, x2: np.ndarray) -> np.ndarray:
|
|
14
|
+
"""Signed parallelogram area ``(x2 - x0) × (x1 - x0)``, float32."""
|
|
15
|
+
a = x2 - x0
|
|
16
|
+
b = x1 - x0
|
|
17
|
+
return a[..., 0] * b[..., 1] - a[..., 1] * b[..., 0]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def perspective_correct(bary: np.ndarray, z: np.ndarray) -> np.ndarray:
|
|
21
|
+
"""Perspective-correct barycentrics for (N, 3) bary and (N, 3) z."""
|
|
22
|
+
pb = np.stack(
|
|
23
|
+
[bary[:, 0] * z[:, 1] * z[:, 2], bary[:, 1] * z[:, 0] * z[:, 2], bary[:, 2] * z[:, 0] * z[:, 1]], axis=-1
|
|
24
|
+
)
|
|
25
|
+
return pb / pb.sum(axis=1, keepdims=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def triangles_bary(
|
|
29
|
+
tri: np.ndarray, width: int, height: int, cull: bool = True, max_candidates: int = 1 << 24
|
|
30
|
+
) -> Iterator[Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]]:
|
|
31
|
+
"""Barycentric rasteriser over (N, 3, 2) float32 triangles.
|
|
32
|
+
|
|
33
|
+
Yields chunks of ``(triangle index, x, y, bary)`` for every pixel whose
|
|
34
|
+
centre has non-negative barycentric coordinates, within the image,
|
|
35
|
+
culling triangles whose signed area is not positive. Within a chunk the
|
|
36
|
+
pixels are in triangle order, then row-major inside each triangle.
|
|
37
|
+
"""
|
|
38
|
+
tri = np.asarray(tri, dtype=np.float32)
|
|
39
|
+
n = len(tri)
|
|
40
|
+
if n == 0:
|
|
41
|
+
return
|
|
42
|
+
box_min = tri.min(axis=1)
|
|
43
|
+
box_max = tri.max(axis=1)
|
|
44
|
+
outside = (box_max[:, 0] < 0) | (box_min[:, 0] > width - 1) | (box_max[:, 1] < 0) | (box_min[:, 1] > height - 1)
|
|
45
|
+
area = edge(tri[:, 0], tri[:, 1], tri[:, 2])
|
|
46
|
+
keep = ~outside
|
|
47
|
+
if cull:
|
|
48
|
+
keep &= area > 0
|
|
49
|
+
else:
|
|
50
|
+
keep &= area != 0
|
|
51
|
+
idx = np.flatnonzero(keep)
|
|
52
|
+
if len(idx) == 0:
|
|
53
|
+
return
|
|
54
|
+
min_x = np.clip(np.floor(box_min[idx, 0]).astype(np.int64), 0, width - 1)
|
|
55
|
+
min_y = np.clip(np.floor(box_min[idx, 1]).astype(np.int64), 0, height - 1)
|
|
56
|
+
max_x = np.clip(np.ceil(box_max[idx, 0]).astype(np.int64), 0, width - 1)
|
|
57
|
+
max_y = np.clip(np.ceil(box_max[idx, 1]).astype(np.int64), 0, height - 1)
|
|
58
|
+
bw = max_x - min_x + 1
|
|
59
|
+
bh = max_y - min_y + 1
|
|
60
|
+
counts = bw * bh
|
|
61
|
+
inv_area = (np.float32(1) / area[idx]).astype(np.float32)
|
|
62
|
+
|
|
63
|
+
start = 0
|
|
64
|
+
total = len(idx)
|
|
65
|
+
while start < total:
|
|
66
|
+
# Take triangles until the candidate budget is reached.
|
|
67
|
+
cum = np.cumsum(counts[start:])
|
|
68
|
+
take = int(np.searchsorted(cum, max_candidates, side="right"))
|
|
69
|
+
take = max(1, min(take, total - start))
|
|
70
|
+
sel = slice(start, start + take)
|
|
71
|
+
c = counts[sel]
|
|
72
|
+
rep = np.repeat(np.arange(start, start + take), c)
|
|
73
|
+
offsets = np.repeat(np.cumsum(c) - c, c)
|
|
74
|
+
local = np.arange(int(c.sum())) - offsets
|
|
75
|
+
x = min_x[rep] + local % bw[rep]
|
|
76
|
+
y = min_y[rep] + local // bw[rep]
|
|
77
|
+
p = np.stack([x, y], axis=-1).astype(np.float32)
|
|
78
|
+
t = tri[idx[rep]]
|
|
79
|
+
ia = inv_area[rep]
|
|
80
|
+
b1 = edge(t[:, 1], t[:, 2], p) * ia
|
|
81
|
+
b2 = edge(t[:, 2], t[:, 0], p) * ia
|
|
82
|
+
b3 = edge(t[:, 0], t[:, 1], p) * ia
|
|
83
|
+
inside = (b1 >= 0) & (b2 >= 0) & (b3 >= 0)
|
|
84
|
+
yield idx[rep[inside]], x[inside], y[inside], np.stack([b1[inside], b2[inside], b3[inside]], axis=-1)
|
|
85
|
+
start += take
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def triangles_fixed(tri: np.ndarray, width: int, height: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
89
|
+
"""28.4 fixed-point half-space rasteriser with a top-left fill
|
|
90
|
+
convention, over (N, 3, 2) triangles. Returns ``(triangle index, x, y)``
|
|
91
|
+
for the covered pixels inside ``width`` x ``height``."""
|
|
92
|
+
tri = np.asarray(tri, dtype=np.float32)
|
|
93
|
+
n = len(tri)
|
|
94
|
+
if n == 0:
|
|
95
|
+
empty = np.zeros(0, dtype=np.int64)
|
|
96
|
+
return empty, empty, empty
|
|
97
|
+
X = round2int(16 * tri[..., 0])
|
|
98
|
+
Y = round2int(16 * tri[..., 1])
|
|
99
|
+
X1, X2, X3 = X[:, 0], X[:, 1], X[:, 2]
|
|
100
|
+
Y1, Y2, Y3 = Y[:, 0], Y[:, 1], Y[:, 2]
|
|
101
|
+
DX12, DX23, DX31 = X1 - X2, X2 - X3, X3 - X1
|
|
102
|
+
DY12, DY23, DY31 = Y1 - Y2, Y2 - Y3, Y3 - Y1
|
|
103
|
+
min_x = (X.min(axis=1) + 0xF) >> 4
|
|
104
|
+
max_x = (X.max(axis=1) + 0xF) >> 4
|
|
105
|
+
min_y = (Y.min(axis=1) + 0xF) >> 4
|
|
106
|
+
max_y = (Y.max(axis=1) + 0xF) >> 4
|
|
107
|
+
C1 = DY12 * X1 - DX12 * Y1 + ((DY12 < 0) | ((DY12 == 0) & (DX12 > 0)))
|
|
108
|
+
C2 = DY23 * X2 - DX23 * Y2 + ((DY23 < 0) | ((DY23 == 0) & (DX23 > 0)))
|
|
109
|
+
C3 = DY31 * X3 - DX31 * Y3 + ((DY31 < 0) | ((DY31 == 0) & (DX31 > 0)))
|
|
110
|
+
# Only pixels inside the image can be reported, so clip the boxes first.
|
|
111
|
+
lo_x = np.clip(min_x, 0, width)
|
|
112
|
+
hi_x = np.clip(max_x, 0, width)
|
|
113
|
+
lo_y = np.clip(min_y, 0, height)
|
|
114
|
+
hi_y = np.clip(max_y, 0, height)
|
|
115
|
+
bw = np.maximum(hi_x - lo_x, 0)
|
|
116
|
+
bh = np.maximum(hi_y - lo_y, 0)
|
|
117
|
+
counts = bw * bh
|
|
118
|
+
rep = np.repeat(np.arange(n), counts)
|
|
119
|
+
if len(rep) == 0:
|
|
120
|
+
empty = np.zeros(0, dtype=np.int64)
|
|
121
|
+
return empty, empty, empty
|
|
122
|
+
offsets = np.repeat(np.cumsum(counts) - counts, counts)
|
|
123
|
+
local = np.arange(int(counts.sum())) - offsets
|
|
124
|
+
x = lo_x[rep] + local % bw[rep]
|
|
125
|
+
y = lo_y[rep] + local // bw[rep]
|
|
126
|
+
x16 = x << 4
|
|
127
|
+
y16 = y << 4
|
|
128
|
+
inside = (
|
|
129
|
+
(C1[rep] + DX12[rep] * y16 - DY12[rep] * x16 > 0)
|
|
130
|
+
& (C2[rep] + DX23[rep] * y16 - DY23[rep] * x16 > 0)
|
|
131
|
+
& (C3[rep] + DX31[rep] * y16 - DY31[rep] * x16 > 0)
|
|
132
|
+
)
|
|
133
|
+
return rep[inside], x[inside], y[inside]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def lines(p1: np.ndarray, p2: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
137
|
+
"""Bresenham from the rounded start to the rounded end, end exclusive,
|
|
138
|
+
over (N, 2) endpoints. Returns ``(line index, x, y)`` with each line's
|
|
139
|
+
pixels in drawing order, lines in input order."""
|
|
140
|
+
p1 = np.asarray(p1, dtype=np.float32).copy()
|
|
141
|
+
p2 = np.asarray(p2, dtype=np.float32).copy()
|
|
142
|
+
n = len(p1)
|
|
143
|
+
if n == 0:
|
|
144
|
+
empty = np.zeros(0, dtype=np.int64)
|
|
145
|
+
return empty, empty, empty
|
|
146
|
+
x1, y1 = p1[:, 0].copy(), p1[:, 1].copy()
|
|
147
|
+
x2, y2 = p2[:, 0].copy(), p2[:, 1].copy()
|
|
148
|
+
steep = np.abs(y2 - y1) > np.abs(x2 - x1)
|
|
149
|
+
x1s, y1s = np.where(steep, y1, x1), np.where(steep, x1, y1)
|
|
150
|
+
x2s, y2s = np.where(steep, y2, x2), np.where(steep, x2, y2)
|
|
151
|
+
flip = x1s > x2s
|
|
152
|
+
xa, xb = np.where(flip, x2s, x1s), np.where(flip, x1s, x2s)
|
|
153
|
+
ya, yb = np.where(flip, y2s, y1s), np.where(flip, y1s, y2s)
|
|
154
|
+
dx = (xb - xa).astype(np.float32)
|
|
155
|
+
dy = np.abs(yb - ya).astype(np.float32)
|
|
156
|
+
error = dx / np.float32(2)
|
|
157
|
+
y = round2int(ya)
|
|
158
|
+
ystep = np.where(ya < yb, 1, -1)
|
|
159
|
+
x = round2int(xa)
|
|
160
|
+
max_x = round2int(xb)
|
|
161
|
+
steps = np.maximum(max_x - x, 0)
|
|
162
|
+
total = int(steps.sum())
|
|
163
|
+
out_line = np.empty(total, dtype=np.int64)
|
|
164
|
+
out_x = np.empty(total, dtype=np.int64)
|
|
165
|
+
out_y = np.empty(total, dtype=np.int64)
|
|
166
|
+
# Each line's pixels are contiguous in the output; step k of line i goes to
|
|
167
|
+
# slot start[i] + k.
|
|
168
|
+
start = np.cumsum(steps) - steps
|
|
169
|
+
active = steps > 0
|
|
170
|
+
k = 0
|
|
171
|
+
while active.any():
|
|
172
|
+
ids = np.flatnonzero(active)
|
|
173
|
+
slot = start[ids] + k
|
|
174
|
+
px, py = x[ids], y[ids]
|
|
175
|
+
out_line[slot] = ids
|
|
176
|
+
out_x[slot] = np.where(steep[ids], py, px)
|
|
177
|
+
out_y[slot] = np.where(steep[ids], px, py)
|
|
178
|
+
error[ids] -= dy[ids]
|
|
179
|
+
inc = error[ids] < 0
|
|
180
|
+
y[ids] += np.where(inc, ystep[ids], 0)
|
|
181
|
+
error[ids] += np.where(inc, dx[ids], np.float32(0))
|
|
182
|
+
x[ids] += 1
|
|
183
|
+
k += 1
|
|
184
|
+
active = steps > k
|
|
185
|
+
return out_line, out_x, out_y
|