Разработка AI для описания визуального контента для незрячих
Системы описания изображений для незрячих и слабовидящих пользователей — это не просто image captioning. Требования принципиально другие: описание должно быть практически полезным, а не поэтически красивым. Пользователю важно знать не «красивый закат над морем», а «вы стоите перед стеклянной дверью, на ней написано PUSH, слева — кнопка домофона». Задача делится на несколько сценариев: navigation assistance (что перед пользователем), document reading (текст на упаковке/вывеске), face recognition (кто рядом), currency/product identification.
Image Description Pipeline
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 # immediate / informational
class AccessibleImageDescriber:
"""
Описание изображений для незрячих пользователей.
Три уровня детализации:
- Brief (1 предложение): для быстрой ориентации
- Standard (3-5 предложений): основная информация
- Detailed (полное описание): для важных документов
VLM: Qwen2-VL-7B-Instruct или InternVL2-8B.
"""
# Промпты адаптированы для accessibility
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
)
# TrOCR для чёткого распознавания текста (документы, упаковки)
self.ocr_processor = TrOCRProcessor.from_pretrained(ocr_model)
self.ocr_model = VisionEncoderDecoderModel.from_pretrained(
ocr_model
).to(device)
# Детектор текстовых областей (EAST или DB-Net через OpenCV DNN)
self._text_detector = None # загружается по требованию
def describe(self, image: np.ndarray,
context: str = 'navigation',
lang: Optional[str] = None) -> VisualDescription:
"""
Основной метод описания.
context: navigation / document / social / product
"""
target_lang = lang or self.language
pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
# Selection промпта
base_prompt = self.PROMPTS.get(context, self.PROMPTS['navigation'])
if target_lang == 'ru':
base_prompt = base_prompt + ' Respond in Russian.'
# VLM inference
vlm_description = self._run_vlm(pil, base_prompt)
# OCR для текстового контента
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, # VLM не возвращает confidence напрямую
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]:
"""Быстрое OCR для текста на изображении"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# EAST text detector через OpenCV
# Упрощённая версия: прямое TrOCR на всё изображение
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:
"""Генерация краткой навигационной подсказки"""
# Разделить изображение на 3 зоны: лево/центр/право
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]:
"""Простое извлечение ключевых объектов"""
# В production — NER модель
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 'купюра не распознана')
}
| Сценарий | Модель | Качество |
|---|---|---|
| Навигация в помещении | Qwen2-VL-7B | SPICE 22–26 |
| Распознавание текста/вывесок | TrOCR-base | CER 3–8% |
| Описание людей | InternVL2-8B | BLEU-4 28–34% |
| Распознавание купюр | EfficientNet-B0 | 94–98% |
| Идентификация продуктов | CLIP + каталог | Recall@5 78–85% |
Latency требования: для navigation — не более 2–3 секунды на ответ (пешеход движется); для document reading — 5–10 секунд допустимы. Offline-режим критичен: пользователь должен работать без интернета.
| Задача | Срок |
|---|---|
| Navigation description + OCR (один контекст) | 5–8 недель |
| Мульти-контекст + мобильное приложение (iOS/Android) | 10–16 недель |
| Полная accessibility платформа с голосовым UI | 18–28 недель |







