ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【python/OpenCV】画像の特定の色を抽出する方法

 今回はpythonOpenCVを使って画像の特定の色を抽出する方法の備忘録です。
 OpenCVでは2つの方法で特定の色を抽出できるので、その2つの方法を説明していきます。

 では始めます。


1:RGBでの色抽出方法
 まず一つ目の方法としてRGBでの色の抽出方法があります。その方法は以下の記述でできます。

import cv2
import numpy as np

image = cv2.imread('./test1.png') # ファイル読み込み

# BGRでの色抽出
bgrLower = np.array([102, 255, 255])    # 抽出する色の下限(BGR)
bgrUpper = np.array([102, 255, 255])    # 抽出する色の上限(BGR)
img_mask = cv2.inRange(image, bgrLower, bgrUpper) # BGRからマスクを作成
result = cv2.bitwise_and(image, image, mask=img_mask) # 元画像とマスクを合成

 ※OpenCVでは「RGBではなくBGRになっている」ので色の順番には注意してください。

 抽出する色の上限下限を作成してinRange関数でマスクを取得します。そのマスクを元画像に適応するとこで、範囲内の色以外を黒くするという方法になっています。


2:HSVでの色抽出方法
 もう一つの方法としてHSVでの色の抽出方法があります。

 HSVとは「色相(Hue)・彩度(Saturation)・明度(Value)」で色を表したもので、以下のリンクからRGB・HSVの変換ができます。

 ちなみにOpenCVでのそれぞれの範囲は以下の通りとなっています。

名称 値の範囲 備考
色相(H) 0〜180 本来は0〜360だがOpenCVでは1/2の範囲になる
彩度(S) 0〜255 値が0に近づくほど白く、255に近づくほどHの色になる
明度(V) 0〜255 値が0に近づくほど黒く、255に近づくほどHの色になる

 そのHSVを使った色の抽出方法は以下の記述でできます。

import cv2
import numpy as np

image = cv2.imread('./test1.png') # ファイル読み込み

# HSVでの色抽出
hsvLower = np.array([30, 153, 255])    # 抽出する色の下限(HSV)
hsvUpper = np.array([30, 153, 255])    # 抽出する色の上限(HSV)
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 画像をHSVに変換
hsv_mask = cv2.inRange(hsv, hsvLower, hsvUpper)    # HSVからマスクを作成
result = cv2.bitwise_and(image, image, mask=hsv_mask) # 元画像とマスクを合成

 1の手法と似ていますが、上限下限がHSVになっているのと一度画像をHSV形式に変換しているところが違っています。


3:実装例
 色抽出の方法がわかったので実際に以下の画像から背景色を抽出してみたいと思います。
f:id:rikoubou:20190221185338p:plain

 上記の画像を「test1.png」として保存し、同じ階層に以下のファイルを作成して実行します。

・colorExtraction.py

#
# 色抽出のサンプルコード
#
import numpy as np
import cv2
from time import sleep

# メイン関数
def main():
    image = cv2.imread('./test1.png') # ファイル読み込み

    # BGRでの色抽出
    bgrLower = np.array([102, 255, 255])    # 抽出する色の下限
    bgrUpper = np.array([102, 255, 255])    # 抽出する色の上限
    bgrResult = bgrExtraction(image, bgrLower, bgrUpper)
    cv2.imshow('BGR_test1', bgrResult)
    sleep(1)

    # HSVでの色抽出
    hsvLower = np.array([30, 153, 255])    # 抽出する色の下限
    hsvUpper = np.array([30, 153, 255])    # 抽出する色の上限
    hsvResult = hsvExtraction(image, hsvLower, hsvUpper)
    cv2.imshow('HSV_test1', hsvResult)
    sleep(1)

    while True:
        # キー入力を1ms待って、keyが「q」だったらbreak
        key = cv2.waitKey(1)&0xff
        if key == ord('q'):
            break

    cv2.destroyAllWindows()


