Pythonでトイカメラ風加工

python

 トイカメラ、すなわちおもちゃカメラですね。デジカメのエフェクトにも存在しますが、これをPython使って再現させてみます。

 Photoshopを使った作例がWeb上には多くあがっています。これをPythonでトレースします。

 過去の記事でraw画像が持つノイズや歪み、トーンやボケを補正する処理をいろいろ書いてきましたが、その逆をしてやると、きれいに撮れた画像がトイカメラ、オールドレンズで撮った画像のようになります。それをPythonスクリプトで書いてみようと思います。過去の補正処理をまとめた記事はこちら。

逆補正

 通常の写真補正の逆をします。まずレンズの特性が悪いことを考慮させ、画像をぼかします。続いてノイズの重畳、コントラスト強調、周辺減光の再現、 トーン調整を行ってい行きます。

今回この豚さんを入力例にします。

ぼかし

 まず、プラレンズのようにレンズのMTFが低いことをイメージさせるために、ガウシアンフィルタを使ってぼかします。OpenCVの力を使って簡単に済ませます。

img = cv2.GaussianBlur(img, (7, 7), 0)

 こんな感じ。1行ですね。ココで指定した7というサイズは画像次第で可変にした方がよいと思います。

 ぼけました。

ノイズ重畳

 SNが悪いセンサをイメージします。ガウスノイズを重畳させます。今度はnumpyを使います。

h, w, ch = src.shape
gauss = np.random.normal(0, sigma, (h, w, ch))
gauss = gauss.reshape(h, w, ch)

 これで、平均0、標準偏差sigmaのガウスノイズが作れます。これを画像に足しこみます。

img = np.clip(img + gauss, 0, 255)

 画素のクリッピングもしています。こうしないと値が回り込みます。

 それっぽいノイズが乗りました

コントラスト強調

 続いてコントラストを強めます。これで少し雰囲気が出ます。これはスプライン曲線でトーンカーブを近似します。これには scipy を使いました。以前これに関しては記事にしていますので詳細はこちらも読んでみてください。ココでは画像を0~255から0~1へ正規化してから処理してます。

from scipy.interpolate import splrep, splev

xs = [0, 0.25, 0.5, 0.75, 1]
ys = [0, 0.15, 0.5, 0.85, 1]
img = img / 255
tck = splrep(xs, ys)  # スプライン
img = splev(img, tck)  # スプライン適用

これで得られるトーンカーブは

 こんな感じ。この処理できつめのコントラスト強調がかかって、

 だいぶ古めかしい写真になってきました。

周辺減光

 レンズの周辺ほど暗くなる現象です。オールドレンズ風になります。 meshgridを使って中心からの距離配列を作って、その値に応じて画像を暗く(ゲインダウン)していきます。以前この現象の補正処理の記事で解説しているので、詳細割愛します。

 関数にするとこんな感じ。RGB独立で暗くさせることができますが、今回のユースケースではRGB同値でよさそうです。この補正値gain_paramsに負の値を入れれば周辺が暗く補正されます。

##
# @fn peripheral_light_correct()
# @brief 周辺減光補正関数
# @param img 入力画像(numpy array)
# @param gain_params 補正値(R,G,B独立)(np array)<br>
#        例えば0.8を指定すると画像の長い側の辺の端で1.8倍のゲインがかかる。
# @details 画像中心からの距離に応じた減光補正を行う。
# @return 補正後の画像
def peripheral_light_correct(img, gain_params):
    h, w = img.shape[:2]
    size = max([h, w])  # 幅、高の大きい方を確保
 
    # 1.画像の中心からの距離画像作成
    x = np.linspace(-w / size, w / size, w)
    y = np.linspace(-h / size, h / size, h)  # 長い方の辺が1になるように正規化
    xx, yy = np.meshgrid(x, y)
    r = np.sqrt(xx ** 2 + yy ** 2)
 
    # 2.距離画像にゲインを乗じ、ゲインマップを作製
    gain0 = gain_params[0] * r + 1
    gain1 = gain_params[1] * r + 1
    gain2 = gain_params[2] * r + 1
    gainmap = np.dstack([gain0, gain1, gain2])
 
    # 3.画像にゲインマップを乗じる
    return np.clip(img * gainmap, 0., 255)

 この関数を通すと画像は

 少し暗すぎるかな?

トーン調整

 ここからは正直撮れた写真次第だと思います。カラーバランスを崩してやるとそれっぽい雰囲気になります。崩し方はそれぞれだと思いますが、この例では赤っぽく崩してみました。これもコントラスト強調と同じようにスプラインを使うのですが、RGBの色毎に異なるトーンカーブをかけてみます。

img = img / 255
xs = [0, 0.25, 0.5, 0.75, 1]

# RED
ys = [0, 0.15, 0.5, 0.85, 1]
tck = splrep(xs, ys)  # スプライン
img[:, :, 2] = splev(img[:, :, 2], tck)  # スプライン適用

