Skip to content

Commit 5ebd1d1

Browse files
authored
Support ply and pcd formats in sai-cli process (#20)
* Add option to export optimized pointclouds in sai-cli tool * Fix typo * Dont create images folder if exporting ply or pcd pointcloud * Cleanup code * Revert unintentional changes
1 parent 80ae49d commit 5ebd1d1

File tree

2 files changed

+38
-16
lines changed

2 files changed

+38
-16
lines changed

python/cli/process/process.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""
22
Post-process data in Spectacular AI format and convert it to input
3-
for NeRF or Gaussian Splatting methods.
3+
for NeRF or Gaussian Splatting methods, or export optimized pointclouds in ply and pcd formats.
44
"""
55

66
# --- The following mechanism allows using this both as a stand-alone
77
# script and as a subcommand in sai-cli.
88

99
def define_args(parser):
1010
parser.add_argument("input", help="Path to folder with session to process")
11-
parser.add_argument("output", help="Output folder")
12-
parser.add_argument('--format', choices=['taichi', 'nerfstudio'], default='nerfstudio', help='Output format')
11+
parser.add_argument("output", help="Output folder, or filename with .ply or .pcd extension if exporting pointcloud")
12+
parser.add_argument('--format', choices=['taichi', 'nerfstudio'], default='nerfstudio', help='Output format.')
1313
parser.add_argument("--cell_size", help="Dense point cloud decimation cell size (meters)", type=float, default=0.1)
1414
parser.add_argument("--distance_quantile", help="Max point distance filter quantile (0 = disabled)", type=float, default=0.99)
1515
parser.add_argument("--key_frame_distance", help="Minimum distance between keyframes (meters)", type=float, default=0.05)
@@ -33,10 +33,17 @@ def process(args):
3333
import json
3434
import os
3535
import shutil
36+
import tempfile
3637
import numpy as np
3738
import pandas as pd
3839
from collections import OrderedDict
3940

41+
# Overwrite format if output is set to pointcloud
42+
if args.output.endswith(".ply"):
43+
args.format = "ply"
44+
elif args.output.endswith(".pcd"):
45+
args.format = "pcd"
46+
4047
useMono = None
4148

4249
def interpolate_missing_properties(df_source, df_query, k_nearest=3):
@@ -262,6 +269,10 @@ def onMappingOutput(output):
262269
if visualizer is not None:
263270
visualizer.onMappingOutput(output)
264271

272+
if args.format in ['ply', 'pcd']:
273+
if output.finalMap: finalMapWritten = True
274+
return
275+
265276
if not output.finalMap:
266277
# New frames, let's save the images to disk
267278
for frameId in output.updatedKeyFrames:
@@ -288,7 +299,7 @@ def onMappingOutput(output):
288299
img = undistortedFrame.image.toArray()
289300

290301
bgrImage = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
291-
fileName = f"{args.output}/tmp/frame_{frameId:05}.{args.image_format}"
302+
fileName = f"{tmp_dir}/frame_{frameId:05}.{args.image_format}"
292303
cv2.imwrite(fileName, bgrImage)
293304

294305
# Find colors for sparse features
@@ -307,7 +318,7 @@ def onMappingOutput(output):
307318
if frameSet.depthFrame is not None and frameSet.depthFrame.image is not None and not useMono:
308319
alignedDepth = frameSet.getAlignedDepthFrame(undistortedFrame)
309320
depthData = alignedDepth.image.toArray()
310-
depthFrameName = f"{args.output}/tmp/depth_{frameId:05}.png"
321+
depthFrameName = f"{tmp_dir}/depth_{frameId:05}.png"
311322
cv2.imwrite(depthFrameName, depthData)
312323

313324
DEPTH_PREVIEW = False
@@ -330,7 +341,7 @@ def onMappingOutput(output):
330341
sparsePointCloud = OrderedDict()
331342
imageSharpness = []
332343
for frameId in output.map.keyFrames:
333-
imageSharpness.append((frameId, blurScore(f"{args.output}/tmp/frame_{frameId:05}.{args.image_format}")))
344+
imageSharpness.append((frameId, blurScore(f"{tmp_dir}/frame_{frameId:05}.{args.image_format}")))
334345

335346
# Look two images forward and two backwards, if current frame is blurriest, don't use it
336347
for i in range(len(imageSharpness)):
@@ -377,11 +388,11 @@ def onMappingOutput(output):
377388
"camera_id": index # camera id, not used
378389
}
379390

380-
oldImgName = f"{args.output}/tmp/frame_{frameId:05}.{args.image_format}"
391+
oldImgName = f"{tmp_dir}/frame_{frameId:05}.{args.image_format}"
381392
newImgName = f"{args.output}/images/frame_{index:05}.{args.image_format}"
382393
os.rename(oldImgName, newImgName)
383394

384-
oldDepth = f"{args.output}/tmp/depth_{frameId:05}.png"
395+
oldDepth = f"{tmp_dir}/depth_{frameId:05}.png"
385396
newDepth = f"{args.output}/images/depth_{index:05}.png"
386397
if os.path.exists(oldDepth):
387398
os.rename(oldDepth, newDepth)
@@ -484,13 +495,6 @@ def detect_device_preset(input_dir):
484495
if device: break
485496
return (device, cameras)
486497

487-
# Clear output dir
488-
shutil.rmtree(f"{args.output}/images", ignore_errors=True)
489-
os.makedirs(f"{args.output}/images", exist_ok=True)
490-
tmp_dir = f"{args.output}/tmp"
491-
tmp_input = f"{tmp_dir}/input"
492-
copy_input_to_tmp_safe(args.input, tmp_input)
493-
494498
config = {
495499
"maxMapSize": 0,
496500
"useSlam": True,
@@ -499,6 +503,17 @@ def detect_device_preset(input_dir):
499503
"icpVoxelSize": min(args.key_frame_distance, 0.1)
500504
}
501505

506+
if args.format in ['ply', 'pcd']:
507+
config["mapSavePath"] = args.output
508+
else:
509+
# Clear output dir
510+
shutil.rmtree(f"{args.output}/images", ignore_errors=True)
511+
os.makedirs(f"{args.output}/images", exist_ok=True)
512+
513+
tmp_dir = tempfile.mkdtemp()
514+
tmp_input = tempfile.mkdtemp()
515+
copy_input_to_tmp_safe(args.input, tmp_input)
516+
502517
device_preset, cameras = detect_device_preset(args.input)
503518

504519
useMono = args.mono or (cameras != None and cameras == 1)
@@ -575,6 +590,11 @@ def detect_device_preset(input_dir):
575590
except:
576591
print(f"Failed to clean temporary directory, you can delete these files manually, they are no longer required: {tmp_dir}", flush=True)
577592

593+
try:
594+
shutil.rmtree(tmp_input)
595+
except:
596+
print(f"Failed to clean temporary directory, you can delete these files manually, they are no longer required: {tmp_input}", flush=True)
597+
578598
if not finalMapWritten:
579599
print('Mapping failed: no output generated')
580600
exit(1)
@@ -589,7 +609,7 @@ def detect_device_preset(input_dir):
589609
print(f"output-model-dir: data/{name}/output", flush=True)
590610
print(f"train-dataset-json-path: 'data/{name}/train.json'", flush=True)
591611
print(f"val-dataset-json-path: 'data/{name}/val.json'", flush=True)
592-
elif args.format == 'nerfstudio':
612+
else:
593613
print(f'output written to {args.output}', flush=True)
594614

595615
if __name__ == '__main__':

python/cli/visualization/visualizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ def __initDisplay(self):
315315

316316
def __close(self):
317317
assert(self.shouldQuit)
318+
self.map.reset()
319+
self.poseTrail.reset()
318320
if self.displayInitialized:
319321
self.displayInitialized = False
320322
pygame.quit()

0 commit comments

Comments
 (0)