Source code for sic_framework.devices.common_naoqi.motion_affect_transformation

"""

"""


[docs] class MotionAffectTransformation: """ Apply affect-based transformations to NAOqi motion dictionaries. The methods adjust amplitude, timing, posture, and enforce joint limits based on valence/arousal or emotion labels. """
[docs] def transform_values(self, motion, valence, arousal): """ Transform a motion by applying flow, time, and weight changes based on valence and arousal. :param dict motion: Motion dictionary with the shape `{ "motion": { joint_name: {"angles": list[float], "times": list[float]} } }`. :param float valence: Valence value in [-1, 1] influencing amplitude and posture. :param float arousal: Arousal value in [-1, 1] influencing repetition and speed. :returns: The transformed motion dictionary. :rtype: dict """ motion = self.modify_flow_parameters(motion, valence) motion = self.modify_time_parameters(motion, arousal) motion = self.modify_weight_parameters(motion, valence, arousal) return self.angle_limit(motion)
[docs] def transform_label(self, motion, emotion_label): """ Transform a motion by first mapping an emotion label to valence/arousal values. :param dict motion: Motion dictionary to transform. :param str emotion_label: Discrete emotion label (e.g., "happy", "sad", "angry"). :returns: The transformed motion dictionary. :rtype: dict """ valence, arousal = self.values_from_emotion(emotion_label) return self.transform_values(motion, valence, arousal)
[docs] def angle_limit(self, motion): """ Clamp joint angles to the robot's physical limits. :param dict motion: Motion dictionary to validate and clamp. :returns: The motion dictionary with angles limited to valid ranges. :rtype: dict """ for jointName in motion["motion"].keys(): if jointName not in self.hand_joints and jointName not in self.leg_joints: minimum, maximum = self.limit_check(jointName) for angle in motion["motion"][jointName]["angles"]: index = motion["motion"][jointName]["angles"].index(angle) print(angle) if angle < minimum: new_angle = minimum print( jointName, ": ", angle, " is smaller than ", minimum, " so changed to ", new_angle, ) motion["motion"][jointName]["angles"][index] = new_angle elif angle > maximum: new_angle = maximum print( jointName, ": ", angle, " is larger than ", maximum, " so changed to ", new_angle, ) motion["motion"][jointName]["angles"][index] = new_angle else: pass pass return motion
[docs] def modify_flow_parameters(self, motion, valence): """ Adjust motion amplitudes and blend towards a linear trajectory based on valence. :param dict motion: Motion dictionary to modify. :param float valence: Valence value affecting amplitude (positive increases, negative slightly decreases). :returns: The updated motion dictionary. :rtype: dict """ amplitude = self.amplitude(valence) pivot_states = self.pivot_states(motion, self.leg_joints) theta_init = pivot_states[0] theta_end = pivot_states[-1] for jointName in motion["motion"].keys(): if jointName not in self.leg_joints: for i in range(0, len(motion["motion"][jointName]["times"])): normalized_time = ( motion["motion"][jointName]["times"][i] - motion["motion"][jointName]["times"][0] ) / ( motion["motion"][jointName]["times"][-1] - motion["motion"][jointName]["times"][0] + 1 ) line_angle = ( theta_init * (1 - normalized_time) + theta_end * normalized_time ) motion["motion"][jointName]["angles"][i] = ( amplitude * motion["motion"][jointName]["angles"][i] + (1 - amplitude) * line_angle ) return motion
[docs] def modify_time_parameters(self, motion, arousal): """ Adjust repetition (implicit via angle scaling) and speed (time scaling) based on arousal. :param dict motion: Motion dictionary to modify. :param float arousal: Arousal value affecting repetition and speed. :returns: The updated motion dictionary. :rtype: dict """ repetitions = self.repetition(arousal) for jointName in motion["motion"].keys(): if jointName not in self.leg_joints: angles = [] for angle in motion["motion"][jointName]["angles"]: angle = angle * (repetitions + 1) angles.append(angle) motion["motion"][jointName]["angles"] = angles speed = self.speed(arousal) for jointName in motion["motion"].keys(): if jointName not in self.leg_joints: times = [] for time in motion["motion"][jointName]["times"]: time = time * float(speed) times.append(time) motion["motion"][jointName]["times"] = times return motion
[docs] def modify_weight_parameters(self, motion, valence, arousal): """ Add or adjust posture-related joints (e.g., head pitch) based on valence and arousal. :param dict motion: Motion dictionary to modify. :param float valence: Valence value influencing posture. :param float arousal: Arousal value influencing posture and added joints. :returns: The updated motion dictionary. :rtype: dict """ head_pose = self.head_pose(valence, arousal) first_joint = list(motion["motion"].keys())[0] start_time = motion["motion"][first_joint]["times"][0] if "HeadPitch" not in motion["motion"].keys(): motion["motion"]["HeadPitch"] = { "angles": [head_pose, head_pose], "times": [0, start_time], } else: motion["motion"]["HeadPitch"]["angles"] = [ (head_pose + x) for x in motion["motion"]["HeadPitch"]["angles"] ] if arousal < -0.5: for jointName in self.bend: if jointName not in self.leg_joints: if jointName not in motion["motion"]: motion["motion"][jointName] = { "angles": [self.bend[jointName], self.bend[jointName]], "times": [0, start_time], } elif arousal > 0.5: for jointName in self.upright: if jointName not in self.leg_joints: if jointName not in motion["motion"]: motion["motion"][jointName] = { "angles": [ self.upright[jointName], self.upright[jointName], ], "times": [0, start_time], } else: for jointName in self.neutral: if jointName not in self.leg_joints: if jointName not in motion["motion"]: motion["motion"][jointName] = { "angles": [ self.neutral[jointName], self.neutral[jointName], ], "times": [0, start_time], } return motion
[docs] @staticmethod def pivot_states(motion, ignore_joints): """ Collect unique time points across joints, excluding specified joints. :param dict motion: Motion dictionary containing joint time arrays. :param list[str] ignore_joints: Joint names to ignore when collecting time points. :returns: Sorted unique list of time points. :rtype: list[float] """ time_points = [] for joint_name in motion["motion"].keys(): if joint_name not in ignore_joints: times = motion["motion"][joint_name]["times"] for time in times: time_points.append(time) return sorted(set(time_points))
[docs] @staticmethod def amplitude(valence): """ Map valence to an amplitude scaling factor. :param float valence: Valence value in [-1, 1]. :returns: Amplitude multiplier (>0). :rtype: float """ # correlation between amplitude of motion and valence of the affect if valence > 0: amplitude_factor = 1 + valence else: amplitude_factor = 1 + 0.5 * valence return amplitude_factor
[docs] @staticmethod def repetition(arousal): """ Map arousal to a repetition factor. Positive arousal increases the factor; non-positive returns 1. :param float arousal: Arousal value in [-1, 1]. :returns: Repetition multiplier (>=1). :rtype: float """ # positive arousal is associated with an increase in the repetition of the motion # negative arousal does not change the repetition of the motion if arousal > 0: repetition_factor = 1 + abs(2 * arousal) else: repetition_factor = 1 return repetition_factor
[docs] @staticmethod def speed(arousal): """ Map arousal to a speed scaling factor for time values. :param float arousal: Arousal value in [-1, 1]. :returns: Speed multiplier (>0). :rtype: float """ # speed influences the perceived arousal: increase portrays high arousal, # whereas reduction in speed portrays low arousal if arousal > 0: speed_factor = 1 + arousal else: speed_factor = 1 - 0.5 * arousal return speed_factor
[docs] @staticmethod def head_pose(valence, arousal, up=0.506145, down=0.349066): """ Compute a head pitch offset based on valence and arousal. :param float valence: Valence value. :param float arousal: Arousal value. :param float up: Maximum upward pitch in radians. :param float down: Maximum downward pitch in radians. :returns: Head pitch offset in radians. :rtype: float """ # vertical head pose is important for expressing affects in the first and third quadrant # pre-defined angels for up and down are set # if valence < 0 and arousal < 0: headpose = -down * valence elif valence > 0 and arousal > 0: headpose = up * valence else: headpose = 0 return headpose
@property def leg_joints(self): """ Retrieve the list of leg joint names. :returns: List of leg joint identifiers. :rtype: list[str] """ return [ "LAnklePitch", "LAnkleRoll", "LHipPitch", "LHipRoll", "LHipYawPitch", "LKneePitch", "RAnklePitch", "RAnkleRoll", "RHipPitch", "RHipRoll", "RHipYawPitch", "RKneePitch", ] @property def hand_joints(self): """ Retrieve the list of hand joint names. :returns: List of hand joint identifiers. :rtype: list[str] """ return ["LHand", "RHand"] @property def upright(self): """ Retrieve a posture dictionary for an upright (expanded) stance. :returns: Mapping from joint name to target angle in radians. :rtype: dict[str, float] """ return { # also called expanded "LHipYawPitch": -0.17, "LHipRoll": 0.09, "LHipPitch": 0.13, "LKneePitch": -0.08, "LAnklePitch": 0.08, "LAnkleRoll": -0.13, "RHipYawPitch": -0.17, "RHipRoll": -0.09, "RHipPitch": 0.13, "RKneePitch": -0.08, "RAnklePitch": 0.08, "RAnkleRoll": 0.13, } @property def neutral(self): """ Retrieve a posture dictionary for a neutral stance. :returns: Mapping from joint name to target angle in radians. :rtype: dict[str, float] """ return { "LHipYawPitch": 0.0, "LHipRoll": 0.0, "LHipPitch": 0.0, "LKneePitch": 0.0, "LAnklePitch": 0.0, "LAnkleRoll": 0.0, "RHipYawPitch": 0.0, "RHipRoll": 0.0, "RHipPitch": 0.0, "RKneePitch": 0.0, "RAnklePitch": 0.0, "RAnkleRoll": 0.0, } @property def bend(self): """ Retrieve a posture dictionary for a bent (shrunk) stance. :returns: Mapping from joint name to target angle in radians. :rtype: dict[str, float] """ return { # also called shrunk "LHipYawPitch": 0.0, "LHipRoll": 0.0, "LHipPitch": -0.44, "LKneePitch": 0.69, "LAnklePitch": -0.35, "LAnkleRoll": 0.0, "RHipYawPitch": 0.0, "RHipRoll": 0.0, "RHipPitch": -0.44, "RKneePitch": 0.69, "RAnklePitch": -0.35, "RAnkleRoll": 0.0, }
[docs] @staticmethod def limit_check(joint): """ Look up the minimum and maximum allowed angles for a given joint. :param str joint: Joint name to query. :returns: Tuple of (minimum, maximum) allowed angles in radians. :rtype: tuple[float, float] """ limit_table = { "HeadYaw": {"minimum": -2.0857, "maximum": 2.0857}, "HeadPitch": {"minimum": -0.6720, "maximum": 0.5149}, "LShoulderPitch": {"minimum": -2.0857, "maximum": 2.0857}, "LShoulderRoll": {"minimum": -0.3142, "maximum": 1.3265}, "LElbowYaw": {"minimum": -2.0857, "maximum": 2.0857}, "LElbowRoll": {"minimum": -1.5446, "maximum": -0.0349}, "LWristYaw": {"minimum": -1.8238, "maximum": 1.8238}, "RShoulderPitch": {"minimum": -2.0857, "maximum": 2.0857}, "RShoulderRoll": {"minimum": 1.3265, "maximum": 0.3142}, "RElbowYaw": {"minimum": -2.0857, "maximum": 2.0857}, "RElbowRoll": {"minimum": 0.0349, "maximum": 1.5446}, "RWristYaw": {"minimum": -1.8238, "maximum": 1.8238}, "LHipYawPitch": {"minimum": -1.145303, "maximum": 0.740810}, "RHipYawPitch": {"minimum": -1.145303, "maximum": 0.740810}, "LHipRoll": {"minimum": -0.379472, "maximum": 0.790477}, "LHipPitch": {"minimum": -1.535889, "maximum": 0.484090}, "LKneePitch": {"minimum": -0.092346, "maximum": 2.112528}, "LAnklePitch": {"minimum": 1.189516, "maximum": 0.922747}, "LAnkleRoll": {"minimum": -0.397880, "maximum": 0.769001}, "RHipRoll": {"minimum": -0.790477, "maximum": 0.379472}, "RHipPitch": {"minimum": -1.535889, "maximum": 0.484090}, "RKneePitch": {"minimum": -0.103083, "maximum": 2.120198}, "RAnklePitch": {"minimum": 1.186448, "maximum": 0.932056}, "RAnkleRoll": {"minimum": -0.768992, "maximum": 0.397880}, } return limit_table[joint]["minimum"], limit_table[joint]["maximum"]
[docs] @staticmethod def values_from_emotion(emotion_label): """ Map an emotion label to its (valence, arousal) pair. :param str emotion_label: Emotion label key (e.g., "happy", "sad"). :returns: Tuple containing (valence, arousal). :rtype: tuple[float, float] """ value_table = { "excited": {"valence": 0.3, "arousal": 0.8}, # oranje "happy": {"valence": 0.9, "arousal": 0.3}, # geel "pleased": {"valence": 1, "arousal": 0.2}, # lichtguldenroedegeel "content": {"valence": 0.95, "arousal": -0.2}, # lichtgeel "calm": {"valence": 0.8, "arousal": -0.4}, # lichtgroen "relaxed": {"valence": 0.7, "arousal": -0.5}, # ijsblauw "sleepy": {"valence": 0, "arousal": -0.8}, # blauwpaars "tired": {"valence": -0.4, "arousal": -0.85}, # paars "sad": {"valence": -0.7, "arousal": -0.5}, # middenpaars "frustrated": {"valence": -0.9, "arousal": 0.3}, # middenroodviolet "disgust": {"valence": -0.4, "arousal": 0.6}, # dieproze "angry": {"valence": -0.6, "arousal": 0.6}, # donkerrood "afraid": {"valence": -0.7, "arousal": 0.8}, # donkergroen "neutral": {"valence": 0, "arousal": 0}, # wit } return ( value_table[emotion_label]["valence"], value_table[emotion_label]["arousal"], )