Как стать автором
Обновить

Пишем сканер штрихкодов на c++

Уровень сложностиСредний
Время на прочтение10 мин
Количество просмотров11K

Не так давно у меня возникла необходимость в сканере штрихкода. Конечно можно было бы взять готовый сканнер откуда-нибудь из интернета, НО зачем? Зачем если можем написать сами? Именно с такими мыслями я сел и написал собственный сканер штрихкода. Правда сканирует пока что только из изображений, но это исправимо.

Что же такое штрихкод

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

Такое определение штрихкода даётся на википедии. Штрихкоды мы можем найти на практически всех современных товарах. Они удобны в использовании. Существует несколько разновидностей штрихкодов EAN-8 EAN-13 UPC code56... Я же написал сканнер только для EAN-8 и   EAN-13.


Непосредственно код

Ну что ж, перейдём от слов к делу... У нас есть два штрихкода в формате png:

Изображения
EAN-8
EAN-8
EAN-13
EAN-13

Я сгенерировал их на первых в результатах поиска онлайн генераторах штрихкодов

Я прочитал эти изображения тем, что было под рукой - библиотекой 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.
Благодарю за то, что уделили время прочтению этой достаточно объёмной статьи❤️! Как всегда буду рад конструктивной критики и полезным замечаниям. Всем до новых встреч!

Теги:
Хабы:
+13
Комментарии15

Публикации

Работа

QT разработчик
5 вакансий
Программист C++
96 вакансий

Ближайшие события