##背景
UWSCの配布・サポートが終わったので、以前UWSCで書いたマクロをPythonで書き直した
そこで書き直した内容のうち、NoxPlayerをマクロで動かす上で重要な内容をまとめてみようと思った
内容として、Nox操作に必要なADBコマンドの送り方、プログラムとNoxで双方向的な操作、あいまい画像認識の方法など述べる
ソースコードを乗せるが、改変しながら書いたので間違ってるかも
##環境
NoxPlayer 6.6.0.0
Python 3.6
opencv-python 4.1.2.30
##ADBコマンドを送って操作する
NoxPlayerでAndroid操作を自動化するにあたって、一々マウス操作では煩わしい
そのためバックグラウンドで操作する必要があるが、そのためにはADBコマンドを使う必要がある
→Noxにはnox_adb.exeが標準で付属しているため、これを用いる
最初に、PythonでUWSCのDOSCMDに当たる関数は以下のように書ける
from subprocess import run, PIPE
def doscmd(directory, command):
completed_process = run(command, stdout=PIPE, shell=True, cwd=directory, universal_newlines=True, timeout=10)
return completed_process.stdout
このdoscmdを使ってADBコマンドを送るときは、例えばタップ操作の場合
def send_cmd_to_adb(cmd):
_dir = "D:/Program Files/Nox/bin"
return doscmd(_dir, cmd)
def tap(x, y):
_cmd = "nox_adb shell input touchscreen tap " + str(x) + " " + str(y)
send_cmd_to_adb(_cmd)
と簡潔に記述できる
その他のADBコマンドは調べれば出てくる(後にも乗せるけど)
##logcatで挙動を見る
自動化する際、logcatでアプリやAndroidの挙動を動的に見れると捗るが、そのときは
def show_log():
_cmd = "nox_adb logcat -d"
_pipe = send_cmd_to_adb(_cmd)
return _pipe
と書くとlogcatが出力される
例えばNox標準のファイルマネージャーをADBコマンドで開いて、開いたことを確認するためには
def start_app():
_cmd = "nox_adb shell am start -n com.cyanogenmod.filemanager/.activities.NavigationActivity"
_pipe = send_cmd_to_adb(_cmd)
print(_pipe)
start_app()
Starting: Intent { cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity }
でも良いが、logcatを用いて
def clear_log():
_cmd = "nox_adb logcat -c"
send_cmd_to_adb(_cmd)
def get_log():
_cmd = "nox_adb logcat -v raw -d -s ActivityManager:I | find \"com.cyanogenmod.filemanager/.activities.NavigationActivity\""
_pipe = send_cmd_to_adb(_cmd)
print(_pipe)
clear_log()
start_app()
get_log()
START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity bnds=[334,174][538,301] (has extras)} from pid 681
Start proc com.cyanogenmod.filemanager for activity com.cyanogenmod.filemanager/.activities.NavigationActivity: pid=14454 uid=10018 gids={50018, 1028, 1015, 1023}
Displayed com.cyanogenmod.filemanager/.activities.NavigationActivity: +691ms
START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity bnds=[334,174][538,301] (has extras)} from pid 681
Start proc com.cyanogenmod.filemanager for activity com.cyanogenmod.filemanager/.activities.NavigationActivity: pid=14562 uid=10018 gids={50018, 1028, 1015, 1023}
Displayed com.cyanogenmod.filemanager/.activities.NavigationActivity: +604ms
START u0 {flg=0x10000000 cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity} from pid 14665
Start proc com.cyanogenmod.filemanager for activity com.cyanogenmod.filemanager/.activities.NavigationActivity: pid=14675 uid=10018 gids={50018, 1028, 1015, 1023}
Displayed com.cyanogenmod.filemanager/.activities.NavigationActivity: +584ms
START u0 {flg=0x10000000 cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity} from pid 14771
START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.cyanogenmod.filemanager/.activities.NavigationActivity bnds=[334,174][538,301] (has extras)} from pid 681
Start proc com.cyanogenmod.filemanager for activity com.cyanogenmod.filemanager/.activities.NavigationActivity: pid=14792 uid=10018 gids={50018, 1028, 1015, 1023}
Displayed com.cyanogenmod.filemanager/.activities.NavigationActivity: +589ms
こんな感じで挙動が見れる
アプリ操作の自動化の際は、Activity遷移やGC動作(メモリ解放)のタイミングを見ることで色々できる
clear_log→具体的な動作→get_log→判別、みたいに書くと画像認識させなくても画面状態が判別できたりする
##画面上の指定した画像をタップさせる
アプリを自動化させる際に、ゲーム等では動的にオブジェクトが移動するため、プログラム上でオブジェクトを認識させなければならない
そのためそのオブジェクトを画像認識させることで、プログラム上で処理させることができる
具体的なやり方として
1.ADBコマンドで画面内のスクリーンショットを取る
2.1のスクリーンショットを元に、該当する画像(テンプレート)がないかOpenCVで画像認識させる
3.画像認識をもとに動作を決定する
といった方法が最も簡潔だった
利点としては
・ウィンドウが最小化されててもバックグラウンド動作する
・またウィンドウサイズに関わらず画像認識が機能し、さらに座標の取り方が楽
###手順1
ADBコマンドで画像内のスクリーンショットをとり、PC内部に保存する場合は
_DIR_ANDROID_CAPTURE = "/sdcard/_capture.png"
_NAME_INTERNAL_CAPTURE_FOLDER = "pics"
def capture_screen(dir_android, folder_name):
_cmd = "nox_adb shell screencap -p " + dir_android
_pipe = send_cmd_to_adb(_cmd)
_cmd = "nox_adb pull " + dir_android+ " " + folder_name
send_cmd_to_adb(_cmd)
capture_screen(_DIR_ANDROID_CAPTURE, _NAME_INTERNAL_CAPTURE_FOLDER)
で、D:/Program Files/Nox/bin/pics/_capture.pngが生成される
またテンプレートを作成する際は、この画像をもとに作ると、Noxのウィンドウサイズに関係なく作成することができる
###手順2
具体的には以下のように、画像内でテンプレートに一致する部分の中心座標を返すといった方法が考えられる
main.pyと同じディレクトリにimg/temp.pngのテンプレートがあるときは
import cv2
import numpy as np
_DIR_INTERNAL_CAPTURE = "D:/Program Files/Nox/bin/pics/_capture.png"
_DIR_TEMP = "img/temp.png"
_THRESHOLD = 0.9 #類似度
def get_center_position_from_tmp(dir_input, dir_tmp):
_input = cv2.imread(dir_input)
_temp = cv2.imread(dir_tmp)
gray = cv2.cvtColor(_input, cv2.COLOR_RGB2GRAY)
temp = cv2.cvtColor(_temp, cv2.COLOR_RGB2GRAY)
_h, _w = _temp.shape
_match = cv2.matchTemplate(_input, _temp, cv2.TM_CCOEFF_NORMED)
_loc = np.where(_match >= _THRESHOLD)
try:
_x = _loc[1][0]
_y = _loc[0][0]
return _x + _w / 2, _y + _h / 2
except IndexError as e:
return -1, -1
get_center_position_from_tmp(_DIR_INTERNAL_CAPTURE , _DIR_TEMP)
と書ける
###手順3
手順2で取得した座標をタップさせたい場合は、先程のtapメソッドとget_center_position_from_tmpメソッドより
_DIR_INTERNAL_CAPTURE = "D:/Program Files/Nox/bin/pics/_capture.png"
_DIR_TEMP = "img/temp.png"
x,y = get_center_position_from_tmp(_DIR_INTERNAL_CAPTURE, _DIR_TEMP)
tap(x, y)
と記述できる
具体例として、以下のような画面を
テンプレート
でタップしたいときは、スクショ取って画像を、GIMPとかで該当部分を切り抜いて、img/tmp.pngに置く
その後プログラム上でcapture_screenメソッド→get_center_position_from_tmpメソッド→tapメソッドの順番で実行させればいい
つまり今までのを、まとめると
from subprocess import run, PIPE
import cv2
import numpy as np
_DIR_NOX = "D:/Program Files/Nox/bin"
_DIR_ANDROID_CAPTURE = "/sdcard/_capture.png"
_NAME_INTERNAL_CAPTURE_FOLDER = "pics"
_DIR_INTERNAL_CAPTURE = "D:/Program Files/Nox/bin/pics/_capture.png"
_DIR_TEMP = "img/temp.png" #ここにブックマークの画像を入れた
_THRESHOLD = 0.9 #類似度
def main():
capture_screen(_DIR_ANDROID_CAPTURE, _NAME_INTERNAL_CAPTURE_FOLDER)
x, y = get_center_position_from_tmp(_DIR_INTERNAL_CAPTURE, _DIR_TEMP)
tap(x, y)
def capture_screen(dir_android, folder_name):
_cmd = "nox_adb shell screencap -p " + dir_android
_pipe = send_cmd_to_adb(_cmd)
_cmd = "nox_adb pull " + dir_android+ " " + folder_name
send_cmd_to_adb(_cmd)
get_center_position_from_tmp(_DIR_INTERNAL_CAPTURE , _DIR_TEMP)
def get_center_position_from_tmp(dir_input, dir_tmp):
_input = cv2.imread(dir_input)
_temp = cv2.imread(dir_tmp)
gray = cv2.cvtColor(_input, cv2.COLOR_RGB2GRAY)
temp = cv2.cvtColor(_temp, cv2.COLOR_RGB2GRAY)
_h, _w = _temp.shape
_match = cv2.matchTemplate(_input, _temp, cv2.TM_CCOEFF_NORMED)
_loc = np.where(_match >= _THRESHOLD)
try:
_x = _loc[1][0]
_y = _loc[0][0]
return _x + _w / 2, _y + _h / 2
except IndexError as e:
return -1, -1
def doscmd(directory, command):
completed_process = run(command, stdout=PIPE, shell=True, cwd=directory, universal_newlines=True, timeout=10)
return completed_process.stdout
def send_cmd_to_adb(cmd):
return doscmd(_DIR_NOX, cmd)
def tap(x, y):
_cmd = "nox_adb shell input touchscreen tap " + str(x) + " " + str(y)
send_cmd_to_adb(_cmd)
if __name__ == '__main__':
main()
##おまけ
###ログが消えないようにする
実行後にすぐログが消えるとprintした内容がわからなくなるので、ログが残るようにする必要がある
そのためには、マクロ終了時にa = input()を記述する
def main():
print("Start")
#処理部
print("Finish")
_a = input()
if __name__ == "__main__":
main()
###ADBコマンドで使うやつ
##タップ
def tap(x, y):
_cmd = "nox_adb shell input touchscreen tap " + str(x) + " " + str(y)
send_cmd_to_adb(_cmd)
##スワイプ
def swipe(x1, y1, x2, y2, seconds):
_millis = seconds * 1000
_cmd = "nox_adb shell input touchscreen swipe " + str(x1) + " " + str(y1) + " " \
+ str(x2) + " " + str(y2) + " " + str(_millis)
send_cmd_to_adb(_cmd)
##ロングタップ
def long_tap(x, y, seconds):
swipe(x, y, x, y, seconds)
##アプリ(アクティビティ)スタート
def start_app():
_cmd = "nox_adb shell am start -n com.hoge.fuga/.MyActivity"
send_cmd_to_adb(_cmd)
##アプリストップ
def start_app():
_cmd = "nox_adb shell am force-stop com.hoge.fuga"
send_cmd_to_adb(_cmd)
##ホームに戻る
def return_home():
_cmd = "nox_adb shell input keyevent KEYCODE_HOME"
send_cmd_to_adb(_cmd)
##端末の日付を変える(この方法だけ情報がまとまってなかったので一応記載)
import datetime
def set_date(delta_days):
_ANDROID_DATE_FORMAT = "%Y%m%d.%H%M%S"
_day = datetime.datetime.today() - datetime.timedelta(days=delta_days)
_days_ago = _day.strftime(_ANDROID_DATE_FORMAT)
_cmd = "nox_adb shell date -s " + date_2days_ago
send_cmd_to_adb(_cmd)
###exeファイル化する
Pyinstallerを使ってexe化すると捗る
以下のサイトを参考にするといい(書くの面倒くさい)
・PyInstallerでexeファイル化 - Qiita
Pycharmで使う場合は以下も参照、aerobiomatのExternalToolsを使う方法が正解
・Configuring Pycharm to run Pyinstaller
###設定ファイルを作る
環境によってNox本体のディレクトリが変わってしまう場合があるが、exeファイル化してしまうとプログラムを変更することができなくなってしまう
したがって予めconfig.iniファイルを作る必要がある
今回の例では、exeファイルとconfig.iniが同じディレクトリにあることを想定して、
import configparser
def main():
config_ini = configparser.ConfigParser()
config_ini.read('config.ini', encoding='utf-8')
_ADB_DIR = config_ini['DEFAULT']['NoxDirectory']
print("start macro")
#処理部
print("finish macro")
_a = input()
if __name__ == "__main__":
main()
[DEFAULT]
NoxDirectory = D:/Program Files/Nox/bin
と書くことができる