drenayaz commited on
Commit
9c3b53c
·
1 Parent(s): 6b77c3c

improve ui and clean code

Browse files
hand_tracker_v2/hand_tracker.py CHANGED
@@ -8,7 +8,6 @@ mp_drawing = mp.solutions.drawing_utils
8
  mp_drawing_styles = mp.solutions.drawing_styles
9
  mp_hands = mp.solutions.hands
10
 
11
-
12
  class HandTracker:
13
  """Hand Tracker using MediaPipe Hands to detect hand positions."""
14
 
@@ -31,24 +30,6 @@ class HandTracker:
31
  img = cv2.flip(img, 1)
32
 
33
  results = self.hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
34
- # if results.multi_hand_landmarks is not None:
35
- # palm_centers = []
36
- # for landmarks in results.multi_hand_landmarks:
37
- # middle_finger_pip_landmark = landmarks.landmark[
38
- # mp_hands.HandLandmark.MIDDLE_FINGER_PIP
39
- # ]
40
- # palm_center = np.array(
41
- # [middle_finger_pip_landmark.x, middle_finger_pip_landmark.y]
42
- # )
43
-
44
- # # Normalize the palm center to the range [-1, 1]
45
- # # Flip the x-axis
46
- # palm_center = [-(palm_center[0] - 0.5) * 2, (palm_center[1] - 0.5) * 2]
47
- # palm_centers.append(palm_center)
48
-
49
- # return palm_centers
50
- # return None
51
- # if results.multi_hand_landmarks is not None:
52
  if results.multi_hand_landmarks is not None:
53
  hand_positions = []
54
  for landmarks in results.multi_hand_landmarks:
@@ -58,8 +39,6 @@ class HandTracker:
58
  palm_center = np.array(
59
  [middle_finger_pip_landmark.x, middle_finger_pip_landmark.y]
60
  )
61
- # Normalize the palm center to the range [-1, 1]
62
- # Flip the x-axis
63
  palm_center = self._norm(palm_center)
64
 
