Skip to content

Commit 4c35c7f

Browse files
authored
Merge pull request #82 from dfgHiatus/LordOfDragons-vivefacialtracker
(feat) Add support for Vive Faical Tracker
2 parents a68f443 + fe8b57b commit 4c35c7f

File tree

9 files changed

+1723
-45
lines changed

9 files changed

+1723
-45
lines changed

.gitignore

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ BabbleApp/babble_settings.json
66
BabbleApp/babble_settings.backup
77
BabbleApp/build
88
BabbleApp/dist
9+
/BabbleApp/Models/dev
10+
/BabbleApp/venv
911
/vivetest
1012
/training
1113
.vscode
1214
/testing
1315
/old_models
1416
/Lib
1517
/share
16-
/BabbleApp/Models/dev
1718

1819
scripts/example_build_app_and_installer.bat
1920
scripts/example_build_app_and_installer.bat
2021
scripts/installer.iss
2122
Glia_cap.py
2223
lazyass_minecraft_script.py
2324
scripts/example_build_app_and_installer.bat
24-
/BabbleApp/venv
25+
*.kdev*
26+
*.code-workspace

BabbleApp/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.log

BabbleApp/babbleapp.py

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import requests
2323
import threading
2424
import asyncio
25+
import logging
2526
from ctypes import c_int
2627
from babble_model_loader import *
2728
from camera_widget import CameraWidget
@@ -111,6 +112,9 @@ async def async_main():
111112

112113
# Run the update check
113114
await check_for_updates(config, notification_manager)
115+
116+
# Uncomment for low-level Vive Facial Tracker logging
117+
# logging.basicConfig(filename='BabbleApp.log', filemode='w', encoding='utf-8', level=logging.INFO)
114118

115119
cancellation_event = threading.Event()
116120
ROSC = False

BabbleApp/camera.py

+81-24
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
import numpy as np
44
import queue
55
import serial
6-
import serial.tools.list_ports
7-
import threading
6+
import sys
87
import time
8+
import traceback
9+
import threading
10+
from enum import Enum
11+
import serial.tools.list_ports
912
from lang_manager import LocaleStringManager as lang
1013

1114
from colorama import Fore
1215
from config import BabbleConfig, BabbleSettingsConfig
13-
from utils.misc_utils import get_camera_index_by_name, list_camera_names, is_nt
14-
from enum import Enum
15-
import sys
16+
from utils.misc_utils import get_camera_index_by_name, list_camera_names
17+
18+
from vivefacialtracker.vivetracker import ViveTracker
19+
from vivefacialtracker.camera_controller import FTCameraController
1620

1721
WAIT_TIME = 0.1
1822
BUFFER_SIZE = 32768
@@ -24,6 +28,7 @@
2428
# packet (packet-size bytes)
2529
ETVR_HEADER = b"\xff\xa0\xff\xa1"
2630
ETVR_HEADER_LEN = 6
31+
PORTS = ("COM", "/dev/ttyACM")
2732

2833

