Building a Desktop C++ Barcode Scanner with Slimmed-Down OpenCV and Webcam

OpenCV is a powerful library capable of handling a wide range of computer vision tasks. However, its comprehensive functionality can result in a large library size, which may be excessive for applications that only require cross-platform camera streaming. In this article, we will demonstrate how to slim down OpenCV to retain only the essential camera functionalities. We will then guide you through the process of building a desktop barcode scanner in C++ using the tailored OpenCV and Dynamsoft C++ Barcode SDK v10.x on Windows and Linux. A CMake project will be created to manage the build process efficiently.

Prerequisites

Step 1: Tailoring OpenCV for Camera-Only Functionality

To reduce the size of OpenCV and include only the necessary components for camera streaming, you need to configure the OpenCV build process to exclude unnecessary modules. This involves specifying build options to disable certain features and modules that are not required for your application. Here’s how you can do it:

  1. Obtain the OpenCV source code from the official OpenCV repository on GitHub:

     git clone https://github.com/opencv/opencv.git
     cd opencv
    
  2. Use CMake to configure the build options. We will disable all the non-essential modules and keep only the core, highgui, videoio, and imgproc modules needed for camera functionalities.

     mkdir build
     cd build
     cmake -DBUILD_SHARED_LIBS=ON -DBUILD_opencv_world=OFF -DBUILD_opencv_apps=OFF -DBUILD_opencv_calib3d=OFF -DBUILD_opencv_dnn=OFF -DBUILD_opencv_features2d=OFF -DBUILD_opencv_flann=OFF -DBUILD_opencv_gapi=OFF -DBUILD_opencv_ml=OFF -DBUILD_opencv_objdetect=OFF -DBUILD_opencv_photo=OFF -DBUILD_opencv_stitching=OFF -DBUILD_opencv_video=OFF -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_DOCS=OFF ..
    
  3. Once the configuration is complete, build OpenCV with the specified options.

     cmake --build . --config Release
    

By following these steps, you will have a streamlined version of OpenCV that includes only the necessary components for handling camera input, significantly reducing the library size. This tailored version of OpenCV will be used in the subsequent steps to build a desktop barcode scanner application in C++.

Step 2: Setting Up the CMake Project with OpenCV Libraries

