技術記事

HotPDFでPDFページ内の回転QRコードをデコードする

HotPDFは、読み込んだPDFページ内の回転QRシンボルを、サンプリング済みモジュール行列をデコーダ内部でD4の8方向すべてに正規化することでデコードします。線形シンボロジには効く外側の回転リトライはQRには効かず、その理由を理解しておくと、壊れてはいないのに壊れて見えるデコーダを1日追い回さずに済みます

シナリオはごく平凡です。スキャンされた納品書がPDFで届き、各ページがQRラベルを担い、スキャンオペレーターはトレイが受け付ける向きで紙の束を放り込んだ。ラベルには正立のものがあり、90度ずれているものがあり、数枚は逆さまです。バーコードデコーダを呼ぶと、半分のページは解決し、残る半分はエラーも出さずに空で返ってきます

スキャンマスクの回転では回転QRが直らない理由

QRのファインダーパターン配置は意図的に非対称であり、画像全体の回転はその非対称を取り除くのではなく保存するからです。QR Codeはファインダー正方形を左上、右上、左下の3隅に置き、右下を空けます(ISO/IEC 18004:2015 §6.3.3)。この欠けた隅こそが向きの手がかりです。ページビットマップを90度回転させると、欠けは単に別の隅へ移るだけです。3隅の配置を自分自身へ写す自明でない平面回転は存在しないため、正規の配列しか受け付けないデコーダは、試みを順番に全部拒否します

これが重要なのは、一見もっともな対策が間違った方だからです。自然な発想は、リトライを外側に括り付けることです。ページをレンダリングし、マスクをデコーダに渡し、失敗したらマスクを回転して90度、180度、270度でもう一度試す。Code 39ならこの方針はまさに正解です。線形シンボロジにはスタートとストップのパターンがあり、バーが水平に走ればスキャナは見つけられるからです。QRでは、保証された4連敗の後に「何も見つからず」という報告が続くだけです

モジュール行列に適用するD4群

正規化の正しい場所はサンプリングの後、ピクセルマスクではなくブールのモジュールグリッドの上です。デコーダがシンボルをn×nの明暗モジュール行列へ解決した後は、正方形の二面体群、4回転×2鏡映の計8候補方向を列挙できます。各候補についてファインダーの三角形をチェックし、3つのファインダーが左上、右上、左下の位置に着地する最初の候補が真の向きです。そこから先は既存のパイプラインが無変更で走ります。フォーマット情報ビット、ジグザグのデータ配置、Reed-Solomon誤り訂正はいずれも正規の行列を仮定しており、今はそれを手に入れるからです

D4 群の 0、90、180、270 度回転における同一 HotPDF QR モジュール行列の 4 通りの描画。3 つのファインダーパターンが隅を移動し、空き隅もそれに伴って動くため、デコーダに対して左上、右上、左下にファインダーを提示するのは正規の向きだけであることを示す
ピクセルマスクの回転ではQRファインダーの非対称は取り除けません。そこでHotPDFはサンプリング済みモジュール行列の上でD4の方向を列挙し、ファインダーが左上、右上、左下に着地する最初の候補を採用します

これを安上がりにする性質が2つあります。行列はレンダリング済みビットマップに比べて小さく、8回の転置は8回のページレンダリングよりはるかに安いこと。そして行列はサンプラーが作るクリーンなブール配列であり、途中の変換が、一度もサンプリングされていない値を混入できないことです

バージョン検出は除算ではなく整除性の探索

モジュール数は、サンプリング幅を仮定したモジュールサイズで割り出すものではありません。ここを間違えると、高解像度レンダリングでのデコード失敗の微妙な原因になります。バージョンvのQRシンボルは一辺4v + 17モジュールであり、バージョン1は21モジュール、バージョン40は177です。幅126ピクセルのマスクは、1モジュール6ピクセルのバージョン1とも、より小さいモジュールサイズのいくつかの上位バージョンとも、等しく整合します。素朴な除算はそのうちの1つを選び、たいてい外れます

効くのは候補バージョンにわたる整除性の探索です。バージョン40から1へ下り、モジュール数がサンプリング幅を余りなく割り切り、かつ1モジュールあたり3ピクセル以上を残す候補を残し、生き残った中で最小のバージョンを採ります。3ピクセルの下限が、粗いシンボルの馬鹿げたほど高密度な読みを探索が受け入れるのを止め、最小バージョンのルールが残る曖昧さを、スキャナが実際に出すであろう読みの方へ解決します

