OpenCVで瞬き検出を行う

やりたいこと

USBカメラでリアルタイムで取得している映像から瞬きを検出するプログラムを書いてみます。

以下のサイトを見ながら実装しました。

youtu.be

 

使用するもの

  • Anaconda
  • VSCode
  • USB カメラ(内臓でも可能)

環境構築

anaconda navigatorを開き、「create」から新規環境を作成します。

pythonのバージョンは3.9.0ですることにします。

環境が完成したら、緑色の三角ボタンを押してTerminalを開きます。

Terminalで以下を打ち込んでください。

$ pip install mediapipe

$ pip install cvzone

それでは、この環境を使って書いていきましょう。

実装

新規ディレクトリを作成し、VSCodeを開きます。

ディレクトリの中で、git bashを開いてcode .と打ち込むと開くことができます。

まずは、先ほど構築した環境を使う設定をしましょう。

右下の〇.〇〇.〇〇-bitと書かれているところをクリックします。その後、画面上に今までに作成した環境リストが出てくるので、そこから先ほど作成した環境を選びます。

 

次に、新規ファイル(Python)を作成します。

※ここで、コードの中身よりまずは実装したいという方は以下をコピペして実行してみてください。

while True:

    if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    success, img = cap.read()
    img, faces = detector.findFaceMesh(img, draw=False)

    if faces:
        face = faces[0]
        for id in idList:
            cv2.circle(img,face[id], 5, color, cv2.FILLED)

        leftUp = face[159]
        leftDown = face[23]
        leftLeft = face[130]
        leftRight = face[243]

        lengthVer,_ = detector.findDistance(leftUp,leftDown)
        lengthHor,_ = detector.findDistance(leftLeft,leftRight)

        cv2.line(img,leftUp,leftDown,color2,3)
        cv2.line(img,leftLeft,leftRight,color2,3)

        ratio = int((lengthVer/lengthHor)*100)
        ratioList.append(ratio)
        if len(ratioList) > 3:
            ratioList.pop(0)
        ratioAvg = sum(ratioList) / len(ratioList)

        if ratioAvg < 29 and counter == 0: # CuddlyScope: 39, Interated Cam: 29
            blinkCounter += 1
            color = color1
            counter = 1
        if counter != 0:
            counter +=1
            if counter > 10:
                counter = 0
                color = color2
        cvzone.putTextRect(img,f'Blink Count: {blinkCounter}', (50,100), colorR=color)
        imgPlot = plotY.update(ratioAvg, color)
        img = cv2.resize(img,(640,360))
        imgStack = cvzone.stackImages([img,imgPlot], 2,1)
    else:
        img = cv2.resize(img,(640,360))
        imgStack = cvzone.stackImages([img,img], 2,1)

    cv2.imshow('Image', imgStack)
    cv2.waitKey(1)

まずは、カメラで取得した映像を画面に表示するところからしましょう。 以下をコピペしてください。


    import cv2

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)

while True:
    sccess, img = cap.read()
    img = cv2.resize(img, (640,360))
    cv2.imshow("Imgage", img)
    cv2.waitKey(1)

これで一度実行してみてください。実行方法はターミナルでpython <ファイル名>か、右上の再生ボタンをクリックすることで出来ます。 ※VideoCaptureのところに0と入植していますが、ここを変えることで外部のUSBカメラに変更することもできます。 それでは次は、mediapipeを使用して顔の各点を取得していきましょう。以下をコピペしてください。


    import cv2
import cvzone
from cvzone.FaceMeshModule import FaceMeshDetector

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
detector = FaceMeshDetector(maxFaces=1)

while True:
    if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    sccess, img = cap.read()
    img, faces = detector.findFaceMesh(img, draw=True)

    img = cv2.resize(img, (640,360))
    cv2.imshow("Imgage", img)
    cv2.waitKey(1)

これに変更して実行すると顔上の点を取得できます。 ※findFaceMeshのところでdrawをFalseにすると描画を無くせます。 では次は、目の周りの点だけを取り出します。 以下をコピペしてください。


    import cv2
import cvzone
from cvzone.FaceMeshModule import FaceMeshDetector

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
detector = FaceMeshDetector(maxFaces=1)

color1 = (255,0,255)
color2 = (0,200,0)
color = color1

idList = [22,23,24,26,110,157,158,159,160,130,243]

