Zoom画面をキャプチャー

Zoom画面を録画することはできるが、スライドを共有する形の発表ではスライドごとに画像として保存して、あとでPDFにまとめるほうが楽である。そこで、Zoom画面を毎秒キャプチャし、差分がほぼ 0 の場合は捨てて、異なるキャプチャだけを保存するコードをPythonで書くことにした。

ChatGPT 4oに聞いたところ、pygetwindowでウィンドウを取得し、pyautoguiでスクリーンショットを撮る方法を教えてくれたが、どちらもうまく働かない。さらに聞いたらpyobjcのQuartzモジュールが使えそうなことがわかった。一方、スクリーンキャプチャはPILでもできるようだが、結局はmacOSのscreencaptureコマンドを呼び出している。それなら自前でやったほうが良さそうだ。

ということで、まず pip install pyobjc して、次のようにすることにした。これでマルチスクリーンにも対応するはずである。

Zoomのミーティングウィンドウは "Zoom Meeting" という名前であるが、同じ名前のウィンドウが二つできる。一方が全員の画面、もう一方が発表者の共有画面である。どうやって区別すればいいか、わからなかったが、たまたまうちでは全員の画面は縦長、共有画面は横長にしているので、幅と高さを比べて調べることにした。

ウィンドウの位置とサイズがわかれば、macOSの screencapture コマンドで画面キャプチャし、PNGファイルに保存する。これを開いて、.convert("L") でグレイスケールに変換する。これだけでもいいが、少しずれる可能性も考えて、Gaussian blur をかけている。

この画像を一つ前と比べて、十分違っていなければ削除する。これを毎秒繰り返す。

違っているかの判断は、差分のノルムで判断する。最初ChatGPTが教えてくれたコードは np.array() でNumPy配列に変換して差分をとるものであったが、なぜかうまくいかない。よく考えたら、画像の要素は uint8 なので、例えば 99 - 100 は -1 ではなく 255 になる。これではうまくないので、int16 に変換してから差分をとることにした。

#! /usr/bin/env python3

import os
import time
import subprocess
import numpy as np
from PIL import Image, ImageFilter
import Quartz # pip install pyobjc

blur_radius = 10

num = 0
img_prev = np.array([])
while True:
    while os.path.exists(f"zoom{num:04}.png"):
        num += 1
    file = f"zoom{num:04}.png"
    window_list = Quartz.CGWindowListCopyWindowInfo(
        Quartz.kCGWindowListOptionOnScreenOnly,
        Quartz.kCGNullWindowID
    )
    captured = False
    for window in window_list:
        if window.get("kCGWindowName", "") == "Zoom Meeting":
            bounds = window.get("kCGWindowBounds", {})
            x = int(bounds.get("X", 0))
            y = int(bounds.get("Y", 0))
            w = int(bounds.get("Width", 0))
            h = int(bounds.get("Height", 0))
            if w > h:
                # print(f"screencapture -x -R {x},{y},{w},{h} {file}")
                subprocess.call(["screencapture", "-x", "-R", f"{x},{y},{w},{h}", file])
                captured = True
                break
    if not captured:
        break
    img = np.int16(
        Image.open(file).convert("L").filter(ImageFilter.GaussianBlur(blur_radius))
    )
    if img.size == img_prev.size:
        diff = np.linalg.norm(img - img_prev) / (255 * np.sqrt(img.size))
        # print(f"{num:04} {diff:.3f} {'' if diff < 0.01 else '*'}")
        if diff < 0.01:
            os.unlink(file)
    img_prev = img
    time.sleep(1)