From 5e1c1341aa5f0dc5c147e0af5be83630d12e602d Mon Sep 17 00:00:00 2001 From: moscovskayaliza Date: Mon, 23 Mar 2026 17:15:35 +0300 Subject: [PATCH] readme + models --- README.md | 102 ++++++++++++++++++++++++++++++++- config.py | 2 +- gesture_control/arm_control.py | 70 +++++++++++++++------- gesture_control/state.py | 2 +- robot/annotate.py | 95 ------------------------------ 5 files changed, 151 insertions(+), 120 deletions(-) delete mode 100644 robot/annotate.py 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()