Skip to content

Commit b8cf75f

Browse files
authored
Python visualizer camera controls (#7)
1 parent 7d7042e commit b8cf75f

File tree

2 files changed

+157
-31
lines changed

2 files changed

+157
-31
lines changed

python/cli/process/process.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,10 @@ def detect_device_preset(input_dir):
533533
if not args.fast: parameter_sets.append('offline-icp')
534534

535535
if args.preview3d:
536-
from spectacularAI.cli.visualization.visualizer import Visualizer
537-
visualizer = Visualizer()
536+
from spectacularAI.cli.visualization.visualizer import Visualizer, VisualizerArgs
537+
visArgs = VisualizerArgs()
538+
visArgs.targetFps = 30
539+
visualizer = Visualizer(visArgs)
538540

539541
with open(tmp_input + "/vio_config.yaml", 'wt') as f:
540542
base_params = 'parameterSets: %s' % json.dumps(parameter_sets)

python/cli/visualization/visualizer.py

Lines changed: 153 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(self):
3535
self.prevLookAtEye = None
3636
self.prevLookAtTarget = None
3737

38-
def compute(self, eye, target, paused):
38+
def update(self, eye, target, paused):
3939
if self.prevLookAtEye is not None:
4040
if paused: return self.prevLookAtEye, self.prevLookAtTarget
4141
eyeSmooth = self.alpha * eye + (1.0 - self.alpha) * self.prevLookAtEye
@@ -53,19 +53,130 @@ def reset(self):
5353
self.prevLookAtEye = None
5454
self.prevLookAtTarget = None
5555

56+
class CameraControls2D:
57+
def __init__(self):
58+
self.zoomSpeed = 1.0
59+
self.translateSpeed = 1.0
60+
self.reset()
61+
62+
def reset(self):
63+
self.lastMousePos = None
64+
self.draggingRight = False
65+
self.camera_pos = np.array([0.0, 0.0, 0.0])
66+
self.zoom = 1.0
67+
68+
def update(self, event):
69+
if event.type == pygame.MOUSEBUTTONDOWN:
70+
if event.button == 3: # Right mouse button
71+
self.draggingRight = True
72+
elif event.button == 4: # Scroll up
73+
self.zoom *= 0.95 * self.zoomSpeed
74+
elif event.button == 5: # Scroll down
75+
self.zoom *= 1.05 * self.zoomSpeed
76+
self.lastMousePos = pygame.mouse.get_pos()
77+
elif event.type == pygame.MOUSEBUTTONUP:
78+
if event.button == 1:
79+
self.draggingLeft = False
80+
elif event.button == 3:
81+
self.draggingRight = False
82+
elif event.type == pygame.MOUSEMOTION:
83+
if self.draggingRight:
84+
# Drag to move
85+
mouse_pos = pygame.mouse.get_pos()
86+
dx = mouse_pos[0] - self.lastMousePos[0]
87+
dy = mouse_pos[1] - self.lastMousePos[1]
88+
self.camera_pos[0] += 0.01 * dx * self.translateSpeed
89+
self.camera_pos[1] += 0.01 * dy * self.translateSpeed
90+
self.lastMousePos = pygame.mouse.get_pos()
91+
92+
def transformViewMatrix(self, viewMatrix):
93+
viewMatrix[:3, 3] += self.camera_pos
94+
return viewMatrix
95+
96+
class CameraControls3D:
97+
def __init__(self):
98+
self.zoomSpeed = 1.0
99+
self.translateSpeed = 1.0
100+
self.rotateSpeed = 1.0
101+
self.reset()
102+
103+
def __rotationMatrixX(self, a):
104+
return np.array([
105+
[1, 0, 0],
106+
[0, np.cos(a), -np.sin(a)],
107+
[0, np.sin(a), np.cos(a)]
108+
])
109+
110+
def __rotationMatrixZ(self, a):
111+
return np.array([
112+
[np.cos(a), -np.sin(a), 0],
113+
[np.sin(a), np.cos(a), 0],
114+
[0, 0, 1]
115+
])
116+
117+
def reset(self):
118+
self.lastMousePos = None
119+
self.draggingLeft = False
120+
self.draggingRight = False
121+
self.camera_pos = np.array([0.0, 0.0, 0.0])
122+
self.yaw = 0
123+
self.pitch = 0
124+
self.zoom = 1.0
125+
126+
def update(self, event):
127+
if event.type == pygame.MOUSEBUTTONDOWN:
128+
if event.button == 1: # Left mouse button
129+
self.draggingLeft = True
130+
elif event.button == 3: # Right mouse button
131+
self.draggingRight = True
132+
elif event.button == 4: # Scroll up
133+
self.camera_pos[2] -= 0.25 * self.zoomSpeed
134+
elif event.button == 5: # Scroll down
135+
self.camera_pos[2] += 0.25 * self.zoomSpeed
136+
self.lastMousePos = pygame.mouse.get_pos()
137+
elif event.type == pygame.MOUSEBUTTONUP:
138+
if event.button == 1:
139+
self.draggingLeft = False
140+
elif event.button == 3:
141+
self.draggingRight = False
142+
elif event.type == pygame.MOUSEMOTION:
143+
if self.draggingRight:
144+
# Drag to move
145+
mouse_pos = pygame.mouse.get_pos()
146+
dx = mouse_pos[0] - self.lastMousePos[0]
147+
dy = mouse_pos[1] - self.lastMousePos[1]
148+
self.camera_pos[0] += 0.01 * dx * self.translateSpeed
149+
self.camera_pos[1] += 0.01 * dy * self.translateSpeed
150+
elif self.draggingLeft:
151+
# Drag to rotate (yaw and pitch)
152+
mouse_pos = pygame.mouse.get_pos()
153+
dx = mouse_pos[0] - self.lastMousePos[0]
154+
dy = mouse_pos[1] - self.lastMousePos[1]
155+
self.yaw += 0.001 * dx * self.rotateSpeed
156+
self.pitch += 0.003 * dy * self.rotateSpeed
157+
self.lastMousePos = pygame.mouse.get_pos()
158+
159+
def transformViewMatrix(self, viewMatrix):
160+
viewMatrix[:3, 3] += self.camera_pos
161+
viewMatrix[:3, :3] = self.__rotationMatrixX(self.pitch) @ viewMatrix[:3, :3] # rotate around camera y-axis
162+
viewMatrix[:3, :3] = viewMatrix[:3, :3] @ self.__rotationMatrixZ(self.yaw) # rotate around world z-axis
163+
return viewMatrix
164+
56165
class VisualizerArgs:
57166
# Window
58167
resolution = "1280x720" # Window resolution
59168
fullScreen = False # Full screen mode
60169
visualizationScale = 10.0 # Generic scale of visualizations. Affects color maps, camera size, etc.
61170
backGroundColor = [1, 1, 1] # Background color RGB color (0-1).
62171
keepOpenAfterFinalMap = False # If false, window is automatically closed on final mapper output
172+
targetFps = 0 # 0 = render when vio output is available, otherwise tries to render at specified target fps
63173

64174
# Camera
65175
cameraNear = 0.01 # Camera near plane (m)
66176
cameraFar = 100.0 # Camera far plane (m)
67177
cameraMode = CameraMode.THIRD_PERSON # Camera mode (options: AR, 3rd person, 2D). Note: AR mode should have 'useRectification: True'
68178
cameraSmooth = True # Enable camera smoothing in 3rd person mode
179+
cameraFollow = True # When true, camera follows estimated camera pose. Otherwise, use free camera (3rd person, 2D)
69180
flip = False # Vertically flip image in AR mode
70181

71182
# Initial state for visualization components
@@ -133,6 +244,7 @@ def __init__(self, args=VisualizerArgs()):
133244
self.displayInitialized = False
134245
self.outputQueue = []
135246
self.outputQueueMutex = Lock()
247+
self.clock = pygame.time.Clock()
136248

137249
# Window
138250
self.fullScreen = args.fullScreen
@@ -144,8 +256,8 @@ def __init__(self, args=VisualizerArgs()):
144256
# Camera
145257
self.cameraMode = self.args.cameraMode
146258
self.cameraSmooth = CameraSmooth() if args.cameraSmooth else None
147-
self.initialZoom = args.visualizationScale / 10.0
148-
self.zoom = self.initialZoom
259+
self.cameraControls2D = CameraControls2D()
260+
self.cameraControls3D = CameraControls3D()
149261

150262
# Toggle visualization components
151263
self.showGrid = args.showGrid
@@ -218,36 +330,41 @@ def __render(self, cameraPose, width, height, image=None, colorFormat=None):
218330
glClearColor(*self.args.backGroundColor, 1.0)
219331
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT)
220332