# BGRで特定の色を抽出する関数
def bgrExtraction(image, bgrLower, bgrUpper):
    img_mask = cv2.inRange(image, bgrLower, bgrUpper) # BGRからマスクを作成
    result = cv2.bitwise_and(image, image, mask=img_mask) # 元画像とマスクを合成
    return result

# HSVで特定の色を抽出する関数
def hsvExtraction(image, hsvLower, hsvUpper):
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 画像をHSVに変換
    hsv_mask = cv2.inRange(hsv, hsvLower, hsvUpper)    # HSVからマスクを作成
    result = cv2.bitwise_and(image, image, mask=hsv_mask) # 元画像とマスクを合成
    return result


if __name__ == '__main__':
    main()

 すると以下のようにどちらの方法でも背景色のみが抽出できます。
f:id:rikoubou:20190221185739p:plain


 以上がpythonOpenCVで特定の色を抽出する方法です。

 基本的に画像から色を抽出する時ははっきりとした色ではなくグラデーションがある場合が多いです。
 どちらか一方だけではなく、画像の内容や目的によってBGRの方法とHSVの方法を使い分ける必要が出てくるかと思います。


・参考資料

【python】クラスの継承

rikoubou.hatenablog.com

 一つ前の記事では単体テストをやりました。今回はタイトルにある通りpythonでのクラスの継承です。

 正直参考資料のページを参照した方がわかりやすいかと思いますが、自分のための備忘録として記事に残しておきます。

 では始めます。


・継承の記述方法
 pythonでのクラスの継承の記述方法は簡単で以下の通りです。

class 新しいクラス名(継承元クラス名):
	# 以下略

 継承元の関数をそのまま使いたい場合は特に何も記述する必要はありません。
 逆に継承元の関数の内容を上書きしたい場合は、単純に同じ関数名で記述すればOKです。

 具体例として以下の単純なクラスの継承例を作りました。

#
# 継承テスト例
#

# 継承元のクラス
class ParentClass():
	def __init__(self, name):
		self.name = name
	
	def showName(self):
		print("Parent name is ", self.name)

# ParentClassを継承したクラス1
class ChildClass1(ParentClass):
	def showChildName(self):
		"""
		showChildName関数を新たに追加
		"""
		print("showChildName is ", self.name)

# ParentClassを継承したクラス2
class ChildClass2(ParentClass):
	def showName(self):
		"""
		showName関数を上書き
		"""
		print("Child2 name is ", self.name)

 また上記の内容を呼び出して実行するプログラムが以下の通りです。

# import文は省略

# メイン関数
def main():
	parent = ParentClass("Parent")
	parent.showName()

	child1 = ChildClass1("child1")
	child1.showName()
	child1.showChildName()

	child2 = ChildClass2("child2")
	child2.showName()

if __name__ == '__main__':
    main()

 上記を実行すると以下のような結果になります。
f:id:rikoubou:20190220162707p:plain

 2行目は「ChildClass1」からshowName関数を呼んだ結果が表示されています。ChildClass1自体にはshowName関数の記述は一切ありませんが、継承元の「ParentClass」に記述されているshowName関数が呼ばれているのがわかります。

 3行目は「ChildClass1」に新しく追加した関数であるshowChildName関数の結果が表示されています。

 4行目は「ChildClass2」で継承元の内容を上書きしたshowChildName関数の結果がちゃんと表示されています。


 以上がpythonでのクラスの継承方法です。

 似たような機能だけど少し違う、または元の機能にちょっとだけ追加するクラスが必要になった場合にはこの継承機能にお世話になることになると思います。


・参考資料

【python】単体テストの記述方法

rikoubou.hatenablog.com

 以前書いた記事でpythonのクラスとファイル分けをしました。

 今回はこの時作成したsumClass.pyを例として単体試験の書き方の備忘録という内容です。

 では始めます。


1:単体テスト用の書き方
 pythonにはデフォルトで単体試験用のクラスが用意されています。
 それを使用するには以下の一文で「unittest」をインポートします。

