【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)
この記事では、PythonのOpenCVをつかって、画像をグレースケール、モノ(2値化)、輪郭抽出、マスク(くり抜き)、背景透過を行っていきます。Python3.xを対象としてます。 フリー画像サイト 「unsplash」 さんからお借りした画像(左上)をベースに、こんな感じでOpenCVで加工していきます。
OpenCVのインストール
pipでOpenCVをインストールしましょう。numpyも使うのでインストールします。
$ pip install opencv-python
$ pip install opencv-contrib-python
$ pip install numpy
OpenCVでグレースケール変換
まずは、OpenCVでグレースケール変換です。とても簡単ですね! imread の第二引数に 0 を指定することでグレースケール変換できちゃいます。
import cv2
filename = "sample.jpg"
img = cv2.imread(filename , 0) #グレースケールで読み込む
cv2.imwrite(filename.split('.')[0]+"_gray.jpg", img)
オブジェクトからグレースケールに変換したい場合は、cvtColorを使って可能です。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
OpenCVで2値化(白黒、モノ)
2値化とは、画像を白と黒の2色だけで表現することをいいます。グレースケールでは白と黒の中間灰色もありましたが、2値化では白と黒だけしかありません。
プログラム的には、簡単です。まずカラー画像をグレースケール変換します。その後に、threshold関数の閾値で判定して白か黒に振り分けます。
import cv2
filename = "sample.jpg"
img = cv2.imread(filename , 0) #グレースケールで読み込む
# height, width = img.shape
# threshold = 96 # 閾値
# for i in range(height):
# for j in range(width):
# if img[i][j] < threshold:
# img[i][j] = 0
# else:
# img[i][j] = 255
ret, img_t32 = cv2.threshold(img, 32, 255, cv2.THRESH_BINARY)
ret, img_t64 = cv2.threshold(img, 64, 255, cv2.THRESH_BINARY)
ret, img_t96 = cv2.threshold(img, 96, 255, cv2.THRESH_BINARY)
ret, img_t128 = cv2.threshold(img, 128, 255, cv2.THRESH_BINARY)
ret, img_t192 = cv2.threshold(img, 192, 255, cv2.THRESH_BINARY)
cv2.imwrite(filename.split('.')[0] + "_mono_32.jpg", img_t32)
cv2.imwrite(filename.split('.')[0] + "_mono_64.jpg", img_t64)
cv2.imwrite(filename.split('.')[0] + "_mono_96.jpg", img_t96)
cv2.imwrite(filename.split('.')[0] + "_mono_128.jpg", img_t128)
cv2.imwrite(filename.split('.')[0] + "_mono_192.jpg", img_t192)
閾値を変えることで、表情がぜんぜん違って見えるので面白いですね!
OpenCVで色の反転(ネガ)
OpenCVで色を反転させるのはとても簡単です。次のプログラムでいけちゃいます。グレースケールやモノ画像の反転も可能です。
import cv2
filename = "sample.jpg"
img = cv2.imread(filename)
img_r = cv2.bitwise_not(img) # 反転処理
cv2.imwrite(filename.split('.')[0]+"_color_hanten.jpg", img_r)
ここまでくると、輪郭の抽出もできそうです!
OpenCVで輪郭の抽出
findContours を使うと輪郭の抽出がサクッとできます。次のプログラムでは、グレースケール → 2値化 → 反転 → 輪郭抽出 の流れで輪郭を抽出してます。import cv2
filename = "sample.jpg"
img = cv2.imread(filename, 0) # グレースケール
height, width = img.shape
ret, thresh = cv2.threshold(img, 96, 255, cv2.THRESH_BINARY)# 閾値で2値化
img_r = cv2.bitwise_not(thresh) # 反転処理
contours, hierarchy = cv2.findContours(img_r, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # 輪郭抽出
img_color = cv2.cvtColor(img_r, cv2.COLOR_GRAY2BGR) # 輪郭を赤い線で描くためにカラー画像へ変換
contours = list(filter(lambda x: cv2.contourArea(x) > 5000, contours)) # 小さい輪郭は無視する
cv2.drawContours(img_color, contours, -1, color=(0, 0, 255), thickness=2) # 輪郭を描画
cv2.imwrite(filename.split('.')[0]+"_rinkaku.jpg", img_color)
(反転処理はしなくてもよいのですが、ここまでの「おさらい」として付け足しました。)
OpenCVで指定した色を別の色に変える(HSV色空間)
HSV色空間とは、「色相(Hue)」「彩度(Saturation)」「明度(Value)」の三要素で表される手法です。色相は0〜179、彩度と明度は0〜255の値で表されます。
次のプログラムでは、numpyの where 関数を使って色相が120以上であれば60を引き算することで色を変えてます。写真のように、ピンク色の部分だけを青色に変えることができました。HSV色空間で処理すれば、特定の色だけを別の色に自然?な形で変えることができます。面白いですね!
import cv2
import numpy as np
filename = "sample.jpg"
img = cv2.imread(filename)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # BGR->HSV変換
hsv_2 = np.copy(hsv)
print('色相の最小値:' + str(hsv[:,:,0].min()))
print('色相の最大値:' + str(hsv[:,:,0].max()))
# 色相が120以上であれば-60する
hsv_2[:, :, 0] = np.where(hsv[:, :, 0]>120, hsv[:, :, 0]-60, hsv[:, :, 0])
print('色相の最小値:' + str(hsv_2[:,:,0].min()))
print('色相の最大値:' + str(hsv_2[:,:,0].max()))
bgr = cv2.cvtColor(hsv_2, cv2.COLOR_HSV2BGR) # HSV->BGR変換
cv2.imwrite(filename.split('.')[0]+"_change_color.jpg", bgr)
OpenCVでマスク画像の作成(背景を抽出)
緑はHSV色空間で表すと、50付近にあたります。元画像の背景色は緑色が使われてますから、次のプログラムでHSV空間の範囲を指定してくり抜くことができます。
import cv2
import numpy as np
filename = "sample.jpg"
img = cv2.imread(filename)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 緑色の検出
hsv_min = np.array([30, 30, 0])
hsv_max = np.array([100,255,255])
mask = cv2.inRange(hsv, hsv_min, hsv_max) # HSVからマスクを作成
masked_img = cv2.bitwise_and(img, img, mask=mask)
cv2.imwrite(filename.split('.')[0]+"_masked_img.jpg", masked_img)
OpenCVで画像と画像の合成(女性のみを表示)
女性のみをくり抜く場合はどうすればよいでしょうか?inRange の逆をやりたいのですが、良いやり方が分かりませんでした。仕方がないので、緑以外のHSV色空間を2回に分けて抽出し、2枚の画像パーツを重ね合わせることで女性のみを表示できました。addWeighted を使った、画像の合成のテクニックが覚えられます。
import cv2
import numpy as np
filename = "sample.jpg"
img = cv2.imread(filename)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hsv_min = np.array([0, 0, 0])
hsv_max = np.array([40,255,255])
mask = cv2.inRange(hsv, hsv_min, hsv_max)
masked_img1 = cv2.bitwise_and(img, img, mask=mask)
hsv_min = np.array([100, 0, 0])
hsv_max = np.array([180,255,255])
mask = cv2.inRange(hsv, hsv_min, hsv_max)
masked_img2 = cv2.bitwise_and(img, img, mask=mask)
blended = cv2.addWeighted(src1=masked_img1,alpha=1,src2=masked_img2,beta=1,gamma=0)
cv2.imwrite(filename.split('.')[0]+"_only_woman.jpg", blended)
OpenCVで背景透過の画像を作成(くり抜き)
先ほどの黒い背景を透過にするにはどうすればよいでしょうか?黒を透明に置換できればイケそうですが、女性の部分にも黒が含まれているため、そこは除外して背景のみを透過したいです。 そこで、先ほどやった輪郭の抽出を使い、女性部分のマスク作って背景を透過させました。
import cv2
import numpy as np
filename = "sample.jpg"
img = cv2.imread(filename)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# HSV色空間から背景の緑だけ削除する
hsv_min = np.array([0, 0, 0])
hsv_max = np.array([40,255,255])
mask = cv2.inRange(hsv, hsv_min, hsv_max)
masked_img1 = cv2.bitwise_and(img, img, mask=mask)
hsv_min = np.array([100, 0, 0])
hsv_max = np.array([180,255,255])
mask = cv2.inRange(hsv, hsv_min, hsv_max)
masked_img2 = cv2.bitwise_and(img, img, mask=mask)
# 2つの画像を足し合わせる
blended = cv2.addWeighted(src1=masked_img1,alpha=1,src2=masked_img2,beta=1,gamma=0)
cv2.imwrite(filename.split('.')[0]+"_kurinuki_blended.jpg", blended)
# 画像を2値化する
gray = cv2.cvtColor(blended, cv2.COLOR_BGR2GRAY)
cv2.imwrite(filename.split('.')[0]+"_kurinuki_gray.jpg", gray)
ret, mono = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)
cv2.imwrite(filename.split('.')[0]+"_kurinuki_mono.jpg", mono)
# 輪郭を抽出する
contours, hierarchy = cv2.findContours(mono, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
rinkaku = cv2.cvtColor(mono, cv2.COLOR_GRAY2BGR)
contours = list(filter(lambda x: cv2.contourArea(x) > 5000, contours))
cv2.drawContours(rinkaku, contours, -1, color=(255, 255, 255), thickness=-1) # 輪郭からマスクを作成
cv2.imwrite(filename.split('.')[0]+"_kurinuki_rinkaku.jpg", rinkaku)
# 輪郭画像から背景透過のマスクを作成する
mask = np.all(rinkaku[:,:,:] == [0, 0, 0], axis=-1)
dst = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
dst[mask, 3] = 0
cv2.imwrite(filename.split('.')[0]+"_kurinuki.png", dst)
# 参考: https://algorithm.joho.info/programming/python/opencv-color-detection/
(もっと良い方法がありそうですが、とりあえずは学習も兼ねてこれでよしとします。)
おわり
いかがだったでしょうか?私もまだまだOpenCVの学習中です。あとはnumpyやHSV色空間など、細かな処理を覚えていけばOpenCVも使いこなせそうですね!