nyanpyou Note

主な目的は調べたり作ったりしたプログラミング備忘録(予定)

OpenCVとNumpyで比較明合成をしたい話

低速度シャッター(といっても0.2秒くらい)で撮影をしていると、映したいものが2枚の写真に跨ってしまうことが稀によくある。
そこで、パソコンを使って合成を行い、視認性の良い写真を作りたくなった。何か良い合成方法はないかと調べてみると、この比較明合成という方法が今回のニーズに合っているように思える。 capa.getnavi.jp 天体撮影とかで良く行われている方法で、2つの画素を比べて明るい方を採用するという単純な仕組みだそうだ。画像編集用の商業ソフトやフリーソフトなどに搭載されている機能だそうだが、それでは融通が利かないこともありそうなので、自分で作ることにした。
使うのはいつものOpenCVとNumpy。正直よく調べれば既にモジュールとして実装されていそうだが、そこは目をつぶることにする。れ…練習だから…(言い訳)

Pythonでは、画素の要素を1個ずつforとインデックスを使って取り出して計算すると、半端なく処理が遅くなることは経験済みなので、Numpyに備わっている機能を使って画素の要素にアクセスする。

画像の要素にアクセスする

import cv2
import numpy as np

A = cv2.imread("A.jpg")
#(0,0)のr成分を抜き出す
r = A.item(0,0,2)
#(0,0)のg成分を抜き出す
g = A.item(0,0,1)
#(0,0)のb成分を抜き出す
b = A.item(0,0,0)

画像の要素を書き換える

import cv2
import numpy as np

A = cv2.imread("A.jpg")
#(0,0)のr成分を255に書き換える
r = A.itemset((0,0,2), 255)
#(0,0)のg成分を255に書き換える
g = A.itemset((0,0,1), 255)
#(0,0)のb成分を255に書き換える
b = A.itemset((0,0,0), 255)

これらを使って、2枚の画像で比較明合成を行うプログラムを作ってみる。やることは単純に、2枚の画像を端から比べていって、輝度の明るい方の画素を採用する形にした。

比較明合成

#比較明合成
import cv2
import numpy as np

#.shape[0]:height .shape[1]:width .shape[2]:color

A = cv2.imread("A.jpg")
B = cv2.imread("B.jpg")
C = np.zeros((A.shape[0],A.shape[1],A.shape[2]), np.uint8)

for i in range(A.shape[0]-1):#height行
    for j in range(A.shape[1]-1):#width列
        #RGBの値から輝度値に変換 opencvで読み込むとBGRの順で格納されている
        #Y = 0.299 * R + 0.587 * G + 0.114 * B
        A_Y = 0.299*A.item(i,j,2) + 0.587*A.item(i,j,1) + 0.114*A.item(i,j,0)
        B_Y = 0.299*B.item(i,j,2) + 0.587*B.item(i,j,1) + 0.114*B.item(i,j,0)
        
        if B_Y>A_Y:
            C.itemset((i,j,0),B.item(i,j,0))
            C.itemset((i,j,1),B.item(i,j,1))
            C.itemset((i,j,2),B.item(i,j,2))
        else:
            C.itemset((i,j,0),A.item(i,j,0))
            C.itemset((i,j,1),A.item(i,j,1))
            C.itemset((i,j,2),A.item(i,j,2))

cv2.imshow("result", C)
cv2.imwrite("result.jpg", C)
cv2.waitKey(0)
cv2.destroyAllWindows

f:id:nyanpyou106:20200214160008j:plainf:id:nyanpyou106:20200214160026j:plain
この2枚を合成する
f:id:nyanpyou106:20200214160107j:plain
合成結果。少し元写真がズレているためacerの文字が浮き上がってしまっている。
このように明るい部分だけを合成することが出来た。
上のコード中で空の(全ての要素を0で埋めた)画像Cを作る際にnp.zerosを使っているが、必ずnp.uint8でintを指定するのを忘れない(自戒)。指定しないままにするとfloatが選択されて、プレビュー画面がおかしなことになる(cv2.imwriteで保存した画像は、問題なく見えるようだった)。

以下改善点と妄想

インデックスで直接画素にアクセスしていないとはいえ、一眼レフで撮影した写真のようにサイズが大きくなると、処理が終わるまでにそこそこ時間がかかる(30秒くらい)。2重for文を入れているから仕方ないのだろうか。Numpyには色々な行列計算の機能が備わっているため、しっかり調べたら出来そうな気もする。ぱっと見つけて使えそうだなと思った機能を2個メモしておく。

Numpyで配列の要素同士を掛け算する。

import numpy as np
a = np.array([[1,2],[3,4]])
b = np.array([[5,6],[7,8]])
print(a*b)
>>
[[5,12]
 [21,32]]

同様に配列同士で四則演算すると、同じ要素同士で計算された配列が返ってくる。

Numpyで配列の要素を条件に従って書き換える。

import numpy as np
a = np.array([[0,50,100],[150,200,250]])
print(np.where(a <= 150, 0, 1))
>>
[[0,0,0]
 [0,1,1]]


処理のステップは以下のようにできないだろうか。

処理のステップ(妄想)

  1. 2枚の画像を取り込んで(A、Bとする)、グレースケールに変換する(A'、B'とする)。
  2. 2つの配列を減算(B'-A')し、np.whereを使って、輝度が0以下になった要素を0、それ以外の要素を1に置き換えた配列Cを作る。
  3. その後配列Cの0と1を逆にした配列Dも作る。
  4. BとCを使って、Bの方が明るい画素以外を0にした配列B''を作る。
  5. 同様にAとDを使って、配列A''を作る。
  6. A''とB''を加算して合成画像を作成する。

実際に試しに作ってみたが、配列AとBが3次元配列(カラー画像)なのに対して、配列Cが2次元配列(グレースケールのA'B'を元に作成)になるため、どうにもうまく配列B''が作れない。最初はBとCで掛け算すれば1の部分だけ抜き出せるやろ~とか思っていたのだが、operands could not be broadcast together with shapes~とValue Errorが出て怒られてしまう。forで要素にアクセスすると元も子もないので、何かいい方法はないだろうか…。