screencapに対する考察 - adbでスクリーンキャプチャを取る方法

# はじめに

Androidのscreencapに関する考察です。
今更？と突っ込みを受けそうな内容ですが……。

# 背景

Pythonを使い、Androidの画面をadbでキャプチャし、OpenCVのテンプレートマッチング機能やadbを使い、特定の場所を自動的にタップし、その作業を繰り返す、というプログラムを作っていました。

Androidの画面のキャプチャには、`adb screencap -p /sdcard/screen.png`の様なコマンドを実行し、取得していたのですが、使用している端末がZenfone 3 Maxという、貧弱なスマホのせいか、画面のキャプチャに時間が掛かっている様に感じました。

そこで高速化をする為の方法を調べてみる事にしました。

結論としては、一定の効果はあったものの、劇的な効果は無い、微妙なものが出来上がりました。
また、最新の高性能のスマートフォンを使った場合には、今回記事にした工夫は、意味がないかも知れません。

とはいえ、得たものもあったので、自分の備忘録としてメモ書きです。

# 検証環境
OS : Windows 10 Version 1809（October 2018 Update)
Python : Anaconda, Python Version 3.7.0

CPU : Corei7-7700 3.60GHz
GPU : 無し（Intel Graphics）

Android Device : ASUS Zenfone 3 Max - ZC520TL

**Linux、および、MacOSでは未検証です。検証の予定もありません。**

# adbでスクリーンキャプチャをとる方法

adbでスクリーンキャプチャを撮るには、下記のコマンドを実行します。
下記のように説明されている事が一般的な様です。

```bash:adbを使ったスクリーンキャプチャ
adb shell screencap -p /sdcard/screen.png
adb pull /sdcard/screen.png
adb shell rm /sdcard/screen.png
```

本体の`/sdcard/screen.png`に、一度キャプチャ結果を書き出して、pullコマンドでファイルをPCへ取り出し、rmコマンドでファイルを消す、という手順です。

# リダイレクトによるスクリーンキャプチャ

実は上記コマンド、下記でもOKの様です。
※Windows環境でしか確認していませんが……

**ただし、cmd.exeで実行して下さい。Powershellだと失敗します。（リダイレクトがUnicodeで出力されるから）**
**Powershellで実行する方法は、面倒なので調べませんでした。**

```bash:リダイレクトを使用
adb exec-out screencap -p > screen.png
```

exec-outオプションとは何かが、実は良く解っていません。
adbコマンドの説明を見たのですが、exec-outについて、何も書かれていないようです。

どうも、Binary Dataをそのまま出力する為のコマンドの様なのですが、それもはっきりしません。
更に言えば、**古いVersionのadbでは動作しない様です**。

この情報について、参考にしたサイトは下記の通りです。
下記サイトにこのやり方が書かれていたので、動かしてみたら動いた、という次第です。

>Note to Self: Fast Android Screen Capture
http://blog.macuyiko.com/post/2017/note-to-self-fast-android-screen-capture.html


因みに、下記はだめです。Linuxでは動作しますが、Windowsで失敗します。
原因は改行コードの取り扱いです。下記コマンドだと、改行コードの変換（LF→CR+LF）が発生するからです。

```text:Windowsでは動作しない
adb shell screencap -p > screen.png
```

改行コードを変換（CR+LF→LFに変換）すれば、リダイレクトによって、スクリーンキャプチャを撮る事ができます。

(改行コードを変換する何か)を作れば、下記コマンドで実行できる筈です。

```text:Windowsで動作させる例
adb shell screencap -p | (改行コードを変換する何か) > screen.png
```

# -pオプションをつけない

ここからが本題です。

adbの-pオプションは、png形式で出力する事を指定するオプションです。
また、-pオプションを明示的に記載しなくとも、下記の様に出力ファイルの拡張子を「png」にすれば、-pオプションを付加したのと同じことになります。

```bash:PNG形式で出力される
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png
```

では、-pオプションを付加しないと、何が出力されるのでしょうか。

```bash:-pオプションをつけない
adb exec-out screencap > result.raw
```

調べてみた所、下記のフォーマットを持った、Rawデータ（1ピクセル毎にRGBAが記載されたデータ）が出力されます。

>stackoverflow
https://stackoverflow.com/questions/22034959/what-format-does-adb-screencap-sdcard-screenshot-raw-produce-without-p-f


