Пишем сканер штрихкодов на c++
Не так давно у меня возникла необходимость в сканере штрихкода. Конечно можно было бы взять готовый сканнер откуда-нибудь из интернета, НО зачем? Зачем если можем написать сами? Именно с такими мыслями я сел и написал собственный сканер штрихкода. Правда сканирует пока что только из изображений, но это исправимо.
Что же такое штрихкод
Штрихово́й код — графическая информация, наносимая на поверхность, маркировку или упаковку изделий, предоставляющая возможность считывания её техническими средствами — последовательность чёрных и белых полос, либо других геометрических фигур.
Такое определение штрихкода даётся на википедии. Штрихкоды мы можем найти на практически всех современных товарах. Они удобны в использовании. Существует несколько разновидностей штрихкодов 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 fByteArray; // Наш итоговый массив значений пикселей
cv::Point point(0, 0); // точка-позиция в полосочке
for (int col = 0; col < formatted.cols; col++) {
point.x = col;
unsigned char el = formatted.at(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 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 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 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
:
std::vector L_code = {
"0001101", "0011001", "0010011",
"0111101", "0100011", "0110001",
"0101111", "0111011", "0110111",
"0001011"
};
std::vector R_code = {
"1110010", "1100110", "1101100",
"1000010", "1011100", "1001110",
"1010000", "1000100", "1001000",
"1110100"
};
std::vector G_code = {
"0100111", "0110011", "0011011",
"0100001", "0011101", "0111001",
"0000101", "0010001", "0001001",
"0010111"
};
std::vector LG_code = {
"LLLLLL", "LLGLGG", "LLGGLG",
"LLGGGL", "LGLLGG", "LGGLLG",
"LGGGLL", "LGLGLG", "LGLGGL",
"LGGLGL"
};
Простите за тавтологию в заглавии :)
Try_EAN8 — функция
Эта функция проверяет входной std::vector
нулей и единиц (бит) штрихкода, проходится по нему и выдаёт выходной std::vector
зашифрованных в штрихкоде циферок.
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 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 белая линия.
unsigned int CheckCodeTable(unsigned char Bits7[8], std::vector 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 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.
Благодарю за то, что уделили время прочтению этой достаточно объёмной статьи❤️! Как всегда буду рад конструктивной критики и полезным замечаниям. Всем до новых встреч!
Habrahabr.ru прочитано 14350 раз