333+
if self.args.cameraFollow:
334+
cameraToWorld = cameraPose.getCameraToWorldMatrix()
335+
else:
336+
cameraToWorld = np.array([
337+
[0, 0, 1, 0 ],
338+
[-1, 0, 0, 0 ],
339+
[ 0, -1, 0, 0],
340+
[0, 0, 0, 1]]
341+
)
342+
221343
near, far = self.args.cameraNear, self.args.cameraFar
222344
if self.cameraMode == CameraMode.AR:
223345
if image is not None:
224346
# draw AR background
225347
glDrawPixels(width, height, GL_LUMINANCE if colorFormat == spectacularAI.ColorFormat.GRAY else GL_RGB, GL_UNSIGNED_BYTE, image.data)
226348
viewMatrix = cameraPose.getWorldToCameraMatrix()
227349
projectionMatrix = cameraPose.camera.getProjectionMatrixOpenGL(near, far)
228-
if self.args.flip: projectionMatrix = np.array([1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]) @ projectionMatrix
350+
if self.args.flip: projectionMatrix = np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) @ projectionMatrix
229351
elif self.cameraMode == CameraMode.THIRD_PERSON:
230-
# TODO: implement mouse controls
231-
cameraToWorld = cameraPose.getCameraToWorldMatrix()
232352
up = np.array([0.0, 0.0, 1.0])
233353
forward = cameraToWorld[0:3, 2]
234354
eye = cameraToWorld[0:3, 3] - 10.0 * forward + 5.0 * up
235355
target = cameraToWorld[0:3, 3]
236-
if self.cameraSmooth: eye, target = self.cameraSmooth.compute(eye, target, self.shouldPause)
237-
viewMatrix = lookAt(eye, target, up)
356+
if self.cameraSmooth: eye, target = self.cameraSmooth.update(eye, target, self.shouldPause)
357+
viewMatrix = self.cameraControls3D.transformViewMatrix(lookAt(eye, target, up))
238358
projectionMatrix = cameraPose.camera.getProjectionMatrixOpenGL(near, far)
239-
projectionMatrix[0, 0] *= 1.0 / self.zoom
240-
projectionMatrix[1, 1] *= 1.0 / self.zoom
241359
elif self.cameraMode == CameraMode.TOP_VIEW:
242-
cameraToWorld = cameraPose.getCameraToWorldMatrix()
243360
eye = cameraToWorld[0:3, 3] + np.array([0, 0, 15])
244361
target = cameraToWorld[0:3, 3]
245362
up = np.array([-1.0, 0.0, 0.0])
246-
viewMatrix = lookAt(eye, target, up)
247-
left = -25.0 * self.zoom
248-
right = 25.0 * self.zoom
249-
bottom = -25.0 * self.zoom / self.aspectRatio # divide by aspect ratio to avoid strecthing (i.e. x and y directions have equal scale)
250-
top = 25.0 * self.zoom / self.aspectRatio
363+
viewMatrix = self.cameraControls2D.transformViewMatrix(lookAt(eye, target, up))
364+
left = -25.0 * self.cameraControls2D.zoom
365+
right = 25.0 * self.cameraControls2D.zoom
366+
bottom = -25.0 * self.cameraControls2D.zoom / self.aspectRatio # divide by aspect ratio to avoid strecthing (i.e. x and y directions have equal scale)
367+
top = 25.0 * self.cameraControls2D.zoom / self.aspectRatio
251368
projectionMatrix = getOrthographicProjectionMatrixOpenGL(left, right, bottom, top, -1000.0, 1000.0)
252369

