I was looking at ways to create QR codes and combining them with OpenCV. One particular library I was looking to use was Project Nayuki's QR Code Generator.

The license is MIT, which is generally what I use for all my projects. And it consists of a single .hpp and .cpp file, which is easy to drop into existing projects.

At first glance, I almost dismissed the library because I couldn't see an easy way to save or access the generated QR codes. But it turned out to be simple, and I wanted to document what I ended up doing.

Understanding Project Nayuki's QR Code Generator

Generating a QR code is extremely simple:

#include "qrcodegen.hpp" // ... const qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText("this is a simple test", qrcodegen::QrCode::Ecc::HIGH); // but then what do we do with this "qr" object?

To understand what we need to do to access or display the QR code, we need to understand some terminology.

What is a QR code?

QR codes are composed of black and white spaces arranged in a square grid. What we normally think of as a "pixel", QR codes calls "module". This example QR code has a width and height of exactly 21 modules. That means a total of 441 modules (21x21), some of which is used for position markers, timing patterns, correction information, data, etc.

example QR code measuring 21x21 modules

The empty white border around the QR code is called the "quiet zone". For standard QR codes, the quiet zone measures 4 modules in height and width.

This brings us back to our qrcodegen::QrCode object. There are 2 methods we need to call:

The getSize() method returns 21 for the example above.

And the getModule() takes 2 parameters -- X and Y coordinates which begin in the top-left corner -- and returns a bool to indicate if a module is white or black. false means white and true means black.

Using OpenCV

So if you call getModule() in a loop for all X and Y coordinates, you can build the QR code. The problem is if each QR code module maps to a single pixel, you'll end up with a QR code that is so small, it is unusable. With this example, having a size of 21, the QR code will be 21x21 pixels, which is too small to be usable.

a QR code that measure only 21x21 pixels, too small to be usable

The solution is quite simple. Each module in the QR code needs to map to multiple pixels, which results in a larger image. For example, instead of mapping a module to a single pixel, if each module maps to a square of 10x10 pixels, the previous example which originally measured 21x21 pixels would now measure 210x210 pixels:

290x290 pixel QR code

That mapping of modules to blocks of pixels can be done with the following OpenCV code:

cv::Mat generate_qr_code(const std::string & str, const size_t maximum_image_size_in_pixels = 640, const size_t quiet_zone_in_modules = 4) { const cv::Scalar black(0, 0, 0); const cv::Scalar white(255, 255, 255); const qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(str.c_str(), qrcodegen::QrCode::Ecc::LOW); const size_t qr_size = qr.getSize(); // number of dots ("modules") per side, but this does not include the quiet zone const size_t scale = maximum_image_size_in_pixels / (qr_size + 2 * quiet_zone_in_modules); const size_t border_size = scale * quiet_zone_in_modules; // the size of a single border, in pixels const size_t final_size = scale * qr_size + 2 * border_size; // image dimensions including quiet zone (border) #ifdef DEBUG std::cout << "qr_size=" << qr_size << " quiet_zone=" << quiet_zone_in_modules << " scale=" << scale << " border_size=" << border_size << " final_size=" << final_size << std::endl; #endif cv::Mat mat(final_size, final_size, CV_8UC3, white); for (size_t y = 0; y < qr_size; y ++) { for (size_t x = 0; x < qr_size; x ++) { if (qr.getModule(x, y)) { mat(cv::Rect(scale * (x + quiet_zone_in_modules), scale * (y + quiet_zone_in_modules), scale, scale)) = black; } } } return mat; }

By specifying the maximum image size, this code will calculate the scaling factor that must be applied. It then iterates over the QR code modules. Every time a module needs to be filled in, we create a region-of-interest of the required size, and we set that entire region to black.


Turns out in my case, I needed more than QR codes for my project. I needed some 1-dimension barcodes as well so I ended up going back to Zint, which I've used in the past. But I was impressed with Project Nayuki's QR Code Generator and thought it deserved a blog post entry to show how easy it is to use with OpenCV.

Last modified: 2023-11-04
Stéphane Charette, stephanecharette@gmail.com