126 ピクセルのサンプリング済みマスク上の QR シンボルに対する HotPDF のバージョン検出の歩み。候補モジュール数 4v+17 をバージョン 40 から 1 まで順に、余りなく割り切れるかと 3 ピクセルのモジュール下限で試し、生き残った最小バージョンが勝つ
QRのモジュール数は、マスク幅を仮定のモジュールサイズで割るのではなく、候補バージョンにわたる整除性の探索から来ます。生き残った最小バージョンが曖昧さを解決します
var
  Pdf: THotPDF;
  Options: THPDFBarcodeDecodeOptions;
  Codes: THPDFDecodedBarcodes;
  Info: THPDFBarcodeDecodeInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('delivery-notes.pdf');
    Options := THPDFBarcodeDecodeOptions.Default;
    Options.DPI := 300;
    Options.RotationPolicy := bdrpFallback;
    Options.MinimumConfidence := 0.5;
    Options.MaxResults := 16;
    if Pdf.DecodeLoadedPageBarcodes(0, Options, Codes, Info) then
      for I := 0 to High(Codes) do
        if Codes[I].Symbology = bsyQRCode then
          Writeln(Codes[I].Text, '  at ',
            Format('%.0f', [Codes[I].OrientationDegrees]), ' degrees');
  finally
    Pdf.Free;
  end;
end;

THPDFBarcodeDecodeOptions.Defaultはゼロ初期化ではなく、値の入ったレコードを返します。DPIがゼロや結果上限がゼロは、何も返ってこないように見えるのに一見妥当な指定であり、だからこそ重要です。RotationPolicyが制御するのは外側のリトライだけです。bdrpNoneは1回だけレンダリングし、bdrpFallbackは最初のパスが失敗した後に他の向きをリトライし、bdrpAllはすべての向きを無条件にレンダリングします。QRの正規化はデコーダ内部で起きるため、QRのページは3つのポリシーのどれでも最初の試みで解決します。ポリシーは、本当にそれを必要とする線形シンボロジのためにあるのです

ビットマップ変換がピクセルを捏造していないことをどう証明するか

両側のインクを数え、合計が一致することを要求します。回転はピクセルの置換にすぎないため、出力の非ゼロセル数は入力と等しくなければなりません。外側リトライ経路のマスク回転が、入る時に4800セルのセットを報告し、出る時には7439だったとき、そのたった1つの比較だけで、ジオメトリを1行も読まずに変換を断罪できました

原因は平凡で、ルールとして持ち帰る価値のあるものでした。SetLengthでサイズを取った動的配列は、関数の結果としてランタイムがクリアしない経路を旅する場合、ゼロで到着する保証がありません。回転が決して書かないセルは、以前そこにあったバイトをそのまま運びます。古いバイトの中には非ゼロのものがあり、非ゼロはインクを意味します。修正は1行、置換ループの前にFillChar(Result[0], N, 0)を置くこと。そしてそれが含意する規律はもっと広いです。マスクやビットマップバッファを返す関数はどれも、アロケーションセマンティクスを頼らず、出力を明示的にクリアすべきです

この欠陥が3リリース生き延びた理由は、欠陥そのものより面白いです。QRが向き処理をデコーダの中へ引っ越させた時点で、QRは外側のマスク回転を一切通らなくなり、そのコードパスの残る消費者はCode 39だけになりました。共有インフラはこの種のバグをしょっちゅう隠します。ある機能のカバレッジがパスをテスト済みに見せかける一方、実際にそのパスに依存する機能には自前のカバレッジが何もない。新機能が使わなくなったパスには、なお使うテストが1つ必要です

結果をページ座標で読み戻す

デコーダが生み出す幾何値はすべて、試行ビットマップの座標フレームで表現されており、呼び出し側が要るのはPDFユーザー空間での値です。この変換は2段階で走ります。リトライが適用した90度回転を元に戻し、次にユーザー空間をビットマップへ写したレンダリング変換を元に戻す。THPDFDecodedBarcodeに届くのは、ユーザー空間の軸平行バウンディングボックスであり、LeftBottomRightTopはYが上向きに増えるPDFの規約に従い、さらに反時計回りのOrientationDegreesが付きます

