Pythonで(zoom等の)講義のスクショを自動化する

こんにちは。
今回は少し技術的なことについて書こうと思います。
大学だとzoomでパワポを用いた講義があると思います。そのような場合スライドが配布されることが多いですが、まれにされないこともあり、後での復習用に毎回スライドが変わるたびにスクショする、という方もいらっしゃると思います。私もたまにそうすることがあり、今回はそれを自動化するコードを書いてみました。環境はMacとjupyter notebookです。なお、著作権等の関係でパワポを共有できないケースが往々にしてあるのは存じており、そのような状況下で不正にデータの複製を行うことを本稿は推奨するものではないということを明記しておきます。まあどんなものも使い方次第というスタンスであるということもあるのでここは読者の皆さんの良識に委ねたいと思います。

また、最後のところにコードファイルを置いておくので、これの中のセルを順次実行することでここに書いてある作業を実現できます。

では僕が実際にした手順を追っていこうと思います。少しでも多くの方の参考になれば幸いです。
まずpuautogui(色々自動化するのに便利なやつ)を入れるのに、以下のサイトからコードを頂戴してコマンドラインに貼り付けました。
https://qiita.com/hirohiro77/items/78e26a59c2e45a0fe4e3
多分必要なのはpyautoguiなのでそれだけをpipでインストール、でもいいかもしれません。

次に、デスクトップ内にディレクトリを作成します。これをすることで後でまとめて写真をpdf化することができます。
これは例えばFinderでデスクトップを選択後新規フォルダを作成、でもいいですしCUIが好きな方ならcd Desktop/ とmkdir (ディレクトリ名)と書くだけで作れます。
次に、Jupiter notebookを開き、先ほど作ったディレクトリ内に新たなコードのファイルを作成します。これで、このファイル内のコードを回した結果できる写真群がそのディレクトリ内に保存されるようになります。

では、実際に打ち込むコードの詳細について説明をします。

コードは大きく分けて4つのパーツがあります。


初めのセルには以下のコードを打ち込み、実行します。
pip install pygame
これをやることで音源データを扱いやすくなります(なぜこれを使うかは後述)

次のセルには以下のコードを打ち込み、実行します。ここでは、画面の左上、右下部分をそれぞれ選択します。ドの音が3回なった後また時間を置いて3回なるので、初めの3回がなる途中で画面の一番左上、次の3回がなる途中でマウスを画面の一番右下に移動させましょう。
#範囲選択前に3回音を再生(C)
#全体(画面の左上から右下まで全体を選択)
import pyautogui as pt
import time
import cv2
import os
import math
import numpy as np
from scipy.io import wavfile # install : conda install scipy
from pygame import mixer # pip install pygame
# パラメータ
FREQ = 261.626 # 生成するサイン波の周波数(note#60 C4 ド)
SAMPLE_RATE = 44100 # サンプリングレート
# 16bitのwavファイルを作成
wavfile.write("do.wav", SAMPLE_RATE,
(np.sin(np.arange(SAMPLE_RATE) * FREQ * np.pi * 2 / SAMPLE_RATE) * 32767.0).astype(np.int16))
# wavファイルをロードして再生
mixer.init() # mixerを初期化
mixer.music.load("do.wav") # wavをロード
time.sleep(2)
print('キャプチャ範囲の左上座標にマウスカーソルを合わせる')
for i in range(3):
mixer.music.play(1) # wavを1回再生

time.sleep(0.1)
mixer.music.stop()
time.sleep(0.9)

A=int(pt.position()[0])
B=int(pt.position()[1])

print('左上座標:' + str(pt.position()))

# 2秒待機
time.sleep(2)

# 右下座標を取得
print('キャプチャ範囲の右下座標にマウスカーソルを合わせる')
time.sleep(2)
for i in range(3):
mixer.music.play(1) # wavを1回再生

time.sleep(0.1)
mixer.music.stop()
time.sleep(0.9)
C=int(math.fabs(pt.position()[0]-A))
D=int(math.fabs(pt.position()[1]-B))

print('右下座標:' + str(pt.position()))

次のセルに以下のコードを打ち込み、実行します。ここで、実際にスクショしたい範囲(例えばzoomのパワポが占めるエリアの左上、右下)を選択します。ドの音が3回なった後また時間を置いて3回なるので、初めの3回がなる途中で画面の選択したい、つまりスクショを撮りたい長方形の領域の一番左上、次の3回がなる途中でマウスを領域の一番右下に移動させましょう。
#範囲選択前に3回音を再生(C)
#該当範囲を選択
time.sleep(2)
print('キャプチャ範囲の左上座標にマウスカーソルを合わせる')
for i in range(3):
mixer.music.play(1) # wavを1回再生

time.sleep(0.1)
mixer.music.stop()
time.sleep(0.9)

aa=int(pt.position()[0])
bb=int(pt.position()[1])

print('左上座標:' + str(pt.position()))

