【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)

【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)
【Python】OpenCVで画像操作いろいろ(グレースケール・モノ・輪郭抽出・切り抜く・透過)

この記事では、PythonのOpenCVをつかって、画像をグレースケール、モノ(2値化)、輪郭抽出、マスク(くり抜き)、背景透過を行っていきます。Python3.xを対象としてます。 フリー画像サイト 「unsplash」 さんからお借りした画像(左上)をベースに、こんな感じでOpenCVで加工していきます。

OpenCVで加工する画像素材
OpenCVで加工する画像素材

OpenCVのインストール

pipでOpenCVをインストールしましょう。numpyも使うのでインストールします。

shell
$ pip install opencv-python
$ pip install opencv-contrib-python
$ pip install numpy

OpenCVでグレースケール変換

OpenCVでグレースケール変換
OpenCVでグレースケール変換

まずは、OpenCVでグレースケール変換です。とても簡単ですね! imread の第二引数に 0 を指定することでグレースケール変換できちゃいます。

cv_gray.py
import cv2

filename = "sample.jpg"
img = cv2.imread(filename , 0) #グレースケールで読み込む 
 
cv2.imwrite(filename.split('.')[0]+"_gray.jpg", img)

オブジェクトからグレースケールに変換したい場合は、cvtColorを使って可能です。

py
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

OpenCVで2値化(白黒、モノ)

OpenCVで2値化、閾値32
OpenCVで2値化、閾値32
OpenCVで2値化、閾値64
OpenCVで2値化、閾値64
OpenCVで2値化、閾値96
OpenCVで2値化、閾値96
OpenCVで2値化、閾値128
OpenCVで2値化、閾値128
OpenCVで2値化、閾値192
OpenCVで2値化、閾値192

2値化とは、画像を白と黒の2色だけで表現することをいいます。グレースケールでは白と黒の中間灰色もありましたが、2値化では白と黒だけしかありません。

プログラム的には、簡単です。まずカラー画像をグレースケール変換します。その後に、threshold関数の閾値で判定して白か黒に振り分けます。

cv_mono.py
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でカラー写真の反転
OpenCVでカラー写真の反転
OpenCVでグレースケールの反転
OpenCVでグレースケールの反転
OpenCVでモノ反転
OpenCVでモノ反転

OpenCVで色を反転させるのはとても簡単です。次のプログラムでいけちゃいます。グレースケールやモノ画像の反転も可能です。

cv_hanten.py
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で輪郭の抽出

OpenCVで輪郭の抽出
OpenCVで輪郭の抽出

findContours を使うと輪郭の抽出がサクッとできます。次のプログラムでは、グレースケール → 2値化 → 反転 → 輪郭抽出 の流れで輪郭を抽出してます。
cv_rinkaku.py
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色空間)

OpenCVで指定した色を別の色に変える
OpenCVで指定した色を別の色に変える

HSV色空間とは、「色相(Hue)」「彩度(Saturation)」「明度(Value)」の三要素で表される手法です。色相は0〜179、彩度と明度は0〜255の値で表されます。

HSV色空間
HSV色空間

次のプログラムでは、numpyの where 関数を使って色相が120以上であれば60を引き算することで色を変えてます。写真のように、ピンク色の部分だけを青色に変えることができました。HSV色空間で処理すれば、特定の色だけを別の色に自然?な形で変えることができます。面白いですね!

cv_change_color.py
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でマスク画像の作成(背景を抽出)

OpenCVでマスク画像の作成(背景を抽出)
OpenCVでマスク画像の作成(背景を抽出)

緑はHSV色空間で表すと、50付近にあたります。元画像の背景色は緑色が使われてますから、次のプログラムでHSV空間の範囲を指定してくり抜くことができます。

cv_mask.py
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で画像と画像の合成(女性のみを表示)

OpenCVで画像と画像の合成(女性のみを表示)
OpenCVで画像と画像の合成(女性のみを表示)

女性のみをくり抜く場合はどうすればよいでしょうか?inRange の逆をやりたいのですが、良いやり方が分かりませんでした。仕方がないので、緑以外のHSV色空間を2回に分けて抽出し、2枚の画像パーツを重ね合わせることで女性のみを表示できました。addWeighted を使った、画像の合成のテクニックが覚えられます。

cv_gousei.py
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で背景透過の画像を作成(くり抜き)

背景の緑を削除
背景の緑を削除
グレースケール化
グレースケール化
2値化
2値化
輪郭抽出でマスクを作成
輪郭抽出でマスクを作成
マスクをかける
マスクをかける

先ほどの黒い背景を透過にするにはどうすればよいでしょうか?黒を透明に置換できればイケそうですが、女性の部分にも黒が含まれているため、そこは除外して背景のみを透過したいです。 そこで、先ほどやった輪郭の抽出を使い、女性部分のマスク作って背景を透過させました。

cv_kurinuki.py
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も使いこなせそうですね!

関連記事

アイデアノート > 画像・動画処理
最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

この記事で紹介した商品

Pandasで画像・動画処理をはじめよう!
Python学習にオススメの本をご紹介!