Hugo.V commited on
Commit
ec7cda2
·
1 Parent(s): 4492f35

commentaires

Browse files
Files changed (1) hide show
  1. jericho/main.py +123 -124
jericho/main.py CHANGED
@@ -38,175 +38,174 @@ def moving_average(values: list[float], window: int = 7) -> np.ndarray: # fct li
38
  return smoothed[: len(arr)]
39
 
40
  class SignalMapper: # classe map signaux visage vers robot
41
- # constantes limites angles robot
42
- HEAD_YAW_MAX = np.deg2rad(5)
43
- HEAD_PITCH_MAX = np.deg2rad(15)
44
- HEAD_ROLL_MAX = np.deg2rad(15)
45
- HEAD_Y_MAX = 0.03
46
- HEAD_Z_MAX = 0.03
47
- BODY_YAW_MAX = np.deg2rad(15)
48
- ANTENNA_MAX = np.deg2rad(120)
49
- ANTENNA_MAX_alt = np.deg2rad(45)
50
- ANTENNA_MAX_col = np.deg2rad(110)
51
- COMBINED_YAW_LIMIT = np.deg2rad(60)
52
-
53
- # multiplicateurs amplifs mouvements
54
- YAW_AMP = 8.0
55
- PITCH_AMP = 5.0
56
- ROLL_AMP = 3.0
57
- CX_TO_Y_SCALE = 0.12
58
- CX_TO_BODY_YAW = 0.8
59
- CY_TO_Z_SCALE = -0.10
60
 
61
  # fenetres lissage
62
- SMOOTH_WINDOW_HEAD = 7
63
 
64
- def __init__(self, frames: list[dict]):
65
- self.frames = frames
66
  self._compute_baselines() # calcul moyennes ref
67
  self.commands = self._build_commands() # genere coms robot
68
 
69
  def _compute_baselines(self): # fct valeurs moyennes tte la video
70
- def mean(key, calc_func=None):
71
- if calc_func: return float(np.mean([calc_func(f) for f in self.frames]))
72
- return float(np.mean([f[key] for f in self.frames]))
73
- def std(key, calc_func=None):
74
- if calc_func: return max(float(np.std([calc_func(f) for f in self.frames])), 1e-6)
75
- return max(float(np.std([f[key] for f in self.frames])), 1e-6)
76
 
77
  # calcul refs pour chaque signal (tete + position uniquement, emotions deja dans le JSON via emotion.py)
78
- self.yaw_mean = mean("yaw")
79
- self.pitch_mean = mean("pitch")
80
- self.roll_mean = mean("roll")
81
- self.cx_mean = mean("face_cx")
82
- self.cy_mean = mean("face_cy")
83
 
84
  def _colere(self, t_state): # bouge antennes mode colere
85
- if t_state < 0.15:
86
- angle = -1.0 * (t_state / 0.15) * self.ANTENNA_MAX_col
87
- elif t_state < 0.40:
88
- angle = (-1.0 + 1.0 * ((t_state - 0.15) / 0.15)) * self.ANTENNA_MAX_col
89
- elif t_state < 0.65:
90
- angle = -1.0 * ((t_state - 0.3) / 0.15) * self.ANTENNA_MAX_col
91
- else:
92
- angle = -1.0 * self.ANTENNA_MAX_col
93
- return angle, -angle
94
 
95
  def _tristesse(self, t_state): # bouge antennes mode tristesse
96
- target_angle = -1.0 * (1.0 - math.exp(-1.5 * t_state)) * self.ANTENNA_MAX
97
- return (target_angle - 0.2), -(target_angle - 0.2)
98
 
99
  def _joie(self, t_state): # bouge antennes mode joie (vibre)
100
- oscillation = 0.5 * math.sin(2 * math.pi * 4.0 * t_state)
101
- angle = (0.5 + oscillation) * self.ANTENNA_MAX_alt
102
- return -angle, angle
103
 
104
  def _content(self, t_state): # bouge antennes mode content
105
- oscillation = 0.8 * math.sin(2 * math.pi * 0.5 * t_state)
106
- angle = oscillation * self.ANTENNA_MAX_alt
107
- return angle, angle
108
 
109
  def _surprise(self, t_state): # bouge antennes mode surprise
110
- angle = 1.0 * (1.0 - math.exp(-14.0 * t_state)) * self.ANTENNA_MAX_alt
111
- return -(angle - 0.8), (angle - 0.8)
112
 
113
  def _neutre(self, t_state): # bouge antennes mode neutre
114
- angle = 1.0 * (1.0 - math.exp(-14.0 * t_state)) * self.ANTENNA_MAX_alt
115
- return -(angle - 0.3), (angle - 0.3)
116
 
117
  def _peur(self, t_state): # bouge antennes mode peur (tremble)
118
- tremblement = 0.1 * math.sin(2 * math.pi * 15.0 * t_state)
119
- angle = -1.0 * (1.0 - math.exp(-1.5 * t_state) + tremblement) * self.ANTENNA_MAX
120
- return (angle - 0.2), -(angle - 0.2)
121
 
122
  def _compute_antennas(self, emotion, t_state): # fct select mvt selon emotion
123
- if emotion == "TRISTESSE":
124
- return self._tristesse(t_state)
125
- elif emotion == "JOIE":
126
- return self._joie(t_state)
127
- elif emotion == "CONTENT":
128
- return self._content(t_state)
129
- elif emotion == "SURPRISE":
130
- return self._surprise(t_state)
131
- elif emotion == "NEUTRE":
132
- return self._neutre(t_state)
133
- elif emotion == "COLERE":
134
- return self._colere(t_state)
135
- elif emotion == "PEUR":
136
- return self._peur(t_state)
137
- else:
138
- angle = -1.0 * self.ANTENNA_MAX_col
139
- return angle, -angle
140
 
141
  def _build_commands(self) -> list[dict]: # assemble signaux en tab commandes
142
- n = len(self.frames)
143
- raw_yaw = np.array([f["yaw"] - self.yaw_mean for f in self.frames])
144
- raw_pitch = np.array([f["pitch"] - self.pitch_mean for f in self.frames])
145
- raw_roll = np.array([f["roll"] - self.roll_mean for f in self.frames])
146
- raw_cx = np.array([f["face_cx"] - self.cx_mean for f in self.frames])
147
- raw_cy = np.array([f["face_cy"] - self.cy_mean for f in self.frames])
148
-
149
- w_h = self.SMOOTH_WINDOW_HEAD
150
-
151
- # applique fct lissage (tete + position uniquement, emotions deja calculees par emotion.py)
152
- s_yaw = moving_average(raw_yaw, w_h)
153
- s_pitch = moving_average(raw_pitch, w_h)
154
- s_roll = moving_average(raw_roll, w_h)
155
- s_cx = moving_average(raw_cx, w_h)
156
- s_cy = moving_average(raw_cy, w_h)
157
-
158
- commands = []
159
- current_emotion = "NEUTRE"
160
- emotion_start_time = 0.0
161
 
162
  for i in range(n): # boucle pr chaque image
163
- t = self.frames[i]["timestamp_s"]
164
 
165
  # mise a echelle + clamp pos tete
166
- head_yaw = self._clamp(np.deg2rad(s_yaw[i] * self.YAW_AMP), self.HEAD_YAW_MAX)
167
- head_pitch = self._clamp(np.deg2rad(s_pitch[i] * self.PITCH_AMP), self.HEAD_PITCH_MAX)
168
- head_roll = self._clamp(np.deg2rad(s_roll[i] * self.ROLL_AMP), self.HEAD_ROLL_MAX)
169
- head_y = self._clamp(s_cx[i] * self.CX_TO_Y_SCALE, self.HEAD_Y_MAX)
170
- head_z = self._clamp(s_cy[i] * self.CY_TO_Z_SCALE, self.HEAD_Z_MAX)
171
- body_yaw = self._clamp(s_cx[i] * self.CX_TO_BODY_YAW, self.BODY_YAW_MAX)
172
 
173
  diff = body_yaw - head_yaw # verif coherence corps cou
174
- if abs(diff) > self.COMBINED_YAW_LIMIT:
175
- body_yaw = head_yaw + np.sign(diff) * self.COMBINED_YAW_LIMIT
176
 
177
  # recupere emotion deja calculee par emotion.py (stockee dans signals.json)
178
- detected_emotion = self.frames[i].get("emotion", "NEUTRE")
179
 
180
  if detected_emotion != current_emotion: # top chrono si changmt emo
181
- current_emotion = detected_emotion
182
- emotion_start_time = t
183
 
184
- t_state = t - emotion_start_time
185
- ant_left, ant_right = self._compute_antennas(current_emotion, t_state) # calcul mvt antennes
186
 
187
  commands.append({ # ajoute com dico complet
188
- "timestamp_s": round(t, 4),
189
- "head": {
190
- "x": 0.0,
191
- "y": round(float(head_y), 5),
192
- "z": round(float(head_z), 5),
193
- "roll": round(float(head_roll), 5),
194
- "pitch": round(float(head_pitch), 5),
195
- "yaw": round(float(head_yaw), 5),
196
  },
197
- "body_yaw": round(float(body_yaw), 5),
198
- "antennas": [
199
- round(float(self._clamp(ant_left, self.ANTENNA_MAX)), 5),
200
- round(float(self._clamp(ant_right, self.ANTENNA_MAX)), 5),
201
  ],
202
- "emotion": current_emotion
203
  })
204
- return commands
205
 
206
- @staticmethod
207
  def _clamp(value: float, limit: float) -> float: # limite valeurs min max
208
- return float(max(-limit, min(limit, value)))
209
-
210
 
211
  class VideoPayload(BaseModel): # struct payload upload video web
212
  name: str
 
38
  return smoothed[: len(arr)]
39
 
40
  class SignalMapper: # classe map signaux visage vers robot
41
+ # limites angles robot
42
+ HEAD_YAW_MAX = np.deg2rad(5) # max rota tete g/d
43
+ HEAD_PITCH_MAX = np.deg2rad(15) # max inclin tete ht/bas
44
+ HEAD_ROLL_MAX = np.deg2rad(15) # max penchement tete
45
+ HEAD_Y_MAX = 0.03 # max pos y tete
46
+ HEAD_Z_MAX = 0.03 # max pos z tete
47
+ BODY_YAW_MAX = np.deg2rad(15) # max rota corps g/d
48
+ ANTENNA_MAX = np.deg2rad(120) # max ampli antennes
49
+ ANTENNA_MAX_alt = np.deg2rad(45) # max ampli antennes alt
50
+ ANTENNA_MAX_col = np.deg2rad(110) # max ampli antennes colere
51
+ COMBINED_YAW_LIMIT = np.deg2rad(60) # max ecart tete corps
52
+
53
+ # multiplicateurs mouvements
54
+ YAW_AMP = 8.0 # coeff multi rota g/d
55
+ PITCH_AMP = 5.0 # coeff multi inclin ht/bas
56
+ ROLL_AMP = 3.0 # coeff multi penchement
57
+ CX_TO_Y_SCALE = 0.12 # scale cx vers pos y
58
+ CX_TO_BODY_YAW = 0.8 # scale cx vers rota corps
59
+ CY_TO_Z_SCALE = -0.10 # scale cy vers pos z
60
 
61
  # fenetres lissage
62
+ SMOOTH_WINDOW_HEAD = 7 # val fenetre lissage tete
63
 
64
+ def __init__(self, frames: list[dict]): # init de la classe
65
+ self.frames = frames # recup frames video
66
  self._compute_baselines() # calcul moyennes ref
67
  self.commands = self._build_commands() # genere coms robot
68
 
69
  def _compute_baselines(self): # fct valeurs moyennes tte la video
70
+ def mean(key, calc_func=None): # fct calc moyenne
71
+ if calc_func: return float(np.mean([calc_func(f) for f in self.frames])) # moy si fct calc donnee
72
+ return float(np.mean([f[key] for f in self.frames])) # moy simple sur cle
73
+ def std(key, calc_func=None): # fct calc ecart type
74
+ if calc_func: return max(float(np.std([calc_func(f) for f in self.frames])), 1e-6) # ecart type si fct
75
+ return max(float(np.std([f[key] for f in self.frames])), 1e-6) # ecart type simple
76
 
77
  # calcul refs pour chaque signal (tete + position uniquement, emotions deja dans le JSON via emotion.py)
78
+ self.yaw_mean = mean("yaw") # moy ref rota g/d
79
+ self.pitch_mean = mean("pitch") # moy ref inclin ht/bas
80
+ self.roll_mean = mean("roll") # moy ref penchement
81
+ self.cx_mean = mean("face_cx") # moy ref centre x
82
+ self.cy_mean = mean("face_cy") # moy ref centre y
83
 
84
  def _colere(self, t_state): # bouge antennes mode colere
