Я распознал паспорт. Чем я могу это доказать?. llm.. llm. MRZ.. llm. MRZ. ocr.. llm. MRZ. ocr. ollama.. llm. MRZ. ocr. ollama. python.. llm. MRZ. ocr. ollama. python. Tesseract.. llm. MRZ. ocr. ollama. python. Tesseract. Машинное обучение.. llm. MRZ. ocr. ollama. python. Tesseract. Машинное обучение. Обработка изображений.. llm. MRZ. ocr. ollama. python. Tesseract. Машинное обучение. Обработка изображений. распознавание документов.

Система работает и разбирает документы. Претензия у меня к ней одна, зато неустранимая изнутри: я не могу доказать ни одно из полученных значений.

Ниже — опись. По каждому полю: как оно читается, чем проверяется и чего эта проверка стоит. Последняя строка описи самая интересная, потому что напротив неё не стоит ничего.

Что за задача

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

Ограничение, которое определило всё остальное: паспорта наружу не уходят. Чужие персональные данные прислали не для того, чтобы я раздавал их по чужим серверам. Поэтому никаких облачных распознавалок — всё считается локально, Ollama и gemma3:12b на машине в локальной сети, температура ноль, ответ строгим JSON.

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

for k in range(4):
    rotated = np.rot90(in_img, k=k)
    faces = faceCascade.detectMultiScale(cv2.cvtColor(rotated, cv2.COLOR_BGR2GRAY),
                                         scaleFactor=1.1, minNeighbors=5,
                                         minSize=(40, 40))
    for (x, y, w, h) in faces:
        aspect = h / float(w)
        portraitPenalty = 1.0 if aspect >= 1.0 else 0.3
        score = w * h * portraitPenalty

Сразу скажу, чего этот конвейер не делает: перспективу он не выпрямляет. Снятый под углом паспорт таким и остаётся.

Поле первое: серия и номер

Серия и номер напечатаны вертикально вдоль правого поля страницы. Вырезаю правую полосу в двадцать процентов ширины, поворачиваю на девяносто градусов, автоконтраст, нерезкая маска, увеличение в пять раз, PNG — чтобы не добавлять к своим бедам ещё и артефакты JPEG. Полученную картинку показываю модели.

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

if f'{series}{number}' == '0101123456':
    series, number = None, None

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

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

DIGITS_ONLY_CONFIG = r"--psm 7 -c tessedit_char_whitelist=0123456789"

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

Итог по полю: проверка есть, но ловит она только грубое. Формат «четыре плюс шесть» пропустит любую неверную цифру, если их всё равно десять.

Поле второе: дата рождения

Читается вместе с фамилией и именем, одним запросом по всему развороту. Требование к формату записано в промпте: строго «ДД.ММ.ГГГГ», иначе поле пустое. На выходе сервиса стоит валидатор, который это же и проверяет.

Проверяется, таким образом, форма записи. Значение не проверяется ничем.

А значение модель искажает предсказуемым образом: путает шестёрку с восьмёркой, восьмёрку с девяткой. Это обычная беда распознавания, тут у меня претензий нет. Претензия в том, что «06.08.1970» пройдёт мой валидатор ровно так же гладко, как «08.06.1970», и обе даты выглядят одинаково правдоподобно. Человек, глядя на результат, тоже ничего не заподозрит.

Поле третье: фамилия и имя

Проверить нечем. Совсем.

Нет ни формата, ни диапазона, ни справочника, с которым имело бы смысл сверяться. Что модель написала, то и уходит дальше.

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

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

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

Строка четвёртая: сам документ

Теперь то, из-за чего я и считаю всю работу незаконченной.

Прежде чем извлекать поля, надо убедиться, что перед нами паспорт. У меня это отдельный запрос — к той же самой модели: «это разворот российского паспорта, да или нет».

Сторож сделан из того же материала, что и подозреваемый.

Появился он не от хорошей жизни. Среди снимков попался акт приёма-передачи — обычная бумага с подписями, никакого паспорта в кадре. Модель посмотрела на неё и уверенно вернула серию и номер. Четыре цифры, шесть цифр, всё как положено. Их там не было: я дал ей задание найти серию и номер, а она такая программа, которая задание выполняет. Всегда. Из чего угодно.

Отсюда третье возражение — про коммерческие движки, специально написанные под российские паспорта. Знаю, что они есть. Вопрос в цене и в том, что я не видел ни одного честного отчёта от человека, который поставил такой движок на поток телефонных снимков из мессенджера. И вопрос доказательства это всё равно не снимает: чужой закрытый движок отвечает «это паспорт» с тем же основанием, что и моя модель, — со своим словом.

