Distributed Barcode Reading and Inter-Process Communication with ZeroMQ

ZeroMQ is a high-performance asynchronous messaging library, aimed at use in distributed or concurrent applications. It supports common messaging patterns (pub/sub, request/reply, client/server and others) over a variety of transports (TCP, in-process, inter-process, multicast, WebSocket and more), making inter-process messaging as simple as inter-thread messaging. 1

ZeroMQ can be used in barcode decoding, as well. First, using its request/reply pattern, we can do cross-language programming. For example, we can create a decoding program using the C++ library of Dynamsoft Barcode Reader (DBR) and call it in Python. Second, using its push/pull pattern, we can create a distributed barcode reading program which processes more than 25 images per second.

In this article, we are going to create two demo apps using the request/reply pattern and the push/pull pattern.

Credits to the pyzmq docs.

Inter-Process Communication for Cross-Language Programming

Two processes written in two different languages can communicate with each other using ZeroMQ. The request/reply pattern of ZeroMQ is suitable for this scenario.

In a client/server model, the client sends a request and the server replies to the request. The request/reply pattern of ZeroMQ is much the same.2 We can use this pattern to create a decoding program in C++ and a calling program in Python.

Create a C++ program which replies to decoding requests

  1. Create a C++ project and include the Dynamsoft Barcode Reader SDK, ZeroMQ and jsoncpp by following their guides: DBR, cppzmq, jsoncpp.

  2. Create a ZMQ Socket in REP pattern.

     zmq::context_t zmq_context(1);
     zmq::socket_t zmq_socket(zmq_context, ZMQ_REP);
    
  3. Bind REP socket to tcp://*:6666.

     zmq_socket.bind("tcp://*:6666");
    
  4. If it receives a message which contains the path of an image file, then read the file and reply with the decoding results in JSON.

     while (true)
     {
         zmq::message_t recv_msg;
         zmq_socket.recv(recv_msg, zmq::recv_flags::none);
         std::string msg = recv_msg.to_string();
         struct stat buffer;
         if (stat(msg.c_str(), &buffer) == 0) { //if file exists
             reader.DecodeFile(msg.c_str(), ""); //reader is an instance of Dynamsoft Barcode Reader
             TextResultArray* results = NULL;
             reader.GetAllTextResults(&results);
             std::string s = "";
             Json::Value rootValue = Json::objectValue;
             rootValue["results"] = Json::arrayValue;
             for (int iIndex = 0; iIndex < results->resultsCount; iIndex++)
             {
                 PTextResult tr = results->results[iIndex];
                 Json::Value result = Json::objectValue;
                 result["barcodeText"] = tr->barcodeText;
                 result["barcodeFormat"] = tr->barcodeFormatString;
                 result["confidence"] = tr->results[0]->confidence;
                 result["x1"] = tr->localizationResult->x1;
                 result["x2"] = tr->localizationResult->x2;
                 result["x3"] = tr->localizationResult->x3;
                 result["x4"] = tr->localizationResult->x4;
                 result["y1"] = tr->localizationResult->y1;
                 result["y2"] = tr->localizationResult->y2;
                 result["y3"] = tr->localizationResult->y3;
                 result["y4"] = tr->localizationResult->y4;
                 rootValue["results"].append(result);
             }
             Json::StreamWriterBuilder builder;
             std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
             std::ostringstream os;
             writer->write(rootValue, &os);
             s = os.str();
             zmq::message_t response;
             response.rebuild(s.c_str(), s.length());
             zmq_socket.send(response, zmq::send_flags::dontwait);
             CBarcodeReader::FreeTextResults(&results);
         else {
             zmq_socket.send(zmq::str_buffer("Received"), zmq::send_flags::dontwait);
         }
    

Create a Python program which makes decoding requests

  1. Install the Python bindings of ZeroMQ.

     pip install pyzmq
    
  2. Create a ZMQ Socket in REQ pattern.

     import zmq
     context = zmq.Context()
     socket = context.socket(zmq.REQ)
    
  3. Connect to the C++ program:

     socket.connect("tcp://localhost:6666")
    
  4. Get the image path from user input, send the path, wait for the decoding to be done and receive the decoding results in JSON.

     while True:
         print("Input image path: ")
         img_path = input()
         socket.send(bytes(img_path,"utf-8"))
         message = socket.recv()
         print(message.decode("utf-8"))
    

Distributed Barcode Reading

The push/pull pattern lets us distribute messages to multiple workers, arranged in a pipeline. A push socket will distribute sent messages to its pull clients evenly. This is equivalent to the producer/consumer model but the results computed by the consumer are not sent upstream but downstream to another pull/consumer socket (result collector). 3

Push and Pull

This pattern can be used for the distributed barcode reader. In the following part, we are going to create a Python app which can have several consumers to increase the barcode reading performance.

Producer

  1. Create a ZMQ Socket in PUSH pattern.

     context = zmq.Context()
     zmq_socket = context.socket(zmq.PUSH)
    
  2. Bind it to tcp://*:5557.

     # Start your result manager and workers before you start your producers
     zmq_socket.bind("tcp://*:5557")
    
  3. Load image files of a folder to a list.

     def load_files_list(img_folder):
         files_list = []
         for filename in os.listdir(img_folder):
             name, ext = os.path.splitext(filename)
             if ext.lower() in ('.png','.jpg','.jpeg','.bmp','.tif', '.tiff','.pdf'):
                 files_list.append(filename)
         return files_list
    
  4. Dispatch decoding messages. The message only contains the HTTP URL of the file, since directly sending the content of files takes much memory.

     for filename in files_list:
         url = get_file_url(filename)
         work_message = { 'url': url}
         time.sleep(0.01) # sleep so that messages won't be sent to only one idle consumer
         zmq_socket.send_json(work_message)
            
    

    The get_file_url method:

     def get_url(img_path):
         ip = "192.168.191.1"
         port = 5111
         url = "http://"+str(ip)+":"+str(port)+"/image/"+urllib.parse.quote(img_path)
         return url
    

    An HTTP server is required to serve images. Here, we use Flask to create one:

     #coding=utf-8
     from flask import Flask, send_file
    
     app = Flask(__name__, static_url_path='/', static_folder='static')
     port = 5111
    
     @app.route('/image/<img_path>')
     def get_image(img_path):
         print(img_path)
         return send_file(img_path)
    
     if __name__ == '__main__':
         app.run(host='0.0.0.0',port=port)
    

Consumer

  1. Create a ZMQ Socket in PULL pattern and a ZMQ socket in PUSH pattern. Connect the receiver to the producer and the sender to the result collector.

     ip = 127.0.0.1
     consumer_id = random.randrange(1,10005) #Create an ID for the consumer
     context = zmq.Context()
     consumer_receiver = context.socket(zmq.PULL)
     consumer_receiver.connect("tcp://"+ip+":5557")
     consumer_sender = context.socket(zmq.PUSH)
     consumer_sender.connect("tcp://"+ip+":5558")
    
  2. Wait for decoding messages from the producer, decode files and send decoding results to the result collector.

     while True:
         work = consumer_receiver.recv_json()
         url = work['url']
         reading_result=reader.decode_file_stream(requests.get(url).content) # The requests library is used to download images
         result = { 'consumer' : consumer_id, 'reading_result' : reading_result, 'session_id': work['session_id'], 'url': url}
         print(result)
         consumer_sender.send_json(result)
    
  3. A DynamsoftBarcodeReader class is created to read file bytes using DBR.

     from dbr import *
     import os
     import base64
    
     class DynamsoftBarcodeReader():
         def __init__(self):
             self.dbr = BarcodeReader()
             self.dbr.init_license("<license>")
             if os.path.exists("template.json"):
                 print("Found template")
                 self.dbr.init_runtime_settings_with_file("template.json")
    
         def set_runtime_settings_with_template(self, template):
             self.dbr.init_runtime_settings_with_string(template, conflict_mode=EnumConflictMode.CM_OVERWRITE)
                
         def decode_file_stream(self, image_bytes):
             result_dict = {}
             results = []
                
             text_results = self.dbr.decode_file_stream(bytearray(image_bytes))
             if text_results!=None:
                 for tr in text_results:
                     result = {}
                     result["barcodeFormat"] = tr.barcode_format_string
                     result["barcodeFormat_2"] = tr.barcode_format_string_2
                     result["barcodeText"] = tr.barcode_text
                     result["barcodeBytes"] = str(base64.b64encode(tr.barcode_bytes))[2:-1]
                     result["confidence"] = tr.extended_results[0].confidence
                     results.append(result)
                     points = tr.localization_result.localization_points
                     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]
             result_dict["results"] = results
                
             return result_dict
    

Result Collector

  1. Create a ZMQ Socket in PULL pattern.

     context = zmq.Context()
     results_receiver = context.socket(zmq.PULL)
    
  2. Bind it to tcp://*:5558.

     results_receiver.bind("tcp://*:5558")
    
  3. Receive decoding results from the consumer.

     collector_data = {}
     while True:
         result = results_receiver.recv_json()
         if result['consumer'] in collector_data:
             collector_data[result['consumer']] = collector_data[result['consumer']] + 1
         else:
             collector_data[result['consumer']] = 1
         print(collector_data)
    

Execute the programs on separate shells as all programs have a while loop that we will discard later:

python http_server.py
python result_collector.py
python consumer.py
python consumer.py
python producer.py

The result shows the distribution of transmitted results to result collector:

{3362: 122, 9312: 93}

Performance Test

To evaluate the performance, we use elapsed time. The started time of decoding and the number of files are passed to the result collector so that we can calculate the time.

if result['size']==decoded:
    elapsedTime = int(time.time())*1000 - result['start_time']
    print("Done in "+str(elapsedTime))

An EAN13 barcode dataset containing 215 images is used.

Here are the test results of different numbers of consumers running on a Windows 10 PC with an i5-10400 CPU:

  1. One consumer: 13.439s
  2. Two consumers: 9.343s. Distribution: 122, 93
  3. Three consumers: 7.706s. Distribution: 74, 71, 70
  4. Four consumers: 6.744. Distribution: 59, 52, 53, 51
  5. Five consumers: 6.234. Distribution: 47, 45, 45, 40, 38
  6. Six consumers: 5.605 Distribution: 39, 37, 35, 35, 33, 36

We can see that by using the push/pull pattern with several consumers, the barcode reading performance can be greatly improved.

Android device as a consumer

The consumer can also run on embedded devices like Raspberry Pi and mobile phones.

Dynamsoft Barcode Reader has an Android library and ZeroMQ has a pure Java implementation named JeroMQ, so it is easy to create an Android app which works as a consumer.

Screenshot of the Android app:

Android

A performance test on the same dataset mentioned above is done using two Android devices. One is equipped with Snapdragon 630 and the other is equipped with Unisoc T610.

  1. On Snapdragon 630: 126.275s
  2. On Unisoc T610: 112.702s
  3. On both of them: 74.225s

However, if we use the PC and the Android devices together, it will decrease the performance since there is a performance gap between them and ZeroMQ will try to distribute messages evenly. So the consumers should have similar computing power.

Source Code

https://github.com/xulihang/Distributed-Barcode-Reading

References