プログラミング
アニメーション可能な半径を持つ高速ぼかし
Fast Blur with Animated Radius (aras-p.info)
要約
この記事では、画像処理における高速ぼかしアルゴリズムについて論じています。特に、ぼかし半径がアニメーションする場合や大きい場合に効率的な「ダウンサンプリング、ぼかし、アップサンプリング」という手法を提案しています。この手法は、従来のデュアル川瀬ぼかしの限界を克服し、より滑らかで高品質な結果を提供します。
全文翻訳
約1年前、Blenderコンポジター関連のテスト中に、その「高速ぼかし」モードが場合によってはそれほど速くないことに気づき、Blenderにデュアル川瀬ぼかしモードが必要なのではないかと考え始めました。しかし、それは限界があり、多くの試行錯誤の後、既存のぼかしのアイデアをいくつか組み合わせてSmol Gaussianを開発しました。
誰もが知っているように、ガウスぼかしを最も単純な方法で行うと、ぼかし半径の二乗に比例するコストがかかります。しかし幸いなことに、ガウスぼかしは分離可能なので、水平と垂直のぼかしを別々に行うことができ、コストは半径に比例して線形にスケールします。これは少しぼかすだけであれば問題ありませんが、非常に大きなぼかしが必要な場合は遅くなります。より大きな半径にぼかしをスケールさせる方法はたくさんあります。FrostKiwiはそれらについて非常に良いブログ記事を書いています: Video Game Blurs (and how the best one works)。そして、その記事の最後にある「最高の」手法はデュアル川瀬です。デュアル川瀬は実装が非常に簡単で、非常に高速に動作し、非常に優れています。
ただし、1) 滑らかに変化するぼかし半径が必要な場合、または 2) 水平と垂直で異なる量のぼかしが必要な場合は例外です。私の場合は、両方が必要でした。そこで、デュアル川瀬を拡張しようと試行錯誤を続けました。ある程度機能するものができましたが、ガウス形状に非常に近い別のぼかし方法を見つけ、ぼかし半径がアニメーションする場合により良く感じられます。別の言い方をすれば、私が発見したのは、他の多くの人々も発見していること、つまり「ダウンサンプリング、ぼかし、アップサンプリング」です。これにはかなりの詳細があります。
もし今日の読書制限に達しているなら、ここで止めて、インタラクティブなWebGPUぼかしプレイグラウンドをチェックしてください。そうでなければ、読み進めてください。
以下のビデオでは、ぼかし半径がアニメーションされ(XとYを5から1000まで一緒に増加させ、XとYを個別に減少させます)、アニメーションループ中に元の画像が回転します。元の画像には非常に明るいオブジェクト(線形強度500と50)があります。ここでは、追加のグレアを加えて「私は明るいですよ」を示す元の画像を示します(テストに使用された実際の画像にはグレアはありません)。
Smol Gaussian
私のSmol Gaussianは次のとおりです:作業解像度にダウンサンプリングし(軸ごとに異なる場合があります)、そこで小さな分離可能なガウスを適用し、フル解像度画像に再構築します。
これは想像上の擬似コードでどのように機能するかを示しています。
Image smol_gaussian(Image input, float2 radius) {
float2 sigma = max(radius, 0) / 3;
int2 orig_size = input.size;
int2 working_size = orig_size;
Image image = input;
// 作業解像度にダウンサンプリングします。
while (true) {
float2 scale = orig_size / float2(working_size);
bool2 reduce = (sigma / scale >= 6) && (working_size > 1);
if (!any(reduce)) break;
int2 next_size = select(working_size, ceil(working_size / 2.0), reduce);
// ソースピクセル領域を積分し、奇数次元に注意します。
// 縮小された軸に1ピクセルの境界を保持して、元の画像の端を維持します。
// 連続する2つの正確な半分は、単一の4x縮小にマージできます。
image = downsample_with_border_preserve(image, next_size);
working_size = next_size;
}
for (axis in {X, Y}) {
float scale = float(orig_size[axis]) / working_size[axis];
float down_variance = (scale * scale - 1) / 12;
float up_variance = scale == 1 ? 0 : scale * scale * (scale > 2 ? 1.0/3 : scale == 2 ? 3.0/16 : 1.0/6);
// 分散推定値は元のピクセル単位です。作業ピクセルに変換します。
float residual_sigma = sqrt(max(0, sigma[axis] * sigma[axis] - down_variance - up_variance)) / scale;
// 通常の正規化された1Dガウスパス。3*sigmaと4*sigmaの間でゼロに滑らかにテーパー処理されます。
// 隣接する重みをペアにしてバイリニアテクスチャサンプルにします。
// シグマが無視できる軸をスキップします。
image = gaussian_pass(image, axis, residual_sigma);
}
// 各軸:バイリニアで最大2倍の拡大、2倍を超えると3次Bスプライン。
return reconstruct(image, orig_size);
}
このアルゴリズムは、2026年9月時点のSkiaのGPUガウスぼかし(FilterResult::Builder::blurおよびFilterResult::rescale in FilterResultTypes.cpp)に似ています。独立したX/Yスケーリング、バイリニアフィルタリングを使用するテクスチャサンプルの配置、元の画像のエッジを維持するダウンサンプリングされた画像上の1ピクセルの境界(これにより、明るい内部が大きな半径で結果を過度に明るくしない)です。
Skiaぼかしと比較した追加点は次のとおりです。
奇数サイズの画像をダウンサンプリングする際、ピクセル領域の積分をより正確に行うため、孤立した明るいピクセルがサンプリング位相シフトでちらつくのを防ぎます。ダウンサンプリングされたレベルはceil-halvedサイズであり、sigma=6でダウンサンプリングが開始されます(実際には、ぼかしサイズ18、36、72、144、…が新しいレベルに切り替わります)。Skiaは代わりに連続的にスケーリングし、作業シグマを4未満に保ちます。
最終的なガウスぼかしを行う際、ダウンサンプリングと後続の再構築によって導入されるぼかしを考慮に入れます。つまり、分散をぼかしカーネルから差し引きます。最終的なガウスカーネルはsigma=4まで拡張され(つまり、sigma=3で切り捨てられません)、sigma 3..4領域の重みはゼロに達するようにテーパー処理されます。これにより、非常に明るいハイライトでの「ぼかしがカットオフされる」のを減らし、半径がアニメーションされ、タップ数が変更されるときに見た目が良くなります。
最終的なフル画像サイズへの再構築は、2倍の拡大を超えると、バイリニアの代わりに3次Bスプライン(完全に正のカーネルなのでリンギングなし)を使用します。これにより、バイリニアのスロープ不連続性を回避するのに役立ちます。
正確な2倍の縮小のペアは、最適化として単一の4倍縮小として実行されます。
これらすべてに関連する作業:
Skia SkImageFilterTypes.cpp、前述の通り。
Fabian Giesen、「Gaussian blur kernels」gdalgorithmsリスト(2009):ローパスフィルタ、低解像度でぼかし、その後アップサンプリング。彼はバイリニアアップサンプリングで十分だと示唆していますが、私のテストでは非常に明るい(HDR)ぼかしオブジェクトはバイリニアの「スロープステップ」を示したため、3次を使用しました。
Intel、「An Investigation of Fast Real-Time GPU-Based Image Blur Algorithms」(2014)には「Working in Lower Resolution」セクションがありますが、いつそれに切り替えるか、またはアニメーションぼかし半径をどのように処理するかについての詳細はあまりありません。
GPU Gems 2、Chapter 20、「Fast Third-Order Texture Filtering」には、バイリニアサンプルを使用した3次Bスプラインフィルタリングのトリックがあり、これは再構築部分で使用したものです。
これらは特に新規性や洞察力のあるものではなく、Skiaぼかしと比較して、おそらく少数の小さな品質調整だと考えています。
破棄されたアイデア:クロスフェードする作業解像度。
レベルの切り替えを隠すために、隣接する解像度をブレンドしようとしました。アイデアは次のとおりです。最終的なぼかしを行う解像度が変更されると、理論的にはぼかし半径がアニメーションされる場合に目に見える「ジャンプ」が発生する可能性があります。そのため、解像度スイッチがsigma=6で発生する場合、sigma=5からすでに開始して、現在の解像度と次の解像度の両方を評価し、smoothstepカーブを使用してそれらをブレンドします。両方の評価は同じ最終ぼかし量をターゲットにしていますが、異なるグリッドで、それぞれ異なる量のガウスタップを使用します。しかし、これはパフォーマンスを犠牲にします。なぜなら、遷移領域(ぼかしサイズ15..18、30..36、70..72など)では、2つのガウスぼかしが実行され、それらがブレンドされるからです。X/Y半径が異なる場合、実際には3つのガウスぼかしを評価してブレンドする必要があるかもしれません。
私のテストでは、これはほとんど視覚的な違いをもたらしませんでしたが、パフォーマンスを犠牲にし、実装をより複雑にしました。そのため、最終的にこれは破棄されました。
デュアル川瀬
プレイグラウンドは、デュアル川瀬の拡張バージョンも実装しています。Marius Bjørge、「Bandwidth-Efficient Rendering」(SIGGRAPH 2015)を参照してください。
元のバージョンは、水平と垂直のぼかし量が等しく、連続的な「ぼかし半径」設定ではなく、離散的な「ぼかしピラミッドレベル数」制御のみをサポートします。
任意のぼかしサイズの場合:隣接する離散ぼかしレベルをブレンドします。これはobs-composite-blurに似ています。ステップ間の分数位置を使用してブレンドし、t * (2 + t) / 3で再マッピングします。これにより、線形ブレンドよりも少し良く感じられます。
独立したX/Yぼかし半径の場合:より小さいぼかし半径に達すると、その軸に沿ったさらなる縮小を停止します。上記のレベル間ブレンドの場合、3つの異なるぼかし結果をブレンドする必要があるかもしれません。
これは機能しますが、...