import numpy as np class ArmController: def __init__(self, config, mirror=False): self.config = config self.mirror = mirror self.shoulder_idx = {'left': 11, 'right': 12} self.wrist_idx = {'left': 15, 'right': 16} self.hip_idx = {'left': 23, 'right': 24} self.dead_zone = config.get('dead_zone', 0.2) self.debug = config.get('debug', False) def _get_shoulder_width(self, landmarks): """Ширина плеч для нормировки горизонтальных смещений.""" left = landmarks[11][:2] right = landmarks[12][:2] width = np.linalg.norm(right - left) if width < 50 or width > 300: return None return width def _get_torso_height(self, landmarks): """Высота торса для нормировки вертикальных смещений.""" left_shoulder = landmarks[11][:2] right_shoulder = landmarks[12][:2] left_hip = landmarks[23][:2] right_hip = landmarks[24][:2] shoulder_center = (left_shoulder + right_shoulder) / 2 hip_center = (left_hip + right_hip) / 2 height = np.linalg.norm(shoulder_center - hip_center) if height < 50: return None return height def _horizontal_displacement_rel(self, landmarks, side): """ Нормированное горизонтальное смещение запястья относительно плеча. При mirror=True инвертируем знак, чтобы скомпенсировать отражение. """ if landmarks[self.shoulder_idx[side]][3] < 0.5 or landmarks[self.wrist_idx[side]][3] < 0.5: return 0.0 shoulder = landmarks[self.shoulder_idx[side]][:2] wrist = landmarks[self.wrist_idx[side]][:2] shoulder_width = self._get_shoulder_width(landmarks) if shoulder_width is None: return 0.0 disp = wrist[0] - shoulder[0] if self.mirror: disp = -disp return disp / shoulder_width def _vertical_displacement_rel(self, landmarks, side): """ Нормированное вертикальное смещение запястья относительно плеча. Положительное – запястье выше плеча (рука вверх), отрицательное – ниже. Зеркало не влияет. """ if landmarks[self.shoulder_idx[side]][3] < 0.5 or landmarks[self.wrist_idx[side]][3] < 0.5: return 0.0 shoulder = landmarks[self.shoulder_idx[side]][:2] wrist = landmarks[self.wrist_idx[side]][:2] torso_height = self._get_torso_height(landmarks) if torso_height is None: return 0.0 # Так как y растёт вниз, то (shoulder[1] - wrist[1]) > 0, если запястье выше disp = shoulder[1] - wrist[1] return disp / torso_height def compute_speeds(self, landmarks): # Проверка видимости ключевых точек if (landmarks[11][3] < 0.5 or landmarks[12][3] < 0.5 or landmarks[15][3] < 0.5 or landmarks[16][3] < 0.5): #if self.debug: #print("Руки не видны") return 0.0, 0.0 # Правая рука (индекс 16) — линейная скорость lin_rel = self._horizontal_displacement_rel(landmarks, 'right') # Левая рука (индекс 15) — угловая скорость ang_rel = self._vertical_displacement_rel(landmarks, 'left') if self.debug: print(f"lin_rel={lin_rel:.3f}, ang_rel={ang_rel:.3f}") # Линейная скорость (только вперёд) if lin_rel < self.dead_zone: linear = 0.0 else: linear = min(lin_rel, 1.0) * self.config['max_speed_linear'] # Угловая скорость (положительная — вправо, отрицательная — влево) if abs(ang_rel) < self.dead_zone: angular = 0.0 else: ang_rel_clipped = np.clip(ang_rel, -1.0, 1.0) angular = ang_rel_clipped * self.config['max_speed_angular'] return linear, angular