Позиционирование QR-кода OpenCV4 и анализ исходного кода идентификации

OpenCV
Позиционирование QR-кода OpenCV4 и анализ исходного кода идентификации

Оригинальный адрес:158. Никто не может /index.PHP/ ах...

Проанализируйте исходный код C++ для распознавания QR-кода, используемого в OpenCV4.
QRCodeDetector в основном включает функции обнаружения и декодирования для внешнего использования для обнаружения и декодирования QR-кодов.
На этот раз давайте сначала рассмотрим часть позиционирования.

основная функция

Класс, который фактически обрабатывает часть позиционирования QR-кода в QRCodeDetector, — это класс QRDetect.
Основные функции класса QRDetect следующие:

// 初始化
void init(const Mat& src, double eps_vertical_ = 0.2, double eps_horizontal_ = 0.1);
// 获取定位,左上·右上·左下三个定位标记的中心点
bool localization();
// 获取二维码四边形区域的四个顶点
bool computeTransformationPoints();
// 计算两条线交叉点
static Point2f intersectionLines(Point2f a1, Point2f a2, Point2f b1, Point2f b2);

Принципиальный анализ

qrcode
QRCode

На этот раз я подготовил изображение QR-кода со слегка наклонным углом.
Чтобы четко идентифицировать черные и белые блоки в QR-коде, изображение обрабатывается в оттенках серого, а затем бинаризируется.
Затем найдите точки, соответствующие правилам двумерного кода, из бинаризованного изображения.

// QRDetect::init函数
// ...
// 二值化
adaptiveThreshold(barcode, bin_barcode, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 83, 2);
// ...

Найдите центральную точку метки позиционирования

поискГоризонтальные линиифункция

Как показано на рисунке ниже, функция searchHorizontalLines в основном предназначена для поиска цветовых блоков на изображении, которые соответствуют соотношению 1:1:3:1:1 по горизонтальной линии.
То есть горизонтальная линия сразу за черным квадратом в центре.

search_horizontal_lines
search_horizontal_lines

В функции сканируйте строку за строкой, начиная с первой строки, и сохраняйте положение цветового блока инверсии черного и белого цветов.

// QRDetect::searchHorizontalLines函数
// ...
uint8_t future_pixel = 255;
for (int x = pos; x < width_bin_barcode; x++)
{
    if (bin_barcode_row[x] == future_pixel)
    {
        future_pixel = static_cast<uint8_t>(~future_pixel); // 8位反转运算,0 or 255
        pixels_position.push_back(x);
    }
}
// ...

Затем пройдите положение черных и белых перевернутых цветных блоков в этой строке и найдите сегмент линии, который соответствует соотношению 1:1:3:1:1, а отклонение находится в пределах допустимого диапазона.

// QRDetect::searchHorizontalLines函数
// ...
for (size_t i = 2; i < pixels_position.size() - 4; i+=2)
{
    // 五条线段的长度
    test_lines[0] = static_cast<double>(pixels_position[i - 1] - pixels_position[i - 2]);
    test_lines[1] = static_cast<double>(pixels_position[i    ] - pixels_position[i - 1]);
    test_lines[2] = static_cast<double>(pixels_position[i + 1] - pixels_position[i    ]);
    test_lines[3] = static_cast<double>(pixels_position[i + 2] - pixels_position[i + 1]);
    test_lines[4] = static_cast<double>(pixels_position[i + 3] - pixels_position[i + 2]);

    double length = 0.0, weight = 0.0;  // TODO avoid 'double' calculations

    for (size_t j = 0; j < test_lines_size; j++) { length += test_lines[j]; }

    if (length == 0) { continue; }
    for (size_t j = 0; j < test_lines_size; j++)
    {
        // 根据1:1:3:1:1比例,中间的线段应占7分之3的比例,其余为7分之1
        // 累加线段偏移此比例的值
        if (j != 2) { weight += fabs((test_lines[j] / length) - 1.0/7.0); }
        else        { weight += fabs((test_lines[j] / length) - 3.0/7.0); }
    }

    // 偏移值在容差范围内的话保存进结果
    if (weight < eps_vertical)
    {
        Vec3d line;
        line[0] = static_cast<double>(pixels_position[i - 2]); // 水平线x值
        line[1] = y; // 水平线y值
        line[2] = length; // 水平线长度
        result.push_back(line);
    }
}
// ...

