バーコードを読み取る
書き始めたのは 2020/12/28 ですが、この記事は OPENLOGI Advent Calendar 2020 20 日目の記事です。
バーコードから機材を知る - Qiita の続きでもあります。
バーコードを読み取る、というとバーコード画像から中身のデータを取得するまでの一覧の流れをなぞりたいところです。絵で描くとこういう流れを想定します。
これは PlantUML で描きました。2020 年はこれで図をたくさん描きました。来年もたくさん描くと思います。
図を見て分かるように、バーコードで R(ead), E(val), P(rint) をしたいと思います。 REPL を実装したかったですが L(oop) は実装できなかったです。現実世界の画を取り込んでアプケーション内で白黒の模様を読み取るのはハードルが高かったです。バーコードスキャナを安価で買えることに感謝しました。
この記事ではカメラから画像を読み取って白黒の線をビット列に変換できた、という体からスタートします。バーコードとして印刷された白黒の線からバーコードの種類を特定し、バーコードのデータを読み取り、読み取った内容と全く同じバーコードを印刷すること、そして入力に使ったバーコードと出力したバーコードが同じであれば、バーコードを正しく読み取れたと判断します。
作ったもの
Rust を書いたことがなかったので Hello World を兼ねて作ってみました。ほぼ同じことをする Perl 版の実装を作ってから Rust 版を作りました。
いくつかの処理は抜粋しますが、オリジナルは Gist で確認できます。比較のために CODE39 のバーコードも扱っています。
Read
実際の画像にはバーコード以外にも様々なものが映ります。そのせいで画像を読み取ることの敷居はそれなりに高いです。今回は検証として、バーコードだけが入った画像ファイルを扱います。
実はそのフォーマットは前回の記事でチラ見せしてました。ASCII 文字で表示した「1」と「0」というデータ、これは「1」を黒線として、「0」を白線として見えますよね。この解釈を元にした汎用画像フォーマット PBM を使います。
では NW7 のバーコードを書いてみましょう。条件は前回の記事にならって以下のものを考えます。
- 細エレメント幅を 12
- 太エレメント幅を 24
- スタートキャラクタ「A」
- ストップキャラクタ「A」
- データ「12」
- キャラクタ間ギャップ幅 = 細エレメント幅
- 最小クワイエットゾーンを付ける
この条件で「0」と「1」の見分けを付け易いように「_」で要素ごとを区切ってみます。これが今回の記事で扱うオリジナルの画像データです。ビット列っぽく見えてきましたね。
000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000111111111111111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000000000000000111111111111000000000000111111111111111111111111_000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000
この画像データはバーコードとしてスキャンできるのか?PNG に変換してスマホやバーコードスキャナを使ってスキャンしてみましょう。
やること
- 「_」を抜く
- PBM フォーマットのヘッダを付ける
- データを複数行にコピー
- PBM フォーマットを PNG フォーマットに変換
pnmtopng <(echo 000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000111111111111111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000000000000000111111111111000000000000111111111111111111111111_000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000 \
| perl -nle 's/_//g; printf qq(P1\n%d %d\n), length $_, 50; for $i (1 .. 50){print}; print length $_' \
)> nw7-A12A.png
このコマンドで作った画像が↓です。「12」というデータがスキャンできると思います。
今回は細エレメントの幅が 12 ドットですが、カメラ性能などによって幅は変わってくるでしょう。ただし今回は実装を単純化するため、細エレメントの幅を 1 として各エレメント幅は細エレメントを基準とした比で扱います。
オリジナルのデータ部分から 1 行だけ抜き出して、細エレメント幅を 1 ドットに変換してみましょう。
$ echo 000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000111111111111111111111111000000000000000000000000111111111111_000000000000_111111111111000000000000111111111111000000000000000000000000111111111111000000000000111111111111111111111111_000000000000_111111111111000000000000111111111111111111111111000000000000000000000000111111111111000000000000000000000000111111111111_000000000000 \
| sed -e 's/111111111111/1/g' -e 's/000000000000/0/g'
0_1011001001_0_101011001_0_101001011_0_1011001001_0
だいぶ短かくなりました。これをプログラムで扱う画像とします。Read 完了です。
アプリケーション では extract という関数で実装しました。
fn extract(input: &str) -> (BarcodeType, String) {
lazy_static! {
static ref UNDERSCORE: Regex = Regex::new(r"_").unwrap();
static ref NW7_PATTERN: String = format!(
r"(?x)
{}
(.+) # body
{}",
NW7_START_A_WITH_QUIET, NW7_END_A_WITH_GAP_AND_QUIET
);
static ref CODE39_PATTERN: String = format!(
r"(?x)
{}
(.+) # body
{}",
CODE39_START_ASTERISK_WITH_QUIET, CODE39_END_ASTERISK_WITH_GAP_AND_QUIET
);
static ref NW7: Regex = Regex::new(&NW7_PATTERN).unwrap();
static ref CODE39: Regex = Regex::new(&CODE39_PATTERN).unwrap();
}
let digit = UNDERSCORE.replace_all(input, "");
if NW7.is_match(&digit) {
let caps = NW7.captures(&digit).unwrap();
return (BarcodeType::NW7, (&caps[1]).to_string());
} else if CODE39.is_match(&digit) {
let caps = CODE39.captures(&digit).unwrap();
return (BarcodeType::CODE39, (&caps[1]).to_string());
} else {
panic!("unknown input.");
}
}
Eval
「0」と「1」が読み取れたら NW7 として読み取ってみましょう。
NW7 は最初にスタートキャラクタ、最後にストップキャラクタがあります。話を簡単にするためにスタートキャラクタもストップキャラクタも「A」に限定してみましょう。
クワイエットゾーンは「0」、キャラクタ間ギャップも「0」。残るデータは「101011001」と「101001011」です。それぞれ「1」と「2」です。
白黒の模様からデータが取り出せました。
アプリケーション では parse という関数で実装しました。
fn parse(barcode_type: &BarcodeType, body: &str) -> Vec<char> {
let mut result: Vec<char> = vec![];
let mut index = 0;
match barcode_type {
BarcodeType::NW7 => loop {
if index >= body.len() {
break;
}
let gap = &body[index..index + 1];
if gap == "1" {
panic!("no gap. i:{}", index);
}
let character = &body[index + 1..index + 1 + NW7_DIGIT];
if !NW7_SYMBOL_TABLE.contains_key(character) {
panic!("no symbol. character:{}", character);
}
let value = NW7_SYMBOL_TABLE[character];
result.push(value);
index += NW7_DIGIT + 1;
},
BarcodeType::CODE39 => loop {
if index >= body.len() {
break;
}
let gap = &body[index..index + 1];
if gap == "1" {
panic!("no gap. i:{}", index);
}
let character = &body[index + 1..index + 1 + CODE39_DIGIT];
if !CODE39_SYMBOL_TABLE.contains_key(character) {
panic!("no symbol. character:{}", character);
}
let value = CODE39_SYMBOL_TABLE[character];
result.push(value);
index += CODE39_DIGIT + 1;
},
};
return result;
}
読み取ったバーコードの解釈が正しければ、同じバーコードを作れるはずです。プログラムの入力に使ったバーコードと同じバーコードを出力してみて、見比べてみましょう。
バーコードの出力にはページ記述言語に ESC/POS が使えるラベルプリンタを用意しました。
PNG の画像と実ラベルが同じデータを持っていればバーコードのビット列を読み取れたと言えるでしょう。
アプリケーション では main 関数の中の print で実装しました。
print!(
"{ESC}w\x03{ESC}h\x32{ESC}f\x00{ESC}H\x00{ESC}{}{NULL}{LF}",
match barcode_type {
BarcodeType::NW7 => format!("k\x06A{}A", value),
BarcodeType::CODE39 => format!("k\x04{}", value),
},
ESC = ESC,
NULL = NULL,
LF = LF
);
使い方
バーコードのビット列を標準入力で受け取ります。データは「1」と「2」だけ読み取れて「_」はスキップします。NW7 と CODE39 の読み取りと印刷が可能です。
# NW7
echo 0_1011001001_0_101011001_0_101001011_0_10110010010 \
| cargo run > /dev/rfcomm0
# CODE39
echo 0_100010111011101_0_111010001010111_0_101110001010111_0_100010111011101_0 \
| cargo run > /dev/rfcomm0
これを実行したときに印刷したラベルがこれです。
PBM 形式から作った画像(再掲)と NW7 のバーコードを見比べると同じデータになってますね。
この実装では一方的にラベルプリンタに PDL を渡すだけなので Bluetooth 接続でも USB 接続でも同じ印刷結果を得られました。ただし Linux での Bluetooth 接続はペアリング後にデバイスファイルを作っておく必要がありました。
Linux のデバイスファイル例
- Bluetooth: /dev/rfcomm0
- USB: /dev/usb/lp0
Mac は Blutooth でペアリングするとデバイスファイルの作成までしてくれました。USB 接続した場合に簡単にデータを投げる方法は分からなかったです。
Mac のデバイスファイル例
- Bluetooth: /dev/tty.BlueToothPrinter-HS_SPP
- USB: ?
反省
クリスマス前までに REPL の L (Loop) をしたかったです。でも Rust を書けたのでもう満足です。