With the tailored version of OpenCV built, the next step is to set up a CMake project that utilizes these libraries to build a simplified camera library.

  1. Set up the project directory structure as follows:

     camera_lib/
     ├── CMakeLists.txt
     ├── example
     |   ├── CMakeLists.txt
     |   └── main.cpp
     ├── lib/
     |   ├── linux/
     |   |   ├── libopencv_core.so
     |   |   ├── libopencv_highgui.so
     |   |   ├── libopencv_imgcodecs.so
     |   |   ├── libopencv_imgproc.so
     |   |   └── libopencv_videoio.so
     │   └── windows/
     |       ├── opencv_core480.dll
     |       ├── opencv_highgui480.dll
     |       ├── opencv_imgcodecs480.dll
     |       ├── opencv_imgproc480.dll
     |       ├── opencv_videoio480.dll
     |       ├── opencv_core480.lib
     |       ├── opencv_highgui480.lib
     |       ├── opencv_imgcodecs480.lib
     |       ├── opencv_imgproc480.lib
     |       └── opencv_videoio480.lib
     ├── src/
     │   ├── camera_lib.cpp
     │   └── CMakeLists.txt
     └── include/
         ├── camera_lib.h
         └── opencv2
    

    The CMake project is organized into several key directories: src, example, lib, and include. The src directory houses the source files for the camera library, which leverages the tailored OpenCV libraries. The example directory contains the main application file, showcasing how to use the camera library to implement barcode scanning functionality. The lib directory holds the OpenCV libraries for both Windows and Linux platforms. Finally, the include directory contains the header files necessary for the camera library and OpenCV.

  2. Create the CMakeLists.txt file in the project root directory:

     cmake_minimum_required(VERSION 3.10)
     project(camera_lib)
        
     add_subdirectory(src)
     add_subdirectory(example)
    
    
  3. Create the CMakeLists.txt file in the src directory:

     cmake_minimum_required(VERSION 3.10)
     project(camera_lib)
        
     # Set the library path based on the operating system
     if(WIN32)
         link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../lib/windows)
         set(OPENCV_LIBS opencv_core480 opencv_highgui480 opencv_videoio480 opencv_imgproc480)
     elseif(UNIX)
         link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../lib/linux)
         set(OPENCV_LIBS opencv_core opencv_highgui opencv_videoio opencv_imgproc)
     endif()
        
     # Create the shared library
     add_library(camera_lib SHARED camera_lib.cpp)
     target_include_directories(camera_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include)
     target_link_libraries(camera_lib ${OPENCV_LIBS})
        
     if(MSVC)
         target_compile_definitions(camera_lib PRIVATE CAMERA_LIB_EXPORTS)
     endif()
    
  4. Create the CMakeLists.txt file in the example directory:

     cmake_minimum_required(VERSION 3.10)
     project(camera_example)
        
     if(WIN32)
         link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../lib/windows ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/platforms/win/lib)
         file(GLOB DLL_FILES "${CMAKE_CURRENT_SOURCE_DIR}/../lib/windows/*.dll")
         set(DBR_LIBS "DynamsoftCorex64" "DynamsoftLicensex64" "DynamsoftCaptureVisionRouterx64" "DynamsoftUtilityx64")
     elseif(UNIX)
         SET(CMAKE_CXX_FLAGS "-std=c++11 -O3 -Wl,-rpath=$ORIGIN")
         SET(CMAKE_INSTALL_RPATH "$ORIGIN")
         link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../lib/linux ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/platforms/linux)
         set(DBR_LIBS "DynamsoftCore" "DynamsoftLicense" "DynamsoftCaptureVisionRouter" "DynamsoftUtility" pthread)
     endif()
        
     # Create the executable
     add_executable(camera_example main.cpp)
     target_include_directories(camera_example PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/include)
     target_link_libraries(camera_example camera_lib ${DBR_LIBS})
        
     if(WIN32)
         add_custom_command(TARGET camera_example POST_BUILD
             COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:camera_lib> $<TARGET_FILE_DIR:camera_example>)
        
         # Copy the DLL files to the executable directory
         foreach(DLL_FILE ${DLL_FILES})
             add_custom_command(TARGET camera_example POST_BUILD
                 COMMAND ${CMAKE_COMMAND} -E copy_if_different
                 ${DLL_FILE}
                 $<TARGET_FILE_DIR:camera_example>)
         endforeach()
        
         add_custom_command(TARGET camera_example POST_BUILD
         COMMAND ${CMAKE_COMMAND} -E copy_directory
         ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/platforms/win/bin/      
         $<TARGET_FILE_DIR:camera_example>)
     endif()
        
     if(UNIX)
         add_custom_command(TARGET camera_example POST_BUILD
             COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:camera_lib> $<TARGET_FILE_DIR:camera_example>)
     endif()
        
     add_custom_command(TARGET camera_example POST_BUILD
     COMMAND ${CMAKE_COMMAND} -E copy
     ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/DBR-PresetTemplates.json
     $<TARGET_FILE_DIR:camera_example>/DBR-PresetTemplates.json)
        
     add_custom_command(TARGET camera_example POST_BUILD
     COMMAND ${CMAKE_COMMAND} -E copy
     ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/DLR-PresetTemplates.json
     $<TARGET_FILE_DIR:camera_example>/DLR-PresetTemplates.json)
        
     add_custom_command(TARGET camera_example POST_BUILD
     COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:camera_lib>/CharacterModel
     COMMAND ${CMAKE_COMMAND} -E copy_directory
     ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/10.x/sdk/CharacterModel
     $<TARGET_FILE_DIR:camera_example>/CharacterModel)
    

    Replace the DBR_LIBS variable with the actual library names of the Dynamsoft Barcode SDK v10.x for your platform.

