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