10 Commits
Author SHA1 Message Date
gestures4 08c5b66ace mount versions of pose-detectors combined 2026-07-17 17:10:00 +03:00
gestures4 8a43b58710 sasha mod 2026-07-11 15:18:21 +03:00
gestures4 e26b312b2f last alex 2026-07-11 12:34:25 +03:00
gestures6 e74f1b09d7 changed cross detection to silly and fixed deps 2026-07-03 17:18:54 +03:00
Eliza Moscovskaya 1b9a1b2c3d Update 'README.md' 2026-07-03 10:10:42 +03:00
Eliza Moscovskaya d3d6c647a6 Update 'README.md' 2026-07-02 15:34:55 +03:00
moscovskayaliza b0f04daa78 hotfix to make it works with ros 2026-06-25 18:16:58 +03:00
moscovskayaliza 0b3c74c463 upd readme 2026-06-25 13:51:10 +03:00
moscovskayaliza 488db0f2db moved speed compution in lib 2026-06-25 13:34:01 +03:00
moscovskayaliza bf4be1cdff upd installation 2026-06-25 13:28:28 +03:00
4 changed files with 550 additions and 91 deletions
+86 -20
View File
@@ -19,7 +19,8 @@ gesture_rec/
├── skeleton/ # Детекция скелета
│ ├── oak_pose_detector.py # Детектор на oak (efficienthrnet)
│ └── mediapipe_detector.py # MediaPipe
├── gesture_control/ # Распознавание статичных жестов
├── gesture_control/
│ ├── arm_control.py # Преобразование позы в значения для угловой и линейной скоростей
│ └── special_gestures.py # Специальные жесты (геометрия или ML)
├── ml_gestures/ # ML для статичных жестов
│ ├── feature_extractor.py # Нормализация landmarks
@@ -51,17 +52,28 @@ cd gesture_rec
git submodule update --init --recursive
```
### 2. Создание виртуального окружения
Рекомендуется использовать виртуальное окружение и Python 3.11:
Рекомендуется использовать виртуальное окружение и Python 3.10:
```
python3.11 -m venv venv
python3.10 -m venv venv
source venv/bin/activate
```
Если у вас не скачен питон этой версии, сначала выполните:
```
sudo apt install python3.11 python3.11-venv
sudo apt install python3.10 python3.10-venv
```
Если у нас не устанавливается питон 3.10, то это потому что он отсутствует в официальных репозиториях по умолчанию, надо добавить репозиторий перед скачиванием:
```
sudo apt update && sudo apt install -y software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
```
To install pip:
sudo apt install -y python3-pip
### 3. Установка зависимостей
### 3.1. Способ 1
@@ -78,6 +90,7 @@ sudo apt install python3.11 python3.11-venv
pip install --upgrade pip
pip install numpy==1.24.3
pip install pandas==2.0.3
pip install pyyaml
pip install opencv-python==4.12.0.88
pip install opencv-python-headless==4.12.0.88
pip install matplotlib==3.7.5
@@ -107,8 +120,30 @@ sudo apt install python3.11 python3.11-venv
requests
pip install numpy==1.24.3
pip install scipy==1.11.0
Важно: красные предупреждения о нехватке зависимостей для tensorflow будут, но можно их игнорировать, т.к. эти модули не используются в данном проекте
#### Типичные проблемы во время установки:
Важно: красные предупреждения о нехватке зависимостей для tensorflow будут, но можно их игнорировать, т.к. эти модули не используются в данном проекте, а их установка мешает зависимостям MediaPipe.
1. Если будет ошибка с `"AttributeError: google..."` но это потому что `tensorflow` подменяет версию библиотеки `protobuf`, выполните:
```
pip uninstall protobuf google protobuf
pip install protobuf==3.20.3
```
Примечание: если у вас слабая видеокарта или нет CUDA, используйте `tensorflow-cpu` вместо `tensorflow`.
2. Если будет проблема с функцией cv2.imshow() то, переустановите cv2 без заголовков:
```
pip uninstall opencv-python opencv-python-headless
pip install opencv-python==4.12.0.88
```
3. Если будет ошибка с PIL, попробуйте обновить библиотеку:
```
pip upgrage pillow
```
4. **Внимание:** если вам пришлось делать после основной установки какие-то дополнительные, обязательно зафиксируйте еще раз версию numpy!
```
pip install numpy==1.24.3
```
### 4. Проверка работы камеры
Для веб-камеры достаточно, чтобы она была доступна по индексу (встроенная 0). Для OAK-D потребуется подключить устройство и установить права доступа (сделать это надо один раз):
@@ -178,7 +213,38 @@ vis = detector.draw_landmarks(frame, landmarks)
Подробнее о параметрах модели в [репозитории](https://github.com/kschlegel/OAK-HumanPoseEstimation.git).
### 1. Геометрическое распознавание жестов
### 1. Определение значений скоростей
По умолчанию преобразование позы в скорости реализовано в классе `ArmController` (файл `arm_control.py`).
Чтобы изменить логику управления, выполните одно из действий:
1. Изменить метод `compute_speeds` в `arm_control.py` – он должен принимать аргумент landmarks (список из 33 точек MediaPipe) и возвращать кортеж (linear, angular) – числа с плавающей точкой.
2. Создать свой класс-наследник от `ArmController` и переопределить `compute_speeds`. Затем в `main.py` заменить создание экземпляра на свой класс.
После этого не забудьте изменить параметры словаря `ARM_CONTROL`, вы можете добавлять туда свои поля и читать их в методе. Текущий класс создается и используется следующим образом:
```
from gesture_control.arm_control import ArmController
mirror = True # для всех фронтальных камер
ARM_CONTROL = {
'linear_arm': 'right',
'angular_arm': 'left',
'max_speed_linear': 1.0,
'max_speed_angular': 1.0,
'dead_zone': 0.2,
'debug': False
}
arm_control = ArmController(ARM_CONTROL, mirror=mirror)
linear, angular = arm_control.compute_speeds(landmarks) # Дальше эти скорости можно подавать на контроллер
```
- `ARM_CONTROL` словарь:
- `linear_arm` – рука для линейной скорости ('left' или 'right')
- `angular_arm` – рука для угловой скорости
- `max_speed_linear` – макс. линейная скорость (м/с)
- `max_speed_angular` – макс. угловая скорость (рад/с)
- `dead_zone` – зона нечувствительности (0.0–1.0)
- `debug` – выводить отладочную информацию в консоль
### 2. Геометрическое распознавание жестов
Класс `SpecialGestureDetector` в режиме `mode='geometric'` анализирует координаты скелета и применяет набор правил.
На текущий момент геометрически распознаются два жеста:
@@ -187,7 +253,7 @@ vis = detector.draw_landmarks(frame, landmarks)
Все пороги (уверенность `min_conf`, коэффициенты) заданы внутри `_geometric_predict` и могут быть подстроены под ваши условия. Чтобы распознавать собственный жест, отредактируйте метод `_geometric_predict` в файле `gesture_control/special_gestures.py`.
#### 1.1. Порядок действий:
#### 2.1. Порядок действий:
1. Определите, какие ключевые точки MediaPipe участвуют в жесте (список индексов см. в коде или в документации MediaPipe).
2. Напишите условие, используя координаты `(x, y)` нужных точек. Например, жест «рука в сторону»: `left_wrist[0] > left_shoulder[0] + width` и `right_wrist[0] < right_shoulder[0] - width`.
3. Вставьте проверку до финального return 'none', чтобы при совпадении условий возвращать строку с именем вашего жеста.
@@ -201,15 +267,15 @@ if arms_out:
```
**Важно:** геометрический режим не требует обучения, но чувствителен к позе и ракурсу. Для более сложных жестов рекомендуем использовать ML.
#### 1.2. Использование в коде:
#### 2.2. Использование в коде:
```
from gesture_control.special_gestures import SpecialGestureDetector
detector = SpecialGestureDetector(mode='geometric')
gesture = detector.predict(landmarks) # вернет none или название жеста
```
### 2. ML-распознаванием статичных жестов
### 3. ML-распознаванием статичных жестов
Распознавание отдельных кадров с помощью обученного классификатора.
#### 2.1. Сбор датасета
#### 3.1. Сбор датасета
Создай папку для изображений. Можешь поместить туда фотографии жестов из интернета. Либо же можешь самостоятельно снять изображения с камеры:
Скрипт `utils/capture_photo.py` сохраняет изображения с камеры.
```
@@ -224,7 +290,7 @@ python3 utils/capture_photo.py --dir data/raw --camera_type web
Управление: `s` – начать обратный отсчёт (3 сек) и сохранить фото, `q` выход.
Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить.
#### 2.2. Разметка
#### 3.2. Разметка
Скрипт `utils/annotate.py` показывает каждое фото с распознанным скелетом и позволяет назначить класс:
```
python3 utils/annotate.py --folder data/raw --classes dome,cross,none --output data.csv
@@ -237,7 +303,7 @@ python3 utils/annotate.py --folder data/raw --classes dome,cross,none --output d
Управление: для каждого фото нажмите цифру, соответствующую жесту (1–dome, 2cross, 3none), или `n` для пропуска или `q` – выйти. При выходе данные сохранятся в `gesture_data.csv`. CSV с колонками `class`, `f0…f98` (99 нормализованных координат).
#### 2.3. Обучение модели
#### 3.3. Обучение модели
```
python3 ml_gestures/train.py --csv data.csv --model ml_gestures/models/special_model.pkl --type mlp --balance --target_classes dome,cross
```
@@ -252,7 +318,7 @@ python3 ml_gestures/train.py --csv data.csv --model ml_gestures/models/special_m
После обучения сохраняются модель и отчёт `special_model_report.json` с метриками (accuracy, precision/recall/f1 по классам, матрица ошибок).
#### 2.4. Оценка модели
#### 3.4. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `special_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте:
```
python3 ml_gestures/evaluate.py --csv data.csv --model ml_gestures/models/special_model.pkl --test_size 0.2
@@ -264,7 +330,7 @@ python3 ml_gestures/evaluate.py --csv data.csv --model ml_gestures/models/specia
Печатает результаты тестирования в консоль.
#### 2.5. Использование обученной модели
#### 3.5. Использование обученной модели
```
from ml_gestures.predict import MLGesturePredictor
@@ -272,9 +338,9 @@ predictor = MLGesturePredictor('model.pkl', class_names=['dome','cross','none'])
gesture = predictor.predict(landmarks) # возвращает строку с классом
```
### 3. ML-распознавание динамических жестов
### 4. ML-распознавание динамических жестов
Распознавание жестов по последовательности кадров с помощью LSTM.
#### 3.1. Сбор датасета
#### 4.1. Сбор датасета
Скрипт `utils/record_dynamic.py` записывает серию кадров (скелет) в течение заданной длительности.
```
python3 utils/record_dynamic --label wave_right --output dynamic_data.csv --duration 2.0
@@ -289,7 +355,7 @@ python3 utils/record_dynamic --label wave_right --output dynamic_data.csv --dura
Управление: нажмите `space`, через 3 секунды начнется запись, последовательность точек с меткой сохранится в файл `dynamic_data.csv`. В CSV сохраняются колонки: `label`, `sequence_id`, `frame_idx`, `f0…f98`. Нажмите `q` чтобы закончить.
**Важно**: при записи в консоль выводится число кадров последовательности – используйте его как ориентир для `--max_len` в обучении (можно округлить вверх).
#### 3.2. Обучение LSTM
#### 4.2. Обучение LSTM
```
python3 ml_gestures_dynamic/train --data dynamic_data.csv --model dynamic_model.h5 --max_len 14 --epochs 50 --test_size 0.2
```
@@ -304,7 +370,7 @@ python3 ml_gestures_dynamic/train --data dynamic_data.csv --model dynamic_model.
После обучения сохраняются: модель (`.h5`), файл с классами (`_classes.pkl`) и отчёт (`_report.json`) с метриками.
#### 3.3. Оценка модели
#### 4.3. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `dynamic_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте:
```
python3 ml_gestures_dynamic/evaluate --data dynamic_data.csv --model dynamic_model.h5 --max_len 14 --test_size 0.2
@@ -315,7 +381,7 @@ python3 ml_gestures_dynamic/evaluate --data dynamic_data.csv --model dynamic_mod
- `--max_len` – длина последовательности (количество кадров). Должен совпадать с длиной, использованной при записи. Если последовательности короче, они дополняются нулями; если длиннее – обрезаются.
- `--test_size` – доля тестовой выборки (по умолчанию 0.2).
#### 3.4. Использование в коде
#### 4.4. Использование в коде
Класс `DynamicGesturePredictor` накапливает кадры в буфере и выдаёт предсказание, когда накоплено достаточно данных.
```
from ml_gestures_dynamic.predict import DynamicGesturePredictor
+324
View File
@@ -0,0 +1,324 @@
import numpy as np
import cv2
def _clip_unit(v):
return float(np.clip(v, -1.0, 1.0))
def _apply_dead_zone(v, dz):
return 0.0 if abs(v) < dz else v
def _robust_metrics(landmarks, min_conf=0.5):
"""
Compute shoulder_center, shoulder_width, torso_height robustly.
Uses hips if available; otherwise falls back to nose/shoulder geometry.
Returns:
shoulder_center (np.array shape (2,))
shoulder_width (float)
torso_height (float)
ok (bool)
"""
def pt(i):
return np.array(landmarks[i][:2], dtype=float), float(landmarks[i][3])
l_sh, c_lsh = pt(11)
r_sh, c_rsh = pt(12)
if c_lsh < min_conf or c_rsh < min_conf:
return None, 0.0, 0.0, False
shoulder_center = (l_sh + r_sh) / 2.0
shoulder_width = float(np.linalg.norm(r_sh - l_sh))
if shoulder_width < 1e-3:
return shoulder_center, 0.0, 0.0, False
# Try hips
l_hip, c_lhip = pt(23)
r_hip, c_rhip = pt(24)
if c_lhip >= min_conf and c_rhip >= min_conf:
hip_center = (l_hip + r_hip) / 2.0
torso_height = float(np.linalg.norm(hip_center - shoulder_center))
if torso_height >= 1e-3:
return shoulder_center, shoulder_width, torso_height, True
# Fallbacks (upper-body only)
nose, c_nose = pt(0)
if c_nose >= min_conf:
nose_to_shoulder = abs(nose[1] - shoulder_center[1])
torso_height = max(1.6 * nose_to_shoulder, 0.9 * shoulder_width)
else:
torso_height = max(1.2 * shoulder_width, 1.0)
return shoulder_center, shoulder_width, float(torso_height), True
class ArmControllerMethod1:
"""
Method 1: Single-hand driving with right wrist.
- Linear: vertical offset of right wrist from shoulder center (normalized by torso height)
- Angular: horizontal offset of right wrist from shoulder center (normalized by shoulder width)
"""
def __init__(self, config, mirror=False):
self.config = config
self.mirror = mirror
self.dead_zone = config.get('dead_zone', 0.1)
self.debug = config.get('debug', False)
self.min_conf = config.get('min_conf', 0.5)
def compute_speeds(self, landmarks, frame_shape=None):
if landmarks is None:
return 0.0, 0.0
# Require: shoulders + right wrist
need = [11, 12, 16]
if any(landmarks[i][3] < self.min_conf for i in need):
return 0.0, 0.0
shoulder_center, shoulder_width, torso_height, ok = _robust_metrics(landmarks, self.min_conf)
if not ok or shoulder_width < 1e-3 or torso_height < 1e-3:
return 0.0, 0.0
r_wr = landmarks[16][:2]
# Positive linear when wrist above shoulder center (forward)
linear = (shoulder_center[1] - r_wr[1]) / torso_height
# Positive angular when wrist to the right of shoulder center
angular = (r_wr[0] - shoulder_center[0]) / shoulder_width
if self.mirror:
angular = -angular
linear = _clip_unit(_apply_dead_zone(linear, self.dead_zone))
angular = _clip_unit(_apply_dead_zone(angular, self.dead_zone))
if self.debug:
print(f"[M1] L:{linear:.2f} A:{angular:.2f}")
return linear, angular
def draw_overlay(self, frame, landmarks=None):
if frame is None:
return frame
h, w = frame.shape[:2]
# Draw center cross (screen center approximation)
cv2.line(frame, (w // 2, 0), (w // 2, h), (0, 0, 0), 1)
cv2.line(frame, (0, h // 2), (w, h // 2), (0, 0, 0), 1)
# Draw right wrist
if landmarks is not None and landmarks[16][3] > 0.5:
x, y = int(landmarks[16][0]), int(landmarks[16][1])
cv2.circle(frame, (x, y), 8, (0, 255, 255), -1)
return frame
class ArmControllerMethod2:
"""
Method 2: Two-hand blended control.
- Linear: average vertical offset of both wrists from shoulder center (normalized by torso height)
- Angular: horizontal balance of wrists around shoulder center (normalized by shoulder width)
"""
def __init__(self, config, mirror=False):
self.config = config
self.mirror = mirror
self.dead_zone = config.get('dead_zone', 0.1)
self.debug = config.get('debug', False)
self.min_conf = config.get('min_conf', 0.5)
def compute_speeds(self, landmarks, frame_shape=None):
if landmarks is None:
return 0.0, 0.0
# Require: shoulders + both wrists
need = [11, 12, 15, 16]
if any(landmarks[i][3] < self.min_conf for i in need):
return 0.0, 0.0
shoulder_center, shoulder_width, torso_height, ok = _robust_metrics(landmarks, self.min_conf)
if not ok or shoulder_width < 1e-3 or torso_height < 1e-3:
return 0.0, 0.0
l_wr = landmarks[15][:2]
r_wr = landmarks[16][:2]
# Linear: average elevation of both wrists
lin_l = (shoulder_center[1] - l_wr[1]) / torso_height
lin_r = (shoulder_center[1] - r_wr[1]) / torso_height
linear = 0.5 * (lin_l + lin_r)
# Angular: horizontal balance
ang = ((r_wr[0] - shoulder_center[0]) - (shoulder_center[0] - l_wr[0])) / shoulder_width
angular = ang
if self.mirror:
angular = -angular
linear = _clip_unit(_apply_dead_zone(linear, self.dead_zone))
angular = _clip_unit(_apply_dead_zone(angular, self.dead_zone))
if self.debug:
print(f"[M2] L:{linear:.2f} A:{angular:.2f}")
return linear, angular
def draw_overlay(self, frame, landmarks=None):
if frame is None:
return frame
if landmarks is not None:
for idx, color in [(15, (255, 0, 255)), (16, (0, 255, 255))]:
if landmarks[idx][3] > 0.5:
x, y = int(landmarks[idx][0]), int(landmarks[idx][1])
cv2.circle(frame, (x, y), 8, color, -1)
return frame
class ArmControllerMethod3:
"""
Method 3: Elbow-augmented control.
- Linear: average vertical offset of elbows (normalized by torso height)
- Angular: wrist horizontal balance (normalized by shoulder width)
"""
def __init__(self, config, mirror=False):
self.config = config
self.mirror = mirror
self.dead_zone = config.get('dead_zone', 0.1)
self.debug = config.get('debug', False)
self.min_conf = config.get('min_conf', 0.5)
def compute_speeds(self, landmarks, frame_shape=None):
if landmarks is None:
return 0.0, 0.0
# Require shoulders; prefer elbows for linear; wrists for angular.
need_base = [11, 12]
if any(landmarks[i][3] < self.min_conf for i in need_base):
return 0.0, 0.0
elbows_ok = (landmarks[13][3] >= self.min_conf and landmarks[14][3] >= self.min_conf)
wrists_ok = (landmarks[15][3] >= self.min_conf and landmarks[16][3] >= self.min_conf)
if not elbows_ok and not wrists_ok:
return 0.0, 0.0
shoulder_center, shoulder_width, torso_height, ok = _robust_metrics(landmarks, self.min_conf)
if not ok or shoulder_width < 1e-3 or torso_height < 1e-3:
return 0.0, 0.0
# Linear: prefer elbows, fallback to wrists average if elbows missing
if elbows_ok:
l_el = landmarks[13][:2]
r_el = landmarks[14][:2]
lin_l = (shoulder_center[1] - l_el[1]) / torso_height
lin_r = (shoulder_center[1] - r_el[1]) / torso_height
linear = 0.5 * (lin_l + lin_r)
else:
l_wr = landmarks[15][:2]
r_wr = landmarks[16][:2]
lin_l = (shoulder_center[1] - l_wr[1]) / torso_height
lin_r = (shoulder_center[1] - r_wr[1]) / torso_height
linear = 0.5 * (lin_l + lin_r)
# Angular: use wrists if available, else 0
if wrists_ok:
l_wr = landmarks[15][:2]
r_wr = landmarks[16][:2]
angular = ((r_wr[0] + l_wr[0]) - 2 * shoulder_center[0]) / shoulder_width
else:
angular = 0.0
if self.mirror:
angular = -angular
linear = _clip_unit(_apply_dead_zone(linear, self.dead_zone))
angular = _clip_unit(_apply_dead_zone(angular, self.dead_zone))
if self.debug:
print(f"[M3] L:{linear:.2f} A:{angular:.2f}")
return linear, angular
def draw_overlay(self, frame, landmarks=None):
if frame is None:
return frame
if landmarks is not None:
for idx, color in [(13, (0, 200, 0)), (14, (0, 200, 0)), (15, (0, 255, 255)), (16, (255, 0, 255))]:
if landmarks[idx][3] > 0.5:
x, y = int(landmarks[idx][0]), int(landmarks[idx][1])
cv2.circle(frame, (x, y), 6, color, -1)
return frame
class ArmControllerMethod4:
"""
Method 4: 3x3 grid based on landmark 19 (right index finger tip).
Screen split at 2/5 and 3/5 (both axes). Center band = 0.
Proportional speed away from the center bands.
"""
def __init__(self, config, mirror=False):
self.config = config
self.mirror = mirror
self.finger_idx = 19 # right index finger tip
self.debug = config.get('debug', False)
def compute_speeds(self, landmarks, frame_shape=None):
linear = 0.0
angular = 0.0
if frame_shape is None or landmarks is None:
return 0.0, 0.0
if landmarks[self.finger_idx][3] < 0.5:
return 0.0, 0.0
h, w = int(frame_shape[0]), int(frame_shape[1])
x = landmarks[self.finger_idx][0]
y = landmarks[self.finger_idx][1]
# Angular (horizontal): center band 2/5..3/5 = 0
if 2 * w / 5 <= x <= 3 * w / 5:
angular = 0.0
elif x > 3 * w / 5:
angular = (x * 5) / (2 * w) - 1
else:
angular = (x - 3 * w / 5) / (2 * w / 5)
# Linear (vertical): center band 2/5..3/5 = 0
if 2 * h / 5 <= y <= 3 * h / 5:
linear = 0.0
elif y > 3 * h / 5:
linear = -((y * 5) / (2 * h) - 1)
else:
linear = -(y - 3 * h / 5) / (2 * h / 5)
if self.mirror:
angular = -angular
if self.debug:
print(f"[M4] L:{linear:.2f} A:{angular:.2f}")
return _clip_unit(linear), _clip_unit(angular)
def draw_overlay(self, frame, landmarks=None):
if frame is None:
return frame
h, w = frame.shape[:2]
x1, x2 = int(w * 2 / 5), int(w * 3 / 5)
y1, y2 = int(h * 2 / 5), int(h * 3 / 5)
# Grid lines
cv2.line(frame, (x1, 0), (x1, h), (0, 0, 0), 2)
cv2.line(frame, (x2, 0), (x2, h), (0, 0, 0), 2)
cv2.line(frame, (0, y1), (w, y1), (0, 0, 0), 2)
cv2.line(frame, (0, y2), (w, y2), (0, 0, 0), 2)
# Highlight active cell + finger
if landmarks is not None and landmarks[self.finger_idx][3] > 0.5:
fx, fy = int(landmarks[self.finger_idx][0]), int(landmarks[self.finger_idx][1])
cx0, cx1 = (0, x1) if fx < x1 else ((x2, w) if fx > x2 else (x1, x2))
cy0, cy1 = (0, y1) if fy < y1 else ((y2, h) if fy > y2 else (y1, y2))
overlay = frame.copy()
cv2.rectangle(overlay, (cx0, cy0), (cx1, cy1), (0, 255, 255), -1)
frame = cv2.addWeighted(overlay, 0.2, frame, 0.8, 0)
cv2.circle(frame, (fx, fy), 8, (0, 255, 255), -1)
cv2.circle(frame, (fx, fy), 12, (0, 120, 120), 2)
return frame
+139 -71
View File
@@ -1,116 +1,184 @@
import numpy as np
from ml_gestures.predict import MLGesturePredictor
class SpecialGestureDetector:
def __init__(self, mode='geometric', model_path=None, class_names=None):
"""
Detects special static gestures using either simple geometric rules or an ML classifier.
Supported gesture labels:
- 'cross' : forearms crossed near the chest
- 'light' : right arm pose approximating a 'light' toggle
- 'dome' : arms forming a dome above the head
- 'none' : no special gesture detected
"""
def __init__(self, mode='geometric', model_path=None, class_names=None, debug=False, thresholds=None):
self.mode = mode
self.debug = debug
# Defaults for geometric detection
self.th = {
'min_conf': 0.5,
'shoulder_width_min': 30.0,
'torso_height_min': 10.0,
'chest_band': 0.25, # widened to be more forgiving
'wrists_near_factor': 0.6, # relaxed for dome
'elbow_far_factor': 0.9, # relaxed for dome
'light_elbow_min': 45.0,
'light_elbow_max': 120.0,
'light_shoulder_min': -5.0,
'light_shoulder_max': 20.0
}
if thresholds:
self.th.update(thresholds)
if mode == 'ml':
from ml_gestures.predict import MLGesturePredictor
if model_path is None or class_names is None:
raise ValueError("Для ML нужны model_path и class_names")
raise ValueError("For ML mode, provide model_path and class_names")
self.ml_predictor = MLGesturePredictor(model_path, class_names)
print("Использую статический ML классификатор")
if self.debug:
print("SpecialGestureDetector: Using ML classifier")
else:
self.ml_predictor = None
print("Использую геометрические отношения для детекции специальных жестов")
self.debug = False # Включите для отладки
if self.debug:
print("SpecialGestureDetector: Using geometric rules")
def predict(self, landmarks):
if landmarks is None:
return 'none'
if self.mode == 'geometric':
return self._geometric_predict(landmarks)
else:
return self.ml_predictor.predict(landmarks)
def _geometric_predict(self, landmarks):
# Индексы MediaPipe
idx = {
'nose': 0,
'left_shoulder': 11,
'right_shoulder': 12,
'left_elbow': 13,
'right_elbow': 14,
'left_wrist': 15,
'right_wrist': 16,
'left_hip': 23,
'right_hip': 24,
'left_shoulder': 11, 'right_shoulder': 12,
'left_elbow': 13, 'right_elbow': 14,
'left_wrist': 15, 'right_wrist': 16,
'left_hip': 23, 'right_hip': 24,
}
# Повышенный порог уверенности для специальных жестов
min_conf = 0.5
required = ['left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist', 'nose']
min_conf = self.th['min_conf']
# Only upper-body required (hips optional)
required = [
'left_shoulder', 'right_shoulder',
'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist',
'nose'
]
for p in required:
if landmarks[idx[p]][3] < min_conf:
if self.debug:
print(f"{p} low confidence")
print(f"[SG] Low confidence for {p}: {landmarks[idx[p]][3]:.2f}")
return 'none'
# Координаты (x, y)
l_sh = landmarks[idx['left_shoulder']][:2]
r_sh = landmarks[idx['right_shoulder']][:2]
l_el = landmarks[idx['left_elbow']][:2]
r_el = landmarks[idx['right_elbow']][:2]
l_wr = landmarks[idx['left_wrist']][:2]
r_wr = landmarks[idx['right_wrist']][:2]
l_hip = landmarks[idx['left_hip']][:2]
r_hip = landmarks[idx['right_hip']][:2]
nose = np.array(landmarks[idx['nose']][:2])
shoulder_center_y = (l_sh[1] + r_sh[1]) / 2
hip_center_y = (l_hip[1] + r_hip[1]) / 2
torso_height = hip_center_y - shoulder_center_y
l_sh = np.array(landmarks[idx['left_shoulder']][:2], dtype=float)
r_sh = np.array(landmarks[idx['right_shoulder']][:2], dtype=float)
l_el = np.array(landmarks[idx['left_elbow']][:2], dtype=float)
r_el = np.array(landmarks[idx['right_elbow']][:2], dtype=float)
l_wr = np.array(landmarks[idx['left_wrist']][:2], dtype=float)
r_wr = np.array(landmarks[idx['right_wrist']][:2], dtype=float)
nose = np.array(landmarks[idx['nose']][:2], dtype=float)
l_hip = np.array(landmarks[idx['left_hip']][:2], dtype=float)
r_hip = np.array(landmarks[idx['right_hip']][:2], dtype=float)
c_lhip = landmarks[idx['left_hip']][3]
c_rhip = landmarks[idx['right_hip']][3]
shoulder_center_y = (l_sh[1] + r_sh[1]) / 2.0
shoulder_width = np.linalg.norm(r_sh - l_sh)
if shoulder_width < 30 or torso_height < 10:
if shoulder_width < self.th['shoulder_width_min']:
if self.debug:
print(f"[SG] Shoulder width too small: {shoulder_width:.1f}")
return 'none'
# ---- Вспомогательные функции ----
def angle_between_vectors(v1, v2):
"""Угол между двумя векторами в градусах (0..180)"""
cos_a = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
return np.arccos(np.clip(cos_a, -1.0, 1.0)) * 180 / np.pi
# Torso height: prefer hips if visible, otherwise fallback using nose/shoulders
if c_lhip >= min_conf and c_rhip >= min_conf:
hip_center_y = (l_hip[1] + r_hip[1]) / 2.0
torso_height = hip_center_y - shoulder_center_y
else:
nose_to_shoulder = abs(nose[1] - shoulder_center_y)
torso_height = max(1.6 * nose_to_shoulder, 0.9 * shoulder_width)
hip_center_y = shoulder_center_y + torso_height
def elbow_angle(shoulder, elbow, wrist):
"""Угол в локте (плечо-локоть-запястье)"""
v1 = shoulder - elbow
v2 = wrist - elbow
if torso_height < self.th['torso_height_min']:
if self.debug:
print(f"[SG] Torso height too small: {torso_height:.1f}")
return 'none'
def angle_between_vectors(v1, v2):
n1 = np.linalg.norm(v1)
n2 = np.linalg.norm(v2)
if n1 < 1e-6 or n2 < 1e-6:
return 0.0
cos_a = np.dot(v1, v2) / (n1 * n2)
cos_a = float(np.clip(cos_a, -1.0, 1.0))
return np.degrees(np.arccos(cos_a))
def joint_angle(p_prev, p_joint, p_next):
v1 = p_prev - p_joint
v2 = p_next - p_joint
return angle_between_vectors(v1, v2)
# ---- Вычисляем углы ----
l_angle = elbow_angle(l_sh, l_el, l_wr) # угол в левом локте
r_angle = elbow_angle(r_sh, r_el, r_wr) # угол в правом локте
def segments_intersect(p1, p2, p3, p4):
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
d1 = cross(p3, p4, p1)
d2 = cross(p3, p4, p2)
d3 = cross(p1, p2, p3)
d4 = cross(p1, p2, p4)
return (d1 * d2 < 0) and (d3 * d4 < 0)
# ---- КРЕСТ ----
# 1. Оба локтя сильно согнуты (< 100°)
elbows_bent = (l_angle < 100 and r_angle < 100)
# 2. Левое запястье правее правого (перекрест)
wrists_crossed = l_wr[0] > r_wr[0] + 5 # небольшой запас в пикселях (можно и 0)
# 3. Запястья находятся между плечами и бёдрами по Y (уровень груди)
wrists_at_chest = (
shoulder_center_y - 0.3 * torso_height < l_wr[1] < hip_center_y + 0.3 * torso_height and
shoulder_center_y - 0.3 * torso_height < r_wr[1] < hip_center_y + 0.3 * torso_height
)
def line_intersection(p1, p2, p3, p4):
d1 = p2 - p1
d2 = p4 - p3
denom = d1[0] * d2[1] - d1[1] * d2[0]
if abs(denom) < 1e-6:
return None
t = ((p3[0] - p1[0]) * d2[1] - (p3[1] - p1[1]) * d2[0]) / denom
return p1 + t * d1
cross = elbows_bent and wrists_crossed and wrists_at_chest
if cross:
# Angles (geometric cues)
l_elbow_angle = joint_angle(l_sh, l_el, l_wr)
r_elbow_angle = joint_angle(r_sh, r_el, r_wr)
r_shoulder_like_angle = joint_angle(r_hip, r_sh, r_el)
# 1) CROSS
forearms_cross = segments_intersect(l_el, l_wr, r_el, r_wr)
intersection = line_intersection(l_el, l_wr, r_el, r_wr)
intersection_on_chest = False
if intersection is not None:
band = self.th['chest_band'] * torso_height
intersection_on_chest = (shoulder_center_y - band) < intersection[1] < (hip_center_y + band)
if self.debug:
print(f"[SG] cross_check: intersect={forearms_cross}, chest={intersection_on_chest}")
if forearms_cross and intersection_on_chest:
return 'cross'
# ---- ДОМИК ----
# 1. Запястья выше носа
wrists_above_nose = (l_wr[1] < nose[1] and r_wr[1] < nose[1])
# 2) LIGHT
if (self.th['light_elbow_min'] < r_elbow_angle < self.th['light_elbow_max'] and
self.th['light_shoulder_min'] < r_shoulder_like_angle < self.th['light_shoulder_max']):
return 'light'
# 2. Локти выше плеч (верхняя граница плеч – min по Y среди плеч)
# 3) DOME
wrists_above_nose = (l_wr[1] < nose[1] and r_wr[1] < nose[1])
shoulders_top_y = min(l_sh[1], r_sh[1])
elbows_above_shoulders = (l_el[1] < shoulders_top_y and r_el[1] < shoulders_top_y)
# 3. Расстояние между локтями > расстояние между плечами
elbow_distance = np.linalg.norm(l_el - r_el)
elbows_far_apart = elbow_distance > shoulder_width
# 4. Расстояние между запястьями < половины ширины плеч
elbows_far_apart = elbow_distance > (self.th['elbow_far_factor'] * shoulder_width)
wrist_distance = np.linalg.norm(l_wr - r_wr)
wrists_near = wrist_distance < 0.5 * shoulder_width
wrists_near = wrist_distance < (self.th['wrists_near_factor'] * shoulder_width)
dome = wrists_above_nose and elbows_above_shoulders and elbows_far_apart and wrists_near
if dome:
if self.debug:
print(f"[SG] dome_check: wrists_above={wrists_above_nose}, elbows_above={elbows_above_shoulders}, "
f"elbow_d={elbow_distance:.1f}, wrist_d={wrist_distance:.1f}")
if wrists_above_nose and elbows_above_shoulders and elbows_far_apart and wrists_near:
return 'dome'
return 'none'
+1
View File
@@ -6,6 +6,7 @@ pip install --upgrade pip
pip install numpy==1.24.3
pip install pandas==2.0.3
pip install pyyaml==6.0.3
pip install opencv-python==4.12.0.88
pip install opencv-python-headless==4.12.0.88
pip install matplotlib==3.7.5