85
+ if t_state < 0.15: # phase 1 debut
86
+ angle = -1.0 * (t_state / 0.15) * self.ANTENNA_MAX_col # angle tombe vite
87
+ elif t_state < 0.40: # phase 2
88
+ angle = (-1.0 + 1.0 * ((t_state - 0.15) / 0.15)) * self.ANTENNA_MAX_col # angle remonte
89
+ elif t_state < 0.65: # phase 3
90
+ angle = -1.0 * ((t_state - 0.3) / 0.15) * self.ANTENNA_MAX_col # angle retombe
91
+ else: # phase 4 fin
92
+ angle = -1.0 * self.ANTENNA_MAX_col # maintient angle bas
93
+ return angle, -angle # renvoi angle g et d inverses
94
 
95
  def _tristesse(self, t_state): # bouge antennes mode tristesse
96
+ target_angle = -1.0 * (1.0 - math.exp(-1.5 * t_state)) * self.ANTENNA_MAX # calc descente douce
97
+ return (target_angle - 0.2), -(target_angle - 0.2) # renvoi angle g et d avec decallage
98
 
99
  def _joie(self, t_state): # bouge antennes mode joie (vibre)
100
+ oscillation = 0.5 * math.sin(2 * math.pi * 4.0 * t_state) # calc onde rapide
101
+ angle = (0.5 + oscillation) * self.ANTENNA_MAX_alt # mix onde et ampli
102
+ return -angle, angle # renvoi angle g et d
103
 
104
  def _content(self, t_state): # bouge antennes mode content
105
+ oscillation = 0.8 * math.sin(2 * math.pi * 0.5 * t_state) # calc onde lente
106
+ angle = oscillation * self.ANTENNA_MAX_alt # mix onde et ampli
107
+ return angle, angle # renvoi angle g et d mm sens
108
 
109
  def _surprise(self, t_state): # bouge antennes mode surprise
110
+ angle = 1.0 * (1.0 - math.exp(-14.0 * t_state)) * self.ANTENNA_MAX_alt # calc montee tres rapide
111
+ return -(angle - 0.8), (angle - 0.8) # renvoi angle g et d decalles
112
 
113
  def _neutre(self, t_state): # bouge antennes mode neutre
114
+ angle = 1.0 * (1.0 - math.exp(-14.0 * t_state)) * self.ANTENNA_MAX_alt # calc montee rapide
115
+ return -(angle - 0.3), (angle - 0.3) # renvoi angle g et d decalles
116
 
117
  def _peur(self, t_state): # bouge antennes mode peur (tremble)
118
+ tremblement = 0.1 * math.sin(2 * math.pi * 15.0 * t_state) # calc micro ondes
119
+ angle = -1.0 * (1.0 - math.exp(-1.5 * t_state) + tremblement) * self.ANTENNA_MAX # mix baisse et tremblement
120
+ return (angle - 0.2), -(angle - 0.2) # renvoi angle g et d decalles
121
 
122
  def _compute_antennas(self, emotion, t_state): # fct select mvt selon emotion
123
+ if emotion == "TRISTESSE": # si emo est tristesse
124
+ return self._tristesse(t_state) # exe fct tristesse
125
+ elif emotion == "JOIE": # si emo est joie
126
+ return self._joie(t_state) # exe fct joie
127
+ elif emotion == "CONTENT": # si emo est content
128
+ return self._content(t_state) # exe fct content
129
+ elif emotion == "SURPRISE": # si emo est surprise
130
+ return self._surprise(t_state) # exe fct surprise
131
+ elif emotion == "NEUTRE": # si emo est neutre
132
+ return self._neutre(t_state) # exe fct neutre
133
+ elif emotion == "COLERE": # si emo est colere
134
+ return self._colere(t_state) # exe fct colere
135
+ elif emotion == "PEUR": # si emo est peur
136
+ return self._peur(t_state) # exe fct peur
137
+ else: # si emo pas co
138
+ angle = -1.0 * self.ANTENNA_MAX_col # angle defaut
139
+ return angle, -angle # renvoi angle defaut
140
 
141
  def _build_commands(self) -> list[dict]: # assemble signaux en tab commandes
