Pythonを使ってセピア調の画像に加工してみようと思います。単純に茶色っぽい画像を作るだけでは面白くないので、ある程度色合いの調整余地を残し、加えて少し古めかしくして趣のある雰囲気に仕上げていきます。
セピア調
セピアというのはWikipediaによると、どうやらイカ墨語源で、それをインクの素材として使ったことに由来するようです。退色して茶色っぽい雰囲気になるのは、当時の紙とこのイカ墨のせいのようです。
基本的にはモノクロ写真の時代に撮影した写真が、経時変化して天然のセピア調の写真になるのですが、今回はこれを処理で強制的に起こさせます。基本的には古い写真の劣化なのでその雰囲気を出すための処理もおまけで付けます。
デジカメのモードにもそれっぽいものがありますが、それを真似します。
処理
やはり王道はPhotoshopです。簡単にボタン一つで処理してくれるようです。やはりニーズはあるんでしょうね。画像処理として簡単に調べるだけでも、いろいろなやり方が紹介されています。がどれもそれっぽくなるようにRGBを混ぜるだけの処理で、どんな色合いになるか直観的ではないです。
なので今回は少し真面目に、赤褐色だけでなく黄系とか、好みの色合いの写真にも簡単に後加工できるようにしてみようと思います。加えて劣化を表現するためにノイズ重畳、コントラスト強調、レンズの周辺光量ダウンの処理を加えます。
セピア調変換(HSV変換)
今回はOpenCVを使って、HSV空間からの画像加工を行います。HSV色空間とは簡単に言うと、色の種類(H)、鮮やかさ(S)、明るさ(V)の3要素から色を表現する色空間の事です。Hは通常角度で表現され、0度に赤が置かれ、ぐるっといろんな色を経由して一周して360度で赤に戻るような値です。
ただ、OpenCVのHSV世界ではどうも角度を0~180度までに丸めているようで、180度で一周する仕様のようです。HSVの値の範囲が少し直感と違う動きをしました。8bit整数演算したら変換の往復で値が欠落しそうです。
RGBからの変換は、
img_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
この1行で済んでしまいます。Hの値がそれぞれどんな値をしているか調べるためのコードを書いてみました。180x64pixelの画像として生成させます。SとVは255固定。
import cv2 import numpy as np import numpy.matlib hsv = np.full((64, 180, 3), 255, dtype=np.uint8) pos2 = np.arange(0, 180) # 0 ~ 180 hsv[:, :, 0] = np.matlib.repmat(pos2, 64, 1).astype(np.uint8) cv2.imwrite("color_bar.JPG", cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR))
このコードで作られる1周の色(H=0~180)は
こんな感じ。左から右へ向かって0度から360度まで(OpenCV上は0~180)の色を示しています。左(0度)と右(360度)で同じ赤を示しています。くどいですが、OpenCVのHの範囲は0~180です。
この色の種類(H)をある特定の色合い(セピアであれば赤褐色)に固定し、鮮やかさ(S)も特定の値で固定したものに対して、明るさに相当する部分にモノクロ写真を当てはめればそれっぽいセピア調の画像になります。
やはりWikipedia曰く、セピアトーンとは、HSV(29°, 60%, 42%)である。ともあるので、今回はこの色をベースとして値を決めることにします。
Hを値半分にすると15。Sを255に正規化してやると、60%は153となるので、コードとしては、HSV画像のHを15で埋める。Sを153で埋める。Vをグレースケール画像にする。そしてRGB画像に戻す。のステップで
import cv2 import numpy as np img = cv2.imread("input.jpg") img_hsv = np.zeros_like(img) img_hsv[:, :, 0] = np.full_like(img_hsv[:, :, 0], 15, dtype=np.uint8) img_hsv[:, :, 1] = np.full_like(img_hsv[:, :, 1], 153, dtype=np.uint8) img_hsv[:, :, 2] = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR) cv2.imwrite("output.jpg", img)
で済んでしまいます。これで、この令和の田園風景が、
一気に明治の雰囲気になります。
ただちょっと赤すぎる気がします。ココでHの値を少し大きくすると黄色味が強くなります。例えば21とかそんな値にすると、
こんな雰囲気に調整できます。好みですがHの値で色合いは調整できます。基本的なセピア調への変換はこれで完了です。
ノイズ重畳
本筋ではないですが、古めかしく見せるためにノイズの乗せます。以前トイカメラ風加工をした時と同じ処理です。
import numpy as np h, w, ch = img.shape gauss = np.random.normal(0, sigma, (h, w, ch)) gauss = gauss.reshape(h, w, ch) img = np.clip(img + gauss, 0, 255).astype(np.uint8)
これで、平均0、標準偏差sigmaのガウスノイズを作り、画像に足しこんでいます。最後画素のクリッピングもしています。こうしないと値が回り込みます。
コントラスト強調
これまた以前の記事と同じ処理を入れます。トーンカーブをスプライン曲線で近似して画像に施す処理です。
from scipy.interpolate import splrep, splev xs = [0, 0.25, 0.5, 0.75, 1] ys = [0, 0.15, 0.5, 0.85, 0.99] img = img / 255 tck = splrep(xs, ys) # スプライン img = splev(img, tck)*255 # スプライン適用
白を真っ白にしたくなかったので、今回は出力を1.0に張り付けません。最大値を99%に落としておきます。こうすれば白も少し黄ばんで色づきます。
周辺減光
画像の周辺を暗くする処理です。オールドレンズの雰囲気を出します。これも以前のトイカメラ風で使った関数を引っ張ってきます。オリジナルの関数はこっちの記事で詳細記載がありますので詳しくはそちらを読んでいただければと思います。
## # @fn peripheral_light_correct() # @brief 周辺減光補正関数 # @param img 入力画像(numpy array) # @param gain_params 補正値(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.距離画像にゲインを乗じ、ゲインマップを作製 gainmap = gain_params * r + 1 # 3.画像にゲインマップを乗じる return np.clip(img * gainmap, 0., 255)
結果
ここまで頑張ると、結果として冒頭に示したように
こんな雰囲気になります。味が出ます。
まとめ
基本はHSVの色空間遊びです。HとSをある値に固定するとセピア調に限らず、モノクロ写真に色の味付けができるようになります。セピアってことで今回は少し凝って少しレトロ調を加えました。それっぽい雰囲気にはなったかなと思います。
コード
全コードです。この規模なのでベタ書きです。
import cv2 import numpy as np from scipy.interpolate import splrep, splev ## # @fn peripheral_light_correct() # @brief 周辺減光補正関数 # @param img 入力画像(numpy array) # @param gain_params 補正値(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.距離画像にゲインを乗じ、ゲインマップを作製 gainmap = gain_params * r + 1 # 3.画像にゲインマップを乗じる return np.clip(img * gainmap, 0., 255) ############################### def main(): img = cv2.imread("input.JPG") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) h, w = gray.shape gauss = np.random.normal(0, 40, (h, w)) gauss = gauss.reshape(h, w) gray = np.clip(gray + gauss, 0, 255).astype(np.uint8) xs = [0, 0.25, 0.5, 0.75, 1] ys = [0, 0.15, 0.5, 0.85, 0.99] gray = gray / 255 tck = splrep(xs, ys) # スプライン gray = splev(gray, tck)*255 # スプライン適用 gray = peripheral_light_correct(gray, -0.4).astype(np.uint8) img_hsv = np.zeros_like(img) img_hsv[:, :, 0] = np.full_like(img_hsv[:, :, 0], 15, dtype=np.uint8) img_hsv[:, :, 1] = np.full_like(img_hsv[:, :, 1], 153, dtype=np.uint8) img_hsv[:, :, 2] = gray img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR) cv2.imwrite("sepia.JPG", img) if __name__ == '__main__': main()
コメント