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

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

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

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