Python/OpenCVでLinuxの画面をキャプチャしてみる

OpenCVでWebカメラなんかの映像をキャプチャしようとすると、大抵の環境ではバックエンドとしてffmpegを使うようになっています。 このバックエンドは、コンパイル時のオプションで変えることが出来て、GStreamerを使うようにも出来るようです。

GStreamerを使うと、画面のキャプチャとかも出来るようで…ということは、リアルタイムにウィンドウの映像を取得して加工が出来る!? OBSごっことか出来てとても楽しそう。

というわけで、やってみました。

必要なものをコンパイル

Gentooを使っているとこのあたりは簡単。 gstreamerってUSEフラグを付けてコンパイルするだけで終わります。

Gentooでない場合はリポジトリからソースを落してきてよしなにコンパイルしてください。

$ sudo emerge USE=gstreamer opencv

テストでやるイメージで一行で書いていますが、実際は設定ファイルに書いておいたほうが良い気がします。

ついでに、ximagesrcというGStreamerのプラグインもインストールしておきます。 このプラグインがウィンドウのキャプチャを担当することになります。

$ sudo emerge gst-plugins-ximagesrc

対象のウィンドウIDを確認する

ウィンドウをキャプチャする前に、キャプチャしたいウィンドウのIDを確認します。 これにはxwininfoってコマンドが便利です。

$ xwininfo | grep 'Window id'
xwininfo: Window id: 0x220008b "python-opencv-screen-capture.mdx + (~) - VIM"

コマンドを実行すると固まるので、その状態でキャプチャしたいウィンドウをクリックすると上記のような出力を得られます。 この場合、0x220008bの部分が後に使うウィンドウIDになります。

ウィンドウではなく画面をまるごとキャプチャしたい場合は、ウィンドウIDとして0を使ってください。 省略すると勝手に0になるっぽいけれど、もし明示したいときには。

GStreamerだけで動作確認をしてみる

ximagesrcがちゃんとインストール出来ているかを確認するために、まずはGStreamer単体で動作確認をしてみます。

さきほど確認したウィンドウIDを使って、以下のようなコマンドを実行します。

$ WINDOW_ID='0x220008b'  # さっき確認したID
$ gst-launch-1.0 ximagesrc xid=$WINDOW_ID ! videoconvert ! autovideosink

上手くいけば、新しくウィンドウが開いてキャプチャされた映像が表示されます。

デスクトップをキャプチャした映像がウィンドウに表示されて、そのウィンドウがキャプチャされて…の合わせ鏡状態が出来る

右がコマンドを実行しているコンソールで、左がキャプチャされたウィンドウです。 分かりやすいように、スクリーン全体をキャプチャさせています。

補足: BadMatch (invalid parameter attributes)エラーについて

キャプチャを実行しているときに元のウィンドウをリサイズしてしまうと、以下のようなエラーが出て動作が停止してしまいます。

X Error of failed request:  BadMatch (invalid parameter attributes)
  Major opcode of failed request:  130 (MIT-SHM)
  Minor opcode of failed request:  4 (X_ShmGetImage)
  Serial number of failed request:  40
  Current serial number in output stream:  40

筆者はタイル型のウィンドウマネージャを使っているので、起動した瞬間にリサイズされてクラッシュして焦りました。 色々試してはみたのですが、回避の方法を見つけられませんでした。 とりあえずキャプチャ中はリサイズしない運用で。相性が悪い…。

Pythonからウィンドウをキャプチャする

PythonのOpenCVからGStreamerを扱うには以下のようにします。 コンパイル時にバックエンドを指定してあるので、cv2.VideoCapturegst-launch-1.0に渡したようなオプションを直接渡すだけ。

import cv2


WINDOW_ID = '0x220008b'  # さっき確認したID


video = cv2.VideoCapture(f'ximagesrc xid={WINDOW_ID} ! videoconvert ! appsink')

while cv2.waitKey(1) != 27:
    ok, img = video.read()
    if not ok:
        break

    cv2.imshow('test', img)

もちろん、xidに0を渡す(もしくはxidオプションを省略する)とスクリーン全体をキャプチャ出来ます。

ちなみに、フレームレートはウィンドウの更新に合わせて可変のようです。 画面が更新されない限り、readメソッドでずっとブロックされ続けます。

2020-08-13 追記

実験している中で、画像が変にズレるというか傾くというか、横幅を正しく検知出来ていないっぽい症状が出ることがありました。 以下の画像のような感じになっちゃう。

画像が白黒になっていて、垂直なはずの線が左上から右下に伸びるように傾いているキャプチャ画像

画像サイズが正しくないことが原因のようなので、videoscaleを挟んでサイズを明示してやることで解決出来ます。

video = cv2.VideoCapture(f'ximagesrc xid={WINDOW_ID} ! videoconvert ! videoscale ! video/x-raw,width={WIDTH},height={HEIGHT} ! appsink')

これで、以下のように綺麗に表示されるようになるはずです。

ちゃんとカラーになって、歪みも無い状態になったキャプチャ画像

加工したりして楽しむ

試しに画面の色を反転させる(img = 255 - imgする)遊びをしてみました。 スクリーン全体を写してあげれば白黒を繰り返すように。

スクリーン全体をキャプチャして、色を反転させて表示している。その画面がまたキャプチャに映り込んで、左上から白黒のウィンドウが繰り返すように表示されている。

たのしい! …たのしいか?


参考: