From 7892398b49e68b9749deafb1dc657d370fde4ea2 Mon Sep 17 00:00:00 2001 From: Tilman Daab <tilman.daab@kit.edu> Date: Tue, 26 Nov 2024 10:31:48 +0100 Subject: [PATCH] add tool to mirror trajectories (currently for ARMAR-6) --- armarx_control/utils/mirror_trajectory.py | 197 ++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 armarx_control/utils/mirror_trajectory.py diff --git a/armarx_control/utils/mirror_trajectory.py b/armarx_control/utils/mirror_trajectory.py new file mode 100644 index 00000000..01213eb7 --- /dev/null +++ b/armarx_control/utils/mirror_trajectory.py @@ -0,0 +1,197 @@ +import csv +import os.path +import sys + +from enum import Enum, auto +from typing import List, Tuple +import numpy as np + +from scipy.spatial.transform import Rotation + + +class ConversionDirection(Enum): + """ + From which arm to which arm to convert + """ + LEFT_TO_RIGHT = auto() + RIGHT_TO_LEFT = auto() + + +class Space(Enum): + """ + Space in which the trajectory is represented (joint space or task space, i.e., end effector pose) + """ + JOINT_SPACE = auto() + TASK_SPACE = auto() + + +def resolve_path( + source_path_string: str, conversion_direction: ConversionDirection or None = None +) -> Tuple[str, str, ConversionDirection]: + """ + Resolve path without having this script depending on ArmarX (which would offer the ArmarX package path); + e.g., use $ARMARX_WORKSPACE or even the package dir variable. + The function also detects in which direction to convert, and determines the target path. + + :param source_path_string: Path to the csv file to be converted, + may contain environment variables like $ARMARX_WORKSPACE + :param conversion_direction: LEFT_TO_RIGHT or RIGHT_TO_LEFT, or None for auto-detection based on 'LeftArm' and + 'RightArm' in the source_path + :return: [source path, target path, conversion direction] + """ + source_path = os.path.expandvars(source_path_string) + + if 'LeftArm' in source_path: + if conversion_direction is None: + conversion_direction = ConversionDirection.LEFT_TO_RIGHT + target_path = source_path.replace('LeftArm', 'RightArm') + elif 'RightArm' in source_path: + if conversion_direction is None: + conversion_direction = ConversionDirection.RIGHT_TO_LEFT + target_path = source_path.replace('RightArm', 'LeftArm') + else: + assert conversion_direction is not None, 'Could not detect whether to convert from left to right, or vice '\ + 'versa; please specify manually' + assert source_path.endswith('.csv') + if conversion_direction is ConversionDirection.LEFT_TO_RIGHT: + target_path = source_path.replace('.csv', '-right.csv') + elif conversion_direction is ConversionDirection.RIGHT_TO_LEFT: + target_path = source_path.replace('.csv', '-left.csv') + else: + raise KeyError + return source_path, target_path, conversion_direction + + +def detect_space(header: List[str]): + """ + Evaluate the header of the csv file to detect the space + :param header: column labels + :return: space in which the poses are represented + """ + if header == ['timestamp', 'x', 'y', 'z', 'qw', 'qx', 'qy', 'qz']: + space = Space.TASK_SPACE + # TODO: potentially extend the detection code for joint space + elif header[1] == 'ArmL1_Cla1' or header[1] == 'ArmR1_Cla1': + space = Space.JOINT_SPACE + else: + raise RuntimeError('Could not detect the space') + return space + + +def pose7_to_matrix(pose7: np.ndarray) -> np.ndarray: + """ + convert the x/y/z/qw/qx/qy/qz task space pose to a homogeneous matrix + :param pose7: pose in task space as x/y/z/qw/qx/qy/qz + :return: Pose in task space as homogeneous matrix + """ + position = np.array(pose7[:3]) + orientation = Rotation.from_quat(pose7[3:7], scalar_first=True) + pose = np.identity(4) + pose[:3, 3] = position + pose[:3, :3] = orientation.as_matrix() + return pose + + +def matrix_to_pose7(matrix: np.ndarray) -> np.ndarray: + """ + convert the task space pose provided as a homogeneous matrix to a x/y/z/qw/qx/qy/qz task space pose + :param matrix: Pose in task space as homogeneous matrix + :return: pose in task space as x/y/z/qw/qx/qy/qz + """ + assert matrix.shape == (4, 4) + pose7 = np.empty(7) + pose7[:3] = matrix[:3, 3] + orientation = Rotation.from_matrix(matrix[:3, :3]) + pose7[3:] = orientation.as_quat(scalar_first=True) + return pose7 + + +def mirror_pose(line: List[float], space: Space, conversion_direction: ConversionDirection): + """ + Convert an entry of the trajectory csv file, corresponding to a pose + :param line: entries of the csv line + :param space: space in which the poses are represented + :param conversion_direction: from which arm to which one to convert + :return: modified entries of the csv line, corresponding to the mirrored pose + """ + converted_line = [] + # timestamp remains unchanged + converted_line.append(line[0]) + if space == Space.TASK_SPACE: + pose = pose7_to_matrix(line[1:]) + coeff_wise_factors = np.array([ + [1, -1, -1, -1], + [-1, 1, 1, 1], + [-1, 1, 1, 1], + [1, 1, 1, 1] + ], dtype=float) + # on purpose coefficient-wise, and not a matrix multiplication + converted_pose = coeff_wise_factors * pose + converted_line += list(matrix_to_pose7(converted_pose)) + elif space == Space.JOINT_SPACE: + # for ARMAR-6, determined using RobotViewer + coeff_wise_factors = np.array([ + -1, # Cla + -1, # Sho1 + +1, # Sho2 + -1, # Sho3 + +1, # Elb1 + -1, # Elb2 + -1, # Wri1 + +1 # Wri2 + ], dtype=float) + converted_line += list(coeff_wise_factors * np.array(line[1:], dtype=float)) + else: + raise TypeError + return converted_line + + +def mirror_header(header: List[str], conversion_direction: ConversionDirection) -> List[str]: + """ + Adapt the header of the csv file. For task space trajectories, nothing changes. For joint space trajectories, the + joint names might need to be adapted. Here, it is assumed that all "L" and "R" characters are to be swapped in these + joint names, which is a hard-coded solution for the given robots. + TODO: extend if needed + :param header: column labels + :param conversion_direction: from which arm to which one to convert + :return: column labels for mirrored motion + """ + if conversion_direction is conversion_direction.LEFT_TO_RIGHT: + converted_header = [entry.replace('L', 'R') for entry in header] + elif conversion_direction is conversion_direction.RIGHT_TO_LEFT: + converted_header = [entry.replace('R', 'L') for entry in header] + else: + KeyError + return converted_header + + +def main(path: str, conversion_direction: ConversionDirection or None = None): + source_path, target_path, conversion_direction = resolve_path(path, conversion_direction) + print(f'converting from\n\t{source_path}\nto\n\t{target_path}') + converted_lines = [] + assert os.path.isfile(source_path), f'Input file does not exist: {source_path}' + with open(source_path, 'r') as f: + reader = csv.reader(f) + header = next(reader) + space = detect_space(header) + for line in reader: + converted_lines.append(mirror_pose(line, space, conversion_direction)) + + converted_header = mirror_header(header, conversion_direction) + with open(target_path, 'w') as f: + writer = csv.writer(f) + f.write(','.join(converted_header) + '\n') + writer.writerows(converted_lines) + + +if __name__ == '__main__': + if len(sys.argv) > 1: + # use command line parameter + path = sys.argv[1] + else: + dir_path = '$ARMARX_WORKSPACE/armarx_integration/robots/armar7/motions/data/armar7_motions/mobile_kitchen/RightArm/' + file_name_base = 'through_door_pose_arm_primary_lower_01' + for suffix in ['-ts-forward', '-ts-backward', '-update-js-forward', '-update-js-backward']: + file_name = file_name_base + suffix + '.csv' + path = os.path.join(dir_path, file_name) + main(path, None) -- GitLab