Lanczos関数を使って画像を拡大縮小すると、高品質な画像が得られるのだそうだ。
その仕組みを知りたいと思っていつも通りGoogleのお世話になったが、仕組みを解説した情報が見つからない。
見つかるのはLanczosという名前の紹介や、画像処理ソフトの紹介ばかり。
もっとも、探し方が悪いのかもしれないけど。

なぜ高品質な画像が得られるのか?
他の方法とは何が違うのか?

断片的な情報から調べていって、その理由が理解できた。
はっきり言ってこれを解説するのは難しい!

Lanczos関数

まずはLanczos関数について。

そもそもLanczosを何と読むのかが疑問だったが、ランツォシュと読むらしい。
数学者の名前。

Lanczos sinc関数とかLanczos窓関数とかLanczosフィルタとか、人によって色々な呼び方をしていて、正しい呼称がどれなのかはっきりしないが、Lanczos windowed sinc functionsという呼称が正しそうな気がする。

つまり、Lanczosさんが考えたsinc関数で、それをwindow functionとして扱う関数のこと。
(↑いい加減な説明)

sinc関数とは sin(x)をxで割って得られる関数のこと。
正規化したsinc関数は sin(PI * x) / (PI * x) で表される。
xが大きくなると分母は比例して大きくなるが、分子はsin値なので-1と+1の間の値に収まる。
つまり、xが大きくなるとsinc関数の値は0に近づくものの、0に収束することはない。

window functionとは、ある有限区間以外で0となる関数のこと。
無限に続く値は、計算で使うのに都合が悪い。
ある程度xが大きくなったら、sinc関数の値は概ね0であるとみなし、xの範囲を限定して考えることにする。

以上をふまえてLanczos関数の定義を書くと、次の通り。

Lanczos関数は正の整数nをパラメータに持つ。

Lanczos(x) = sinc(x) * sinc(x/n) , |x| <= n
           = 0 , |x| > n
sinc(x) = sin(PI * x) / (PI * x)

画像の拡大縮小処理で使われるのは、大抵はn=2やn=3のLanczos関数らしい。
グラフにすると次の通り(1目盛りが1.0を表す)。

n=2

n=3

この関数は理解できた。
でも、この関数と画像の拡大縮小処理の間にどんな関係が?
この先は画像の拡大縮小処理の仕組みを知らないと話が進まない。

拡大縮小の方法

画像Line1

画像Line1

画像Line1は20x20ピクセルの画像で、見ての通り、斜めの直線が描かれている。
これを虫眼鏡で拡大してみると、画像Line2のようにピクセルが並んでいることがわかる。

画像Line2

画像Line2

次の画像Line3は画像Line1の拡大画像である。

画像Line3

画像Line3

画像処理ソフトを使えばあっという間に生成できるが、自分で計算して拡大画像を生成するのは手間がかかる。

まずは、単純なところで3倍に拡大する方法を考える。
元の画像の1ピクセルは3倍に拡大されるので、拡大画像では3ピクセル分の大きさになる。
実際のピクセルの大きさを無視して、対応するピクセルの位置関係を表すと次のようになる。

            0        1        2        3
src    |        |        |        |        |
dest   |  |  |  |  |  |  |  |  |  |  |  |  |
         0  1  2  3  4  5  6  7  8  9 10 11

元の画像(src)の幅4を3倍に拡大するので、拡大後の画像(dest)では幅12になる。

図からわかる通り、destの0の色は、対応するsrcのピクセル0の色によって決まる。
destの2の色は、対応するsrcのピクセル0の色だ。

縮小の場合も似たような処理が必要になる。
1/3に縮小する場合の、対応するピクセルの位置関係は次の通り。

         0  1  2  3  4  5  6  7  8  9 10 11
src    |  |  |  |  |  |  |  |  |  |  |  |  |
dest   |        |        |        |        |
            0        1        2        3

元の画像(src)の幅12を1/3倍に縮小するので、縮小後の画像(dest)では幅4になる。

destの0の色は、対応するsrcのピクセル0〜2の色によって決まる。
destの2の色は、対応するsrcのピクセル6〜8の色によって決まる。
ここでの計算方法はちょっと悩む必要がある。
3つのピクセルの色の平均を取るのもアリ。
中央のピクセルの色だけを取るのもアリ。
この処理は次の"補間処理"にも関係するので、詳しくは後述。

補間処理

単純に虫眼鏡方式で拡大した画像はタイル画のようになってしまい、綺麗な拡大画像とは言えない。
画像Line1の拡大は、画像Line3ではなく綺麗な斜めの直線(画像Line4)のようになって欲しいところだ。

