Не так давно у меня возникла необходимость в сканере штрихкода. Конечно можно было бы взять готовый сканнер откуда-нибудь из интернета, НО зачем? Зачем если можем написать сами? Именно с такими мыслями я сел и написал собственный сканер штрихкода. Правда сканирует пока что только из изображений, но это исправимо.
Что же такое штрихкод
Штрихово́й код — графическая информация, наносимая на поверхность, маркировку или упаковку изделий, предоставляющая возможность считывания её техническими средствами — последовательность чёрных и белых полос, либо других геометрических фигур.
Такое определение штрихкода даётся на википедии. Штрихкоды мы можем найти на практически всех современных товарах. Они удобны в использовании. Существует несколько разновидностей штрихкодов 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.
Благодарю за то, что уделили время прочтению этой достаточно объёмной статьи❤️! Как всегда буду рад конструктивной критики и полезным замечаниям. Всем до новых встреч!
