← Все статьи

Пустая клетка, повтор и зачёркивание: распознаватель не должен решать, есть ли замер

Продолжение пилота УЗТ: пять видов клетки, чернила, повтор вверх по колонке, зачёркивание и почему эвристика ещё не заменяет крошечный классификатор.

Пустая клетка, повтор и зачёркивание: распознаватель не должен решать, есть ли замер
Содержание

В заметке про компактную CRNN мы оставили открытым конфликт: одна голова не должна одновременно отвечать «есть ли число» и «какое именно». На пустых клетках распознаватель рисует правдоподобную толщину. Здесь — продолжение из того же пилота ai-vision: что уже стоит перед оптическим распознаванием символов (OCR) в конвейере компьютерного зрения, какие виды клетки мы различаем и почему крошечный классификатор «пусто / есть значение» всё ещё впереди, хотя эвристика уже работает.

Это не готовый гайд и не обещание, что правила закрыли задачу. Проект в разработке; репозиторий закрытый — контур без сырых бланков на странице портфолио. Системный слой листа (модель зрения, оценка надёжности, отчёт оператора) — в «почему одной модели мало».

Цена ошибки та же, что на бланке ультразвукового контроля толщины (УЗТ) вообще: пустая клетка, превращённая в 8,0, выглядит как уверенный замер и может уехать в учётную систему предприятия (ERP), если оператор не заметит серую подсветку. Перепутанный повтор стоит рядом: колонка «как сверху» превращается в минус или в единицу, и дальше по строкам едет уже чужая толщина.

Ключевые выводы

«Есть ли замер?» — не задача распознавания. Сначала вид клетки, потом чтение. Иначе сеть обязана что-то сказать даже на сетке без чернил.

В пилоте пять видов: empty, ditto, digit, text, crossed. Это контракт между Python-контуром зрения и TypeScript-сборкой результата, не «ещё пять моделей».

Чернила ищем как синюю ручку и тёмный карандаш, с отсечением тонких линий сетки. Порог доли пикселей маленький: типичная клетка почти пустая, чернила — редкие штрихи.

Повтор (горизонтальный прочерк) не читаем распознавателем. Берём значение вверх по той же колонке. Если источника нет — статус проверки, не выдуманная толщина.

Зачёркивание — отдельный вид: сначала снять удар, потом пытаться прочитать последнее валидное число, иначе клетка остаётся зачёркнутой, а не «ещё одним замером».

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

После чтения нужны детерминированные щиты: нереалистичная толщина выше порога (у нас 80 мм) сбрасывается; узкая колонка без чернил не должна держать галлюцинацию модели.

Почему распознаватель не должен решать, пусто ли поле

Распознаватель обучен выдавать строку. На входе — картинка. На выходе — символы алфавита. Если картинка — только линия таблицы, сеть не умеет «промолчать», пока вы явно не научили её пустому классу. В прошлой заметке дообучение на пустых снижало фантомы и било по заполненным. Вывод инженерный: разведите задачи.

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

В конвейере это выглядит так.

flowchart TB
  crop[Cell_crop]
  crop --> ink[Ink_and_kind]
  ink -->|empty| skip[Skip_OCR]
  ink -->|ditto| up[Resolve_up_column]
  ink -->|crossed| strike[Mask_strike_then_OCR]
  ink -->|digit_or_text| ocr[Handwriting_OCR]
  ocr --> parse[Parse_and_range]
  up --> parse
  strike --> parse
  parse --> field[Structured_field]

Пока кроп не классифицирован, любой движок — и PaddleOCR, и компактная CRNN — кандидат написать толщину из сетки. Классификация дешевле ошибки. Облачная модель зрения на фрагменте здесь не исключение: без вида клетки она тоже обязана «прочитать» пустой прямоугольник.

Пять видов клетки как контракт

Тип клетки в контуре зрения задаётся явно: empty | ditto | digit | text | crossed. Сборка результата на TypeScript читает этот вид, а не пытается заново угадать геометрию по сырому тексту.

Вид Что видим на бланке Что делаем
empty нет чернил, или ниже порога не зовём рукописное распознавание, значение null
ditto горизонтальный повтор «как сверху» распознавание выключаем, ищем источник вверх по колонке
digit похоже на толщину читаем, нормализуем запятую, проверяем диапазон
text «нет элемента», «обогрев», короткая помета не пихаем в замерную CRNN с алфавитом из цифр
crossed удар поверх записи снимаем линию, читаем оставшееся или оставляем зачёркнутым