функция разделения вертикальных линий

Далее по найденной горизонтальной линии найти точку на вертикальной линии, соответствующую закону,
Это делает функция extractVerticalLines в функции SeparateVerticalLines.
Предустановка состоит в том, чтобы начать с вертикальной центральной точки и искать пропорции верхней и нижней сторон по очереди.
Таким образом, соотношение составляет 2:2:6:2:2.

search_vertical_lines
search_vertical_lines

Это в основном то же самое, что и нахождение горизонтальной линии, но поскольку вертикальная линия определяется на основе горизонтальной линии, найденной ранее,
Итак, на этот раз вы можете напрямую определить длину каждого сегмента линии, всего их 6.

// QRDetect::extractVerticalLines
// ...
// --------------- Search vertical up-lines --------------- //

test_lines.clear();
uint8_t future_pixel_up = 255;

int temp_length_up = 0;
for (int j = y; j < bin_barcode.rows - 1; j++)
{
    uint8_t next_pixel = bin_barcode.ptr<uint8_t>(j + 1)[x];
    temp_length_up++; // 遇到颜色反转前长度累加
    if (next_pixel == future_pixel_up)
    {
        future_pixel_up = static_cast<uint8_t>(~future_pixel_up);
        test_lines.push_back(temp_length_up);
        temp_length_up = 0;
        if (test_lines.size() == 3)
            break;
    }
}

// --------------- Search vertical down-lines --------------- //

int temp_length_down = 0;
uint8_t future_pixel_down = 255;
for (int j = y; j >= 1; j--)
{
    uint8_t next_pixel = bin_barcode.ptr<uint8_t>(j - 1)[x];
    temp_length_down++; // 遇到颜色反转前长度累加
    if (next_pixel == future_pixel_down)
    {
        future_pixel_down = static_cast<uint8_t>(~future_pixel_down);
        test_lines.push_back(temp_length_down);
        temp_length_down = 0;
        if (test_lines.size() == 6)
            break;
    }
}
// ...

Определите соотношение длин 6 отрезков и сохраните горизонтальные линии в пределах допустимого диапазона.
Здесь следует отметить, что, поскольку центральный квадрат разделен на два отрезка, соотношение суждений составляет 3/14.

// QRDetect::extractVerticalLines
// ...
// --------------- Compute vertical lines --------------- //

if (test_lines.size() == 6)
{
    double length = 0.0, weight = 0.0;  // TODO avoid 'double' calculations

    for (size_t i = 0; i < test_lines.size(); i++)
        length += test_lines[i];

    CV_Assert(length > 0);
    for (size_t i = 0; i < test_lines.size(); i++)
    {
        if (i % 3 != 0)
        {
            weight += fabs((test_lines[i] / length) - 1.0/ 7.0);
        }
        else
        {
            // 中心方块被分为两段,所以比例是14分之3
            weight += fabs((test_lines[i] / length) - 3.0/14.0);
        }
    }

    if (weight < eps)
    {
        result.push_back(list_lines[pnt]);
    }
}
// ...

После оценки компактности возвращается центральная точка каждого сегмента линии, и функция завершается.

Функции K-средних и точек фиксации

На вертикальной линии есть несколько центральных точек, которые пропорциональны диапазону допуска.
Используйте алгоритм кластеризации K-средних, чтобы разделить все точки на три набора и вычислить их центральные точки,
Обычно в это время получается центр установочной метки.