Итог по строке: проверки нет. Есть мнение программы.

Восемьдесят процентов, которые ничего не значат

Со всеми описанными подпорками система разбирает верно примерно восемь документов из десяти.

Звучит прилично. На деле это ноль.

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

А что было бы полезно? Шестьдесят процентов. Но с пометкой: вот эти шестьдесят прочитаны верно, гарантированно, можно не смотреть; остальные сорок открой сам. Это шестьдесят процентов сэкономленного времени вместо нуля.

Не хватает не точности. Не хватает доказуемости.

Чем на самом деле обеспечены мои гарантии

Вот как выглядит место, где в этом проекте живёт надёжность. Кусок рабочего промпта, не сокращал, только перевёл:

Твоя задача — прочитать ВСЕ цифры в этой строке ровно так, как они напечатаны.
Серия из четырёх цифр — это просто ПЕРВЫЕ ЧЕТЫРЕ цифры по порядку чтения.

НЕ ПРИМЕНЯЙ никаких закономерностей вроде повторения первых двух цифр.
Например, когда ты видишь «27 08 123456»:
  - ВЕРНО:    "Series": "2708"
  - НЕВЕРНО:  "Series": "2727"
  - НЕВЕРНО:  "Series": "2808"

НИКОГДА не выдумывай и не исправляй данные, опираясь на частотные русские
имена или типичные даты. Если что-то неразборчиво — считай это нечитаемым.

Гораздо ЛУЧШЕ вернуть None, если ты не уверен, чем вернуть неправильное
значение.

Разбор с «ВЕРНО» и «НЕВЕРНО» появился после того, как модель устойчиво выдавала серию «2727» там, где напечатано «27 08»: видела первую пару и дорисовывала вторую по образцу. Запрет исправлять по частотным именам — после подмены фамилий. Последняя строка повторена в промпте дважды, разными словами.

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

Теперь сравните со второй половиной проекта — той, где нет ни одной фразы человеческими словами. Каскад Хаара либо находит лицо, либо не находит, но не сочиняет его, чтобы меня не расстраивать. Обрезка по границам не выдумывает документ на пустом столе. Эти куски ошибаются, и чаще, чем мне бы хотелось, но ошибаются они честно: не справился — видно, что не справился.

Уговаривать в этом проекте приходится ровно одну вещь. Она же единственная, чей ответ я не могу проверить.

Что можно подпереть счётом

Одну строку описи закрыть всё-таки можно, и я до этого дошёл поздно.

С первого июля 2011 года в российском паспорте есть машиночитаемая запись — две строки по сорок четыре знака внизу страницы с фотографией. Введена постановлением правительства № 424, правила формирования сейчас живут в приложении 12 к приказу МВД № 186.

Ценно там не то, что запись машиночитаемая. Ценно, что в нижней строке пять контрольных цифр: позиции 10, 20, 28, 43 и 44, алгоритм «модуль 10» с повторяющейся весовой функцией 7-3-1.

Дальше арифметика. Веса 7, 3 и 1 взаимно просты с десяткой — значит подмена любой одной цифры на любую другую всегда меняет контрольную сумму. Не «с высокой вероятностью», а всегда. Та самая путаница шестёрки с восьмёркой, ради которой написаны уговоры в промпте, ловится гарантированно.

Пределы у этого пути честные. Фамилии и имени кириллицей в записи нет — там своя перекодировка, причём не такая, как в загранпаспорте: во внутреннем паспорте кириллица кодируется особой таблицей, где, например, буква «Е» превращается в цифру 2, поэтому готовые библиотеки под международный стандарт разбирают имя неверно. Перестановка двух соседних цифр контрольной суммой ловится не всегда, в отличие от одиночной ошибки. Паспорта старше 2011 года записи не имеют вовсе. И полоса должна попасть в кадр.

И то, на чём я застрял

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

Проблема в обратном случае.

Контрольная сумма различает «сошлось» и «не сошлось». Она не различает «это не паспорт» и «это паспорт, снятый криво». А для оператора это два разных исхода: в первом документ отклоняют, во втором просят человека переснять. Одна и та же несошедшаяся сумма означает и то и другое.

И спросить не у кого. Модель на вопрос «паспорт ли это» отвечает всегда и всегда уверенно — я показал выше, чего стоит её уверенность. Арифметика отвечает только там, где данные уже прочитаны.

Чем в такой ситуации доказывают, что перед тобой документ нужного типа, если прочитать его не удалось?

Коллеги, поделитесь опытом, кто уже решал такую задачу

Я уже думаю вернуться к геометрическим преобразованиям, чтобы опять прибегнуть к Tesseract, но это уже от бесысходности, что ли)))

Автор: Timur555

Источник