while True:
    if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    sccess, img = cap.read()
    img, faces = detector.findFaceMesh(img, draw=False)

    if faces:
      face = faces[0]
      for id in idList:
        cv2.circle(img,face[id], 5, color, cv2.FILLED)
        
    img = cv2.resize(img, (640,360))
    cv2.imshow("Imgage", img)
    cv2.waitKey(1)

目の周辺の各点を取得することができました。 ここから、この点間の距離を取得して瞬き検出を行っていきます。 ここで、単純に距離を取得して閾値よりも短かった場合というような処理をしてしまうと目を開けたまま顔が後ろに下がると目が閉じていると判定されてしまいます。 そのため、目の横幅と縦幅の比率を取って検出を行っていきます。 以下をコピペしてください。


    import cv2
import cvzone
from cvzone.FaceMeshModule import FaceMeshDetector

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
detector = FaceMeshDetector(maxFaces=1)

color1 = (255,0,255)
color2 = (0,200,0)
color = color1

idList = [22,23,24,26,110,157,158,159,160,130,243]

while True:
    if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    sccess, img = cap.read()
    img, faces = detector.findFaceMesh(img, draw=False)

    if faces:
      face = faces[0]
      for id in idList:
        cv2.circle(img,face[id], 5, color, cv2.FILLED)
        leftUp = face[159]
        leftDown = face[23]
        leftLeft = face[130]
        leftRight = face[243]

        lengthVer,_ = detector.findDistance(leftUp,leftDown)
        lengthHor,_ = detector.findDistance(leftLeft,leftRight)

        cv2.line(img,leftUp,leftDown,color2,3)
        cv2.line(img,leftLeft,leftRight,color2,3)
        ratio = int((lengthVer/lengthHor)*100)
        print(ratio)
    img = cv2.resize(img, (640,360))
    cv2.imshow("Imgage", img)
    cv2.waitKey(1)

printの部分で、ターミナルに縦横の比率を出力しています。瞬きをすることによって値が変化し、顔を前後に動かしても値は余り変化していないことが分かります。 それでは、以上の数値を見てだいたいの閾値を決めてカウントしていきましょう。 一番最初に示した、完成形のコードをコピペしてください。


    import cv2
import cvzone
from cvzone.FaceMeshModule import FaceMeshDetector
from cvzone.PlotModule import LivePlot

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
detector = FaceMeshDetector(maxFaces=1)
plotY = LivePlot(640,360,[20,50], invert=True)

idList = [22,23,24,26,110,157,158,159,160,130,243]
ratioList = []
blinkCounter = 0
counter = 0


color1 = (255,0,255)
color2 = (0,200,0)
color = color1

while True:

    if cap.get(cv2.CAP_PROP_POS_FRAMES) == cap.get(cv2.CAP_PROP_FRAME_COUNT):
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

    success, img = cap.read()
    img, faces = detector.findFaceMesh(img, draw=False)

    if faces:
        face = faces[0]
        for id in idList:
            cv2.circle(img,face[id], 5, color, cv2.FILLED)

        leftUp = face[159]
        leftDown = face[23]
        leftLeft = face[130]
        leftRight = face[243]

        lengthVer,_ = detector.findDistance(leftUp,leftDown)
        lengthHor,_ = detector.findDistance(leftLeft,leftRight)

        cv2.line(img,leftUp,leftDown,color2,3)
        cv2.line(img,leftLeft,leftRight,color2,3)

        ratio = int((lengthVer/lengthHor)*100)
        ratioList.append(ratio)
        if len(ratioList) > 3:
            ratioList.pop(0)
        ratioAvg = sum(ratioList) / len(ratioList)

        if ratioAvg < 29 and counter == 0: # CuddlyScope: 39, Interated Cam: 29
            blinkCounter += 1
            color = color1
            counter = 1
        if counter != 0:
            counter +=1
            if counter > 10:
                counter = 0
                color = color2
        cvzone.putTextRect(img,f'Blink Count: {blinkCounter}', (50,100), colorR=color)
        imgPlot = plotY.update(ratioAvg, color)
        img = cv2.resize(img,(640,360))
        imgStack = cvzone.stackImages([img,imgPlot], 2,1)
    else:
        img = cv2.resize(img,(640,360))
        imgStack = cvzone.stackImages([img,img], 2,1)

    cv2.imshow('Image', imgStack)
    cv2.waitKey(1)

今回はcvzone のplotmoduleというものを使用して目の縦横比率を可視化しています。 以上より完成です!