【 opencv 基礎知識 3】 異常検知アプリ作成!③ -フレーム間差分の二値化、二値変化率の認識、画像保存、検知枠の設定-
このお題目は2年前に完遂していたにも関わらず、纏めないまま放置されていたので思い出しも兼ねて書きます!
以前はC++で書いていましたが、pythonで書くと短期作業で実装できるので切り替えました。
環境は次の通り。
- windows 8.1
- python3 (anaconda3-2.3.0)
- cv2
これの続き
motojapan.hateblo.jp
今回のスクリプトでできることは、画像のような感じで、
認識エリアを設定して、その範囲に動きがあればカメラのシャッターを切るといったものです。
例えば、コルクに認識エリアを設定して、(Escキーで決定)
持っていこうとすると犯行現場が撮影される!って感じです。(逮捕!!)
おさらい(カメラ起動/終了とグレースケール画像取得)
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