253370
self.map.render(cameraPose.getPosition(), viewMatrix, projectionMatrix)
@@ -267,12 +384,13 @@ def __processUserInput(self):
267384
for event in pygame.event.get():
268385
if event.type == pygame.QUIT:
269386
self.shouldQuit = True
270-
if event.type == pygame.KEYDOWN:
387+
elif event.type == pygame.KEYDOWN:
271388
if event.key == pygame.K_q: self.shouldQuit = True
272389
elif event.key == pygame.K_SPACE: self.shouldPause = not self.shouldPause
273390
elif event.key == pygame.K_c:
274391
self.cameraMode = self.cameraMode.next()
275-
self.zoom = self.initialZoom
392+
self.cameraControls2D.reset()
393+
self.cameraControls3D.reset()
276394
if self.cameraSmooth: self.cameraSmooth.reset()
277395
elif event.key == pygame.K_PLUS:
278396
self.map.setPointSize(np.clip(self.map.pointSize*1.05, 0.0, 10.0))
@@ -308,11 +426,9 @@ def __processUserInput(self):
308426
else: pygame.display.set_mode((w, h), DOUBLEBUF | OPENGL)
309427
elif event.key == pygame.K_h:
310428
self.printHelp()
311-
if event.type == pygame.MOUSEBUTTONDOWN:
312-
if event.button == 4: # Mouse wheel up
313-
self.zoom = min(10.0*self.initialZoom , self.zoom * 1.05)
314-
elif event.button == 5: # Mouse wheel down
315-
self.zoom = max(0.1*self.initialZoom , self.zoom * 0.95)
429+
else:
430+
if self.cameraMode is CameraMode.THIRD_PERSON: self.cameraControls3D.update(event)
431+
if self.cameraMode is CameraMode.TOP_VIEW: self.cameraControls2D.update(event)
316432

