MotoJapan's Tech-Memo

技術めも

【 opencv 基礎知識 3】 異常検知アプリ作成!③ -フレーム間差分の二値化、二値変化率の認識、画像保存、検知枠の設定-

このお題目は2年前に完遂していたにも関わらず、纏めないまま放置されていたので思い出しも兼ねて書きます!

以前はC++で書いていましたが、pythonで書くと短期作業で実装できるので切り替えました。
環境は次の通り。


これの続き
motojapan.hateblo.jp


今回のスクリプトでできることは、画像のような感じで、
認識エリアを設定して、その範囲に動きがあればカメラのシャッターを切るといったものです。

例えば、コルクに認識エリアを設定して、(Escキーで決定)
f:id:motojapan:20170507013230p:plain
持っていこうとすると犯行現場が撮影される!って感じです。(逮捕!!)
f:id:motojapan:20170507013235p:plain

おさらい(カメラ起動/終了とグレースケール画像取得)

pythonで書くとこんな感じです。

def get_gray_frame(cap, size=DEFAULT_SIZE, flip=FLIP):
	res, frame = cap.read()
	if size is not None and len(size) == 2:
		frame = cv2.resize(frame, size)
	if flip is True:
		frame = frame[:,::-1]
	gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
	return gray

def start_preview(device_id = DEVICE_ID):
	#init camera device
	cap = cv2.VideoCapture(device_id)
	while True:
		start = time.time()
		# get frame
		#frame = get_frame(cap)
		frame = get_gray_frame(cap)

		# display frame
		cv2.imshow('camera preview', frame)
		if cv2.waitKey(1) == 27: # wait 1msec / finish by ESC key
			break

		elapsed_time = time.time() - start
		sys.stdout.write('elapsed_time {:3.3f} [s] \r'.format(1 / elapsed_time))
		sys.stdout.flush()

	# destroy window
	cv2.destroyAllWindows()
	#release camera device
	cap.release()

大事なところは、
1. cv2.VideoCaptureでカメラ起動、cap.release()でカメラ終了
2. cv2.VideoCaptureは引数にDEVICE_IDを設定
  これはPCにWebカメラが2つ以上あった時に、DEVICE_ID=0, DEVICE_ID=1, , , のように設定します。
3. cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)でグレースケール化

ちなみに、グレースケール化したframeの中はこのような感じ

array([[139, 139, 139, ..., 165, 165, 165],
       [139, 139, 138, ..., 164, 164, 164],
       [138, 137, 136, ..., 164, 164, 164],
       ...,
       [ 63,  63,  63, ..., 103, 102, 100],
       [ 64,  63,  63, ..., 103, 101, 100],
       [ 63,  63,  63, ..., 102, 100,  99]], dtype=uint8)

フレーム間差分の二値化

これはcv2が便利な関数(cv2.absdiff/cv2.threshold)を準備しているので是非使います。

def detector(cap, detect_rect):
	detect = False
	prev_frame = None
	crt_frame  = None
	while True:
		start = time.time()
		# get frame
		#frame = get_frame(cap)
		crt_frame = get_gray_frame(cap)

		if prev_frame is not None:
			# diff frame
			diff_frame = cv2.absdiff(crt_frame, prev_frame)
			# binary frame
			diff_b_frame = cv2.threshold(diff_frame, 50, 255, cv2.THRESH_BINARY)[1]
			cv2.imshow('processing preview', diff_b_frame)
			detect, ratio = check_detect(diff_b_frame, detect_rect)
		if detect:
			# destroy window
			cv2.destroyAllWindows()
			return crt_frame

		# display frame
		cv2.imshow('camera preview', crt_frame)
		if cv2.waitKey(250) == 27: # wait 250 msec / finish by ESC key
			break

		prev_frame = crt_frame
		elapsed_time = time.time() - start
		sys.stdout.write('elapsed_time {:3.3f} [s] \r'.format(1 / elapsed_time))
		sys.stdout.flush()

	# destroy window
	cv2.destroyAllWindows()
	return None

特に

diff_frame = cv2.absdiff(crt_frame, prev_frame)

2枚のグレースケール輝度差を出力

diff_b_frame = cv2.threshold(diff_frame, 50, 255, cv2.THRESH_BINARY)[1]

グレースケール画像を2値化 (0 or 255)

(慣れていれば別ですが)これを使わず自力で書くといろいろと考慮することが発生するので、使えるものはどんどん使う。
(arrayの引き算をするにしていも dtype=uint8であるし、0-255に納めなきゃいけないし、、)

二値変化率の認識

