Разборы

Живой разбор · данные Artificial Analysis

Кто двигает ИИ фронтир США / Китай?

Календарь релизов, смена национальных лидеров и разрыв между странами — на одной временной шкале.

Открыть модели

Разберём вашу задачу

Опишите её в двух-трёх предложениях. Я отвечу, что проверить первым и нужен ли здесь ИИ.

Написать в Telegram →
Материал Выберите статью
12 минут
Практика

Как я ускорил локальный OCR в 132 раза — и забраковал самый быстрый вариант

Как точный кеш неизменившихся кадров ускорил локальный Vision OCR в 132 раза на 1440p — и почему победитель бенчмарка оказался опасен для продукта.

Схема точного кеша кадров SecureAI Screen: 0,473 мс для неизменившегося кадра и ускорение OCR в 132 раза на 1440p

На созвоне менеджер открывает CRM. В соседнем окне всплывает карточка клиента: телефон, паспортные данные, ИНН. До выключения демонстрации проходят секунды — достаточно, чтобы запись сохранила утечку. «SecureAI Screen» должен закрыть такие строки маской прямо на экране, пока человек продолжает работать.

Это фоновое приложение для macOS. ScreenCaptureKit получает изображение дисплея, Vision локально распознаёт текст, детекторы проверяют контрольные суммы и контекст, а поверх чувствительных фрагментов появляется защитная маска. Кадры и распознанный текст не уходят в сеть и по умолчанию не сохраняются.

Проблема обнаружилась в самом дорогом участке. Пока пользователь смотрел на неподвижное окно, приложение снова запускало Vision на тех же миллионах пикселей. Повторная обработка кадра 1440p занимала в медиане 62,659 мс. Точное сравнение сократило этот путь до 0,473 мс — в 132 раза. Но эксперимент быстро упёрся в более неприятный вопрос: как пропускать ненужный OCR, не пропуская ни одного изменившегося символа?

Результат за 30 секунд

Неизменившийся кадр 1440p обрабатывается за 0,473 мс вместо 62,659 мс — ускорение 132×. На 4K остаётся 66×. Любое отличие, вплоть до одного пикселя, отправляет кадр в полный OCR. Самый быстрый вариант я не выбрал: он мог занять весь пул ScreenCaptureKit.

Ускорение не должно ослепить защиту

У экранного OCR есть очевидный короткий путь: взять несколько участков кадра, посчитать быстрый отпечаток и вернуть прошлый результат при совпадении. Для превью или аналитики редкая ошибка может быть приемлемой. Здесь ошибка означает открытую строку с персональными данными.

Достаточно одной цифры. Валидный ИНН превращается в другой номер или теряет корректную контрольную сумму. Картинка для человека почти та же, но ответ системы уже другой. Если выборка пикселей не заденет изменённое место, кеш вернёт старый текст и оставит новые данные без маски.

Сравнение указателей тоже не помогло. ScreenCaptureKit может прислать одинаковые пиксели в разных CVPixelBuffer. Адрес буфера говорит, где лежит кадр, но ничего не доказывает о его содержимом. В бенчмарке объекты различались, хотя каждый байт совпадал.

Поэтому правило кеша стало двоичным. Сначала совпадают геометрия, формат пикселей, число плоскостей и bytesPerRow. Затем сравнивается каждый байт. Полное совпадение разрешает вернуть сохранённый OCR; любое отличие запускает Vision заново. Вероятности в этом решении нет.

На первый взгляд сравнивать десятки мегабайт 4K-кадра слишком дорого. На практике последовательное чтение памяти занимает около миллисекунды, а Vision OCR — 60–70 мс. Разница огромна, но не постоянна: чем больше разрешение, тем сильнее растёт цена сравнения. Именно поэтому одного маленького тестового изображения недостаточно.

Схема точного сравнения кадров: неизменившийся кадр возвращает кеш OCR за 0,473 мс, изменившийся запускает полный Vision OCR за 62,659 мс; ускорение 132× на 1440p и 66× на 4K
Схема точного сравнения кадров: неизменившийся кадр возвращает кеш OCR за 0,473 мс, изменившийся запускает полный Vision OCR за 62,659 мс; ускорение 132× на 1440p и 66× на 4K

Как устроен точный кеш из двух кадров