Step 3: Implementing the Camera Library and Barcode Scanner Application

In this step, we will develop a straightforward camera library to capture and display video frames from the webcam. Subsequently, we will create a barcode scanner application that leverages the camera library to stream video frames and decode barcodes in real-time using the Dynamsoft C++ Barcode SDK v10.x.

C++ Camera Library

  1. In the src directory, create the camera_lib.h header file:

     #ifndef CAMERA_LIB_H
     #define CAMERA_LIB_H
        
     #ifdef _WIN32
     #ifdef CAMERA_LIB_EXPORTS
     #define CAMERA_LIB_API __declspec(dllexport)
     #else
     #define CAMERA_LIB_API __declspec(dllimport)
     #endif
     #else
     #define CAMERA_LIB_API
     #endif
        
     #include <cstdint>
        
     enum PixelFormat
     {
         PIXEL_FORMAT_BGR,
         PIXEL_FORMAT_GRAY
     };
        
     extern "C"
     {
         typedef struct
         {
             int width;
             int height;
             int stride;
             PixelFormat pixel_format;
             uint8_t *data;
         } ImageData;
        
         CAMERA_LIB_API int open_camera(int camera_index);
         CAMERA_LIB_API void close_camera();
         CAMERA_LIB_API ImageData get_frame();
         CAMERA_LIB_API void release_frame(ImageData *image);
         CAMERA_LIB_API void display_frame(const char *name, const ImageData *image);
         CAMERA_LIB_API void draw_line(ImageData *image, int x1, int y1, int x2, int y2, int thickness, int r, int g, int b);
         CAMERA_LIB_API void draw_text(ImageData *image, const char *text, int x, int y, int font_scale, int thickness, int r, int g, int b);
         CAMERA_LIB_API int wait_key(int delay);
     }
        
     #endif // CAMERA_LIB_H
        
    

    This header file defines the ImageData struct to represent image data and declares functions for camera initialization, frame retrieval, frame display, and drawing operations.

  2. Implement the camera library functions in the camera_lib.cpp file within the src directory:

     #include "camera_lib.h"
     #include <opencv2/opencv.hpp>
        
     static cv::VideoCapture cap;
        
     int open_camera(int camera_index)
     {
         cap.open(camera_index);
         return cap.isOpened() ? 0 : -1;
     }
        
     void close_camera()
     {
         if (cap.isOpened())
         {
             cap.release();
         }
     }
        
     ImageData get_frame()
     {
         ImageData image = {0, 0, 0, PIXEL_FORMAT_BGR, nullptr};
         if (cap.isOpened())
         {
             cv::Mat frame;
             cap >> frame;
             if (!frame.empty())
             {
                 image.width = frame.cols;
                 image.height = frame.rows;
                 image.stride = frame.step;
                 image.pixel_format = frame.channels() == 3 ? PIXEL_FORMAT_BGR : PIXEL_FORMAT_GRAY;
                 size_t data_size = frame.total() * frame.elemSize();
                 image.data = new uint8_t[data_size];
                 std::memcpy(image.data, frame.data, data_size);
             }
         }
         return image;
     }
        
     void release_frame(ImageData *image)
     {
         if (image->data)
         {
             delete[] image->data;
             image->data = nullptr;
         }
     }
        
     void display_frame(const char *name, const ImageData *image)
     {
         if (image && image->data)
         {
             int type = image->pixel_format == PIXEL_FORMAT_BGR ? CV_8UC3 : CV_8UC1;
             cv::Mat frame(image->height, image->width, type, image->data, image->stride);
             cv::imshow(name, frame);
         }
     }
        
     void draw_line(ImageData *image, int x1, int y1, int x2, int y2, int thickness, int r, int g, int b)
     {
         if (image && image->data)
         {
             int type = image->pixel_format == PIXEL_FORMAT_BGR ? CV_8UC3 : CV_8UC1;
             cv::Mat frame(image->height, image->width, type, image->data, image->stride);
             cv::line(frame, cv::Point(x1, y1), cv::Point(x2, y2), cv::Scalar(b, g, r), thickness);
             std::memcpy(image->data, frame.data, frame.total() * frame.elemSize());
         }
     }
        
     void draw_text(ImageData *image, const char *text, int x, int y, int font_scale, int thickness, int r, int g, int b)
     {
         if (image && image->data)
         {
             int type = image->pixel_format == PIXEL_FORMAT_BGR ? CV_8UC3 : CV_8UC1;
             cv::Mat frame(image->height, image->width, type, image->data, image->stride);
             cv::putText(frame, text, cv::Point(x, y), cv::FONT_HERSHEY_SIMPLEX, font_scale, cv::Scalar(b, g, r), thickness);
             std::memcpy(image->data, frame.data, frame.total() * frame.elemSize());
         }
     }
        
     int wait_key(int delay)
     {
         return cv::waitKey(delay);
     }
        
    

