12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pythonでできるだけ精度の良いキャプチャーソフトを作ってみる(1)

Last updated at Posted at 2020-06-08

#はじめに
OS:windows10 64bit
python:3.7

録画できるソフトは世の中に山ほどありますが、
指定した時間に特定のwebサイトの画面を録画録音する、という
超個人的な需要を満たすソフトがなかったので
脳トレお遊び感覚にpythonで書いてみます。

難易度的に記事を分けて書くほどのものではないですが、
時間の都合上ちまちまとパート分けして書いていきます。

また作業していく中で発生した諸々の問題と、
その対処をたらたら書いてるのでとても冗長な内容になっていますが
ご了承ください。

#方針
全体像としては以下の流れを想定してやってみます。

1.指定した時刻でコマンド実行
2.事前にセットしたURLでブラウザ起動
3.ブラウザのスクリーンショット・音声をキャプチャ
4.ブラウザのスクリーンショット・音声をマージして動画出力

このページでは3について検討します。
pythonの有名どころのライブラリをさらっと確認した感じだと
画像と音声を同時にキャプチャできるものが見つかりませんでした。
(gitを探せばあるかもしれません。あれば教えてもらえるとたすかります。)

ffmpegを使えば全部簡単にできそうな気もしますが、
今回はpythonのコード上で実行するという無意味な縛りの元やってみます。

#音声のキャプチャ
有名どころのpyaudioを使ってみます。

##pyaudioのインストール
人によっては手が止まるかもしれないので、メモとして記載します。
pyaudioのインストール時、私の環境では下記エラーが発生しました。

src/_portaudiomodule.c(29): fatal error C1083: include
ファイルを開けません。'portaudio.h':No such file or directory

ビルドツールが原因で失敗しているようです。
https://minato86.hatenablog.com/entry/2019/04/04/005929#portaudiohNo-such-file-or-directory
上記リンクを参考にして、自分のPCに一致するwhlファイルからインストールします。

pip install PyAudio-0.2.11-cp37-cp37m-win_amd64.whl

なお、私の実行環境はpycham。pythonの仮想環境で実行しています。
上記のコマンドを実行してもCドライブに
インストールされてる大本のpythonにしかインストールされません。

D:\~(中略)~\venv\Scripts\activate.bat

コマンドプロンプトからのactivate.batをたたいて、仮想環境をアクティベートさせた後、
その状態のまま上記のコマンドを叩いてインストールさせました。

#pyaudioで実装
インストールが完了したのでさっそく録音してみます。

Python3で録音してwavファイルに書き出すプログラム
https://ai-trend.jp/programming/python/voice-record/

どんなライブラリか知らないので、とりあえずマイクも何も差さずに
こちらのサイトの録音サンプルソースをそのまま実行してみます。

OSError: [Errno -9998] Invalid number of channels

エラーで止まりました。
このエラーは録音入力デバイスが存在しないことが原因です。
「マイクもなしに何録音しようとしているの?」と怒っています。
audio.openで指定したinput_device_indexが不適切なようです。
パソコンに入っているオーディオデバイスを確認してみましょう

import pyaudio
audio = pyaudio.PyAudio()
for i in range(audio.get_device_count()):
    dev = audio.get_device_info_by_index(i)
    print('name', dev['name'])
    print('index', dev['index'])
   0 Microsoft サウンド マッパー - Output, MME (0 in, 2 out)
<  1 BenQ GL2460 (NVIDIA High Defini, MME (0 in, 2 out)
   2 Realtek Digital Output (Realtek, MME (0 in, 2 out)
   5 Realtek Digital Output (Realtek High Definition Audio), Windows
DirectSound (0 in, 2 out)
   6 Realtek Digital Output (Realtek High Definition Audio), Windows WASAPI
(0 in, 2 out)
   ...
   ...
   ...
  10 ステレオ ミキサー (Realtek HD Audio Stereo input), Windows WDM-KS (2 in, 0 out)
  12 マイク (Realtek HD Audio Mic input), Windows WDM-KS (2 in, 0 out)

沢山でてきました。
BenQやRealtek Digital Outputは出力デバイス、つまりスピーカーだからこれは違う。
入力デバイスっぽいものとしてはマイクも見当たりますが、
これだと外の声を録音することになりそうです。

パソコンの内部の音を入力としてくれそうなものはどれでしょうか。

  10 ステレオ ミキサー (Realtek HD Audio Stereo input), Windows WDM-KS (2 in, 0 out)

ステレオミキサーはパソコン内部の音を入力音声として与える機能です。
ここでは「ステレオミキサー」を選択して実行してみましょう。
(WASAPIでもできるらしいけどここでは無視)
デバイスのインデックスにステレオミキサーの番号を入れて実行してみます。

[Errno -9999] Unanticipated host error

またエラー。OS側の設定も参照している気配を感じます。
OS側の設定を確認しましょう

https://ahiru8usagi.hatenablog.com/entry/Windows10_Recording
サウンドコントロールパネル -> 録音タブ -> ステレオミキサーを有効にし、
windowsの「サウンド」 -> 「入力」をステレオミキサーに選択します。

ということで無事録音できました。(コードは次次章)

#画面のキャプチャ
ImageGrabでスクリーンショットをとってみます。

pip install Pillow

#録画と音声同時キャプチャ
ようやくコード(試作)。
一部問題のあるコードですが途中経過として張ります。

import cv2
import numpy as np
from PIL import ImageGrab
import ctypes
import time
import pyaudio
import wave

# 開始時間の保存

# parentTime = time.time()
# for i in range(10):
#     img_cv = np.asarray(ImageGrab.grab())
# current = time.time()
# diff = (current - parentTime)
# print("fps:" + str(float(10)/diff))


user32 = ctypes.windll.user32
capSize = (user32.GetSystemMetrics(0), user32.GetSystemMetrics(1))

fourcc = cv2.VideoWriter_fourcc(*"DIVX")
writer = cv2.VideoWriter("test.mov", fourcc, 30, capSize)
count = 0
FirstFlag = True

WAVE_OUTPUT_FILENAME = "test.wav"
RECORD_SECONDS = 40

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100
CHUNK = 2 ** 11
audio = pyaudio.PyAudio()
stream = audio.open(format=FORMAT,
                    channels=CHANNELS,
                    rate=RATE,
                    input=True,
                    input_device_index=0,
                    frames_per_buffer=CHUNK)

frames = []

# # 開始時間の保存
# sTime = time.time()
# count = 0

print ("start")
for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)):
    # count+=1
    # if count == 30 :
    #     current = time.time()
    #     diff = (current - sTime)
    #     print("fps:" +  str(float(count)/diff))
    #     sTime = time.time()
    #     count = 0

    # 画像キャプチャ
    img_cv = np.asarray(ImageGrab.grab())
    img = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)
    writer.write(img)

    # 音声キャプチャ
    data = stream.read(CHUNK)
    frames.append(data)
