技術記事

HotPDFの画像ダウンサンプリングカーネルと印刷ディザリング

HotPDFは3種類の画像ダウンサンプリングカーネルをImageDownsampleKernelプロパティから、独立したFloyd-SteinbergパスをRenderOutputDitherから公開しています。前者は写真をサイズ予算に収まるよう縮小した後の見え方を、後者はページが白黒へ落とされた後の見え方を制御します。どちらも既定では無効で、同じ理由によるオプトインです。どちらも現実に時間を食うからです

ここへ辿り着くきっかけの圧力は、よくあるものです。60 MBのスキャン契約書を、10 MB超を拒むメールゲートウェイを通して送らなければならない、あるいは明細の一括出力を、あらゆるグレーのピクセルを紙かトナーのどちらかに落とすファックス式モノクロ機器へ届けなければならない。どちらの問題もリサンプリングの問題であり、どちらにも、見た目の悪い速い答えと、見た目の正しい遅い答えがあります

3つのカーネルが実際に違うところ

THPDFResampleKernelには3つの値があり、速度と品質の曲線上で本当に異なる位置に座っています。rkHalftoneは歴史あるGDIのStretchBlt経路のHALFTONEモードへ委譲しますが、これは名前に反してバイリニア級のフィルタリングです。速く、ラインアートとスクリーンショットには十分で、縮小した写真に一目で分かるジャリジャリしたエッジが出やすい。rkBicubicは分離可能なCatmull-Romカーネルを、rkLanczos3は3ローブのサポートを持つ分離可能なウィンドウ処理sincを走らせます

分離カーネルはどちらも、水平から垂直への2パスで走り、純Pascalで出力ピクセルごとに6から12タップ掛かります。GDI経路よりおよそ1桁遅く、rkHalftoneが既定のままである理由はまさにそこです。数千ページの夜間バッチでは、この差は好みではなくスケジューリングの決断です。ユーザーが待っている1通のドキュメントなら、Lanczos3はほぼタダで、見た目も明らかに良くなります

HotPDF の 3 つのダウンサンプリングカーネルの重み曲線。rkHalftone はサポート幅 1 のバイリニア級 GDI HALFTONE 経路へ委譲し、rkBicubic はサポート幅 2 の分離可能 Catmull-Rom 三次カーネル、rkLanczos3 はサポート幅 3 のウィンドウ処理 sinc を走らせる。速度でおよそ 1 桁を払い、写真の見え方を明確に良くする
3つのカーネル値は速度と品質の曲線上で本当に異なる位置にあります。バイリニア級のGDI経路、Catmull-Rom三次、3ローブのウィンドウ処理sinc。分離カーネルは重みを正規化するため、黒や白を越えたリンギングは出ません

知っておく価値のある実装上の性質が2つあり、出力に何ができて何ができないかを決めます。境界はラップやフェードではなくエッジレプリケーションでクランプされること。そして重みが出力ピクセルごとに正規化されること。この2つを合わせると、結果が黒より下や白より上へリンギングすることはなく、ハードエッジの周りの古典的なLanczosオーバーシュートハローが、エンコード後の画像にクリップされたアーチファクトとして現れません

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // 呼び出しの前に設定する
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

MinimumSavingsBytes引数、上のコードでは4096、がこの操作を誠実に保つガードです。すでに効率よく圧縮された画像の再エンコードは、元より大きなストリームを生み出すことがあり、すべての画像を盲目的に置き換えるダウンサンプラーは、縮小を頼まれたファイルを時折肥大させます。この閾値が言うのは、少なくともこのバイト数を節約するときだけ置き換えを確定せよ、ということです。PreservedCalibratedImageCountはもう1つの保守的な判断を報告します。リサンプリングが損なうキャリブレーション済みカラースペースを運んでいるため触れずにおいた画像です

間違った多項式の係数はなぜ見つけにくいのか

壊れた補間カーネルはクラッシュも例外投げもせず、誰も帰属をつけられない形で微妙に正しくない画像を生むだけだからです。Catmull-Romカーネルは区分三次であり、入れ子Horner形式での外側の分岐は((-0.5t + 2.5)t - 4)t + 2です。この中間の係数を-4ではなく-5と書いても、関数は評価でき、もっともらしい範囲の数値を返し続け、画像を出し続けます

損傷は、0でなければならないW(1)が-1に評価される形で現れます。負の重みが蓄積し、合計はゼロでクリップされ、見える症状は、グラデーションの左端が黒に沈むことと、ステップエッジが中間トーンを失うことです。失敗のどこをとっても多項式を指すものはありません。これを数秒で捉えるチェックは視覚ではなく算術です。補間カーネルはW(0) = 1とW(±1) = W(±2) = 0を満たさなければならず、この3点を外すカーネルは係数エラーだ、以上。ユニットテストでこの3値に主張を置けば、タイプミス欠陥の一クラス丸ごとが消えます

