Разработка AI для распознавания жестового языка
Жестовые языки — полноценные лингвистические системы с грамматикой, отличной от устной речи: ASL, BSL, РЖЯ (русский жестовый язык) не являются «переводом» звуков в жесты. Распознавание жестового языка делится на два класса задач: isolated sign recognition (отдельный знак из словаря) и continuous sign language recognition (CSLR) — расшифровка непрерывной речи. CSLR значительно сложнее: требует CTC-декодирования, сегментации потока и понимания контекста. Реальные применения: субтитры для глухих в реальном времени, переводчик для медицинских учреждений, управление жестами в industrial HMI.
Isolated Sign Recognizer
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)
)
Continuous Sign Language Recognition (CSLR)
class ContinuousSignRecognizer:
"""
CSLR: непрерывный поток жестов → транскрипция.
Architecture: CNN feature extractor → BiLSTM → CTC loss.
Датасет: PHOENIX-2014T (немецкий язестовый язык, 9K предложений).
"""
def __init__(self, model_path: str, vocab_path: str,
device: str = 'cuda'):
self.device = device
# Простейший CSLR пайплайн: только для иллюстрации API
self.model = torch.load(model_path, map_location=device)
self.model.eval()
with open(vocab_path) as f:
vocab = json.load(f)
self.id_to_gloss = {v: k for k, v in vocab.items()}
self.blank_id = vocab.get('<blank>', 0)
self.extractor = HandLandmarkExtractor()
self.feature_buffer: list[np.ndarray] = []
self.last_flush_frame = 0
def process_frame(self, frame: np.ndarray,
frame_id: int) -> Optional[str]:
features = self.extractor.extract(frame)
if features is not None:
self.feature_buffer.append(features)
# Flush каждые 90 кадров (3 секунды @ 30fps)
if frame_id - self.last_flush_frame >= 90 and self.feature_buffer:
result = self._decode_ctc()
self.feature_buffer = []
self.last_flush_frame = frame_id
return result
return None
@torch.no_grad()
def _decode_ctc(self) -> str:
seq = np.stack(self.feature_buffer)
tensor = torch.from_numpy(seq).unsqueeze(0).float().to(self.device)
log_probs = self.model(tensor) # (1, T, vocab)
# Greedy CTC decode
pred_ids = log_probs.argmax(dim=-1).squeeze().cpu().numpy()
glosses = []
prev = self.blank_id
for pid in pred_ids:
if pid != self.blank_id and pid != prev:
glosses.append(self.id_to_gloss.get(pid, '?'))
prev = pid
return ' '.join(glosses)
| Датасет | Метод | 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% | — |
Ключевые трудности: знаки с похожей формой руки различаются только движением (motion-based disambiguation); fingerspelling (буква по букве) требует высокого fps и отдельной модели; у разных носителей языка значительная вариативность исполнения знаков.
| Задача | Срок |
|---|---|
| Изолированные знаки, 100–500 классов | 6–10 недель |
| CSLR с CTC на готовом датасете | 12–18 недель |
| Реальное время + fingerspelling + адаптация под пользователя | 20–30 недель |