import unittest # 単体テストクラスインポート

 テスト内容の実装についてはunittest.TestCase のサブクラスとして作成し、その中に「test_」から始まるテスト関数を以下のように記述していきます。

import unittest # 単体テストクラスインポート

class TestSumClass(unittest.TestCase):
    def test_1(self):
        # テスト1の記述
    
    def test_2(self):
        # テスト2の記述
    
    # 以下略

 テストコードでの戻り値の確認などをする場合は以下のようなチェック関数があります。

# 第1引数と第2引数が同じ場合OK、違う場合NG
self.assertEqual('foo'.upper(), 'FOO')

# 引数の結果がTrueならOK、FalseならNG
self.assertTrue('FOO'.isupper())

# 引数の結果がFalseならOK、TrueならNG
self.assertFalse('Foo'.isupper())

 基本的には上記ぐらいが扱えれば特に問題はないかと思います。


2:単体テスト用のファイルの作成
 テスト用のファイルはどこでもいいのですが、前の記事で作成したmain.pyからsumClass.pyを呼び出しているので同じ階層に作るのが一番わかりやすくて良いかと思います。import文も呼び出しているところのコピペで済みます。
 今回は以下の場所に「test_sum.py」として単体テスト用ファイルを作成しました。
f:id:rikoubou:20190205172929p:plain


3:実際に単体テストを書いてみる
 単体テストの書き方がわかったので実際に「test_sum.py」を書いてみます。

 SumClassの中の「mySum」関数のテストコードを書いてみたのが以下になります。

#
# 単体テストコード
#
import unittest # 単体テストクラスインポート
from sum import sumClass as sc # SumClass読み込み

# テストのクラス
class TestSumClass(unittest.TestCase):
	"""
	test class of sumClass.py
	"""

	# テスト1
	def test_mySum1(self):
		sumInstance = sc.SumClass()
		result = sumInstance.mySum(10, 11)
		self.assertEqual(result, 21)

	# テスト2
	def test_mySum2(self):
		sumInstance = sc.SumClass()
		result = sumInstance.mySum(1, 11)
		self.assertEqual(result, 2)

if __name__ == '__main__':
    unittest.main()

 上記を実行すると「test_mySum1」についてはOKで「test_mySum2」についてはNGになります。
f:id:rikoubou:20190205174834p:plain

 一つでもNGとなった場合は、NGとなった関数名が出た後「FAILED(failures=[NG数])」としてNG数も表示されます。

 ちなみに全てOKだった場合は以下のように、OKとだけ表示されます。
f:id:rikoubou:20190205175114p:plain


 以上が簡単ですがpythonでの単体テストの記述方法です。

 複雑なものを作る場合には単体テストをやる必要が出てくるかと思うので、自分も記述するようにしていきたいです。


・参考資料

【Blender】Blender2.8でレンダリングの背景を360度画像に設定&画像として出力する方法

 今回はBlender2.8でレンダリング時の背景を360度画像に設定&出力する方法です。

 Blender2.8で説明していきますが、基本的にはBlender2.79でも同じ方法でできます。詳しいやり方は参考資料のサイト様を参照してください。

 では始めます。


1:レンダリングの背景を360度パノラマに設定する
 Blender2.8を起動させたら、「World」タブを開いて「Color」の項目の横にある○を左クリックします。
f:id:rikoubou:20190125170558p:plain

 なんでもよいですが、今回は「Brick Texture」を選択します。(Colorの項目に出てくる他の項目でも背景を設定できます)
f:id:rikoubou:20190125172002p:plain

 Colorが「Brick Texture」になり表示される項目が変更されますが、とりあえず値は変更せずそのままにします。
f:id:rikoubou:20190125172140p:plain

 この状態で3D Viewの右上にある「Rendered」ボタンを左クリックすると、以下のように背景がレンガ模様になります。
f:id:rikoubou:20190125172343p:plain

 マウスの中ホイールをドラッグしながらグリグリ画面を動かすと、背景もいい感じに動きます。