Затем в функции fixationPoints эти три точки проверяются.

Убедитесь, что косинусы трех точек находятся в диапазоне:

// QRDetect::fixationPoints
// ...
double cos_angles[3], norm_triangl[3];

norm_triangl[0] = norm(local_point[1] - local_point[2]);
norm_triangl[1] = norm(local_point[0] - local_point[2]);
norm_triangl[2] = norm(local_point[1] - local_point[0]);

cos_angles[0] = (norm_triangl[1] * norm_triangl[1] + norm_triangl[2] * norm_triangl[2]
              -  norm_triangl[0] * norm_triangl[0]) / (2 * norm_triangl[1] * norm_triangl[2]);
cos_angles[1] = (norm_triangl[0] * norm_triangl[0] + norm_triangl[2] * norm_triangl[2]
              -  norm_triangl[1] * norm_triangl[1]) / (2 * norm_triangl[0] * norm_triangl[2]);
cos_angles[2] = (norm_triangl[0] * norm_triangl[0] + norm_triangl[1] * norm_triangl[1]
              -  norm_triangl[2] * norm_triangl[2]) / (2 * norm_triangl[0] * norm_triangl[1]);

const double angle_barrier = 0.85;
if (fabs(cos_angles[0]) > angle_barrier || fabs(cos_angles[1]) > angle_barrier || fabs(cos_angles[2]) > angle_barrier)
{
    local_point.clear();
    return;
}
// ...

Чтобы определить, какая точка является левым верхним углом, определите точку, ближайшую к 90 градусам, по значению косинуса,
И определить, является ли точка с наибольшей площадью (текст непрост для описания), образованная пересечением трехточечного связанного отрезка линии и позиционной метки.

// QRDetect::fixationPoints
// ...
size_t i_min_cos =
   (cos_angles[0] < cos_angles[1] && cos_angles[0] < cos_angles[2]) ? 0 :
   (cos_angles[1] < cos_angles[0] && cos_angles[1] < cos_angles[2]) ? 1 : 2;

size_t index_max = 0;
double max_area = std::numeric_limits<double>::min();
for (size_t i = 0; i < local_point.size(); i++)
{
    const size_t current_index = i % 3;
    const size_t left_index  = (i + 1) % 3;
    const size_t right_index = (i + 2) % 3;

    const Point2f current_point(local_point[current_index]),
        left_point(local_point[left_index]), right_point(local_point[right_index]),
        // 当前点至另外两点的中心点的线段与图像底部线段的交叉点
        central_point(intersectionLines(current_point,
                          Point2f(static_cast<float>((local_point[left_index].x + local_point[right_index].x) * 0.5),
                                  static_cast<float>((local_point[left_index].y + local_point[right_index].y) * 0.5)),
                          Point2f(0, static_cast<float>(bin_barcode.rows - 1)),
                          Point2f(static_cast<float>(bin_barcode.cols - 1),
                                  static_cast<float>(bin_barcode.rows - 1))));


    vector<Point2f> list_area_pnt;
    list_area_pnt.push_back(current_point);

    // 遍历三条线段,并找出与当前定位标识外框所交错的三个点
    vector<LineIterator> list_line_iter;
    list_line_iter.push_back(LineIterator(bin_barcode, current_point, left_point));
    list_line_iter.push_back(LineIterator(bin_barcode, current_point, central_point));
    list_line_iter.push_back(LineIterator(bin_barcode, current_point, right_point));

    for (size_t k = 0; k < list_line_iter.size(); k++)
    {
        LineIterator& li = list_line_iter[k];
        uint8_t future_pixel = 255, count_index = 0;
        for(int j = 0; j < li.count; j++, ++li)
        {
            const Point p = li.pos();
            if (p.x >= bin_barcode.cols ||
                p.y >= bin_barcode.rows)
            {
                break;
            }

            const uint8_t value = bin_barcode.at<uint8_t>(p);
            if (value == future_pixel)
            {
                future_pixel = static_cast<uint8_t>(~future_pixel);
                count_index++;
                if (count_index == 3)
                {
                    list_area_pnt.push_back(p);
                    break;
                }
            }
        }
    }

    // 计算外框交错的三点与当前点形成的四边形面积
    const double temp_check_area = contourArea(list_area_pnt);
    // 形成的面积最大的当前点即为左上角的点
    if (temp_check_area > max_area)
    {
        index_max = current_index;
        max_area = temp_check_area;
    }

}
// 第一个位置放左上角的点
if (index_max == i_min_cos) { std::swap(local_point[0], local_point[index_max]); }
else { local_point.clear(); return; }
// ...

