さあ、いよいよ来ました。
「バーコードリーダーって自作できるの?」の巻、です。
正直なところ、最初は「いやいや、そんな簡単にできるわけないでしょ」と思ってたんですよね。
だって街で見るのは専用機械だし、赤いレーザーがピッと出るし、なんとなくハードルが高そうな佇まいじゃないですか。
ところがどっこい。
理屈だけ追いかけてみると、これがびっくりするほどシンプルだったりするわけです。
スマホでも、ノートPCのインカメでも、押し入れに眠っていた古いUSBカメラでも、根っこの原理は全部同じ。前回まででバーコードの「白黒の線」の話をしてきましたが、今回はそれを「実際にどう読むのか」まで踏み込んで覗いてみようと思います。
基本は「画像を白黒で読むだけ」
前回まででお話しした通り、バーコードの原理はおおざっぱに言うとこの2つ。
- 光の反射率(白は反射しやすい・黒は吸収しやすい)
- バーとスペースの幅の組み合わせ
つまり、「白と黒の線がどう並んでいるか」さえ取り出せれば、もう半分以上は勝ったようなもの、というわけです。
…ということは、です。
レーザーである必要は、別になかったりするんですよね。「白黒を見分けられるセンサー」であれば、原理的にはなんでも代用できる。
そう考えると、カメラって要するに「ちょっと高機能な光センサーの集合体」なので、もう完全に役者として揃ってるじゃないか、という話になります(笑)
レーザー式は「ピッと素早く読む」ことに最適化された方式、カメラ式は「画像として広く取って解析する」方式。アプローチが違うだけで、ゴールは同じというのが面白いところ。
ステップ1:カメラで画像取得
というわけで、まずは王道のステップ1から。バーコードをカメラで「パシャっ」と撮ります。
最近のスマホカメラなら、画素数も光学性能も十分すぎるくらい。USBカメラでもVGA(640×480)クラスあれば、JANコード程度なら問題なく読み取れる、と言われています。
ここで一番大事なのは、画素数よりピントだったりするんですよね。
バーコードって細い線の集合体なので、ほんの少しピンボケしただけで線が滲んで、白と黒の境界がふんわりしてしまう。経験的に、読み取り失敗の8割くらいは「ピントが甘い」か「手ブレ」のどちらかだと思っています(笑)
スマホでバーコード決済アプリが妙にカメラを離せって言ってくるのも、最短ピント距離より近すぎると合焦できないから、なんですよね。あれ、画面のUIだけ見てると意味不明ですが、裏側ではちゃんと光学の都合と戦っていたわけです。
ステップ2:グレースケール変換
撮影した画像は、ふつうは「カラー(RGB)」で記録されます。各画素が赤・緑・青の3チャンネル、それぞれ0〜255の値を持っている、いつものやつですね。
ただ、バーコードに関して言えば、色情報は基本いらない。だって「白か、黒か」だけ知りたいわけですから。
そこで最初にやるのがグレースケール変換。
ざっくり言うとこういう処理になります。
赤・緑・青の成分 → 重みをかけて足す → 濃淡(0〜255の灰色レベル)に圧縮
人間の目は緑に敏感なので、よく使われる式では緑の重みが一番大きくなっていたりします。Pythonなら、OpenCVのcv2.cvtColor(img, cv2.COLOR_BGR2GRAY)一行でおしまい。Pillowならimg.convert("L")で終了。便利な時代になったものです。
import cv2
img = cv2.imread("barcode.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
JavaScript側でブラウザでやりたい場合も、CanvasのgetImageDataで各画素のRGBを取って平均を取れば近い処理が書けます。「画像処理ってなんか難しそう」のイメージで尻込みしがちですが、この段階までは正直、足し算と割り算しかしていません(笑)
ステップ3:二値化(閾値処理)
グレースケール画像ができたら、次は「白か黒か、白黒つけようじゃないか」という作業。これを二値化(Binarization)と呼びます。
たとえば「128より明るかったら白、暗かったら黒」みたいに、ある値(しきい値、閾値)を境にスパッと切り分ける。これだけ。
…なんですが、ここがバーコードリーダー自作の地味な山場だったりするんですよね。
写真って、撮るたびに明るさも光の当たり方も違うじゃないですか。蛍光灯の真下で撮るのか、窓際で撮るのか、夜中に布団の中で撮るのかで、画素の値の分布はガラッと変わる。固定で「128で切る!」とやってしまうと、
- 暗い写真では全部黒に倒れる
- 明るい写真では全部白に倒れる
- 影が入った瞬間にバーの幅が崩壊する
…という、なかなか悲しい事態になるわけです。
そこで賢い人たちが考えたのが、「画像を見て、しきい値を自動で決める」アルゴリズム。代表選手が大津の二値化(Otsu’s method)。日本人研究者の大津展之さんが提案した手法と言われていて、ヒストグラムを見て「白グループと黒グループの分離度が最大になる値」を数学的に計算してくれる、という優れもの。
さらに、画像の場所ごとにしきい値を変える適応的二値化(Adaptive Thresholding)もあって、こちらは「右半分は明るく、左半分は暗い」みたいな照明ムラに強い。バーコードを斜めから撮るときには、こっちが結構効いてくれます。
# 大津の二値化(しきい値を自動で決めてくれる)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
名前が中二っぽいのも含めて、ちょっと好きな手法だったりします(←重要)。
ステップ4:線形スキャン
白黒くっきりした画像ができたら、次はいよいよ走査(スキャン)。
バーコードの中央あたりに、横一直線にラインを引いて、左から右へ画素を順番に読んでいくイメージです。
実際の処理はこんな感じ。
黒(5px) → 白(3px) → 黒(7px) → 白(2px) → …
ようするに「黒と白が、それぞれ何ピクセル続いたか」を順番に記録していく。これだけです。
一次元バーコードの世界ではこの「ランレングス」と呼ばれる幅の並びが、まさに生のデータなんですよね。
ここ、地味なんですが、書いていて一番テンションが上がる場所だったりします。配列をforループで舐めて、「色が変わったタイミング」をひたすら拾うだけ。やっていることは超単純なのに、これがあとで意味のある数字に化けるのを知っているので、なんだか暗号解読モノの最初のページを読んでいるような気分になる(笑)
ちなみに実用的には、ノイズ対策で「1本のラインだけじゃなく、上下何本かのラインで取って多数決」とか、「ラインを少し傾けて走査して、バーコードが斜めでも読めるようにする」みたいな工夫が入ってきます。とはいえ、根っこは「横一線で幅を拾う」だけ。Simple is Bestの権化、というやつですね。
ステップ5:シンボロジー判定とデコード
さて、ここからがいよいよ「頭脳戦」のフェーズ。
バーコードには種類ごとにシンボロジー(規則の体系)が決まっています。同じ「白黒の縞」に見えても、JANコード、Code128、Code39、ITFあたりで、それぞれルールが違うんですよね。
判定のために見るのは、ざっくりこのあたり。
- スタートパターン/ストップパターン(どの並びで始まる/終わるか)
- 各桁を表す「幅の組み合わせ」
- 桁数のルール(JANなら13桁/8桁、など)
- チェックデジット(最後の1桁を使ったエラー検出)の計算方式
たとえばJANコードなら、
黒5px:白3px:黒7px… → 4901234567897
みたいな感じで、最終的に13桁の数字列に復元される。これがレジでスキャンされる「商品コード」の正体、というわけです。
…で、これを全部自前で書こうとするとですね、はっきり言って結構大変です(笑)
スタートパターンの揺らぎ、印刷のかすれ、影、傾き、解像度不足…現実の画像にはノイズが山ほどあって、教科書通りには動いてくれない。
なので、ここは素直にライブラリの肩を借りるのが、効率の良いやり方だったりします。
- ZXing(ゼブラ・クロッシング、と読むらしい):Google社員が20%プロジェクトとして立ち上げたJava製OSSが本家で、Android初期のバーコード認識界隈で広く使われてきた、と言われている定番。各言語へのポートも豊富。
- pyzbar:ZBarというCライブラリのPythonラッパー。
decode(image)を呼ぶだけでバーコードを返してくれる、というありがたい代物。 - ZXing-js:ブラウザJavaScriptで動くZXing系の実装。スマホのカメラ映像をリアルタイムで解析する用途で重宝します。
Pythonでpyzbarを使うと、極端な話、ここまでの処理を全部すっ飛ばして数行で終わってしまったりするんですよね。
from pyzbar.pyzbar import decode
from PIL import Image
for code in decode(Image.open("barcode.jpg")):
print(code.type, code.data.decode("utf-8"))
「ステップ1〜5、なんだったの…?」となるところですが、中で何をやっているかを知っておくと、うまく読めなかったときの対処(照明・角度・解像度・前処理)が一気にしやすくなるので、ちゃんと無駄ではないわけです(笑)
ステップ6:最終チェック&出力
デコードできたら、最後にもう一仕事。チェックデジットで「読み取り結果が壊れていないか」を確認します。
JANコードの場合、最後の1桁は前12桁から計算で求められる検算用の数字、ということになっています。これが合わなければ「読み取り失敗」と判定して、再撮影を促す。逆に言えば、レジで「ピッ」が鳴る瞬間というのは、この検算をパスした合図、ということだったりするわけです。
検算OKなら、晴れて「バーコード読み取り成功」。
お疲れ様でした、というやつですね。
実はこれ、スマホアプリも同じ仕組み
ここまで読んで「あれ、これってもしかして…」と気づいた方、鋭いです。
そう、みなさんが普段使っているスマホのバーコードリーダーアプリも、内部的にはだいたいこの流れをやっています。カメラから画像を取って、グレースケール化して、二値化して、走査して、デコードして、検算して、画面に表示する。
これを毎秒何十回も繰り返しているので、カメラをかざした瞬間に「ピッ」と読めるように見える、というわけです。
裏方の処理量はそれなりに重いんですが、いまどきのスマホCPUからすれば余裕の範疇。カメラの高性能化と画像処理ライブラリの成熟が同時に進んだ結果、専用ハードでしかできなかったことが、誰のポケットの中でも動くようになった、というのが現代のすごいところだと思うんですよね。
…冷静に考えると、それなりに未来です(笑)

まとめ:理解すれば「意外と作れる感」が出てくる
今回の話を一行ずつまとめると、こんな感じになります。
- バーコードは、カメラ画像からでも読み取れる
- 要は「白黒の並びをどう取り出すか」だけの問題
- カメラ → グレースケール → 二値化 → 線形スキャン → デコード → 検算、の6ステップ
- 自前実装は勉強用、実戦はZXingやpyzbarなど既存ライブラリに任せるのが効率的
- 仕組みを知っていると、読み取り失敗の原因(照明・ピント・角度)の見当がつく
理屈を一度通してみると、「あれ、これ自分でも作れるんじゃない?」という気分になってきませんか。
やっていることは画像処理の基本中の基本で、特別な数学も特別な機材もいらない。週末にカメラ付きのRaspberry Piでも引っ張り出してきて、簡易リーダーを組んでみるくらいなら、十分手の届く範囲だったりします。
…いやぁ、ここまで調べたら、ちょっとしたDIY魂が湧いてきますね(笑)
さて、次回は「バーコード vs QRコード」という、似ているようで全然違う「最強兄弟対決」に突入します。
あの黒い四角の中身、実はバーコードとはまったく別の世界が広がっていたりするんですよね。お楽しみに。
| バーコード作成2 DL版 [ダウンロード] 新品価格 |



コメント