# GREEN
ys = [0, 0.15, 0.35, 0.65, 1]
tck = splrep(xs, ys)  # スプライン
img[:, :, 1] = splev(img[:, :, 1], tck)  # スプライン適用

# BLUE
ys = [0, 0.30, 0.5, 0.7, 1]
tck = splrep(xs, ys)  # スプライン
img[:, :, 0] = splev(img[:, :, 0], tck)  # スプライン適用

 こんな感じで適用してみます。その際のカーブは

 こんな感じ。REDに対してはコントラスト強調、GREENは下凸、BLUEはコントラストを抑える方向に崩してみました。

 ちょっとやりすぎましたね。まぁ参考まで。モノクロとか、セピア調というのもありです。

スポンサーリンク

まとめ

 全般ちょっときつめのパラメータになってしまいましたが、効果が一目瞭然という意味ではよかったかもしれません。 以下に作例をのせましたが、ここまでひどくエフェクトかけてはいません。ソース画像の解像度が高いこともあり、もう少しマイルド。 ちなみに冒頭の写真は以下のような別のトーン調整をしています。

red = [0, 0.21, 0.5, 0.79, 1]
green = [0, 0.28, 0.58, 0.78, 1]
blue = [0, 0.30, 0.5, 0.7, 1]

 ひとまずスクリプトとしてこれがざざっとできるのはらくちんですね。いろいろWebを調べるとトイカメラ風の加工はあるので参考にしてみるといいかと思います。

 もともとこれをPythonでやりたかったのは、トイカメラ風タイムラプス動画を作りたかったからで、このスクリプトを挟んでやることで普通に撮った連続画像が昔風の動画になるはずです。まだ試してませんが。今後それもやってみたいと思います。

全コード

 今回試した全コードです。パラメータは直値です。適正な値を見つけながら試してみてください。あと上で示したコードとクリッピングの範囲が微妙に違います。画素値を0~1に正規化しているので。

import cv2
import numpy as np
from scipy.interpolate import splrep, splev

##
# @fn peripheral_light_correct()
# @brief 周辺減光補正関数
# @param img 入力画像(numpy array)
# @param gain_params 補正値(R,G,B独立)(np array)<br>
#        例えば0.8を指定すると画像の長い側の辺の端で1.8倍のゲインがかかる。
# @details 画像中心からの距離に応じた減光補正を行う。
# @return 補正後の画像
def peripheral_light_correct(img, gain_params):
    h, w = img.shape[:2]
    size = max([h, w])  # 幅、高の大きい方を確保

    # 1.画像の中心からの距離画像作成
    x = np.linspace(-w / size, w / size, w)
    y = np.linspace(-h / size, h / size, h)  # 長い方の辺が1になるように正規化
    xx, yy = np.meshgrid(x, y)
    r = np.sqrt(xx ** 2 + yy ** 2)

    # 2.距離画像にゲインを乗じ、ゲインマップを作製
    gain0 = gain_params[0] * r + 1
    gain1 = gain_params[1] * r + 1
    gain2 = gain_params[2] * r + 1
    gainmap = np.dstack([gain0, gain1, gain2])

    # 3.画像にゲインマップを乗じる
    return np.clip(img * gainmap, 0., 1.0)


##
# @fn toycam()
# @brief トイカメラ風画像変換
# @param img 入力画像(numpy array)8bit BGR順想定
# @return 出力画像
def toycam(img):
    # uint8 -> float64変換(0~1)
    img = img / 255
    h, w, ch = img.shape

    # ぼかし
    img = cv2.GaussianBlur(img, (7, 7), 0)

    # ノイズ重畳
    sigma = 0.08
    gauss = np.random.normal(0, sigma, (h, w, ch))
    gauss = gauss.reshape(h, w, ch)
    img = np.clip(img + gauss, 0, 1.0)

    # コントラスト強調
    xs = [0, 0.25, 0.5, 0.75, 1]
    ys = [0, 0.15, 0.5, 0.85, 1]
    tck = splrep(xs, ys)  # スプライン
    img = splev(img, tck)  # スプライン適用

    # 周辺減光
    img = peripheral_light_correct(img, np.array([-0.4, -0.4, -0.4]))

    # トーン調整
    ys = [0, 0.15, 0.5, 0.85, 1]
    tck = splrep(xs, ys)  # スプライン
    img[:, :, 2] = splev(img[:, :, 2], tck)  # スプライン適用

    ys = [0, 0.15, 0.35, 0.65, 1]
    tck = splrep(xs, ys)  # スプライン
    img[:, :, 1] = splev(img[:, :, 1], tck)  # スプライン適用

    ys = [0, 0.30, 0.5, 0.7, 1]
    tck = splrep(xs, ys)  # スプライン
    img[:, :, 0] = splev(img[:, :, 0], tck)  # スプライン適用

    # float64 -> uint8変換
    return (img * 255).astype(np.uint8)


if __name__ == '__main__':
    img = cv2.imread("src.jpg")
    img = toycam(img)
    cv2.imwrite("toy.jpg", img)

作例

python画像処理
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました