Spaces:
Running
Running
Hugo.V commited on
Commit ·
f3e63f0
1
Parent(s): 5618b49
retour acienne vers
Browse files- jericho/main.py +117 -116
jericho/main.py
CHANGED
|
@@ -39,173 +39,174 @@ def moving_average(values: list[float], window: int = 7) -> np.ndarray: # fct li
|
|
| 39 |
|
| 40 |
class SignalMapper: # classe map signaux visage vers robot
|
| 41 |
# 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 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
|
| 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) #
|
| 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 |
class VideoPayload(BaseModel): # struct payload upload video web
|
| 211 |
name: str
|
|
|
|
| 39 |
|
| 40 |
class SignalMapper: # classe map signaux visage vers robot
|
| 41 |
# 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 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
|
| 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
|