f:id:rikoubou:20190125172629g:plain

 これで360度の背景を設定できました。


2:Blenderで360度画像を出力する
 背景を360度のパノラマに設定できたので、これを画像として出力します。

「Render」タブを開いて「Render Engine」のプルダウンから「Cycles」を選択して、Cyclesレンダーに変更します。
f:id:rikoubou:20190125173308p:plain

 Cyclesレンダーに変更後、Cameraを選択した状態で「Camera」タブを開き、「Type」のプルダウンから「Panoramic」を選択します。
f:id:rikoubou:20190125173722p:plain

「Panoramic」にすると表示される項目が変更されるので「Panorama Type」を左クリックします。
f:id:rikoubou:20190125173919p:plain

 出てきたプルダウンの中から「Equirectangular」を選択します。
f:id:rikoubou:20190125174054p:plain

 これで360度でのレンダリング出力設定ができたので、上部メニューの「Render」→「Render Image」で画像としてレンダリング出力します。 
f:id:rikoubou:20190125174232p:plain

 レンダリング結果は以下のようになります。
f:id:rikoubou:20190125174457p:plain

 あとはいつものようにメニューの「Image」→「Save As...」から画像を保存します。
f:id:rikoubou:20190125174546p:plain

 出力した画像は以下のようになります。
f:id:rikoubou:20190125174753p:plain


3:レンダリングの背景を任意の360度画像に設定する
 360度画像が出力できたので、今度はその画像を読み込んで360度背景に設定してみます。

 先ほどと同じように「World」タブを開いて「Color」の項目の横にある○を左クリックします。
f:id:rikoubou:20190125170558p:plain

 今回は「Enviroment Texture」を選択します。
f:id:rikoubou:20190125170821p:plain

 項目が変更されるので「Open」ボタンを左クリックして任意の360度画像を開きます。
f:id:rikoubou:20190125170959p:plain

 すると以下のように360度画像を背景に設定することができます。
f:id:rikoubou:20190125175311p:plain


 以上がBlender2.8で背景を360度画像に設定&出力する方法になります。

 色々使い道があるかと思うので、この方法を使って何か作ってみたいです。


・参考資料

【python】クラスとファイル分け

 今まで色々とpythonを使ってソースコードを書いてきましたが、処理を別ファイルに分けたりクラスに分けたりということを特に意識していませんでした。

 なので今回はpythonでの「クラスとファイル分け」についての備忘録です。

 では始めます。


0:テスト用コード
 とりあえず今回は以下の適当なコードを使ってクラス分けとファイル分けを行なっていきます。

・main.py

#
# テストコード
#
def main():
	print("---start---")

	while True:
		addend1 = input("input the addend1 : ")
		if (addend1 == "end"):
			break

		addend2 = input("input the addend2 : ")
		result = mySum(addend1, addend2)

		showSum(addend1, addend2, result)

	print("---end---")

def mySum(add1, add2):
	return int(add1) + int(add2)

def showSum(add1, add2, result):
	print(str(add1) + " + " + str(add2) + " = " + str(result))

if __name__ == '__main__':
    main()

1:クラス分け
 pythonでのクラスの書き方は以下の通りです。

#
# クラスの書き方
#
class ClassName():
	def __init__(self):
		# コンストラクタの何かしらの処理

	def func1(self, x, y):
		# 何かしらの処理

	def func2(self, x, y, z):
		self.func1(x, y, z)
		# 何かしらの処理

	# 以下略

 これだけ見れば大体わかるかとは思いますが、「class ClassName():」のところがクラス名の定義です。「def __init__(self):」がコンストラクタで、そのあと「def func1(self, x, y):」と「def func2(self, x, y, z):」がクラスが持つ関数になります。

 全ての関数に書かれている引数のselfはインスタンス自身を指しており、クラス内の関数を使う場合には「self.関数名」のようにして呼び出します。

 かなり雑な説明をしましたが、これでクラス分けの方法がわかったので、テストコードをクラス分けしてみます。

 クラス分けすると以下のようになります。

