OpenCVによる蚊の検知

Zoom会議で使うビデオカメラを使って、蚊の飛来を検知できないだろうか。

まずはビデオカメラから画像を取得して画面に表示してみる。cv.VideoCapture() の引数はカメラ番号で、カメラが1台であれば 0 のはずである:

#! /usr/bin/env python3

import cv2 as cv

capture = cv.VideoCapture(0)  # 0, 1, 2, ...
if not capture.isOpened():
    print("Cannot open camera")
    exit()

while (True):
    ret, frame = capture.read()
    if not ret:
        break
    cv.imshow("Frame", frame)
    if cv.waitKey(1) == ord("q"):
        break

capture.release()
cv.destroyAllWindows()

これを例えば test.py という名前で保存し、実行許可を与え(chmod 755 test.py)、実行(./test.py)する。Mac では、最初はカメラにアクセスする許可がないので失敗する。システム環境設定の「セキュリティとプライバシー」の「プライバシー」で、「変更するにはカギをクリックします」をしてから、左側「カメラ」、右側「ターミナル」のチェックを付け、ターミナル(で動くPython)からカメラがアクセスできるようにすると、実行できるようになる。ビデオ表示画面がアクティブな状態でキーボードの「Q」を押すと止まる。

このプログラムを少しずつ改良して、蚊が飛んだらアラートが出る(例えば音を鳴らし画面に位置を表示する)ようにしてみよう。

まず考えられるのは、直前の画像を保存しておき、差分をとって、それがある条件を満たせば、アラートを出すことであろう。カラーの差分は解釈が難しいので、グレースケールに直してから差分の絶対値を求める。それをさらに画素値40を境として2値化し、白(つまり40以上変化のあったピクセル)の個数が一定レベルを超えればアラートを出す。ここでは個数を画面に出力し、いわゆるビープ音を出すための制御文字 "\x07" を出力し、2値化した差分を画面に表示している:

#! /usr/bin/env python3

import cv2 as cv
import numpy as np

capture = cv.VideoCapture(0)  # 0, 1, 2, ...
if not capture.isOpened():
    print("Cannot open camera")
    exit()

prev = None
while (True):
    ret, frame = capture.read()
    if not ret:
        break
    cv.imshow("Frame", frame)
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    if prev is not None:
        delta = cv.absdiff(gray, prev)
        ret, thresh = cv.threshold(delta, 40, 255, cv.THRESH_BINARY)  # 40は要調整
        n = np.count_nonzero(thresh)
        if n > 10:  # 10は要調整
            print(n, "\x07")
            cv.imshow("Frame", thresh)
    prev = gray
    if cv.waitKey(1) == ord("q"):
        break

capture.release()
cv.destroyAllWindows()

さらに cv.imwrite("filename.png", frame) などとしてアラート時の画像を出力すると後で便利である。このようにして出力した画像例(EMEET NOVA HD1080P 1920×1080だが、蚊らしいものの写っているところあたりを256×256に切り出したもの)と、両者の差分の輪郭を緑で示したものを、次に示す:

蚊の画像 蚊の画像 両者の差分の輪郭を緑で示したもの

輪郭を出力するには、例えば次のようにする:

contours, hierarchy = cv.findContours(
    thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
cv.drawContours(frame, contours, -1, (0, 255, 0), 3)
cv.imwrite("diff.png", frame)

ちなみに、両者の写真(切り出したものではなく全体)をグレースケールにしたものの差分の絶対値の和は 4372333、差分の絶対値の平均は約 2.1、差分の絶対値が 40 以上のピクセルは 48 個であった。

改良すべきところはいろいろ考えられる。まず、直前との差分ではなく、直前のいくつかのフレームの移動平均を使う方が安定するであろう。また、蚊は小さいので大きい変化は無視するようにするべきであろう。これらは簡単に実装できる(前者は cv.accumulateWeighted() を使う)。蚊の形状を判断するのは課題である。

ビープよりましなアラート音を出すには、のところで書いたようにすればよい。ただし sd.play() で音が出ているときに再度 sd.play() を呼び出したりしているとハングするようなので、適宜間隔をあける。

Webカメラにはけっこうノイズがある。次の例は、前後のフレームに何も写っていないので、ノイズだと思われる:

ノイズ画像