Не так давно у меня возникла необходимость в сканере штрихкода. Конечно можно было бы взять готовый сканнер откуда-нибудь из интернета, НО зачем? Зачем если можем написать сами? Именно с такими мыслями я сел и написал собственный сканер штрихкода. Правда сканирует пока что только из изображений, но это исправимо.
Что же такое штрихкод
Штрихово́й код — графическая информация, наносимая на поверхность, маркировку или упаковку изделий, предоставляющая возможность считывания её техническими средствами — последовательность чёрных и белых полос, либо других геометрических фигур.
Такое определение штрихкода даётся на википедии. Штрихкоды мы можем найти на практически всех современных товарах. Они удобны в использовании. Существует несколько разновидностей штрихкодов EAN-8 EAN-13 UPC code56... Я же написал сканнер только для EAN-8 и EAN-13.
Непосредственно код
Ну что ж, перейдём от слов к делу... У нас есть два штрихкода в формате png:
Изображения


Я сгенерировал их на первых в результатах поиска онлайн генераторах штрихкодов
Я прочитал эти изображения тем, что было под рукой - библиотекой OpenCV. Возможно это излишество как считаете?
cv::Mat image = cv::imread((const char*)source, cv::IMREAD_GRAYSCALE);
Изображение считывается чёрно-белым и записывается в матрицу cv::Mat. Далее необходимо вырезать из матрицы по центральной высоте полоску длиной в ширину изображения и высотой в 1 пиксель ( widthx1):
cv::Mat formatted(image, // image - наша первоначальная матрица изображения cv::Rect(0, (unsigned int)(image.rows / 2), image.cols, 1) // // прямоугольник необходимых размеров и с необходимой позицией );
Теперь переведём матрицу в удобный формат массива байт-пикселей (1байт=1пиксель) std::vector:
std::vector<unsigned char> fByteArray; // Наш итоговый массив значений пикселей cv::Point point(0, 0); // точка-позиция в полосочке for (int col = 0; col < formatted.cols; col++) { point.x = col; unsigned char el = formatted.at<unsigned char>(point); // получаем значение пикселя в позиции point el = 255 - el; // инвертируем if (el >= 255 / 2) { // проверяем пиксель на чёрный/белый - 1/0 в итоговом массиве fByteArray.push_back(1); // пиксель чёрный - добавим 1 }else fByteArray.push_back(0); // пиксель белый - доба��им 0 }
Здесь мы получаем значение пикселя по позиции point из отформатированной матрицы, инвертируем его (для удобства и понятности) и приравниваем в fByteArray всем белым пикселям значение 0, а всем чёрным значение 1.
Для того чтобы понять какая точка (пиксель) белая, а какая чёрная мы сравниваем значение этой точки со средним значением в цветовой палитре (серым) - 255/2. Если значение точки больше среднего, то точка белая (1), если меньше - чёрная (0)
Для чего мы инвертируем значение пикселя
Дело в том, что в компьютере цвета кодируются в порядке от чёрного (значение цвета = 0) к белому (значение цвета = 255). Условно можно обозначить чёрный, как 0, а белый как 1. В штрихкоде же наоборот: белые полосочки - это нули, а чёрные - это единички. И именно поэтому для того, чтобы не запутаться лучше пиксель инвертировать.
Освобождаем матрицы и на всякий случай проверяем не пустой ли у нас вектор fByteArray:
image.release(); formatted.release(); if (fByteArray.empty()) return false;
fByteArray представляет из себя массив нулей и единиц соответственно пикселям матрицы formatted. Мы должны измерить ширину каждой полоски в штрихкоде.
Для этого мы сохраняем первый элемент массива в переменную lastStat.
В count сохраняем количество последовательных элементов с одинаковыми значениям в массиве.
В цикле проходимся по элементам массива, при этом вне зависимости от значения первого и последнего потоков подобных элементов их пропускаем (он всё равно не несут смысловой нагрузки). Далее начинаем подсчитывать количество подобных элементов записываем полученное число count в массив arrays. Изменяем lastStat на значение нового отличного элемента в массиве и приравниваем count единице.
unsigned char lastStat = fByteArray[0]; // сохраняем значение первого элемента unsigned int count = 0; // количество одинаковых элементов std::vector<unsigned int> arrays; // массив "потоков" одинаковых значений for (unsigned char el : fByteArray) { // объяснено выше и ниже if (lastStat != (bool)el) { if (count != 0) arrays.push_back(count); lastStat = (bool)el; count = 1; continue; } if(count != 0) count++; }
Для тех кто не понял или понял плохо
Представим у нас есть массив значений:[0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0]
Перед циклом lastStat приравнивается первому элементу массива, в нашем случае: lastStat = 0
Первые подобные элементы пропускаются и не входят в arrays, но как только мы доходим до первого отличного от прошлого элемента (позиция 4 в массиве | 1 |), программа начинает считать количество подобных элементов и так, как первый подобный уже в el, то count = 1. Программа натыкается на следующий отличный элемент и count добавляется в arrays.В нашем случае count = 3, и так далее программа работает до последнего элемента fByteArray.
В результате цикла получается массив std::vector arrays, со следующими значениями: [3, 4, 6]
Надеюсь вы поняли, объяснил как смог:)
Освобождаем память и проверяем итоговый массив arrays:
fByteArray.clear(); if (arrays.empty()) { return false; }
Теперь необходимо найти чему равна ширина одного элемента в штрихк��де. Как же это найти?
Дело в том, что ширина одного элемента штрихкода (нуля или единицы) зашифровано в самом же штрихкоде - первые три полоски, срединные три полоски (они служат ещё для одной цели, но об этом позже) и последние три полоски. Все они идут чередуясь чёрная, белая, чёрная и ширина одной из полосок = ширина одного элемента в штрихкоде.
Я реализовал поиск таких трёх полосок максимально просто - я нахожу три первых одинаковых по ширине элемента arrays и записываю из значение в переменную, затем выхожу из цикла:
unsigned int CountPixelsInOne = 0; for (unsigned int i = 0; i < arrays.size(); i++) { if (i + 2 >= arrays.size()) return false; // не нашли нужные три полоски, а массив завершился - штрихкод повреждён или прочитан неправильно if (arrays[i] == arrays[i + 1]) { if (arrays[i + 1] == arrays[i + 2]) { CountPixelsInOne = arrays[i]; break; // последовательность найдена, значение сохранено - ломаем цикл и работаем дальше } } }
Найдём количество нулей или единиц в каждой из полосок штрихкода. Для этого разделим ширину полоски в пикселях на количество пикселей в "базовом" элементе штрихкода и прибавляем остаток от деления (обычно он в районе одного пикселя, поэтому о точности можно не беспокоиться). Получившееся значение записываем в fByteArray, очищаем массив arrays:
for (unsigned int i : arrays) { fByteArray.push_back((i / CountPixelsInOne) + (i % CountPixelsInOne)); } arrays.clear();
А теперь осталось перевести штрихкод в последовательность "бит" штрихкода:
std::vector<unsigned char> EncValues; // Enc = encrypted for (unsigned int i = 1; i <= fByteArray.size(); i++) { unsigned int count = fByteArray[i - 1]; for (unsigned int j = 0; j < count; j++) { if (i % 2 == 0) EncValues.push_back('0'); else EncValues.push_back('1'); } } fByteArray.clear();
Здесь, в цикле мы проходимся по массиву длин полосок штрихкода fByteArray, при чём начальное значениеi равно единице только лишь для удобства. Получаем значение элемента i-1 из fByteArray, далее входим в новый цикл и добавляем столько нулей или единиц в EncValues, скольки равен элемент массива fByteArray. Добавляем мы единичку или нуль зависит от чётности полоски штрихкода. Поздравляю мы близки к победе - мы получили последовательность зашифрованных бит в штрихкоде!
P.S. В программе подразумевается то, что все нечётные полоски - чёрные(1), а чётные - белые(0). В прочем так и выходит при работе всего вышеприведённого кода.
std::vector<unsigned int> result; if (Try_EAN8(EncValues, result)) { EncValues.clear(); } else if (Try_EAN13(EncValues, result)) { EncValues.clear(); } else { return false; }
Здесь всё просто: result - переменная со значением результата сканирования штрихкода. Для начала мы пробуем сканировать штрихкод, как EAN-8, если не получилось то, как EAN-13, если снова не получилось - значит программа не поддерживает такой вид штрихкода или штрихкод не верен - return false;
Для дальнейшей работы нам потребуется чуть глубже вникнуть в устройство штрихкода. Дело в том, что каждая циферка штрихкода кодируется 7ю битами. Значение каждых 7 бит зашифровано в таблицах (т.е. какой цифре равна та или иная последовательность 7 бит - в таблице). Есть несколько разновидностей кодов L-code, R-code и G-code, а так же LG-code:
Таблицы кодов
Цифра | L-код | R-код | G-код |
|---|---|---|---|
0 | 0001101 | 1110010 | 0100111 |
1 | 0011001 | 1100110 | 0110011 |
2 | 0010011 | 1101100 | 0011011 |
3 | 0111101 | 1000010 | 0100001 |
4 | 0100011 | 1011100 | 0011101 |
5 | 0110001 | 1001110 | 0111001 |
6 | 0101111 | 1010000 | 0000101 |
7 | 0111011 | 1000100 | 0010001 |
8 | 0110111 | 1001000 | 0001001 |
9 | 0001011 | 1110100 | 0010111 |
В EAN-13 есть ещё LG-code:
Первая цифра | Первая (левая) группа из 6 цифр |
|---|---|
0 | LLLLLL |
1 | LLGLGG |
2 | LLGGLG |
3 | LLGGGL |
4 | LGLLGG |
5 | LGGLLG |
6 | LGGGLL |
7 | LGLGLG |
8 | LGLGGL |
9 | LGGLGL |
Взято с википедии
В коде я каждую из них записал в std::vector<const char*>:
Таблицы кодов в коде (простите)
std::vector<const char*> L_code = { "0001101", "0011001", "0010011", "0111101", "0100011", "0110001", "0101111", "0111011", "0110111", "0001011" }; std::vector<const char*> R_code = { "1110010", "1100110", "1101100", "1000010", "1011100", "1001110", "1010000", "1000100", "1001000", "1110100" }; std::vector<const char*> G_code = { "0100111", "0110011", "0011011", "0100001", "0011101", "0111001", "0000101", "0010001", "0001001", "0010111" }; std::vector<const char*> LG_code = { "LLLLLL", "LLGLGG", "LLGGLG", "LLGGGL", "LGLLGG", "LGGLLG", "LGGGLL", "LGLGLG", "LGLGGL", "LGGLGL" };
Простите за тавтологию в заглавии :)
Try_EAN8 - функция
Эта функция проверяет входной std::vector<unsigned char> нулей и единиц (бит) штрихкода, проходится по нему и выдаёт выходной std::vector<unsigned int> зашифрованных в штрихкоде циферок.
if (input.size() != 67) return false;
Первым делом проверим количество элементов во входном std::vector. Его размер при правильном считывании штрихкода EAN-8 всегда равен 67=3+(4*7)+1+3+1+(4*7)+3:3 - первые три вспомогательные полоски4*7 - 4 циферки по 7бит каждая1+3+1 - вспомогательные полоски по центру4*7 - следующие 4 циферки по 7бит каждая 3 - последние 3 вспомогательные полоски
std::vector<unsigned int> NumsArrsEAN8; unsigned char* Bits7 = new unsigned char[8]; Bits7[7] = 0; for (unsigned int i = 3; i < input.size() - 3;) { Bits7[0] = input[i]; Bits7[1] = input[i+1]; Bits7[2] = input[i+2]; Bits7[3] = input[i+3]; Bits7[4] = input[i+4]; Bits7[5] = input[i+5]; Bits7[6] = input[i+6]; unsigned int x; if ((x = CheckCodeTable(Bits7, L_code)) <= 9) { NumsArrsEAN8.push_back(x); i += 7; } else if ((x = CheckCodeTable(Bits7, R_code)) <= 9) { NumsArrsEAN8.push_back(x); i += 7; } else { i += 5; } }
Здесь сперва мы определяем выходной std::vector NumArrsEAN8, массив циферки штрихкода (1циферка = 7 бит) Bits7. 8ой элемент массива для нуль-терминатора \0.
Входим в цикл, в котором мы пропускаем первые и последние три элемента - вспомогательные полоски в начале и конце. Вписываем в Bits7 элементы массива input с i по i+6 - семь "бит" штрихкода. Сравниваем их с таблицами кодов.
Первые 4 циферки EAN-8 закодированы по таблице L-code, а следующие 4 цифры - по таблице R-code.
Удобно, что ни в одной из таблиц нет элементов одинаковых с другой таблицей, поэтому можно просто по порядку проверять на совпадение тех или иных 7бит с той или иной таблицей кодов не заморачиваясь с проверками на правую или левую части штрихкода.
То, что возвращаемое CheckCodeTable значение должно быть меньше 9 понятно из обыкновенной математики - цифр всего десять (0-9).
В случае, если проверка ни на одну из таблиц кодов не прошла полагаем, что мы наткнулись на средние три вспомогательные полоски и пропускаем 5 элементов - 1 белая линия, 3 вспомогательные полоски и ещё 1 белая линия.
CheckCodeTable
unsigned int CheckCodeTable(unsigned char Bits7[8], std::vector<const char*> codeTable) { if (codeTable.size() == 10) return 0x0fffffffu; // неправильно задана таблица кодов for (int i = 0; i < 10; i++) { if (std::strncmp((const char*)Bits7, codeTable[i], 7) == 0) { return i; // возвращаем зашифрованную циферку } } return 0xffffffffu; // в таблице кодов совпадения не обнаружено }
Первым делом проверяется соответствует ли размер таблицы 10ти (количество цифр), если нет возвращается код ошибки размера таблицы (проверку возвращаемых значений я реализовал не полно). Далее в цикле проходимся по таблице и сравниваем значения её элементов со значением массива Bits7 и если совпадение найдено возвращает индекс элемента в массиве, если нет, то возвращает (unsigned int)(-1).
Далее необходимо найти контрольное значение и сравнить его с последней (контрольной) циферкой штрихкода:
delete Bits7; // спасаемся от утечек памяти и всего подобного им if (NumsArrsEAN8.size() != 8) // восемь ли циферок у нас получилось? return false; // Штрихкод - неверен или повреждён (3й вариант "руки из попы" - исключён) unsigned int Control = ( 10 - ( ((NumsArrsEAN8[0] + NumsArrsEAN8[2] + NumsArrsEAN8[4] + NumsArrsEAN8[6]) * 3) + (NumsArrsEAN8[1] + NumsArrsEAN8[3] + NumsArrsEAN8[5])) % 10 ); Control = Control == 10 ? 0 : Control;
Контрольная циферка в EAN-8 находится следующим образом: складываем все циферки на нечётных позициях, сумму умножаем на 3. К полученному прибавляем сумму всех циферок на чётных позициях (кроме последней - она контрольная мы её типо не знаем). Итоговое число делим на десять и остаток от деления отнимаем от десяти.
В прочем, ничего сложного в реализации нет, НО стоит учитывать один момент если контрольное число получилось равным десяти, то приравниваем его нулю (циферок то всего лишь от нуля д�� девяти включительно).
P.S. Думаю можно было бы это реализовать и в одну строчку кода - повторное деление по модулю контрольного числа на десять, но и так сойдёт...
if (Control != NumsArrsEAN8[7]) return false; for (unsigned int i : NumsArrsEAN8) { output.push_back(i); } NumsArrsEAN8.clear(); return true;
Сравниваем полученное и прочитанное контрольные числа. Копируем все элементы NumsArrsEAN8 в выходной массив output, очищаем NumsArrsEAN8 и возвращаем истину в знак успешного считывания. Мои поздравления - код работает!
Try_EAN13
В целом код для сканирования EAN-13 и EAN-8 похожи, так же как и сами штрихкоды.
if (input.size() != 95) // новый размер. Больше циферок - больше размер (математика такая же как у EAN-8, но только для 12ти цифр) return false; std::vector<unsigned int> BitsArrsEAN8; NumsArrsEAN8.push_back(0); // оставляем прозапас место для 1ой цифры std::string LG_ = ""; // новое unsigned char* Bits7 = new unsigned char[8]; unsigned int x; Bits7[7] = 0; for (unsigned int i = 3; i < input.size() - 3;) { Bits7[0] = input[i]; Bits7[1] = input[i + 1]; Bits7[2] = input[i + 2]; Bits7[3] = input[i + 3]; Bits7[4] = input[i + 4]; Bits7[5] = input[i + 5]; Bits7[6] = input[i + 6]; if ((x = CheckCodeTable(Bits7, L_code)) <= 9) { NumsArrsEAN8.push_back(x); LG_ += "L"; // новое i += 7; } else if ((x = CheckCodeTable(Bits7, R_code)) <= 9) { NumsArrsEAN8.push_back(x); i += 7; } else if ((x = CheckCodeTable(Bits7, G_code)) <= 9) { NumsArrsEAN8.push_back(x); LG_ += "G"; // новое i += 7; } else { i += 5; } }
Всё так же сравниваем размер исходного массива с требуемым, в цикле проходимся по 7 бит на циферку (кроме первой цифры, о ней позже), пропускаем средние вспомогательные полоски (5 бит), записываем полученные значения в массив NumsArrsEAN8.
Из нового: LG_code - теперь левая половина штрихкода шифруется не только с помощью L_code, но и G_code. Если попался L_code, то записываем в массив LG_ символ L, попался G_code - символ G.
delete Bits7; if ((x = CheckCodeTable((unsigned char*)LG_.c_str(), LG_code, 6)) > 9) return false; NumsArrsEAN8[0] = x;
Мы уже нашли последовательность байт LG_ при нахождении циферок левой половины штрихкода. Теперь надо найти циферку-код страны - это самая первая циферка штрихкода, она то и зашифрована LG_ байтами. По уже известному алгоритму находим эту циферку и запишем по нулевой позиции в NumsArrsEAN8.
if (NumsArrsEAN8.size() != 13) return false; unsigned int Control = (10 - ( ((NumsArrsEAN8[1] + NumsArrsEAN8[3] + NumsArrsEAN8[5] + NumsArrsEAN8[7] + NumsArrsEAN8[9] + NumsArrsEAN8[11]) * 3) +(NumsArrsEAN8[0] + NumsArrsEAN8[2] + NumsArrsEAN8[4] + NumsArrsEAN8[6] + NumsArrsEAN8[8] + NumsArrsEAN8[10])) % 10 ); Control = Control == 10 ? 0 : Control;
Сравниваем размер массива циферок с требуемым (13). Находим контрольное число по уже знакомому нам алгоритму.
if (Control != NumsArrsEAN8[12]) return false; for (unsigned int i : NumsArrsEAN8) { output.push_back(i); } NumsArrsEAN8.clear(); return true;
Сравниваем контрольную циферку с последней в штрихкоде (тоже контрольной). Копируем значения всех элементов NumsArrsEAN8 в выходной массив и возвращаем истину.
Ссылка на файл с полным кодом (немного видоизменённый): github.
Благодарю за то, что уделили время прочтению этой достаточно объёмной статьи❤️! Как всегда буду рад конструктивной критики и полезным замечаниям. Всем до новых встреч!