はじめに
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でスクリーンキャプチャを撮るには、下記のコマンドを実行します。
下記のように説明されている事が一般的な様です。
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で実行する方法は、面倒なので調べませんでした。
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)が発生するからです。
adb shell screencap -p > screen.png
改行コードを変換(CR+LF→LFに変換)すれば、リダイレクトによって、スクリーンキャプチャを撮る事ができます。
(改行コードを変換する何か)を作れば、下記コマンドで実行できる筈です。
adb shell screencap -p | (改行コードを変換する何か) > screen.png
-pオプションをつけない
ここからが本題です。
adbの-pオプションは、png形式で出力する事を指定するオプションです。
また、-pオプションを明示的に記載しなくとも、下記の様に出力ファイルの拡張子を「png」にすれば、-pオプションを付加したのと同じことになります。
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png
では、-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(今回は無視しました) |
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値を削除する
という手順です。
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回ずつ実行し、平均値を取っています。
試行回数は非常に少ないと思いますが、傾向を掴むのが目的なので、まぁ良いかな、と。
プログラムを以下に記載します。少し長いです。
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倍程度です。
ある程度の効果はありましたが、そんなに高速化されているとは言えない様な気もします。微妙な結果と言ったところでしょうか。
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など、スクリーンキャプチャを高速化する為のライブラリがある様です。
ただし、名前を調べただけで、詳しい内容までは調べておりません。
また、性能が低いスマホだからある程度の効果があったのであって、**高性能なスマホでは、そんなに速度は変わらないのでは?**と、考えています。
逆に、画素数が増える分、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