65
  index_finger_tip = np.array([
 
8
  mp_drawing_styles = mp.solutions.drawing_styles
9
  mp_hands = mp.solutions.hands
10
 
 
11
  class HandTracker:
12
  """Hand Tracker using MediaPipe Hands to detect hand positions."""
13
 
 
30
  img = cv2.flip(img, 1)
31
 
32
  results = self.hands.process(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  if results.multi_hand_landmarks is not None:
34
  hand_positions = []
35
  for landmarks in results.multi_hand_landmarks:
 
39
  palm_center = np.array(
40
  [middle_finger_pip_landmark.x, middle_finger_pip_landmark.y]
41
  )
 
 
42
  palm_center = self._norm(palm_center)
43
 
44
  index_finger_tip = np.array([
hand_tracker_v2/main.py CHANGED
@@ -3,13 +3,14 @@ import time
3
 
4
  import cv2
5
  import numpy as np
6
- import math
7
  from reachy_mini import ReachyMini, ReachyMiniApp
8
- from hand_tracker import HandTracker
9
  from scipy.spatial.transform import Rotation as R
10
- from utils import finger_orientation_deg, angle_diff, allow_multiturn
11
- import os
12
- from recorded_moves import RecordedMoves
 
 
13
 
14
  DEBUG = True
15
  FREQUENCY = 50 # Hz
@@ -18,36 +19,92 @@ FREQUENCY = 50 # Hz
18
  class HandTrackerV2(ReachyMiniApp):
19
  custom_app_url: str | None = "http://0.0.0.0:8042"
20
 
21
- hand_pos = None
22
- width, height = None, None
23
- hand_tracker: HandTracker = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  def draw(self, im):
27
  if self.hand_pos is None:
28
- cv2.imshow("Hand Tracker V2", im)
29
- cv2.waitKey(1)
30
- return
31
  if self.hands is None:
32
- cv2.imshow("Hand Tracker V2", im)
33
- cv2.waitKey(1)
34
- return
35
- # draw_palm = [
36
- # (-self.hand_pos[0] + 1) / 2,
37
- # (self.hand_pos[1] + 1) / 2,
38
- # ] # [0, 1]
39
- # im = cv2.circle(
40
- # im,
41
- # (
42
- # int(self.width - draw_palm[0] * self.width),
43
- # int(draw_palm[1] * self.height),
44
- # ),
45
- # radius=5,
46
- # color=(0, 0, 255),
47
- # thickness=-1,
48
- # )
49
- # cv2.imshow("Hand Tracker V2", im)
50
- # cv2.waitKey(1)
51
  for hand in self.hands:
52
  if "palm" in hand:
53
  palm_pos = hand["palm"]
@@ -97,9 +154,7 @@ class HandTrackerV2(ReachyMiniApp):
97
  color=(255, 0, 0),
98
  thickness=-1,
99
  )
100
- cv2.imshow("Hand Tracker V2", im)
101
- cv2.waitKey(1)
102
-
103
 
104
  def update_hand_pos(self, im):
105
  if self.hand_tracker is None:
@@ -115,16 +170,23 @@ class HandTrackerV2(ReachyMiniApp):
115
  # Reset IDLE TIMER
116
  self.last_hand_seen = time.time()
117
  self.is_idle = False
118
- # Choisir la main la plus à droite = x minimal
119
  rightmost_hand = min(hands, key=lambda h: h["palm"][0])
120
  leftmost_hand = max(hands, key=lambda h: h["palm"][0])
121
- self.hand_pos = np.array(rightmost_hand["palm"])
 
 
 
 
 
 
122
  self.left_antenna_angle = - finger_orientation_deg(
123
  rightmost_hand["index_mcp"], rightmost_hand["index_tip"]
124
  )
125
  self.right_antenna_angle = - finger_orientation_deg(
126
  leftmost_hand["index_mcp"], leftmost_hand["index_tip"]
127
  )
 
 
128
  elif self.hand_pos is not None:
129
  self.hand_pos *= 0.9 # Slowly go back to center
130
 
@@ -143,22 +205,14 @@ class HandTrackerV2(ReachyMiniApp):
143
  kz = 0.02
144
  max_delta = 0.3
145
  max_antenna_delta = np.radians(5)
146
- # dt = 0.02
147
-
148
- # Current robot pose state
149
  head_pose = np.eye(4)
150
  euler_rot = np.array([0.0, 0.0, 0.0])
151
 
152
  while not stop_event.is_set():
153
-
154
-
155
  self.hand_count_history.append(self.number_hands)
156
-
157
- # Limiter la taille du buffer
158
  if len(self.hand_count_history) > self.hand_count_buffer_size:
159
  self.hand_count_history.pop(0)
160
 
161
- # Vérifier si toutes les valeurs du buffer sont identiques
162
  if len(self.hand_count_history) == self.hand_count_buffer_size:
163
  stable_hand_count = self.hand_count_history[0]
164
  if all(count == stable_hand_count for count in self.hand_count_history):
@@ -171,23 +225,14 @@ class HandTrackerV2(ReachyMiniApp):
171
  self.previous_number_hands = stable_hand_count
172
 
173
  t0 = time.time()
174
-
175
- # ------------------------
176
- # 1) CHOIX MODE IDLE OU TRACKING
177
- # ------------------------
178
  time_since_last_hand = time.time() - self.last_hand_seen
179
 
180
- if time_since_last_hand > self.idle_timeout:
181
- # Enter idle once
182
  if not self.is_idle:
183
  print("Entering IDLE mode")
184
  self.previous_antenna_angles = reachy_mini.get_present_antenna_joint_positions()
185
-
186
  self.is_idle = True
187
 
188
- # ------------------------
189
- # IDLE: return slowly to neutral
190
- # ------------------------
191
  # Head return to neutral (xyz=euler)
192
  idle_rot = np.array([0.0, 0.0, 0.0]) - euler_rot
193
  idle_rot = np.clip(idle_rot, -max_delta, max_delta)
@@ -213,56 +258,56 @@ class HandTrackerV2(ReachyMiniApp):
213
  self.previous_antenna_angles,
214
  max_antenna_delta
215
  )
216
- self.previous_antenna_angles = antennas
217
 
218
  else:
219
- # ------------------------
220
- # TRACKING MODE
221
- # ------------------------
222
  self.is_idle = False
223
 
224
- if self.hand_pos is None:
225
- time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
226
- continue
227
-
228
- # Tracking error
229
- error = target - self.hand_pos
230
- error = np.clip(error, -max_delta, max_delta)
231
-
232
- # Update head orientation
233
- euler_rot += np.array([
234
- 0.0,
235
- -pitch_kp * error[1],
236
- yaw_kp * error[0]
237
- ])
238
-
239
- euler_rot = np.clip(
240
- euler_rot,
241
- [0.0, -np.deg2rad(30), -np.deg2rad(170)],
242
- [0.0, np.deg2rad(20), np.deg2rad(170)],
243
- )
244
-
245
- # Compute new head pose
246
- head_pose[:3, :3] = R.from_euler("xyz", euler_rot).as_matrix()
247
- head_pose[:3, 3][2] = error[1] * kz
248
-
249
- # Antennas
250
- antennas = np.radians([
251
- self.right_antenna_angle,
252
- self.left_antenna_angle,
253
- ])
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- antennas = allow_multiturn(
256
- antennas,
257
- self.previous_antenna_angles,
258
- max_antenna_delta
259
- )
260
- self.previous_antenna_angles = antennas
261
-
262
- # ------------------------
263
- # 2) MISE À JOUR UNIQUE DU ROBOT
264
- # ------------------------
265
  reachy_mini.set_target(head=head_pose, antennas=antennas)
 
 
266
 
267
  time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
268
 
@@ -270,29 +315,13 @@ class HandTrackerV2(ReachyMiniApp):
270
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
271
  reachy_mini.goto_target(np.eye(4), [0.0, 0.0], body_yaw=0.0, duration=1.0)
272
 
273
- self.previous_antenna_angles = [0.0, 0.0] # [right, left]
274
- self.last_hand_seen = time.time()
275
- self.idle_timeout = 1.0 # secondes avant de passer en idle
276
- self.is_idle = False
277
-
278
- self.number_hands = 0
279
- self.previous_number_hands = 0
280
- self.last_play_sound = time.time()
281
-
282
- self.hand_count_history = [] # buffer des derniers counts
283
- self.hand_count_buffer_size = 3 # nombre de frames consécutives pour valider
284
-
285
- self.recorded_moves = RecordedMoves("pollen-robotics/reachy-mini-emotions-library")
286
-
287
-
288
-
289
  tracking_thread = threading.Thread(
290
  target=self.track, args=(reachy_mini, stop_event)
291
  )
292
  tracking_thread.start()
293
 
294
-
295
  while not stop_event.is_set():
 
296
  im = reachy_mini.media.get_frame()
297
  if im is None:
298
  continue
@@ -301,9 +330,12 @@ class HandTrackerV2(ReachyMiniApp):
301
  self.height, self.width = im.shape[:2]
302
 
303
  self.update_hand_pos(im)
304
- if DEBUG:
305
- self.draw(im)
306
- # time.sleep(0.02)
 
 
 
307
 
308
  if DEBUG:
309
  try:
 
3
 
4
  import cv2
5
  import numpy as np
 
6
  from reachy_mini import ReachyMini, ReachyMiniApp
7
+ from hand_tracker_v2.hand_tracker import HandTracker
8
  from scipy.spatial.transform import Rotation as R
9
+ from hand_tracker_v2.utils import finger_orientation_deg, allow_multiturn
10
+ from hand_tracker_v2.recorded_moves import RecordedMoves
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel
13
+
14
 
15
  DEBUG = True
16
  FREQUENCY = 50 # Hz
 
19
  class HandTrackerV2(ReachyMiniApp):
20
  custom_app_url: str | None = "http://0.0.0.0:8042"
21
 
22
+ def __init__(self):
23
+ super().__init__()
24
+
25
+ # Tracking state
26
+ self.hand_pos = None
27
+ self.hands = None
28
+ self.width = None
29
+ self.height = None
30
+ self.hand_tracker = None
31
+
32
+ # Antennas
33
+ self.left_antenna_angle = 0.0
34
+ self.right_antenna_angle = 0.0
35
+ self.previous_antenna_angles = [0.0, 0.0]
36
+ self.previous_head_pose = np.eye(4)
37
+
38
+ # Hand count + sound triggers
39
+ self.number_hands = 0
40
+ self.previous_number_hands = 0
41
+ self.hand_count_history = []
42
+ self.hand_count_buffer_size = 3
43
+
44
+ self.last_hand_seen = time.time()
45
+ self.idle_timeout = 1.0
46
+ self.is_idle = False
47
+ self.last_play_sound = 0.0
48
+
49
+ self.show_img = False
50
+ self.track_mode = True
51
+ self.antenna_mode = True
52
+ self.preferred_side = "Left"
53
+ self.antenna_mode = "Same Movement"
54
+
55
+ # Recorded moves / sounds
56
+ self.recorded_moves = RecordedMoves(
57
+ "pollen-robotics/reachy-mini-emotions-library"
58
+ )
59
+ self.last_frame = None
60
+
61
+ @self.settings_app.get("/video_feed")
62
+ def video_feed():
63
+ return StreamingResponse(self.frame_generator(),
64
+ media_type="multipart/x-mixed-replace; boundary=frame")
65
+
66
+ class UIState(BaseModel):
67
+ video: bool | None = None
68
+ tracking: bool | None = None
69
+ antenna: bool | None = None
70
+ preferred_side: str | None = None
71
+ antenna_mode: str | None = None
72
+
73
+ @self.settings_app.post("/set_toggles")
74
+ async def set_toggles(state: UIState):
75
+ if state.video is not None:
76
+ self.show_img = state.video
77
+ if state.tracking is not None:
78
+ self.track_mode = state.tracking
79
+ if state.antenna is not None:
80
+ self.antenna_mode = state.antenna
81
+ if state.preferred_side is not None:
82
+ print("Preferred side set to:", state.preferred_side)
83
+ self.preferred_side = state.preferred_side
84
+ if state.antenna_mode is not None:
85
+ print("Index mode set to:", state.antenna_mode)
86
+ self.antenna_mode = state.antenna_mode
87
+ return {"status": "ok"}
88
+
89
+ def frame_generator(self):
90
+ while True:
91
+ if self.last_frame is None:
92
+ time.sleep(0.01)
93
+ continue
94
+ ret, jpeg = cv2.imencode(".jpg", self.last_frame)
95
+ frame = jpeg.tobytes()
96
+ yield (
97
+ b"--frame\r\n"
98
+ b"Content-Type: image/jpeg\r\n\r\n" + frame + b"\r\n"
99
+ )
100
+ time.sleep(0.05)
101
 
102
 
103
  def draw(self, im):
104
  if self.hand_pos is None:
105
+ return im
 
 
106
  if self.hands is None:
107
+ return im
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  for hand in self.hands:
109
  if "palm" in hand:
110
  palm_pos = hand["palm"]
 
154
  color=(255, 0, 0),
155
  thickness=-1,
156
  )
157
+ return im
 
 
158
 
159
  def update_hand_pos(self, im):
160
  if self.hand_tracker is None:
 
170
  # Reset IDLE TIMER
171
  self.last_hand_seen = time.time()
172
  self.is_idle = False
 
173
  rightmost_hand = min(hands, key=lambda h: h["palm"][0])
174
  leftmost_hand = max(hands, key=lambda h: h["palm"][0])
175
+ if self.preferred_side == "Left":
176
+ print(self.preferred_side)
177
+ self.hand_pos = np.array(leftmost_hand["palm"])
178
+ else:
179
+ print(self.preferred_side)
180
+
181
+ self.hand_pos = np.array(rightmost_hand["palm"])
182
  self.left_antenna_angle = - finger_orientation_deg(
183
  rightmost_hand["index_mcp"], rightmost_hand["index_tip"]
184
  )
185
  self.right_antenna_angle = - finger_orientation_deg(
186
  leftmost_hand["index_mcp"], leftmost_hand["index_tip"]
187
  )
188
+ if self.antenna_mode == "Symmetric" and len(hands) == 1:
189
+ self.left_antenna_angle = - self.right_antenna_angle
190
  elif self.hand_pos is not None:
191
  self.hand_pos *= 0.9 # Slowly go back to center
192
 
 
205
  kz = 0.02
206
  max_delta = 0.3
207
  max_antenna_delta = np.radians(5)
 
 
 
208
  head_pose = np.eye(4)
209
  euler_rot = np.array([0.0, 0.0, 0.0])
210
 
211
  while not stop_event.is_set():
 
 
212
  self.hand_count_history.append(self.number_hands)
 
 
213
  if len(self.hand_count_history) > self.hand_count_buffer_size:
214
  self.hand_count_history.pop(0)
215
 
 
216
  if len(self.hand_count_history) == self.hand_count_buffer_size:
217
  stable_hand_count = self.hand_count_history[0]
218
  if all(count == stable_hand_count for count in self.hand_count_history):
 
225
  self.previous_number_hands = stable_hand_count
226
 
227
  t0 = time.time()
 
 
 
 
228
  time_since_last_hand = time.time() - self.last_hand_seen
229
 
230
+ if time_since_last_hand > self.idle_timeout or (not self.track_mode and not self.antenna_mode):
 
231
  if not self.is_idle:
232
  print("Entering IDLE mode")
233
  self.previous_antenna_angles = reachy_mini.get_present_antenna_joint_positions()
 
234
  self.is_idle = True
235
 
 
 
 
236
  # Head return to neutral (xyz=euler)
237
  idle_rot = np.array([0.0, 0.0, 0.0]) - euler_rot
238
  idle_rot = np.clip(idle_rot, -max_delta, max_delta)
 
258
  self.previous_antenna_angles,
259
  max_antenna_delta
260
  )
 
261
 
262
  else:
 
 
 
263
  self.is_idle = False
264
 
265
+ if not self.track_mode:
266
+ head_pose = self.previous_head_pose.copy()
267
+ else:
268
+ if self.hand_pos is None:
269
+ time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
270
+ continue
271
+
272
+ # Tracking error
273
+ error = target - self.hand_pos
274
+ error = np.clip(error, -max_delta, max_delta)
275
+
276
+ # Update head orientation
277
+ euler_rot += np.array([
278
+ 0.0,
279
+ -pitch_kp * error[1],
280
+ yaw_kp * error[0]
281
+ ])
282
+
283
+ euler_rot = np.clip(
284
+ euler_rot,
285
+ [0.0, -np.deg2rad(30), -np.deg2rad(170)],
286
+ [0.0, np.deg2rad(20), np.deg2rad(170)],
287
+ )
288
+
289
+ # Compute new head pose
290
+ head_pose[:3, :3] = R.from_euler("xyz", euler_rot).as_matrix()
291
+ head_pose[:3, 3][2] = error[1] * kz
292
+
293
+ if not self.antenna_mode:
294
+ antennas = self.previous_antenna_angles.copy()
295
+ else:
296
+ # Antennas
297
+ antennas = np.radians([
298
+ self.right_antenna_angle,
299
+ self.left_antenna_angle,
300
+ ])
301
+
302
+ antennas = allow_multiturn(
303
+ antennas,
304
+ self.previous_antenna_angles,
305
+ max_antenna_delta
306
+ )
307
 
 
 
 
 
 
 
 
 
 
 
308
  reachy_mini.set_target(head=head_pose, antennas=antennas)
309
+ self.previous_head_pose = head_pose.copy()
310
+ self.previous_antenna_angles = antennas.copy()
311
 
312
  time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
313
 
 
315
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
316
  reachy_mini.goto_target(np.eye(4), [0.0, 0.0], body_yaw=0.0, duration=1.0)
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  tracking_thread = threading.Thread(
319
  target=self.track, args=(reachy_mini, stop_event)
320
  )
321
  tracking_thread.start()
322
 
 
323
  while not stop_event.is_set():
324
+ t0 = time.time()
325
  im = reachy_mini.media.get_frame()
326
  if im is None:
327
  continue
 
330
  self.height, self.width = im.shape[:2]
331
 
332
  self.update_hand_pos(im)
333
+
334
+ if not self.show_img:
335
+ continue
336
+ im = self.draw(im)
337
+ self.last_frame = im.copy()
338
+ time.sleep(max(0, (1.0 / FREQUENCY) - (time.time() - t0)))
339
 
340
  if DEBUG:
341
  try:
hand_tracker_v2/recorded_moves.py CHANGED
@@ -11,8 +11,6 @@ class RecordedMoves:
11
  """Initialize RecordedMoves."""
12
  self.hf_dataset_name = hf_dataset_name
13
  self.local_path = snapshot_download(self.hf_dataset_name, repo_type="dataset")
14
- # self.local_path = Path(__file__).parent.parent.parent / hf_dataset_name
15
- # print(self.local_path)
16
  self.moves: Dict[str, Any] = {}
17
  self.sounds: Dict[str, Any] = {}
18
 
 
11
  """Initialize RecordedMoves."""
12
  self.hf_dataset_name = hf_dataset_name
13
  self.local_path = snapshot_download(self.hf_dataset_name, repo_type="dataset")
 
 
14
  self.moves: Dict[str, Any] = {}
15
  self.sounds: Dict[str, Any] = {}
16
 
hand_tracker_v2/static/index.html CHANGED
@@ -3,25 +3,91 @@
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
- <title>Reachy Mini example app template</title>
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="stylesheet" href="/static/style.css">
9
  </head>
10
 
11
  <body>
12
- <h1>Reachy Mini – Control Panel</h1>
 
 
 
13
 
14
- <div id="controls">
15
- <label style="display:flex; align-items:center; gap:8px;">
16
- <input type="checkbox" id="antenna-checkbox" checked>
17
- Antennas
18
- </label>
19
 
20
- <button id="sound-btn">Play Sound</button>
21
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- <div id="status">Antennas status: running</div>
24
  <script src="/static/main.js"></script>
25
  </body>
26
 
27
- </html>
 
3
 
4
  <head>
5
  <meta charset="UTF-8">
6
+ <title>Hand Tracking</title>
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="stylesheet" href="/static/style.css">
9
  </head>
10
 
11
  <body>
12
+ <header class="header">
13
+ <h1>Hand Tracking</h1>
14
+ <img src="/static/reachy-mini-waving.png" class="logo" />
15
+ </header>
16
 
17
+ <main class="container">
 
 
 
 
18
 
19
+ <!-- Camera Feed Card -->
20
+ <section class="card camera-card">
21
+ <div class="card-header">
22
+ <h3>Camera Feed</h3>
23
+ <label class="toggle">
24
+ <input type="checkbox" id="toggle-video">
25
+ <span class="slider"></span>
26
+ </label>
27
+ </div>
28
+ <p class="desc">
29
+ Red dot: point the robot is tracking. <br>
30
+ Blue and green dots: used to calculate the index finger angle to determine antenna orientation.
31
+ </p>
32
+
33
+ <img id="video-feed" src="/video_feed" class="video-display">
34
+ </section>
35
+
36
+ <!-- Two side cards -->
37
+ <div class="card-row">
38
+
39
+ <!-- Hand Tracking -->
40
+ <section class="card">
41
+ <div class="card-header">
42
+ <h3>Hand Tracking</h3>
43
+ <label class="toggle">
44
+ <input type="checkbox" id="toggle-tracking" checked>
45
+ <span class="slider"></span>
46
+ </label>
47
+ </div>
48
+
49
+ <p class="desc">
50
+ The system follows your hand in real time.
51
+ If multiple hands are detected, the selected side determines which one is used.
52
+ </p>
53
+
54
+ <div class="option">
55
+ <span>Preferred Side:</span>
56
+ <select id="side-select">
57
+ <option>Left</option>
58
+ <option>Right</option>
59
+ </select>
60
+ </div>
61
+ </section>
62
+
63
+ <!-- Antenna Movement -->
64
+ <section class="card">
65
+ <div class="card-header">
66
+ <h3>Antenna Movement</h3>
67
+ <label class="toggle">
68
+ <input type="checkbox" id="toggle-antennas" checked>
69
+ <span class="slider"></span>
70
+ </label>
71
+ </div>
72
+
73
+ <p class="desc">
74
+ Control both antennas using your index fingers.
75
+ If only one hand is detected, the selected mode decides how both antennas move.
76
+ </p>
77
+
78
+ <div class="option">
79
+ <span>Mode:</span>
80
+ <select id="antenna-mode">
81
+ <option>Same Movement</option>
82
+ <option>Symmetric</option>
83
+ </select>
84
+ </div>
85
+ </section>
86
+
87
+ </div>
88
+ </main>
89
 
 
90
  <script src="/static/main.js"></script>
91
  </body>
92
 
93
+ </html>
hand_tracker_v2/static/main.js CHANGED
@@ -1,47 +1,54 @@
1
- let antennasEnabled = true;
 
 
 
2
 
3
- async function updateAntennasState(enabled) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  try {
5
- const resp = await fetch("/antennas", {
6
  method: "POST",
7
  headers: { "Content-Type": "application/json" },
8
- body: JSON.stringify({ enabled }),
 
 
 
 
 
 
9
  });
10
  const data = await resp.json();
11
- antennasEnabled = data.antennas_enabled;
12
- updateUI();
13
- } catch (e) {
14
- document.getElementById("status").textContent = "Backend error";
15
- }
16
- }
17
-
18
- async function playSound() {
19
- try {
20
- await fetch("/play_sound", { method: "POST" });
21
  } catch (e) {
22
- console.error("Error triggering sound:", e);
 
23
  }
24
  }
