ทำ QR Code Decoder

ในตอนนี้จะว่าด้วยเรื่องการสร้าง QR code Decoder ที่ใช้งานได้บน Raspberry Pi 3 Model B โดยใช้ Camera Module ทำหน้าที่เป็น Reader และใช้ Library ที่ชื่อ pyzbar


ขั้นตอนการทำงาน



เริ่มต้นจากการใช้ Raspberry Pi camera จับภาพ QR code เปลี่ยนข้อมูลภาพให้กลายเป็น Numpy ndarray [1] ส่งต่อไปยัง pyzbar module เพื่อทำการ decode ต่อไป

เหตุผลที่ใช้ Numpy ndarray แทนที่จะใช้เป็น image file  (กล้องจับภาพแล้วบันทึกเป็นแฟ้มภาพก่อน) ก็เพราะผมเห็นว่าข้อมูลภาพที่ได้ไม่ได้มีขนาดใหญ่โต อีกทั้งเป็นภาพขาว-ดำ ซึ่งใช้พื้นที่ในหน่วยความจำไม่มาก และเป็นลดภาระงานการเขียน-อ่านไฟล์อีกด้วย

รู้จัก pyzbar Data Fields
ข้อมูลที่ได้จากการ decode โดย pyzbar ที่สำคัญมี 3 fields คือ (ใน source code จะมี 4 fields)
1. data : เป็นข้อมูลแบบข้อความที่บรรจุอยู่ใน QR code
2. type : ชนิดของ code ที่อ่านได้ รุ่นปัจจุบันอ่านได้เฉพาะ QR code
3. polygon : คือ location หรือ จุดที่ระบุมุมทั้งสี่ของ QR Code ในระพิกัด (x,y coordinate)
4. rect :  ให้ความหมายเช่นเดียวกับ polygon แต่อยู่ในรูปแบบของ tuple (left, top, width, height) ซึ่งง่ายต่อการนำไปใช้วาดกรอบด้วย PyQt


1. ติดตั้ง Modules ใช้งาน
1.1 ติดตั้ง Pillow  (PIL) เป็น Python Image Library

$ sudo python3 -m  pip intalll -U Pillow


1.2 ติดตั้ง pyzbar
1.2.1 ติดตั้ง dependencies

$ sudo apt-get update
$ sudo apt-get install libzbar0 libzbar-dev


1.2.2 ติดตั้ง pyzbar [3]

$ sudo python3 -m  pip intalll  pyzbar


2. ติดตั้ง picamera 
การจะใช้งาน Camera Module ต้องได้รับการติดตั้งและทำการ enable camera module เสียก่อน [2]

3. ติดตั้ง PyQt5

$ sudo apt-get python3-pyqt5



Full Source Code [4]

import sys
import numpy as np

from PyQt5 import QtCore
from PyQt5 import QtWidgets
from PyQt5 import QtGui

from picamera import PiCamera
from pyzbar.pyzbar import decode

class VideoRecorder(QtCore.QObject):
 # To take a snapshot one at a time then emit out in term on numpy ndarray
 image_data = QtCore.pyqtSignal(np.ndarray)
 
 def __init__(self, camera_port=0, parent=None):
  super().__init__(parent)
  self.resolution = (320,240)  
  
  self.camera = PiCamera() 
  self.camera.resolution=self.resolution

  self.timer = QtCore.QBasicTimer()

 def start_recording(self):
  self.timer.start(0, self)

 def timerEvent(self, event):
  if (event.timerId() != self.timer.timerId()):
   return
  
  frame = np.empty((self.resolution[1],self.resolution[0],3),dtype=np.uint8) 
  #capture image

  self.camera.capture(frame,'rgb')
  #emit image out 
  self.image_data.emit(frame)


class DecoderWidget(QtWidgets.QWidget):
 #To display image array and do decoding
 
 def __init__(self,  parent=None):
  super().__init__(parent)
  self.qr_data = []
  self.image = QtGui.QImage()
  self._red = (0, 0, 255)
  self._width = 2
  self._min_size = (30, 30)

 def _decode(self):
  if self.qr_data != [] :
   msg = QtWidgets.QMessageBox()
   msg.setIcon(QtWidgets.QMessageBox.Information)
   msg.setWindowTitle("QR Code Data ")
   msg.setDetailedText(self.qr_data[0].data.decode('utf-8'))
   retval = msg.exec_()
  
  
 
 def image_data_slot(self, image_data: np.ndarray):
  self.qr_data = decode(image_data)
  self.image = self.get_qimage(image_data)
  if self.image.size() != self.size():
   self.setFixedSize(self.image.size())

  self.update()

 def get_qimage(self, image: np.ndarray):
  height, width, colors = image.shape
  bytesPerLine = 3 * width
  QImage = QtGui.QImage
  image = QImage(image.data, width, height, bytesPerLine, QImage.Format_RGB888)
  image = image.rgbSwapped()
  return image

 def paintEvent(self, event):
  painter = QtGui.QPainter(self)
  painter.drawImage(0, 0, self.image)
  self.image = QtGui.QImage()
  
  if self.qr_data != [] :
   col = QtGui.QColor(200, 0, 0)
   col.setNamedColor('#d4d4d4')
   painter.setPen(col)
  
   #painter.setBrush(QtGui.QColor(200, 0, 0))
   rect = self.qr_data[0].rect
   painter.drawRect(rect.left, rect.top, rect.width, rect.height)
  
  

class MainWidget(QtWidgets.QWidget):
 def __init__(self,  parent=None):
  super().__init__(parent)
  self.decoder_widget = DecoderWidget()

  self.recorder = VideoRecorder()
  
  image_data_slot = self.decoder_widget.image_data_slot
  
  self.recorder.image_data.connect(image_data_slot)
  
  layout = QtWidgets.QVBoxLayout()

  layout.addWidget(self.decoder_widget)
  self.run_button = QtWidgets.QPushButton('Decode')
  layout.addWidget(self.run_button)

  self.run_button.clicked.connect(self.decoder_widget._decode)
  self.setLayout(layout)
  self.recorder.start_recording()
  
def main():
 app = QtWidgets.QApplication(sys.argv)

 main_window = QtWidgets.QMainWindow()
 main_widget = MainWidget()
 main_window.setCentralWidget(main_widget)
 main_window.show()
 sys.exit(app.exec_())

if __name__ == '__main__':
    main()      

ผลการทำงาน

โปรแกรมตรวจหาตำแหน่งของ QR Code ในภาพที่ได้จากกล้อง กรอบสีขาวแสดงว่าโปรแกรมได้ตรวจพบ QRCode แล้ว

คลิ๊กปุ่ม "Decode" เพื่อทำการดึงข้อมูลออกจาก QR Code ในภาพแสดงข้อความที่อยู่ใน QR Code


โปรแกรมนี้ได้สร้างขึ้นมาเพื่อใช้เป็นตัวอย่างเพื่อให้เข้าใจการทำงานเท่านั้น ยังมีข้อบกพร่องอยู่ ทั้งนี้ท่านผู้หลังจากทำความเข้าใจก็จะสามารถนำไปปรับปรุงพัฒนาต่อได้



เอกสารอ้างอิง
[1] https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.ndarray.html
[2] https://www.raspberrypi.org/documentation/usage/camera/
[3] https://pypi.org/project/pyzbar/
[4] https://github.com/somchaisomph/blogs/blob/master/qrcode/qt5_decoder.py
Previous
Next Post »