画像Line4

画像Line4

滑らかな拡大を行うためにはギザギザの部分を加工する必要がある。

その前に虫眼鏡方式の拡大について補足。

先に3倍に拡大する方法を説明したが、現実はもう少し厄介だ。
1.5倍に拡大する場合、元の画像の1ピクセルは1.5倍に拡大されるので、拡大画像では1.5ピクセル分の大きさになる。
ところが現実には1.5ピクセルなどという半端な大きさは存在しない。

実際のピクセルの大きさを無視して、対応するピクセルの位置関係を表すと次のようになる。

          0     1     2     3
src    |     |     |     |     |
dest   |   |   |   |   |   |   |
         0   1   2   3   4   5

元の画像(src)の幅4を1.5倍に拡大するので、拡大後の画像(dest)では幅6になる。
図からわかる通り、destの0の色は、対応するsrcのピクセル0の色によって決まる。
destの1の色は、対応するsrcのピクセルが0と1にまたがっているので、どうすべきか迷うところ。

手っ取り早くテキトーに0か1を選んでしまうのもアリ。
0と1の色の平均を取って決めるのもアリ。

その方法を決める前に、例えば1.2倍に拡大するとどうなるかなども考えてみた方がいい。

          0     1     2     3     4
src    |     |     |     |     |     |
dest   |    |    |    |    |    |    |
          0    1    2    3    4    5

元の画像(src)の幅5を1.2倍に拡大するので、拡大後の画像(dest)では幅6になる。

destの1の色は、対応するsrcのピクセルが0と1にまたがっているが、1.5倍の時と異なり、1に相当する部分が大きくなっている。
0と1からテキトーに選ぶなら迷わず1の色だ。
0と1の色の平均を取るなら、単純な平均ではなく、割合を考慮すべきだ。

いずれにせよ、直感的に考えただけでも、テキトーに色を決めてしまう拡大縮小アルゴリズムでは、あまり綺麗な画像を得られそうにない気がする。
割合を考慮して平均を取る方法は、一見すると良さそうなやり方だが、実際にやってみるとピンぼけ写真のようになってしまう。

縮小の例がわかりやすいので、もう一度。
1/3に縮小する場合の、対応するピクセルの位置関係は次の通り。

         0  1  2  3  4  5  6  7  8  9 10 11
src    |  |  |  |  |  |  |  |  |  |  |  |  |
dest   |        |        |        |        |
            0        1        2        3

destの0の色は、対応するsrcのピクセル0から2の色によって決まる。
ここで、3つのピクセルの色の単純な平均値を取ると、ピンぼけ写真ができあがる。

ピンぼけを避けるために中央のピクセルの色だけを選ぶと、destの0はsrcの1、destの1はsrcの4の色になる。
この方法ではくっきりした画像が得られるが、srcの0、2、3、5…の色が無視されていることが問題となる。
例えば縞模様の画像を縮小すると、運悪く縞のピクセルが無視されてしまい、真っ白な画像ができあがってしまうことがある。

ここで一度、数学的なことは忘れて、絵の具で色を塗ることを考える。
destの0を何色にするか考える。
重ね合わせた2枚の絵をじっと見つめると、中央にあるsrcの1で塗りたい気がしてきた。
でもsrcの0と2の色も無視しがたい。
そういう時は、srcの1の色を基調にし、さらにsrcの0と2の色が少し混ざったような色を目指す。
さらに欲を出して、かなり薄めたsrcの3の色を加えてみてもいい。

この「周囲の色をちょっと薄めて加える」というのが補間処理の正体だ。

どのピクセルの色をどの程度まで薄めて混ぜるかを計算する必要がある。
その加減を間違えると綺麗な画像にならない。

感覚的に言うなら、一番近い位置にあるピクセルの色を重視し、離れた位置にあるピクセルの色は少しだけ考慮するということ。

ここでLanczos関数を使って補間処理を行うと、高品質な画像が得られるということらしい。

Lanczos関数のグラフを見ると、距離(横方向)が近い時に大きな値(縦方向)、距離が遠い時に小さな値となっている。
この関数を使って距離に応じた係数を求めることができる。

ただ、この手のカーブを描くグラフは他にもある。

Lanczos関数(n=2)

Lanczos関数(n=2)
Gauss関数

Gauss関数
テント型

テント型

グラフを見比べるとどれも大差ないように思えるが、数学的に厳密に計算するとLanczos関数が最も理想的な値を返すということらしい。
(本当はここが知りたかったのだが、結局理解できなかった…)

補間処理へのLanczos関数の応用