# 2秒待機
time.sleep(2)

# 右下座標を取得
print('キャプチャ範囲の右下座標にマウスカーソルを合わせる')
time.sleep(2)
for i in range(3):
mixer.music.play(1) # wavを1回再生

time.sleep(0.1)
mixer.music.stop()
time.sleep(0.9)
cc=int(math.fabs(pt.position()[0]-aa))
dd=int(math.fabs(pt.position()[1]-bb))

print('右下座標:' + str(pt.position()))


ここまでで領域選択を行ったのですが、少しトリッキーなことをしています。領域を選択するとき、カーソルを選びたい領域の左上、右下におきそれぞれのタイミングで位置情報を取得するのですが、jupiterの画面でコードを回してから画面をzoomのそれに切り替えると、コードの進行の様子が見えないのでいつ位置情報の取得がなされるかわかりません。そのため、ここでは音の情報を利用します。
ドレミファソラシドのドの音が3回なった後位置情報を取得する、というようにしているので、ドの音が1、2回なっている途中でカーソルを移動しておけば意図している位置が取得されるようになっています。
次のセルに以下のコードを打ち込み、実行します。

import pyautogui as pt
import time
import cv2
import os
t=pt.screenshot()
t.save('yyy.png')
im=cv2.imread('yyy.png')
X=im.shape[0]
Y=im.shape[1]
aa=int(aa*Y/C)
bb=int(bb*X/D)
cc=int(cc*Y/C)
dd=int(dd*X/D)
#所要時間(予定)の入力
min=105
s=pt.screenshot(region=(aa,bb,cc,dd))
s.save('{}.png'.format(-1))
length=min*60 #seconds
duration=7
x=int(length/duration)
yy=-1
for i in range(x):
start=time.time()
a=pt.screenshot(region=(aa,bb,cc,dd))
a1=str(i)
YY=str(yy)
a.save('{}.png'.format(a1))
img1=cv2.imread('{}.png'.format(YY))
img2=cv2.imread('{}.png'.format(a1))
z1=img1.shape[0]
z2=img1.shape[1]

judge=1
count=0
for j in range(500):
for k in range(500):
aj=(z1//500)*(j)
ak=(z2//500)*(k)
p1=img1[aj,ak][0]
q1=img2[aj,ak][0]
p2=img1[aj,ak][1]
q2=img2[aj,ak][1]
p3=img1[aj,ak][2]
q3=img2[aj,ak][2]

#相違点の集計
if(p1!=q1 or p2!=q2 or p3!=q3):
count=count+1


#頑健性を持たせる
if(count>=90):
judge=0
#print(i)
end=time.time()-start

if(judge==0): #different

yy=i
#s=a
time.sleep(duration-end)
else:
os.remove('{}.png'.format(a1))
time.sleep(duration-end)

ここでは、どこで画像を保存するかを検討しています。基本的に授業でパワポが切り替わる、もしくはアニメーションが起きるタイミングで画像を取得及び保存する、ということを目標にしています。そこで、ある時間おきに画像を取得し、それ以前の「最新の画像」と比較します。そこで、その2枚が差があると判定する場合新しい画像を取り込み、それを「最新の画像」というように更新するということを行なっています。
ただ、マウスが動いたりするだけで画像が取り込まれたは面倒なので、以下のようにしています。
画面からある一定数の点を抽出し、それぞれの点の色のTupleを新旧2画像で比較し、違うならcountします。そしてその総計がある閾値を超えるなら、2枚の画像が違うものと判定します。
こうする理由はマウスの移動を検出しないようにするため、そして全部の画像中の点について検討していてはコストがかかるのでそれを減らすといういうことがあります。ただ、閾値がある一定以上になると、細かいアニメーションの違いを検知できなくなったりするので、今のこのコードは多少マウスの移動による誤探知を許容し、できるだけ細かいアニメーションを全て拾うようにしています。
この点に関して新しいアイデアがあれば(アルゴリズムの面で改善できるところがあれば)教えていただきたいです。

これらの実行により、決まった時間・決まったスクリーンの範囲のところで変化を感知したら自動的にスクショする、ということができるようになりました。
コード(特に最後)のやつを止めたい時は、jupiter notebookの□ボタンを押せばいいです。
最後に、ディレクトリ内を覗いてみましょう。
おそらく大量の写真が入っているはずです。
名前で順序を変えるというやり方で、上から
-1.png
0.png


・という感じになるはずです(0.pngは1.pngとかのこともあります)
この後、写真を全選択し、クイックアクション→pdf化でpdfが生成されます。
あとは、音声データと写真のデータを消せば、pdfとコードファイルが残るはずです。

最後になりますが、これを回す時間は、4つ目の塊のコードの#所要時間(予定)の入力 というところを適宜変えれば使えると思います。お読みいただきありがとうございました。


よろしければサポートをよろしくお願いいたします。頂いたサポートは新しい勉強用の本の購入に使わせていただきます。