Кеш хранит два снимка пикселей и два соответствующих результата OCR. Одного элемента мало: рабочий стол легко чередует два состояния — например, окно и выпадающее меню. При точном совпадении Vision не запускается, а сохранённые строки получают номер нового кадра и возвращаются в конвейер.

При несовпадении выполняется обычный OCR, после чего новая пара «снимок + распознанные строки» попадает в кеш. Смена разрешения, разрыв последовательности и ручной сброс тоже ведут в полный путь. При остановке защиты пиксельные снимки удаляются вместе с остальным временным состоянием.

Алгоритм, который можно повторить

Базовая версия сводится к пяти шагам:

  1. Сравнить ширину, высоту, формат пикселей, число плоскостей и bytesPerRow. Несовпадение сразу ведёт в OCR.
  2. Сопоставить каждый байт нового кадра с собственной неизменяемой копией, включая содержимое всех плоскостей.
  3. При полном совпадении вернуть сохранённые строки OCR с номером текущего кадра.
  4. При любом отличии выполнить Vision OCR и сохранить новую пару «снимок + результат».
  5. Ограничить кеш двумя элементами и очищать его при остановке защиты, смене дисплея и сбросе конвейера.

Вся оптимизация помещается в это ветвление. Сложность начинается внутри sameBytes: сравнение должно быть исчерпывающим и не удерживать дефицитные буферы захвата.

Эта простая развилка даёт почти весь выигрыш. SIMD, обработку изменённых областей и отдельный поток сравнения стоит добавлять только после профилирования живого приложения. Иначе легко получить сотни строк кода ради долей миллисекунды.

27 попыток оставили четыре финалиста

В прогоне CORAL участвовали два агента, а система сохраняла их попытки и заметки. После исходной версии в истории осталось 27 шагов: выборочные отпечатки, полный хеш, разные размеры SIMD-векторов, LRU на четыре и два кадра, неизменяемые снимки, изменённые области и отдельный поток сравнения. До финальной проверки дошли четыре реализации с разными компромиссами.

  • Exact locked buffer удерживает исходный CVPixelBuffer и сравнивает его с новым кадром. Он очень быстр, но может занять буферы ScreenCaptureKit.
  • Dirty region + locked buffer распознаёт только изменённую область. У него остаётся проблема с буферами, а время ROI ведёт себя нестабильно.
  • Exact immutable snapshot копирует пиксели в собственную память. Буфер захвата освобождается сразу, но появляются дополнительная копия и расход памяти.
  • Exact chunk worker хранит собственные снимки блоками по 128 строк и сравнивает их в двух потоках. Это самый сложный вариант: он добавляет 817 строк кода продукта.

В Exact chunk worker основной поток проверяет 75% блоков, фоновый — оставшиеся 25%. SIMD ускоряет сравнение, но не меняет гарантию: код всё равно проходит по каждому блоку. Перед освобождением память снимка обнуляется. Этот вариант победил среди решений, которые не удерживают буферы ScreenCaptureKit.

Первый результат был красивым — и почти бесполезным

Первый замер дал около 1673×. Такую цифру хочется сразу поставить в заголовок. Но тестовый кадр был размером 1200 × 240 — скорее длинный чек, чем рабочий стол. Я ей не поверил.

Точное сравнение дорожает почти пропорционально числу пикселей, а время Vision меняется гораздо слабее. Значит, ускорение зависит от размера входа сильнее, чем казалось по первому прогону. Я добавил 1080p, 1440p и 4K, а для каждой точки взял медиану трёх независимых запусков.

Для Exact chunk worker формула на 1440p выглядит так: 62,659 мс исходного OCR / 0,473 мс точного сравнения = 132×. Полная матрица показала, как быстро меняется ответ вместе с разрешением:

  • 1200 × 240: 0,021 мс против 39,882 мс у исходного OCR, ускорение 1881×;
  • 1920 × 1080: 0,225 мс против 63,351 мс, ускорение 282×;
  • 2560 × 1440: 0,473 мс против 62,659 мс, ускорение 132×;
  • 3840 × 2160: 1,005 мс против 66,480 мс, ускорение 66×.

Та же кривая появилась у всех точных реализаций. На 1440p они дали 127–149×, на 4K — 54–66×. Это не выброс отдельного кода, а цена чтения растущего массива пикселей.