・main.py

#
# テストコード2
#

# クラス分け
class SumClass():
	def __init__(self):
		print("init end")
	
	def sumLoop(self):
		while True:
			addend1 = input("input the addend1 : ")
			if (addend1 == "end"):
				break

			addend2 = input("input the addend2 : ")
			result = self.mySum(addend1, addend2)

			self.showSum(addend1, addend2, result)

	def mySum(self, add1, add2):
		return int(add1) + int(add2)
	
	def showSum(self, add1, add2, result):
		print(str(add1) + " + " + str(add2) + " = " + str(result))

# メイン関数
def main():
	sumInstance = SumClass()
	print("---start---")
	sumInstance.sumLoop()
	print("---end---")

if __name__ == '__main__':
    main()


2:ファイル分け
 クラス分けができたので、今度は処理を別ファイルに分ける方法です。

 main.pyのSumClass部分を切り出して「sumClass.py」というファイル名で保存します。

・sumClass.py

#
# sumClass.py
#
class SumClass():
	def __init__(self):
		print("init end")
	
	def sumLoop(self):
		while True:
			addend1 = input("input the addend1 : ")
			if (addend1 == "end"):
				break

			addend2 = input("input the addend2 : ")
			result = self.mySum(addend1, addend2)

			self.showSum(addend1, addend2, result)

	def mySum(self, add1, add2):
		return int(add1) + int(add2)
	
	def showSum(self, add1, add2, result):
		print(str(add1) + " + " + str(add2) + " = " + str(result))

 main.pyと同じ階層に「sum」という名前でフォルダを作成し、そのsumフォルダ内に「sumClass.py」を移動させます。
f:id:rikoubou:20190121164125p:plain

 次にmain.pyからsumClass.pyを呼び出す方法ですが、以下のように記述します。

from sum import sumClass as sc

「from」の後に記述するのがpythonの現在位置から呼び出したいフォルダの相対パスです。
「import」の後に記述するのが呼び出すpythonのファイル名、そして「as」の後に記述するのが呼び出すファイルを使いやすくするために新たに名付けた名前です。

 この一文を訳すと「実行するpythonファイルと同じ階層にあるsumというフォルダの中にあるsumClass.pyというファイルをscという名前で使う」という感じです。

 このimportの書き方は以下のページがわかりやすいので参照してください。

 これで外部ファイルの読み込み方法がわかったので、main.pyを以下のように修正します。

・main.py

#
# テストコード3
#
from sum import sumClass as sc # この文を追加して読み込み

# メイン関数
def main():
	sumInstance = sc.SumClass() # scという名前にしたのでそこでインスタンス関数を呼び出す
	print("---start---")
	sumInstance.sumLoop()
	print("---end---")

if __name__ == '__main__':
    main()

 これでクラスとファイル分けができました。


 以上がpythonのクラスとファイル分けの方法です。

 かなり雑ですし色々足りないとは思いますが、ざっくりやる方法ということで。


・参考資料

【Blender】Blender2.8での日本語化設定方法

 そういえばまだBlender2.8の日本語化をやっていなかったのとBlender2.8で場所が変わったりなど色々していたので、その備忘録です。

 では、始めます。


・Blender2.8の日本語化
 Blender2.8を起動させたら上部メニューにある「Edit」→「Preferences」を左クリックします。
f:id:rikoubou:20190116152252p:plain

 以下のように設定画面が表示されます。
f:id:rikoubou:20190116152351p:plain

 左側の「Interface」タブを選択し、「Text」を左クリックして出てきた中にある「Transrate UI」にチェックを入れます。
f:id:rikoubou:20190116152519p:plain

「Language」のプルダウンを左クリックすると言語の一覧が出てくるので「Japanese(日本語)」を左クリックします。
f:id:rikoubou:20190116152723p:plain

 プルダウンから言語を設定したら、以下のように一番下以外のものにチェックを入れます。