print ("finish")


writer.release()
stream.stop_stream()
stream.close()
audio.terminate()

waveFile = wave.open(WAVE_OUTPUT_FILENAME, 'wb')
waveFile.setnchannels(CHANNELS)
waveFile.setsampwidth(audio.get_sample_size(FORMAT))
waveFile.setframerate(RATE)
waveFile.writeframes(b''.join(frames))
waveFile.close()

動画(mov)と音声(wav)を別々に保存するコードです。
録画時間はRECORD_SECONDSに指定した秒数なので40秒。
さてこれを実行してみると結果としては。。。

音声ファイルが39秒
動画ファイルが28秒

音声ファイルはともかく
動画ファイルのほうがめちゃくちゃ怪しい結果になりました。

writer = cv2.VideoWriter("test.mov", fourcc, 30, capSize)

動画を保存するときの設定を適当に30と固定で設定しましたが、
この値が不適切なのだと思われます。
fpsがいくつになっているかざっくり計算しましょう。

    if count == 30 :
        current = time.time()
        diff = (current - sTime)
        print("fps:" +  str(float(count)/diff))
        sTime = time.time()
        count = 0

結果はfps14~19くらい。1秒間に14~19枚の画像が出力される計算。
中身で行われた処理として想像されるのは、、
0.06秒間隔でやってくるフレーム画像に対して、
0.03秒間隔で来てるものとして動画を出力したために、
早送りされて時間が短い動画になったと思われます。

writer = cv2.VideoWriter("test.mov", fourcc, 15, capSize)

fps15に変更して遅すぎる・早すぎるフレームは
スキップ等して書き込めば時間の問題は解決できるかもしれません。

ですがそれ以前の問題として・・・・遅い。
録画できてもカタカタになりそうです。

処理の原因はなんでしょうか。
各処理の速度を図ってみます。

#ImageGrab単体
parentTime = time.time()
for i in range(40):
    img = ImageGrab.grab()
current = time.time()
diff = (current - parentTime)
print("fps:" + str(float(40)/diff))

#ImageGrab+numpy
parentTime = time.time()
for i in range(40):
    img_cv = np.asarray(ImageGrab.grab())
current = time.time()
diff = (current - parentTime)
print("fps:" + str(float(40)/diff))

結果としては、

ImageGrab.grab()単体だと27fps、
ImageGrab.grab()からnumpyへの変換を行うと20fps。
ImageGrab.grab()からnumpyへの変換そしてRGB変換で18fps。

ImageGrab.grab()が遅いとは言いませんが後処理を考えるとすこし問題です。

各変換処理の改善をしてもいいですが、
画像をキャプチャするImageGrab.grab()よりも早いものを探してみます。

上記サイトを参考に、windowsのapiで出力してみます。

# windows_api
import win32gui, win32ui, win32con, win32api
hwin = win32gui.GetDesktopWindow()
width = 1920
height = 1080
left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
hwindc = win32gui.GetWindowDC(hwin)
srcdc = win32ui.CreateDCFromHandle(hwindc)
memdc = srcdc.CreateCompatibleDC()
bmp = win32ui.CreateBitmap()
bmp.CreateCompatibleBitmap(srcdc, 1920, 1080)
memdc.SelectObject(bmp)

parentTime = time.time()
arr = []
for i in range(30):
    memdc.BitBlt((0, 0), (width, height), srcdc, (left, top),
win32con.SRCCOPY)
    arr.append(memdc)
current = time.time()
diff = (current - parentTime)
print("fps:" + str(float(30)/diff))
fps:48.752326144998015

fps48。爆速です。
面白そうなのでこのapiを使う方針で修正します。

時間が無くなったのでまた次回

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?