Pythonでウィンドウのリアルタイム描画と特徴検出

# はじめに
今回はこういうものを作ります。

<iframe width="500" height="400" src="https://www.youtube.com/embed/WfCbJppJh8I" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


YouTubeをブラウザで再生し，その画面に検知処理を行っています。
ウィンドウを動かしたりリサイズしたりしても検知画面が追従します。
こういったデスクトップのリアルタイム画像処理は情報がなかったので今回Pythonで実装してみました。

# アルゴリズム
以下の流れで実装しています。

```
While True:
1.画面領域の認識
2.スクリーンショットを撮影
3.画像検知処理
4.検知画像の表示
```

# 

# 1.画面領域の認識
検知したい画面のウィンドウを指定します。これには以下の処理が含まれます。

```
・ウィンドウタイトルの取得
・winAPIでウィンドウ情報の取得
```
どちらもWinAPIを叩かないといけないですが，学習コストの割にうまみが少ないので手ごろなライブラリで処理したいです。
一つ目は[ahk](https://pypi.org/project/ahk/)というAutoHotKeyのラッパーで簡単に処理できます。

```GetTitle.py
#pip install ahk
from ahk import AHK

window_title = "YouTube"

def GetTitle(window_title):
    ahk = AHK()
    wins = list(ahk.windows())
    titles = [win.title for win in wins]
    for t in titles:
        text = t.decode("shift-jis", errors="ignore")
        if window_title in text:
            return text

```
キーワード入力に対し部分一致でタイトルを取得できます。
ahkは`ahk.windows()`でid一覧を取得でき，ウィンドウタイトルの取得も`.title`で済むのでかなり簡単にウィンドウタイトルを取得できます。取得したテキストはバイナリ列ですのでデコードして使用します。ただし処理速度は遅めなので今回はループ外で一度実行するだけにします。
ウィンドウタイトルを取得したらWinAPIを叩いてウィンドウの位置とサイズを取得します。こちらの処理もahkで取得できますが速さを求めるために今回は使用しません。

```GetWindowRectFromName.py
import ctypes
from ctypes.wintypes import HWND, DWORD, RECT

TargetWindowTitle = GetTitle(window_title)

def GetWindowRectFromName(TargetWindowTitle):
    TargetWindowHandle = ctypes.windll.user32.FindWindowW(0, TargetWindowTitle)
    Rectangle = ctypes.wintypes.RECT()
    ctypes.windll.user32.GetWindowRect(
        TargetWindowHandle, ctypes.pointer(Rectangle))
    return (Rectangle.left, Rectangle.top, Rectangle.right, Rectangle.bottom)

```

# 2.スクリーンショットの撮影
スクリーンショットの撮影方法はいくつかありますが今回は[mss](https://pypi.org/project/mss/)というライブラリを使用します。公式で超早いと謳っているだけにPillowでスクリーンショットを撮影するよりも早いと実感できるくらいには早い。

```SCT.py
#pip install mss
import mss

bbox = GetWindowRectFromName(TargetWindowTitle)

def SCT(bbox):
    with mss.mss() as sct:
        img = sct.grab(bbox)
    return img
```

# 3.画像検知処理
ここは相当の自由度がありますので古典的な物体検出から深層学習までお好きな処理を実装するのが良いです。今回はなんでもよかったですが[OpenCVのCanShift？](https://code-graffiti.com/opencv-meanshift-camshift-tracking-in-python/)を実行してみました。精度は...冒頭を見てみてください。

```face_detection.py
#pip install opencv-python
#curl https://github.com/opencv/opencv/blob/master/data/haarcascades/haarcascade_frontalface_default.xml > haarcascade_frontalface_default.xml
import cv2

img = SCT(bbox)

def FaceDetection(img):
    face_cascade = cv2.CascadeClassifier(
        'haarcascade_frontalface_default.xml')
    face_rects = face_cascade.detectMultiScale(img)
    (face_x, face_y, w, h) = tuple(face_rects[0])
    track_window = (face_x, face_y, w, h)

    roi = img[face_y:face_y+h, face_x:face_x+w]
    hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
    roi_hist = cv2.calcHist([hsv_roi], [0], None, [180], [0, 180])
    cv2.normalize(roi_hist, roi_hist, 0, 255, cv2.NORM_MINMAX)
    term_crit = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    dst = cv2.calcBackProject([hsv], [0], roi_hist, [0, 180], 1)
    rect, track_window = cv2.meanShift(dst, track_window, term_crit)

    return face_rects, track_window
```

# 4.検知画像の表示
OpenCVで矩形画像を作成しています。パラメーターはなんとなくで調整してください。

```DrawRect.py
face_rects, track_window = FaceDetection(img)
x, y, w, h = track_window
img2 = cv2.rectangle(img, pt1=(x, y), pt2=(x+w, y+h), color=(0, 0, 255), thickness=5)
img_show("window", img2, position=(
                bbox[0]*2-bbox[2], bbox[1]-30), size=(bbox[2]-bbox[0], bbox[3]-bbox[1]))
```

# まとめ
以下のコードをYouTubeを開いた状態で実行してみるといい感じの画面であればうまくいきます。いい感じかどうかはタイトルの文字コードに依存しますので変なタイトルのyoutubeだと全く動きません。英語が望ましいです。

```main.py
def main(window_title="YouTube"):

    TargetWindowTitle = GetTitle(window_title)
    
    While True:
        try:
            bbox = GetWindowRectFromName(TargetWindowTitle)

            img = SCT(bbox)

            face_rects, track_window = FaceDetection(img)

            x, y, w, h = track_window
            img2 = cv2.rectangle(img, pt1=(x, y), pt2=(x+w, y+h), color=(0, 0, 255), thickness=5)
            img_show("window", img2, position=(
                        bbox[0]*2-bbox[2], bbox[1]-30), size=(bbox[2]-bbox[0], bbox[3]-bbox[1]))

            # escape sequence
            # ESC to escape
            k = cv2.waitKey(1) & 0xFF
            if k == 27:         # wait for ESC key to exit
                cv2.destroyAllWindows()
                return
            # or topleft mouse to escape
            if AHK().mouse_position == (0, 0):
                cv2.destroyAllWindows()
                return
        except:
            continue

```

OpenCVはすぐにエラー落ちするので雑にtry処理で括っています。また画像を閉じようとするとプロセスがすぐ死にますのでESCキーまたはマウスを左上にもっていくことで終了処理としています。

# おわりに
YouTubeを垂れ流して機械学習とかできたら最高ですよね。でも画像処理といえばGPUは欠かせないのでその辺を解決しないといけません。そのうち挑戦してみたいと思います。