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
-
Create a C++ project and include the Dynamsoft Barcode Reader SDK, ZeroMQ and jsoncpp by following their guides: DBR, cppzmq, jsoncpp.
-
Create a ZMQ Socket in
REP
pattern.zmq::context_t zmq_context(1); zmq::socket_t zmq_socket(zmq_context, ZMQ_REP);
-
Bind REP socket to
tcp://*:6666
.zmq_socket.bind("tcp://*:6666");
-
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
-
Install the Python bindings of ZeroMQ.
pip install pyzmq
-
Create a ZMQ Socket in
REQ
pattern.import zmq context = zmq.Context() socket = context.socket(zmq.REQ)
-
Connect to the C++ program:
socket.connect("tcp://localhost:6666")
-
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
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
-
Create a ZMQ Socket in
PUSH
pattern.context = zmq.Context() zmq_socket = context.socket(zmq.PUSH)
-
Bind it to
tcp://*:5557
.# Start your result manager and workers before you start your producers zmq_socket.bind("tcp://*:5557")
-
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
-
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
-
Create a ZMQ Socket in
PULL
pattern and a ZMQ socket inPUSH
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")
-
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)
-
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
-
Create a ZMQ Socket in
PULL
pattern.context = zmq.Context() results_receiver = context.socket(zmq.PULL)
-
Bind it to
tcp://*:5558
.results_receiver.bind("tcp://*:5558")
-
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:
- One consumer: 13.439s
- Two consumers: 9.343s. Distribution: 122, 93
- Three consumers: 7.706s. Distribution: 74, 71, 70
- Four consumers: 6.744. Distribution: 59, 52, 53, 51
- Five consumers: 6.234. Distribution: 47, 45, 45, 40, 38
- 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:
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.
- On Snapdragon 630: 126.275s
- On Unisoc T610: 112.702s
- 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