Наконец, определите порядок нижней левой и верхней правой точек и решите, следует ли поменять местами по определителю

// QRDetect::fixationPoints
// ...
const Point2f rpt = local_point[0], bpt = local_point[1], gpt = local_point[2];
Matx22f m(rpt.x - bpt.x, rpt.y - bpt.y, gpt.x - rpt.x, gpt.y - rpt.y);
// 行列式反转判断
if( determinant(m) > 0 )
{
    std::swap(local_point[1], local_point[2]);
}
// ...

Найдите вершины четырехугольника области QR-кода

Расчет затопления и выпуклой оболочки

Сначала найдите внешнюю рамку метки позиционирования через центральные точки трех меток позиционирования.
Используйте заливку, чтобы заполнить контур в маске.

Затем вычислите выпуклую оболочку набора из трех внешних ящиков, чтобы получить окружающие точки.

// QRDetect::computeTransformationPoints
// ...
vector<Point> locations, non_zero_elem[3], newHull;
vector<Point2f> new_non_zero_elem[3];
for (size_t i = 0; i < 3; i++)
{
    Mat mask = Mat::zeros(bin_barcode.rows + 2, bin_barcode.cols + 2, CV_8UC1);
    uint8_t next_pixel, future_pixel = 255;
    int count_test_lines = 0, index = cvRound(localization_points[i].x);
    for (; index < bin_barcode.cols - 1; index++)
    {
        next_pixel = bin_barcode.ptr<uint8_t>(cvRound(localization_points[i].y))[index + 1];
        if (next_pixel == future_pixel)
        {
            future_pixel = static_cast<uint8_t>(~future_pixel);
            count_test_lines++;
            if (count_test_lines == 2)
            {
                // 找到外框的点,进行填充
                floodFill(bin_barcode, mask,
                          Point(index + 1, cvRound(localization_points[i].y)), 255,
                          0, Scalar(), Scalar(), FLOODFILL_MASK_ONLY);
                break;
            }
        }
    }
    Mat mask_roi = mask(Range(1, bin_barcode.rows - 1), Range(1, bin_barcode.cols - 1));
    findNonZero(mask_roi, non_zero_elem[i]);
    newHull.insert(newHull.end(), non_zero_elem[i].begin(), non_zero_elem[i].end());
}
// 对三个外框的集合进行凸包计算
convexHull(newHull, locations);
// ...

Среди окружающих точек две точки с наибольшим расстоянием — это две вершины слева внизу и справа вверху.
Точка с наибольшей площадью, которая может быть образована нижним левым и верхним правым, является верхней левой вершиной.

// QRDetect::computeTransformationPoints
// ...
double pentagon_diag_norm = -1;
Point2f down_left_edge_point, up_right_edge_point, up_left_edge_point;
for (size_t i = 0; i < new_non_zero_elem[1].size(); i++)
{
    for (size_t j = 0; j < new_non_zero_elem[2].size(); j++)
    {
        double temp_norm = norm(new_non_zero_elem[1][i] - new_non_zero_elem[2][j]);
        if (temp_norm > pentagon_diag_norm)
        {
            down_left_edge_point = new_non_zero_elem[1][i];
            up_right_edge_point  = new_non_zero_elem[2][j];
            pentagon_diag_norm = temp_norm;
        }
    }
}

