diff --git a/README.md b/README.md
index 8536fce..f2946fd 100644
--- a/README.md
+++ b/README.md
@@ -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
**cross**: P=0.00, R=0.00
**none**: P=0.60, R=0.90 | [Скачать](https://disk.yandex.ru/d/I1WyAfN3PJM9fw) | `dome` (руки над головой домиком)
`cross` (предплечья скрещены на груди)
`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
**cross**: P=0.750, R=1.00
**none**: P=0.90, R=0.90 | [Скачать](https://disk.yandex.ru/d/fcXxQh4LGjqA1g) |`dome` (руки над головой домиком)
`cross` (предплечья скрещены на груди)
`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
**cross**: P=0.750, R=1.00
**none**: P=1.00, R=0.90 | [Скачать](https://disk.yandex.ru/d/4_XCPEeOGvNX6g) |`dome` (руки над головой домиком)
`cross` (предплечья скрещены на груди)
`none` (остальные) |
diff --git a/config.py b/config.py
index 5805fe3..2c952c6 100644
--- a/config.py
+++ b/config.py
@@ -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']
# ===== Робот =====
diff --git a/gesture_control/arm_control.py b/gesture_control/arm_control.py
index 09c4725..f7bcd75 100644
--- a/gesture_control/arm_control.py
+++ b/gesture_control/arm_control.py
@@ -9,6 +9,27 @@ class ArmController:
self.hip_idx = {'left': 23, 'right': 24}
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):
"""Ширина плеч для нормировки горизонтальных смещений."""
@@ -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
+
diff --git a/gesture_control/state.py b/gesture_control/state.py
index a2041be..c40e34f 100644
--- a/gesture_control/state.py
+++ b/gesture_control/state.py
@@ -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):
diff --git a/robot/annotate.py b/robot/annotate.py
deleted file mode 100644
index eb88308..0000000
--- a/robot/annotate.py
+++ /dev/null
@@ -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()