Три разных номера из одной книги — и все проходят контрольную цифру

от автора

У EAN-13 есть контрольная цифра. Тринадцатая цифра пересчитывается по остальным двенадцати, и если номер прочитан с ошибкой — проверка не сойдётся. Так написано в любом объяснении стандарта, и это правда. Проблема в том, что из этой правды обычно делают неверный вывод: «раз контрольная сошлась, номер прочитан верно».

Не сошлась. Точнее — сошлась, но это ничего не доказывает.

Как это выглядит на практике

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

878392246704697852224670463705912467046

Верный — средний, это ISBN. Два других — ошибочные прочтения смазанного кадра. Первая мысль: сейчас отсеем их контрольной цифрой. Считаем по правилу GS1 (веса 1 и 3, начиная слева):

const checkDigit = (body) => {  const sum = [...body].map(Number)    .reduce((total, digit, index) => total + digit * (index % 2 ? 3 : 1), 0);  return (10 - (sum % 10)) % 10;};checkDigit("978522246704"); // 6 — совпадает с последней цифройcheckDigit("878392246704"); // 6 — тоже совпадаетcheckDigit("370591246704"); // 6 — и это тоже

Все три номера контрольную цифру проходят.

Почему так получается

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

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

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

Почему у QR такой беды нет

Стоит сравнить с двумерными кодами. В QR внутри код Рида — Соломона: избыточность занимает от 7 до 30 процентов ёмкости, в зависимости от уровня коррекции. Искажённый кадр либо восстанавливается полностью, либо не декодируется вовсе. Промежуточного состояния «прочиталось, но не то» практически не бывает — чтобы обмануть Рида — Соломона, нужно совпадение, вероятность которого исчезающе мала.

Отсюда практический вывод, который стоит держать в голове при работе с камерой:

  • двумерному коду можно верить с первого чтения;

  • линейному коду с первого чтения верить нельзя.

Что с этим делать

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

Значит, достаточно накапливать голоса и показывать номер только после нескольких одинаковых чтений:

const LINEAR_CONFIRMATIONS = 3;export function tallyConfirmation(tally, value, { needed = LINEAR_CONFIRMATIONS, now = 0, forgetAfterMs = 4000 } = {}) {  // Накопитель забывается, если код надолго ушёл из кадра, — иначе  // следующий товар донашивал бы подтверждения предыдущего.  const fresh = tally && now - tally.at <= forgetAfterMs    ? { counts: { ...tally.counts }, at: now }    : { counts: {}, at: now };  const count = (fresh.counts[value] || 0) + 1;  fresh.counts[value] = count;  return { tally: fresh, count, needed, confirmed: count >= needed };}

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

Тест на реальных данных выглядит так — и это, пожалуй, самый честный тест из всех, что мы написали:

// Настоящая череда прочтений с той самой книгиconst sequence = ["8783922467046", "3705912467046", "9785222467046",                  "9785222467046", "9785222467046"];// В списке результатов остаётся ровно один номер — верныйassert.deepEqual(shown.values, ["9785222467046"]);

Заодно про разрешение

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

Замер показал точную причину. Распознавателю нужно, чтобы код занимал не меньше примерно 110 точек в разбираемом кадре. А мы ужимали кадр камеры до 800 точек по длинной стороне ради экономии батареи. Код маркировки печатают мелким — миллиметров десять на упаковке, — и после ужатия ему доставалось около девяноста точек. Ниже порога. Сколько ни держи телефон, результата быть не могло.

Порог подняли до 1000, и код стал читаться. Мораль простая: прежде чем оптимизировать разрешение, стоит выяснить, с каким минимальным размером объекта работает распознаватель. Мы этого не сделали и потеряли на этом несколько дней.

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

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

Сделали иначе: взяли открытую выгрузку Open Food Facts (ODbL), отобрали российские товары, отбросили записи с неверной контрольной цифрой — таких в исходной базе оказалось 13,5 процента — и нарезали остаток на файлы по первым четырём цифрам номера. Получилось 32 772 товара в 1387 файлах, средний файл — 1,4 КБ.

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

Побочный эффект оказался интереснее основного. Первые семь цифр штрихкода — номер предприятия, который GS1 выдаёт компании один раз, и все её товары начинаются с него. В нашей выборке 32 772 товара распределились всего по 5 924 таким номерам, причём 305 номеров покрывают половину товаров. То есть знание про один товар автоматически распространяется на весь ассортимент производителя — и триста подсказок закрывают половину полки в магазине.

Откуда цифры

Все числа в тексте — результат замеров, а не оценка по памяти. Вот чем проверялось каждое:

Утверждение

Как проверено

Три номера проходят контрольную цифру

Пересчёт по правилу GS1 для каждого: 9785222467046, 8783922467046, 3705912467046 — у всех сходится

13,5 % записей в выгрузке с неверной контрольной

Фильтр отсеял 5 120 записей из 37 892

32 772 товара, 1387 файлов, средний 1,4 КБ

Итог сборки справочника

5 924 номера предприятий, 305 покрывают половину

Группировка товаров по первым семи цифрам

Порог распознавания около 110 точек

Замер на нарисованном Data Matrix: 80 точек — не читается, 110 — читается

Разбор восемь кадров в секунду

Дросселирование цикла: 120 мс между попытками

Уровни коррекции QR (7–30 % избыточности) — из стандарта ISO/IEC 18004, это общеизвестная величина, а не наш замер.

Итог

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

Сканер, о котором речь, работает бесплатно и без регистрации: vitrinaqr.ru/qr-kody/skaner-qr-onlayn/. Исходные наблюдения, цифры и тесты — оттуда же.

ссылка на оригинал статьи https://habr.com/ru/articles/1067472/