Source code for seqikpy.leg_inverse_kinematics

""" Module to calculate inverse kinematics from 3D pose based on IKPy."""

from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, Tuple, Union, Literal, Optional
import logging
import warnings

import numpy as np
from tqdm import trange
from ikpy.chain import Chain

from seqikpy.utils import save_file
from seqikpy.data import INITIAL_ANGLES
from seqikpy.kinematic_chain import (
    KinematicChainBase,
    KinematicChainSeq,
    KinematicChainGeneric,
)

# Ignore the warnings
warnings.filterwarnings("ignore")

logging.basicConfig(
    format=" %(asctime)s - %(levelname)s- %(message)s",
    handlers=[logging.StreamHandler()],
)


[docs] class LegInvKinBase(ABC): """Abstract class to calculate inverse kinematics for leg joints. Parameters ---------- aligned_pos : Dict[str, np.ndarray] Aligned pose from the AlignPose class. Should have the following structure >>> pose_data_dict = { "<side><segment>_leg": np.ndarray[N_frames,N_key_points,3], } kinematic_chain : KinematicChainBase Kinematic chain of the leg. initial_angles : Dict[str, np.ndarray], optional Initial angles of DOFs. If not provided, the default values from data.py will be used. log_level : Literal["DEBUG", "INFO", "WARNING", "ERROR"], optional Logging level as a string, by default "INFO" """ def __init__( self, aligned_pos: Dict[str, np.ndarray], kinematic_chain_class: KinematicChainBase, initial_angles: Optional[Dict[str, np.ndarray]] = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO", ) -> None: self.aligned_pos = aligned_pos self.kinematic_chain_class = kinematic_chain_class self.initial_angles = ( INITIAL_ANGLES if initial_angles is None else initial_angles ) # Get the logger of the module self.logger = logging.getLogger(self.__class__.__name__) numeric_level = getattr(logging, log_level.upper(), None) self.logger.setLevel(numeric_level)
[docs] def calculate_ik( self, kinematic_chain: Chain, target_pos: np.ndarray, initial_angles: np.ndarray = None, ) -> np.ndarray: """Calculates the joint angles in the leg chain.""" # don"t take the last and first ones into account return kinematic_chain.inverse_kinematics( target_position=target_pos, initial_position=initial_angles )
[docs] def calculate_fk( self, kinematic_chain: Chain, joint_angles: np.ndarray ) -> np.ndarray: """Calculates the forward kinematics from the joint dof angles.""" fk = kinematic_chain.forward_kinematics(joint_angles, full_kinematics=True) end_effector_positions = np.zeros((len(kinematic_chain.links), 3)) for link in range(len(kinematic_chain.links)): end_effector_positions[link, :] = fk[link][:3, 3] return end_effector_positions
[docs] def get_scale_factor(self, vector: np.ndarray, length: float) -> float: """Gets scale the ratio between two vectors.""" vector_diff = np.linalg.norm(np.diff(vector, axis=0), axis=1) norm_sum = np.sum(vector_diff) return length / norm_sum
[docs] @abstractmethod def calculate_ik_stage( self, end_effector_pos: np.ndarray, origin: np.ndarray, initial_angles: np.ndarray, segment_name: str, **kwargs, ) -> np.ndarray: """For a given trial pose data, calculates the inverse kinematics. Parameters ---------- end_effector_pos : np.ndarray 3D array containing the position of the end effector pos origin : np.ndarray Origin of the kinematic chain, i.e., Thorax-Coxa joint initial_angles : np.ndarray Initial angles for the optimization seed segment_name : str Leg side, i.e., RF, LF, RM, LM, RH, LH Returns ------- np.ndarray Array containing the cartesian coordinates of the joint positions. The joint angles are saved in a class attribute """
[docs] @abstractmethod def run_ik_and_fk( self, export_path: Union[Path, str] = None, **kwargs ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]: """Runs inverse and forward kinematics for leg joints. Parameters ---------- export_path : Union[Path, str], optional Path where the results will be saved, if None, nothing is saveed, by default None Returns ------- Tuple[Dict[str,np.ndarray], Dict[str,np.ndarray]] Two dictionaries containing joint angles and forward kinematics, respectively. """
[docs] class LegInvKinSeq(LegInvKinBase): """Class to calculate inverse kinematics for leg joints in a sequential manner. This method finds the optimal joint angles within the bounds to match each koint as closely as possible. Parameters ---------- aligned_pos : Dict[str, np.ndarray] Aligned pose from the AlignPose class. Should have the following structure >>> pose_data_dict = { "<side><segment>_leg": np.ndarray[N_frames,N_key_points,3], } kinematic_chain : KinematicChainSeq Kinematic chain of the leg. initial_angles : Dict[str, np.ndarray], optional Initial angles of DOFs. If not provided, the default values from data.py will be used. log_level : Literal["DEBUG", "INFO", "WARNING", "ERROR"], optional Logging level as a string, by default "INFO" Examples -------- >>> from pathlib import Path >>> from seqikpy.kinematic_chain import KinematicChainSeq >>> from seqikpy.leg_inverse_kinematics import LegInvKinSeq >>> from seqikpy.data import BOUNDS, INITIAL_ANGLES >>> from seqikpy.utils import load_file >>> DATA_PATH = Path("../data/anipose_220525_aJO_Fly001_001/pose-3d") >>> f_path = DATA_PATH / "pose3d_aligned.pkl" >>> aligned_pos = load_file(f_path) >>> seq_ik = LegInvKinSeq( aligned_pos=aligned_pos, kinematic_chain_class=KinematicChainSeq( bounds_dof=BOUNDS, legs_list=["RF", "LF"], body_size=None, ), initial_angles=INITIAL_ANGLES ) >>> leg_joint_angles, forward_kinematics = seq_ik.run_ik_and_fk( export_path=DATA_PATH, hide_progress_bar=False ) """ def __init__( self, aligned_pos: Dict[str, np.ndarray], kinematic_chain_class: KinematicChainSeq, initial_angles: Optional[Dict[str, np.ndarray]] = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO", ) -> None: super().__init__(aligned_pos, kinematic_chain_class, initial_angles, log_level) # Create an empty dict for joint angles self.joint_angles_dict = {}
[docs] def calculate_ik_stage( self, end_effector_pos: np.ndarray, origin: np.ndarray, initial_angles: np.ndarray, segment_name: str, **kwargs, ) -> np.ndarray: """For a given trial pose data, calculates the inverse kinematics. Parameters ---------- end_effector_pos : np.ndarray 3D array containing the position of the end effector pos origin : np.ndarray Origin of the kinematic chain, i.e., Thorax-Coxa joint initial_angles : np.ndarray Initial angles for the optimization seed segment_name (kwargs) : str Leg side, i.e., RF, LF, RM, LM, RH, LH stage (kwargs) : int Stage in the sequential calculation, should be between 1 and 4 Returns ------- np.ndarray Array containing the cartesian coordinates of the joint positions. The joint angles are saved in a class attribute """ stage = kwargs.get("stage", 1) hide_progress_bar = kwargs.get("hide_progress_bar", False) if not segment_name in ["RF", "LF", "RM", "LM", "RH", "LH"]: raise ValueError(f"Segment name ({segment_name}) is not valid.") if not 1 <= stage <= 4: raise ValueError(f"Stage ({stage}) should be between 1 and 4.") frames_no = end_effector_pos.shape[0] end_effector_pos_diff = end_effector_pos - origin # Joint angles shape: (number of frames, number of DOFs) joint_angles = np.empty((frames_no, len(initial_angles))) # Forward kinematics shape: (number of frames, number of # joints in the kinematics chain, axes-x,y,z) forward_kinematics = np.empty((frames_no, len(initial_angles), 3)) # if the origin is a vector, convert it to a matrix if origin.size == 3: origin = np.tile(origin, (frames_no, 1)) # Get the kinematic chain for Stage 1 if stage == 1: kinematic_chain = self.kinematic_chain_class.create_leg_chain( stage=stage, leg_name=segment_name, ) # Start the inverse kinematics calculation for t in trange( frames_no, disable=hide_progress_bar, desc=f"{segment_name} stage {stage}", ): # Get the kinematic chain for the other stages if stage in [2, 3, 4]: kinematic_chain = self.kinematic_chain_class.create_leg_chain( leg_name=segment_name, stage=stage, angles=self.joint_angles_dict, t=t, ) # from IPython import embed; embed() # For the first frame, use the given initial angles, for the rest # use the calculated joint angles from the previous time step initial_angles = initial_angles if t == 0 else joint_angles[t - 1, :] # Calculate the inverse kinematics joint_angles[t, :] = self.calculate_ik( kinematic_chain, end_effector_pos_diff[t, :], initial_angles ) # Calculate the forward kinematics for the last stage only if stage == 4: forward_kinematics[t, :] = ( self.calculate_fk(kinematic_chain, joint_angles[t, :]) + origin[t, :] ) # Link names link_names = [link.name for link in kinematic_chain.links] # Store the joint angles based on the stage number # Stage 1: Thorax-Coxa pitch and yaw if stage == 1: self.joint_angles_dict[f"Angle_{segment_name}_ThC_yaw"] = joint_angles[ :, link_names.index(f"{segment_name}_ThC_yaw") ] self.joint_angles_dict[f"Angle_{segment_name}_ThC_pitch"] = joint_angles[ :, link_names.index(f"{segment_name}_ThC_pitch") ] self.logger.debug("Stage 1 is completed!") # Stage 2: Thorax-Coxa roll, Coxa-Trochanter pitch elif stage == 2: self.joint_angles_dict[f"Angle_{segment_name}_ThC_roll"] = joint_angles[ :, link_names.index(f"{segment_name}_ThC_roll") ] self.joint_angles_dict[f"Angle_{segment_name}_CTr_pitch"] = joint_angles[ :, link_names.index(f"{segment_name}_CTr_pitch") ] self.logger.debug("Stage 2 is completed!") # Stage 3: Coxa-Trochanter roll, Femur-Tibia pitch elif stage == 3: self.joint_angles_dict[f"Angle_{segment_name}_CTr_roll"] = joint_angles[ :, link_names.index(f"{segment_name}_CTr_roll") ] self.joint_angles_dict[f"Angle_{segment_name}_FTi_pitch"] = joint_angles[ :, link_names.index(f"{segment_name}_FTi_pitch") ] self.logger.debug("Stage 3 is completed!") # Stage 4: Tibia-Tarsus pitch elif stage == 4: self.joint_angles_dict[f"Angle_{segment_name}_TiTa_pitch"] = joint_angles[ :, link_names.index(f"{segment_name}_TiTa_pitch") ] self.logger.debug("Stage 4 is completed!") return forward_kinematics
[docs] def run_ik_and_fk( self, export_path: Union[Path, str] = None, **kwargs ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]: """Runs inverse and forward kinematics for leg joints. Parameters ---------- export_path : Union[Path, str], optional Path where the results will be saved, if None, nothing is saveed, by default None stages (kwargs) : List[int], optional Stages to run the inverse kinematics. hide_progress_bar (kwargs) : Optional[bool], optional Hide the progress bar, by default True Returns ------- Tuple[Dict[str,np.ndarray], Dict[str,np.ndarray]] Two dictionaries containing joint angles and forward kinematics, respectively. """ stages = kwargs.get("stages", [1, 2, 3, 4]) hide_progress_bar = kwargs.get("hide_progress_bar", False) if max(stages) > 4 or not all(np.diff(stages) == 1): raise ValueError( "Maximum stage number is 4 and the list should be strictly incremental." ) forward_kinematics_dict = {} self.logger.info("Computing joint angles and forward kinematics...") for segment_name, segment_array in self.aligned_pos.items(): if "leg" in segment_name.lower(): # If segment name is RF_leg so the leg name is RF leg_name = segment_name.split("_")[0] # If leg_name is not in body_size, then continue if not leg_name in self.kinematic_chain_class.body_size: self.logger.warning( "Leg %s is not in the kinematic chain, continuing...", leg_name, ) continue # First key point of the segment array is the origin # of the kinematic chain, i.e., Thorax-Coxa joint origin = segment_array[:, 0, :] for stage in stages: # for each stage, the end effector is the corresponding joint end_effector_pos = segment_array[:, stage, :] initial_angles = self.initial_angles[leg_name][f"stage_{stage}"] forward_kinematics_dict[segment_name] = self.calculate_ik_stage( end_effector_pos=end_effector_pos, origin=origin, initial_angles=initial_angles, stage=stage, segment_name=leg_name, hide_progress_bar=hide_progress_bar, ) else: self.logger.debug( "Segment %s is not a leg, continuing...", segment_name ) continue self.logger.debug("Joint angles and forward kinematics are computed.") if export_path is not None: save_file( Path(export_path) / "forward_kinematics.pkl", forward_kinematics_dict, ) save_file( Path(export_path) / "leg_joint_angles.pkl", self.joint_angles_dict, ) self.logger.info( "Joint angles and forward kinematics are saved at %s", export_path, ) return self.joint_angles_dict, forward_kinematics_dict
[docs] class LegInvKinGeneric(LegInvKinBase): """Class to calculate inverse kinematics for leg joints in a generic manner to only match the given end effector. Parameters ---------- aligned_pos : Dict[str, np.ndarray] Aligned pose from the AlignPose class. Should have the following structure >>> pose_data_dict = { "<side><segment>_leg": np.ndarray[N_frames,N_key_points,3], } kinematic_chain : KinematicChainGeneric Kinematic chain of the leg. initial_angles : Dict[str, np.ndarray], optional Initial angles of DOFs. If not provided, the default values from data.py will be used. log_level : Literal["DEBUG", "INFO", "WARNING", "ERROR"], optional Logging level as a string, by default "INFO" Examples -------- >>> from pathlib import Path >>> from seqikpy.kinematic_chain import KinematicChainGeneric >>> from seqikpy.leg_inverse_kinematics import LegInvKinGeneric >>> from seqikpy.data import BOUNDS, INITIAL_ANGLES >>> from seqikpy.utils import load_file >>> DATA_PATH = Path("../data/anipose_220525_aJO_Fly001_001/pose-3d") >>> f_path = DATA_PATH / "pose3d_aligned.pkl" >>> aligned_pos = load_file(f_path) >>> seq_ik = LegInvKinGeneric( aligned_pos=aligned_pos, kinematic_chain_class=KinematicChainGeneric( bounds_dof=BOUNDS, legs_list=["RF", "LF"], body_size=None, ), initial_angles=INITIAL_ANGLES ) >>> leg_joint_angles, forward_kinematics = gen_ik.run_ik_and_fk( export_path=DATA_PATH, hide_progress_bar=False ) """ def __init__( self, aligned_pos: Dict[str, np.ndarray], kinematic_chain_class: KinematicChainGeneric, initial_angles: Optional[Dict[str, np.ndarray]] = None, log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO", ) -> None: super().__init__(aligned_pos, kinematic_chain_class, initial_angles, log_level) # Create an empty dict for joint angles self.joint_angles_dict = {}
[docs] def calculate_ik_stage( self, end_effector_pos: np.ndarray, origin: np.ndarray, initial_angles: np.ndarray, segment_name: str, **kwargs, ) -> np.ndarray: """For a given trial pose data, calculates the inverse kinematics. Parameters ---------- end_effector_pos : np.ndarray 3D array containing the position of the end effector pos origin : np.ndarray Origin of the kinematic chain, i.e., Thorax-Coxa joint initial_angles : np.ndarray Initial angles for the optimization seed segment_name : str Leg side, i.e., RF, LF, RM, LM, RH, LH Returns ------- np.ndarray Array containing the cartesian coordinates of the joint positions. The joint angles are saved in a class attribute """ hide_progress_bar = kwargs.get("hide_progress_bar", False) if not segment_name in ["RF", "LF", "RM", "LM", "RH", "LH"]: raise ValueError(f"Segment name ({segment_name}) is not valid.") frames_no = end_effector_pos.shape[0] end_effector_pos_diff = end_effector_pos - origin # Joint angles shape: (number of frames, number of DOFs) joint_angles = np.empty((frames_no, len(initial_angles))) # Forward kinematics shape: (number of frames, number of # joints in the kinematics chain, axes-x,y,z) forward_kinematics = np.empty((frames_no, len(initial_angles), 3)) # if the origin is a vector, convert it to a matrix if origin.size == 3: origin = np.tile(origin, (frames_no, 1)) # Generic IK, the kinematic chain is the entire leg kinematic_chain = self.kinematic_chain_class.create_leg_chain( leg_name=segment_name, ) # Start the inverse kinematics calculation for t in trange( frames_no, disable=hide_progress_bar, desc=f"Calculating IK {segment_name}", ): # For the first frame, use the given initial angles, for the rest # use the calculated joint angles from the previous time step initial_angles = initial_angles if t == 0 else joint_angles[t - 1, :] # Calculate the inverse kinematics joint_angles[t, :] = self.calculate_ik( kinematic_chain, end_effector_pos_diff[t, :], initial_angles ) forward_kinematics[t, :] = ( self.calculate_fk(kinematic_chain, joint_angles[t, :]) + origin[t, :] ) # Link names link_names = [link.name for link in kinematic_chain.links] # Store the joint angles based on the stage number for link_name in link_names: if "Base" in link_name or "Claw" in link_name: continue self.joint_angles_dict[f"Angle_{link_name}"] = joint_angles[ :, link_names.index(link_name) ] return forward_kinematics
[docs] def run_ik_and_fk( self, export_path: Union[Path, str] = None, **kwargs ) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]: """Runs inverse and forward kinematics for leg joints. Parameters ---------- export_path : Union[Path, str], optional Path where the results will be saved, if None, nothing is saveed, by default None Returns ------- Tuple[Dict[str,np.ndarray], Dict[str,np.ndarray]] Two dictionaries containing joint angles and forward kinematics, respectively. """ hide_progress_bar = kwargs.get("hide_progress_bar", False) forward_kinematics_dict = {} self.logger.info("Computing joint angles and forward kinematics...") for segment_name, segment_array in self.aligned_pos.items(): if "leg" in segment_name.lower(): # If segment name is RF_leg so the leg name is RF leg_name = segment_name.split("_")[0] # If leg_name is not in body_size, then continue if not leg_name in self.kinematic_chain_class.body_size: self.logger.warning( "Leg %s is not in the kinematic chain, continuing...", leg_name, ) continue # First key point of the segment array is the origin # of the kinematic chain, i.e., Thorax-Coxa joint origin = segment_array[:, 0, :] # for the generic IK, endeffector is the claw end_effector_pos = segment_array[:, -1, :] initial_angles = self.initial_angles[leg_name]["stage_4"] forward_kinematics_dict[segment_name] = self.calculate_ik_stage( end_effector_pos=end_effector_pos, origin=origin, initial_angles=initial_angles, segment_name=leg_name, hide_progress_bar=hide_progress_bar, ) else: self.logger.debug( "Segment %s is not a leg, continuing...", segment_name ) continue self.logger.debug("Joint angles and forward kinematics are computed.") if export_path is not None: save_file( Path(export_path) / "forward_kinematics.pkl", forward_kinematics_dict, ) save_file( Path(export_path) / "leg_joint_angles.pkl", self.joint_angles_dict, ) self.logger.info( "Joint angles and forward kinematics are saved at %s", export_path, ) return self.joint_angles_dict, forward_kinematics_dict