f:id:rikoubou:20190116153026p:plain

 すると以下のように日本語の表示になるので、設定を保存したい場合は左下にある「Save Preferences」ボタンを左クリックすると保存されます。
f:id:rikoubou:20190116153152p:plain


 以上がBlender2.8での日本語化の方法です。

 自分は基本的に英語のインターフェースのまま使っていますが、日本語化した方がわかりやすかったり、また一度日本語化することで「日本語を入力/表示できるようになる」という利点があるので、一度はやっておいた方がいいかと思います(自分も一度日本語化してまた元に戻しました)。

 また参考資料として見つけたBlender2.8の日本語化の方法と、最新版では微妙に変更があったようなので今回記事にした次第です。


・参考資料

【python/OpenCV】OpenCVで動画ファイルの読み込みと保存の方法

 今までpythonOpenCVを使ってWebカメラや静止画の加工などはやっていましたが、動画ファイルを扱ったことがなかったのでその備忘録です。(※ちなみにOpenCVでは音声は扱えません)

 OpenCVpythonの他にどうやらopencv_ffmpegを入れてないと使えないようなので、そこは各自で入れておいてください。

 では始めます。


1:動画ファイルの読み込み
 OpenCVで動画の読み込みを行うには以下の記述でできます。

import cv2

video = cv2.VideoCapture("./movie/test.mp4") # 引数がファイルパス
ret, frame = video.read() # 1フレーム読み込み

 基本的にWebカメラを扱う時と同じ感じで、引数にファイルパスを指定してあげるだけです。
 フレームごとの読み込みもread関数で行えばよいです。

 また動画の各情報も以下のようにして取得できます。

width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) # 動画の画面横幅
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 動画の画面縦幅
frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) # 総フレーム数
frame_rate = int(video.get(cv2.CAP_PROP_FPS)) # フレームレート(fps)

 この他にも取得できる情報は色々あるようなので、参考資料にあるサイト様を参照してください。


2:動画ファイルの保存
 動画の読み込み方法がわかったので次は動画ファイルとしての保存方法です。
 その方法は以下の記述でできます。

import cv2

frame_rate = 24.0 # フレームレート
size = (640, 480) # 動画の画面サイズ

fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') # ファイル形式(ここではmp4)
writer = cv2.VideoWriter('./result/outtest.mp4', fmt, frame_rate, size) # ライター作成

ret, frame = video.read()
writer.write(frame) # 画像を1フレーム分として書き込み

writer.release() # ファイルを閉じる

「cv2.VideoWriter_fourcc」関数でファイル形式を指定します。大体mp4形式しか使わないとは思います。

「cv2.VideoWriter」関数でファイルとして保存するライターを作成します。第一引数が保存ファイルパス、第二引数がファイル形式、第三引数がフレームレート、第四引数が画面サイズとなっています。

 ライターに1フレーム分として書き込む時は「write」関数の引数に画像を指定してやればOKです。


3:サンプル
 動画の読み込みと保存方法がわかったので適当に元の動画のコピーを作成するサンプルを作りました。

・movieTest.py

import cv2

print("---start---")

#動画ファイルを読み込む
video = cv2.VideoCapture("./movie/test.mp4")

# 幅と高さを取得
width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
size = (width, height)

#総フレーム数を取得
frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))

#フレームレート(1フレームの時間単位はミリ秒)の取得
frame_rate = int(video.get(cv2.CAP_PROP_FPS))

# 保存用
fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
writer = cv2.VideoWriter('./result/outtest.mp4', fmt, frame_rate, size)

for i in range(frame_count):
    ret, frame = video.read()
    ### ここに加工処理などを記述する ###
    writer.write(frame)

writer.release()
video.release()
cv2.destroyAllWindows()

print("---end---")

 以上がOpenCVで動画ファイルの読み込みと保存の方法です。

 動画のファイルを1フレームずつ読み込んだあとに、何かしらの処理をしてライターの引数に渡してやれば動画を加工することもできそうです。動画を扱って何か作ってみるのもいいかもですね。


・参考資料