HotPDF の bicubic Catmull-Rom 外側分岐のプロット。間違った係数が隠れる理由を示す。入れ子 Horner 形式で -4 を -5 と書くタイプミスも評価は通り、ゼロでなければならない W(1) が -1、W(2) が -2 のまま残る。W(0) が 1 という主張と 2 つのゼロ制約を置けば数秒で捉えられる
壊れたカーネルは決してクラッシュせず、もっともらしく見える数値を返すだけです。だから係数のタイプミスは目で捉えられません。W(0) = 1と正負1、2でのゼロは、3行のユニットテストです

Floyd-Steinbergディザリングとパイプライン内の適所

ディザパスはリサンプリングとは別の問題であり、パイプラインの別の場所に住んでいます。RenderOutputDitherはFloyd-Steinberg誤差拡散をページ合成のに適用します。モノクロ印刷プレビューやファックス式エクスポートにとって意味のある配置はこれだけで、この操作は完成済みラスターを1ピクセル1ビットへ落とすことであって、個々の画像が入力時にどうスケールされたかの話ではないからです

アルゴリズムそのものは短い。輝度は50%で閾値処理され、量子化誤差は古典的な7/16、3/16、5/16、1/16の重みで、右、左下、下、右下の4近傍へ拡散します。出力ピクセルは全チャンネルで0か255です。素朴な代替案、つまり拡散なしのハード閾値が代わりに与えるのは、写真をシルエットに変え、コンテンツを運んでいた中間トーンをすべて失わせることです

HotPDF の Floyd-Steinberg ディザリングのレンダーパイプライン配置。RenderOutputDither はページ合成の後に完成済み 24 ビットラスターに対して走り、輝度を 50 パーセントで閾値処理し、各量子化誤差を 7/16、3/16、5/16、1/16 の重みで右と下へ、累積しなければならない行バッファを通じて拡散し、1 ビットのモノクロ出力を生む
ディザリングは合成の後に属します。完成済みラスターを1ビットへ落とす操作だからであり、画像のスケール方法とは無関係です。拡散の重みは合計1になり、行バッファは上書きではなく累積しなければなりません
// モノクロプレビュー機器向けの描画時ディザリング
Pdf.RenderOutputDither := True;

// あるいは自分で持っているビットマップに同じパスを適用する。ビットマップは
// pf24bit であること。関数は推測ではなく False を返す
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// ドキュメントパイプラインの外でリサンプリングする場合のカーネル直接アクセス
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

誤差拡散には、誰もが一度は噛まれる実装上の細部が1つあります。行間の誤差バッファは累積しなければなりません。次の行の各ピクセルは、現在の行の3つの異なるピクセル、3/16、5/16、1/16のタップから寄与を受けます。コードが加算の代わりに代入をしていたら、各書き込みは前の寄与を捨て、最後のタップだけが生き残ります。画像はそれでもディザがかかって見え、それが気づきにくさの正体ですが、テクスチャは誤りで、階調再現はドリフトします。これを捉えるテストは定量的です。均一な中間グレーのフィールドをディザリングし、内部の被覆率が40から60%の間に着地することを要求します

サイズ削減パイプラインはどの組み合わせを選ぶべきか

カーネルは画像が実際に何であるかに合わせ、ディザリングは圧縮ではなく機器の関心として扱います。サイズ予算を生き延びなければならない写真スキャンには、150や200 DPIでのrkLanczos3が、ピクセル数を4分の1以下に削りながら、人が気づくディテールを保ちます。スクリーンショット、図面、ラインアートには、rkHalftoneが実際のところ十分で、はるかに速い。保存すべき階調グラデーションが少ない画像だからです。1枚ずつ目視できない混合バッチには、rkBicubicが妥当な中間点です。バイリニアより良く、タップ数はLanczos3の大体半分

ダウンサンプリングはいくつかあるレバーの1つであり、必ずしも最大のものではありません。二値スキャンは、DelphiでのネイティブJBIG2二値圧縮で扱うエンコーダの方がたいていずっとよく応えます。勝ちどきはピクセル数ではなくシンボル辞書から来るからです。決める前に、ファイルに実際に何が入っているかを知るのが役立ちます。それが画像とデコードフィルタの抽出の目的です。画像オブジェクトと既存圧縮の棚卸しが、リサンプリングに利益があるかどうかを教えてくれます

結果を表示するプレビュー画面を作っているなら、PDFページのビットマップへのレンダリングで説明したのと同じ描画経路こそがRenderOutputDitherの効く場所です。ディザ付きプレビューとディザ付き出力が、ずれ広がっていく2つの実装ではなく、1本のコードパスから来ることになります

両機能の背後にある大原則は、品質設定は明示的で可逆であるべきだということです。HotPDFは既存アプリケーションが、出力やタイミングの不意の変化なしにアップグレードできるよう、歴史的な挙動を既定として保持し、より見栄えの良い遅い経路を、プロパティ1つの代入の距離に置いています。どちらも、土台となるリソース最適化とレンダリング機構とともに、HotPDF Delphi PDFコンポーネントの一部です