You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
11 KiB
289 lines
11 KiB
from copy import deepcopy
|
|
import pickle
|
|
import queue
|
|
import threading
|
|
import time
|
|
from typing import Dict, List, Tuple
|
|
|
|
import leap
|
|
import leap.events
|
|
import numpy as np
|
|
from scipy.spatial.transform import Rotation as R
|
|
|
|
from decoupled_wbc.control.teleop.streamers.base_streamer import BaseStreamer, StreamerOutput
|
|
|
|
|
|
def xyz2np_array(position: leap.datatypes.Vector) -> np.ndarray:
|
|
return np.array([position.x, position.y, position.z])
|
|
|
|
|
|
def quat2np_array(quaternion: leap.datatypes.Quaternion) -> np.ndarray:
|
|
return np.array([quaternion.x, quaternion.y, quaternion.z, quaternion.w])
|
|
|
|
|
|
def get_raw_finger_points(hand: leap.datatypes.Hand) -> np.ndarray:
|
|
target_points = np.array([np.eye(4) for _ in range(25)])
|
|
target_points[0, :3, :3] = R.from_quat(quat2np_array(hand.palm.orientation)).as_matrix()
|
|
target_points[0, :3, 3] = xyz2np_array(hand.arm.next_joint)
|
|
target_points[1, :3, 3] = xyz2np_array(hand.thumb.bones[0].next_joint)
|
|
target_points[2, :3, 3] = xyz2np_array(hand.thumb.bones[1].next_joint)
|
|
target_points[3, :3, 3] = xyz2np_array(hand.thumb.bones[2].next_joint)
|
|
target_points[4, :3, 3] = xyz2np_array(hand.thumb.bones[3].next_joint)
|
|
target_points[5, :3, 3] = xyz2np_array(hand.index.bones[0].prev_joint)
|
|
target_points[6, :3, 3] = xyz2np_array(hand.index.bones[0].next_joint)
|
|
target_points[7, :3, 3] = xyz2np_array(hand.index.bones[1].next_joint)
|
|
target_points[8, :3, 3] = xyz2np_array(hand.index.bones[2].next_joint)
|
|
target_points[9, :3, 3] = xyz2np_array(hand.index.bones[3].next_joint)
|
|
target_points[10, :3, 3] = xyz2np_array(hand.middle.bones[0].prev_joint)
|
|
target_points[11, :3, 3] = xyz2np_array(hand.middle.bones[0].next_joint)
|
|
target_points[12, :3, 3] = xyz2np_array(hand.middle.bones[1].next_joint)
|
|
target_points[13, :3, 3] = xyz2np_array(hand.middle.bones[2].next_joint)
|
|
target_points[14, :3, 3] = xyz2np_array(hand.middle.bones[3].next_joint)
|
|
target_points[15, :3, 3] = xyz2np_array(hand.ring.bones[0].prev_joint)
|
|
target_points[16, :3, 3] = xyz2np_array(hand.ring.bones[0].next_joint)
|
|
target_points[17, :3, 3] = xyz2np_array(hand.ring.bones[1].next_joint)
|
|
target_points[18, :3, 3] = xyz2np_array(hand.ring.bones[2].next_joint)
|
|
target_points[19, :3, 3] = xyz2np_array(hand.ring.bones[3].next_joint)
|
|
target_points[20, :3, 3] = xyz2np_array(hand.pinky.bones[0].prev_joint)
|
|
target_points[21, :3, 3] = xyz2np_array(hand.pinky.bones[0].next_joint)
|
|
target_points[22, :3, 3] = xyz2np_array(hand.pinky.bones[1].next_joint)
|
|
target_points[23, :3, 3] = xyz2np_array(hand.pinky.bones[2].next_joint)
|
|
target_points[24, :3, 3] = xyz2np_array(hand.pinky.bones[3].next_joint)
|
|
|
|
return target_points
|
|
|
|
|
|
def get_fake_finger_points_from_pinch(hand: leap.datatypes.Hand) -> np.ndarray:
|
|
# print(hand.pinch_strength)
|
|
target_points = np.array([np.eye(4) for _ in range(25)])
|
|
for i in range(25):
|
|
target_points[i] = np.eye(4)
|
|
|
|
# Control thumb based on shoulder button state (index 4 is thumb tip)
|
|
if hand.pinch_strength > 0.3:
|
|
target_points[4, 0, 3] = 0.0 # closed
|
|
else:
|
|
target_points[4, 0, 3] = 1000.0 # open, in mm
|
|
|
|
return target_points
|
|
|
|
|
|
def get_raw_wrist_pose(hand: leap.datatypes.Hand) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
hand_palm_position = xyz2np_array(hand.palm.position)
|
|
hand_palm_normal = np.array([-hand.palm.normal.z, -hand.palm.normal.x, hand.palm.normal.y])
|
|
hand_palm_direction = np.array(
|
|
[-hand.palm.direction.z, -hand.palm.direction.x, hand.palm.direction.y]
|
|
)
|
|
|
|
return hand_palm_position, hand_palm_normal, hand_palm_direction
|
|
|
|
|
|
def get_finger_transform(target_points: np.ndarray, hand_type: str) -> np.ndarray:
|
|
pose_palm_root = target_points[0, :3, 3].copy()
|
|
rot_palm = target_points[0, :3, :3].copy()
|
|
|
|
if hand_type == "right":
|
|
rot_leap2base = R.from_euler("z", [180], degrees=True).as_matrix()
|
|
rot_reverse = np.array(
|
|
[[[0, 0, 1], [0, -1, 0], [1, 0, 0]]]
|
|
) # due to the target_base_rotation in hand IK solver
|
|
else:
|
|
rot_leap2base = np.eye(3)
|
|
rot_reverse = np.array([[[0, 0, -1], [0, -1, 0], [-1, 0, 0]]])
|
|
offset = (target_points[:, :3, 3] - pose_palm_root).copy() / 1000.0 # mm to m
|
|
offset = offset @ rot_palm @ rot_leap2base @ rot_reverse
|
|
|
|
target_points[:, :3, 3] = offset
|
|
|
|
return target_points
|
|
|
|
|
|
def get_wrist_transformation(
|
|
hand_palm_position: np.ndarray,
|
|
hand_palm_normal: np.ndarray,
|
|
hand_palm_direction: np.ndarray,
|
|
hand_type: str,
|
|
pos_sensitivity: float = 1.0 / 800.0,
|
|
) -> np.ndarray:
|
|
T = np.eye(4)
|
|
T[:3, 3] = hand_palm_position[[2, 0, 1]] * pos_sensitivity * np.array([-1, -1, 1])
|
|
direction_np = hand_palm_direction
|
|
palm_normal_np = hand_palm_normal
|
|
if hand_type == "left":
|
|
transform = R.from_euler("y", -90, degrees=True).as_matrix()
|
|
lh_thumb_np = -np.cross(direction_np, palm_normal_np)
|
|
rotation_matrix = np.array([direction_np, -palm_normal_np, lh_thumb_np]).T
|
|
else:
|
|
transform = R.from_euler("xy", [-180, 90], degrees=True).as_matrix()
|
|
rh_thumb_np = np.cross(direction_np, palm_normal_np)
|
|
rotation_matrix = np.array([direction_np, palm_normal_np, rh_thumb_np]).T
|
|
T[:3, :3] = np.dot(rotation_matrix, transform)
|
|
return T
|
|
|
|
|
|
def get_raw_data(
|
|
hands: List[leap.datatypes.Hand], data: Dict[str, np.ndarray] = None
|
|
) -> Dict[str, np.ndarray]:
|
|
if data is None:
|
|
data = {}
|
|
for hand in hands:
|
|
hand_type = "left" if str(hand.type) == "HandType.Left" else "right"
|
|
data[hand_type + "_wrist"] = get_raw_wrist_pose(hand)
|
|
# @runyud: this is a hack to get the finger points from the pinch strength
|
|
data[hand_type + "_fingers"] = get_fake_finger_points_from_pinch(hand)
|
|
assert len(data) == 4, f"Leapmotiondata length should be 4, but got {len(data)}"
|
|
return data
|
|
|
|
|
|
def process_data(raw_data: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:
|
|
data = {}
|
|
for hand_type in ["left", "right"]:
|
|
data[hand_type + "_wrist"] = get_wrist_transformation(
|
|
*raw_data[hand_type + "_wrist"], hand_type
|
|
)
|
|
data[hand_type + "_fingers"] = {
|
|
"position": get_finger_transform(raw_data[hand_type + "_fingers"], hand_type)
|
|
}
|
|
return data
|
|
|
|
|
|
class LeapMotionListener(leap.Listener):
|
|
def __init__(self, verbose=False):
|
|
self.data = None
|
|
self.data_lock = threading.Lock()
|
|
self.verbose = verbose
|
|
|
|
def on_connection_event(self, event):
|
|
print("Connected")
|
|
|
|
def on_device_event(self, event: leap.events.DeviceEvent):
|
|
try:
|
|
with event.device.open():
|
|
info = event.device.get_info()
|
|
except AttributeError:
|
|
# Handle the case where LeapCannotOpenDeviceError is not available
|
|
try:
|
|
info = event.device.get_info()
|
|
except Exception as e:
|
|
print(f"Error opening device: {e}")
|
|
info = None
|
|
|
|
print(f"Found Leap Motion device {info.serial}")
|
|
|
|
def on_tracking_event(self, event: leap.events.TrackingEvent):
|
|
if (
|
|
len(event.hands) == 2 or self.data is not None
|
|
): # only when two hands are detected, we update the data
|
|
with self.data_lock:
|
|
self.data: Dict[str, np.ndarray] = get_raw_data(event.hands, self.data)
|
|
if self.verbose:
|
|
for hand in event.hands:
|
|
hand_type = "left" if str(hand.type) == "HandType.Left" else "right"
|
|
print(
|
|
f"Hand id {hand.id} is a {hand_type} hand with position"
|
|
f"({hand.palm.position.x}, {hand.palm.position.y}, {hand.palm.position.z})."
|
|
)
|
|
|
|
def get_data(self):
|
|
with self.data_lock:
|
|
return deepcopy(self.data)
|
|
|
|
def reset_status(self):
|
|
"""Reset the cache of the streamer."""
|
|
with self.data_lock:
|
|
self.data = None
|
|
|
|
|
|
class FakeLeapMotionListener:
|
|
def __init__(self, data_path=None):
|
|
self.data_lock = threading.Lock()
|
|
with open(data_path, "rb") as f:
|
|
self.data_list = pickle.load(f)
|
|
self.data_index = 0
|
|
|
|
def get_data(self) -> Dict[str, np.ndarray]:
|
|
with self.data_lock:
|
|
data = deepcopy(self.data_list[self.data_index % len(self.data_list)])
|
|
self.data_index += 1
|
|
return data
|
|
|
|
|
|
# Note currently it is a auto-polling based streamer.
|
|
# The connection will start another thread to continue polling data
|
|
# and the listener will continue to receive data from the connection.
|
|
class LeapMotionStreamer(BaseStreamer):
|
|
"""LeapMotion streamer that provides hand tracking data."""
|
|
|
|
def __init__(self, verbose=False, record_data=False, **kwargs):
|
|
self.connection = leap.Connection()
|
|
self.listener = LeapMotionListener(verbose=verbose)
|
|
self.connection.add_listener(self.listener)
|
|
# self.listener = FakeLeapMotionListener(data_path="leapmotion_data_rot.pkl")
|
|
|
|
self.connection.set_tracking_mode(leap.TrackingMode.Desktop)
|
|
|
|
self.record_data = record_data
|
|
self.data_queue = queue.Queue()
|
|
|
|
def reset_status(self):
|
|
"""Reset the cache of the streamer."""
|
|
self.listener.reset_status()
|
|
while not self.listener.get_data():
|
|
time.sleep(0.1)
|
|
|
|
def start_streaming(self):
|
|
self.connection.connect()
|
|
print("Waiting for the first data...")
|
|
time.sleep(0.5)
|
|
while not self.listener.get_data():
|
|
time.sleep(0.1)
|
|
print("First data received!")
|
|
|
|
def stop_streaming(self):
|
|
self.connection.disconnect()
|
|
|
|
def get(self) -> StreamerOutput:
|
|
"""Return hand tracking data as StreamerOutput."""
|
|
# Get raw data and save if recording
|
|
raw_data = self.listener.get_data()
|
|
if self.record_data:
|
|
self.data_queue.put(raw_data)
|
|
|
|
# Process raw data into transformations
|
|
processed_data = process_data(raw_data)
|
|
|
|
# Initialize IK data (ik_keys) - LeapMotion provides hand/finger tracking
|
|
ik_data = {}
|
|
for hand_type in ["left", "right"]:
|
|
# Add wrist poses and finger positions to IK data
|
|
ik_data[f"{hand_type}_wrist"] = processed_data[f"{hand_type}_wrist"]
|
|
ik_data[f"{hand_type}_fingers"] = processed_data[f"{hand_type}_fingers"]
|
|
|
|
# Return structured output - LeapMotion only provides IK data
|
|
return StreamerOutput(
|
|
ik_data=ik_data,
|
|
control_data={}, # No control commands from LeapMotion
|
|
teleop_data={}, # No teleop commands from LeapMotion
|
|
source="leapmotion",
|
|
)
|
|
|
|
def dump_data_to_file(self):
|
|
"""Save recorded data to file if recording was enabled."""
|
|
if not self.record_data:
|
|
return
|
|
|
|
data_list = []
|
|
while not self.data_queue.empty():
|
|
data_list.append(self.data_queue.get())
|
|
with open("leapmotion_data_trans.pkl", "wb") as f:
|
|
pickle.dump(data_list, f)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
streamer = LeapMotionStreamer(verbose=True)
|
|
streamer.start_streaming()
|
|
for _ in range(100):
|
|
streamer.get()
|
|
time.sleep(0.1)
|
|
streamer.stop_streaming()
|
|
streamer.dump_data_to_file()
|