25
 
26
- function updateUI() {
27
- const checkbox = document.getElementById("antenna-checkbox");
28
- const status = document.getElementById("status");
29
-
30
- checkbox.checked = antennasEnabled;
31
-
32
- if (antennasEnabled) {
33
- status.textContent = "Antennas status: running";
34
- } else {
35
- status.textContent = "Antennas status: stopped";
36
- }
37
- }
38
-
39
- document.getElementById("antenna-checkbox").addEventListener("change", (e) => {
40
- updateAntennasState(e.target.checked);
41
  });
42
-
43
- document.getElementById("sound-btn").addEventListener("click", () => {
44
- playSound();
45
- });
46
-
47
- updateUI();
 
1
+ const toggleVideo = document.getElementById("toggle-video");
2
+ const videoFeed = document.getElementById("video-feed");
3
+ const toggleTracking = document.getElementById("toggle-tracking");
4
+ const toggleAntenna = document.getElementById("toggle-antennas");
5
 
6
+
7
+
8
+ // Sauvegarder l'URL du flux
9
+ const videoSrc = "/video_feed";
10
+
11
+ toggleVideo.addEventListener("change", () => {
12
+ if (toggleVideo.checked) {
13
+ // reconnecter le flux
14
+ videoFeed.src = videoSrc;
15
+ } else {
16
+ // couper le flux
17
+ videoFeed.src = "";
18
+ }
19
+ });
20
+
21
+
22
+ async function updateToggleState() {
23
  try {
24
+ const resp = await fetch("/set_toggles", {
25
  method: "POST",
26
  headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({
28
+ video: toggleVideo.checked,
29
+ tracking: toggleTracking.checked,
30
+ antenna: toggleAntenna.checked,
31
+ preferred_side: document.getElementById("side-select").value,
32
+ antenna_mode: document.getElementById("antenna-mode").value
33
+ })
34
  });
35
  const data = await resp.json();
36
+ if (data.status === "ok") {
37
+ document.getElementById("status").textContent =
38
+ `✔ Settings updated`;
39
+ } else {
40
+ document.getElementById("status").textContent =
41
+ `✘ Failed to update settings`;
42
+ }
 
 
 
43
  } catch (e) {
44
+ console.error(e);
45
+ document.getElementById("status").textContent = "Server error";
46
  }
47
  }
