DueBuddy

Тень превращает 5 в 9: почему улучшение картинки ухудшает распознавание

Четыре сильные vision-модели одинаково ошиблись на одном счёте. Разбираем, почему консенсус моделей ничего не доказывает, почему апскейл и шумодав ухудшили точность с 86,4% до 79,3% и как вырезание фрагмента дало 7 верных ответов из 12 вместо одного.

У нас был счёт на 5 540 AED, который четыре разные модели прочитали как 5 940.

Не одна модель. Четыре. Gemini 2.5 Flash, GPT-4o, Claude Sonnet 4, Qwen2.5-VL 72B — разные архитектуры, разные обучающие данные, разные разработчики. Все четыре посмотрели на один и тот же файл и все четыре ошиблись одинаково. Настоящую сумму подтвердил владелец по бумажному оригиналу.

Причина оказалась не в моделях. Причина была в тени.

Как тень ломает цифру

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

Человек такое читает по контексту: «5 540» — правдоподобная сумма, «5 940» — тоже, но глаз цепляется за общую композицию строки. Модель контекста не имеет: она видит двадцать пикселей, из которых восемь попали в тень.

И вот главный вывод, который мы сделали не сразу:

согласие моделей не является доказательством.

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

Первая гипотеза: улучшить картинку

Логика была простой и, как выяснилось, неправильной: если модель плохо видит, надо сделать картинку лучше. Резче, чище, крупнее.

Мы собрали конвейер подготовки. Он делал ровно то, что советуют в туториалах по OCR:

  1. Выравнивал наклон.
  2. Переводил в оттенки серого.
  3. Поднимал контраст.
  4. Подавлял шум.
  5. Повышал резкость.
  6. Увеличивал мелкие снимки до 1400 пикселей по ширине.

Никакого OpenCV — на сервере его нет, и ставить ради этого не хотелось. Всё на numpy и Pillow, лапласиан считается вручную.

Потом мы замерили результат на своих документах.

до подготовки:      86,4% верных полей
после подготовки:   79,3% верных полей

Минус семь процентных пунктов. Мы старались — и стало хуже.

Почему стало хуже

Каждый шаг по отдельности выглядит разумно, а вместе они уничтожают именно то, что модели нужно.

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

Подавление шума съедает тонкие штрихи. Шум и мелкая деталь для фильтра выглядят одинаково: и то и другое — локальный перепад яркости. Фильтр не различает «грязь» и «тонкую линию в единицу пикселя». Вместе с шумом уходят диакритика, тонкие запятые в десятичных разделителях и те самые перекладины.

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

Контраст убивает бледное. Лёгкий серый штамп, водяной знак, бледная печать — всё, что и так на грани, после растягивания уровней либо исчезает, либо превращается в шум.

Отсюда правило, которое мы записали в код: хорошие сканы не трогаем вообще. Подготовка включается только по триггерам, а не «на всякий случай». Наклон больше 1,2 градуса — выравниваем. Ширина меньше 1400 пикселей — увеличиваем. Резкость (дисперсия лапласиана) ниже 90 — помечаем как размытое и предупреждаем пользователя. Всё остальное оставляем как есть.

Вторая гипотеза: не улучшать, а сузить

Настоящая проблема была не в качестве картинки, а в количестве выбора.

Когда модель читает целую страницу счёта, перед ней десятки чисел: количество, цена за единицу, сумма строки, подытог, ставка НДС, сумма налога, итог, аванс, остаток к оплате. Все они — числа. Все они в одном визуальном поле. Модель выбирает «самое похожее на итог» — и ошибается не потому, что не видит, а потому, что видит слишком много.

Мы поменяли подход. Вместо того чтобы улучшать страницу, мы вырезаем область вокруг слова-итога и показываем модели только её. Никаких других чисел рядом — только та строка, где написано TOTAL, Grand Total, Итого к оплате, Suma de plătit.

Где искать это слово: если в PDF есть текстовый слой, координаты слова берутся точно, из самого файла. Если слоя нет — берём нижнюю треть страницы, потому что итог почти всегда там.

И замер:

целая страница:            1 верный ответ из 12
вырезанный фрагмент:       7 верных из 12

Не «немного лучше». В семь раз.

Почему фрагмент работает

Три причины, и ни одна из них не про качество изображения.

Меньше кандидатов. Модели не нужно решать, какое из двадцати чисел является итогом. В кадре одно-два числа, и выбор почти очевиден.

Локальные правки вместо глобальных. Если на фрагменте тень — её можно осветлить именно там, не трогая остальную страницу. Это ровно то, что сломало нам первую гипотезу: мы применяли глобальные фильтры к документам, где 90% площади были в порядке.

Формулировка задачи становится однозначной. Мы не просим «прочитай счёт». Мы просим: «вот фрагмент, верни сумму к оплате, игнорируй цены за единицу, подытоги, НДС и числа из условий оплаты». Меньше свободы — меньше ошибок.

Что фрагмент не решает

Честно про ограничения, потому что без них разбор бесполезен.

Потеря десятичного разделителя. На другом счёте модель прочитала 101.40 как 10 140. Цифры верные, разделитель исчез. Никакое вырезание тут не поможет — нужна отдельная проверка масштаба: если сумма отличается от суммы позиций примерно в десять или сто раз, показываем предупреждение.

Арифметика важнее зрения. Мы перестали верить моделям в том, что можно посчитать. Проверки детерминированные: количество × цена = сумма строки; подытог + НДС = итог; сверка с суммой прописью; сверка с фискальным QR, если он есть. Модель читает, код считает.

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

Выборка маленькая. Двенадцать счетов — это не исследование, это замер на своих документах. Мы не публикуем «точность 92%», потому что для такого утверждения нужна выборка на порядки больше и разметка чужими руками.

Правила, к которым мы пришли

Если коротко, вот весь вывод в шести пунктах:

  1. Не улучшайте изображение без причины. Апскейл, шумодав и резкость часто делают хуже — у нас точность упала с 86,4% до 79,3%.
  2. Сужайте область, а не повышайте качество. Фрагмент вокруг ключевого слова дал 7 верных из 12 против 1 из 12 на целой странице.
  3. Применяйте обработку локально. Тени и контраст — на вырезанном фрагменте, не на всей странице.
  4. Не верьте консенсусу моделей. Четыре модели могут ошибиться одинаково, если ошибка в пикселе.
  5. Считайте кодом, а не моделью. Арифметика, сумма прописью, QR — детерминированные проверки.
  6. Показывайте неуверенность. Если снимок размытый или есть расхождение — пишите «проверьте сумму», а не выдавайте число как факт.

Вывод

Начиналось всё с желания «сделать картинку получше». Закончилось пониманием, что задача была не про картинку, а про неопределённость: чем меньше вариантов видит модель, тем меньше она ошибается.

Иногда лучший способ улучшить распознавание — не трогать изображение и убрать из кадра всё лишнее.

DueBuddyAll articles DueBuddy