アプリの右側に出る「解説」と同じ文章なので、触りながら読むなら、アプリを開いたままそちらを見るほうが早いです。 このページは、通して読みたいときや、あとから探したいときのためのものです。
生成日:2026-09-10(node docs/_gen_docs.js で js/ops/*.js から自動生成)
基本
原画(処理なし)
原理
何も加工せずそのまま表示します。読み込んだ画像の素性(ヒストグラムの形、ノイズの量、白飛び・黒つぶれの有無)を確認するための状態です。
式
g(x, y) = f(x, y)
注意点・見どころ
- まずここでヒストグラムを見てから処理を選ぶと、狙いが説明しやすくなります。
- 右下の「計算時間」はこの状態でもコピーのコストぶんだけ出ます。他の処理の目安と比べるときの基準にしてください。なお時間は2つ出しています。「計算時間」は処理の中身だけを測ったもので、処理どうしを比べるのはこちら。「画面に出るまで」は結果が返るまでの実時間で、軽い処理では直前の描画の割り込みが乗るぶんだけ大きく、Web Worker に逃がした処理では計算が別スレッドで進むぶんだけ小さく出ます。
点処理
トーンカーブ
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| カーブ(点をドラッグ/空白をクリックで追加/右クリックで削除) | curve | [[0,0],[255,255]] | 制御点をドラッグして編集 |
| 対象 | select | luma | 輝度のみ(色比を保つ) / RGB個別 |
原理
入力の明るさ 0〜255 を、出力の明るさ 0〜255 へ写す「対応表(LUT)」を手で描く処理です。カーブが対角線より上にある区間は明るく、下にある区間は暗くなり、傾きが急な区間ほどコントラストが強くなります。周りの画素を一切見ないので、これは点処理の代表例です。
たとえるなら、音量つまみを「小さい音・普通の音・大きい音」で別々に設定できるイコライザです。全体の音量を上げるのではなく、どの音量帯をどれだけ持ち上げるかを決めます。
式
g(x, y) = T( f(x, y) ) T は 0..255 → 0..255 の単調写像 LUT を1本作れば、あとは全画素で表引きするだけ(O(N)、画像サイズに比例)
注意点・見どころ
- 補間には単調保存の3次補間(PCHIP)を使っています。普通のスプラインだと制御点の間で行き過ぎが起き、持ち上げたはずの階調が途中で下がることがあるためです。
- S字カーブは中間調の傾きを立てるので「メリハリが出た」ように見えますが、その代わり両端(暗部・明部)の傾きが寝て階調が潰れます。ヒストグラムの両端がどう潰れるかを見ながら調整してください。
- 「RGB個別」を選ぶと色かぶりの補正ができる反面、色相がねじれます。明るさだけを動かしたいときは「輝度のみ」が安全です。
- 8bit のまま持ち上げると、間引かれた階調が戻らずヒストグラムに櫛の歯状の隙間ができます。下のヒストグラムで確認できます。
ガンマ補正
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| γ | range | 1 | 0.1 〜 3(刻み 0.01) |
| 対象 | select | luma | 輝度のみ(色比を保つ) / RGB個別 |
原理
明るさをべき乗で写し替える処理です。0〜1 に正規化した値を 1/γ 乗するので、γ を大きくすると暗部が大きく持ち上がり、明部はあまり動きません。人間の目は暗い側の差に敏感なので、この「暗部を厚く配分する」性質が理にかなっています。
たとえるなら、階段の段差を一律に下げるのではなく、下の方の段だけ細かく刻み直すようなものです。踏み外しやすい足元だけ丁寧にする、という配分の変更です。
式
g = 255 · ( f / 255 ) ^ (1/γ) γ > 1 … 明るく(暗部が伸びる) / γ < 1 … 暗く / γ = 1 … 恒等 ※ ソフトによっては指数を γ と書く流儀もあります。向きが逆になるので注意。
注意点・見どころ
- トーンカーブの特殊な場合です。カーブを手で描く代わりに、パラメータ1個で形が決まります。デモでは「まずガンマ、次にトーンカーブ」の順で見せると理解が早いです。
- sRGB の画像はすでに約 2.2 のガンマがかかった状態で保存されています。物理的に正しい平均や合成をしたいときは、いったんリニアに戻してから計算する必要があります。ここでは表示値のまま処理しています(同好会でよく話題になる落とし穴)。
- γ を上げると暗部の階調が引き伸ばされるぶん、そこに乗っていたノイズも一緒に持ち上がります。「暗部にディテール」サンプルで確認できます。
- ヒストグラムの山が左右どちらへ、どう伸び縮みするかを見ると、べき乗写像の効き方が一目で分かります。
ヒストグラム平坦化
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 対象 | select | luma | 輝度のみ(色比を保つ) / RGB個別 |
| 強さ | range | 1 | 0 〜 1(刻み 0.01) |
原理
画像全体のヒストグラムを見て、累積度数がまっすぐな直線になるように明るさを割り当て直す処理です。画素の多い明度帯には広い出力範囲を、画素の少ない明度帯には狭い出力範囲を配ります。結果として、混み合っていた階調が引き伸ばされてコントラストが立ちます。
たとえるなら、席の埋まり方に合わせて座席の幅を配り直すことです。満員の車両は幅を広げ、がらがらの車両は詰める。合計の長さは変えずに、混雑を均します。
式
g = round( 255 · ( CDF(f) − CDF_min ) / ( N − CDF_min ) )
CDF(v) = Σ_{k ≤ v} hist(k)、N は総画素数
注意点・見どころ
- 強さを 100% にすると効きすぎることが多いので、実務では 30〜60% で混ぜるのが普通です。このスライダーは「元の恒等写像との線形補間」で実装しています。
- 全画素で1本の LUT を作る「大域的な」手法なので、画面の一部だけ極端に明るい/暗い画像は苦手です。「低コントラスト(暗部にディテール)」サンプルで試すと、右上の明るい窓が飛ぶのが見えます。その弱点を埋めるのが次の CLAHE です。
- 平坦化後のヒストグラムは、教科書の絵のようには平らになりません。8bit の離散値では同じ値の画素をまとめて動かすしかなく、山が間引かれて櫛の歯状になります。下のグラフで確認できます。
- 「RGB個別」は各チャンネルを独立に伸ばすため、色かぶりが強い写真では色が派手に転びます。デモでは「カラーバー」サンプルで両方を比べると違いが分かりやすいです。
CLAHE(適応的平坦化)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| タイル分割数(縦横) | range | 8 | 2 〜 16(刻み 1) |
| クリップ上限 | range | 2.5 | 1 〜 10(刻み 0.1) |
| 強さ | range | 1 | 0 〜 1(刻み 0.01) |
原理
画像をタイルに切り、タイルごとにヒストグラム平坦化をかけます。ただし度数に上限(クリップ)を設け、あふれたぶんを全階調に配り直してから累積を取ります。これで「平坦な部分のノイズだけが極端に持ち上がる」という適応的平坦化の副作用を抑えます。タイルの継ぎ目は、近い4タイルの写像を双線形で混ぜて消します。
たとえるなら、全国一律のテストの偏差値ではなく、クラスごとに評価をつけ直すやり方です。ただし1人だけ突出しても評価が振り切れないよう上限を設け、クラスの境目では隣のクラスの基準と混ぜてなだらかにつなぎます。
式
タイル (i, j) の写像 M_ij(v) = 255 · CDF_clip( v ) g(x, y) = 双線形補間( M_i0j0, M_i1j0, M_i0j1, M_i1j1 )( f(x, y) ) クリップ上限 = clip × (タイル画素数 / 256)
注意点・見どころ
- 「低コントラスト(暗部にディテール)」サンプルで、ヒストグラム平坦化と交互に切り替えてみてください。大域的な平坦化では右上の明るい窓が飛びますが、CLAHE では暗部のディテールを出しながら窓が残ります。ここが同好会での見せどころです。
- クリップ上限を上げるほど普通の適応的平坦化に近づき、ノイズが目立ちます。下げるほど恒等写像に近づいて効果が薄くなります。2〜3 あたりが実用域です。
- タイル数を増やすと局所性が強まりますが、増やしすぎると1タイルあたりの画素が減ってヒストグラムが荒れ、不自然なムラが出ます。
- 医用画像や工業検査で「暗部を見たい」ときの定番です。ただし局所ごとに違う写像をかけるので、画素値の絶対量に意味がある測定用途にはそのまま使えません。
空間フィルタ
平均化 / ガウシアン
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 種類 | select | gauss | ガウシアン / 平均化(ボックス) |
| σ(標準偏差) | range | 2 | 0.3 〜 12(刻み 0.1) |
| 窓の半径 | range | 3 | 1 〜 20(刻み 1) |
原理
注目画素の周りを重み付きで平均する処理です。平均化は窓の中を一律の重みで、ガウシアンは中心が重く外へ行くほど軽い釣鐘型の重みで平均します。どちらも高い周波数(細かい変化)を削るローパスフィルタです。
たとえるなら、アンケートの回答を近所どうしで持ち寄って平均を取るようなものです。平均化は「半径◯m 以内の人を全員同じ1票」で数え、ガウシアンは「近い人ほど重く数える」やり方です。後者のほうが、境界での不自然な切れ方が起きません。
式
ガウシアン:G(x, y) = (1 / 2πσ²) · exp( −(x² + y²) / 2σ² ) 分離可能:G(x, y) = G(x) · G(y) なので、横1回 → 縦1回で済む 計算量:素朴に2次元でやると O(N·k²)、分離すると O(N·2k)
注意点・見どころ
- ガウシアンは分離可能実装にしてあります。σ=5 なら窓は 31×31 = 961 回の積和ですが、横31回+縦31回の62回で同じ結果になります。15倍以上の差で、σ が大きいほど効きます。同好会で最初に見せる「実装の工夫」として分かりやすい題材です。
- 平均化(ボックス)は窓の重みが崖のように切れているため、周波数領域では sinc 関数になり、副次的な山(リンギング)が残ります。「ゾーンプレート」サンプルにかけると、ぼけきらない同心円が半径ごとに残るのが見えます。ガウシアンではこれが起きません。
- ガウシアンを繰り返しかけると、σ は単純に足されず二乗和の平方根で合成されます(σ₁ と σ₂ を続けてかけると √(σ₁²+σ₂²))。中心極限定理から、平均化を何度も繰り返してもガウシアンに近づきます。
- 境界は reflect101(端の画素を重複させない鏡映)で埋めています。ゼロ埋めにすると画像の縁が暗くなるためです。
- σ を大きくすると(10 前後から)待たされるので、そこからは Web Worker に逃がして複数のコアで分担します。分担は画像を横に切った行帯ごとで、縦方向の畳み込みが帯の上下にはみ出すぶんは、隣の帯と重ねて読み直しています。この「のりしろ」があるので、分担しても結果は一枚で計算したときと変わりません。
メディアン
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 窓の半径 | range | 1 | 1 〜 5(刻み 1) |
| 反復回数 | range | 1 | 1 〜 3(刻み 1) |
原理
窓の中の画素を値の順に並べ、真ん中の値を採用します。平均と違って「足し算をしない」のが要点で、窓の中に極端な値が混ざっても、それが順位の端にいる限り結果に影響しません。線形フィルタではないため、重ね合わせも分離もできません。
たとえるなら、審査員の点数から最高点と最低点を捨てて中央の評価を採るやり方です。1人が0点をつけても、平均と違って結果はほとんど動きません。
式
g(x, y) = median{ f(x+i, y+j) : |i| ≤ r, |j| ≤ r }
計算量:窓 k×k で素朴に O(N·k² log k)。窓が大きいときはヒストグラムを使った O(N·k) 実装が定石
注意点・見どころ
- 「ソルト&ペッパーノイズ」サンプルで、ガウシアンと交互に切り替えてください。ガウシアンは黒点を灰色の染みに変えるだけですが、メディアンは半径1でほぼ完全に消します。この差がメディアンの存在意義です。
- 一方でガウシアンノイズ(全画素に薄く乗るノイズ)にはあまり強くありません。「ガウシアンノイズ+段差」サンプルで試すと、平滑化としては平凡だと分かります。ノイズの種類で手法を選ぶ、という話に繋がります。
- エッジは保たれますが、角が丸くなり、細い線は消えます。窓の中で少数派になった時点で中央値から外れるためです。「ぼけた細線・格子」サンプルの細い棒が消える順番を見ると、窓サイズとの関係が分かります。
- 反復回数を増やすと「もうこれ以上変化しない」状態(ルート信号)に収束します。2〜3回で止まるのが普通で、非線形フィルタ特有の面白い性質です。
- 窓が大きいと重くなります。1スレッドでの実測(640×480)は、半径1 が約120 ms、半径3 が約670 ms、半径5 が約1.5秒。中央値を取り出す部分は、窓が25画素を超えたら全体を並べ替えずに済むクイックセレクトへ切り替えています。それでも半径3以上は待たされるので、そこから先は Web Worker に逃がして複数のコアで分担します。
- 反復回数を上げると、Worker の分担も反復1回ぶんずつ区切って進みます。反復2回目は1回目の結果を全部必要とするので、途中で待ち合わせる必要があるためです。「逐次表示」をオンにしておくと、同じ画像が上から順に2度3度と塗り替わっていくのが見え、反復で収束していく様子がそのまま観察できます。
バイラテラル
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| σ_space(距離) | range | 3 | 0.5 〜 8(刻み 0.1) |
| σ_color(値の差) | range | 30 | 2 〜 100(刻み 1) |
| 値の差の測り方 | select | luma | 輝度差(色ずれが出にくい) / RGB のユークリッド距離 |
原理
ガウシアン平滑化の重みに、もう一つ「値がどれだけ違うか」の重みを掛けます。近くにあっても値が大きく違う画素(=エッジの向こう側)はほとんど平均に参加しません。結果として、平坦な部分のノイズだけが消え、エッジは立ったまま残ります。
たとえるなら、近所の人の意見を聞くときに「距離が近い人」だけでなく「自分と意見が近い人」の声を重く採る合議です。道の向こう側の反対意見に引きずられないので、立場の境目がぼやけません。
式
g(p) = (1/W) Σ_q f(q) · exp( −‖p−q‖² / 2σ_s² ) · exp( −|f(p)−f(q)|² / 2σ_c² ) W = Σ_q (同じ2つの重みの積) 重みが画素ごとに変わるので線形ではなく、分離もできない → O(N·k²) が基本
注意点・見どころ
- 「ガウシアンノイズ+段差」サンプルで、ガウシアンと切り替えてください。ガウシアンは段差を一緒になまらせますが、バイラテラルは段差を残したままノイズを削ります。この一点がバイラテラルの価値です。
- σ_color を大きくすると値の差の重みが効かなくなり、ただのガウシアンに近づきます。逆に小さくしすぎると何も平均されず、恒等写像に近づきます。ノイズの標準偏差の2倍前後が目安です。
- σ_color を中途半端に上げると、エッジ付近に絵の具を塗ったような平板な質感(いわゆる「のっぺり」)が出ます。過去10年の美肌処理でよく見た副作用で、強くかけたときの見え方も含めて見せると説得力があります。
- 分離できないうえ重みが画素ごとに変わるため、素直に書くと重い処理です。ここでは重みを表引きにし、窓を σ_space の2倍で打ち切っていますが、それでも1スレッドなら 640×480・σ_space=3 で約0.6秒、σ_space=6 では約1.4秒かかります。この処理集の中で群を抜いて重い処理です。
- 重いので Web Worker に逃がしてあります。画像を横に切って行帯に分け、複数のコアで同時に計算しています。計算量そのものは変わりませんが、待っている間も画面が固まらず、12コア機で3〜4倍速くなります。「逐次表示」をオンにすると、処理済みの帯が上から順に降りてくるのが見えます — 分担のしかたがそのまま画面に出るので、並列化の説明にそのまま使えます。
- 重いのは工夫不足ではなく、計算量そのものが O(N·k²) だからです。0.31 メガ画素 × 13×13 の窓で、内側のループが5千万回以上回ります。根本的に速くするには双方向グリッド(bilateral grid)や permutohedral lattice のように、計算の仕組みごと変える必要があります。
- 「輝度差」を選ぶと、色ノイズがあっても平均の重みが安定します。「RGB距離」は原典に忠実ですが、彩度の高い境界で挙動が変わります。両方を切り替えて比べられるようにしてあります。
アンシャープマスク
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 量 | range | 1 | 0 〜 3(刻み 0.05) |
| 半径 σ | range | 1.6 | 0.3 〜 10(刻み 0.1) |
| しきい値 | range | 0 | 0 〜 60(刻み 1) |
| 差分マスクを表示(何を足しているか) | checkbox | false | オン / オフ |
原理
元画像から、ぼかした画像を引きます。残るのは「ぼかしで失われた成分」=細部だけで、これを元画像に足し戻すと細部が強調されます。名前は「ぼけていないマスク」ではなく、暗室でぼかしたネガをマスクに使った古典的な手法に由来します。
たとえるなら、写真から「大づかみな明暗」だけを取り除いて細かい模様を取り出し、それを元の写真に重ね刷りするようなものです。
式
mask = f − Gσ * f g = f + amount · mask (|mask| ≤ threshold の画素は mask = 0 とする) = (1 + a)·f − a·(Gσ * f) なので、正味は「ハイパスを足す」ことと同じ
注意点・見どころ
- 「ぼけた細線・格子」サンプルはあらかじめ σ=1.6 でぼかしてあります。同じ σ=1.6 を指定すると細部がよく戻ります。ぼけの大きさと半径を合わせる、という感覚を掴むのに向いています。
- 「差分マスクを表示」にチェックを入れると、実際に足し引きしている成分そのものが見えます(128 が0)。同好会では、まずこれを見せてから足し戻すと納得されやすいです。
- エッジの両側に明るい/暗い縁取り(ハロー)が出ます。半径を大きくするほど太く目立ちます。過剰なシャープネスが「不自然」に見える正体はこれです。
- しきい値は、平坦な部分のノイズまで強調しないためのものです。0 のままだと、ノイズの多い写真では粒状感が一気に増えます。「ガウシアンノイズ+段差」サンプルで、しきい値を上げていくと効果が分かります。
- 実は「元画像 − ぼかし」はラプラシアンの近似です。σ の違う2つのガウシアンの差(DoG)とも近い関係にあり、エッジ検出の Laplacian や LoG と地続きの処理です。
周波数領域
FFT スペクトル
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 表示 | select | power | 対数パワースペクトル / 位相(色相=角度/明るさ=強さ) / 振幅だけで再構成(位相を捨てる) / 位相だけで再構成(振幅を一定に) |
| 対数の効き | range | 3 | 0 〜 6(刻み 0.1) |
| 境界処理(2の冪への埋め方) | select | reflect | 鏡映で埋める(既定) / ゼロで埋める(十字が出ます) / Hann 窓を掛ける(十字が消えます) |
| 周波数の目盛りを重ねる | checkbox | true | オン / オフ |
原理
画像を「いろいろな細かさの縞模様の重ね合わせ」として書き直したものが、2次元フーリエ変換です。出てくるスペクトルは、中央が DC(画像全体の明るさ)、中央から離れるほど細かい縞に対応します。輝点の位置がその縞の向きと細かさを、明るさがその縞がどれだけ含まれているかを表します。
たとえるなら、音を「ドレミの成分がそれぞれどれだけ入っているか」に分解する作業の、2次元版です。楽譜のどの音がどれだけ鳴っているかを見るのと同じ要領で、画像のどの細かさの模様がどれだけ入っているかを見ます。
実装は Cooley-Tukey の FFT を自前で書いています。素朴に定義どおり計算すると 640×480 で 10¹¹ 回の掛け算になり現実的でないところを、「偶数番目と奇数番目に分けて再利用する」ことで N log N まで落としています。2次元は「行ごとに1次元 → 列ごとに1次元」に分けられる(分離可能)ので、1次元の FFT がひとつあれば足ります。
式
F(u, v) = Σ_x Σ_y f(x, y) · exp( −2πi (ux/W + vy/H) ) f(x, y) = (1/WH) Σ_u Σ_v F(u, v) · exp( +2πi (ux/W + vy/H) ) 表示しているのは log(1 + a·|F| / max|F|) / log(1 + a) (a = 10^対数の効き) 計算量:定義どおりなら O(N²)、FFT なら O(N log N)
注意点・見どころ
- 「ゾーンプレート」サンプルを開いてみてください。中心から外へ向かって縞が細かくなる画像なので、スペクトルはドーナツ状のリングになります。画像の見た目とスペクトルの対応が一番わかりやすい題材です。
- 「幾何図形」サンプルでは、直線に対して直角の向きに輝線が伸びます。エッジは「その向きに垂直な縞の集まり」だからです。斜線の束が別の角度の輝線を作るのも見えます。
- 「周期ノイズ(斜め縞)」サンプルでは、中心から離れた場所に孤立した輝点が1組出ます。これが縞の正体です。ここを潰せば縞だけが消えます(スペクトルマスク/周波数フィルタのノッチ)。同好会で一番効く流れは、この点を見せてから消しに行く順番です。
- 「境界処理」を「ゼロで埋める」に切り替えると、スペクトルに強い十字が現れます。FFT は画像が上下左右に無限に繰り返していると仮定するので、右端と左端の食い違いが「縦に走る段差」として扱われ、その成分が十字になって出ます。画像そのものには無い模様です。「Hann 窓」にすると縁が滑らかに落ちるので十字は消えます。既定の「鏡映」はその中間です。
- 「振幅だけで再構成」と「位相だけで再構成」を見比べてください。振幅だけだと何が写っていたか分かりませんが、位相だけにすると輪郭がはっきり読み取れます。画像の構造は主に位相のほうに入っている、という有名な事実がそのまま見えます。振幅は「どんな細かさの縞がどれだけあるか」しか言っておらず、「それがどこにあるか」は位相が持っているためです。
- 対数で圧縮して表示しています。DC の成分は他より 10⁵ 倍以上大きいことが普通で、線形のまま描くと中央の1点以外は真っ黒になります。「対数の効き」を 0 に近づけると、その真っ黒な状態が見られます。
- radix-2 の FFT は長さが2の冪でないと回らないので、640×480 の画像は 1024×512 の格子に広げてから変換しています。表示しているのはその 1024×512 のスペクトルを画面の大きさに縮めたものです。左端が −ナイキスト周波数(0.5 サイクル/画素=2画素で1周期)、中央が 0、右端が +ナイキスト手前にあたります。重ねてある円は 0.125 / 0.25 / 0.375 サイクル/画素の目盛りです。
周波数フィルタ
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 種類 | select | lowpass | ローパス(細かい成分を落とす=ぼかす) / ハイパス(大きな明暗を落とす=輪郭だけ) / バンドパス(ある細かさだけ通す) / バンドストップ(ある細かさだけ落とす) / ノッチ(特定の1点を潰す=周期ノイズ除去) |
| 切り方 | select | gauss | 理想(崖のように切る) / ガウシアン(なだらかに切る) / バターワース(切れ味を次数で選ぶ) |
| バターワースの次数 | range | 2 | 1 〜 10(刻み 1) |
| しきい周波数 | range | 0.06 | 0.005 〜 0.5(刻み 0.005) |
| 帯の幅 | range | 0.05 | 0.005 〜 0.4(刻み 0.005) |
| ノッチの位置 u(横) | range | 0.18 | -0.5 〜 0.5(刻み 0.002) |
| ノッチの位置 v(縦) | range | 0.09 | -0.5 〜 0.5(刻み 0.002) |
| ノッチの半径 | range | 0.03 | 0.005 〜 0.2(刻み 0.005) |
| 対象 | select | rgb | RGB 個別(色のまま処理) / 輝度のみ(3倍速い) |
| 境界処理(2の冪への埋め方) | select | reflect | 鏡映で埋める(既定) / ゼロで埋める |
| ビューアに出すもの | select | image | 処理後の画像 / フィルタを掛けたあとのスペクトル |
原理
空間領域での畳み込みは、周波数領域では「掛け算1回」になります(畳み込み定理)。そこで画像を FFT でスペクトルに直し、残したい成分に 1、消したい成分に 0 に近い値を掛けてから逆変換します。中心付近を残せばローパス(ぼかし)、外側を残せばハイパス(輪郭抽出)、輪状に残せばバンドパスです。
たとえるなら、オーディオのイコライザです。低音つまみを下げるのがハイパス、高音つまみを下げるのがローパス。特定の周波数だけをピンポイントで削るのがノッチ(ハウリング対策と同じ考え方)で、画像では周期ノイズの除去に使います。
式
G(u, v) = F(u, v) · H(u, v)、 g = IFFT(G) r = √(u² + v²)(正規化周波数、単位はサイクル/画素) 理想ローパス : H = 1 (r ≤ D₀), 0 (r > D₀) ガウシアン : H = exp( −r² / 2D₀² ) バターワース : H = 1 / (1 + (r/D₀)^(2n)) ハイパス : H_hp = 1 − H_lp バンドパス : H = H_lp(D₀+w/2) · (1 − H_lp(D₀−w/2)) ノッチ : H = 1 − D(‖(u,v)−(u₀,v₀)‖) − D(‖(u,v)+(u₀,v₀)‖)
注意点・見どころ
- まず「切り方」を理想にして、しきい周波数を下げてみてください。輪郭のまわりに同心円状の波紋が出ます。これがリンギング(Gibbs 現象)です。周波数を崖のように切ることは、空間領域では裾を引く sinc 関数を畳み込むことと同じで、その裾が波紋になります。「ガウシアン」に切り替えると波紋が消えます。ガウシアンはフーリエ変換してもガウシアンのままで、裾を引かないためです。
- バターワースは次数で切れ味を選べます。n=1 でなだらか、n を上げると理想に近づいてリンギングも戻ってきます。「切れ味と副作用は引き換え」という、フィルタ設計でいつも出てくる関係がそのまま見えます。
- ガウシアンのローパスは、空間フィルタの「ガウシアンぼかし」と同じ処理です。σ_space と D₀ は反比例の関係(D₀ ≈ 1/(2πσ))にあります。空間フィルタのガウシアンと結果を見比べると、まったく別の道を通って同じ場所に着くことが確認できます。
- ハイパスにすると平均の明るさ(DC 成分)まで落ちるので、結果は 0 のまわりに散らばり、クリップされて暗く潰れます。輪郭が見えていれば正常です。「差分」表示や、しきい周波数を小さくした状態から始めると読み取りやすくなります。
- ノッチが本命の使いどころです。「周期ノイズ(斜め縞)」サンプルに切り替え、まず「FFT スペクトル」で中心から離れた孤立した輝点を見つけてください。その位置を u, v に入れて半径を少し取ると、縞だけが消えて元の絵が戻ります。空間フィルタ(メディアンやガウシアン)では、絵をなまらせない限り縞は取れません。周波数領域でやる価値が一番はっきり出る例です。
- ノッチは必ず点対称の位置と一緒に潰しています。実数の画像のスペクトルは F(−u, −v) = conj(F(u, v)) という対称性を持っていて、片方だけ削るとこの対称が崩れ、逆変換の結果に虚部が残ってしまうためです。手で塗るマスク(スペクトルマスク)でも同じ理由で、筆は必ず反対側にも乗ります。
- 「対象」を輝度のみにすると3倍速くなりますが、出力は白黒になります。RGB 個別はチャンネルごとに独立して変換するので、640×480 で順変換と逆変換あわせて約 0.33 秒かかります。ワーカーに逃がしてあるので待っている間も画面は動きます。
- 640×480 の画像は 1024×512 の格子に広げてから変換しています(radix-2 の FFT は長さが2の冪でないと回らないため)。広げた部分は鏡映で埋め、逆変換のあとで元の大きさに切り戻しています。
スペクトルマスク(手で塗る)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| スペクトル(ドラッグで遮る/右ドラッグで戻す) | spectrum | (初期状態) | スペクトルの上をドラッグして編集 |
| マスクの縁のぼかし | range | 2 | 0 〜 10(刻み 1) |
| 反転(塗ったところだけ通す) | checkbox | false | オン / オフ |
| 対象 | select | rgb | RGB 個別(色のまま処理) / 輝度のみ(3倍速い) |
| 境界処理(2の冪への埋め方) | select | reflect | 鏡映で埋める(既定) / ゼロで埋める |
| ビューアに出すもの | select | image | 処理後の画像 / マスクを掛けたあとのスペクトル |
原理
周波数フィルタの通過率を、式で決める代わりに手で描きます。右のスペクトルを筆でなぞると、その周波数成分が落ちます。落とした状態で逆変換すれば、その成分だけが抜けた画像が戻ってきます。
たとえるなら、録音した音を波形ではなくスペクトログラムの上で編集する作業です。「この帯域のこの音だけ消す」を、耳ではなく目で狙って行います。画像でも同じことができる、というのがこの処理の主張です。
筆は必ず点対称の位置にも同時に乗ります。実数の画像のスペクトルは F(−u, −v) = conj(F(u, v)) を満たしていて、片側だけ消すとこの対称が崩れ、逆変換の結果に虚部が残って絵が壊れるためです。
式
G(u, v) = F(u, v) · M(u, v)、 g = IFFT(G) M は手描きのマスク(0 = 遮る 〜 1 = 通す) マスクは正規化周波数 −0.5〜0.5 を 128 等分した格子で持ち、 対称条件 M(−u, −v) = M(u, v) を塗るときに強制している
注意点・見どころ
- まず「周期ノイズ(斜め縞)」サンプルで試してください。スペクトルの中心から離れた場所に、孤立した輝点が1組あります。そこを塗ると縞だけが消え、下にあった絵が出てきます。空間フィルタでは絵をなまらせずに縞を取ることはできません。周波数領域を扱う理由が、この1回で伝わります。
- 次に「幾何図形」サンプルで、中心から放射状に伸びる輝線を1本だけ塗ってみてください。その向きのエッジだけが消えます。「向き」という情報がスペクトルのどこに入っているかが体で分かります。
- 中心(DC)を塗ると平均の明るさが失われ、画像全体が中間調に沈みます。逆に中心だけを残すとのっぺりした濃淡だけになります。中心付近が「大まかな明暗」、外側が「細かい模様」だという対応を確かめるのに使えます。
- 「マスクの縁のぼかし」を 0 にすると、崖のように切ることになるので輪郭に波紋(リンギング)が出ます。2〜4 に上げると消えます。周波数フィルタの「理想 vs ガウシアン」とまったく同じ話が、手描きでも起きます。
- 「反転」を入れると、塗ったところだけを通します。輝点を1組塗ってから反転すると、その縞だけを取り出した画像が見られます。ノイズを消すのと、ノイズを抽出するのは、マスクの裏表の関係にあります。
- プリセットのボタンで、ローパス・ハイパス・バンドパス・十字(縦横の成分を落とす)をすぐ作れます。そこから筆で足し引きするのが手早い使い方です。「十字」は、境界処理をゼロ詰めにしたときに出る十字を消すのに使えます。
- マスクは 128×128 の固定格子で持っているので、作業解像度を 640 から 1024 に上げても塗り直しは要りません。画像を差し替えても残ります。
- ビューアの「出すもの」を「マスクを掛けたあとのスペクトル」にすると、中央の大きな画面でスペクトルを確認できます。遮った場所には赤みが乗ります。画面共有では、大きいほうで位置を指しながら右で塗る、という使い方になります。
- 塗るたびに順変換・逆変換をやり直しています(640×480・RGB で約 0.33 秒)。筆を止めた 0.09 秒後にまとめて計算し、途中の値は捨てているので、ドラッグ中に計算が積み上がることはありません。
ノイズと評価
ノイズ付加
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 種類 | select | gauss | ガウシアン(読み出しノイズ) / ソルト&ペッパー(欠陥・伝送エラー) / ショット(光子数のゆらぎ) / 周期ノイズ(斜め縞。周波数領域で消せます) |
| 標準偏差 σ | range | 20 | 1 〜 60(刻み 1) |
| 飛ぶ画素の割合 | range | 0.05 | 0.005 〜 0.3(刻み 0.005) |
| 最大光子数 | range | 30 | 3 〜 300(刻み 1) |
| 縞の間隔 | range | 5 | 3 〜 30(刻み 0.5) |
| 縞の向き | range | 27 | 0 〜 175(刻み 1) |
| 縞の濃さ | range | 30 | 5 〜 60(刻み 1) |
| 色ごとに独立に乗せる(色ノイズになります) | checkbox | false | オン / オフ |
| 乱数の種 | range | 1 | 1 〜 20(刻み 1) |
原理
ノイズは「どこから来たか」で性質が変わり、性質が違えば効く除去手法も変わります。ここでは由来の異なる3種類を分けて乗せられるようにしています。
ガウシアンノイズは、センサの読み出し回路などで全画素に薄く乗る加算性のノイズです。値は元の明るさに関係なく、平均0・標準偏差σの正規分布で揺れます。
ソルト&ペッパーノイズは、一部の画素だけが真っ白か真っ黒に飛ぶノイズです。欠陥画素や伝送エラーが由来で、「少数の画素が完全に壊れる」という点で他の2つとまったく違います。
ショットノイズは、光そのものの粒々しさから来ます。同じ明るさの面を撮っても、届く光子の個数はポアソン分布でばらつきます。暗い部分ほど相対的なばらつきが大きくなるのが特徴で、この点だけが他の2つと違います。
周期ノイズは、上の3つとまったく毛色が違います。ランダムではなく、一定の間隔でくり返す縞が絵に重なったものです。印刷物をスキャンしたときの網点、電気的な干渉、機械の振動などが由来で、実際に起きるのはたいてい斜めの縞です。ランダムでない=規則があるということは、周波数領域で見れば一点に集まるということでもあります。だからこれだけは、絵をぼかさずに縞だけを抜き取れます。「スペクトルマスク」や「周波数フィルタ」の出番はここです。
たとえるなら、ガウシアンは「全員の回答に薄く測定誤差が乗る」状態、ソルト&ペッパーは「回答用紙が数枚だけ破れて読めない」状態、ショットは「そもそも集まった票数が少ないので割合がぶれる」状態、周期ノイズは「録音にハムが混じっている」状態です。ハムは決まった高さの音なので、その音だけを止められます。対処のしかたがそれぞれ違うことが想像できると思います。
式
ガウシアン:g = f + N(0, σ²) ソルト&ペッパー:確率 p で g = 0 または 255、それ以外は g = f ショット:g = Poisson(f · λ_max / 255) · 255 / λ_max (ショットだけは f に依存する=信号依存ノイズ) 周期:g = f + A · sin(2π(u₀x + v₀y))、u₀ = cosθ / T、v₀ = sinθ / T (T は縞の間隔、θ は向き。ランダムでないので乱数の種は効かない)
注意点・見どころ
- ショットノイズだけは、明るいところより暗いところが荒れます。光子数 N のポアソン分布は標準偏差が √N なので、相対的なばらつき √N / N = 1/√N は N が小さいほど大きくなるためです。「最大光子数」を 10 くらいまで下げて、階段サンプルの暗い段と明るい段を見比べてください。他の2種類では、この明暗による差は出ません。
- ガウシアンノイズはメディアンよりバイラテラルやガウシアンが効き、ソルト&ペッパーはメディアンが圧倒的に効きます。「ノイズ除去の比較」を開くと、この対応が数値で確かめられます。
- 「色ごとに独立に乗せる」を入れると、輝度だけでなく色もざらつきます。実際のカメラでは色ノイズのほうが目につきやすく、輝度ノイズと分けて処理するのが普通です(バイラテラルの「値の差の測り方」を輝度差にしてあるのも同じ理由です)。
- 乱数は座標から直接作っています(位置ハッシュ)。順番に引く乱数だと、行帯に切って別々のコアで計算したときに帯ごとに乱数列が食い違い、継ぎ目が横縞になって出てしまいます。同じ画素には必ず同じノイズが乗るようにしておけば、どう分担しても結果は変わりません。並列化と乱数の相性の悪さを避ける定石です。
- 「乱数の種」を変えると、同じ設定で別のノイズが出ます。除去結果がたまたま良かっただけでないかを確かめるのに使ってください。
- PSNR / SSIM は右下に出ます。σ=20 のガウシアンノイズで PSNR は 22 dB 前後です。この数値がどのくらいの見た目に対応するかを、ここで体に入れておくと後の比較が読みやすくなります。
- 周期ノイズを乗せたら、[PNG保存]してから読み込み直し、「スペクトルマスク」を開いてください。スペクトルに輝点が1組だけ立ち、黄色い破線が「ここ」と教えてくれます。塗ると縞だけが消えて、絵はぼけません。ぼかしでは絶対にこうならない(縞を消すころには絵も消えている)ので、周波数領域を使う理由がこの一往復で分かります。
- 「縞の向き」に 0°(縦縞)と 90°(横縞)は選ばないでください。スペクトルの中心を通る十字の上に乗ってしまい、輝点として狙えなくなります。十字には画像の縁の不連続などが必ず出るため、輝点さがしは十字を最初から見ないようにしてあります。実際の周期ノイズが斜めに出やすいのは幸運で、だからこの手が使えます。
- 「縞の間隔」を 3 画素まで詰めると輝点はスペクトルの外周へ、30 画素まで広げると中心へ寄ります。中心に寄せすぎると、絵そのものの大きな明暗と同じ場所に重なるので、塗ったときに絵まで持っていかれます。「細かいノイズほど取りやすい」というのは、この位置関係のことです。
- 乱数を使っていないので、「乱数の種」は効きません(隠してあります)。同じ座標には必ず同じ値が乗るため、行帯に切って別々のコアで計算しても継ぎ目は出ません。
- スペクトルマスクで消すときは、「筆の太さ」を 8、「マスクの縁のぼかし」を 1 にしてください。既定の「筆5・ぼかし2」だと落ち切りません(640×480 の実測で、PSNR が 21.6 dB → 27.0 dB までしか戻らない。筆8・ぼかし1 なら 43.6 dB)。ぼかしは筆より小さくするのが原則で、同じか大きいとマスクの谷が浅くなり、縞が半分しか落ちません。
- 白飛び・黒つぶれしやすい絵には乗せないでください。明るい背景(たとえば 242)に振幅 30 の縞を足すと 255 で頭打ちになり、サインの山が平らに刈り取られます。歪んだ波は基本波のちょうど 2 倍の位置にも輝点を作るので、印が 4 つではなく 6 つになり、しかも飽和で消えた情報は塗っても戻りません(合成図形のサンプルで実測 33.9 dB。飽和の少ない絵なら 40.6 dB)。音でいえば、アンプを歪ませると倍音が乗って音色が変わるのと同じことです。教材としては面白いので、わざと白い絵で試してみるのも一興です。
- 輝点は 4 つ出ます。強い 1 組が本物で、弱いもう 1 組は「2の冪へ広げるときに鏡で折り返している」ことの副産物です(鏡に映すと縞の傾きが左右反転するため)。本物の 1 組だけを塗るほうが結果は良くなります(43.6 dB 対 41.1 dB)。境界処理の都合がスペクトルに顔を出す例として、そのまま見せられます。
ノイズ除去の比較
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 表示 | select | tile | 4分割で並べる(実演向け) / ノイズあり(全解像度) / メディアン(全解像度) / バイラテラル(全解像度) / ガウシアン(全解像度) |
| ノイズの種類 | select | gauss | ガウシアン(読み出しノイズ) / ソルト&ペッパー(欠陥・伝送エラー) / ショット(光子数のゆらぎ) |
| ノイズの σ | range | 20 | 1 〜 60(刻み 1) |
| 飛ぶ画素の割合 | range | 0.05 | 0.005 〜 0.3(刻み 0.005) |
| 最大光子数 | range | 30 | 3 〜 300(刻み 1) |
| ノイズを色ごとに独立に乗せる | checkbox | false | オン / オフ |
| 乱数の種 | range | 1 | 1 〜 20(刻み 1) |
| メディアンの窓の半径 | range | 1 | 1 〜 4(刻み 1) |
| バイラテラルの σ_space | range | 2 | 0.5 〜 5(刻み 0.1) |
| バイラテラルの σ_color | range | 40 | 2 〜 100(刻み 1) |
| ガウシアンの σ | range | 1.5 | 0.3 〜 6(刻み 0.1) |
原理
ノイズの種類によって、効く除去手法が変わります。それを同じ画像・同じノイズで並べて確かめるための処理です。原画にノイズを乗せ、メディアン・バイラテラル・ガウシアンの3手法で消し、原画との PSNR / SSIM を並べます。
見どころは2つあります。1つめは「万能な手法は無い」こと。ソルト&ペッパーにはメディアンが圧勝しますが、ガウシアンノイズでは最下位に落ちます。2つめは「物差しによって答えが変わる」こと。PSNR で選ぶ手法と SSIM で選ぶ手法は、しばしば別になります。
たとえるなら、汚れの落とし方の比較です。泥はねなら1粒ずつ取り除く(メディアン)のが早く、全体のくすみなら面でならす(ガウシアン)ほうが早い。柄を消さずにくすみだけ取るには手間のかかるやり方(バイラテラル)が要る、という関係です。
式
PSNR = 10 · log₁₀( 255² / MSE ) MSE = 平均二乗誤差(RGB 3チャンネル) SSIM = 平均 [ (2μxμy + C₁)(2σxy + C₂) / ((μx² + μy²+ C₁)(σx² + σy² + C₂)) ] 窓は 11×11・σ=1.5 のガウシアン、C₁ = (0.01·255)²、C₂ = (0.03·255)²
注意点・見どころ
- まず「ノイズの種類」を〈ソルト&ペッパー〉にしてください。メディアンが両方の物差しで圧勝し、PSNR は他の2つを 14 dB 以上引き離します。ここでのバイラテラルはほとんど効きません(ノイズあり 18.5 dB に対して 19.1 dB)。飛んだ画素の値が周りとかけ離れているせいで、バイラテラルがそれを「エッジ」と見なして守ってしまうためです。手法の性格が裏目に出る例として分かりやすいところです。
- 次に〈ガウシアン〉に戻すと、メディアンは最下位に落ちます。ノイズの種類で順位が入れ替わる — 万能な手法は無い、というのがこの処理の主題です。
- PSNR と SSIM はよく食い違います。★は列ごとに付くので、2つの★が別の手法に付くことがあります。〈ジーメンススター〉がその例で、PSNR ではバイラテラル、SSIM ではガウシアンが勝ちます。PSNR は誤差の大きさしか見ないのに対し、SSIM は局所の平均・コントラスト・模様の並び方を見るためです。バイラテラルは平坦なところに残ったノイズを「エッジらしきもの」として守ってしまうので、誤差は小さいのに局所のばらつきが残り、SSIM で損をします。どちらに★が付くかは絵によって変わるので、サンプルを何枚か切り替えてみてください。
- ぼかしが必ず得になるとは限りません。〈幾何図形〉や〈ゾーンプレート〉のような細い線の多い絵では、ガウシアンの PSNR が「ノイズあり」を下回ります(幾何図形で 22.0 dB 対 23.6 dB)。σ=1.5 のぼかしが線とエッジを壊す損失のほうが、消したノイズの得より大きいためです。ところが同じ場面で SSIM は 0.850 と高く出ます。面積の大半を占める平坦な背景が滑らかになるからで、2つの物差しが正反対を指す一番はっきりした例です。
- ガウシアンの σ を上げていくと、PSNR はどこかで頭打ちになって下がりはじめますが、SSIM はそこからしばらく伸び続けます。「どこまでぼかすのが最適か」の答えが物差しによって違う、ということです。数値をひとつだけ見て決めてはいけない、という話にそのまま使えます。
- 〈4分割で並べる〉は、1枚の画像の中に4面を貼り合わせて返しています。ビューアが「原画1枚 ⇄ 処理後1枚」の作りなので、並べるところまで処理の側でやってしまうほうが簡単だからです。そのぶん各面は縦横とも半分の解像度になります(4面とも同じ縮め方なので、比較の公平さは保たれます)。粒の立ち方まで見たいときは、表示を1手法に切り替えて全解像度で見るか、ルーペを当ててください。
- 数値は必ず「原画」との比較です。ノイズを乗せる前の状態を持っているのは、この処理の中だけです。ふだんの画像処理で PSNR が測れないのは、正解にあたる原画が手元に無いからで、ここではノイズを自分で乗せることでその正解を用意しています。
- この処理は行帯に分けられないので、1つのワーカーに丸ごと渡しています。右下の「実行」欄に Worker 1 並列と出るのはそのためです。PSNR / SSIM が画像全体の集計で、帯ごとに出せないのが理由です。
- バイラテラルの σ_space は既定を 2.0 にしてあります(単体の処理では 3.0)。3手法ぶんを続けて計算するので、待ち時間を1秒以内に収めるためです。時間をかけてよければ上げてください。
エッジ検出
Sobel / Prewitt
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 演算子 | select | sobel | Sobel(1:2:1) / Prewitt(1:1:1) / Scharr(3:10:3) |
| 表示 | select | mag | 勾配強度 |∇f| / X方向の微分(横のエッジ検出) / Y方向の微分(縦のエッジ検出) / 勾配方向(色相)+強度(明るさ) |
| 表示ゲイン | range | 1 | 0.2 〜 8(刻み 0.1) |
原理
明るさの変化率(1階微分)を求めます。ノイズがあると素の差分は暴れるので、微分する方向と直交する方向に平滑化を掛け合わせた 3×3 の窓を使います。Sobel は平滑化の重みが 1:2:1、Prewitt は 1:1:1、Scharr は 3:10:3 で、この重み以外は同じ骨格です。
たとえるなら、坂の傾きを測るのに、1歩ぶんの高低差だけで判断せず、横に少し幅を取って均してから測るようなものです。足元の小石(ノイズ)に振り回されずに済みます。
式
Sobel_x = [−1 0 +1; −2 0 +2; −1 0 +1] = [1 2 1]ᵀ ⊗ [−1 0 +1] |∇f| = √(gx² + gy²)、 方向 θ = atan2(gy, gx) 分離可能なので 3×3 でも積和は 9 回でなく 6 回で済む
注意点・見どころ
- 「勾配方向」表示は、色相が向きを、明るさが強さを表します。「幾何図形」サンプルで見ると、円の周りで色相が一周し、平行な直線が同じ色になるのが分かります。方向という情報が実在することを見せるのに一番効く表示です。
- X方向の微分は「縦のエッジ」に反応します(横方向に値が変化しているから)。ここは同好会でも毎回ひっかかる点なので、gx と gy を切り替えて見せると早いです。
- Scharr は 3×3 の枠の中で回転対称性が最も良くなるよう重みを決めたもので、斜めのエッジで角度誤差が小さくなります。斜線の束を「勾配方向」で見ると差が出ます。
- 1階微分はエッジの「位置」ではなく「強さ」を返します。太い帯として出るので、細い線に絞るには非極大抑制が要ります。それをやるのが Canny です。
- 実務では、この勾配強度をそのまま閾値処理してエッジとする場面も多く、その場合は照明ムラに弱いという弱点がついて回ります。
Laplacian / LoG
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 種類 | select | log | Laplacian(4近傍) / Laplacian(8近傍) / LoG(ガウシアンで平滑化してから) |
| σ(LoG の平滑化) | range | 1.6 | 0.4 〜 8(刻み 0.1) |
| 表示 | select | signed | 符号付き(128 が 0) / 絶対値 / ゼロ交差(エッジ位置) |
| 表示ゲイン | range | 4 | 0.5 〜 20(刻み 0.5) |
| ゼロ交差の最小段差 | range | 4 | 0 〜 30(刻み 0.5) |
原理
2階微分をとります。1階微分(Sobel)がエッジで山になるのに対し、2階微分はエッジの手前で正、向こう側で負になり、エッジのちょうど真上で 0 を横切ります。この「ゼロ交差」がエッジの位置を1画素の精度で示します。方向を持たない(回転対称に近い)のも特徴です。
たとえるなら、坂道を車で走ったときの「傾き」ではなく「体にかかる前後の揺れ」を見るようなものです。坂の始まりで押しつけられ、終わりで引かれ、坂の真ん中では揺れがゼロになります。
式
∇²f = ∂²f/∂x² + ∂²f/∂y² 4近傍:[0 1 0; 1 −4 1; 0 1 0] 8近傍:[1 1 1; 1 −8 1; 1 1 1] LoG:∇²(Gσ * f) = (∇²Gσ) * f … 平滑化と微分の順序は交換できる
注意点・見どころ
- 2階微分はノイズに非常に弱いので、素の Laplacian を実画像にかけるとほぼノイズしか出ません。先にガウシアンで平滑化する LoG が実用形です。σ を 0.4 から上げていくと、ノイズが引っ込んで構造が出てくる様子が見えます。
- 「平滑化してから微分」と「微分したガウシアンを畳み込む」は数学的に同じです(畳み込みの結合則)。実装ではどちらを選んでもよく、ここでは前者を使っています。
- ゼロ交差表示は閉じた輪郭を作りやすい一方、コントラストが弱い場所でも律儀にエッジを引きます。「最小段差」で弱い交差を捨てられるようにしてあります。
- LoG は σ の違う2つのガウシアンの差(DoG)で近似でき、それが SIFT のキーポイント検出に使われています。アンシャープマスクの差分マスクとも兄弟関係で、同じ道具が用途によって名前を変えている例です。
- 8近傍は斜め方向にも反応するぶん感度が高く、そのぶんノイズも拾います。4近傍と切り替えて比べてください。
Canny(段階表示つき)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 表示する段階 | select | final | ① 平滑化後 / ② 勾配強度 / ③ 勾配方向(4方向に量子化) / ④ 非極大抑制の後 / ⑤ 二重閾値(白=強 / 灰=弱) / ⑥ ヒステリシス後(最終結果) |
| σ(平滑化) | range | 1.4 | 0.4 〜 6(刻み 0.1) |
| 下側しきい値 | range | 20 | 0 〜 255(刻み 1) |
| 上側しきい値 | range | 55 | 0 〜 255(刻み 1) |
原理
1986年に John Canny が「良い検出・良い位置決め・1本の応答」という3つの条件から導いた手順です。①ガウシアンで平滑化 →②勾配を求める →③方向に沿って非極大を抑制し1画素幅にする →④2つのしきい値で強/弱に分ける →⑤強エッジと繋がっている弱エッジだけを残す、の5段構えになっています。
たとえるなら、山の稜線を地図に引く作業です。まず等高線を滑らかにし、傾きを測り、尾根の頂点だけを残し、はっきりした尾根を確実に採用したうえで、そこから続いている自信のない部分も辿って繋ぎます。
式
① S = Gσ * f ② gx, gy = Sobel(S)、|∇S| = √(gx²+gy²)、θ = atan2(gy, gx) ③ NMS:|∇S|(p) が θ 方向の両隣以上でなければ 0 ④ 強 = |∇S| ≥ high、弱 = low ≤ |∇S| < high ⑤ 強に8連結で繋がる弱だけを残す
注意点・見どころ
- 「表示する段階」を①から順に切り替えると、なぜこの手順が必要なのかが1つずつ分かります。同好会ではこの順に見せるのが一番効きます。とくに③→④で太い帯が1本の線に痩せる瞬間が山場です。
- しきい値は勾配強度の最大値を255に正規化した値で指定しています。画像ごとに絶対値が大きく変わるので、この正規化がないとスライダーの目盛りが意味を持ちません。実装によっては大津の方法などで自動決定します。
- high : low は 2:1〜3:1 が経験則です。⑤の表示で灰色(弱エッジ)がどこに散らばっているかを見てから⑥に切り替えると、ヒステリシスが何を拾い何を捨てたかが分かります。
- σ を上げるとノイズに強くなる代わりに、エッジの位置がずれ、近接した2本のエッジが融合します。検出性能と位置精度はトレードオフだ、というのが Canny の議論の核心部分です。
- 非極大抑制で見ている「隣」は、勾配方向を4つに丸めた先の画素です。より正確にやるなら、方向に沿って線形補間した値と比べます。ここでは実装が読めることを優先して丸めています。
- 出力は2値の線画なので、この後は輪郭追跡やハフ変換に渡すのが定番の流れです。
二値化とモルフォロジー
二値化(固定/大津/適応的)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 閾値の決め方 | select | otsu | 固定(手で決める) / 大津の判別分析法(自動) / 適応的(場所ごとに決める) |
| 閾値 | range | 128 | 0 〜 255(刻み 1) |
| 窓の半径 | range | 16 | 3 〜 60(刻み 1) |
| 差の下駄 C | range | 6 | -30 〜 30(刻み 1) |
| 窓の測り方 | select | mean | 平均(積分画像・窓によらず一定時間) / ガウシアン加重平均 |
| 白黒を入れ替える | checkbox | false | オン / オフ |
原理
明るさがしきい値を超えたら白、超えなければ黒に振り分けて、画像を2値だけの絵にする処理です。ここから先の「形を数える」処理(面積、個数、輪郭、モルフォロジー)は、たいていこの二値画像を入口にします。
難しいのは「しきい値をいくつにするか」だけで、この処理はその決め方を3通り用意してあります。固定は手で決める。大津はヒストグラムの形から自動で決める。適応的は1枚に1つではなく、画素ごとにその周りを見て決める。
たとえるなら、答案の合格ラインの引き方です。固定は「60点以上」と先に決め打つやり方、大津は点数の分布を見て「ここで2つの山が一番きれいに割れる」ところに引くやり方、適応的は「クラスごとの平均点を基準に引く」やり方です。教室によって難易度が違うなら、最後のやり方でないと公平になりません。
式
固定・大津:g(x, y) = 255 if f(x, y) > t, else 0 大津:t = argmax σ²_b(t)、σ²_b(t) = ω₀(t)·ω₁(t)·( μ₀(t) − μ₁(t) )² ω は各クラスの画素の割合、μ は各クラスの平均。分離度 η = σ²_b / σ²_total 適応的:g(x, y) = 255 if f(x, y) > mean_W(x, y) − C mean_W は (x, y) を中心とする窓 W の平均。積分画像 S を使うと 窓の合計 = S(x₁,y₁) − S(x₀,y₁) − S(x₁,y₀) + S(x₀,y₀) の引き算3回で出る
注意点・見どころ
- まず〈幾何図形〉サンプルで「固定」を選び、閾値を動かしてください。右下に出る「分離度 η」が一番大きくなるところを手で探して、そのあと「大津」に切り替えると、同じ値に落ち着きます。大津がやっているのは、この手探りを 256 通り全部試すことだけです。
- 大津の前提は「ヒストグラムに山が2つある」ことです。〈グレー階段+ランプ〉のように山がたくさんある絵や、〈ゾーンプレート〉のように山が1つしかない絵では、出てくる閾値に意味がありません。分離度 η がその目安になります。η が 0.7 を下回るようなら、大津の答えは信用しないほうがよいと思ってください。
- 〈低コントラスト(暗部にディテール)〉サンプルで、大津と適応的を切り替えてみてください。この絵は左上が暗く右下が明るいので、1枚に1つの閾値では、暗い側が全部黒に、明るい側が全部白に潰れます。適応的にすると、どちらの側の細かい模様も同じように出ます。照明のムラがある書類をスキャンしたときに起きることと同じです。
- 適応的の「差の下駄 C」は、周りの平均とほとんど同じ明るさの画素をどちらに寄せるかの調整です。C を 0 にすると、真っ平らな場所(周りと同じ明るさ)ではノイズの上下だけで白黒が決まってしまい、ごま塩のような模様が出ます。C を少し足すと、しきい値が平均より C だけ下がるので、平坦なところはまとめて白に倒れて模様が消えます。書類のスキャンで「地の紙は白、文字だけ黒」にしたいときに効くのがこの下駄です。逆に C をマイナスにすると平坦なところが黒く塗り潰れるので、動かすなら 0 以上が実用の範囲です。
- 窓の平均は積分画像で出しています。窓の中を毎回足すと窓の面積に比例して重くなりますが、積分画像を1枚作っておけば、どんな大きさの窓でも足し引き4回で済みます。窓の半径を 3 から 60 まで動かしても右下の「計算時間」がほとんど変わらないのは、そのためです。〈ガウシアン加重平均〉のほうは畳み込みなので、半径を上げると重くなります。見比べると、一定時間で済むありがたみが分かります。なお、この処理の「計算時間」はベンチマークと同じ測り方で出しています(必ず Web Worker で計算し、1回空回ししてから2回まわして速いほうを採る)。1回きりだとごみ集めやワーカーの温まり具合だけで数倍に振れて、「変わらない」ことが読めなくなるためです。比べるときは「計算時間」の行を見てください(「画面に出るまで」は3回まわすぶん3倍ほどになります)。
- 窓を小さくしすぎると、太い線や広い面の内側が「周りと同じ明るさ」になってしまい、真ん中が抜けて輪郭だけが残ります。窓は「消したい構造よりも大きく」が目安です。〈ぼけた細線・格子〉サンプルで半径を動かすと、抜け始める境目が見えます。
- 大津の閾値も窓の平均も、画像全体を見ないと出せないので、prepare で先に作ってしまっています。おかげで画素ごとの処理は「1つの数と比べるだけ」になり、帯の境目で閾値が食い違う心配がありません。ただし、この処理は複数のコアに分担させても速くなりません。重い計算がその前段にあり、そこはワーカーごとに丸ごと繰り返されてしまうからです。そのため FFT や比較と同じく、まとめて1つのワーカーへ渡しています。右下の「実行」欄に Worker 1 並列と出るのはそのためです。
モルフォロジー(膨張・収縮・開閉)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 操作 | select | dilate | 膨張(白をふくらませる) / 収縮(白をやせさせる) / オープニング(収縮→膨張) / クロージング(膨張→収縮) / トップハット(元 − オープニング) |
| 構造要素の形 | select | rect | 四角(分離できる) / 十字(分離できる) / 円(分離できない) |
| 構造要素の半径 | range | 2 | 1 〜 8(刻み 1) |
| 反復回数 | range | 1 | 1 〜 3(刻み 1) |
| 先に二値化する(大津) | checkbox | true | オン / オフ |
| 白黒を入れ替える | checkbox | false | オン / オフ |
| 結果を伸ばして表示する | checkbox | true | オン / オフ |
原理
「構造要素」と呼ぶ小さな窓を画像の上で滑らせ、窓の中の最大値(膨張)か最小値(収縮)を採る処理です。二値画像なら「窓の中に白が1画素でもあれば白にする」のが膨張、「窓が全部白のときだけ白を残す」のが収縮になります。形そのものを太らせたり痩せさせたりする操作なので、面積や個数を数える前の下ごしらえによく使われます。
この2つを組み合わせると、大きさで選り分けができるようになります。収縮してから膨張する(オープニング)と、構造要素より細いものは収縮の段で消えてしまい、膨張しても戻ってきません。太いものだけが元の大きさに戻ります。逆順(クロージング)なら、小さな穴やすき間だけが埋まります。
たとえるなら、ふるいです。オープニングは目より小さい粒を落とすふるい、クロージングは隙間に砂を流し込んで埋める作業。トップハットは「ふるいで落ちたものだけを集める」操作にあたります。
式
膨張:(f ⊕ B)(x) = max{ f(x − b) : b ∈ B }
収縮:(f ⊖ B)(x) = min{ f(x + b) : b ∈ B }
オープニング:f ∘ B = (f ⊖ B) ⊕ B クロージング:f • B = (f ⊕ B) ⊖ B
トップハット:f − (f ∘ B)
双対性:f ⊕ B = 255 − ( (255 − f) ⊖ B )
注意点・見どころ
- この処理は「明るいほう」を前景として扱うので、白がふくらみます。〈幾何図形〉のように黒い図形が白い紙に載っている絵では、膨張を選ぶと図形のほうが痩せて見えます。〈白黒を入れ替える〉を入れると図形が白になり、今度は図形がふくらみます。膨張と収縮は白黒を入れ替えると入れ替わる、というのがモルフォロジーの双対性です。式にすると f ⊕ B = 255 − ((255 − f) ⊖ B)。
- 〈ソルト&ペッパーノイズ〉サンプルで、オープニングとクロージングを見比べてください。オープニングは白い粒(ソルト)だけを消し、クロージングは黒い粒(ペッパー)だけを消します。両方消したいなら片方をかけてからもう片方をかけます。メディアンが一度に両方消すのと比べると、モルフォロジーは「どちらを消すか選べる」のが持ち味だと分かります。
- オープニングとクロージングは冪等です。つまり2回かけても1回かけたのと同じ結果になります(反復回数を2にしても絵が変わらないのはそのため。膨張と収縮のほうは反復するたびに進みます)。「これ以上ふるいにかけても落ちるものはない」という、ふるいとしての性質そのものです。
- トップハットは「元の絵 − オープニングした絵」です。オープニングが太いものだけを残すので、引き算すると細いもの・小さいものだけが残ります。〈先に二値化する〉を外してグレースケールのまま〈低コントラスト(暗部にディテール)〉にかけると、大きな明暗のムラだけがオープニングで残り、引き算で消えます。照明ムラを取り除いて細部だけ取り出す、という実務でよくある使い方がそのまま見えます。構造要素の半径を「残したいものより大きく」するのがコツです。
- 構造要素の形で結果が変わります。四角は角ばった太り方をし、円は等方に太ります。十字は縦横だけに伸びるので、斜めの線には効きが弱くなります。〈幾何図形〉の斜線の束にかけると違いがはっきり出ます。理屈のうえでは円が素直ですが、四角と十字には「分離できる」という実装上の利点があります。
- 四角の窓の最大値は「横に最大 → 縦に最大」の2段で同じ結果になります。ガウシアンの分離と同じ理屈で、窓の面積 k² に比例していた計算が一辺 2k で済みます。十字は横線と縦線の和集合なので、横だけの最大と縦だけの最大を取って大きいほうを採れば出ます。円だけはこの手が使えないので素朴に回しています。640×480・半径8の膨張で、四角が 50 ms、十字が 51 ms なのに対し、円は 305 ms。6倍の差です。3つを切り替えて右下の「計算時間」を見比べると、分離できることの値打ちがそのまま読めます。
- この処理の「計算時間」は、ベンチマークと同じ測り方で出しています。①軽い四角や十字でも必ず Web Worker で計算する(右下の「実行」欄がいつも Worker 1 並列になるのはそのため)。②1回空回ししてから2回まわし、速かったほうを採る(「(2回の最小)」という但し書きが付きます)。どちらも速くするためではなく、3つの形を同じ条件で測るためです。 なぜここまでするかというと、1回きりの計測が当てにならないからです。実測で、同じ設定・同じ経路のまま円が 192 ms と 447 ms になりました(2.3倍)。じゃまが入る理由は2つあって、1つはメインスレッドで計算するとページの大きなデータごしに走るためごみ集め(GC)が挟まること、もう1つはワーカーが作られた直後だとコードがまだ最適化されていないこと。どちらも「じゃまが入った回」を作るので、最小値を採れば「じゃまが入らなければこの速さ」が読めます。node docs/_bench.js が「1回空回ししてから3回計測してその中央値」を採っているのと同じ理屈です。 そのぶん計算は3回まわるので、「画面に出るまで」は計算時間の3倍ほどになります。速さを見せたいなら測り方を揃える、という話で、性能を語るときの心得そのものです。
- さらに速くする定石として、van Herk / Gil-Werman のアルゴリズムがあります。1次元の最大値を「前向きの累積最大」と「後ろ向きの累積最大」の2本に分けて持つと、窓の大きさによらず1画素あたり定数回で済みます。ここでは実装していませんが、二値化の窓平均を積分画像で出したのと同じ発想です。
- オープニングは「収縮の結果が全部そろってから膨張する」ので、行帯に切って別々に計算できません。帯の境目で、隣の帯にはみ出した収縮の結果が使えないためです。そこで prepare で最後まで作りきり、丸ごと1つのワーカーに渡しています。
センサと幾何
デモザイク(バイリニア vs AHD)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 表示 | select | tile | 4分割で並べる(実演向け) / 原画(全解像度) / Bayer 生データ(全解像度) / バイリニア(全解像度) / AHD(全解像度) |
| CFA の並び | select | RGGB | RGGB(左上が R) / BGGR(左上が B) / GRBG(左上が G・右が R) / GBRG(左上が G・右が B) |
| Bayer の見せ方 | select | color | モザイク(受け取った色に置く) / 素の値(グレー) |
| AHD の後処理(色差メディアン) | range | 1 | 0 〜 3(刻み 1) |
原理
デジタルカメラのセンサは、1つの画素で1つの色しか測れません。そこで画素ごとに赤・緑・青のどれか1色だけを通すフィルタを市松に並べ(カラーフィルタ配列、Bayer 配列)、足りない2色をあとから計算で埋めます。この埋める処理がデモザイクです。
つまり、私たちが見ているカラー写真は、画素の 2/3 が推測で埋められた絵だということになります。緑が半分、赤と青が 1/4 ずつなのは、人の目が緑の周波数帯に一番敏感で、明るさの手がかりの大半を緑が担っているからです。
ここでは原画をいったん Bayer 配列に間引いてから2つのやり方で戻し、原画と突き合わせます。ふだんは測れない「正解」が手元にあるので、どれだけ正しく埋められたかを数字で言えます。
たとえるなら、虫食いだらけの楽譜を復元する作業です。バイリニアは前後の音符の平均でとりあえず埋める。AHD は「横に読んだ場合」と「縦に読んだ場合」の両方を書いてみて、前後と自然につながるほうを採ります。
式
バイリニア(G の場合):Ĝ(x,y) = ( G(x−1,y) + G(x+1,y) + G(x,y−1) + G(x,y+1) ) / 4
AHD の方向別 G(Hamilton-Adams、横向き):
Ĝ_h(x,y) = ( G(x−1,y) + G(x+1,y) ) / 2 + ( 2C(x,y) − C(x−2,y) − C(x+2,y) ) / 4
C は中心画素の色(R か B)。第2項は「G と C の差が滑らかである」ことを使った補正
色差の補間:R̂ = Ĝ + bilinear( R − Ĝ ) B̂ = Ĝ + bilinear( B − Ĝ )
一様性:H_d(x) = #{ y ∈ N₄(x) : |L_d(x) − L_d(y)| ≤ εL かつ ‖ab_d(x) − ab_d(y)‖² ≤ εab }
εL = min( 横向きの左右差の大, 縦向きの上下差の大 ) (εab も同様)
3×3 の和が大きいほうの向きを採る
注意点・見どころ
- 偽色やジッパーノイズは1〜2画素の細かさで出るので、〈4分割で並べる〉では見えません。4分割は各面を 2×2 の箱平均で半分に縮めており、その平均そのものが偽色を薄める働きをするからです(実測で、色差の誤差の平均が縮める前の 45〜58% に落ちます)。4分割は「どちらが良いか」を数値で一目で示すための表示で、粒を見るための表示ではありません。
- 粒を見たいときは、表示を〈バイリニア(全解像度)〉にしてキー `4`(差分表示)を押してください。原画との差が増幅されて出るので、どこにどれだけ誤差が乗っているかがそのまま見えます。〈ゾーンプレート〉なら、外側の細かいリングのところに網目状の誤差がはっきり出ます。そのまま表示を〈AHD(全解像度)〉に切り替えると、同じ場所の誤差が目に見えて減ります。差分の倍率スライダーを上げると、弱い誤差も拾えます。
- 色そのものを見たいときは、〈バイリニア(全解像度)〉のままキー `2`(処理後)に戻し、ルーペの倍率を 16 以上に上げて細かいところに当ててください。白黒の縞のはずのところに、緑やピンクの色が乗っているのが見えます。右下の「偽色(色差の誤差)」がその量で、〈ゾーンプレート〉では 4.35 → 1.03、〈ジーメンススター〉では 1.09 → 0.23 まで下がります。ジーメンススターは偽色が弱い(ゾーンプレートの 1/4)ので、目で見るならゾーンプレートのほうが向いています。
- ジッパーノイズは「横に読むか縦に読むか」を間違えたときに出ます。細い横線を縦向きに補間すると、線のある行と無い行を混ぜてしまい、1行おきに明るさが振れます。これが階段状の縞に見えるのでジッパーと呼ばれます。AHD が横向きと縦向きの両方を計算してから選ぶのは、この間違いを避けるためです。
- 偽色は「明るさは合っているのに色だけ違う」形で出ます。緑は半分の画素で測れているのに、赤と青は 1/4 しかないためです。細かい模様のところでは赤と青の推測が外れ、白黒の縞にピンクや緑が乗ります。対策として色差(R−G, B−G)を補間するのが定石で、AHD もバイリニアの色差補間を土台にしています。
- AHD の「一様性」は、上下左右の4近傍と比べて「明るさも色も近い」ものがいくつあるかを数えたものです。正しい向きに補間できていれば模様がつながるので近傍と馴染み、間違えていれば1画素おきに暴れるので馴染みません。許容範囲(ε)を固定値ではなく、その場所の横方向・縦方向の差から作っているのがこの手法の要で、平坦なところでは厳しく、模様のあるところでは緩く判定されます。
- 「AHD の後処理(色差メディアン)」の効き目は、〈ソルト&ペッパーノイズ〉で〈AHD(全解像度)〉にすると一番はっきり見えます。飛んだ画素はそこで1色しか測れていないので、戻すと色のついた点になります。メディアンを 0 → 1 → 3 と上げると、その色だけが消えていきます。実測(640×480)で、色差の誤差が強い画素(20階調超)が 20,885 → 3,172 → 482 画素、平均が 6.08 → 3.41 → 1.86 まで下がります。 白黒の粒はそのまま残ります。メディアンをかけているのは色差(R−G と B−G)だけで、明るさには触っていないからです。輝度の誤差は 6.86 → 5.59 とほとんど動きません。「色ノイズだけを消して解像感は落とさない」という、実際のカメラの絵作りでも使われる手です。 ただしタダではありません。同じ絵で PSNR は 21.7 → 22.9 dB と良くなる一方、SSIM は 0.882 → 0.858 とわずかに下がります。細かいものが少しだけ均されるためで、かけすぎない(既定は1回)のが無難です。 なお〈ゾーンプレート〉のようにノイズの無い絵では、AHD の段階ですでに強い偽色が残っていないので(色差の誤差の最大が 14 階調)、メディアンを動かしても見た目は変わりません。
- 「CFA の並び」を変えると結果が変わります。RGGB と BGGR は赤と青が入れ替わるだけですが、GRBG / GBRG では緑の市松が半画素ずれます。実際のカメラではセンサごとに並びが決まっており、RAW 現像ソフトはこれを読み違えると全体が変な色になります。
- PSNR での差は絵によって大きく変わります。640×480 の実測で、〈ジーメンススター〉が 29.4 → 37.7 dB、〈ゾーンプレート〉が 26.4 → 37.3 dB と 8〜11 dB も開くのに対し、〈低コントラスト〉では 42.7 → 46.5 dB にとどまります。細かい模様や斜めの線が多いほど「向きの選択」が効くからです。ナイキスト周波数の近くでは、どんな手法でも原理的に色を復元できない領域が残ります。
- 〈カラーバー+彩度グラデ〉だけは AHD が負けます(33.6 dB 対 31.8 dB)。誤差の在りかを調べると、画面の 8% にすぎない色の境界の列に集中していて、そこでの平均二乗誤差はバイリニアの 289 に対して AHD が 461 でした。方向別の G が使っている「G と R(B)の差は滑らかだ」という前提が、彩度の高い色がいきなり切り替わる縦線のところでだけ破れ、補正項が行き過ぎるためです。細かい模様には強いが、色のはっきりした段差では素朴な平均のほうが素直、という交換条件がそのまま出た例です。
- AHD は赤と青を対等には扱いません。RGGB の絵の赤と青を入れ替えて BGGR で解いても、結果は完全には一致しません。方向を選ぶ判定を CIELab でやっており、Lab では赤と青の重みが違うからです(バイリニアのほうは赤も青も同じ式なので、ぴたり一致します)。不具合ではなく、判定の物差しを人の感じ方に寄せたことの裏返しです。
- この処理は行帯に分けられないので、1つのワーカーに丸ごと渡しています。右下の「実行」欄に Worker 1 並列と出るのはそのためです。PSNR / SSIM が画像全体の集計であること、AHD の方向選択が近傍を広く見ることの2つが理由です。
- Bayer 配列そのものを見たいときは、表示を〈Bayer 生データ(全解像度)〉にして、色が一様なところにルーペを当ててください。〈カラーバー+彩度グラデ〉の白い帯が分かりやすく、2×2 のうち赤1・緑2・青1 が並んでいるのがはっきり見えます。細かい模様のところに当てると、下の絵の模様とモザイクが混ざって読み取れません。ルーペの倍率は 16 以上に上げてください(中央の赤枠が1画素です)。
アフィン変換(回転・拡大・せん断)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| 回転角 | range | 20 | -180 〜 180(刻み 1) |
| 拡大率 | range | 1 | 0.2 〜 3(刻み 0.05) |
| せん断(横方向) | range | 0 | -1 〜 1(刻み 0.05) |
| 平行移動 x | range | 0 | -50 〜 50(刻み 1) |
| 平行移動 y | range | 0 | -50 〜 50(刻み 1) |
| 補間 | select | bilinear | 最近傍(1点) / バイリニア(4点) / バイキュービック(16点) |
| 外側の扱い | select | zero | 黒で埋める / 端の画素を伸ばす / 鏡映 |
| 往復させて誤差を見る(元に戻す) | checkbox | false | オン / オフ |
原理
画像を平行移動・回転・拡大縮小・せん断する処理です。この4つはまとめて2×3 の行列ひとつで書けて、どれだけ重ねがけしても行列の掛け算1回にまとまります。直線が直線のまま、平行な線が平行のまま保たれるのがアフィン変換の性質です。
実装で肝心なのは「向き」です。入力の画素を行き先へ飛ばす(順写像)と、行き先が飛び飛びになって隙間が空いたり、2つが同じ場所へ落ちたりします。そこで逆向きに、出力の画素ごとに「これはもとの画像のどこを読めばよいか」を逆行列で求めます(逆写像)。こうすると出力の全画素がちょうど1回ずつ埋まります。
ただし読む場所は小数になります。1.7 行目の 3.2 列目に画素はありません。そこで周りの画素から作るのが補間です。
たとえるなら、方眼紙に描いた絵を斜めの方眼紙に写す作業です。写す先のマスを1つずつ見て「元の絵ではこのあたり」と当たりを付けて色を決める。当たりの付け方が補間の種類にあたります。
式
順方向:[x'] = [a b][x − cx] + [cx + tx] [y'] [c d][y − cy] [cy + ty] [a b] = zoom · [cosθ −sinθ] · [1 sh] [c d] [sinθ cosθ] [0 1] 逆写像(実際に回す向き):もとの位置 = M⁻¹ · (出力の位置 − 中心 − 平行移動) + 中心 補間の重み 最近傍 : 一番近い1点 バイリニア : (1−fx)(1−fy), fx(1−fy), (1−fx)fy, fx·fy の4点 バイキュービック:Catmull-Rom(a = −0.5)の4×4 = 16点
注意点・見どころ
- まず〈幾何図形〉で回転角を 20° あたりにして、「補間」を3つとも切り替えてください。最近傍では斜線と円の輪郭が階段状に割れます。バイリニアにすると滑らかになり、バイキュービックにすると輪郭がもう少し締まります。ルーペを当てると1画素単位で違いが見えます。
- バイキュービックの重みには負の部分があります(Catmull-Rom の a = −0.5)。そのため黒と白の段差の脇で、白側が少し行き過ぎ、黒側が少し沈みます。輪郭が締まって見えるのはこの行き過ぎのおかげでもあります。〈グレー階段+ランプ〉の段差にルーペを当てると分かります。ぼけを嫌って締めれば、代わりに行き過ぎが出る、という交換条件です。
- 〈往復させて誤差を見る〉を入れると、変換したあとに逆変換で元に戻します。理屈のうえでは原画に戻るはずですが、戻りません。補間を2回通るからです。〈ジーメンススター〉を 20° 回して往復させた実測で、最近傍 30.3 dB/バイリニア 31.2 dB/バイキュービック 34.8 dB。最近傍が一番低いのは「ぼけない代わりに位置がずれる」ためで、ずれは半画素まで出ます。画像編集で回転をやり直すたびに絵が甘くなるのは、この積み重ねです。 数値は「行きと戻りの両方で画像の中を読めた画素」だけで測っています。回転で画面の外へ出た隅はそもそも情報が無いので、そこまで数えると補間の損ではなく隅の黒さを測ることになるからです。右下の「比べた範囲」がその割合で、20° なら 87.6% です。
- 回転角を 90° や 180° にすると、往復の誤差が完全に消えます(PSNR が ∞ と出ます)。読む位置が画素の真上にぴたりと乗り、補間が1点だけを拾うので、どの補間を選んでも同じです。半端な角度でだけ損が出る、というのが補間の損の正体で、写真の 90° 回転が何度やっても劣化しないのもこれが理由です。
- 「外側の扱い」は、回したときに空いた隅をどう埋めるかです。黒で埋めるのが素直ですが、そのあとさらにフィルタをかけるような場面では、黒との段差が偽のエッジになります。端の画素を伸ばす(clamp)か鏡映にしておくと、その段差が出ません。
- 拡大率を 3 倍まで上げると、補間の違いが一番はっきり出ます。最近傍は画素が四角いまま大きくなり、バイリニアはぼけ、バイキュービックはその中間で輪郭が立ちます。逆に 0.2 倍まで縮めると、3つとも似たような結果になりますが、これは「縮めるときは本来ぼかしてから間引くべきなのに、していない」ためです(折り返しが出ます)。縮小には別の手当てが要る、という話につながります。
- この処理は行帯に分けられません。出力画素が入力のどこを読むかは変換しだいで、帯の外まで必要になりうるからです。FFT や比較と同じく、丸ごと1つのワーカーへ渡しています。右下の「実行」欄に Worker 1 並列と出るのはそのためです。
- 補間を切り替えると右下の「計算時間」が 16 → 22 → 66 ms(640×480)と上がります。読む点の数(1 → 4 → 16)におおむね比例していて、「1画素あたり何点読むか」がそのまま時間になっていることが読めます。この「1画素あたりに読む点の数」をタップ数と呼び、右下の「補間」の行に出しています。この数字はベンチマークと同じ測り方で出しています(必ずワーカーで計算し、1回空回ししてから2回まわして速いほうを採る)。1回きりだと、ごみ集めやワーカーの温まり具合だけで数倍に振れてしまうためです。そのぶん「画面に出るまで」は3倍ほどになります。
レンズ歪みと補正(樽型・糸巻き型)
パラメータ
| 名前 | 種類 | 既定値 | 範囲・選択肢 |
|---|---|---|---|
| すること | select | apply | 歪ませる(レンズを通した絵を作る) / 補正する(歪んだ絵をまっすぐに) / 歪ませてから補正する(誤差を見る) |
| 歪み係数 k₁ | range | -0.25 | -0.5 〜 0.5(刻み 0.01) |
| 歪み係数 k₂ | range | 0 | -0.2 〜 0.2(刻み 0.01) |
| ズーム | range | 1 | 0.5 〜 1.5(刻み 0.01) |
| 格子を重ねてから歪ませる | checkbox | true | オン / オフ |
| 補間 | select | bilinear | 最近傍(1点) / バイリニア(4点) / バイキュービック(16点) |
| 外側の扱い | select | zero | 黒で埋める / 端の画素を伸ばす / 鏡映 |
原理
レンズは光を曲げる道具なので、まっすぐな線がまっすぐに写るとは限りません。広角レンズでは画面の縁が中心に寄って、四角い建物の輪郭が樽のようにふくらんで見えます(樽型歪み)。望遠側では逆に外へ引っ張られ、糸巻きのようにへこみます(糸巻き型歪み)。
どちらも「中心からの距離だけが伸び縮みして、向きは変わらない」という形をしています。そこで中心からの距離 r を r·(1 + k₁r² + k₂r⁴) に置き換えるだけで、かなりよく表せます。奇数乗の項が無いのは、レンズが回転対称だからです。
補正はこの逆をやります。ただし「まっすぐな絵から歪んだ絵を作る」のと「歪んだ絵をまっすぐにする」のとでは、式を解く向きが逆になります。前者は式を逆に解く必要があり、解析的には解けないので数値で解きます。
たとえるなら、伸び縮みするゴムシートに絵を描く作業です。中心を押さえたまま外周を引っ張れば糸巻き型、外周を縮めれば樽型。補正は同じゴムを反対向きに引き戻すことにあたります。
式
歪みの式(中心からの距離だけを動かす) rᵈ = rᵤ · (1 + k₁·rᵤ² + k₂·rᵤ⁴) rᵤ … まっすぐな絵での半径 rᵈ … 歪んだ絵での半径(どちらも隅を 1 に正規化) k₁ < 0 で樽型、k₁ > 0 で糸巻き型 補正(出力=まっすぐ):出力の rᵤ から上の式で rᵈ を出し、そこを読む ← 式で直に出る 歪ませる(出力=歪んだ絵):出力の rᵈ から rᵤ を求める ← 上の式を Newton 法で解く Newton 法:rᵤ ← rᵤ − f(rᵤ) / f'(rᵤ) f(rᵤ) = rᵤ(1 + k₁rᵤ² + k₂rᵤ⁴) − rᵈ f'(rᵤ) = 1 + 3k₁rᵤ² + 5k₂rᵤ⁴
注意点・見どころ
- まず〈幾何図形〉で、〈格子を重ねてから歪ませる〉を入れたまま k₁ を −0.5 から +0.5 まで動かしてください。重ねた格子の線が、マイナス側では外へふくらみ(樽型)、プラス側では内へへこみます(糸巻き型)。歪みは「まっすぐな線がどう曲がるか」で見るのが一番早いので、格子を既定でオンにしてあります。
- k₂ は外周だけを効かせる項です。r⁴ に比例するので、中心付近ではほとんど効かず、隅で急に効きます。実際のレンズでは「中心寄りは樽型なのに隅で糸巻きに転じる」ことがあり(陣笠型)、k₁ と k₂ を逆符号にするとその形が作れます。k₁ = −0.3、k₂ = +0.2 あたりで格子を見ると、線が途中で曲がり方を変えるのが分かります。
- 「すること」を〈歪ませる〉と〈補正する〉で切り替えると、同じ k₁ でも絵が逆向きに変わります。歪ませるほうは式を逆に解く必要があり、Newton 法で 2048 段の表を先に作ってから引いています。1画素ごとに反復すると 30 万回ぶんの反復になりますが、表なら作るのは 2048 回で済み、あとは引くだけです。「同じ計算を何度もするなら表にする」という定石そのものです。
- 〈歪ませてから補正する〉を選ぶと、往復させた結果が出ます。理屈のうえでは原画に戻るはずですが、右下の PSNR は無限大になりません。しかも歪みが強いほど戻りが悪くなります(〈ジーメンススター〉・格子オフ・バイリニアで、k₁ = −0.1 が 33.5 dB、−0.25 で 32.2 dB、−0.4 で 28.9 dB)。樽型で内側に押し込まれた外周部は、そこで画素が間引かれてしまい、引き伸ばしても戻らないためです。補正は「歪みを打ち消す」ことはできても「失われた解像度を取り戻す」ことはできません。
- 往復の数値は「歪ませた段と補正した段の両方で画像の中を読めた画素」だけで測っています。隅は歪ませた時点で画面の外へ出てしまい、そもそも情報が無いので、そこまで数えると補間の損ではなく隅の黒さを測ることになるからです。右下の「比べた範囲」がその割合です。数字を読むときは〈格子を重ねる〉を外してください。1画素幅の線は2回の再標本化で真っ先に壊れるので、格子を入れたままだと 20 dB 前後まで落ちます。
- 往復の数値では、補間の順位が絵によって入れ替わります。〈ジーメンススター〉のような細かい絵ではバイキュービックが最良ですが、〈低コントラスト〉のようになだらかな絵では最近傍が一番高く出ます(k₁ = −0.25 で最近傍 44.0 dB 対 バイリニア 39.7 dB)。なだらかな絵では隣の画素との差が小さいので、最近傍のずれによる誤差より、バイリニアのぼかしによる誤差のほうが大きくなるためです。「どの補間が良いか」は絵の細かさで決まる、という話にそのまま使えます。
- k₁ をプラス(糸巻き型)にして往復させると、最近傍だけ PSNR が ∞ になります。糸巻き型を作る段では中心から外へ引き伸ばす=拡大なので、最近傍だと元の画素がそのまま複製されるだけで、縮めて戻すときに複製の1つを選び直せば完全に元へ戻るためです。倍率がちょうど逆数の拡大→縮小は、最近傍に限って可逆になります。ただし「比べた範囲」は 78% まで落ちます(残りは画面の外へ出ています)。
- 補正すると画面の四隅に黒い領域が出ます(樽型の場合)。歪んだ絵には、まっすぐな絵の隅にあたる場所が写っていないためです。実際のカメラやソフトはここでわずかに拡大して黒を追い出します。「ズーム」を 1.1 倍あたりに上げると同じことが起きるので、切り替えて見比べてください。画角が少し狭くなるのが代償です。
- 「補間」を切り替えると、歪みの強い外周ほど差が出ます。最近傍では格子の線が階段状に割れ、バイリニアでは滑らかになります。中心付近はほとんど動かないので差が出ません。変形の大きいところでだけ補間の質が効く、というのがここで見えることです。右下の「計算時間」もタップ数に応じて上がりますが、この数字はベンチマークと同じ測り方(必ずワーカーで計算し、1回空回ししてから2回まわして速いほうを採る)で出しています。1回きりだと数倍に振れて比べられないためで、そのぶん「画面に出るまで」は3倍ほどになります。
- この処理は行帯に分けられません。出力画素が入力のどこを読むかが半径によって変わり、帯の外まで必要になりうるからです。丸ごと1つのワーカーへ渡しています。右下の「実行」欄に Worker 1 並列と出るのはそのためです。