-
Notifications
You must be signed in to change notification settings - Fork 0
/
pipeline.py
384 lines (328 loc) · 11.3 KB
/
pipeline.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
"""pipeline for segmenting glb files"""
import argparse
from pathlib import Path
import logging
from typing import Optional
from PIL.Image import Image
from tqdm import tqdm
from tqdm.contrib.logging import logging_redirect_tqdm
from py3dtiles.tileset.tileset import TileSet, Tile
from classify import ImageSegmenter, texture_to_vertices
from glb import GLBSegment
from tileset import TilesetTraverser, convert_tileset
LOG: logging.Logger = logging.getLogger(__name__)
class Pipeline(TilesetTraverser):
"""pipeline for segmenting glb files in 3D tilesets
exports each glb file into a new glb file with metadata for each
subprimitive
attributes
----------
INPUT_DIR: pathlib.Path
input directory of the tileset
OUTPUT_DIR: pathlib.Path
output directory of the tileset
root_uri: pathlib.Path
root uri of the tileset to resolve relative paths
glb_count: int
number of glb files segmented
GLB_PBAR: tqdm.tqdm
progress bar for glb files
tileset_count: int
number of tilesets segmented
TILESET_PBAR: tqdm.tqdm
progress bar for tilesets
examples
--------
>>> Pipeline.INPUT_DIR = Path("input")
>>> Pipeline.OUTPUT_DIR = Path("output")
>>> Pipeline.pipeline(Path("tileset.json"))
"""
glb_count: int = 0
"""number of glb files segmented"""
GLB_PBAR = tqdm(desc="GLB files", unit=" .glb")
"""progress bar for glb files"""
tileset_count: int = 0
"""number of tilesets segmented"""
TILESET_PBAR = tqdm(desc="Tilesets", unit=" tileset")
"""progress bar for tilesets"""
@classmethod
def reset(cls) -> None:
"""reset the counters and progress bars
this method should be called before running the pipeline again
"""
cls.glb_count = 0
cls.tileset_count = 0
cls.GLB_PBAR.reset()
cls.TILESET_PBAR.reset()
@staticmethod
def get_classified_glb(path: Path) -> Optional[GLBSegment]:
"""returns a GLBSegment with subprimitives classified by semantic
segmentation of the texture image
parameters
----------
path: pathlib.Path
path to glb file
returns
-------
GLBSegment or None
GLBSegment with subprimitives classified by semantic segmentation
of the texture image or None if the GLBSegment could not be loaded
"""
glb = GLBSegment(path)
try:
glb.load_meshes()
except ValueError as err:
LOG.error("Error loading meshes: %s", err)
return None
for mesh in glb.meshes:
for primitive in tqdm(
mesh,
desc="Segmenting textures",
unit="texture",
leave=False,
):
texture: Optional[Image] = primitive.get_texture_image()
if texture is None:
LOG.warning("Primitive in %s has no texture", path)
continue
LOG.info("Predicting semantic segmentation")
seg = ImageSegmenter(texture).predict_semantic()
primitive.vertices_to_class = texture_to_vertices(
primitive.data.points, primitive.data.tex_coord, seg
)
return glb
@classmethod
def export_classified_glb(cls, uri: Path) -> None:
"""exports a GLBSegment with subprimitives classified by semantic
segmentation of the texture image
parameters
----------
uri: pathlib.Path
path to glb file
"""
uri_: Path = uri.relative_to(cls.INPUT_DIR)
if (cls.OUTPUT_DIR / uri_).exists():
LOG.info("GLB directory already exists")
cls.GLB_PBAR.update()
return
glb: Optional[GLBSegment] = cls.get_classified_glb(uri)
if glb is None:
return
glb.export(cls.OUTPUT_DIR / uri_)
cls.glb_count += 1
cls.GLB_PBAR.update()
@classmethod
def segment_tileset(cls, tileset: TileSet) -> None:
"""traverse tileset and segment root tile
parameters
----------
tileset: py3dtiles.tileset.TileSet
tileset to traverse
"""
LOG.info("Segmenting tileset")
cls.tileset_count += 1
cls.segment_tile(cls.get_tileset_tile(tileset))
cls.TILESET_PBAR.update()
@classmethod
def segment_tile(cls, tile: Tile) -> None:
"""traverse tile and segment content and children
parameters
----------
tile: py3dtiles.tile.Tile
tile to traverse
"""
cls.convert_tile_content(tile)
cls.convert_tile_children(tile)
@classmethod
def convert_tile_content(cls, tile: Tile) -> None:
"""convert tile content for glb files and tilesets
if content is a glb file, export a GLBSegment with subprimitives
classified by semantic segmentation of the texture image. if content is
a tileset, segment the tileset
parameters
----------
tile: py3dtiles.tile.Tile
tile to convert
"""
uri: Optional[Path] = cls.get_tile_content(tile)
if uri is None:
return
if uri.suffix == ".glb":
LOG.info("Segmenting tile")
cls.export_classified_glb(uri)
if uri.suffix == ".json":
cls.segment_tileset(TileSet.from_file(uri))
@classmethod
def convert_tile_children(cls, tile: Tile) -> None:
"""convert tile children by segmenting each child tile
parameters
----------
tile: py3dtiles.tile.Tile
tile to convert
"""
if tile.children is None:
return
for child in tile.children:
cls.segment_tile(child)
@classmethod
def pipeline(cls, path: Path) -> None:
"""
pipeline for segmenting glb files in 3D tilesets
exports each glb file into a new glb file with metadata for each
subprimitive
parameters
----------
path: pathlib.Path
path to tileset
"""
LOG.info("Starting segmentation")
LOG.info("Loading tileset")
tileset: TileSet = TileSet.from_file(cls.INPUT_DIR / path)
cls.segment_tileset(tileset)
LOG.info("Segmentation complete")
class PipelineSeparate(Pipeline):
"""pipeline for segmenting glb files in 3D tilesets
exports each subprimitive into a new glb file and exports the tileset
with updated metadata and content uris
attributes
----------
INPUT_DIR: pathlib.Path
input directory of the tileset
OUTPUT_DIR: pathlib.Path
output directory of the tileset
root_uri: pathlib.Path
root uri of the tileset to resolve relative paths
glb_count: int
number of glb files segmented
GLB_PBAR: tqdm.tqdm
progress bar for glb files
tileset_count: int
number of tilesets segmented
TILESET_PBAR: tqdm.tqdm
progress bar for tilesets
examples
--------
>>> PipelineSeparate.INPUT_DIR = Path("input")
>>> PipelineSeparate.OUTPUT_DIR = Path("output")
>>> PipelineSeparate.pipeline(Path("tileset.json"))
"""
@classmethod
def segment_tileset(cls, tileset: TileSet) -> None:
LOG.info("Segmenting tileset")
count: str = hex(cls.tileset_count)[2:]
cls.tileset_count += 1
if tileset.root_uri is not None:
cls.root_uri = tileset.root_uri
convert_tileset(tileset, ImageSegmenter.get_labels())
cls.segment_tile(tileset.root_tile)
cls.write_tileset(tileset, f"tileset_{count}.json")
cls.TILESET_PBAR.update()
@classmethod
def convert_tile_content(cls, tile: Tile) -> None:
if tile.content is None:
return
uri: Optional[Path] = cls.get_tile_content(tile)
if uri is None:
return
if uri.suffix == ".glb":
LOG.info("Segmenting tile")
cls.rewrite_tile(tile, uri)
if uri.suffix == ".json":
count: str = hex(cls.tileset_count)[2:]
tile.content["uri"] = f"tileset_{count}.json"
cls.segment_tileset(TileSet.from_file(uri))
@classmethod
def rewrite_tile(cls, tile: Tile, uri: Path) -> None:
"""rewrite tile content and append subprimitives to tile contents
parameters
----------
tile: py3dtiles.tile.Tile
tile to rewrite
uri: pathlib.Path
path to glb file
"""
tile.content = None
if tile.contents is None:
tile.contents = []
count: str = hex(cls.glb_count)[2:]
if (cls.OUTPUT_DIR / f"glb{count}").exists():
LOG.info("GLB directory already exists")
for file in (cls.OUTPUT_DIR / f"glb{count}").iterdir():
tile.contents.append(
{
"uri": f"glb{count}/{file.name}",
"group": int(file.stem.split("_")[1]),
}
)
cls.glb_count += 1
cls.GLB_PBAR.update()
return
glb: Optional[GLBSegment] = cls.get_classified_glb(uri)
if glb is None:
return
LOG.info("Rewriting tile")
for i, mesh in enumerate(glb.meshes):
for j, primitive in enumerate(mesh):
for class_id, subprimitive in primitive.subprimitives.items():
_uri: str = f"glb{count}/mesh{i}_{j}_{class_id}.glb"
primitive.export_subprimitive(
subprimitive, cls.OUTPUT_DIR / _uri
)
tile.contents.append({"uri": _uri, "group": class_id})
cls.glb_count += 1
cls.GLB_PBAR.update()
@classmethod
def pipeline(cls, path: Path) -> None:
"""pipeline for segmenting glb files in 3D tilesets
exports each subprimitive into a new glb file
parameters
----------
path: pathlib.Path
path to tileset
"""
super().pipeline(path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Segment a 3D tileset")
parser.add_argument(
"-f",
"--filename",
type=str,
help="filename of tileset",
default="tileset.json",
)
parser.add_argument(
"-i",
"--input-dir",
type=str,
help="input directory for tileset",
)
parser.add_argument(
"-o",
"--output-dir",
type=str,
help="output directory for tileset",
)
parser.add_argument(
"-s",
"--submeshes",
action="store_true",
help="export by splitting glb files",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="increase output verbosity",
)
args: argparse.Namespace = parser.parse_args()
if args.input_dir:
Pipeline.INPUT_DIR = Path(args.input_dir)
if args.output_dir:
Pipeline.OUTPUT_DIR = Path(args.output_dir)
if args.verbose:
logging.basicConfig(level=logging.INFO)
with logging_redirect_tqdm():
if args.submeshes:
PipelineSeparate.pipeline(Path(args.filename))
else:
Pipeline.pipeline(Path(args.filename))