Это ровно то разделение «по типу визуальной задачи», которое в прошлой статье было таблицей намерений. Здесь оно уже в коде: классификатор содержимого клетки плюс разбор повтора в колонке.

Текст в замере — не баг бланка. Операторы пишут короткие пометы. Парсер измеряет не только 12,3, но и словарные ярлыки вроде «нет элемента» и «обогрев», и отсекает римские номера секций, которые распознаватель иногда приклеивает к числу. Это правила, не вторая нейросеть. Вид text как раз говорит сборке: не пытайся втиснуть строку в миллиметры любой ценой.

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

Чернила: синяя ручка, карандаш, сетка

Детектор чернил смотрит пространство HSV на синий диапазон ручки и серую яркость на тёмный карандаш. Тонкие горизонтали и вертикали сетки пытаемся не считать записью: морфологическое раскрытие эллипсом. Доля «чернильных» пикселей сравнивается с порогом порядка 0,8% кадра клетки — для рукописи этого достаточно, для жирной сетки порог легко превысить, если не вычесть линии.

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

Порог — компромисс. Слишком высокий: бледный карандаш станет empty, замер пропадёт, контур проверки потом будет дочитывать по hasInk=true и пустому значению. Слишком низкий: сетка станет «есть чернила», распознаватель нарисует 11,1. В пилоте мы сознательно держим порог низким и чистим вид вторым проходом: повтор, зачёркивание, текст.

Телефон и планшетный сканер дают разный контраст. Синяя ручка на желтоватой бумаге и карандаш на серой ксерокопии — не один и тот же HSV. Поэтому порог не «магическая константа продукта», а настройка под плёнку и свет. Менять его без подсчёта пропусков заполненных клеток — способ тихо потерять тонкий 8,2.

Повтор вверх по колонке

Горизонтальный прочерк на бланке УЗТ означает «как в строке выше в этой точке». Это не минус толщины и не «нет данных». Если отдать прочерк в распознаватель, получите -, 1 или случайный хвост цифры.

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

Цепочка повторов нормальна: три прочерка подряд должны все сослаться на одну исходную толщину, а не друг на друга как на пустоту. Поэтому обход сверху вниз, а не «взять соседа и остановиться». Если исходная клетка сама в review, повтор наследует сомнение: копировать уверенный ok из сомнительного источника нельзя.

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

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

Зачёркивание: сначала снять удар

Зачёркнутая клетка — частый полевой жест: написали, передумали, ударили линию, иногда написали новое число ниже. Если кормить кроп распознавателю как есть, сеть читает кашу из старого числа и удара.

Детектор ищет сильный или слабый сигнал удара на маске чернил: вытянутое пятно, горизонтальная морфология, иногда прямые от преобразования Хафа. Если удар есть, пробуем закрасить его белым и прочитать оставшееся; из кандидатов предпочитаем нижнюю валидную толщину — как правило, это исправление, а не зачёркнутое старое. Не вышло — вид остаётся crossed, значение может уйти в проверку, а не в ok.

Это хрупко. Слабый удар на тонкой девятке выглядит как сетка; жирный удар съедает полезные штрихи. Поэтому зачёркивание не сливается ни с пустым, ни с повтором: оператору в HTML-отчёте нужна другая подсветка, чем «просто нет числа».

Не каждый удар значит «прочитай нижнее». В части регламентов зачёркнутое поле считается пустым независимо от того, что ещё видно. Тогда контур должен остановиться на виде crossed и не звать распознаватель: иначе вы «восстановите» отменённый замер и отправите его в учёт как живой. В пилоте мы пробуем нижнее число, потому что на наших бланках так чаще правят толщину. Это доменное правило, его надо явно записать, а не прятать в пороге пятен.

Когда рукописное распознавание не вызываем

На сетке замеров вызов распознавателя включается только если есть чернила и предварительный вид не повтор. Пустое — запись кропа и cellKind=empty. Повтор — кроп есть, распознавания нет. Зачёркивание — отдельная ветка чтения. Иначе — ocr_handwriting_cell: по умолчанию печатный/рукописный каскад, по флагу — компактная CRNN.

Это экономит время и, главное, не даёт пустой клетке шанса стать толщиной. Второй щит стоит уже после моделей: если значение в узкой колонке есть, а контур зрения не видит чернил, ячейка очищается — типичная галлюцинация «сеточной» модели на пустой IV.а. Третий щит — потолок толщины 80 мм: слив соседних цифр (96 вместо пустого) не проходит доменную проверку.

Узкая колонка IV.а в пилоте чаще пустая и легче кормит фантомы: рамка занимает большую долю кадра, рукопись, если она есть, теснее. Отдельная сеть на эту колонку не нужна — нужна более жёсткая связка «нет чернил → сбросить значение». Если ослабить щит «чтобы не потерять редкий замер», вы вернёте массовые 11,1 на пустых.

Печатные служебные поля (зона, диаметр, тип) идут другим распознавателем с обрезкой рамок сетки и увеличением. Дату шапки не суём в MeasurementCrnn: день, месяц, год проверяет обычный код. Замерная голова с алфавитом из десяти цифр и запятой не должна «чуть-чуть» стать универсальным читателем бланка — это уже было тезисом предыдущей заметки; здесь он закреплён развилкой в run_cv.

Почему эвристика ещё не конец

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

Следующий шаг из плана — крошечный классификатор «пусто / есть значение» (и, логично, расширенный набор видов) перед замерной головой. Эвристика остаётся как слабый фильтр и как признаки: доля чернил, отношение сторон пятна, факт удара. Классификатор учится на кропах, которые эвристика систематически путает. Пока его нет, честно держим хвост в статусе review и не повышаем порог ok на сомнительных десятых — это уже дисциплина системной заметки.

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

Не делаем отдельную сеть на каждую колонку. Виды общие для всей сетки I–IV. Меняется только геометрия колонки IV.а (она уже, чаще пустая) — отсюда отдельный щит «нет чернил → сбросить значение», а не вторая модель.

Типичные ошибки

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

Вторая — смешать повтор и пустое в одном статусе. Оператор тогда не отличает «здесь не мерили» от «здесь стоит как строкой выше, но источник потерялся».

Третья — поднять порог чернил после пачки фантомов и не посчитать, сколько заполненных клеток ушло в empty. Вентиль, который «лечит» пустые, легко душит бледный почерк.

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

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

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

Что сделать сегодня

Если вы режете бланк на клетки и сразу зовёте распознаватель — остановите вызов на пустых.

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

Посчитайте отдельно: сколько пустых получили ложную толщину; сколько заполненных потеряли из-за высокого порога чернил; сколько повторов уехали в - или 1.

Не дообучайте замерную голову на пустых «до победы». Сначала вентиль, потом узкое распознавание, потом человек на review.

Частые вопросы

Разве нельзя просто добавить класс «пусто» в CRNN?

Можно, и мы пробовали смешивать отрицательные примеры. Фантомы падают, цифры тоже. Класс «пусто» внутри головы с временным выравниванием (CTC) — компромисс в тех же весах, которые должны читать тонкий хвост девятки. Вентиль дешевле.

Чем повтор отличается от прочерка в служебных полях?

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

Зачёркивание всегда надо читать?

Нет. Иногда удар значит «игнорировать клетку». Мы пытаемся прочитать нижнее валидное число, потому что на наших бланках так чаще исправляют замер. Если в вашем регламенте зачёркнутое всегда пусто — оставьте вид crossed и не зовите распознаватель.

Нужна ли нейросеть, чтобы найти чернила?

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

Как это стыкуется с облачной моделью зрения?

Контур зрения отдаёт вид и кроп. Облачный проход не должен затирать пустое уверенной галлюцинацией: щит «нет чернил → сбросить значение» как раз про это. Низкая уверенность и hasInk=true при пустом значении — повод для проверки человеком, не для молчаливого ok.

Что почитать дальше

Замерная голова, 640 тысяч параметров и конфликт пустых внутри CTCкомпактная замерная сеть. Контур всего бланка и отчёт оператора — одной модели мало. Механика последовательностей — от свёртки до трансформера. Набор как продукт — инженерия датасетов. Цена пропуска — экономика сбоя. Цикл «один рычаг за прогон» — улучшение качества головы.

Заключение

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

Открыто то, что эвристика не умеет: уверенные ошибки на жирной сетке и бледном почерке. Туда пойдёт крошечный классификатор, а не вторая универсальная модель. Практический шаг на этой неделе — посчитать фантомы пустых до и после вентиля hasInk / cellKind. Если разница маленькая, вы всё ещё кормите распознаватель сеткой.