if (down_left_edge_point == Point2f(0, 0) ||
    up_right_edge_point  == Point2f(0, 0) ||
    new_non_zero_elem[0].size() == 0) { return false; }

double max_area = -1;
up_left_edge_point = new_non_zero_elem[0][0];

for (size_t i = 0; i < new_non_zero_elem[0].size(); i++)
{
    vector<Point2f> list_edge_points;
    list_edge_points.push_back(new_non_zero_elem[0][i]);
    list_edge_points.push_back(down_left_edge_point);
    list_edge_points.push_back(up_right_edge_point);

    double temp_area = fabs(contourArea(list_edge_points));
    if (max_area < temp_area)
    {
        up_left_edge_point = new_non_zero_elem[0][i];
        max_area = temp_area;
    }
}
// ...

corner_points
corner_points

Четвертая вершина в правом нижнем углу определяется пересечением нижнего левого и верхнего правого внешних блоков, доходящих до нижнего правого угла.

transformation_points.push_back(down_left_edge_point);
transformation_points.push_back(up_left_edge_point);
transformation_points.push_back(up_right_edge_point);
transformation_points.push_back(
    intersectionLines(down_left_edge_point, down_max_delta_point,
                      up_right_edge_point, up_max_delta_point));

перспективное преобразование

В части декодирования четыре найденные вершины используются для преобразования перспективы, а изображение преобразуется во фронтальную перспективу.
В основном используются функции findHomography и warpPerspective (perspectiveTransform можно использовать для простого преобразования координат).

const Point2f centerPt = QRDetect::intersectionLines(original_points[0], original_points[2],
                                                     original_points[1], original_points[3]);
if (cvIsNaN(centerPt.x) || cvIsNaN(centerPt.y))
    return false;

const Size temporary_size(cvRound(test_perspective_size), cvRound(test_perspective_size));

vector<Point2f> perspective_points;
perspective_points.push_back(Point2f(0.f, 0.f));
perspective_points.push_back(Point2f(test_perspective_size, 0.f));

perspective_points.push_back(Point2f(test_perspective_size, test_perspective_size));
perspective_points.push_back(Point2f(0.f, test_perspective_size));

perspective_points.push_back(Point2f(test_perspective_size * 0.5f, test_perspective_size * 0.5f));

vector<Point2f> pts = original_points;
pts.push_back(centerPt);
// 单应矩阵
Mat H = findHomography(pts, perspective_points);
Mat bin_original;
adaptiveThreshold(original, bin_original, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 83, 2);
Mat temp_intermediate;
// 图片转换
warpPerspective(bin_original, temp_intermediate, H, temporary_size, INTER_NEAREST);
no_border_intermediate = temp_intermediate(Range(1, temp_intermediate.rows), Range(1, temp_intermediate.cols));

Тогда фактическая функция декодирования вызывается библиотекой quirc, которая не будет здесь объясняться.

Суммировать

Весь процесс сводится к:

  1. Отсканируйте изображение по горизонтали и вертикали и найдите правильные точки по трем меткам позиционирования.
  2. Используйте kmeans, чтобы найти центральную точку трех наборов, то есть получить центр трех позиционных меток.
  3. FloodFill заполняет внешний фрейм, а затем использует выпуклую оболочку для вычисления окружающих точек трех внешних фреймов.
  4. Две точки с наибольшим расстоянием — это вершины нижнего левого и верхнего правого углов четырехугольника QR-кода, а точка, которая может образовать наибольшую площадь с нижней левой и верхней правой вершинами, — это верхняя левая вершина.
  5. Точка в правом нижнем углу получается из пересечения выносных линий нижней левой и верхней правой вершин
  6. Используйте получившиеся четыре вершины для преобразования перспективы и преобразования во фронтальное изображение.
  7. Вызов библиотеки quirc для декодирования