ボケ質

 飲んで帰って、深夜に球面収差とボケ質の関係を考えつつ、コードを書いた。正確性はかなり怪しいが、こういうことかなぁ、と自分なりに考えてみた。

1.球面収差特性とボケ質

 この図の意味が何となくでもイメージできない人は、早くも置いてけぼり。一から人に説明する気はあまりなく、基本的に自分の理解をまとめるために記事を書いているので。

 (左上図)レンズの周方向に対する球面収差を示す。縦軸はレンズの周方向で、0が中心、1がレンズの外周端。左はレンズから見て手前に合焦し、右は奥に合焦する。
 (右上図)左方向にレンズがあり、0.0の位置にフォーカスがあっているとき、レンズを通る光の点がどのように拡散するか、を示している。多分。

 (左下図)右上図の右のほう(後ろボケ)のある位置での、周方向(横軸)に対する光の強さの分布(ヒストグラム)を示す。この図だと右の方にピークがある・・・つまりボケのエッジに光が集まっていることを示す。
 (右下図)それをボケの形にマッピング(極座標→1周させてXY座標に変換)したもの。

 すべての図に言えることだが、単位は考えていない。定量的に見えて、とても定性的な実験なので、悪しからず。


 上図の例だと、オーバーコレクションのため、点光源の後ろボケのエッジに明るい縁取りが出てしまっていることが分かる。多分、上の写真みたいなリング状のボケになることだろう。おなじみSuper Takumar 50/1.4。

2.球面収差の影響

 収差と前ボケ、合焦位置での解像、後ろボケの関係の例をいくつか示す。以下、右図の丸はそれぞれ、12時の位置の点がレンズ中央での合焦位置(レンズトータルでもっともシャープに写る位置とは限らない)、左半分は前ボケ、右半分は後ろボケである。

例① 球面収差が非常に小さい場合

 この例では非常に良好に球面収差が補正されているので、12時の位置にある点が小さく(=解像度が高く)、前ボケ、後ろボケとも嫌な癖がなく綺麗である。優等生。
 もっとも小さい点(12時)と、その次に小さい点(1時と11時)のサイズ差が大きく、APO-LANTHARではF2の割に被写界深度が浅いと言われるのは、このせいなのだろうか。APO-LANTHAR欲しい。


 次は、あまり補正されていない場合。もっともシャープに写る位置、つまり最小となる点は12時ではなく11時の点で、その大きさは例①の最小点よりはいくらか大きく、その周辺がぼやーっとなっている。つまり解像度は低く、にじんでいる。前ボケにエッジが出ている。後ろボケはとても綺麗だ。

例② あまり補正されていない場合

3.補正量違いでのボケ質の変化

 補正しているが、アンダーな場合。解像度も低めで、前ボケも後ろボケもイマイチ。やはり最小となる点は10~11時の位置である。

例③ アンダーコレクション

 いわゆる適正補正。後ろボケはちょっとやばくなってきた。最小となる点は11時だが、解像度は悪くない。

例④ しっかり補正されている場合

 過剰補正(オーバーコレクション)・・・後ろボケにかなりはっきりしたエッジが出ている。いかにも二線ボケしそう。前ボケは綺麗に見える。最小となる点は12時になった。

例⑤ オーバーコレクション

 逆オーバーコレクション(上図の逆)も試してみた。左右反転するだけかな。後ろボケはガウスボケっぽくて綺麗そうだ。ただしボケの中心に暗い部分があるのは嫌かもしれない。点光源なら問題ないけど。

例⑥ 逆オーバーコレクション

4.絞りによる変化

 ベースは例②とする。

例② 再掲

 例②から1段絞った場合、11時の最小点でのにじみはなくなる。後ろボケは相変わらず綺麗。前ボケの質は多少改善している?

例⑦ 1段絞った場合

 2段絞ると、12時の点と11時の点がほぼ同じサイズになっている。つまり、このレンズは開放から少し絞るとフォーカス位置が奥に移動することになる(フォーカスシフト)。

例⑧ 2段絞った場合

 私の好きな収差レンズであるNOKTON classic 35/1.4 IIは、公式サイトによれば意図的に収差を残しつつ、絞ればシャープ、I型からフォーカスシフトを低減、とのことだ。また実際撮ってみると、開放でにじみは出るし、点光源の後ろボケに多少明るい縁取りが出る。にじみやボケの縁取りはF2まで絞るとかなり消える。

 例③のアンダーコレクションに近いのかな、と思った。私の勝手な予想は下図の通りだが、根拠はない。

