19 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
moscovskayaliza c72ff7ded0 fixed geom gestures 2026-06-25 10:42:40 +03:00
moscovskayaliza 2999f8f274 upd installation guide 2026-06-24 19:20:26 +03:00
moscovskayaliza f494a214fd upd req 2026-06-24 15:43:12 +03:00
moscovskayaliza 028adb6f06 upd readme with oak problem 2026-06-24 14:24:23 +03:00
moscovskayaliza fbe72d28d0 upd readme 2026-06-24 12:01:47 +03:00
moscovskayaliza 24ba4e3840 started new readme 2026-06-23 17:50:49 +03:00
moscovskayaliza e2e24a5e7e oak detector skelet 2026-06-23 14:51:45 +03:00
moscovskayaliza 266adb892a upd readme 2026-06-23 11:16:51 +03:00
9 changed files with 1098 additions and 184 deletions
+4
View File
@@ -0,0 +1,4 @@
[submodule "submodules/OAK-HumanPoseEstimation"]
path = submodules/OAK-HumanPoseEstimation
url = https://github.com/kschlegel/OAK-HumanPoseEstimation.git
branch = main
+376 -122
View File
@@ -1,163 +1,417 @@
## О проекте # Распознавание жестов на основе скелета
Проект позволяет управлять роботом (в симуляторе) с помощью жестов рук, распознаваемых через камеру. Используется MediaPipe для детекции скелета. Проект позволяет распознавать статические и динамические жесты человека по данным скелета, полученным с помощью MediaPipe (или OAK‑камеры). Реализованы:
- детекция скелета (33 ключевые точки);
- геометрическое и ML‑распознавание специальных жестов (например, «домик», «крест»);
- сбор и разметка собственных наборов данных;
- обучение моделей (MLP, Random Forest, Logistic Regression для статики; LSTM – для динамики);
- оценка качества моделей.
**Проект не включает симулятор робота или управление движением** – это отдельный репозиторий. Здесь представлена только библиотека распознавания жестов.
## Структура проекта ## Структура проекта
``` ```
gesture_robot/ gesture_rec/
├── skeleton/ # Детекция скелета (MediaPipe) ├── camera # Методы для захвата видеоряда с разных источников
── mediapipe_detector.py ── base_camera.py # Базовый класс
├── gesture_control/ # Управление жестами │ ├── factory.py # Конвейер создания камеры
── special_gestures.py # Специальные жесты по геометрии ── oak_camera.py # Реализация для oak
├── ml_gestures/ # ML для специальных жестов │ └── web_camera.py # Реализация для web
├── skeleton/ # Детекция скелета
│ ├── oak_pose_detector.py # Детектор на oak (efficienthrnet)
│ └── mediapipe_detector.py # MediaPipe
├── gesture_control/
│ ├── arm_control.py # Преобразование позы в значения для угловой и линейной скоростей
│ └── special_gestures.py # Специальные жесты (геометрия или ML)
├── ml_gestures/ # ML для статичных жестов
│ ├── feature_extractor.py # Нормализация landmarks
│ ├── predict.py # класс-обёртка для предсказания
│ └── train.py # обучение классификаторов
├── ml_gestures_dynamic/ # ML для динамических жестов
│ ├── feature_extractor.py │ ├── feature_extractor.py
│ ├── predict.py │ ├── sequence_utils.py # Загрузка данных
── train.py ── predict.py # Класс для предсказания (с буфером)
├── ml_gestures_dynamic/ # ML для динамических жестов │ ├── evaluate.py # Оценка LSTM
── feature_extractor.py ── train.py # Обучение LSTM
│ ├── sequence_utils.py ├── utils/ # Вспомогательные скрипты
│ ├── predict.py │ ├── annotate.py # Разметка изображений
│ ├── evaluate.py │ ├── capture_photo.py # Съёмка фото с камеры
│ └── train.py │ └── record_dynamic.py # Разметка видеопоследовательности
└── utils/ # Вспомогательные скрипты └── submodules # Внешние зависимости (git submodule)
── annotate.py # Разметка изображений ── OAK-HumanPoseEstimation # Репозиторий для работы с OAK
├── capture_photo.py # Съёмка фото с камеры
└── record_dynamic.py # Разметка видеопоследовательности
``` ```
## Подготовка и запуск ## Установка и настройка
### 1. Клонирование репозитория
### Установка зависимостей
pip install -r requirements.txt
### Настройка параметров
Все основные параметры вынесены в `config.py`. Основные:
- `CAMERA_ID` - индекс камеры (по умолчанию 0)
- `MIRROR_CAMERA` - зеркальное отображение (True для фронтальной камеры).
- `ARM_CONTROL` - настройки управления жестами (геометрией) управления
- `linear_arm` - рука, отвечающая за изменение линейной скорости
- `angular_arm` - рука, отвечающая за изменение угловой скорости
- `max_speed_linear` - максимальная линейная скорость (по умолчанию 1)
- `max_speed_angular` - максимальная угловая скорость (по умолчанию 1)
- `dead_zone` - мёртвая зона - доля от ширины плеч/высоты торса (по умолчанию 0.2) для предотвращения ложных срабатываний
- `debug` - Режим отладки, вкл/выкл логи (по умолчанию False)
- `SPECIAL_GESTURE_MODE` - способ детекции специальных статических жестов (ml или geometric)
- `ML_GESTURE_MODEL` - путь до весов ml модели статических жестов
- `ML_GESTURE_CLASSES` - список классов детектируемых жестов, + none
- `ROBOT_MODE` - simulator (мини игра проехать роботом мимо препятствий) или dummy (простой модуль, пишуший отправленную команду, для первичной отладки)
- `ROBOT_IMAGE_PATH` - путь до картинки робота, который будет ездить в симуляции (если не указать будет треугольник просто)
- параметры для симулятора карты:
- `MAP_WIDTH` - ширина карты
- `MAP_HEIGHT` - высота карты
- `MAP_OBSTACLES` - лист препятствий в формате (x, y, width, height)
- `START_POS` - стартовая позиция робота
- `FINISH_POS` - позиция финиша
- `ROBOT_RADIUS` - радиус робота
### Примеры запуска
#### Запуск с геометрическим распознаванием
1. Настройте `config.py`:
- `MIRROR_CAMERA = True` – для фронтальной камеры.
- `SPECIAL_GESTURE_MODE = 'geometric'`.
2. Запустите основной скрипт:
``` ```
python3 main.py git clone --recursive https://git.robofob.ru/sirius/gesture_rec.git
cd gesture_rec
```
Если вдруг уже склонировали без `--recursive`, выполните подгрузку модулей:
```
cd gesture_rec
git submodule update --init --recursive
```
### 2. Создание виртуального окружения
Рекомендуется использовать виртуальное окружение и Python 3.10:
```
python3.10 -m venv venv
source venv/bin/activate
``` ```
3. Запустится симулятор и изображение с камеры
#### Запуск с ML-распознаванием статических жестов Если у вас не скачен питон этой версии, сначала выполните:
1. Соберите датасет с нужными изображениями фото. Можете заснять собственные через `utils/capture_photo.py`:
``` ```
python3 -m utils/capture_photo.py --dir data/raw 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
Сделайте bash скрипт исполняемым и запустите последовательность установки:
chmod +x install_deps.sh
./install_deps.sh
### 3.2. Способ 2
Вручную:
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
pip install protobuf==3.20.3
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
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
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
#### Типичные проблемы во время установки:
Важно: красные предупреждения о нехватке зависимостей для 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 потребуется подключить устройство и установить права доступа (сделать это надо один раз):
1. Подключите камеру по usb, если сразу запустите скрипт, то вылетит с ошибкой `No available devices`.
2. Выполните команду:
```
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="03e7", MODE="0666"' | sudo tee /etc/udev/rules.d/80-movidius.rules
```
3. Затем:
```
sudo udevadm control --reload-rules && sudo udevadm trigger
```
4. После этого отключите камеру от USB и подключите снова. Теперь камера должна быть доступна.
Также OAK-D можно использовать, чтобы прям на камере выполнять вычисление скелета, для этого убедитесь, что модуль `OAK-HumanPoseEstimation` загружен - его код используется в `skeleton/oak_pose_detector.py`.
Затем перейдите в эту подпапку и скачайте модели детектирования скелета:
```
cd submodules/OAK-HumanPoseEstimation/models/
wget "https://drive.google.com/uc?export=download&id=1AUszSCMSc5dCATnZn1jK8PzMMyLQ__5M" -O models.zip
unzip models.zip
```
Если команда не работает, просто перейдите по ссылке в браузере и скачайте вручную.
## Работа с проектом
Все скрипты (обучение, сбор данных, оценка) рассчитаны на запуск из корневой директории проекта.
### 0. Распознавание скелета
#### 0.1. MediaPipe
Основной класс `skeleton.mediapipe_detector.MediaPipeDetector`. Этот детектор должен принимать на вход изображение:
```
from skeleton.mediapipe_detector import MediaPipeDetector
detector = MediaPipeDetector(model_complexity=1, min_detection_confidence=0.5)
result = detector.detect(frame_bgr)
if result['success']:
landmarks = result['landmarks'] # (33,4) x, y, z, visibility
# для отрисовки:
vis = detector.draw_landmarks(frame, result['pose_landmarks'])
```
Параметры:
- `model_complexity` – 0 (лёгкая), 1 (средняя) или 2 (тяжёлая). Влияет на точность и FPS.
- `min_detection_confidence` – порог уверенности для детекции (0.5–0.9).
**Важно**: этой модели нужно получать само изображение, поэтому тип камеры для неё не важен, работает со всеми.
#### 0.2. На борту OAK
Для экономии ресурсов, можно детектировать скелет сразу с камеры. Такой метод работает **только с OAK камерой, и недоступен для web**. Класс - `skeleton.oak_pose_detector.OakPoseDetector`. Хоть функционал похож с детектором MediaPipe - основное отличие в том, что нельзя подать на вход изображение.
```
from skeleton.oak_pose_detector import OakPoseDetector
detector = OakPoseDetector(
model_type='efficienthrnet1',
detection_threshold=0.2,
shaves=6
)
frame, landmarks = detector.get_frame_and_pose()
# для отрисовки:
vis = detector.draw_landmarks(frame, landmarks)
```
Подробнее о параметрах модели в [репозитории](https://github.com/kschlegel/OAK-HumanPoseEstimation.git).
### 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'` анализирует координаты скелета и применяет набор правил.
На текущий момент геометрически распознаются два жеста:
- "домик"
- "крест"
Все пороги (уверенность `min_conf`, коэффициенты) заданы внутри `_geometric_predict` и могут быть подстроены под ваши условия. Чтобы распознавать собственный жест, отредактируйте метод `_geometric_predict` в файле `gesture_control/special_gestures.py`.
#### 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', чтобы при совпадении условий возвращать строку с именем вашего жеста.
Пример добавления жеста "руки в стороны":
```
arms_out = (l_wr[0] > l_sh[0] + shoulder_width*0.5 and
r_wr[0] < r_sh[0] - shoulder_width*0.5)
if arms_out:
return 'arms_out'
```
**Важно:** геометрический режим не требует обучения, но чувствителен к позе и ракурсу. Для более сложных жестов рекомендуем использовать ML.
#### 2.2. Использование в коде:
```
from gesture_control.special_gestures import SpecialGestureDetector
detector = SpecialGestureDetector(mode='geometric')
gesture = detector.predict(landmarks) # вернет none или название жеста
```
### 3. ML-распознаванием статичных жестов
Распознавание отдельных кадров с помощью обученного классификатора.
#### 3.1. Сбор датасета
Создай папку для изображений. Можешь поместить туда фотографии жестов из интернета. Либо же можешь самостоятельно снять изображения с камеры:
Скрипт `utils/capture_photo.py` сохраняет изображения с камеры.
```
python3 utils/capture_photo.py --dir data/raw --camera_type web
```
Параметры запуска:
- `--dir` - папка для сохранения (по умолчанию `captured` в корне проекта).
- `--camera` - ID веб-камеры (по умолчанию 0).
- `--camera_type` - `web` или `oak` (по умолчанию `web`).
- `--no-mirror` - отключить зеркалирование (по умолчанию включено).
Управление: `s` – начать обратный отсчёт (3 сек) и сохранить фото, `q` выход.
Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить. Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить.
2. Разметьте фото с помощью аннотатора: #### 3.2. Разметка
Скрипт `utils/annotate.py` показывает каждое фото с распознанным скелетом и позволяет назначить класс:
``` ```
python3 -m 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
``` ```
Для каждого фото нажмите цифру, соответствующую жесту (1–dome, 2cross, 3none), или n для пропуска. При выходе данные сохранятся в `data.csv`. Параметры запуска:
- `--folder` – папка с изображениями.
- `--classes` – список классов через запятую (порядок соответствует цифрам 1,2,3…).
- `--output` – выходной CSV (по умолчанию gesture_data.csv).
- `--max_display_size` – размер окна для предпросмотра (ширина,высота, по умолчанию 800,600).
3. Обучите модель Управление: для каждого фото нажмите цифру, соответствующую жесту (1–dome, 2cross, 3none), или `n` для пропуска или `q` – выйти. При выходе данные сохранятся в `gesture_data.csv`. CSV с колонками `class`, `f0…f98` (99 нормализованных координат).
```
python3 -m ml_gestures/train.py --csv data.csv --model ml_gestures/models/special_model.pkl --type mlp --balance --target_classes dome,cross
```
Параметр `balance` уменьшит целевые классы (в параметре `target_classes`) до размера наименьшего из них, чтобы избежать перекоса. Класс none остаётся неизменным.
4. Оценка модели #### 3.3. Обучение модели
```
python3 ml_gestures/train.py --csv data.csv --model ml_gestures/models/special_model.pkl --type mlp --balance --target_classes dome,cross
```
Параметры запуска:
- `--csv` – путь к размеченному CSV (обязательно).
- `--model` – путь для сохранения модели (обязательно, расширение .pkl).
- `--type` – тип модели: linear (Logistic Regression), mlp (MLPClassifier) или rf (RandomForestClassifier). По умолчанию mlp.
- `--test_size` – доля тестовой выборки (по умолчанию 0.2).
- `--random_state` – seed для воспроизводимости (по умолчанию 42).
- `--balance` – если указан, балансирует целевые классы (указанные в --target_classes) до минимального размера среди них. Класс none не трогается.
- `--target_classes` – список классов для балансировки через запятую (по умолчанию все классы, кроме none).
После обучения сохраняются модель и отчёт `special_model_report.json` с метриками (accuracy, precision/recall/f1 по классам, матрица ошибок).
#### 3.4. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `special_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте: После обучения модель сохраняется, и создаётся отчёт `special_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте:
``` ```
python3 -m 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
``` ```
5. Подключите модель в `config.py` - `--csv` – путь к размеченному CSV (обязательно).
``` - `--model` – путь к сохраненной модели (обязательно, расширение .pkl).
SPECIAL_GESTURE_MODE = 'ml' - `--test_size` – доля тестовой выборки (по умолчанию 0.2).
ML_GESTURE_MODEL = 'ml_gestures/models/special_model.pkl' - `--random_state` – seed для воспроизводимости (по умолчанию 42).
ML_GESTURE_CLASSES = ['dome', 'cross', 'none']
```
6. Запустите основной скрипт:
```
python3 main.py
```
7. Запустится симулятор и изображение с камеры
#### Запуск с ML-распознаванием динамических жестов Печатает результаты тестирования в консоль.
1. Соберите датасетс нужными последовательностями жестов.
#### 3.5. Использование обученной модели
``` ```
python -m utils.record_dynamic --label wave_right --output dynamic_data.csv --duration 2.0 from ml_gestures.predict import MLGesturePredictor
predictor = MLGesturePredictor('model.pkl', class_names=['dome','cross','none'])
gesture = predictor.predict(landmarks) # возвращает строку с классом
``` ```
Нажмите `space`, подождите 3 секунды, последовательность точек с меткой сохранится в файл `dynamic_data.csv`. Нажмите `q` чтобы закончить.
2. Обучите модель ### 4. ML-распознавание динамических жестов
Распознавание жестов по последовательности кадров с помощью LSTM.
#### 4.1. Сбор датасета
Скрипт `utils/record_dynamic.py` записывает серию кадров (скелет) в течение заданной длительности.
``` ```
python -m ml_gestures_dynamic.train --data dynamic_data.csv --model dynamic_model.h5 --max_len 14 --epochs 50 --test_size 0.2 python3 utils/record_dynamic --label wave_right --output dynamic_data.csv --duration 2.0
``` ```
Укажите путь до вашего файла, укажите путь, куда сохранить модель, а также укажите длину последовательности кадров (зависит от вашей камеры и железа, будет выводится при сборе данных) Параметры запуска:
3. Оценка модели - `--label` – название жеста (обязательно).
- `--output` – CSV-файл для сохранения (по умолчанию `dynamic_data.csv`).
- `--duration` – длительность записи в секундах (по умолчанию 3.0).
- `--camera_type` – web или oak (по умолчанию web).
- `--no-mirror` – отключить зеркалирование
Управление: нажмите `space`, через 3 секунды начнется запись, последовательность точек с меткой сохранится в файл `dynamic_data.csv`. В CSV сохраняются колонки: `label`, `sequence_id`, `frame_idx`, `f0…f98`. Нажмите `q` чтобы закончить.
**Важно**: при записи в консоль выводится число кадров последовательности – используйте его как ориентир для `--max_len` в обучении (можно округлить вверх).
#### 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
```
Параметры запуска:
- `--data` – путь к CSV-файлу или папке с несколькими CSV (все будут объединены).
- `--model` – путь для сохранения модели (.h5).
- `--max_len` – длина последовательности (количество кадров). Должен совпадать с длиной, использованной при записи. Если последовательности короче, они дополняются нулями; если длиннее – обрезаются.
- `--lstm_units` – число нейронов в LSTM (по умолчанию 64).
- `--epochs` – количество эпох (по умолчанию 50).
- `--batch_size` – размер батча (по умолчанию 16).
- `--test_size` – доля тестовой выборки (по умолчанию 0.2).
После обучения сохраняются: модель (`.h5`), файл с классами (`_classes.pkl`) и отчёт (`_report.json`) с метриками.
#### 4.3. Оценка модели
После обучения модель сохраняется, и создаётся отчёт `dynamic_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте: После обучения модель сохраняется, и создаётся отчёт `dynamic_model_report.json` с метриками (`accuracy`, `precision`, `recall`, `f1`, `confusion matrix`). Для повторной оценки используйте:
``` ```
python -m 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
```
5. Подключить модель в `config.py`
```
DYNAMIC_GESTURE = {
'enabled': True, # включить/выключить
'model_path': 'ml_gestures/models/dynamic_model.h5',
'classes_path': 'ml_gestures//odels/dynamic_model_classes.pkl',
'window_size': 14, # длина буфера
'threshold': 0.7, # порог уверенности
'actions': {
'wave_left': 'reset', # при жесте wave_left – перезапуск симулятора
'wave_right': 'restart', # при wave_right рестарт
}
``` ```
Параметры запуска:
- `--data` – путь к CSV-файлу или папке с несколькими CSV (все будут объединены).
- `--model` – путь для сохранения модели (.h5).
- `--max_len` – длина последовательности (количество кадров). Должен совпадать с длиной, использованной при записи. Если последовательности короче, они дополняются нулями; если длиннее – обрезаются.
- `--test_size` – доля тестовой выборки (по умолчанию 0.2).
### Управление в симуляторе #### 4.4. Использование в коде
Симулятор – поле с препятствиями, стартом и финишем. Робот движется согласно командам. При столкновении – игра заканчивается (перезапуск по `r`). Закрытие окна игры или нажатие `q` в окне камеры – выход. Класс `DynamicGesturePredictor` накапливает кадры в буфере и выдаёт предсказание, когда накоплено достаточно данных.
1. Управление скоростями ```
- Линейная скорость (вперёд) – горизонтальное положение правой руки (рука вдоль тела – 0, вытянута в сторону – максимум). from ml_gestures_dynamic.predict import DynamicGesturePredictor
- Угловая скорость – вертикальное положение левой руки (рука на уровне плеча – 0, вверх – поворот вправо, вниз – поворот влево).
![Схема управления скоростями](images/arm_control.png)
2. Специальные жесты. Режим распознавания может быть геометрическим (по правилам) или обучаемым (ML-модель). predictor = DynamicGesturePredictor(
- Домик – обе руки над головой (включает управление). model_path='dynamic_model.h5',
- Крест – предплечья скрещены на груди (выключает управление). classes_path='dynamic_model_classes.pkl',
window_size=14, # должно совпадать с max_len при обучении
![Схема управления жестами](images/gesture_control.png) threshold=0.5 # минимальная уверенность для выдачи класса
)
# В цикле обработки кадров:
predictor.add_frame(landmarks) # landmarks (33,4) или None
gesture = predictor.predict() # возвращает класс или None, если недостаточно данных/уверенность ниже порога
```
## Возможные проблемы ## Возможные проблемы
1. Камера не работает - проверьте `CAMERA_ID` в `config.py` (обычно 0 или 1). 1. Камера не работает - проверьте права доступа, индекс камеры (--camera). Для OAK убедитесь, что устройство подключено и depthai установлен.
2. Скелет не определяется – убедитесь, что человек стоит на расстоянии 1–2 метра, плечи в кадре. Можете также повысить сложность модели определения скелета (`skeleton/mediapipe_detector.py`), но скажется на производительности. 2. Скелет не определяется – стойте на расстоянии 1–2 м, плечи в кадре. Снизьте min_detection_confidence или увеличьте model_complexity в MediaPipeDetector.
3. Ложные срабатывания жестов – в геометрическом режиме увеличьте `min_conf` в `special_gestures.py` или переключитесь на ML-режим с большим количеством примеров 3. Ложные срабатывания жестов – в геометрическом режиме увеличьте `min_conf` в `special_gestures.py` или переключитесь на ML-режим с большим количеством примеров.
4. Робот не движется – проверьте, включено ли управление (жест "домик") и видимость рук. 4. Не хватает памяти для LSTM – уменьшите --max_len или --batch_size, используйте меньше --lstm_units.
5. Ошибка импорта при запуске скриптов – все исполняемые скрипты автоматически добавляют корень проекта в sys.path, поэтому запускайте из корня проекта.
## Датасеты ## Датасеты (на точках MediaPipe)
| Название | Описание | Ссылка | Количество примеров | Классы | | Название | Описание | Ссылка | Количество примеров | Классы |
|----------|----------|--------|---------------------|--------| |----------|----------|--------|---------------------|--------|
| **special_gestures_v1** | Набор фотографий для распознавания специальных жестов (домик, крест, none). Собран с помощью `capture_photo.py` и стоковых изображений, размечен через `annotate.py`. | [Скачать](https://disk.yandex.ru/d/F25kMjrmZ8xwRA) | 77 (после балансировки, 13 на класс dome/cross, 51 none) | `dome`, `cross`, `none` | | **special_gestures_v1** | Набор фотографий для распознавания специальных жестов (домик, крест, none). Собран с помощью `capture_photo.py` и стоковых изображений, размечен через `annotate.py`. | [Скачать](https://disk.yandex.ru/d/F25kMjrmZ8xwRA) | 77 (после балансировки, 13 на класс dome/cross, 51 none) | `dome`, `cross`, `none` |
| **dynamic_data_v1** | csv файл с последовательностью точек скелета для 14 кадров | [Скачать](https://disk.yandex.ru/d/hnm6apfuxJsEyw) | 81 (по 20 на махи правой и левой рукой, 61 на none) | `none`, `wave_left`, `wave_right` | | **dynamic_data_v1** | csv файл с последовательностью точек скелета для 14 кадров | [Скачать](https://disk.yandex.ru/d/hnm6apfuxJsEyw) | 81 (по 20 на махи правой и левой рукой, 61 на none) | `none`, `wave_left`, `wave_right` |
## Модели ## Модели (на точках MediaPipe)
| Название | Тип | Архитектура / параметры | Вход | Выход | Точность (test) | Precision / Recall (по классам) | Ссылка | Распознаваемые жесты | | Название | Тип | Архитектура / параметры | Вход | Выход | Точность (test) | Precision / Recall (по классам) | Ссылка | Распознаваемые жесты |
|----------|-----|-------------------------|------|-------|-----------------|--------------------------------|--------|----------------------| |----------|-----|-------------------------|------|-------|-----------------|--------------------------------|--------|----------------------|
| **special_gestures_mlp** | MLP (scikit-learn) | Скрытые слои: (64, 32), активация ReLU, Adam, early stopping, 500 эпох | 99 нормализованных координат скелета (33 точки × 3) | 3 класса (dome, cross, none) | **0.56** | **dome**: P=0.00, R=0.00<br>**cross**: P=0.00, R=0.00<br>**none**: P=0.60, R=0.90 | [Скачать](https://disk.yandex.ru/d/I1WyAfN3PJM9fw) | `dome` (руки над головой домиком)<br>`cross` (предплечья скрещены на груди)<br>`none` (остальные) | | **special_gestures_mlp** | MLP (scikit-learn) | Скрытые слои: (64, 32), активация ReLU, Adam, early stopping, 500 эпох | 99 нормализованных координат скелета (33 точки × 3) | 3 класса (dome, cross, none) | **0.56** | **dome**: P=0.00, R=0.00<br>**cross**: P=0.00, R=0.00<br>**none**: P=0.60, R=0.90 | [Скачать](https://disk.yandex.ru/d/I1WyAfN3PJM9fw) | `dome` (руки над головой домиком)<br>`cross` (предплечья скрещены на груди)<br>`none` (остальные) |
+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
+144 -62
View File
@@ -1,102 +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.6 # Only upper-body required (hips optional)
required = ['left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow', required = [
'left_wrist', 'right_wrist', 'left_hip', 'right_hip'] '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]
hip_center = (l_hip + r_hip) / 2 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) shoulder_width = np.linalg.norm(r_sh - l_sh)
if shoulder_width < 50 or shoulder_width > 300: if shoulder_width < self.th['shoulder_width_min']:
if self.debug: if self.debug:
print("shoulder_width out of range") 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
# 1. Запястья перекрещены (левое правее правого) И на уровне груди (ниже плеч, выше бедер) if c_lhip >= min_conf and c_rhip >= min_conf:
wrists_crossed = l_wr[0] > r_wr[0] hip_center_y = (l_hip[1] + r_hip[1]) / 2.0
wrists_chest_level = (max(l_wr[1], r_wr[1]) > max(l_sh[1], r_sh[1]) and torso_height = hip_center_y - shoulder_center_y
min(l_wr[1], r_wr[1]) < hip_center[1]) 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
# 2. Локти тоже на уровне груди (примерно) if torso_height < self.th['torso_height_min']:
elbows_chest_level = (max(l_el[1], r_el[1]) > max(l_sh[1], r_sh[1]) * 0.9 and if self.debug:
min(l_el[1], r_el[1]) < hip_center[1] * 1.1) print(f"[SG] Torso height too small: {torso_height:.1f}")
return 'none'
# 3. Руки согнуты (предплечья короче верхней части руки) def angle_between_vectors(v1, v2):
left_bent = np.linalg.norm(l_wr - l_el) < np.linalg.norm(l_el - l_sh) * 0.9 n1 = np.linalg.norm(v1)
right_bent = np.linalg.norm(r_wr - r_el) < np.linalg.norm(r_el - r_sh) * 0.9 n2 = np.linalg.norm(v2)
arms_bent = left_bent and right_bent 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))
# 4. Запястья близко к центру тела (не сильно отведены, типично для креста на груди) def joint_angle(p_prev, p_joint, p_next):
body_center_x = (l_sh[0] + r_sh[0]) / 2 v1 = p_prev - p_joint
wrists_near_center = (abs(l_wr[0] - body_center_x) < shoulder_width * 0.6 and v2 = p_next - p_joint
abs(r_wr[0] - body_center_x) < shoulder_width * 0.6) return angle_between_vectors(v1, v2)
cross = (wrists_crossed and wrists_chest_level and elbows_chest_level and def segments_intersect(p1, p2, p3, p4):
arms_bent and wrists_near_center) 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)
if self.debug and cross: def line_intersection(p1, p2, p3, p4):
print(f"CROSS: crossed={wrists_crossed}, chest_w={wrists_chest_level}, " d1 = p2 - p1
f"chest_e={elbows_chest_level}, bent={arms_bent}, near_center={wrists_near_center}") d2 = p4 - p3
if cross: 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
# 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' return 'cross'
# ----- ДОМИК (руки над головой) ----- # 2) LIGHT
head_y = landmarks[idx['nose']][1] - 50 if (self.th['light_elbow_min'] < r_elbow_angle < self.th['light_elbow_max'] and
arms_up = (l_wr[1] < head_y and r_wr[1] < head_y) self.th['light_shoulder_min'] < r_shoulder_like_angle < self.th['light_shoulder_max']):
if arms_up: return 'light'
dist = np.linalg.norm(l_wr - r_wr)
rel_dist = dist / shoulder_width # 3) DOME
if rel_dist < 1.5: wrists_above_nose = (l_wr[1] < nose[1] and r_wr[1] < nose[1])
return 'dome' 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)
elbow_distance = np.linalg.norm(l_el - r_el)
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 < (self.th['wrists_near_factor'] * shoulder_width)
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' return 'none'
Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

+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 "все установлено"
+200
View File
@@ -0,0 +1,200 @@
# ====== 33 точки mediapipe ======
'''
0 - нос
1 - внутренний угол левого глаза
2 - центр левого глаза
3 - внешний угол левого глаза
4 - внутренний угол правого глаза
5 - центр правого глаза
6 - внешний угол правого глаза
7 - левое ухо
8 - правое ухо
9 - левый угол рта
10 - правый угол рта
11 - левое плечо
12- правое плечо
13 - левый локоть
14 - правый локоть
15 - левое запястье
16 - правое запястье
17 - левый мизинец
18 - правый мизинец
19 - левый указательный палец
20 - правй указательный палец
21- левый большой палец
22 -правый большой палец
23 - левое бедро
24 - правое бедро
25 - левое колено
26 - правое колено
27 - левая голень/лодыжка
28 - правая голень/лодыжка
29 - левая пятка
30 - правая пятка
31 - левый носок
32 - правый носок
'''
# ====== 17 точек efficienthrnet ======
'''
0 - нос
1 - левый глаз
2 - правый глаз
3 - левое ухо
4 - правое ухо
5 - левое плечо
6 - правое плечо
7 - левый локоть
8 - правый локоть
9 - левое запястье
10 - правое запястье
11 - левое бедро
12 - правое бедро
13 - левое колено
14 - правое колено
15 - левая лодыжка
16 - правая лодыжка
'''
import sys
from pathlib import Path
REPO_PATH = Path(__file__).parent.parent / "submodules" / "OAK-HumanPoseEstimation"
sys.path.append(str(REPO_PATH))
import cv2
import depthai as dai
import numpy as np
import daipipeline
from daipipeline import create_pipeline, get_model_list
from poseestimators import get_poseestimator
class OakPoseDetector:
EH_TO_MP = {
0: 0, # nose
1: 2, # left eye
2: 5, # right eye
3: 7, # left ear
4: 8, # right ear
5: 11, # left shoulder
6: 12, # right shoulder
7: 13, # left elbow
8: 14, # right elbow
9: 15, # left wrist
10: 16, # right wrist
11: 23, # left hip
12: 24, # right hip
13: 25, # left knee
14: 26, # right knee
15: 27, # left ankle
16: 28, # right ankle
}
MP_CONNECTIONS = [(EH_TO_MP[0], EH_TO_MP[1]),
(EH_TO_MP[1], EH_TO_MP[3]),
(EH_TO_MP[0], EH_TO_MP[2]),
(EH_TO_MP[2], EH_TO_MP[4]),
(EH_TO_MP[5], EH_TO_MP[6]),
(EH_TO_MP[5], EH_TO_MP[7]),
(EH_TO_MP[7], EH_TO_MP[9]),
(EH_TO_MP[6], EH_TO_MP[8]),
(EH_TO_MP[8], EH_TO_MP[10]),
(EH_TO_MP[5], EH_TO_MP[11]),
(EH_TO_MP[6], EH_TO_MP[12]),
(EH_TO_MP[11], EH_TO_MP[12]),
(EH_TO_MP[11], EH_TO_MP[13]),
(EH_TO_MP[13], EH_TO_MP[15]),
(EH_TO_MP[12], EH_TO_MP[14]),
(EH_TO_MP[14], EH_TO_MP[16])]
def __init__(self, model_type='efficienthrnet1', shaves=6, detection_threshold=0.2, **kwargs):
daipipeline.MODELS_FOLDER = str(REPO_PATH / "models")
self.model_type = model_type
self.model_list = get_model_list()
if model_type not in self.model_list:
raise ValueError(f"Unknown model: {model_type}")
self.model_config = self.model_list[model_type]
self.kwargs = kwargs
self.kwargs['shaves'] = shaves
self.kwargs['detection_threshold'] = int(detection_threshold * 100)
if 'decoder' not in self.kwargs:
self.kwargs['decoder'] = None
self.pose_estimator = get_poseestimator(self.model_config, **self.kwargs)
self.pipeline = create_pipeline(self.model_config,
camera=True,
passthrough=False,
**self.kwargs)
self.device = dai.Device(self.pipeline)
self.preview_queue = self.device.getOutputQueue("preview", maxSize=1, blocking=False)
self.pose_queue = self.device.getOutputQueue("pose", maxSize=1, blocking=False)
def get_frame_and_pose(self):
preview = self.preview_queue.tryGet()
if preview is None:
return None, None
frame = preview.getCvFrame()
raw_output = self.pose_queue.tryGet()
if raw_output is None:
return frame, None
# получаем результат в формате модели
personwise_keypoints = self.pose_estimator.get_pose_data(raw_output)
if personwise_keypoints.shape[0] == 0:
return frame, None
kps = personwise_keypoints[0] # (17, 3) (x, y, confidence)
mp_landmarks = np.zeros((33, 4), dtype=np.float32)
for eh_idx, mp_idx in self.EH_TO_MP.items():
x, y, conf = kps[eh_idx]
if conf > 1.0:
conf = conf / 100.0
mp_landmarks[mp_idx] = [x, y, 0.0, conf]
return frame, mp_landmarks
def draw_landmarks(self, image, landmarks, conf_threshold=0.1):
if landmarks is None:
return image.copy()
vis = image.copy()
for (i, j) in self.MP_CONNECTIONS:
if i < len(landmarks) and j < len(landmarks):
if landmarks[i][3] > conf_threshold and landmarks[j][3] > conf_threshold:
pt1 = (int(landmarks[i][0]), int(landmarks[i][1]))
pt2 = (int(landmarks[j][0]), int(landmarks[j][1]))
cv2.line(vis, pt1, pt2, (0, 255, 0), 2)
for i, (x, y, z, conf) in enumerate(landmarks):
if conf > conf_threshold:
cv2.circle(vis, (int(x), int(y)), 3, (255, 0, 0), -1)
return vis
def release(self):
if hasattr(self, 'device'):
self.device.close()
def main():
detector = OakPoseDetector(model_type='efficienthrnet1', detection_threshold=0.2)
print("Запуск. Нажмите 'q' для выхода.")
while True:
frame, landmarks = detector.get_frame_and_pose()
if frame is None:
continue
vis = detector.draw_landmarks(frame, landmarks) if landmarks is not None else frame
cv2.imshow("Pose Estimation", vis)
if cv2.waitKey(1) == ord('q'):
break
detector.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()