Цель 100× честно выполняется до 2560 × 1440 включительно. Утверждение «OCR ускорен в 1881 раз» верно только для узкой фикстуры 1200 × 240 и ничего не обещает владельцу 4K-монитора.

Поэтому в заголовок попало 132×: это полноценный кадр 1440p и реализация, которая не удерживает буферы захвата. Рядом в статье остаётся 66× для 4K. Хорошая цифра должна пережить смену тестовой картинки.

График ускорения exact chunk worker: 1881× на 1200 × 240, 282× на Full HD, 132× на 1440p и 66× на 4K; цель 100× выполнена до 1440p
График ускорения exact chunk worker: 1881× на 1200 × 240, 282× на Full HD, 132× на 1440p и 66× на 4K; цель 100× выполнена до 1440p

Бенчмарк выбрал победителя. Архитектура его дисквалифицировала

Exact locked buffer показал отличное время и потребовал меньше всего изменений — 274 строки. Он не копирует пиксели, а сохраняет исходный CVPixelBuffer для будущего сравнения. В изолированном тесте это выглядит идеальным решением.

Но queueDepth у «SecureAI Screen» равен двум, и кеш тоже удерживает два кадра. Оптимизация способна занять весь доступный пул ScreenCaptureKit. Синтетический бенчмарк этого не видит: он сам создаёт буферы и не зависит от живого потока захвата. Быстрая функция могла оставить приложение без следующего кадра.

У locked-варианта даже пиковый RSS тестового набора оказался ниже: около 545 МБ против 620 МБ у снимков. Это была опасная подсказка. Общая память процесса ничего не говорит о том, кто владеет двумя дефицитными буферами захвата. Важной метрикой здесь оказался не RSS, а давление на очередь ScreenCaptureKit.

Поэтому лидер таблицы выбыл. Snapshot-варианты копируют пиксели в память приложения и сразу отпускают CVPixelBuffer. Exact chunk worker оказался быстрее обычного неизменяемого снимка: 132× против 127× на 1440p и 66× против 54× на 4K. Он стал безопасной основой для следующей итерации, хотя его сложность ещё предстояло сократить.

Сравнение exact locked buffer и immutable snapshot: разница 0,021 мс, но locked-вариант удерживает до двух буферов при queueDepth 2, поэтому для production выбран snapshot
Сравнение exact locked buffer и immutable snapshot: разница 0,021 мс, но locked-вариант удерживает до двух буферов при queueDepth 2, поэтому для production выбран snapshot

Изменённые области: 4× быстрее — или медленнее полного OCR

ScreenCaptureKit сообщает прямоугольники, которые изменились после предыдущего кадра. Идея привлекательная: сохранить старые строки вне этих областей, а Vision отправить только маленький фрагмент. На рабочем столе курсор, часы или уведомление часто меняют лишь несколько процентов изображения.

На небольшом изменении фона этот путь занял 15,725 мс вместо примерно 63 мс полного OCR — почти 4× быстрее. Но когда изменённый прямоугольник содержал текст, время выросло до 83,485 мс. Оптимизация стала медленнее исходного пути.

Причина — сумма мелких расходов. Область нужно проверить и расширить, сопоставить с прежними строками, выполнить новый Vision-запрос, затем собрать результат в прежнем порядке. На удачном фрагменте эта работа окупается. На насыщенном текстом ROI — нет.

Есть и условие корректности: непрерывная последовательность кадров. Если конвейер пропустил кадр или получил неверный baseSequence, прямоугольник изменений нельзя считать полным описанием разницы. В таком случае код возвращается к полному OCR. Этот резервный путь занял 60,426 мс и выдал новый корректный результат.

Я оставил обработку изменённых областей как дополнительную оптимизацию для доказанно удобных случаев. Основной путь строится на точном сравнении всего кадра: оно проще, предсказуемее и дало больший выигрыш без зависимости от содержимого ROI.

Как я проверял, что ускорение не ослабило защиту

Успешный кеш-хит — лёгкая проверка. Опасны ложные попадания: кадр изменился, а система вернула старый текст. В обычном приложении это испортило бы данные; здесь новая строка с ИНН могла остаться без маски.

