11 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
moscovskayaliza 0415ff9f42 add bash install 2026-06-25 11:57:42 +03:00
4 changed files with 613 additions and 88 deletions
+101 -17
View File
@@ -19,7 +19,8 @@ gesture_rec/
├── skeleton/ # Детекция скелета ├── skeleton/ # Детекция скелета
│ ├── oak_pose_detector.py # Детектор на oak (efficienthrnet) │ ├── oak_pose_detector.py # Детектор на oak (efficienthrnet)
│ └── mediapipe_detector.py # MediaPipe │ └── mediapipe_detector.py # MediaPipe
├── gesture_control/ # Распознавание статичных жестов ├── gesture_control/
│ ├── arm_control.py # Преобразование позы в значения для угловой и линейной скоростей
│ └── special_gestures.py # Специальные жесты (геометрия или ML) │ └── special_gestures.py # Специальные жесты (геометрия или ML)
├── ml_gestures/ # ML для статичных жестов ├── ml_gestures/ # ML для статичных жестов
│ ├── feature_extractor.py # Нормализация landmarks │ ├── feature_extractor.py # Нормализация landmarks
@@ -51,17 +52,45 @@ cd gesture_rec
git submodule update --init --recursive git submodule update --init --recursive
``` ```
### 2. Создание виртуального окружения ### 2. Создание виртуального окружения
Рекомендуется использовать виртуальное окружение: Рекомендуется использовать виртуальное окружение и Python 3.10:
``` ```
python3.11 -m venv venv python3.10 -m venv venv
source venv/bin/activate source venv/bin/activate
``` ```
Если у вас не скачен питон этой версии, сначала выполните:
```
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. Установка зависимостей
### 3.1. Способ 1
Сделайте bash скрипт исполняемым и запустите последовательность установки:
chmod +x install_deps.sh
./install_deps.sh
### 3.2. Способ 2
Вручную:
pip install --upgrade pip pip install --upgrade pip
pip install numpy==1.24.3 pip install numpy==1.24.3
pip install pandas==2.0.3 pip install pandas==2.0.3
pip install pyyaml
pip install opencv-python==4.12.0.88 pip install opencv-python==4.12.0.88
pip install opencv-python-headless==4.12.0.88 pip install opencv-python-headless==4.12.0.88
pip install matplotlib==3.7.5 pip install matplotlib==3.7.5
@@ -91,6 +120,30 @@ source venv/bin/activate
requests requests
pip install numpy==1.24.3 pip install numpy==1.24.3
pip install scipy==1.11.0 pip install scipy==1.11.0
#### Типичные проблемы во время установки:
Важно: красные предупреждения о нехватке зависимостей для 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. Проверка работы камеры ### 4. Проверка работы камеры
Для веб-камеры достаточно, чтобы она была доступна по индексу (встроенная 0). Для OAK-D потребуется подключить устройство и установить права доступа (сделать это надо один раз): Для веб-камеры достаточно, чтобы она была доступна по индексу (встроенная 0). Для OAK-D потребуется подключить устройство и установить права доступа (сделать это надо один раз):
@@ -160,7 +213,38 @@ vis = detector.draw_landmarks(frame, landmarks)
Подробнее о параметрах модели в [репозитории](https://github.com/kschlegel/OAK-HumanPoseEstimation.git). Подробнее о параметрах модели в [репозитории](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'` анализирует координаты скелета и применяет набор правил. Класс `SpecialGestureDetector` в режиме `mode='geometric'` анализирует координаты скелета и применяет набор правил.
На текущий момент геометрически распознаются два жеста: На текущий момент геометрически распознаются два жеста:
@@ -169,7 +253,7 @@ vis = detector.draw_landmarks(frame, landmarks)
Все пороги (уверенность `min_conf`, коэффициенты) заданы внутри `_geometric_predict` и могут быть подстроены под ваши условия. Чтобы распознавать собственный жест, отредактируйте метод `_geometric_predict` в файле `gesture_control/special_gestures.py`. Все пороги (уверенность `min_conf`, коэффициенты) заданы внутри `_geometric_predict` и могут быть подстроены под ваши условия. Чтобы распознавать собственный жест, отредактируйте метод `_geometric_predict` в файле `gesture_control/special_gestures.py`.
#### 1.1. Порядок действий: #### 2.1. Порядок действий:
1. Определите, какие ключевые точки MediaPipe участвуют в жесте (список индексов см. в коде или в документации MediaPipe). 1. Определите, какие ключевые точки MediaPipe участвуют в жесте (список индексов см. в коде или в документации MediaPipe).
2. Напишите условие, используя координаты `(x, y)` нужных точек. Например, жест «рука в сторону»: `left_wrist[0] > left_shoulder[0] + width` и `right_wrist[0] < right_shoulder[0] - width`. 2. Напишите условие, используя координаты `(x, y)` нужных точек. Например, жест «рука в сторону»: `left_wrist[0] > left_shoulder[0] + width` и `right_wrist[0] < right_shoulder[0] - width`.
3. Вставьте проверку до финального return 'none', чтобы при совпадении условий возвращать строку с именем вашего жеста. 3. Вставьте проверку до финального return 'none', чтобы при совпадении условий возвращать строку с именем вашего жеста.
@@ -183,15 +267,15 @@ if arms_out:
``` ```
**Важно:** геометрический режим не требует обучения, но чувствителен к позе и ракурсу. Для более сложных жестов рекомендуем использовать ML. **Важно:** геометрический режим не требует обучения, но чувствителен к позе и ракурсу. Для более сложных жестов рекомендуем использовать ML.
#### 1.2. Использование в коде: #### 2.2. Использование в коде:
``` ```
from gesture_control.special_gestures import SpecialGestureDetector from gesture_control.special_gestures import SpecialGestureDetector
detector = SpecialGestureDetector(mode='geometric') detector = SpecialGestureDetector(mode='geometric')
gesture = detector.predict(landmarks) # вернет none или название жеста gesture = detector.predict(landmarks) # вернет none или название жеста
``` ```
### 2. ML-распознаванием статичных жестов ### 3. ML-распознаванием статичных жестов
Распознавание отдельных кадров с помощью обученного классификатора. Распознавание отдельных кадров с помощью обученного классификатора.
#### 2.1. Сбор датасета #### 3.1. Сбор датасета
Создай папку для изображений. Можешь поместить туда фотографии жестов из интернета. Либо же можешь самостоятельно снять изображения с камеры: Создай папку для изображений. Можешь поместить туда фотографии жестов из интернета. Либо же можешь самостоятельно снять изображения с камеры:
Скрипт `utils/capture_photo.py` сохраняет изображения с камеры. Скрипт `utils/capture_photo.py` сохраняет изображения с камеры.
``` ```
@@ -206,7 +290,7 @@ python3 utils/capture_photo.py --dir data/raw --camera_type web
Управление: `s` – начать обратный отсчёт (3 сек) и сохранить фото, `q` выход. Управление: `s` – начать обратный отсчёт (3 сек) и сохранить фото, `q` выход.
Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить. Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить.
#### 2.2. Разметка #### 3.2. Разметка
Скрипт `utils/annotate.py` показывает каждое фото с распознанным скелетом и позволяет назначить класс: Скрипт `utils/annotate.py` показывает каждое фото с распознанным скелетом и позволяет назначить класс:
``` ```
python3 utils/annotate.py --folder data/raw --classes dome,cross,none --output data.csv python3 utils/annotate.py --folder data/raw --classes dome,cross,none --output data.csv
@@ -219,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 нормализованных координат). Управление: для каждого фото нажмите цифру, соответствующую жесту (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 python3 ml_gestures/train.py --csv data.csv --model ml_gestures/models/special_model.pkl --type mlp --balance --target_classes dome,cross
``` ```
@@ -234,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 по классам, матрица ошибок). После обучения сохраняются модель и отчёт `special_model_report.json` с метриками (accuracy, precision/recall/f1 по классам, матрица ошибок).
#### 2.4. Оценка модели #### 3.4. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `special_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте: После обучения модель сохраняется, и создаётся отчёт `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 python3 ml_gestures/evaluate.py --csv data.csv --model ml_gestures/models/special_model.pkl --test_size 0.2
@@ -246,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 from ml_gestures.predict import MLGesturePredictor
@@ -254,9 +338,9 @@ predictor = MLGesturePredictor('model.pkl', class_names=['dome','cross','none'])
gesture = predictor.predict(landmarks) # возвращает строку с классом gesture = predictor.predict(landmarks) # возвращает строку с классом
``` ```
### 3. ML-распознавание динамических жестов ### 4. ML-распознавание динамических жестов
Распознавание жестов по последовательности кадров с помощью LSTM. Распознавание жестов по последовательности кадров с помощью LSTM.
#### 3.1. Сбор датасета #### 4.1. Сбор датасета
Скрипт `utils/record_dynamic.py` записывает серию кадров (скелет) в течение заданной длительности. Скрипт `utils/record_dynamic.py` записывает серию кадров (скелет) в течение заданной длительности.
``` ```
python3 utils/record_dynamic --label wave_right --output dynamic_data.csv --duration 2.0 python3 utils/record_dynamic --label wave_right --output dynamic_data.csv --duration 2.0
@@ -271,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` чтобы закончить. Управление: нажмите `space`, через 3 секунды начнется запись, последовательность точек с меткой сохранится в файл `dynamic_data.csv`. В CSV сохраняются колонки: `label`, `sequence_id`, `frame_idx`, `f0…f98`. Нажмите `q` чтобы закончить.
**Важно**: при записи в консоль выводится число кадров последовательности – используйте его как ориентир для `--max_len` в обучении (можно округлить вверх). **Важно**: при записи в консоль выводится число кадров последовательности – используйте его как ориентир для `--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 python3 ml_gestures_dynamic/train --data dynamic_data.csv --model dynamic_model.h5 --max_len 14 --epochs 50 --test_size 0.2
``` ```
@@ -286,7 +370,7 @@ python3 ml_gestures_dynamic/train --data dynamic_data.csv --model dynamic_model.
После обучения сохраняются: модель (`.h5`), файл с классами (`_classes.pkl`) и отчёт (`_report.json`) с метриками. После обучения сохраняются: модель (`.h5`), файл с классами (`_classes.pkl`) и отчёт (`_report.json`) с метриками.
#### 3.3. Оценка модели #### 4.3. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `dynamic_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте: После обучения модель сохраняется, и создаётся отчёт `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 python3 ml_gestures_dynamic/evaluate --data dynamic_data.csv --model dynamic_model.h5 --max_len 14 --test_size 0.2
@@ -297,7 +381,7 @@ python3 ml_gestures_dynamic/evaluate --data dynamic_data.csv --model dynamic_mod
- `--max_len` – длина последовательности (количество кадров). Должен совпадать с длиной, использованной при записи. Если последовательности короче, они дополняются нулями; если длиннее – обрезаются. - `--max_len` – длина последовательности (количество кадров). Должен совпадать с длиной, использованной при записи. Если последовательности короче, они дополняются нулями; если длиннее – обрезаются.
- `--test_size` – доля тестовой выборки (по умолчанию 0.2). - `--test_size` – доля тестовой выборки (по умолчанию 0.2).
#### 3.4. Использование в коде #### 4.4. Использование в коде
Класс `DynamicGesturePredictor` накапливает кадры в буфере и выдаёт предсказание, когда накоплено достаточно данных. Класс `DynamicGesturePredictor` накапливает кадры в буфере и выдаёт предсказание, когда накоплено достаточно данных.
``` ```
from ml_gestures_dynamic.predict import 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 import numpy as np
from ml_gestures.predict import MLGesturePredictor
class SpecialGestureDetector: 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.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': if mode == 'ml':
from ml_gestures.predict import MLGesturePredictor
if model_path is None or class_names is None: 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) self.ml_predictor = MLGesturePredictor(model_path, class_names)
print("Использую статический ML классификатор") if self.debug:
print("SpecialGestureDetector: Using ML classifier")
else: else:
self.ml_predictor = None self.ml_predictor = None
print("Использую геометрические отношения для детекции специальных жестов") if self.debug:
self.debug = False # Включите для отладки print("SpecialGestureDetector: Using geometric rules")
def predict(self, landmarks): def predict(self, landmarks):
if landmarks is None:
return 'none'
if self.mode == 'geometric': if self.mode == 'geometric':
return self._geometric_predict(landmarks) return self._geometric_predict(landmarks)
else: else:
return self.ml_predictor.predict(landmarks) return self.ml_predictor.predict(landmarks)
def _geometric_predict(self, landmarks): def _geometric_predict(self, landmarks):
# Индексы MediaPipe
idx = { idx = {
'nose': 0, 'nose': 0,
'left_shoulder': 11, 'left_shoulder': 11, 'right_shoulder': 12,
'right_shoulder': 12, 'left_elbow': 13, 'right_elbow': 14,
'left_elbow': 13, 'left_wrist': 15, 'right_wrist': 16,
'right_elbow': 14, 'left_hip': 23, 'right_hip': 24,
'left_wrist': 15,
'right_wrist': 16,
'left_hip': 23,
'right_hip': 24,
} }
# Повышенный порог уверенности для специальных жестов min_conf = self.th['min_conf']
min_conf = 0.5 # Only upper-body required (hips optional)
required = ['left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow', required = [
'left_wrist', 'right_wrist', 'nose'] 'left_shoulder', 'right_shoulder',
'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist',
'nose'
]
for p in required: for p in required:
if landmarks[idx[p]][3] < min_conf: if landmarks[idx[p]][3] < min_conf:
if self.debug: if self.debug:
print(f"{p} low confidence") print(f"[SG] Low confidence for {p}: {landmarks[idx[p]][3]:.2f}")
return 'none' return 'none'
# Координаты (x, y) l_sh = np.array(landmarks[idx['left_shoulder']][:2], dtype=float)
l_sh = landmarks[idx['left_shoulder']][:2] r_sh = np.array(landmarks[idx['right_shoulder']][:2], dtype=float)
r_sh = landmarks[idx['right_shoulder']][:2] l_el = np.array(landmarks[idx['left_elbow']][:2], dtype=float)
l_el = landmarks[idx['left_elbow']][:2] r_el = np.array(landmarks[idx['right_elbow']][:2], dtype=float)
r_el = landmarks[idx['right_elbow']][:2] l_wr = np.array(landmarks[idx['left_wrist']][:2], dtype=float)
l_wr = landmarks[idx['left_wrist']][:2] r_wr = np.array(landmarks[idx['right_wrist']][:2], dtype=float)
r_wr = landmarks[idx['right_wrist']][:2] nose = np.array(landmarks[idx['nose']][:2], dtype=float)
l_hip = landmarks[idx['left_hip']][:2]
r_hip = landmarks[idx['right_hip']][:2] l_hip = np.array(landmarks[idx['left_hip']][:2], dtype=float)
nose = np.array(landmarks[idx['nose']][:2]) r_hip = np.array(landmarks[idx['right_hip']][:2], dtype=float)
c_lhip = landmarks[idx['left_hip']][3]
shoulder_center_y = (l_sh[1] + r_sh[1]) / 2 c_rhip = landmarks[idx['right_hip']][3]
hip_center_y = (l_hip[1] + r_hip[1]) / 2
torso_height = hip_center_y - shoulder_center_y shoulder_center_y = (l_sh[1] + r_sh[1]) / 2.0
shoulder_width = np.linalg.norm(r_sh - l_sh) 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' return 'none'
# ---- Вспомогательные функции ---- # Torso height: prefer hips if visible, otherwise fallback using nose/shoulders
def angle_between_vectors(v1, v2): if c_lhip >= min_conf and c_rhip >= min_conf:
"""Угол между двумя векторами в градусах (0..180)""" hip_center_y = (l_hip[1] + r_hip[1]) / 2.0
cos_a = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6) torso_height = hip_center_y - shoulder_center_y
return np.arccos(np.clip(cos_a, -1.0, 1.0)) * 180 / np.pi 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): if torso_height < self.th['torso_height_min']:
"""Угол в локте (плечо-локоть-запястье)""" if self.debug:
v1 = shoulder - elbow print(f"[SG] Torso height too small: {torso_height:.1f}")
v2 = wrist - elbow 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) return angle_between_vectors(v1, v2)
# ---- Вычисляем углы ---- def segments_intersect(p1, p2, p3, p4):
l_angle = elbow_angle(l_sh, l_el, l_wr) # угол в левом локте def cross(o, a, b):
r_angle = elbow_angle(r_sh, r_el, r_wr) # угол в правом локте 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)
# ---- КРЕСТ ---- def line_intersection(p1, p2, p3, p4):
# 1. Оба локтя сильно согнуты (< 100°) d1 = p2 - p1
elbows_bent = (l_angle < 100 and r_angle < 100) d2 = p4 - p3
# 2. Левое запястье правее правого (перекрест) denom = d1[0] * d2[1] - d1[1] * d2[0]
wrists_crossed = l_wr[0] > r_wr[0] + 5 # небольшой запас в пикселях (можно и 0) if abs(denom) < 1e-6:
# 3. Запястья находятся между плечами и бёдрами по Y (уровень груди) return None
wrists_at_chest = ( t = ((p3[0] - p1[0]) * d2[1] - (p3[1] - p1[1]) * d2[0]) / denom
shoulder_center_y - 0.3 * torso_height < l_wr[1] < hip_center_y + 0.3 * torso_height and return p1 + t * d1
shoulder_center_y - 0.3 * torso_height < r_wr[1] < hip_center_y + 0.3 * torso_height
)
cross = elbows_bent and wrists_crossed and wrists_at_chest # Angles (geometric cues)
if cross: 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' return 'cross'
# ---- ДОМИК ---- # 2) LIGHT
# 1. Запястья выше носа if (self.th['light_elbow_min'] < r_elbow_angle < self.th['light_elbow_max'] and
wrists_above_nose = (l_wr[1] < nose[1] and r_wr[1] < nose[1]) 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]) 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) 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) elbow_distance = np.linalg.norm(l_el - r_el)
elbows_far_apart = elbow_distance > shoulder_width elbows_far_apart = elbow_distance > (self.th['elbow_far_factor'] * shoulder_width)
# 4. Расстояние между запястьями < половины ширины плеч
wrist_distance = np.linalg.norm(l_wr - r_wr) 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 self.debug:
if dome: 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 'dome'
return 'none' return 'none'
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
set -e
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
pip install protobuf==3.20.3
# PyTorch CPU - нужен только для tensorflow, достаточно под CPU в целом, но можно поставить GPU, но тогда возиться с CUDA
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
# MediaPipe
pip install --no-deps mediapipe==0.10.11
pip install attrs
pip install scikit-learn==1.3.2
pip install joblib==1.4.2
pip install depthai==2.28.0
# TensorFlow
pip install --no-deps tensorflow==2.13.1
pip install \
absl-py \
astunparse \
flatbuffers \
gast \
google-pasta \
grpcio \
h5py \
keras==2.13.1 \
libclang \
opt-einsum \
tensorboard==2.13.0 \
tensorflow-estimator==2.13.0 \
termcolor \
wrapt \
requests
pip install numpy==1.24.3
pip install scipy==1.11.0
echo "все установлено"