ここまでくればあとは何をトリガーにframeを保存するかだけです。
今回は二値の変化率が一定の閾値を超えたかどうかで判断しています。
(例えば0.2)

def check_detect(b_frame, detect_rect):
	detect_rect.modify()
	window = b_frame[detect_rect.y : detect_rect.y + detect_rect.h, detect_rect.x : detect_rect.x + detect_rect.w]
	#check change ratio of binary values
	ratio = np.mean(window) / 255
	if ratio > 0.2:
		return True, ratio
	return False, ratio

ポイントはこれ

ratio = np.mean(window) / 255

numpy.meanによる平均値を使いますが、二値化は0 or 255なので、255で割って0 ~ 1の範囲に収めます。
windowは検知エリアのフレーム情報です。

画像保存

検知エリアをつける場合はcv2.rectangleなどで検知エリアを書き込みます。
※ただし、今回はすべてグレースケールへ落とし込んでいるので、(255, 255, 255) -> (255, 255, 0) としても枠は白です。

# save image
if initial_img is not None:
	save_image('./save', '0_initial_image_with_rect.png', initial_img)
if detect_img is not None:
	save_image('./save', '1_detect_image.png', detect_img)
	cv2.rectangle(detect_img, (detect_rect.x, detect_rect.y), (detect_rect.x + detect_rect.w, detect_rect.y + detect_rect.h), (255, 255, 255), 2)
	save_image('./save', '2_detect_image_with_rect.png', detect_img)

検知枠の設定

最初に設定するものを最後に説明することになりましたが、cv2.setMouseCallbackを使ってマウス操作で検知枠を設定します。

class Rect:
	def __init__(self, x, y, w, h):
		self.x = x
		self.y = y
		self.w = w
		self.h = h

	def modify(self):
		if self.w < 0:
			self.w *= -1
			self.x -= self.w
		if self.h < 0:
			self.h *= -1
			self.y -= self.h

class Meta:
	def __init__(self, window_name, img, rect):
		self.img = img
		self.img_bk =np.copy(img)
		self.rect = rect
		self.window_name = window_name

def mouse_event(event, x, y, flags, param):
	if event == cv2.EVENT_LBUTTONDOWN:		
		param.img = np.copy(param.img_bk)
		param.rect.x = x
		param.rect.y = y
	if event == cv2.EVENT_MOUSEMOVE and flags == cv2.EVENT_FLAG_LBUTTON:
		param.img = np.copy(param.img_bk)
		param.rect.w = x - param.rect.x
		param.rect.h = y - param.rect.y
		cv2.rectangle(param.img, (param.rect.x, param.rect.y), (param.rect.x + param.rect.w, param.rect.y + param.rect.h), (255, 255, 255), 2)
		cv2.imshow(param.window_name, param.img)
	if event == cv2.EVENT_LBUTTONUP:
		param.img = np.copy(param.img_bk)
		param.rect.w = x - param.rect.x
		param.rect.h = y - param.rect.y
		cv2.rectangle(param.img, (param.rect.x, param.rect.y), (param.rect.x + param.rect.w, param.rect.y + param.rect.h), (255, 255, 255), 2)
		cv2.imshow(param.window_name, param.img)

def configure_detect_rectangle(cap):
	crt_frame = get_gray_frame(cap)
	window_name = 'configure detect rectangle'
	detect_rect = Rect(0, 0, 0, 0)
	meta = Meta(window_name, crt_frame, detect_rect)
	cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
	cv2.setMouseCallback(window_name, mouse_event, meta)
	cv2.imshow(window_name, crt_frame)
	while True:
		if cv2.waitKey(1) == 27: # wait 1msec / finish by ESC key
			break
	cv2.destroyAllWindows()
	return detect_rect, meta.img

大事なところだけ抜粋すると、この辺です。

param.img = np.copy(param.img_bk)

cv2.rectangleは第一引数のframeに直接書き込みます。
なので検知枠はドラックをしていると常に書き込まれ続けて、大量の四角形が画像に書かれてしまいます。
pythonの場合、primitive以外の型は、(=)オペレータで渡すと、shallowコピーになるので、np.copyでリフレッシュが必要。
(もっといいやり方あったら是非教えてください)

おわりに

ふぅ、終わましたね。

私の場合、昔ロードバイクのタイヤだけ盗まれるという珍事件に巻き込まれたこともあり、
2年前はロードバイクに検知枠をつけて異常があった時にメールが届くようにしていました。(写真付き)

使い方は様々でよいかと思います。(悪用はダメ)
最近だったらSlack経由で通知するとか。

ソースコードgithubにおいてあるのでこちらからどうぞ。
github.com