LoginSignup
27
29

More than 3 years have passed since last update.

NoxPlayerをPythonで動かすための基礎

Last updated at Posted at 2020-01-31

背景

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)

と記述できる

具体例として、以下のような画面を

テンプレート
temp.png
でタップしたいときは、スクショ取って画像を、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が同じディレクトリにあることを想定して、

main.py
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()
config.ini
[DEFAULT]
NoxDirectory = D:/Program Files/Nox/bin

と書くことができる

27
29
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
29