ここまで理解できたにも関わらず、実際に応用するのはさらに難しい。

Lanczos関数(n=2)は、距離が2以上の時に値が0になるので、2までの距離を考慮すればいい。
ここでいう距離の扱いが難しい。

まず、距離の単位は決まっていないので、cmでもインチでも好きに決められる。
ただ表示媒体によって変化するサイズよりも、実際の画像をベースとしたサイズの方が良さそうなので、1ピクセルを1の距離と考えることにする。
(もちろん、2ピクセルを1の距離と考えることもできる)

注目しているピクセルから距離2の範囲内にあるピクセルは、相対座標で表すと-2, -1, 0, +1, +2だ。
距離が2の時のLanczos関数の値は0なので、2の距離にあるピクセルの色は無視できる。
つまり、距離が-1, 0, +1のピクセルの色だけを考慮すれば良さそうに思えるかもしれない。

ところが、この考え方をした場合に利用するLanczos関数の値は、距離が-1, 0, +1の時の値だけであり、sinc関数の特性を表した値になっていない。
また、この考え方ではGauss関数やテント型を利用した場合と何ら違いがない。
この考え方では、Lanczos関数を使っても綺麗な拡大画像は得られそうにない。

それはさておき、実際にLanczos関数(n=2)を使って実際に3倍に拡大してみる。

            0        1        2        3
src    |        |        |        |        |
dest   |  |  |  |  |  |  |  |  |  |  |  |  |
         0  1  2  3  4  5  6  7  8  9 10 11

destが4の色を決めるには、距離が2までのピクセルの色を知る必要がある。

ここでいう距離とは、srcのピクセルでの距離なのか、destのピクセルでの距離なのか?

最初にsrcのピクセルでの距離を考えてみる。
destの4に相当するsrcのピクセルは1だ。
srcの1から距離2の範囲のピクセルは-1〜3だ。もちろん、-1などというピクセルは実在しないが。

次にdestのピクセルでの距離を考えてみる。
destの4から距離2の範囲のピクセルは3〜5だ。
destのピクセル2や6は距離が2なので、Lanczos関数の値が0になるため、無視できる。
destの4を基準にした時の3〜5までの距離は-1, 0, +1であり、先に書いた通り、これではLanczos関数の特徴は現れず、Gauss関数やテント型を使った場合との違いはない。

従って、srcでの距離で考えるべきだ。

次にもっと大きく拡大した時のことを考えてみる。
例えば10倍に拡大。

                 0                   1
src    |                   |                   |
dest   | | | | | | | | | | | | | | | | | | | | |
        0 1 2 3 4 5 6 7 8 9 10  12  14  16  18  20

srcのピクセルでの距離を使って計算すると、destの5に相当するsrcのピクセルは0なので、距離2の範囲内のsrcのピクセルは-2〜2になる。
同様に考えると、destの0から9も同じ結果になる。
ここで、srcの範囲が同じなので、destの0から9は同じ色になってしまうと考えるのは早計だ。

距離については、もう少し厳密に考える必要がある。

ピクセルの幅

先ほど1ピクセルの距離を1と表すことに決めた。
ピクセルは1ずつ並んでいるので、隣り合ったピクセルの距離は1だ。

小さすぎてあまり意識していないが、ピクセルにも幅がある。
ディスプレイが1280x1024ドット表示なら、ディスプレイの横幅を物差しで測って1280で割れば、それが1ピクセルの幅だ。
実際の幅はどうでもいいが、幅がゼロではないということが重要だ。

幅があるなら左端と右端がある。中央もある。

    0       1       2       3
|       |       |       |       |
|←           →| ... 長さ2
|←   →|         ... 長さ1
    |←   →|     ... 長さ1

ピクセル0と1の距離を測る時、ピクセル0の左端からピクセル1の右端までを測ってしまったら、その長さは2だ。
ピクセル0の左端からピクセル1の左端まで、あるいは中央から中央までを測らなければ正しい距離にならない。
これを忘れないことが大事。

これをふまえて、ピクセル0の座標を 0.0以上1.0未満として考える。
つまり、ピクセルNの座標は N以上、N+1.0未満だ。

距離を厳密に考えた場合の拡大処理

もう一度、10倍拡大の話。

                 0                   1
src    |                   |                   |
dest   | | | | | | | | | | | | | | | | | | | | |
        0 1 2 3 4 5 6 7 8 9 10  12  14  16  18  20

srcのピクセルでの距離をベースに、destの5の色を決定する処理を考える。