|場所|バイト数|説明|
|---|---|---|
|1~4byte|4byte|width|
|5~8byte|4byte|height|
|9~12byte|4byte|[pixelformat](https://developer.android.com/reference/android/graphics/PixelFormat)（今回は無視しました）|
|13byte~|-|画素。RGBA順に並んでおり、4byteで1ピクセル。本来はpixelformatに従って並んでいる筈。|



Rawデータを使うメリットは、（恐らく）スピードの速さだと思います。

私の使っているスマホは、Zenfone 3 Maxという、性能が貧弱なスマホです。
そのため、PNG形式で出力しようとすると、本体のエンコード処理で、かなり時間が掛かってしまうのでは、と考えました。

# RawデータをOpenCV形式に変換するプログラム

今回はPythonを使い、OpenCVで使用できるプログラムを作ってみました。以下がそのコードです。

動作としては、

- `adb exec-out screencap`を実行。RawDataを取得。
- Rawデータのフォーマットに従い、1～12byte目の情報を取得。高さと幅を求める。
- numpyデータに変換
- 配列の形状を変換（RGBAを要素として、高さ×幅の行列を作る）
- 要素を入れ替える（OpenCVはBGRAの順序なので）
- Alpha値を削除する

という手順です。

```python
import subprocess
import numpy
import cv2

def capture_screen_1():
    '''
    スクリーンキャプチャを取るための関数。Rawデータを処理

    Returns
    ----------
    img : opencv Mat
        OpenCV形式のイメージ

    '''
    result = []

    # adb exec-out screencap
    try:
        result = subprocess.check_output(['adb', 'exec-out', 'screencap'])
    except:
        return None

    # wigth, heightを取得。
    wigth = int.from_bytes(result[0:4], 'little')
    height = int.from_bytes(result[4:8], 'little')
    _ = int.from_bytes(result[8:12], 'little')

    # ここのCopyは必須。そうでないと、編集が出来ない
    tmp = numpy.frombuffer(result[12:], numpy.uint8, -1, 0).copy() 

    # 配列の形状変換。
    # 1つの要素がRGBAである、height * widthの行列を作る。
    img = numpy.reshape(tmp, (height, wigth, 4))    

    # 要素入れ替え。
    # RawDataはRGB、OpenCVはBGRなので、0番目の要素と、2番目の要素を入れ替える必要がある。
    b = img[:, :, 0].copy()               # ここのコピーも必須
    img[:, :, 0] = img[:, :, 2]
    img[:, :, 2] = b

    # alpha値を削除。alpha値が必要な場合は、下記行は消しても良いかも？
    img2 = numpy.delete(img, 3, 2)

    return img2

# 検証用コード
if __name__ == "__main__":
    img = capture_screen_1()
    
    # 表示させる部分。キーを押すと終了する。
    cv2.namedWindow('window')
    cv2.imshow('window', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
```

# 実行速度の検証

他に下記のプログラムを作り、実行速度を計測する事にしました。

- (先ほど説明した)Rawデータを扱うプログラム
- 'adb exec-out screencap -p`を実行し、OpenCV形式に変換するプログラム
- PNG形式のファイルを本体に保存し、pullコマンドでPNGファイルを取り出し、OpenCVに形式に変換するプログラム

実行速度の計測は、各プログラムを20回ずつ実行し、平均値を取っています。
試行回数は非常に少ないと思いますが、傾向を掴むのが目的なので、まぁ良いかな、と。

プログラムを以下に記載します。少し長いです。

```python
import subprocess
import numpy
import cv2
import time
import os

def capture_screen_1():
    '''
    スクリーンキャプチャを取るための関数。Rawデータを処理

    Returns
    ----------
    img : opencv Mat
        OpenCV形式のイメージ

    '''
    result = []

    # adb exec-out screencap
    try:
        result = subprocess.check_output(['adb', 'exec-out', 'screencap'])
    except:
        return None

    # wigth, heightを取得。
    wigth = int.from_bytes(result[0:4], 'little')
    height = int.from_bytes(result[4:8], 'little')
    _ = int.from_bytes(result[8:12], 'little')

    # ここのCopyは必須。そうでないと、編集が出来ない
    tmp = numpy.frombuffer(result[12:], numpy.uint8, -1, 0).copy() 
    
    # 配列の形状変換。
    # 1つの要素がRGBAである、height * widthの行列を作る。
    img = numpy.reshape(tmp, (height, wigth, 4))    

    # 要素入れ替え。
    # RawDataはRGB、OpenCVはBGRなので、0番目の要素と、2番目の要素を入れ替える必要がある。
    b = img[:, :, 0].copy()               # ここのコピーも必須
    img[:, :, 0] = img[:, :, 2]
    img[:, :, 2] = b

    # alpha値を削除。alpha値が必要な場合は、下記行は消しても良いかも？
    img2 = numpy.delete(img, 3, 2)

    return img2

def capture_screen_2():
    '''
    スクリーンキャプチャを取るための関数。PNG形式で取得。

    Returns
    ----------
    img : opencv Mat
        OpenCV形式のイメージ

    '''
    # adb exec-out screencap
    try:
        result = subprocess.check_output(['adb', 'exec-out', 'screencap', '-p'])
    except:
        return None

    # imdecodeで読み込み
    return cv2.imdecode(numpy.frombuffer(result, numpy.uint8), cv2.IMREAD_COLOR)


def capture_screen_3():
    '''
    スクリーンキャプチャを取るための関数。PNG形式で取得。PULLでとってくる。

    Returns
    ----------
    img : opencv Mat
        OpenCV形式のイメージ

    '''
    # adb shell screencap -p /sdcard/screen.png
    # adb pull /sdcard/screen.png result.png
    # adb shell rm /sdcard/screen.png
    try:
        subprocess.check_output(['adb', 'shell', 'screencap', '-p', '/sdcard/screen.png'])
        subprocess.check_output(['adb', 'pull', '/sdcard/screen.png', 'result.png'])
        subprocess.check_output(['adb', 'shell', 'rm', '/sdcard/screen.png'])
    except:
        return None

    # result.pngをオープン。すべて読み込む。
    with open('result.png', 'rb') as f:
        result = f.read()

    # result.pngを削除
    os.remove('result.png')

    # imdecodeで読み込み
    return cv2.imdecode(numpy.frombuffer(result, numpy.uint8), cv2.IMREAD_COLOR)


def benchmark_test(func):
    '''
    ベンチマーク
    '''
    CAP_COUNT = 20

    time_total = 0
    
    for _ in range(CAP_COUNT):
        start = time.time()
        func()
        result_time = time.time() - start
        time_total += result_time
    
    result_time = time_total / CAP_COUNT
    print ("{0} time:{1}".format(func.__name__, result_time) + "[sec]")

if __name__ == "__main__":     
    benchmark_test(capture_screen_1)
    benchmark_test(capture_screen_2)
    benchmark_test(capture_screen_3)
```

実行結果は下記の通り。検証環境は、一番最初に記した通りです。

上から、RawData、PNG(Redirect)、PNG(Pullコマンド使用)の順です。
Rawデータを扱うのが一番早く、pullコマンドを使うと遅くなるのは確かですが、その差は3倍程度です。

ある程度の効果はありましたが、そんなに高速化されているとは言えない様な気もします。微妙な結果と言ったところでしょうか。

```text:実行結果、#以降は後で追加したコメント
capture_screen_1 time:1.0686011910438538[sec] # Raw Data (exec-out)
capture_screen_2 time:2.1270569443702696[sec] # PNG (exec-out -p)
capture_screen_3 time:3.3975932598114014[sec] # PNG (exec-out -p /sdcard/screen.png; adb pull ...)
```

# 結論

今回の結果を見る限り、素直にPNGフォーマットで出力しても、そんなに影響は無い様に思います。
とはいえ、2秒くらいは改善できているので、私が作成したプログラムでは、RawDataでキャプチャする方法を採用する事にしました。

更にスクリーンキャプチャを高速化する為には、[minicap](https://github.com/openstf/minicap)など、スクリーンキャプチャを高速化する為のライブラリがある様です。
ただし、名前を調べただけで、詳しい内容までは調べておりません。

また、性能が低いスマホだからある程度の効果があったのであって、**高性能なスマホでは、そんなに速度は変わらないのでは？**と、考えています。
逆に、画素数が増える分、PNG形式の方が早いのかも知れません。

残念ながら、高性能なスマホを私は持っていません。その為、機種ごとの検証が出来ないのが残念です。
今後も高性能なスマホを買う予定はありませんので、この検証は実施せずに終わりそうです。

# 参考文献
Note to Self: Fast Android Screen Capture
http://blog.macuyiko.com/post/2017/note-to-self-fast-android-screen-capture.html

stackoverflow
https://stackoverflow.com/questions/22034959/what-format-does-adb-screencap-sdcard-screenshot-raw-produce-without-p-f

Androidの画面キャプチャを高速化するための試行錯誤
https://qiita.com/setsulla/items/14457accded130e971d2