Desktop Barcode Scanner Application

In the example/main.cpp file, follow these steps to complete the barcode scanner application:

  1. Include the necessary headers and define a BarcodeResult struct to store barcode decoding results:

     #include <iostream>
     #include <vector>
     #include <mutex>
     #include "camera_lib.h"
        
     #include "DynamsoftCaptureVisionRouter.h"
     #include "DynamsoftUtility.h"
        
     using namespace dynamsoft::license;
     using namespace dynamsoft::cvr;
     using namespace dynamsoft::dbr;
     using namespace dynamsoft::utility;
     using namespace dynamsoft::basic_structures;
        
     struct BarcodeResult
     {
         std::string type;
         std::string value;
         int x1, y1, x2, y2, x3, y3, x4, y4;
         int frameId;
     };
    
  2. Set the license key for the Dynamsoft Barcode SDK in the main function:

     int main()
     {
         if (open_camera(0) == 0)
         {
             std::cout << "Camera opened successfully." << std::endl;
        
             int iRet = -1;
             char szErrorMsg[256];
             iRet = CLicenseManager::InitLicense("LICENSE-KEY", szErrorMsg, 256);
             if (iRet != EC_OK)
             {
                 std::cout << szErrorMsg << std::endl;
             }
        
             ...
         }
         return 0;
     }
    
  3. Instantiate CCaptureVisionRouter and CImageSourceAdapter objects for buffering and processing video frames from the camera:

     class MyVideoFetcher : public CImageSourceAdapter
     {
     public:
         MyVideoFetcher(){};
         ~MyVideoFetcher(){};
         bool HasNextImageToFetch() const override
         {
             return true;
         }
         void MyAddImageToBuffer(const CImageData *img, bool bClone = true)
         {
             AddImageToBuffer(img, bClone);
         }
     };
    
     ...
     CCaptureVisionRouter *cvr = new CCaptureVisionRouter;
        
     MyVideoFetcher *fetcher = new MyVideoFetcher();
     fetcher->SetMaxImageCount(4);
     fetcher->SetBufferOverflowProtectionMode(BOPM_UPDATE);
     fetcher->SetColourChannelUsageType(CCUT_AUTO);
     cvr->SetInput(fetcher);
     ...
    
  4. Implement the OnDecodedBarcodesReceived callback function to receive barcode decoding results:

     class MyCapturedResultReceiver : public CCapturedResultReceiver
     {
         virtual void OnDecodedBarcodesReceived(CDecodedBarcodesResult *pResult) override
         {
             std::lock_guard<std::mutex> lock(barcodeResultsMutex);
        
             if (pResult->GetErrorCode() != EC_OK)
             {
                 std::cout << "Error: " << pResult->GetErrorString() << std::endl;
             }
             else
             {
                 auto tag = pResult->GetOriginalImageTag();
                 if (tag)
                     std::cout << "ImageID:" << tag->GetImageId() << std::endl;
                 int count = pResult->GetItemsCount();
                 std::cout << "Decoded " << count << " barcodes" << std::endl;
        
                 barcodeResults.clear();
                 for (int i = 0; i < count; i++)
                 {
                     const CBarcodeResultItem *barcodeResultItem = pResult->GetItem(i);
                     if (barcodeResultItem != NULL)
                     {
                         std::cout << "Result " << i + 1 << std::endl;
                         std::cout << "Barcode Format: " << barcodeResultItem->GetFormatString() << std::endl;
                         std::cout << "Barcode Text: " << barcodeResultItem->GetText() << std::endl;
                         CPoint *points = barcodeResultItem->GetLocation().points;
        
                         BarcodeResult result;
                         result.type = barcodeResultItem->GetFormatString();
                         result.value = barcodeResultItem->GetText();
                         result.frameId = tag->GetImageId();
                         result.x1 = points[0][0];
                         result.y1 = points[0][1];
                         result.x2 = points[1][0];
                         result.y2 = points[1][1];
                         result.x3 = points[2][0];
                         result.y3 = points[2][1];
                         result.x4 = points[3][0];
                         result.y4 = points[3][1];
        
                         barcodeResults.push_back(result);
                     }
                 }
             }
        
             std::cout << std::endl;
         }
     };
    
     ...
     CCapturedResultReceiver *capturedReceiver = new MyCapturedResultReceiver;
     cvr->AddResultReceiver(capturedReceiver);
    
     errorCode = cvr->StartCapturing(CPresetTemplate::PT_READ_BARCODES, false, errorMsg, 512);
     ...
    
  5. Create an infinite loop to continuously append video frames to the fetcher object, and then draw the available barcode results on the video frames:

     for (int i = 1;; ++i)
     {
         ImageData frame = get_frame();
         if (frame.data)
         {
             CFileImageTag tag(nullptr, 0, 0);
             tag.SetImageId(i);
             CImageData data(frame.height * frame.stride,
                             frame.data,
                             frame.width,
                             frame.height,
                             frame.stride,
                             IPF_RGB_888,
                             0,
                             &tag);
             fetcher->MyAddImageToBuffer(&data);
    
             {
                 std::lock_guard<std::mutex> lock(barcodeResultsMutex);
                 for (const auto &result : barcodeResults)
                 {
                     // Draw the bounding box
                     draw_line(&frame, result.x1, result.y1, result.x2, result.y2, 2, 0, 255, 0);
                     draw_line(&frame, result.x2, result.y2, result.x3, result.y3, 2, 0, 255, 0);
                     draw_line(&frame, result.x3, result.y3, result.x4, result.y4, 2, 0, 255, 0);
                     draw_line(&frame, result.x4, result.y4, result.x1, result.y1, 2, 0, 255, 0);
    
                     // Draw the barcode type and value
                     std::string text = result.type + ": " + result.value;
                     draw_text(&frame, text.c_str(), result.x1, result.y1 - 10, 1, 2, 0, 255, 0);
                 }
             }
    
             display_frame("1D/2D Barcode Scanner", &frame);
             if (wait_key(30) >= 0)
             { // Add a delay and check for key press
                 release_frame(&frame);
                 break; // Exit the loop if any key is pressed
             }
             release_frame(&frame);
         }
     }
    
  6. Build the project using CMake:

     mkdir build
     cd build
     cmake ..
     cmake --build . --config Release
    

    desktop camera cpp barcode scanner

Source Code

https://github.com/yushulx/cmake-cpp-barcode-qrcode/tree/main/camera_lib