317433
def onVioOutput(self, cameraPose, image=None, width=None, height=None, colorFormat=None):
318434
if self.shouldQuit: return
@@ -358,22 +474,22 @@ def onMappingOutput(self, mapperOutput):
358474
self.outputQueue.append(output)
359475

360476
def run(self):
477+
vioOutput = None
478+
361479
while not self.shouldQuit:
362480
self.__processUserInput()
363481

364-
if self.shouldPause or len(self.outputQueue) == 0:
365-
time.sleep(0.01)
366-
continue
367-
368482
# Process VIO & Mapping API outputs
369-
vioOutput = None
370483
while self.outputQueueMutex and len(self.outputQueue) > 0:
484+
if self.shouldPause: break
485+
371486
output = self.outputQueue.pop(0)
372487
if output["type"] == "vio":
373488
vioOutput = output
374489
cameraPose = vioOutput["cameraPose"]
375490
self.poseTrail.append(cameraPose.getPosition())
376-
break
491+
# Drop vio outputs if target fps is too low
492+
if self.args.targetFps == 0: break
377493
elif output["type"] == "slam":
378494
mapperOutput = output["mapperOutput"]
379495
self.map.onMappingOutput(mapperOutput)
@@ -390,6 +506,14 @@ def run(self):
390506
vioOutput["image"],
391507
vioOutput["colorFormat"])
392508

509+
if self.args.targetFps > 0:
510+
# Try to render at target fps
511+
self.clock.tick(self.args.targetFps)
512+
else:
513+
# Render whenever vioOutput is available
514+
vioOutput = None
515+
time.sleep(0.01)
516+
393517
self.__close()
394518

395519
def printHelp(self):

0 commit comments

Comments
 (0)