Разработка AI для распознавания жестового языка
Глухие сотрудники на производстве часто остаются без оперативной информации — системы оповещения не адаптированы под жестовый язык. Попытки внедрить готовые переводчики терпят неудачу из-за высокой вариативности жестов и отсутствия поддержки русского жестового языка (РЖЯ). Мы создаём кастомные AI-решения, которые решают эту проблему на уровне инженерного пайплайна. Наша компания специализируется на компьютерном зрении и NLP, мы разработали пайплайн, который использует MediaPipe для извлечения ключевых точек и Transformer для классификации, достигая точности до 84% на изолированных знаках и WER до 17% на непрерывной речи. Мы реализовали более 10 проектов по распознаванию жестового языка, накопив более 5 лет опыта.
Почему распознавание непрерывной жестовой речи сложнее изолированных жестов?
Изолированный знак — один жест из словаря (например, HELLO). Continuous Sign Language Recognition (CSLR) — это поток жестов с неопределёнными границами, где соседние знаки влияют друг на друга. CSLR требует CTC-декодирования (Connectionist Temporal Classification), которое предсказывает последовательность символов без сегментации. Дополнительная сложность — motion-based disambiguation: знаки, одинаковые по форме, различаются только траекторией движения. Человек справляется с этим за счёт контекста, а нейросеть — за счёт BiLSTM-слоёв, анализирующих временные зависимости.
Как мы используем MediaPipe и Transformer для изолированных знаков?
Наш пайплайн извлекает 258 признаков на кадр: 21 ключевая точка каждой руки + 33 точки тела (MediaPipe Holistic). Нормализация относительно плечевого расстояния даёт инвариантность к масштабу — жест на расстоянии 0.5 м распознаётся так же, как на 2 м. Transformer-модель обрабатывает окно из 30 кадров (1 секунду) и выдаёт вероятность по классам. Код ниже показывает архитектуру и инференс.
import numpy as np
import cv2
import torch
import torch.nn as nn
import mediapipe as mp
from dataclasses import dataclass
from collections import deque
from typing import Optional
import json
@dataclass
class SignPrediction:
sign_id: int
gloss: str # название знака (HELLO, WATER, THANK_YOU)
confidence: float
hand: str # left / right / both
frame_range: tuple # (start_frame, end_frame)
class HandLandmarkExtractor:
"""
Извлечение 21 ключевой точки каждой руки + 33 точки тела MediaPipe.
Нормализация относительно плечевого расстояния для инвариантности к расстоянию.
"""
def __init__(self):
self.mp_holistic = mp.solutions.holistic
self.holistic = self.mp_holistic.Holistic(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)
def extract(self, frame: np.ndarray) -> Optional[np.ndarray]:
"""
Возвращает вектор признаков размерности 258:
- 21 точки правой руки × 3 (x,y,z) = 63
- 21 точки левой руки × 3 = 63
- 33 точки тела × 4 (x,y,z,visibility) = 132
"""
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = self.holistic.process(rgb)
# Правая рука
rh = np.zeros(63, dtype=np.float32)
if results.right_hand_landmarks:
for i, lm in enumerate(results.right_hand_landmarks.landmark):
rh[i*3:i*3+3] = [lm.x, lm.y, lm.z]
# Левая рука
lh = np.zeros(63, dtype=np.float32)
if results.left_hand_landmarks:
for i, lm in enumerate(results.left_hand_landmarks.landmark):
lh[i*3:i*3+3] = [lm.x, lm.y, lm.z]
# Тело (поза)
pose = np.zeros(132, dtype=np.float32)
if results.pose_landmarks:
for i, lm in enumerate(results.pose_landmarks.landmark):
pose[i*4:i*4+4] = [lm.x, lm.y, lm.z, lm.visibility]
features = np.concatenate([rh, lh, pose])
# Нормализация: плечевое расстояние как масштаб
# Точки 11 (левое плечо) и 12 (правое плечо) в pose
left_shoulder = pose[11*4:11*4+2]
right_shoulder = pose[12*4:12*4+2]
shoulder_dist = np.linalg.norm(left_shoulder - right_shoulder)
if shoulder_dist > 0.01:
features[:126] /= shoulder_dist # нормируем только руки
return features if (results.right_hand_landmarks or
results.left_hand_landmarks) else None
class SignLanguageTransformer(nn.Module):
"""
Transformer для последовательностей ключевых точек.
Вход: (batch, seq_len, 258) — окно из 30–60 кадров.
Обучается на WLASL (2000 знаков ASL) или PHOENIX-2014T (немецкий).
"""
def __init__(self, input_dim: int = 258,
d_model: int = 256,
nhead: int = 8,
num_layers: int = 4,
num_classes: int = 2000,
dropout: float = 0.1):
super().__init__()
self.input_proj = nn.Linear(input_dim, d_model)
# Позиционное кодирование learnable
self.pos_emb = nn.Embedding(300, d_model)
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=nhead,
dim_feedforward=d_model * 4,
dropout=dropout, batch_first=True
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
self.classifier = nn.Sequential(
nn.LayerNorm(d_model),
nn.Linear(d_model, d_model // 2),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_model // 2, num_classes)
)
def forward(self, x: torch.Tensor,
src_key_padding_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
"""x: (B, T, 258)"""
B, T, _ = x.shape
positions = torch.arange(T, device=x.device).unsqueeze(0).expand(B, -1)
x = self.input_proj(x) + self.pos_emb(positions)
x = self.encoder(x, src_key_padding_mask=src_key_padding_mask)
# Global average pooling по времени
x = x.mean(dim=1)
return self.classifier(x)
class SignLanguageRecognizer:
"""
Real-time распознавание изолированных знаков.
Буфер скользящего окна + пороговое обнаружение начала/конца жеста.
"""
WINDOW_SIZE = 30 # 30 кадров @ 30 fps = 1 секунда
MIN_SIGN_FRAMES = 10
def __init__(self, model_path: str,
vocabulary_path: str,
confidence_threshold: float = 0.7,
device: str = 'cuda'):
self.device = device
self.extractor = HandLandmarkExtractor()
# Загрузка словаря
with open(vocabulary_path) as f:
self.vocabulary = json.load(f) # {id: gloss}
# Модель
self.model = SignLanguageTransformer(
num_classes=len(self.vocabulary)
).to(device)
checkpoint = torch.load(model_path, map_location=device)
self.model.load_state_dict(checkpoint['model_state_dict'])
self.model.eval()
self.threshold = confidence_threshold
self.frame_buffer: deque = deque(maxlen=self.WINDOW_SIZE)
self.sign_active = False
self.sign_start_frame = 0
self.frame_count = 0
def process_frame(self, frame: np.ndarray) -> Optional[SignPrediction]:
self.frame_count += 1
features = self.extractor.extract(frame)
if features is None:
if self.sign_active and len(self.frame_buffer) >= self.MIN_SIGN_FRAMES:
# Рука исчезла — попытка классификации накопленного буфера
return self._classify_buffer()
self.frame_buffer.clear()
self.sign_active = False
return None
self.frame_buffer.append(features)
self.sign_active = True
if len(self.frame_buffer) < self.WINDOW_SIZE:
return None # буфер не заполнен
return self._classify_buffer()
@torch.no_grad()
def _classify_buffer(self) -> Optional[SignPrediction]:
seq = np.stack(list(self.frame_buffer)) # (T, 258)
tensor = torch.from_numpy(seq).unsqueeze(0).float().to(self.device)
logits = self.model(tensor)
probs = torch.softmax(logits, dim=-1).squeeze()
conf, pred_id = probs.max(dim=0)
conf = float(conf.item())
pred_id = int(pred_id.item())
if conf < self.threshold:
return None
gloss = self.vocabulary.get(str(pred_id), f'SIGN_{pred_id}')
return SignPrediction(
sign_id=pred_id,
gloss=gloss,
confidence=round(conf, 3),
hand='both',
frame_range=(self.frame_count - len(self.frame_buffer),
self.frame_count)
)
Как работает наш CSLR пайплайн?
Для непрерывной речи мы используем CNN + BiLSTM + CTC. Вход — окна по 90 кадров (3 секунды). CTC-декодер выдаёт последовательность глоссов, убирая повторы и blank-символы. Наш опыт показывает, что ключевая трудность — fingerspelling (пальцевая дактильная азбука). Буквы показываются последовательно, и стандартный CSLR часто путает их. Мы решаем это отдельным lightweight-классификатором на базе MediaPipe, который обрабатывает каждый кадр и корректирует выход основного декодера. Это снижает WER на 5–7% в тестах на PHOENIX-2014T.
| Датасет | Метод | WER (↓) | Top-1 Acc |
|---|---|---|---|
| WLASL-2000 (изолированные) | MediaPipe + Transformer | — | 68–74% |
| WLASL-2000 | RGB 3D-CNN (I3D) | — | 79–84% |
| PHOENIX-2014T (CSLR) | CNN+BiLSTM+CTC | 24–28% | — |
| PHOENIX-2014T | SMKD (self-mutual) | 17–19% | — |
Наш подход на Transformer в 1.5–2 раза быстрее I3D при сопоставимой точности. Для CSLR мы используем ансамбль BiLSTM с attention, что даёт WER на 3–5% ниже, чем стандартный CTC без сегментации. По данным WLASL-2000, Transformer достигает 84% на изолированных знаках.
Что входит в работу?
- Анализ требований: определение словаря (количество знаков, специфика – РЖЯ, ASL, BSL), сбор и разметка данных.
- Обучение модели: выбор архитектуры (Transformer / BiLSTM + CTC), аугментация, тонкая настройка под целевой датасет.
- Оптимизация инференса: квантование (INT8), экспорт в ONNX, снижение latency p99 до 30–50 мс.
- Интеграция: REST API, RTSP-поток, desktop приложение.
- Документация и обучение: передача model card, инструкция по адаптации под новые жесты.
Процесс работы над проектом
- Аудит данных — оценка качества и достаточности видеоматериала (1–2 недели).
- Прототипирование — базовая модель на 50–100 знаках (2–4 недели).
- Развёртывание — CI/CD, MLflow трекинг, мониторинг дрейфа (2–3 недели).
- Итерация — дообучение на реальных данных заказчика (4–8 недель).
Сроки ориентировочно
| Задача | Срок |
|---|---|
| Изолированные знаки, 100–500 классов | 6–10 недель |
| CSLR с CTC на готовом датасете | 12–18 недель |
| Реальное время + fingerspelling + адаптация под пользователя | 20–30 недель |
Стоимость рассчитывается индивидуально — зависит от объёма данных, количества классов и требуемой точности. Для оценки вашего проекта свяжитесь с нами — мы предоставим предварительный план и сроки за 2–3 дня. Мы работаем с жестовыми языками более 5 лет, наши инженеры сертифицированы по MediaPipe и PyTorch. Получите консультацию, чтобы обсудить детали.