NOKTON classic 35/1.4 IIの予想

5.Pythonコード

 酔っぱらって書いたPythonコードを掲載しておく。72~74行目で収差を変えて実験した。絞りの影響は、rのレンジを0~1 → 0~0.707などに変更すればいい。

import cv2
import numpy as np
import matplotlib.pyplot as plt

####
def gamma_cor(img, gamma):
    """ ガンマ補正 """
    lut = np.empty((1,256), np.uint8)
    for i in range(256):
        lut[0,i] = np.clip(pow(i / 255.0, gamma) * 255.0, 0, 255)
    
    im_gamma = cv2.LUT(img, lut)
    return im_gamma

####
def put_circle(img, rs, hist, cx, cy, scale):
    """ 周方向の光束密度ヒストグラムを元に、指定の位置にボケの絵を描く
        処理が適当過ぎるため、とても遅い """
    for x in range(img.shape[1]):
        for y in range(img.shape[0]):
            dist = scale * ((x - cx)**2 + (y - cy)**2)**0.5
            if dist <= rs.max():
                img[y, x] += hist[rs>=dist][0]
    return img

####
def get_hist_d(d, a1, a2, a3):
    """ 周方向の球面収差を示す3次式の係数と距離から、
        光束密度ヒストグラムを作成する """
    y2s = []
    for r in np.arange(0, 1.001, 0.001):
        x0 = -10
        y0 = r
        x1 = a3*(r**3) + a2*(r**2) + a1*r
        y1 = 0
        x2 = d
        y2 = (x2 - x1) * (y1 - y0) / (x1 - x0) + y1
        y2s.append(abs(y2))
    y2s = np.array(y2s)
    rs = np.arange(0, 1.01, 0.01)
    hist = np.histogram(y2s, bins=rs)

    # moving average
    rs1 = np.arange(0, 1.0, 0.01)
    # valid_convolve from https://zenn.dev/bluepost/articles/1b7b580ab54e95
    hist_c = valid_convolve(hist[0], 5)

    return rs1, hist_c

####
def create_img(a1, a2, a3):
    img = np.zeros((1001, 1001, 3))
    scale = 0.002
    n = 12
    for i in range(1, n):
        r_c = 400
        theta_rad = i / n * 2 * 3.1416 + 1.5708
        cx = img.shape[1]//2 + r_c * np.cos(theta_rad)
        cy = img.shape[0]//2 + r_c * np.sin(theta_rad)

        d = i / n * 4 - 2
        rs1, hist_c = get_hist_d(d, a1, a2, a3)
        img = put_circle(img, rs1, hist_c, cx, cy, scale)

    img = img / img.max() * 255
    img = img.astype(np.uint8)
    img = gamma_cor(img, gamma=0.5)
    return img

#### 設定
# 球面収差特性の3次関数
a1 = 0.0
a2 = -0.8
a3 = 0.0
####

r = np.arange(0, 1, 0.001)
p = a3*(r**3) + a2*(r**2) + a1*r

plt.rcParams["font.size"] = 8
fig, ax = plt.subplots(1, 2)
plt.tight_layout()
fig.set_figwidth(10)
fig.set_figheight(5)

# 球面収差特性
ax[0].set_xlim(-1,1)
ax[0].set_ylim(0,1)
ax[0].plot(p, r)
ax[0].set_ylabel("r")
ax[0].grid(color="#EEEEEE")

# ボケの図
img = create_img(a1, a2, a3)
ax[1].imshow(img)

plt.show()
fig.savefig(f"bokeh_{a1}_{a2}_{a3}.jpg")

おまけ:ぼくのかんがえたさいきょうのしゅうさ

 中心部から外周端のちょっと手前まではフラットで、外周端だけアンダーコレクションなのがいい。ほぼフラットなので解像度は高いが、外周がアンダーなので、開放では合焦部の芯はあるが周囲が少しにじみ、後ろボケのエッジは非常に柔らかくなる。

ほぼフラットだが、外周端だけアンダー

 1段絞るとフルコレクションで、解像度は良好、フォーカスシフトもない。この辺りまでは円形絞りを保って欲しい。

1段絞ると、収差なし

 XF35mm F1.4は大体こんな感じなのではないかと思う。円形絞りも保ってくれる。