142
+ n = len(self.frames) # nb img total
143
+ raw_yaw = np.array([f["yaw"] - self.yaw_mean for f in self.frames]) # recup et deduit moy yaw
144
+ raw_pitch = np.array([f["pitch"] - self.pitch_mean for f in self.frames]) # recup et deduit moy pitch
145
+ raw_roll = np.array([f["roll"] - self.roll_mean for f in self.frames]) # recup et deduit moy roll
146
+ raw_cx = np.array([f["face_cx"] - self.cx_mean for f in self.frames]) # recup et deduit moy cx
147
+ raw_cy = np.array([f["face_cy"] - self.cy_mean for f in self.frames]) # recup et deduit moy cy
148
+
149
+ w_h = self.SMOOTH_WINDOW_HEAD # recup val lissage
150
+
151
+ # applique fct lissage tete + position
152
+ s_yaw = moving_average(raw_yaw, w_h) # lisse yaw
153
+ s_pitch = moving_average(raw_pitch, w_h) # lisse pitch
154
+ s_roll = moving_average(raw_roll, w_h) # lisse roll
155
+ s_cx = moving_average(raw_cx, w_h) # lisse cx
156
+ s_cy = moving_average(raw_cy, w_h) # lisse cy
157
+
158
+ commands = [] # init tab coms vides
159
+ current_emotion = "NEUTRE" # init emo de depart
160
+ emotion_start_time = 0.0 # init tps de depart
161
 
162
  for i in range(n): # boucle pr chaque image
163
+ t = self.frames[i]["timestamp_s"] # recup tps img i
164
 
165
  # mise a echelle + clamp pos tete
166
+ head_yaw = self._clamp(np.deg2rad(s_yaw[i] * self.YAW_AMP), self.HEAD_YAW_MAX) # calc max yaw tete
167
+ head_pitch = self._clamp(np.deg2rad(s_pitch[i] * self.PITCH_AMP), self.HEAD_PITCH_MAX) # calc max pitch
168
+ head_roll = self._clamp(np.deg2rad(s_roll[i] * self.ROLL_AMP), self.HEAD_ROLL_MAX) # calc max roll
169
+ head_y = self._clamp(s_cx[i] * self.CX_TO_Y_SCALE, self.HEAD_Y_MAX) # calc max pos y
170
+ head_z = self._clamp(s_cy[i] * self.CY_TO_Z_SCALE, self.HEAD_Z_MAX) # calc max pos z
171
+ body_yaw = self._clamp(s_cx[i] * self.CX_TO_BODY_YAW, self.BODY_YAW_MAX) # calc max yaw corps
172
 
173
  diff = body_yaw - head_yaw # verif coherence corps cou
174
+ if abs(diff) > self.COMBINED_YAW_LIMIT: # si ecart trop grand
175
+ body_yaw = head_yaw + np.sign(diff) * self.COMBINED_YAW_LIMIT # corrige ecart max
176
 
177
  # recupere emotion deja calculee par emotion.py (stockee dans signals.json)
178
+ detected_emotion = self.frames[i].get("emotion", "NEUTRE") # recup emo sinon neutre
179
 
180
  if detected_emotion != current_emotion: # top chrono si changmt emo
181
+ current_emotion = detected_emotion # maj nv emo
182
+ emotion_start_time = t # maj tps depart emo
183
 
184
+ t_state = t - emotion_start_time # calc duree emo
185
+ ant_left, ant_right = self._compute_antennas(current_emotion, t_state) # calc mvt antennes
186
 
187
  commands.append({ # ajoute com dico complet
188
+ "timestamp_s": round(t, 4), # tps au format court
189
+ "head": { # dico tete
190
+ "x": 0.0, # axe x
191
+ "y": round(float(head_y), 5), # val y
192
+ "z": round(float(head_z), 5), # val z
193
+ "roll": round(float(head_roll), 5), # val roll
194
+ "pitch": round(float(head_pitch), 5), # val pitch
195
+ "yaw": round(float(head_yaw), 5), # val yaw
196
  },
197
+ "body_yaw": round(float(body_yaw), 5), # val yaw corps
198
+ "antennas": [ # tab antennes
199
+ round(float(self._clamp(ant_left, self.ANTENNA_MAX)), 5), # verif et garde angle g
200
+ round(float(self._clamp(ant_right, self.ANTENNA_MAX)), 5), # verif et garde angle d
201
  ],
202
+ "emotion": current_emotion # recup nom emo
203
  })
204
+ return commands # renvoi toutes les coms pr rob
205
 
206
+ @staticmethod # decorateur statique sans self
207
  def _clamp(value: float, limit: float) -> float: # limite valeurs min max
208
+ return float(max(-limit, min(limit, value))) # recadre val entre lim max et min
 
209
 
210
  class VideoPayload(BaseModel): # struct payload upload video web
211
  name: str