AI Sign Language Recognition System

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
AI Sign Language Recognition System
Medium
~2-4 weeks
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822

Разработка 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 недель