長方形DataMatrix(DMRE)の比較照合を実現するまで — バーコードリーダー+Python
製品ラベルに印刷された2つの長方形DataMatrixコードを読み取り、内容が一致するか比較照合する。
出荷前の検品で必要になった作業ですが、既存のツールが見つからず、バーコードリーダーの設定でつまづき、最終的にPythonアプリを自作しました。
スマートフォンで使えるWeb版(Code Compare)も公開しています。
この記事では、バーコードリーダー+Pythonで実現するまでの過程を記録しています。
なお、この記事で「長方形のDataMatrix」と呼んでいるものは、正式にはDMRE(Data Matrix Rectangular Extension)といい、ISO/IEC 21471で規格化されています。
通常のDataMatrixは正方形ですが、DMREはそれを拡張し、8×48、20×36、22×48モジュールなどの長方形フォーマットを追加した規格です。
ISO/IEC 16022(通常のDataMatrix規格)にも一部の長方形サイズは含まれていますが、DMREではさらに多くのサイズが定義されています。
そもそも比較できるソフトが無い
最初にぶつかったのは、DMREを2つ比較照合できるソフトが見つからないという問題でした。
スマートフォンのアプリをいくつか試しましたが、長方形のDataMatrixが読めないものが多く、読めたとしても結果をテキスト表示するだけ。
2つのDataMatrixを比較できるアプリは見つかりませんでした。
探し方が悪いのかもしれませんが、かなりニッチな領域だと感じました。
無いなら作るしかないと思い、まずはWebアプリに挑戦しました。
しかし、手元にあったのは古いWebカメラで、解像度が低すぎて1回の読み取りに2分程度かかりました。
解像度の良いカメラを買うぐらいならと、バーコードリーダーを購入することにしました。
使用機材
- バーコードリーダー: Tera HW0002(ワイヤレス)
- PC: Windows11(Python インストール済み)
DMREが読み取れない
HW0002を購入し、バーコードやQRコードを試すと問題なく読めました。
しかし、業務で使うDataMatrixを読み取ろうとすると反応しません。
HW0002の製品紹介にはDataMatrix対応と書いてあったのに、なぜ読めないのか。
調べてみると、DataMatrixには正方形と長方形の2種類が存在することをここで初めて知りました。
Webで正方形のDataMatrixのサンプルを見つけて試すと、それは読めます。
業務で使っているのは長方形のDataMatrix(DMRE)で、これだけが読めないと判明しました。
販売元のTeraに「長方形のDataMatrixが読み取れない」と問い合わせたところ、DMREを有効にするための設定用バーコードを提供してもらいました。
この設定用バーコードをHW0002で読み取るだけで、DMREが有効になります。
同梱の説明書にはこの設定方法の記載がありませんでした(当時の話なので、現在は不明です)。
設定用バーコード
以下が、Teraから提供された設定用バーコードです。
HW0002でこのバーコードを読み取ると、DMREの読み取りが有効になります。
同じ問題にぶつかった方へ: バーコードリーダーでDMREが読めない場合、販売元に問い合わせると設定用バーコードを提供してもらえる可能性があります。
補足: 筆者が動作を確認したのは20×48モジュール(実測 約5mm×12mm)のDMREのみです。他のDMREフォーマットでの動作は未確認です。
QRコードを誤って読んでしまう
DMREが読めるようになった後、次の問題が発生しました。
製品ラベルにはDataMatrixとQRコードが近い位置に印刷されており、DataMatrixを読み取ろうとすると、近くのQRコードの方を読んでしまいます。
QRコードは3隅にファインダーパターン(大きな正方形マーカー)を持つため、一般的にDataMatrixより位置検出が速いとされています。
両方が視界に入ると、QRコードが先に認識されてしまうようです。
HW0002の設定で、QRコードの読み取りを無効化しました。
この設定方法は同梱のマニュアルに記載されています。
業務でQRコードを読む必要がなかったため、DataMatrixだけに反応する設定にすることで誤読がなくなりました。
Excelでの比較を断念
バーコードリーダーで読み取った文字列をExcelに入力し、関数で比較する方法をまず試しました。
当然、カーソルを毎回手で移動するのは手間なので、VBAマクロでセルの移動を自動化しましたが、何回か比較を繰り返していると、なぜかカーソルがどこかに飛んで行方不明になる現象が発生しました。
マクロの作りが悪いのかもしれませんが、「カーソルを特定のセルに確実に留めておく」こと自体がExcelでは面倒だと感じました。
他にも、マウスとバーコードリーダーの持ち替えが毎回発生することや、比較結果(OK/NG)が関数の表示だけで見落としやすいこともあり、Excelで作るのをあきらめました。
Pythonアプリを自作
Excelで感じた問題を解消するために、以下の方針でPythonアプリを作りました。
- バーコードリーダーの入力だけで操作が完結する(マウス不要)
- 1回目と2回目の読み取りを自動で切り替える
- 一致なら「OK」を緑、不一致なら「NG」を赤で、画面いっぱいに表示
- 音でもOK/NGを区別できるようにする(画面を見ていなくてもわかる)
- 1回目の読み取り後に3秒のインターバルを設ける(連続誤読を防止)
インストール
ソースコード(BarcodeCheck_PyQt5.py)
# Windows専用(winsoundを使用)
from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QVBoxLayout,
QLineEdit, QPushButton, QDialog
)
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFont
import sys
import winsound
class BarcodeApp(QWidget):
def __init__(self):
super().__init__()
self.reading_flag = 1 # 1回目 or 2回目
self.flash_timer = QTimer()
self.flash_state = False
self.flash_timer.timeout.connect(self.toggle_flash)
self.interim_timer = QTimer()
self.interim_timer.timeout.connect(self.end_interim)
self.countdown_timer = QTimer()
self.countdown_timer.timeout.connect(self.update_countdown)
self.in_interim = False
self.interim_dialog = None
self.initUI()
def initUI(self):
self.setWindowTitle('Barcode Reader Comparison')
self.showFullScreen()
layout = QVBoxLayout()
layout.setSpacing(5)
# バーコードリーダーからの入力を受け取る隠しフィールド
self.barcode_entry = QLineEdit(self)
self.barcode_entry.setStyleSheet('border: none;')
self.barcode_entry.setFixedSize(1, 1)
self.barcode_entry.returnPressed.connect(self.handle_barcode_entry)
layout.addWidget(self.barcode_entry)
# 1回目の読み取り表示欄
self.label_1 = QLabel('1回目')
self.label_1.setAlignment(Qt.AlignCenter)
self.label_1.setFont(QFont("Helvetica", 48))
layout.addWidget(self.label_1)
self.entry_1 = QLineEdit(self)
self.entry_1.setFont(QFont("Helvetica", 48))
self.entry_1.setAlignment(Qt.AlignCenter)
self.entry_1.setFocusPolicy(Qt.NoFocus)
layout.addWidget(self.entry_1)
# 2回目の読み取り表示欄
self.label_2 = QLabel('2回目')
self.label_2.setAlignment(Qt.AlignCenter)
self.label_2.setFont(QFont("Helvetica", 48))
layout.addWidget(self.label_2)
self.entry_2 = QLineEdit(self)
self.entry_2.setFont(QFont("Helvetica", 48))
self.entry_2.setAlignment(Qt.AlignCenter)
self.entry_2.setFocusPolicy(Qt.NoFocus)
layout.addWidget(self.entry_2)
# 結果表示(画面いっぱいにOK/NG)
self.result_label = QLabel('')
self.result_label.setAlignment(Qt.AlignCenter)
self.result_label.setFont(QFont("Helvetica", 500))
self.result_label.setStyleSheet("font-weight: bold;")
layout.addWidget(self.result_label)
# 終了ボタン
self.exit_button = QPushButton('Exit', self)
self.exit_button.setFont(QFont("Helvetica", 24))
self.exit_button.clicked.connect(self.close)
layout.addWidget(self.exit_button)
self.setLayout(layout)
self.barcode_entry.setFocus()
def handle_barcode_entry(self):
if self.in_interim:
return
barcode = self.barcode_entry.text().strip()
if not barcode:
self.barcode_entry.clear()
return
if self.reading_flag == 1:
self.flash_timer.stop() # 前回のNGフラッシュを停止
self.entry_1.setText(barcode)
self.result_label.clear()
self.result_label.setStyleSheet('')
self.entry_2.clear()
self.reading_flag = 2
self.start_interim()
elif self.reading_flag == 2:
self.entry_2.setText(barcode)
self.compare_barcodes()
self.reading_flag = 1
self.barcode_entry.clear()
self.barcode_entry.setFocus()
def start_interim(self):
self.in_interim = True
self.interim_timer.start(3000)
# 前回のダイアログが残っていたら閉じる
if self.interim_dialog is not None:
self.interim_dialog.close()
self.interim_dialog.deleteLater()
self.interim_dialog = QDialog(self)
self.interim_dialog.setWindowTitle("インターバル中")
self.interim_dialog.setFixedSize(900, 150)
dialog_layout = QVBoxLayout()
self.countdown_label = QLabel("3秒後に再開します...", self.interim_dialog)
self.countdown_label.setFont(QFont("Helvetica", 24))
self.countdown_label.setAlignment(Qt.AlignCenter)
dialog_layout.addWidget(self.countdown_label)
self.interim_dialog.setLayout(dialog_layout)
self.interim_dialog.show()
self.countdown_timer.stop()
self.countdown_timer.start(1000)
self.countdown_value = 3
def update_countdown(self):
self.countdown_value -= 1
if self.countdown_value > 0:
self.countdown_label.setText(f"{self.countdown_value}秒後に再開します...")
else:
self.countdown_timer.stop()
def end_interim(self):
self.in_interim = False
self.interim_timer.stop()
if self.interim_dialog is not None:
self.interim_dialog.accept()
def compare_barcodes(self):
barcode1 = self.entry_1.text()
barcode2 = self.entry_2.text()
if barcode1 == barcode2:
self.result_label.setText('OK')
self.result_label.setStyleSheet(
'color: green; background-color: lightgreen; font-weight: bold;')
winsound.Beep(1000, 200)
else:
self.result_label.setText('NG')
self.result_label.setStyleSheet(
'color: red; background-color: pink; font-weight: bold;')
winsound.Beep(500, 500)
self.start_flash()
def start_flash(self):
self.flash_state = True
self.flash_timer.start(500)
def toggle_flash(self):
if self.result_label.text() == 'NG':
if self.flash_state:
self.result_label.setStyleSheet(
'color: red; background-color: yellow; font-weight: bold;')
else:
self.result_label.setStyleSheet(
'color: red; background-color: pink; font-weight: bold;')
self.flash_state = not self.flash_state
else:
self.flash_timer.stop()
def mousePressEvent(self, event):
"""画面クリックでフォーカスを隠しフィールドに戻す"""
self.barcode_entry.setFocus()
super().mousePressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = BarcodeApp()
ex.show()
sys.exit(app.exec_())
コードのポイント
バーコードリーダーの入力をどう受け取るか
バーコードリーダーは、読み取ったデータをキーボード入力として送信し、最後にEnterキーを送ります。
この性質を利用して、1×1ピクセルの見えないテキストフィールドを画面に配置し、常にそこにフォーカスを当てています。
self.barcode_entry.setFixedSize(1, 1) # 見えないサイズ
self.barcode_entry.returnPressed.connect(self.handle_barcode_entry) # Enter = 読み取り完了
表示用フィールドには Qt.NoFocus を設定し、フォーカスが移動しないようにしています。
さらに、画面をクリックしてもフォーカスが隠しフィールドに戻るよう、mousePressEvent をオーバーライドしています。
def mousePressEvent(self, event):
self.barcode_entry.setFocus()
super().mousePressEvent(event)
Excelで苦労した「カーソルが行方不明になる問題」は、この仕組みで根本的に解消しました。
空読み取りの防止
バーコードリーダーがEnterキーだけを送信するケースに備えて、空文字列のチェックを入れています。
barcode = self.barcode_entry.text().strip()
if not barcode:
self.barcode_entry.clear()
return
なぜ3秒のインターバルが必要か
バーコードリーダーの読み取り速度はカメラより圧倒的に速いです。
1回目を読み取った直後に手元の別のコードが視界に入ると、操作者が意図しないタイミングで2回目として読み取ってしまうことがありました。
3秒のインターバルを設け、その間の入力を無視することで、操作者が2つ目のコードに移動する時間を確保しています。
self.interim_timer.start(3000) # 3秒間は入力を無視
OK/NGの表示と音
フォントサイズ500の巨大表示と、音の高低でOK/NGを区別しています。
NG時は背景がピンクと黄色で交互にフラッシュします。
検品作業中は画面をずっと見ているわけではないので、音による通知が重要です。
OKは高い短い音(1000Hz, 200ms)、NGは低い長い音(500Hz, 500ms)で、聞き間違えにくくしています。
PCとバーコードリーダーが用意できない場合や、出先で使いたい場合は、スマートフォンのカメラで比較照合できるWebアプリも公開しています。
カメラでの読み取りはバーコードリーダーに比べて速度が劣りますが、機材が不要で、ブラウザだけで使えます。
大量の比較にはバーコードリーダー+Pythonアプリ、少量や出先での確認にはWeb版、と使い分けるのがおすすめです。