はじめに
画像のフィルタ処理やリサイズの品質を目視で確認する方法の一つに、ゾーンプレートを使う方法があります。
ゾーンプレートとは、音声信号で言うところのスイープ信号の画像版のようなもので、低周波から高周波までを満遍なく規則的に含む人工的に生成した画像です。
今回は実際にゾーンプレートを生成し、リサイズのアルゴリズムの品質を簡単にですが確認してみます。
ゾーンプレート生成ツール
ゾーンプレートの生成式
以下のサイトを参考にしました。
式を自分なりに書き直します。
luminance = amplitude \times \sin{(\frac{\pi}{2}(\frac{(x – center_x)^2}{radius_x} + \frac{(y – center_y)^2}{radius_y})+\frac{\pi}{2})} + offset
このままだと\sinの引数の計算で割り算が早めに出てきてしまい、計算精度が落ちてしまうので、なるべく整数計算で精度を保つように式変形します。
記述量を少なくするために変数名を以下の通りに置き換えます。
\begin{aligned}
c_x &= center_x \\
c_y &= center_y \\
r_x &= radius_x \\
r_y &= radius_y
\end{aligned}
\sinの引数部分について式変形していきます。
\begin{aligned}
&\frac{\pi}{2}(\frac{(x – c_x)^2}{r_x} + \frac{(y – c_y)^2}{r_y})+\frac{\pi}{2} \\
=& \frac{\pi}{2}(\frac{(x – c_x)^2}{r_x} + \frac{(y – c_y)^2}{r_y} + 1) \\
=& \frac{r_x}{r_x}\frac{r_y}{r_y}\frac{\pi}{2}(\frac{(x – c_x)^2}{r_x} + \frac{(y – c_y)^2}{r_y} + 1) \\
=& \frac{\pi}{2r_x r_y}(r_y (x – c_x)^2 + r_x (y – c_y)^2 + r_x r_y)
\end{aligned}
ゾーンプレートは画像なので、x、y、c_x、c_y、r_x、r_yを全て整数とします。そもそも計算精度がどの程度影響するのかはわかりませんので、この辺りは好みだと思います。今回実装するツールで用いる式は変形後の以下の式です。
luminance = amplitude \times \sin{(\frac{\pi}{2r_x r_y}(r_y (x – c_x)^2 + r_x (y – c_y)^2 + r_x r_y))} + offset
ゾーンプレート生成ツールの実装
NumpyとPillow(PIL)を組み合わせて実装します。ゾーンプレートを生成するところそのものはNumpyで実装し、得られた輝度値の二次元配列を画像化してファイルに保存する処理にPillowを使います。
import argparse
import os.path
import numpy
import PIL.Image
def __make_zoneplate(width, height, r_h, r_v):
center_x = width // 2
center_y = height // 2
zoneplate = numpy.ndarray(shape=(height, width), dtype=numpy.uint8)
for y in range(0, height):
for x in range(0, width):
a = ((x - center_x)**2) * r_v
b = ((y - center_y)**2) * r_h
c = r_h * r_v
v = 127 * numpy.sin((a + b + c) * numpy.pi / (2 * c)) + 128
zoneplate[y, x] = v
return zoneplate
def __main(output_filepath, width, height, r_h, r_v):
zoneplate = __make_zoneplate(width, height, r_h, r_v)
picture = PIL.Image.fromarray(zoneplate)
picture.save(output_filepath)
return None
if __name__ == "__main__":
argment_parser = argparse.ArgumentParser()
argment_parser.add_argument("-o", "--output_zoneplate_picture_filepath", type=str, required=True)
argment_parser.add_argument("-s", "--size", type=int, default=512, choices=range(16, 4096))
argment_parser.add_argument("--width", type=int, choices=range(16, 4096))
argment_parser.add_argument("--height", type=int, choices=range(16, 4096))
argment_parser.add_argument("--horizontal_radius", type=int, choices=range(16, 4096))
argment_parser.add_argument("--vertical_radius", type=int, choices=range(16, 4096))
args = argment_parser.parse_args()
if args.width is None:
args.width = args.size
if args.height is None:
args.height = args.size
if args.horizontal_radius is None:
args.horizontal_radius = args.size // 2
if args.vertical_radius is None:
args.vertical_radius = args.size // 2
__main(args.output_zoneplate_picture_filepath, args.width, args.height, args.horizontal_radius, args.vertical_radius)
exit(0)
基本的に必ず指定しなければならない引数は出力先のファイルパスのみです。それ以外のパラメータはデフォルト値で処理しても構わないと考えます。
例えば生成するゾーンプレートの画像の縦横比は1対1の正方形をよく使うので、size
引数にそのあたりのパラメータを集約しています。
場合によっては縦横比を変えたいかもしれないので、その場合に対応できるようにwidth
引数やheight
引数を持たせています。
また、滅多に使わないと思いますが、ゾーンプレートの同心円半径も縦横それぞれ変更できるように引数を用意しています。
このツールを使って実際にゾーンプレート画像を生成してみます。
$ python make_zoneplate.py -o zoneplate_512x512.png
生成したゾーンプレート画像を図1に示します。
図1. “デフォルトで生成したゾーンプレート |
画像リサイズツールの実装
今回は実験的にリサイズしたいだけなので、簡易的な画像リサイズツールをPillow(PIL)を使って作成します。
import argparse
import os
import PIL.Image
if __name__ == "__main__":
resample_type_table = {
"nearest": PIL.Image.NEAREST,
"bilinear": PIL.Image.BILINEAR,
"bicubic": PIL.Image.BICUBIC,
"lanczos": PIL.Image.LANCZOS
}
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input_picture_filepath", type=str, required=True)
parser.add_argument("-o", "--output_picture_filepath", type=str)
parser.add_argument("-n", "--numerator", type=int, default=1)
parser.add_argument("-d", "--denominator", type=int, default=1)
parser.add_argument("-f", "--filter_type", type=str, default="bilinear", choices=resample_type_table.keys())
args = parser.parse_args()
if args.output_picture_filepath is None:
filename, extention = os.path.splitext(args.input_picture_filepath)
args.output_picture_filepath = f'{filename}_resized{extention}'
resample_type = resample_type_table[args.filter_type]
picture = PIL.Image.open(args.input_picture_filepath)
resized_width = (picture.size[0] * args.numerator) // args.denominator
resized_height = (picture.size[1] * args.numerator) // args.denominator
resized_picture = picture.resize(size=(resized_width, resized_height), resample=resample_type)
resized_picture.save(args.output_picture_filepath)
exit(0)
リサイズ比は有理数比で指定するようにしています。有理数比だと引数を分子と分母で2つ必要としますがどちらも整数なので、私に取っては浮動小数点数で拡大比率を指定する方法より扱いやすいです。
また、拡大縮小の種別をフィルタタイプという引数で指定できるようにしています。Pillowではresample
というパラメータですが、信号処理的にはフィルタ処理とリサンプル処理を分けて考えたいのでこのような名前にしています。これも個人の好みだと思います。
ゾーンプレートを縮小してみる
各ツールも出そろったので、数種のリサイズ手法を使って実際にゾーンプレートを縮小してみます。全て\frac{1}{2}に縮小してみます。
最近傍法(Nearest Neighbor)
$ python resize -i zoneplate_512x512.png -o zoneplate_resized_nearest.png -n 1 -d 2 -f nearest
図2. “Nearestによるゾーンプレートの縮小 |
これは明らかに本来存在しない円が周囲8方向に現れていますね。この本来存在していないはずの円模様は、ゾーンプレートにとっての エイリアシングノイズ(モアレ) です。
最近傍法は単なる間引き処理のようなものなので、元々の画像に含まれている高周波成分が全てエイリアシングとして折り返して現れてしまっています。ダメですね。
線形補間(BiLinear)
$ python resize -i zoneplate_512x512.png -o zoneplate_resized_bilinear.png -n 1 -d 2 -f bilinear
図3. “BiLinearによるゾーンプレートの縮小 |
思ったより品質が良いですね。
三次補間(BiCubic)
$ python resize -i zoneplate_512x512.png -o zoneplate_resized_bicubic.png -n 1 -d 2 -f bicubic
図4. “BiCubicによるゾーンプレートの縮小 |
線形補間とあまり差がないように見えます。閲覧しているディスプレイのせいでしょうか?
Lanczos
$ python resize -i zoneplate_512x512.png -o zoneplate_resized_lanczos.png -n 1 -d 2 -f lanczos
図5. “Lanczosによるゾーンプレートの縮小 |
三次補間と比べると、上下左右に現れていたモアレが気持ち弱まっているように見えます。一番品質が良さそうです。
まとめ
ゾーンプレートを拡大縮小する事で、視覚的にその処理の品質を確認できるようになりました。拡大縮小はフィルタとリサンプルの処理なので、まさに信号処理の理論が生きてくるところですが今回は理論的な話は一切ありません。画像なんて見た目良ければ全て良し、です。
そう言った面では、主観的に品質評価するための手段の一つとして、ゾーンプレートというものがあるのだ、と覚えておくと、何かにつけて役立つと思います。