readme + models
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
Тут будут зависимости, инструкции и краткие описания
|
||||
## О проекте
|
||||
Проект позволяет управлять роботом (в симуляторе) с помощью жестов рук, распознаваемых через камеру. Используется MediaPipe для детекции скелета.
|
||||
|
||||
## Структура проекта
|
||||
```
|
||||
@@ -26,4 +27,101 @@ gesture_robot/
|
||||
|
||||
## Подготовка и запуск
|
||||
|
||||
## Примеры запуска
|
||||
### Установка зависимостей
|
||||
|
||||
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
|
||||
```
|
||||
3. Запустится симулятор и изображение с камеры
|
||||
|
||||
#### Запуск с ML-распознаванием специальных жестов
|
||||
1. Соберите датасет с нужными изображениями фото. Можете заснять собственные через `utils/capture_photo.py`:
|
||||
```
|
||||
python3 -m utils/capture_photo.py --dir data/raw
|
||||
```
|
||||
Нажмите `s`, подождите 3 секунды, фото сохранится в папку `data/raw`. Нажмите `q` чтобы закончить.
|
||||
2. Разметьте фото с помощью аннотатора:
|
||||
```
|
||||
python3 -m utils/annotate.py --folder data/raw --classes dome,cross,none --output data.csv
|
||||
```
|
||||
Для каждого фото нажмите цифру, соответствующую жесту (1–dome, 2–cross, 3–none), или n для пропуска. При выходе данные сохранятся в `data.csv`.
|
||||
3. Обучите модель
|
||||
```
|
||||
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. Оценка модели
|
||||
После обучения модель сохраняется, и создаётся отчёт `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
|
||||
```
|
||||
5. Подключите модель в `config.py`
|
||||
```
|
||||
SPECIAL_GESTURE_MODE = 'ml'
|
||||
ML_GESTURE_MODEL = 'ml_gestures/models/special_model.pkl'
|
||||
ML_GESTURE_CLASSES = ['dome', 'cross', 'none']
|
||||
```
|
||||
6. Запустите основной скрипт:
|
||||
```
|
||||
python3 main.py
|
||||
```
|
||||
7. Запустится симулятор и изображение с камеры
|
||||
|
||||
### Управление в симуляторе
|
||||
Симулятор – поле с препятствиями, стартом и финишем. Робот движется согласно командам. При столкновении – игра заканчивается (перезапуск по `r`). Закрытие окна игры или нажатие `q` в окне камеры – выход.
|
||||
1. Управление скоростями
|
||||
- Линейная скорость (вперёд) – горизонтальное положение правой руки (рука вдоль тела – 0, вытянута в сторону – максимум).
|
||||
- Угловая скорость – вертикальное положение левой руки (рука на уровне плеча – 0, вверх – поворот вправо, вниз – поворот влево).
|
||||
2. Специальные жесты. Режим распознавания может быть геометрическим (по правилам) или обучаемым (ML-модель).
|
||||
- Домик – обе руки над головой (включает управление).
|
||||
- Крест – предплечья скрещены на груди (выключает управление).
|
||||
|
||||
## Возможные проблемы
|
||||
1. Камера не работает - проверьте `CAMERA_ID` в `config.py` (обычно 0 или 1).
|
||||
2. Скелет не определяется – убедитесь, что человек стоит на расстоянии 1–2 метра, плечи в кадре. Можете также повысить сложность модели определения скелета (`skeleton/mediapipe_detector.py`), но скажется на производительности.
|
||||
3. Ложные срабатывания жестов – в геометрическом режиме увеличьте `min_conf` в `special_gestures.py` или переключитесь на ML-режим с большим количеством примеров
|
||||
4. Робот не движется – проверьте, включено ли управление (жест "домик") и видимость рук.
|
||||
|
||||
## Датасеты
|
||||
| Название | Описание | Ссылка | Количество примеров | Классы |
|
||||
|----------|----------|--------|---------------------|--------|
|
||||
| **special_gestures_v1** | Набор фотографий для распознавания специальных жестов (домик, крест, none). Собран с помощью `capture_photo.py` и стоковых изображений, размечен через `annotate.py`. | [Скачать](https://disk.yandex.ru/d/F25kMjrmZ8xwRA) | 77 (после балансировки, 13 на класс dome/cross, 51 none) | `dome`, `cross`, `none` |
|
||||
|
||||
## Модели
|
||||
| Название | Тип | Архитектура / параметры | Вход | Выход | Точность (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_rf** | Random Forest | 50 деревьев, max_depth=10, random_state=42 | 99 нормализованных координат скелета (33 точки × 3) | 3 класса (dome, cross, none) | **0.87** | **dome**: P=0.67, R=1.00<br>**cross**: P=0.750, R=1.00<br>**none**: P=0.90, R=0.90 | [Скачать](https://disk.yandex.ru/d/fcXxQh4LGjqA1g) |`dome` (руки над головой домиком)<br>`cross` (предплечья скрещены на груди)<br>`none` (остальные) |
|
||||
| **special_gestures_lin** | Logistic Regression | max_iter=1000, random_state=42 | 99 нормализованных координат скелета (33 точки × 3) | 3 класса (dome, cross, none) | **0.94** | **dome**: P=1.00, R=1.00<br>**cross**: P=0.750, R=1.00<br>**none**: P=1.00, R=0.90 | [Скачать](https://disk.yandex.ru/d/4_XCPEeOGvNX6g) |`dome` (руки над головой домиком)<br>`cross` (предплечья скрещены на груди)<br>`none` (остальные) |
|
||||
|
||||
@@ -16,7 +16,7 @@ class Config:
|
||||
|
||||
# ===== Специальные жесты =====
|
||||
SPECIAL_GESTURE_MODE = 'ml' #ml #geometric
|
||||
ML_GESTURE_MODEL = '/home/ubuntu/sirius/gesture_rec/special_model.pkl'
|
||||
ML_GESTURE_MODEL = '/home/ubuntu/sirius/models/rf/special_model.pkl'
|
||||
ML_GESTURE_CLASSES = ['dome', 'cross', 'none']
|
||||
|
||||
# ===== Робот =====
|
||||
|
||||
@@ -10,6 +10,27 @@ class ArmController:
|
||||
self.dead_zone = config.get('dead_zone', 0.2)
|
||||
self.debug = config.get('debug', False)
|
||||
|
||||
def _get_side_indices(self, side):
|
||||
"""
|
||||
Возвращает (shoulder_idx, wrist_idx) для заданной стороны (left/right).
|
||||
При mirror=True интерпретируем сторону как в реальности: левая/правая рука.
|
||||
"""
|
||||
if self.mirror:
|
||||
if side == 'left':
|
||||
s_idx = 12
|
||||
w_idx = 16
|
||||
else:
|
||||
s_idx = 11
|
||||
w_idx = 15
|
||||
else:
|
||||
if side == 'left':
|
||||
s_idx = 11
|
||||
w_idx = 15
|
||||
else:
|
||||
s_idx = 12
|
||||
w_idx = 16
|
||||
return s_idx, w_idx
|
||||
|
||||
def _get_shoulder_width(self, landmarks):
|
||||
"""Ширина плеч для нормировки горизонтальных смещений."""
|
||||
left = landmarks[11][:2]
|
||||
@@ -35,49 +56,55 @@ class ArmController:
|
||||
def _horizontal_displacement_rel(self, landmarks, side):
|
||||
"""
|
||||
Нормированное горизонтальное смещение запястья относительно плеча.
|
||||
При mirror=True инвертируем знак, чтобы скомпенсировать отражение.
|
||||
Сторона `side` — это реальная сторона руки
|
||||
"""
|
||||
if landmarks[self.shoulder_idx[side]][3] < 0.5 or landmarks[self.wrist_idx[side]][3] < 0.5:
|
||||
s_idx, w_idx = self._get_side_indices(side)
|
||||
|
||||
if landmarks[s_idx][3] < 0.5 or landmarks[w_idx][3] < 0.5:
|
||||
return 0.0
|
||||
shoulder = landmarks[self.shoulder_idx[side]][:2]
|
||||
wrist = landmarks[self.wrist_idx[side]][:2]
|
||||
|
||||
shoulder = landmarks[s_idx][:2]
|
||||
wrist = landmarks[w_idx][:2]
|
||||
|
||||
shoulder_width = self._get_shoulder_width(landmarks)
|
||||
if shoulder_width is None:
|
||||
return 0.0
|
||||
|
||||
disp = wrist[0] - shoulder[0]
|
||||
if self.mirror:
|
||||
disp = -disp
|
||||
return disp / shoulder_width
|
||||
|
||||
def _vertical_displacement_rel(self, landmarks, side):
|
||||
"""
|
||||
Нормированное вертикальное смещение запястья относительно плеча.
|
||||
Положительное – запястье выше плеча (рука вверх), отрицательное – ниже.
|
||||
Зеркало не влияет.
|
||||
Вертикальное смещение: верх/низ запястья относительно плеча.
|
||||
Сторона `side` — реальная сторона руки.
|
||||
"""
|
||||
if landmarks[self.shoulder_idx[side]][3] < 0.5 or landmarks[self.wrist_idx[side]][3] < 0.5:
|
||||
s_idx, w_idx = self._get_side_indices(side)
|
||||
|
||||
if landmarks[s_idx][3] < 0.5 or landmarks[w_idx][3] < 0.5:
|
||||
return 0.0
|
||||
shoulder = landmarks[self.shoulder_idx[side]][:2]
|
||||
wrist = landmarks[self.wrist_idx[side]][:2]
|
||||
|
||||
shoulder = landmarks[s_idx][:2]
|
||||
wrist = landmarks[w_idx][:2]
|
||||
|
||||
torso_height = self._get_torso_height(landmarks)
|
||||
if torso_height is None:
|
||||
return 0.0
|
||||
# Так как y растёт вниз, то (shoulder[1] - wrist[1]) > 0, если запястье выше
|
||||
|
||||
disp = shoulder[1] - wrist[1]
|
||||
return disp / torso_height
|
||||
|
||||
def compute_speeds(self, landmarks):
|
||||
# Проверка видимости ключевых точек
|
||||
if (landmarks[11][3] < 0.5 or landmarks[12][3] < 0.5 or
|
||||
landmarks[15][3] < 0.5 or landmarks[16][3] < 0.5):
|
||||
#if self.debug:
|
||||
#print("Руки не видны")
|
||||
if self.debug:
|
||||
print("Руки не видны")
|
||||
return 0.0, 0.0
|
||||
|
||||
# Правая рука (индекс 16) — линейная скорость
|
||||
lin_rel = self._horizontal_displacement_rel(landmarks, 'right')
|
||||
# Левая рука (индекс 15) — угловая скорость
|
||||
ang_rel = self._vertical_displacement_rel(landmarks, 'left')
|
||||
linear_side = self.config['linear_arm']
|
||||
angular_side = self.config['angular_arm']
|
||||
|
||||
lin_rel = self._horizontal_displacement_rel(landmarks, linear_side)
|
||||
ang_rel = self._vertical_displacement_rel(landmarks, angular_side)
|
||||
|
||||
if self.debug:
|
||||
print(f"lin_rel={lin_rel:.3f}, ang_rel={ang_rel:.3f}")
|
||||
@@ -88,7 +115,7 @@ class ArmController:
|
||||
else:
|
||||
linear = min(lin_rel, 1.0) * self.config['max_speed_linear']
|
||||
|
||||
# Угловая скорость (положительная — вправо, отрицательная — влево)
|
||||
# Угловая скорость
|
||||
if abs(ang_rel) < self.dead_zone:
|
||||
angular = 0.0
|
||||
else:
|
||||
@@ -96,3 +123,4 @@ class ArmController:
|
||||
angular = ang_rel_clipped * self.config['max_speed_angular']
|
||||
|
||||
return linear, angular
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class ControlState:
|
||||
def __init__(self, initial_enabled=True):
|
||||
def __init__(self, initial_enabled=False):
|
||||
self.enabled = initial_enabled
|
||||
|
||||
def update(self, special_gesture):
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import cv2
|
||||
import os
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
|
||||
sys.path.append(str(Path(__file__).parent.parent))
|
||||
from skeleton.mediapipe_detector import MediaPipeDetector
|
||||
from ml_gestures.feature_extractor import normalize_landmarks
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Разметка изображений для обучения')
|
||||
parser.add_argument('--folder', required=True, help='Папка с изображениями')
|
||||
parser.add_argument('--classes', default='dome,cross,none',
|
||||
help='Список классов через запятую')
|
||||
parser.add_argument('--output', default='gesture_data.csv',
|
||||
help='Имя выходного CSV-файла')
|
||||
args = parser.parse_args()
|
||||
|
||||
classes = [c.strip() for c in args.classes.split(',')]
|
||||
key_to_class = {str(i+1): cls for i, cls in enumerate(classes)}
|
||||
print("Классы:", classes)
|
||||
|
||||
detector = MediaPipeDetector()
|
||||
|
||||
image_extensions = ('.jpg', '.jpeg', '.png', '.bmp')
|
||||
image_files = [f for f in os.listdir(args.folder) if f.lower().endswith(image_extensions)]
|
||||
image_files.sort()
|
||||
print(f"Найдено {len(image_files)} изображений.")
|
||||
|
||||
csv_file = args.output
|
||||
file_exists = os.path.isfile(csv_file)
|
||||
if not file_exists:
|
||||
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
# 99 признаков (33 точки * 3 координаты)
|
||||
writer.writerow(['class'] + [f'f{i}' for i in range(99)])
|
||||
|
||||
for idx, filename in enumerate(image_files):
|
||||
filepath = os.path.join(args.folder, filename)
|
||||
print(f"\n[{idx+1}/{len(image_files)}] {filename}")
|
||||
|
||||
image = cv2.imread(filepath)
|
||||
if image is None:
|
||||
continue
|
||||
|
||||
result = detector.detect(image)
|
||||
if not result['success']:
|
||||
cv2.imshow('No skeleton', image)
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
continue
|
||||
|
||||
landmarks = result['landmarks']
|
||||
features = normalize_landmarks(landmarks) # вектор из 99 чисел
|
||||
|
||||
display = detector.draw_landmarks(image, result['pose_landmarks'])
|
||||
h, w = display.shape[:2]
|
||||
|
||||
# Панель с инструкцией
|
||||
overlay = display.copy()
|
||||
cv2.rectangle(overlay, (0, h-80), (w, h), (50,50,50), -1)
|
||||
cv2.addWeighted(overlay, 0.6, display, 0.4, 0, display)
|
||||
|
||||
y = h - 60
|
||||
for i, cls in enumerate(classes):
|
||||
cv2.putText(display, f"{i+1}:{cls}", (10 + i*120, y),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,255), 2)
|
||||
cv2.putText(display, "n:skip q:quit", (10, y+30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255,255,0), 2)
|
||||
|
||||
cv2.imshow('Annotation', display)
|
||||
key = cv2.waitKey(0) & 0xFF
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
if key == ord('q'):
|
||||
break
|
||||
elif key == ord('n'):
|
||||
continue
|
||||
else:
|
||||
key_char = chr(key) if key < 256 else None
|
||||
if key_char in key_to_class:
|
||||
selected = key_to_class[key_char]
|
||||
with open(csv_file, 'a', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow([selected] + features.tolist())
|
||||
print(f"Сохранено: {selected}")
|
||||
else:
|
||||
print("Неверная клавиша")
|
||||
|
||||
print("Готово.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user