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

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

この記事では、PythonのOpenCVをつかって、画像をグレースケール、モノ(2値化)、輪郭抽出、マスク(くり抜き)、背景透過を行っていきます。Python3.xを対象としています。

フリー画像サイト 「unsplash」 さんからお借りした画像(左上)をベースに、こんな感じでOpenCVで加工していきます。

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

OpenCVのインストール

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

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

OpenCVでグレースケール変換

OpenCVでグレースケール変換
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で輪郭の抽出

OpenCVで輪郭の抽出
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色空間)

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

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

HSV色空間
HSV色空間

次のプログラムでは、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でマスク画像の作成(背景を抽出)

OpenCVでマスク画像の作成(背景を抽出)
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で画像と画像の合成(女性のみを表示)

OpenCVで画像と画像の合成(女性のみを表示)
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も使いこなせそうですね!

記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
Python学習にオススメの本をご紹介!
Pandasでデータサイエンスはじめよう!
関連記事