Поэтому проверочная матрица была построена вокруг границы между «точно тот же кадр» и «хотя бы один байт другой»:

  • одинаковые пиксели в разных CVPixelBuffer должны дать попадание в кеш;
  • чередование двух кадров должно обслуживаться двумя элементами кеша;
  • один изменённый пиксель должен запустить полный OCR;
  • 256 случайных изменений тоже должны запустить OCR;
  • новый текст или другая контрольная сумма ИНН должны сбросить старый результат;
  • смена разрешения и очистка состояния должны исключить попадание;
  • разрыв последовательности обязан отключить обработку изменённой области.

Все четыре финальных варианта прошли эту матрицу и восемь штатных проверок SecureAIScreenChecks. После изменения одного пикселя время возвращалось к 64–67 мс: Vision действительно запускался заново. То же происходило после случайных изменений и замены текста. Системного замедления изменившихся кадров относительно исходной версии я не увидел.

Граница доказательства остаётся узкой: тесты подтверждают отсутствие известных пропусков в проверенных сценариях. Они не заменяют длительный прогон с настоящим ScreenCaptureKit и несколькими дисплеями.

Подойдёт ли этот подход вашему проекту

Точный кеш полезен, когда входы часто повторяются, а сравнение заметно дешевле основной модели. Грубую экономику можно оценить до разработки: выигрыш ≈ доля повторов × (стоимость OCR − стоимость сравнения) − копирование и память. Затем стоит ответить на шесть вопросов:

  • Какая доля входов повторяется в живом сценарии, а не в тестовой последовательности?
  • Сколько занимает точное сравнение на самом большом поддерживаемом входе?
  • Что означает равенство: видимые пиксели, полный stride, несколько плоскостей, метаданные?
  • Можно ли хранить исходный объект, не отбирая ресурс у поставщика данных?
  • Что происходит после одного изменённого байта, смены геометрии и сброса состояния?
  • Участвуют ли в бенчмарке настоящая очередь, давление на память и источник кадров?

Пока этих измерений нет, ускорена тестовая функция, а не продукт. Самая частая ошибка — оптимизировать среднюю задержку и не заметить, что очередь кадров, память или редкий промах стали хуже.

Что этот эксперимент изменил в моём подходе

Первым делом я теперь проверяю, появилась ли во входе новая информация. На рабочем столе экран подолгу не меняется, поэтому отказ от повторного вычисления даёт больше, чем микротюнинг самой модели.

Гарантия кеша должна быть не слабее гарантии основного алгоритма. Для приватности вероятностный отпечаток слишком рискован: одна пропущенная цифра отменяет смысл маскировки. Поэтому сравнение осталось точным.

Измерять нужно систему вокруг функции. Locked buffer победил по времени и объёму изменений, но queueDepth = 2 полностью изменил решение. В таблице задержек этого риска не было видно.

Размер входа входит в результат. Ускорение 1673× на полоске 1200 × 240 не переносится на 4K. После матрицы осталось менее эффектное, зато проверяемое обещание: 132× на 1440p и 66× на 4K для основы, которая не удерживает буферы захвата.

Что я внедрил бы первым

Финальный Exact chunk worker добавляет 817 строк в четыре файла продукта: собственное владение памятью, обнуление, SIMD, постоянный поток, LRU, обработку изменённых областей и контроль последовательности. Это хороший эксперимент и слишком большой первый патч. Чем шире оптимизация, тем труднее понять причину будущего сбоя.

Первую рабочую версию я намеренно сделал бы скучнее:

  1. Двухэлементный кеш неизменяемых снимков без обработки изменённых областей и постоянного фонового потока.
  2. Не меньше 30 минут живого захвата на 1080p, 1440p и 4K, включая переключение окон и несколько дисплеев.
  3. Метрики доставленных и пропущенных кадров, p50/p95 задержки, CPU, RSS и давления на очередь ScreenCaptureKit.
  4. SIMD, параллельное сравнение и изменённые области возвращать по одному — только если профиль показывает конкретное узкое место.

132× — хорошая цифра для заголовка. В продукт я понесу другое: точный кеш, который при любом сомнении возвращается к полному OCR, и бенчмарк, способный опозорить красивую цифру до релиза. Для системы защиты это полезнее рекорда.

Ваша реакция

Как вам разбор?