図からわかる通り、destの5に相当するsrcのピクセルは0だが、もう少し厳密に考える。
destの5の色を決める時に、左端や右端の色は隣り合ったピクセルの影響を受けやすいので避け、中央座標を基準として色を決めた方が良さそうだ。
つまり、destの5の中央座標は5.5で、それに相当するsrcの座標は0.55だ。この座標が距離0に相当する。
srcの0.55から距離2の範囲内の座標は-1.45〜2.55で、この範囲内に含まれるsrcのピクセルは-2〜2だ。

srcに-2や-1という位置のピクセルは存在しないので、ここではとりあえず無視しておき、0〜2について考える。
srcの座標0.55が距離0に相当することは先に書いた通り。
ここからsrcのピクセル0までの距離を考えるが、この時もピクセルに幅があることを忘れてはいけない。
srcのピクセル0の中央座標は0.50なので、|0.50-0.55|で距離0.05に相当する。
同様にsrcのピクセル2の中央座標2.50は|2.50-0.55|で距離1.95となる。
この距離こそがLanczos関数で使う距離だ。

距離を厳密に考えた場合の縮小処理

今度は縮小処理の距離の考え方について考えてみる。
拡大と同じと思っていると大間違い。

1/10に縮小する場合を考える。

        0 1 2 3 4 5 6 7 8 9 10  12  14  16  18  20
src    | | | | | | | | | | | | | | | | | | | | |
dest   |                   |                   |
                 0                   1

まず、拡大と同様にsrcのピクセルでの距離を考えてみる。

destの0の中央座標は0.5なので、これに相当するsrcの座標は5.0だ。
srcの5.0から距離2の範囲内の座標は3.0〜7.0で、この範囲内に含まれるsrcのピクセルは3〜7だ。

destの1の中央座標は1.5なので、これに相当するsrcの座標は15.0だ。
srcの15.0から距離2の範囲内の座標は13.0〜17.0で、この範囲内に含まれるsrcのピクセルは13〜17だ。

つまり、srcのピクセル8〜12はdestの0や1の色を決める際にまったく考慮されない。
この方法では、縞模様の画像を処理した時に真っ白な画像になってしまう可能性がある。

次に、拡大とは逆にdestのピクセルでの距離を考えてみる。

destの0の中央座標は0.5なので、距離2の範囲内の座標は-1.5〜2.5だ。
ここで、-1.5や2.5は距離が2.0なのでLanczos関数の値は0となる。
ただし、まだこれらを無視してはいけない。

destの-1.5〜2.5に相当するsrcの座標は-15.0〜25.0だ。
この範囲内のsrcのピクセルは-15〜25となる。

図からわかる通り、ずいぶん多くの範囲のピクセルを考慮することになる。

ここで、srcのピクセル-15の距離を考える。
srcのピクセル-15の中央座標は-14.5なので、これに相当するdestの座標は-1.45だ。
destの0の中央座標0.5が距離0なので、destの座標-1.45までの距離は|-1.45-0.5|で1.95となる。

また、srcのピクセル25の中央座標は25.5なので、これに相当するdestの座標は2.55だ。
destの座標2.55までの距離は|2.55-0.5|で2.05となる。
(これは距離2より遠いので、無視できる)

要するに、拡大と縮小では距離に対する考え方がかなり違う。
destの1ピクセルの色を計算するために必要なsrcのピクセル数は、拡大の場合は最大で6ピクセルだが、縮小の場合は縮小率に応じて(小さくするほど)増えるので、計算量も違う。

まとめ

一概に、Lanczos関数を使っているから高画質と考えるのは危険。

調べていく中で、ソースが付属しているフリーソフトなどを見かける機会もあったが、座標(距離)の小数部を丁寧に処理していないために、Lanczos関数の値が正確に計算できていない物もあった。
たとえLanczos関数を使ったとしても、距離の計算が雑では高品質な画像は得られない。

なお、ピクセルの中央値を意識した場合とそうでない場合(ピクセルの左端を使う場合)は結果の画像が0.5ピクセル分ずれる。

n=3のLanczos関数はn=2よりも高画質になるらしい。
ただ、n=3の方が広範囲のピクセルを考慮することになる。
距離が遠いピクセルの影響は微々たるものとはいえ、どうしてもピンぼけ画像っぽくなる傾向はある。特に極端に大きな倍率の拡大縮小処理の場合。

なお、説明では負の座標の(存在しない)ピクセルについては無視しているが、実際にはこの部分を鏡像などで補って考えないといけない。
きちんとやるのはなかなか大変だ。

透明度付きの画像の場合はどうすればいいのだろう。
背景色を指定して不透明な画像に変換してから処理すれば問題ないが、透明度情報を維持したまま拡大縮小するには…?