С развитием LLM моделей AI начали появляться разные ИИ агенты, автоматизирующие задачи.
Но есть задачи, типа рутинного создания папок в облаке или удаления файлов, которые хорошо бы автоматизировать, но ручками сделать можно.
А есть задачи, где без дополнительной технической помощи никак. Сейчас я говорю например о тех, которые в связи с инвалидностью просто физически не могут осуществлять элементарные для большинства операции.
Сегодня я вспомнил об этом посте моего незрячего знакомого Дениса Шишкина. Суть поста в том, что сегодня нейронные сети так популярны, а вещами, которые как ожидается могут быть простыми, но очень полезными для незрячих, никто не занимается.
Пять минут разработки показали, что даже без AI возможно сделать достаточно много. Я решил попробовать сделать больше и написать эту статью, дабы осветить тему и поделиться своими наработками и размышлениями.
Важный дисклеймер: AI, ML, CV - не являются моими основными направлениями. Более того я откровенно признаюсь, что не знаю, как многое из этого работает внутри. А потому те задачи, которые я решаю, я решаю с позиции своего опыта и знаний, которые как мы уже поняли ограничены.
Дисклеймер №2: поскольку я незрячий, именно о невизуальной доступности речь. Но есть много иных инвалидностей, каждая со своими особенностями и также нуждающиеся в технической поддержке.
Описание проблемы
Если вы не знаете, как незрячие пользуются дивайсами, на Хабре есть эта и эта статьи, которые раскрывают предмет достаточно подробно. Потому позвольте мне подробно на этом не останавливаться и лишь пояснить, что есть программы экранного доступа, которые с помощью синтеза речи озвучивают содержимое экрана, позволяют осуществлять навигацию и взаимодействовать с элементами.
А еще здесь я писал о СДВГ.
Одной из самых известных программ экранного доступа является NVDA, являющаяся открытой и бесплатной.
В основе способности NVDA описывать графический интерфейс лежит использование API специальных возможностей (Accessibility APIs). Это наборы программных интерфейсов, которые разработчики операционных систем и приложений встраивают в свои продукты, чтобы вспомогательные технологии, такие как программы экранного доступа, могли получать информацию об элементах управления.
Как мы знаем, приложения бывают разные: настольные (из которых UWP, Windows Forms, интерфейсы иных библиотек и API), веб (фронт), консольные, иные. А потому, NVDA использует разные API для обеспечения максимальной совместимости с различными приложениями:
Microsoft UI Automation (UIA): Это современный API от Microsoft, который предоставляет наиболее полную и подробную информацию об элементах интерфейса в приложениях Windows.
Microsoft Active Accessibility (MSAA): Более старый, но все еще широко используемый API. NVDA обращается к нему для поддержки устаревших приложений, которые не совместимы с UIA.
IAccessible2: Разработанный в сотрудничестве с сообществом открытого исходного кода, этот API расширяет возможности MSAA и активно используется в таких приложениях, как Mozilla Firefox.
Java Access Bridge: Специализированный мост, который позволяет NVDA взаимодействовать с приложениями, написанными на языке программирования Java.
Через эти API NVDA запрашивает у каждого элемента интерфейса (будь то кнопка, текстовое поле, меню или ссылка) его свойства: имя (что это за элемент, например, "ОК"), роль (тип элемента, например, "кнопка"), значение (содержимое элемента, например, введенный текст), состояние (активен, отмечен, свернут) и другую информацию, такую как его положение на экране.
Касаемо веб-содержимого, NVDA запрашивает у браузера доступ к структуре страницы через специализированные API доступности, такие как IAccessible2. Технически, создается виртуальный буфер, состоящий из текстового представления полученного DOM. Благодаря буферу пользователь может перемещаться по веб-странице так, как будто это обычный текстовый документ, используя клавиши со стрелками для чтения по строкам или по символам. Можно использовать горячие клавиши для мгновенной навигации по типам элементов, таким как ссылки, заголовки, списки и иные.
Но важно, что способность NVDA видеть приложение напрямую зависит от того, предоставляет ли это приложение информацию о себе через поддерживаемые API доступности. Если разработчики инструментария для создания графического интерфейса не реализовали эту поддержку, то для NVDA приложение будет слепым пятном.
Когда я изучал Python, во многой литературе показывались примеры создания GUI-интерфейса на библиотеке tkinter, которая не предоставляет необходимую информацию. NVDA запрашивает у окна Tkinter-приложения информацию о находящихся в нем элементах, и не получает ответа. Для NVDA окно такого приложения - это просто прямоугольная область без каких-либо опознаваемых кнопок, текстовых полей или меню. Пользователь может услышать только заголовок окна, но не сможет взаимодействовать с его содержимым стандартными средствами.
Элемент canvas в HTML5 позволяет разработчикам рисовать графику на лету с помощью JavaScript. Это может быть что угодно: от диаграмм и графиков до полноценных игр. Для NVDA элемент canvas по умолчанию является единым растровым изображением, как картинка в формате JPEG. В нем нет отдельных объектов (кнопок, текста), которые можно было бы запросить через API. Если разработчик специально не предпринял значительных усилий по добавлению запасного текстового контента или использованию экспериментальных технологий доступности для canvas, его содержимое будет невидимым для экранного чтеца.
И еще один, для многих неочевидный пример, игры.
Да, незрячие играют в игры. Потому что в шутерах, например, есть 3д звук, по которому можно осуществлять навигацию и нацеливаться на врагов, но есть сложности с прицелом, который может скакать вверх и вниз. Эти проблемы, насколько мне известно, на консолях решаются дополнительным функционалом автоприцела, но я не щупал, лично ничего сказать не могу.
Известные API, использующиеся для создания игр, такие как DirectX или OpenGL, отрисовывают изображение напрямую на видеокарте, минуя стандартные элементы интерфейса операционной системы. И для примера в меню настроек игры NVDA не сможет прочитать пункты "Графика", "Звук", "Управление". Для экранного чтеца весь экран игры - это одна большая, неинтерактивная область. Доступность в таких играх возможна только в том случае, если разработчики сами встроили в игру собственный механизм озвучивания интерфейса.
Вы можете меня спросить: "а зачем тебе работать с недоступными приложениями?".
Ответ: иногда нет выхода. Например, семплер Kontakt, использующийся в создании музыки и аранжировок. Или Unity и Unreal Engine, игровые движки, для полноценного взаимодействия с которыми не обойтись без использования их редакторов, которые, к сожалению, соответствующей доступности не имеют.
Эти проблемы пытались решать еще много лет назад. И решали, с помощью например определения координат тех или иных элементов и осуществления доступа по ним. Но есть разные разрешения экранов, масштабы, и привычка разработчиков менять интерфейс чуть ли не в каждом обновлении!
Начинаем размышлять о решении
Пост Дениса, на который я ссылался выше, побудил во мне мысли о способах реализации. Но идея требовала доработки, ибо обучение нейросети выглядит уныло во всех смыслах.
Чтобы научить модель понимать все возможные десктоп-интерфейсы (особенно кастомные движки типа Kontakt/Unity/Unreal), нужны десятки тысяч размеченных скриншотов и долгое обучение. Это дорого и долго, а устойчивость к темам/скинам/масштабированию всё равно будет хромать.
Есть ИИ агенты, которые лихо могут работать в браузере, создавая сотни папок и выполняя иные рутинные задачи. Но как мы помним, в вебе почти все доступно, и заниматься поиском объектов не нужно.
А вот использование компьютерного зрения, это именно то, к чему я решил присмотреться.
Для решения многих проблем доступности, нужно:
Найти объект на экране;
Установить его положение;
Поместить на него курсор мыши;
Активировать!
Первый успех
Я примерно представлял свою первую программу, которая должна была найти ярлык Visual Studio на рабочем столе, обнаружить ее координаты (которые я смогу использовать из NVDA), а еще лучше, сразу установить курсор!
Последовательность действий (алгоритм) представлялся также просто:
Делаем снимок всего экрана в данный момент;
Загружаем шаблон искомого объекта;
Ищем это маленькое изображение внутри большого изображения (скриншота);
OpenCV возвращает точные пиксельные координаты верхнего левого угла найденного элемента на скриншоте.
Но был один факт, который мог все поломать.
Поскольку я решил писать на Python, надо было учесть, что большинство библиотек - кросс-платформенные. А поскольку мы собираемся не просто найти объект, но и подвинуть мышь к нему, я ожидаю, что библиотека будет использовать системный API, который в разных ОС работает по разному!
Экран монитора или смартфона состоит из множества крошечных светящихся точек - пикселей.
Есть физические координаты - это координаты, которые напрямую соответствуют этим реальным, физическим пикселям.
А есть логические координаты, которые представляют собой абстракцию, которую использует операционная система и приложения. Они были введены для того, чтобы интерфейсы программ выглядели одинаково на экранах с разной плотностью пикселей.
Современные дисплеи, особенно на смартфонах и ноутбуках, имеют очень высокую плотность пикселей. Если бы операционная система и программы рисовали элементы интерфейса в масштабе 1:1 (один логический пиксель равен одному физическому), то кнопки, текст и иконки на таких экранах были бы невероятно мелкими и нечитаемыми.
Чтобы решить эту проблему, операционные системы применяют масштабирование, которое можно настроить в параметрах Windows, где можно поставить масштаб 125%, 150% и т.д.
Если вы ожидаете, что воспользовавшись Print Screen на клавиатуре, вы получаете логические пиксели - вы ошибаетесь.
Программа, например, браузер, думает, что ей нужно нарисовать окно размером 800x600 пикселей. Она оперирует логическими координатами.
В настройках системы пользователя стоит масштабирование 150. Операционная система перехватывает команду от браузера и говорит: "Подожди, для этого дисплея с высокой плотностью пикселей 800x600 будет слишком мелко. Я увеличу все в 1.5 раза".
Windows отрисовывает окно браузера, используя для этого уже 1200x900 физических пикселей на экране (800 * 1.5 = 1200; 600 * 1.5 = 900). Текст и иконки также проходят через процесс масштабирования, чтобы выглядеть четко, а не просто быть растянутыми.
Когда нажимаем Print Screen, система делает фотографию финального изображения, которое уже подготовлено для отправки на монитор. Она копирует в буфер обмена именно тот массив из 1200x900 физических пикселей.
Исторически, macOS чаще оперировала логическими точками, в то время как Windows-API традиционно был ближе к физическим пикселям. Я не разбирался детально, но из прочитанного я понял, что вроде как при попытке двигать мышь в MacOS, это осуществляется именно по логическим пикселям.
И здесь я потратил много времени на то, чтобы разобраться, как оно работает именно в Windows.
Я находил неоднозначную информацию.
В Windows есть такое понятие, как DPI Awareness (DPI-осведомленность) - это свойство процесса которое сообщает Windows, как приложение будет реагировать на масштабирование:
Неосведомленный - приложение думает, что работает при 100% масштабе (96 DPI). Windows видит это и включает виртуализацию: она растягивает окно приложения. При этом некоторые API для такого приложения могут возвращать значения в логических координатах, чтобы не сломать его внутреннюю логику.
Осведомленный - приложение само заявляет, что умеет корректно отрисовывать свой интерфейс при разных настройках масштаба. В этом случае Windows не вмешивается так грубо, и приложение получает от системы реальные данные о масштабе, чтобы самостоятельно адаптировать свой интерфейс.
Я находил информацию о том, что перемещение мыши может работать с логическими пикселями, если процесс не является DPI-осведомленным. Но также я нашел, что SetCursorPos из WinAPI оперирует физическими пикселями.
Как успокоение, процесс можно сделать DPI-осведомлённым, но мне так не хотелось заморачиваться с этими низкоуровневыми деталями...
В итоге, я реализовал алгоритм таким образом:
Заметка для админов: в markdown не работает нормально форматирование кода, и даже скрытие текста. Пока так, а обратную связь сделал.
import cv2
import pyautogui
import numpy as np
TEMPLATE_IMAGE_PATH = "C:\\test\\icon.jpg" # этолонный файл, который ищем
try:
screenshot = pyautogui.screenshot() # скриним
screenshot_np = np.array(screenshot) #переводим из pil формата в np
main_image = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR) # меняем с RGB на BGR, потому что OpenCV работает в BGR
template = cv2.imread(TEMPLATE_IMAGE_PATH, cv2.IMREAD_COLOR) # читаем
if template is None:
print(f"Ошибка: не удалось загрузить шаблон из {TEMPLATE_IMAGE_PATH}")
else:
result = cv2.matchTemplate(main_image, template, cv2.TM_CCOEFF_NORMED) # выполняет сопоставление эталона с основным изображением
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) # мин/макс значения на карте совпадений
confidence_threshold = 0.8
if max_val >= confidence_threshold:
print(f"Шаблон найден с уверенностью {max_val:.2f}")
top_left = max_loc
h, w, _ = template.shape
center_x = top_left[0] + w // 2
center_y = top_left[1] + h // 2
print(f"Координаты центра найденного элемента: ({center_x}, {center_y})")
pyautogui.moveTo(center_x, center_y, duration=0.5)
print("Курсор перемещен!")
else:
print(f"Шаблон не найден. Максимальная уверенность: {max_val:.2f}")
except FileNotFoundError:
print(f"Ошибка: файл шаблона не найден по пути {TEMPLATE_IMAGE_PATH}")
except Exception as e:
print(f"Произошла непредвиденная ошибка: {e}")
Здесь мы четко следуем алгоритму: скриним; ищем; если уверенность совпадения высока вычисляем координаты центра найденной области, чтобы двигать курсор именно в центр иконки и плавно двигаем мышь!
Продолжение размышлений
Иногда так бывает, что ты погрузился в теорию, понервничал, а оно взяло и сработало без дополнительной работы с DPI, без иных разбирательств, что там внутри библиотеки и что за API используется.
Короче говоря, программа сработала! Она нашла ярлык Visual Studio, навела на него мышь, осталось кликнуть!
Без костыля правда не обошлось. Поскольку это не нечто релизное, я сделал time.sleep(5) чтобы успеть скрыть окна и перейти на рабочий стол. Потом консоль подтвердила выполнение, а NVDA подсказал, что координаты мыши изменились.
Но проблему я обнаружил. Огромную, что тот небоскреб в вашем городе (хотя быть может вы живете в раю без этих махин?).
А как получать эталон?
Я делал скрин рабочего стола. Просил GPT вырезать иконку. Получился просто какой-то синий фон.
Мне помогло дополнение к NVDA, которое когда-то по моему заказу написал @cyrmax. Вообще-то, оно должно было копировать изображение, находящееся под курсором. Но с иконками оно тоже справилось.
Код можно почитать здесь.
Но для того, чтобы это сработало, мне надо поместить курсор на объект. А если я помещаю курсор на объект, зачем мне его искать? Весь смысл же в том, чтобы найти объект, который найти средствами NVDA я не могу.
И здесь возникает несколько идей:
Каждый раз, для каждого объекта, нанимать кого-нибудь, который за копеечку обрежет скрин и сделает эталон. А завтра интерфейс лом лом (благодарочка, любимые девы/QA);
Пытаться обрезать программно по тем же пикселям, которые просить AI определить. Не знаю, пробовал разные модели, справлялись плохо;
Можно придумать программу, которая обходит окно, делит на сетку, вырезает части и по OCR ищет нужные надписи. Но надписи же есть не везде;
С надписями можно проще, сразу найти текст по OCR, найти координаты, расширить, но в NVDA есть OCR и по нему можно пользоваться.
Есть еще 2 варианта.
Zero-shot detection
Идея обучать нейросети все же не так глупа. Но это сделано, дабы иные пользовались!
Zero-shot detection - это тип модели компьютерного зрения, которая может находить на изображении объекты, которые она никогда не видела во время своего обучения.
Традиционное обнаружение объектов требует обучения на тысячах изображений, где вручную размечены "кнопки", "иконки", "флажки" и т.д. Модель учится распознавать только те категории, которые видела. Если показать ей что-то новое, она не справится.
А Zero-shot обучается на огромном количестве пар "изображение + текст". Она учится понимать не конкретные объекты, а связь между визуальными характеристиками и их текстовым описанием.
Проще говоря, вместо того чтобы показывать модели эталонное изображение и говорить: "Найди вот это", вы даете ей скриншот и говорите: "Найди на этом изображении 'кнопку добавления библиотеки'" или "Найди 'иконку компиляции. Модель использует свое обобщенное понимание того, как выглядят кнопки и что означает текст, чтобы найти нужный элемент.
Я смог загрузить модель, для теста я взял google/owlvit-base-patch32. Но какие описания я бы не давал, и просто Visual Studio, и иконка Visual Studio, он ни то что уверенность не рассчитывал, ему не из чего было рассчитывать!
Это объясняется просто, такие модели, как правило, тренируются не на изображениях софта, а на фото. То есть собаку найдет, а ярлычок потеряет.
Попробовал иную модель, grounding-dino-base. С ней результаты есть, но с маленькой точностью.
В итоге, либо я не доразобрался, либо все же это не очень подходит для моей задачи.
LLM
Я решил возвратиться к идее использования LLM, даже если они нормально не могут обрезать изображения.
Есть такой промпт:
На приложенном изображении экрана найди значок ярлыка Visual Studio. В качестве ответа предоставь:
координата X верхнего левого угла,
координата Y верхнего левого угла,
ширина элемента в пикселях,
высота элемента в пикселях
Отдаю GPT5 и Gemini 2.5 Pro
Ответ GPT: 18, 377, 60, 59
Ответ Gemini: 11, 302, 86, 76
Центр по GPT: 48, 406
Центр по Gemini: 54, 340
Ээээх! А сработал то именно GPT!
Идея реализации
А идея проста: подключить AI по API, предоставить пользователю запрос описания, а координаты получать в json.
И таким образом получаем даже то, чего многие не имеют. Это ни все решает, так как есть всплывающие менюшки, иные динамические элементы.
Дальнейшие перспективы
Задачей не было разработать решение. Я желал поделиться проблемой, личными изысканиями о способах решения. Возможно здесь есть гении AI и CV, которые могут подсказать, направить, альтернативы показать.
Сейчас действительно надо понять, как быть с динамическими элементами, когда к пункту б не одна остановка, а несколько. Слышал о SikuliX, но не пробовал.
А мне, заинтересованному в играх, вообще интересно разработать аимы для незрячих. Не которые в голову целятся, а хотя бы находят прицелом ближайшего противника.
Представьте: я нацеливаюсь по 3д звуку. Я понимаю, что противник впереди, но он может быть сверху, снизу, и дело даже не в том, что прицел не посередине, а что противник может быть в окне и т.д.
И такое можно было бы придумать, но не думаю, что с помощью LLM. Ждать по 10 секунд? А ведь здесь же не просто на пиксели надо направить, а прицелом двигать.
Здесь вероятно высокопроизводительное компьютерное зрение. Нужна модель которая не просто анализирует статичное изображение, а делает это десятки раз в секунду. Есть вроде YOLO, но это пока размышления, к практике я не переходил. Это надо обучать, а дальше я пока не разбирался.
По ходу исследований я нашел существующие продукты, которые решают поставленные задачи и даже более того, пытаются быть ИИ агентами. Продукты, которые созданы именно для незрячих. И они платные! Я крайне негативно отношусь к платному софту для лиц с инвалидностью, потому надеюсь на создание альтернатив.
Надеюсь, это чтиво было познавательным и интересным! Добро пожаловать на мои Telegram и Youtube.