Algorithm & Image Processing
輝度判定とアルファ補間で作る背景透過スタンプ
紙に書いた文字をデジタル化する際、最大の壁となるのは「紙の白さ」と「インクの境界」の判定です。単純な閾値カットでは消えすぎてしまう繊細な線を、いかに滑らかに抽出するか。その核心部を解説します。
このツールの入力から出力まで
現行コードの確認日:
- 入力
- 紙に描いた線などの写真と、明るさの境界値・出力色を指定します。
- 処理
- 各画素の明るさをしきい値と比べ、白い側を透明にし、残す線を指定色へ置き換えます。
- 出力
- 出力は透明 PNG です。
まず確認する点:しきい値を下げると薄い線も消えます。影を取り除く効果と細い線を残す条件が両立するか、保存用の処理画像で確認します。
実装の確認箇所:src/pages/image/handwriting-stamp/index.astro。以下のコード抜粋と出力条件は、この本番実装に基づいて説明します。
仕組みを読み進めるときは、Canvas APIと画像処理(画像寸法、drawImage、画素と透明度の扱い)、メモリ管理と画像処理(画素バッファ、縮小と容量予算)が参考になります。
1. RGBから「輝度」を抽出する判定ロジック
本ツールは紙を物体として認識するのではなく、明るさで透明度を決めます。ピクセルごとのRGB値に対し、人間の視覚特性に基づいた重み付けを行って「輝度(明るさ)」を算出しています。
算出した輝度と、ユーザーがスライダーで設定した「境界線」を比較することで、背景として透明にする部分と、スタンプとして残す部分を分けています。
輝度から出力アルファを求める例です。実装では threshold の直下 30 段階を徐々に透明にします。
function stampAlpha(r, g, b, threshold = 180) {
const brightness = r * 0.299 + g * 0.587 + b * 0.114;
if (brightness > threshold) return 0;
if (brightness > threshold - 30)
return Math.floor(255 * (threshold - brightness) / 30);
return 255;
} この実装のメリットは、青みがかった写真や黄色みがかった古い紙でも、明るさの差で文字を残せる点にあります。ただし影や汚れも暗い部分として残るため、均一な照明で撮った白い紙に向きます。
2. ジャギーを防ぐ「動的アルファブレンディング」
背景を透明にする際、特定の明るさを境に「100%透明か0%透明か」を切り替えると、文字の縁がガタガタ(ジャギー)になってしまいます。
本ツールでは、境界線付近に「遊び(エッジ領域)」を設け、輝度に応じて透明度をグラデーション状に変化させる補間処理を施しています。これにより、ボールペンの掠れや印鑑の質感を残したまま、自然な切り抜きを実現しています。
このエッジ領域は空間的な「数ピクセル幅」ではなく、輝度値で 30 の範囲です。線の周囲をぼかす処理ではありません。
3. ブラウザ完結を実現する「フロントエンドHEIC変換」
iPhoneユーザーが撮影した写真は .heic 形式であることが多く、通常のCanvasでは読み込めません。これをサーバーに送らず処理するため、heic2any ライブラリを用いてブラウザ上で動的にJPEG/PNGへ変換するパイプラインを構築しています。
HEIC の変換完了を待ってから描画します。async / await は後続処理の順序を管理するもので、以降のピクセル計算がメインスレッドを使わなくなるという意味ではありません。
出力の仕様と確認する点
境界値は 50〜240。値を下げるほど薄い線や紙の影が消え、上げるほど残る部分が増えます。まず紙の背景が消える位置まで下げ、必要な線も消えないか確認します。
プレビューは幅 800px 以下ですが、保存には長辺 2000px 以下に縮小した処理画像を使います。「フル解像度」はこの処理画像の大きさで、元ファイルの解像度を無制限に保持するわけではありません。
出力は透明 PNG です。色は元の色・黒・朱・ローズ・金から選び、色変更は残った全画素へ適用します。文字認識やベクター化は行わず、線の汚れを自動で補修する機能もありません。