48
 
49
+ // Ajouter un event listener à chaque toggle
50
+ [toggleVideo, toggleTracking, toggleAntenna].forEach(toggle => {
51
+ toggle.addEventListener("change", updateToggleState);
 
 
 
 
 
 
 
 
 
 
 
 
52
  });
53
+ document.getElementById("side-select").addEventListener("change", updateToggleState);
54
+ document.getElementById("antenna-mode").addEventListener("change", updateToggleState);
 
 
 
 
hand_tracker_v2/static/style.css CHANGED
@@ -1,25 +1,131 @@
 
1
  body {
2
- font-family: sans-serif;
3
- margin: 24px;
 
 
 
4
  }
5
 
6
- #sound-btn {
7
- padding: 10px 20px;
8
- border: none;
 
 
 
 
9
  color: white;
10
- cursor: pointer;
11
- font-size: 16px;
12
- border-radius: 6px;
13
- background-color: #3498db;
14
  }
15
 
16
- #status {
17
- margin-top: 16px;
18
- font-weight: bold;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
- #controls {
 
 
 
 
 
 
 
 
22
  display: flex;
 
23
  align-items: center;
24
- gap: 20px;
25
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* General Layout */
2
  body {
3
+ margin: 0;
4
+ padding: 0;
5
+ background: #1f2937;
6
+ font-family: "Inter", sans-serif;
7
+ color: #222;
8
  }
