Skip to content

Commit a59299a

Browse files
committed
sai-cli smooth v1: output full smoothed trajectory using pose trails
1 parent 44a3d43 commit a59299a

File tree

2 files changed

+180
-0
lines changed

2 files changed

+180
-0
lines changed

python/cli/sai_cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from .process.process import define_subparser as process_define_subparser
44
from .record.record import define_subparser as record_define_subparser
5+
from .smooth import define_subparser as smooth_define_subparser
56

67
def parse_args():
78
parser = argparse.ArgumentParser(description='Spectacular AI command line tool')
89
subparsers = parser.add_subparsers(title='subcommands', dest='subcommand', required=True)
910
process_define_subparser(subparsers)
1011
record_define_subparser(subparsers)
12+
smooth_define_subparser(subparsers)
1113
return parser.parse_args()
1214

1315
def main():

python/cli/smooth.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""
2+
Post-process a session and generate a smoothed trajectory
3+
"""
4+
import json
5+
from process.process import parse_input_dir, auto_config
6+
7+
# --- The following mechanism allows using this both as a stand-alone
8+
# script and as a subcommand in sai-cli.
9+
10+
def define_args(parser):
11+
parser.add_argument("input", help="Path to folder with session to process")
12+
parser.add_argument("output", help="Output JSONL file with smoothed trajectory")
13+
parser.add_argument('--device_preset', help="Device preset. Automatically detected in most cases")
14+
parser.add_argument("--key_frame_distance", help="Minimum distance between keyframes (meters)", type=float, default=0.15)
15+
parser.add_argument('--fast', action='store_true', help='Fast but lower quality settings')
16+
parser.add_argument('--internal', action='append', type=str, help='Internal override parameters in the form --internal=name:value')
17+
parser.add_argument("--preview", help="Show current key frame", action="store_true")
18+
parser.add_argument("--preview3d", help="Show 3D visualization", action="store_true")
19+
return parser
20+
21+
def define_subparser(subparsers):
22+
sub = subparsers.add_parser('smooth', help=__doc__.strip())
23+
sub.set_defaults(func=smooth)
24+
return define_args(sub)
25+
26+
def compute_full_trajectory(keyFrames, poseTrails, outputToCam):
27+
import spectacularAI
28+
import numpy as np
29+
30+
trajectory = {}
31+
for kf in keyFrames.values():
32+
camPose = kf.frameSet.primaryFrame.cameraPose.pose
33+
pose = camPose.asMatrix() @ outputToCam
34+
trajectory[camPose.time] = (0, pose)
35+
36+
# TODO: can be improved
37+
STEP_PENALTY = 0.5
38+
for tOlder, tNewer, newerToOlder in poseTrails[::-1]:
39+
if tNewer in trajectory:
40+
olderToNewer = np.linalg.inv(newerToOlder)
41+
dt, poseNewer = trajectory[tNewer]
42+
dt += tNewer - tOlder + STEP_PENALTY
43+
if tOlder not in trajectory or trajectory[tOlder][0] > dt:
44+
trajectory[tOlder] = (dt, poseNewer @ olderToNewer)
45+
if tOlder in trajectory:
46+
dt, poseOlder = trajectory[tOlder]
47+
dt += tNewer - tOlder + STEP_PENALTY
48+
if tNewer not in trajectory or trajectory[tNewer][0] > dt:
49+
trajectory[tNewer] = (dt, poseOlder @ newerToOlder)
50+
51+
for t in sorted(trajectory.keys()):
52+
pose = spectacularAI.Pose.fromMatrix(t, trajectory[t][1])
53+
yield({
54+
'time': t,
55+
'position': { c: getattr(pose.position, c) for c in 'xyz' },
56+
'orientation': { c: getattr(pose.orientation, c) for c in 'wxyz' }
57+
})
58+
59+
def smooth(args):
60+
import spectacularAI
61+
import numpy as np
62+
63+
# Globals
64+
visualizer = None
65+
isTracking = False
66+
finalMapWritten = False
67+
poseTrails = []
68+
outputToCam = None
69+
70+
def process_mapping_output(output):
71+
nonlocal visualizer
72+
nonlocal finalMapWritten
73+
74+
if visualizer is not None:
75+
visualizer.onMappingOutput(output)
76+
77+
if output.finalMap:
78+
trajectory = compute_full_trajectory(output.map.keyFrames, poseTrails, outputToCam)
79+
80+
with open(args.output, "w") as outFile:
81+
for output in trajectory:
82+
outFile.write(json.dumps(output) + "\n")
83+
finalMapWritten = True
84+
85+
elif len(output.updatedKeyFrames) > 0:
86+
frameId = output.updatedKeyFrames[-1]
87+
keyFrame = output.map.keyFrames.get(frameId)
88+
if not keyFrame: return
89+
90+
frameSet = keyFrame.frameSet
91+
targetFrame = frameSet.rgbFrame
92+
if not targetFrame: targetFrame = frameSet.primaryFrame
93+
if not targetFrame or not targetFrame.image: return
94+
img = targetFrame.image.toArray()
95+
96+
if args.preview:
97+
import cv2
98+
bgrImage = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
99+
cv2.imshow("Frame", bgrImage)
100+
cv2.setWindowTitle("Frame", "Key frame candidate #{}".format(frameId))
101+
cv2.waitKey(1)
102+
103+
def on_vio_output(vioOutput):
104+
nonlocal visualizer, isTracking, outputToCam
105+
wasTracking = isTracking
106+
isTracking = vioOutput.status == spectacularAI.TrackingStatus.TRACKING
107+
if wasTracking and not isTracking:
108+
print('warning: Lost tracking!')
109+
110+
if outputToCam is None:
111+
# TODO: hacky... improve API
112+
primaryCam = vioOutput.getCameraPose(0)
113+
camToWorld = primaryCam.pose.asMatrix()
114+
outToWorld = vioOutput.pose.asMatrix()
115+
outputToCam = np.linalg.inv(camToWorld) @ outToWorld
116+
117+
for p in vioOutput.poseTrail:
118+
currentToPastPose = np.linalg.inv(p.asMatrix()) @ vioOutput.pose.asMatrix()
119+
poseTrails.append((p.time, vioOutput.pose.time, currentToPastPose))
120+
121+
if visualizer is not None:
122+
visualizer.onVioOutput(vioOutput.getCameraPose(0), status=vioOutput.status)
123+
124+
def on_mapping_output(output):
125+
try:
126+
process_mapping_output(output)
127+
except Exception as e:
128+
print(f"ERROR: {e}", flush=True)
129+
raise
130+
131+
device_preset, cameras = parse_input_dir(args.input)
132+
useMono = cameras != None and len(cameras) == 1
133+
134+
config = auto_config(device_preset,
135+
key_frame_distance=args.key_frame_distance,
136+
mono=useMono,
137+
fast=args.fast,
138+
internal=args.internal)
139+
140+
if args.preview3d:
141+
from spectacularAI.cli.visualization.visualizer import Visualizer, VisualizerArgs
142+
visArgs = VisualizerArgs()
143+
visArgs.targetFps = 30
144+
visArgs.showCameraModel = False
145+
visualizer = Visualizer(visArgs)
146+
147+
print(config)
148+
149+
replay = spectacularAI.Replay(args.input, mapperCallback = on_mapping_output, configuration = config, ignoreFolderConfiguration = True)
150+
replay.setOutputCallback(on_vio_output)
151+
152+
try:
153+
if visualizer is None:
154+
replay.runReplay()
155+
else:
156+
replay.startReplay()
157+
visualizer.run()
158+
replay.close()
159+
except Exception as e:
160+
print(f"Something went wrong! {e}", flush=True)
161+
raise e
162+
163+
replay = None
164+
165+
if not finalMapWritten:
166+
print('Smoothing failed: no output generated')
167+
exit(1)
168+
169+
print("Done!\n", flush=True)
170+
171+
if __name__ == '__main__':
172+
def parse_args():
173+
import argparse
174+
parser = argparse.ArgumentParser(description=__doc__.strip())
175+
parser = define_args(parser)
176+
return parser.parse_args()
177+
178+
smooth(parse_args())

0 commit comments

Comments
 (0)