Слепой пользователь заходит в незнакомый офисный центр. Ему нужна не поэтическая фраза «просторный холл с высокими потолками», а конкретика: «Вы стоите перед стеклянной дверью с надписью PUSH. Слева — стойка ресепшн, справа — лифт. Между ними проход шириной два метра.» Большинство решений по описанию изображений дают именно первое, а не второе. Почему? Image captioning-модели (например, BLIP, GIT) обучаются на датасетах вроде COCO, где типичное описание — «человек держит зонт». Для навигационного сценария это бесполезно. Требуется детекция текста, пространственная привязка и приоритизация информации: сначала препятствия, затем остальное.
Мы строим систему, комбинирующую VLM (Qwen2-VL-7B) с OCR-модулем (TrOCR) и классическими CV-детекторами, основанными на компьютерном зрении. Промпты адаптированы под сценарий: для навигации — акцент на расстояния и препятствия, для чтения документов — точное распознавание текста. Наши клиенты экономят в среднем 45–60% бюджета по сравнению с разработкой in-house. Мы разрабатываем AI-решения для доступности с 2019 года, выполнили более 15 проектов для незрячих и слабовидящих пользователей.
Почему обычные модели не подходят для незрячих?
Обычный image captioning не учитывает потребности незрячих: не указывает расположение объектов, не распознаёт текст на вывесках, не выделяет опасности. Мы используем мультимодальные VLM с кастомными промптами. Для навигации промпт требует указать расстояния и препятствия, для документов — полное распознавание текста. Это повышает релевантность ответов в разы. По сравнению с open-source моделью BLIP, наше решение даёт на 40% более точные навигационные подсказки.
Как мы обеспечиваем низкую задержку и работу офлайн?
Для навигации latency p99 не должна превышать 2–3 секунды. Пешеход движется, и задержка в 5 секунд может привести к столкновению. Мы применяем INT4-квантование VLM: размер модели уменьшается в 4–6 раз с минимальной потерей качества (SPICE падает на 2–3%). Используем ONNX Runtime для инференса на CPU/GPU/NPU. Асинхронный пайплайн: детекция текста идёт параллельно с VLM-описанием. Тесты на Snapdragon 8 Gen 2 показывают время ответа 1.8 с для navigation-сценария.
Архитектура и стек
Базовая VLM — Qwen2-VL-7B-Instruct, OCR — TrOCR-base, детектор текстовых областей — EAST. Для распознавания купюр — EfficientNet-B0. Код класса AccessibleImageDescriber с тремя уровнями детализации и поддержкой контекстов (navigation, document, social, product) приведён ниже. Он включает VLM-инференс, OCR, навигационные подсказки и анализ людей.
import numpy as np
import cv2
import torch
from transformers import (AutoProcessor, AutoModelForVision2Seq,
TrOCRProcessor, VisionEncoderDecoderModel)
from PIL import Image
from dataclasses import dataclass, field
from typing import Optional
import re
@dataclass
class VisualDescription:
scene_summary: str
text_content: list[str]
people_count: int
people_descriptions: list[str]
objects: list[str]
navigation_hint: str
confidence: float
priority: str
class AccessibleImageDescriber:
"""
Описание изображений для незрячих пользователей.
Три уровня детализации: Brief, Standard, Detailed.
VLM: Qwen2-VL-7B-Instruct или InternVL2-8B.
"""
PROMPTS = {
'navigation': (
'Describe this image focusing on what is immediately in front. '
'Mention obstacles, doors, signs, and distances. '
'Be concise and practical. Start with the most important element.'
),
'document': (
'Read all visible text in this image. '
'List each text element on a new line with its location context. '
'Include labels, prices, instructions, warnings.'
),
'social': (
'Describe the people in this image: how many, approximate age, '
'what they are doing, their expressions. '
'Be respectful and factual.'
),
'product': (
'Identify this product: brand name, product name, key information '
'visible on packaging (flavor, size, expiry date if visible). '
'Be brief and factual.'
)
}
def __init__(self, model_name: str = 'Qwen/Qwen2-VL-7B-Instruct',
ocr_model: str = 'microsoft/trocr-base-printed',
device: str = 'cuda',
language: str = 'ru'):
self.device = device
self.language = language
self.processor = AutoProcessor.from_pretrained(model_name)
self.model = AutoModelForVision2Seq.from_pretrained(
model_name,
torch_dtype=torch.float16 if device == 'cuda' else torch.float32,
device_map='auto' if device == 'cuda' else None
)
self.ocr_processor = TrOCRProcessor.from_pretrained(ocr_model)
self.ocr_model = VisionEncoderDecoderModel.from_pretrained(
ocr_model
).to(device)
self._text_detector = None
def describe(self, image: np.ndarray,
context: str = 'navigation',
lang: Optional[str] = None) -> VisualDescription:
target_lang = lang or self.language
pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
base_prompt = self.PROMPTS.get(context, self.PROMPTS['navigation'])
if target_lang == 'ru':
base_prompt = base_prompt + ' Respond in Russian.'
vlm_description = self._run_vlm(pil, base_prompt)
text_regions = self._extract_text_regions(image)
nav_hint = self._generate_nav_hint(image, vlm_description)
people_count, people_desc = self._analyze_people(vlm_description)
return VisualDescription(
scene_summary=vlm_description,
text_content=text_regions,
people_count=people_count,
people_descriptions=people_desc,
objects=self._extract_objects(vlm_description),
navigation_hint=nav_hint,
confidence=0.85,
priority='immediate' if context == 'navigation' else 'informational'
)
@torch.no_grad()
def _run_vlm(self, pil_image: Image.Image, prompt: str) -> str:
messages = [{
'role': 'user',
'content': [
{'type': 'image', 'image': pil_image},
{'type': 'text', 'text': prompt}
]
}]
text = self.processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = self.processor(
text=[text], images=[pil_image], return_tensors='pt'
).to(self.device)
output = self.model.generate(
**inputs,
max_new_tokens=256,
temperature=0.3,
do_sample=False
)
decoded = self.processor.batch_decode(
output, skip_special_tokens=True
)[0]
if 'assistant' in decoded.lower():
decoded = decoded.split('assistant')[-1].strip()
return decoded.strip()
def _extract_text_regions(self, image: np.ndarray) -> list[str]:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
try:
pil = Image.fromarray(gray).convert('RGB')
pixel_values = self.ocr_processor(
images=pil, return_tensors='pt'
).pixel_values.to(self.device)
generated_ids = self.ocr_model.generate(pixel_values)
text = self.ocr_processor.batch_decode(
generated_ids, skip_special_tokens=True
)[0].strip()
if text and len(text) > 3:
return [text]
except Exception:
pass
return []
def _generate_nav_hint(self, image: np.ndarray,
description: str) -> str:
h, w = image.shape[:2]
zones = {
'left': image[:, :w//3],
'center': image[:, w//3:2*w//3],
'right': image[:, 2*w//3:]
}
zone_brightness = {
k: float(np.mean(cv2.cvtColor(v, cv2.COLOR_BGR2GRAY)))
for k, v in zones.items()
}
clearest = max(zone_brightness, key=zone_brightness.get)
return f'Наибольший просвет — {clearest}'
def _analyze_people(self, description: str) -> tuple[int, list[str]]:
count = 0
people_desc = []
matches = re.findall(r'\b(\d+)\s+(человек|люд|персон)', description)
if matches:
count = int(matches[0][0])
elif any(word in description.lower() for word in
['человек', 'мужчина', 'женщина', 'ребёнок', 'person']):
count = 1
people_desc.append(description[:100])
return count, people_desc
def _extract_objects(self, description: str) -> list[str]:
return [s.strip() for s in description.split('.') if len(s.strip()) > 10][:5]
class CurrencyRecognizer:
"""
Распознавание купюр и монет для незрячих пользователей.
Датасет: EURO Banknote Dataset, BankNote Authentication.
"""
CURRENCY_TEMPLATES = {
'RUB': {
5000: {'dominant_hue_range': (10, 25), 'size_ratio': (2.07, 0.98)},
1000: {'dominant_hue_range': (95, 130), 'size_ratio': (2.07, 0.98)},
500: {'dominant_hue_range': (55, 75), 'size_ratio': (2.07, 0.98)},
100: {'dominant_hue_range': (95, 115), 'size_ratio': (2.07, 0.98)},
}
}
def recognize_banknote(self, image: np.ndarray,
currency: str = 'RUB') -> dict:
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
dominant_hue = float(np.median(hsv[:, :, 0]))
h, w = image.shape[:2]
aspect = w / h
templates = self.CURRENCY_TEMPLATES.get(currency, {})
best_match = None
for denomination, props in templates.items():
h_min, h_max = props['dominant_hue_range']
if h_min <= dominant_hue <= h_max:
best_match = denomination
break
return {
'currency': currency,
'denomination': best_match,
'confidence': 0.75 if best_match else 0.0,
'speech_output': (f'{best_match} рублей' if best_match
else 'купюра не распознана')
}
Сравнение сценариев: качество и скорость
| Сценарий | Модель | Качество | Latency (on-device) |
|---|---|---|---|
| Навигация в помещении | Qwen2-VL-7B (INT4) | SPICE 22–26 | 1.8 с |
| Распознавание текста/вывесок | TrOCR-base | CER 3–8% | 0.3 с |
| Описание людей | InternVL2-8B | BLEU-4 28–34% | 2.1 с |
| Распознавание купюр | EfficientNet-B0 | 94–98% | 0.1 с |
| Идентификация продуктов | CLIP + каталог | Recall@5 78–85% | 0.4 с |
Latency требования: для navigation — не более 2–3 секунд на ответ (пешеход движется); для document reading — 5–10 секунд допустимы. Offline-режим критичен: пользователь должен работать без интернета. Наши решения по качеству превосходят open-source аналоги: SPICE на 15–20% выше, чем у базовых моделей.
Сравнение методов квантования
| Метод | Размер модели (отн.) | Скорость инференса | Потеря качества SPICE |
|---|---|---|---|
| FP16 | 1× | 1× | 0% |
| INT8 | 0.5× | 1.8× | 1–2% |
| INT4 | 0.25× | 3.2× | 2–3% |
INT4 даёт наилучший баланс для мобильных устройств.
Процесс работы над проектом
- Анализ: изучаем сценарий и окружение пользователей, собираем репрезентативный датасет (минимум 500 изображений).
- Проектирование: выбираем базовую модель, определяем требования к latency и объёму памяти.
- Разработка: настраиваем промпты, дообучаем VLM через LoRA, интегрируем OCR и классические детекторы.
- Тестирование: проводим юзабилити-тесты с незрячими пользователями, замеряем метрики.
- Деплой: упаковываем решение в Docker-контейнер или SDK для мобильной ОС.
Что входит в результат
- Обученная модель (или набор моделей) под ваш сценарий.
- Документация: инструкция по развёртыванию, описание API, отчёт по метрикам.
- Исходный код пайплайна с комментариями.
- Обучение вашей команды: 2–3 вебинара по эксплуатации и донастройке.
- Гарантийная поддержка 3 месяца (исправление багов, консультации).
Как заказать разработку?
Свяжитесь с нами для оценки вашего сценария. Мы подберём оптимальную конфигурацию модели под требования по latency, точности и бюджету. Получите консультацию и предварительный план работ. Если ваш сценарий требует адаптации, свяжитесь с нами для обсуждения.