2934
class CameraState(Enum):
@@ -54,13 +59,15 @@ def __init__(
5459
self.cancellation_event = cancellation_event
5560
self.current_capture_source = config.capture_source
5661
self.cv2_camera: "cv2.VideoCapture" = None
62+
self.vft_camera: FTCameraController = None
5763

5864
self.serial_connection = None
5965
self.last_frame_time = time.time()
6066
self.fps = 0
6167
self.bps = 0
6268
self.start = True
6369
self.buffer = b""
70+
self.frame_number = 0
6471
self.FRAME_SIZE = [0, 0]
6572

6673
self.error_message = f'{Fore.YELLOW}[{lang._instance.get_string("log.warn")}] {lang._instance.get_string("info.enterCaptureOne")} {{}} {lang._instance.get_string("info.enterCaptureTwo")}{Fore.RESET}'
@@ -78,6 +85,8 @@ def run(self):
7885
print(
7986
f'{Fore.CYAN}[{lang._instance.get_string("log.info")}] {lang._instance.get_string("info.exitCaptureThread")}{Fore.RESET}'
8087
)
88+
if self.vft_camera is not None:
89+
self.vft_camera.close()
8190
return
8291
should_push = True
8392
# If things aren't open, retry until they are. Don't let read requests come in any earlier
@@ -86,8 +95,15 @@ def run(self):
8695
self.config.capture_source is not None
8796
and self.config.capture_source != ""
8897
):
89-
ports = ("COM", "/dev/ttyACM")
90-
if any(x in str(self.config.capture_source) for x in ports):
98+
isSerial = any(x in str(self.config.capture_source) for x in PORTS)
99+
100+
if isSerial:
101+
if self.cv2_camera is not None:
102+
self.cv2_camera.release()
103+
self.cv2_camera = None
104+
if self.vft_camera is not None:
105+
self.vft_camera.close()
106+
self.device_is_vft = False;
91107
if (
92108
self.serial_connection is None
93109
or self.camera_status == CameraState.DISCONNECTED
@@ -96,25 +112,51 @@ def run(self):
96112
port = self.config.capture_source
97113
self.current_capture_source = port
98114
self.start_serial_connection(port)
99-
else:
100-
if (
115+
elif ViveTracker.is_device_vive_tracker(self.config.capture_source):
116+
if self.cv2_camera is not None:
117+
self.cv2_camera.release()
118+
self.cv2_camera = None
119+
self.device_is_vft = True;
120+
121+
if self.vft_camera is None:
122+
print(self.error_message.format(self.config.capture_source))
123+
# capture_source is an index into a list of devices, so it should be treated as such
124+
if self.cancellation_event.wait(WAIT_TIME):
125+
return
126+
try:
127+
# Only create the camera once, reuse it
128+
self.vft_camera = FTCameraController(get_camera_index_by_name(self.config.capture_source))
129+
self.vft_camera.open()
130+
should_push = False
131+
except Exception:
132+
print(traceback.format_exc())
133+
if self.vft_camera is not None:
134+
self.vft_camera.close()
135+
else:
136+
# If the camera is already open it don't spam it!!
137+
if (not self.vft_camera.is_open):
138+
self.vft_camera.open()
139+
should_push = False
140+
elif (
101141
self.cv2_camera is None
102142
or not self.cv2_camera.isOpened()
103143
or self.camera_status == CameraState.DISCONNECTED
104-
or self.config.capture_source != self.current_capture_source
144+
or get_camera_index_by_name(self.config.capture_source) != self.current_capture_source
105145
):
146+
if self.vft_camera is not None:
147+
self.vft_camera.close()
148+
self.device_is_vft = False;
149+
106150
print(self.error_message.format(self.config.capture_source))
107151
# This requires a wait, otherwise we can error and possible screw up the camera
108152
# firmware. Fickle things.
109153
if self.cancellation_event.wait(WAIT_TIME):
110154
return
111-
155+
112156
if self.config.capture_source not in self.camera_list:
113157
self.current_capture_source = self.config.capture_source
114158
else:
115-
self.current_capture_source = get_camera_index_by_name(
116-
self.config.capture_source
117-
)
159+
self.current_capture_source = get_camera_index_by_name(self.config.capture_source)
118160

119161
if self.config.use_ffmpeg:
120162
self.cv2_camera = cv2.VideoCapture(
@@ -139,9 +181,12 @@ def run(self):
139181
self.cv2_camera.set(
140182
cv2.CAP_PROP_FPS, self.settings.gui_cam_framerate
141183
)
184+
142185
should_push = False
143186
else:
144187
# We don't have a capture source to try yet, wait for one to show up in the GUI.
188+
if self.vft_camera is not None:
189+
self.vft_camera.close()
145190
if self.cancellation_event.wait(WAIT_TIME):
146191
self.camera_status = CameraState.DISCONNECTED
147192
return
@@ -151,24 +196,35 @@ def run(self):
151196
if should_push and not self.capture_event.wait(timeout=0.02):
152197
continue
153198
if self.config.capture_source is not None:
154-
ports = ("COM", "/dev/ttyACM")
155-
if any(x in str(self.config.capture_source) for x in ports):
199+
if isSerial:
156200
self.get_serial_camera_picture(should_push)
157201
else:
158202
self.__del__()
159-
self.get_cv2_camera_picture(should_push)
203+
self.get_camera_picture(should_push)
160204
if not should_push:
161205
# if we get all the way down here, consider ourselves connected
162206
self.camera_status = CameraState.CONNECTED
163207

164-
def get_cv2_camera_picture(self, should_push):
208+
def get_camera_picture(self, should_push):
165209
try:
166-
ret, image = self.cv2_camera.read()
167-
if not ret:
168-
self.cv2_camera.set(cv2.CAP_PROP_POS_FRAMES, 0)
169-
raise RuntimeError(lang._instance.get_string("error.frame"))
210+
image = None
211+
# Is the current camera a Vive Facial Tracker and have we opened a connection to it before?
212+
if self.vft_camera is not None and self.device_is_vft:
213+
image = self.vft_camera.get_image()
214+
if image is None:
215+
return
216+
self.frame_number = self.frame_number + 1
217+
elif self.cv2_camera is not None and self.cv2_camera.isOpened():
218+
ret, image = self.cv2_camera.read()
219+
if not ret:
220+
self.cv2_camera.set(cv2.CAP_PROP_POS_FRAMES, 0)
221+
raise RuntimeError(lang._instance.get_string("error.frame"))
222+
self.frame_number = self.cv2_camera.get(cv2.CAP_PROP_POS_FRAMES) + 1
223+
else:
224+
# Switching from a Vive Facial Tracker to a CV2 camera
225+
return
226+
170227
self.FRAME_SIZE = image.shape
171-
frame_number = self.cv2_camera.get(cv2.CAP_PROP_POS_FRAMES)
172228
# Calculate FPS
173229
current_frame_time = time.time() # Should be using "time.perf_counter()", not worth ~3x cycles?
174230
delta_time = current_frame_time - self.last_frame_time
@@ -179,8 +235,9 @@ def get_cv2_camera_picture(self, should_push):
179235
self.bps = image.nbytes * self.fps
180236

181237
if should_push:
182-
self.push_image_to_queue(image, frame_number + 1, self.fps)
238+
self.push_image_to_queue(image, self.frame_number, self.fps)
183239
except Exception:
240+
FTCameraController._logger.exception("get_image")
184241
print(
185242
f'{Fore.YELLOW}[{lang._instance.get_string("log.warn")}] {lang._instance.get_string("warn.captureProblem")}{Fore.RESET}'
186243
)

BabbleApp/camera_widget.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,7 @@ def render(self, window, event, values):
323323
if any(x in str(value) for x in ports):
324324
self.config.capture_source = value
325325
else:
326-
cam = get_camera_index_by_name(value) # Set capture_source to the UVC index. Otherwise treat value like an ipcam if we return none
327-
if cam != None:
328-
self.config.capture_source = cam
329-
elif is_valid_int_input(value):
326+
if is_valid_int_input(value):
330327
self.config.capture_source = int(value)
331328
else:
332329
self.config.capture_source = value

BabbleApp/utils/misc_utils.py

+10-15
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,16 @@ def is_uvc_device(device):
7171
def list_linux_uvc_devices():
7272
"""List UVC video devices on Linux (excluding metadata devices)"""
7373
try:
74-
result = subprocess.run(["v4l2-ctl", "--list-devices"], stdout=subprocess.PIPE)
75-
output = result.stdout.decode("utf-8")
76-
77-
lines = output.splitlines()
74+
# v4l2-ctl --list-devices breaks if video devices are non-sequential.
75+
# So this might be better?
76+
result = glob.glob("/dev/video*");
7877
devices = []
7978
current_device = None
80-
for line in lines:
81-
if not line.startswith("\t"):
82-
current_device = line.strip()
83-
else:
84-
if "/dev/video" in line and is_uvc_device(line.strip()):
85-
devices.append(
86-
line.strip()
87-
) # We return the path like '/dev/video0'
79+
for line in result:
80+
if is_uvc_device(line):
81+
devices.append(
82+
line
83+
) # We return the path like '/dev/video0'
8884

8985
return devices
9086

@@ -147,10 +143,9 @@ def get_camera_index_by_name(name):
147143
cam_list = list_camera_names()
148144

149145
# On Linux, we use device paths like '/dev/video0' and match directly
146+
# OpenCV expects the actual /dev/video#, not the offset into the device list
150147
if os_type == "Linux":
151-
for i, device_path in enumerate(cam_list):
152-
if device_path == name:
153-
return i
148+
return int(str.replace(name,"/dev/video",""));
154149

155150
# On Windows, match by camera name
156151
elif is_nt:

0 commit comments

Comments
 (0)