レンダリング済みページビットマップから、ブールのモジュール行列へのサンプリング、D4 正規化、整除性によるバージョン検出、Reed-Solomon デコードを経て、リトライの 90 度回転とレンダリング変換を元に戻す 2 段階の座標変換へ至る HotPDF バーコードパイプライン。THPDFDecodedBarcode がユーザー空間で Left、Bottom、Right、Top、OrientationDegrees を公開する
デコーダ内部のQR正規化により、ページは最初の試みで解決します。2段階の座標変換が、試行ビットマップの結果をユーザー空間の軸平行ボックスへ変えます

2段階目の変換の向きを取り違えると、症状は厄介です。テキストは完璧にデコードされるのに、レビューオーバーレイのために描いたボックスが、正しい位置の鏡像に着地します。デコーダの上にレビューインターフェースを築く人は、既知のフィクスチャに対して主張を置くべきです。シンボルを意図的にページの隅の近くへ置き、Y軸の反転が一目で見えるようにして。同じ考え方は、レンダリング境界を越えるあらゆる座標に当てはまり、だからこそDelphiでPDFページをビットマップへレンダリングするを、デコーダの上に何かを築く前に理解しておく価値があるのです

内蔵デコーダがやること、やらないこと

内蔵デコーダは有界で依存のない実装であり、静かに品質を落とすのではなく、限界について正直です。Code 39とQRを認識し、BCHで保護されたフォーマットビットとマスクパターンを、データを公開する前に検証します。そして壊れたシンボルへの誤り回復は試みません。入力が、斑な光の下の曲がったラベルの写真なら、それは別の問題クラスであり、専用エンジンが要ります

// 自前のエンジンに差し替える:IHPDFBarcodeDecoder を実装し、
// デコーダ対応オーバーロードへ渡す。ページレンダリング、予算、
// 座標マッピング、重複排除は引き続き HotPDF が担う
if not Pdf.DecodeLoadedPageBarcodes(PageIndex, MyDecoder, Options,
     Codes, Info) then
  case Info.Status of
    bdsBudgetExceeded:
      Log('raise MaxPixels or lower DPI: ' + string(Info.Diagnostic));
    bdsRenderError:
      Log('page did not render: ' + string(Info.Diagnostic));
    bdsDecoderError:
      Log(string(Info.DecoderName) + ' failed: ' + string(Info.Diagnostic));
  end;

THPDFBarcodeDecodeInfoは、本番パイプラインが実力を発揮する場所です。RotationAttemptCountDecoderCallCountは、外側のリトライがそもそも走ったかどうかを教え、ReceivedResultCountAcceptedResultCountの対比は、何も見つけられなかったデコーダと、見つけたものを全部拒んだ信頼度閾値を切り分け、RenderedPixelsPeakWorkingBytesは、バッチジョブがスラッシングを始めたときにグラフにすべきものです。空の結果セットにbdsSucceededが付くのは、ページに本当に読めるシンボルがないという意味であり、bdsBudgetExceededとは別の運用上の事実です

予算フィールドは、デフォルト任せではなく意図的な決断に値します。MaxPixelsMaxWorkingBytesが存在するのは、DPIが2乗で掛かるからです。A4ページで300 DPIから600 DPIへ動くと、レンダリングコストもピーク確保量も4倍になり、巨大なページボックスを申告する信頼できない入力は、スキャンジョブをメモリ不足インシデントに変え得ます。上限は、最悪の正当なドキュメントが要る量に設定し、その上でbdsBudgetExceededに外れ値を、より遅く隔離された経路へ振り分けさせます

ドキュメントが、機械可読ラベルと、インデックスする予定の印刷テキストを混ぜるなら、バーコードデコーダはHotPDF内のテンプレートマッチングOCRで扱う認識エンジンと自然に組み合わさります。同じ話の生成側はHotPDFでPDFにバーコードを描くにあります。どちらも同じレンダリングと予算のインフラ上で走るため、片方にまともな上限を設定済みのパイプラインは、もう片方をほぼタダで手に入れます

回転許容は、効いているときは見えず、効かないときには激烈に腹が立つ種類の機能です。そしてこの工学的教訓はQRを越えて一般化できます。正規化は、セマンティックな表現にできる限り近い場所で行い、データが捕獲時のあらゆる偶然をまだ運んでいるピクセル層では行わない。HotPDFはこれを、同じ取り込みパイプラインがたいてい必要とするレンダリング、OCR、ページ解析の部品とともに、HotPDF Delphi PDFコンポーネントの一部として出荷しています