9
 
10
+ /* Header */
11
+ .header {
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ gap: 16px;
16
+ padding: 17px 0; /* reduced height */
17
  color: white;
 
 
 
 
18
  }
19
 
20
+ .header .logo {
21
+ height: 50px; /* slightly smaller */
22
+ }
23
+
24
+ /* Main container */
25
+ .container {
26
+ max-width: 1100px;
27
+ margin: auto;
28
+ padding: 20px;
29
+ }
30
+
31
+ /* Cards */
32
+ .card {
33
+ background: white;
34
+ padding: 22px;
35
+ border-radius: 14px;
36
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
37
+ margin-bottom: 24px;
38
+ }
39
+
40
+ .card-row {
41
+ display: flex;
42
+ gap: 24px;
43
+ }
44
+
45
+ .card-row .card {
46
+ flex: 1;
47
+ }
48
+
49
+ .card h3 {
50
+ margin: 0;
51
+ font-size: 18px;
52
  }
53
 
54
+ /* Video Feed */
55
+ .video-display {
56
+ width: 100%;
57
+ border-radius: 12px;
58
+ margin-top: 12px;
59
+ }
60
+
61
+ /* Card header with toggle */
62
+ .card-header {
63
  display: flex;
64
+ justify-content: space-between;
65
  align-items: center;
66
+ }
67
+
68
+ /* Switch toggle */
69
+ .toggle {
70
+ position: relative;
71
+ width: 46px;
72
+ height: 24px;
73
+ }
74
+
75
+ .toggle input {
76
+ opacity: 0;
77
+ width: 0;
78
+ height: 0;
79
+ }
80
+
81
+ .slider {
82
+ position: absolute;
83
+ cursor: pointer;
84
+ background: #ccc;
85
+ border-radius: 24px;
86
+ inset: 0;
87
+ transition: 0.3s;
88
+ }
89
+
90
+ .slider::before {
91
+ content: "";
92
+ position: absolute;
93
+ height: 18px;
94
+ width: 18px;
95
+ left: 3px;
96
+ bottom: 3px;
97
+ background: white;
98
+ border-radius: 50%;
99
+ transition: 0.3s;
100
+ }
101
+
102
+ input:checked + .slider {
103
+ background: #3b82f6;
104
+ }
105
+
106
+ input:checked + .slider::before {
107
+ transform: translateX(22px);
108
+ }
109
+
110
+ /* Options */
111
+ .option {
112
+ margin-top: 12px;
113
+ display: flex;
114
+ justify-content: space-between;
115
+ background: #f3f4f6;
116
+ padding: 10px 14px;
117
+ border-radius: 10px;
118
+ }
119
+
120
+ select {
121
+ border: none;
122
+ background: transparent;
123
+ font-size: 14px;
124
+ }
125
+
126
+ /* Description text */
127
+ .desc {
128
+ margin: 10px 0 14px;
129
+ font-size: 14px;
130
+ color: #444;
131
+ }
hand_tracker_v2/utils.py CHANGED
@@ -48,4 +48,8 @@ def allow_multiturn(new_joints: list[float], prev_joints: list[float], max_delta
48
  else:
49
  diff = -max_delta
50
  new_joints[i] = prev_joints[i] + diff
 
 
 
 
51
  return new_joints
 
48
  else:
49
  diff = -max_delta
50
  new_joints[i] = prev_joints[i] + diff
51
+ if new_joints[i] > 3* math.pi:
52
+ new_joints[i] -= 2*math.pi
53
+ elif new_joints[i] < -3* math.pi:
54
+ new_joints[i] += 2*math.pi
55
  return new_joints