私がグラフィックボットを曞いたずき、それが䜕に倉わったのか。 ペンギヌボット

この蚘事では、最小限の劎力ず時間で幅広い日垞業務を自動化できるツヌルを䜜成した経隓を分析したす。



たえがき



ボットを䜜成しお、ロゞックず反応速床を必芁ずするいく぀かのタスクを実行する必芁がありたした。 APIに乗り蟌んでプログラムバむナリを遞択したくありたせんでした。 芖芚的な自動化を採甚するこずが決定されたした。 いく぀かのボットを芋぀けたしたが、どれも私の芁件に達しおいたせんでした。遅すぎるか、スクリプト郚分が倧幅に削枛されおいるか、ビゞュアルコンポヌネントを操䜜するための機胜が䞍十分でした。 過去にビゞュアルボットを䜿甚しお成功した経隓があるのでスクリプト郚分は遅く、倧幅に調敎されおいたすが、実装するこずにしたした。



最初に必芁な機胜



次の機胜が必芁でした





既存のアナログ



アナログはたくさんありたすが、それぞれに長所ず短所がありたす。 最も機胜的なものを怜蚎しおください。





Sikuli-䟿利な機胜の巚倧なセット、䟿利なPythonスクリプト蚀語ず構文、クロスプラットフォヌム、いく぀かの譊告、

しかし、最初のタスクには正確な反応速床が必芁であり、テッセラクトのためにほずんどの郚分を提䟛するこずはできたせんでした。

2番目の問題は、Java 1.6にバむンドするこずです。これにより、タンバリンずダンスするこずなく、たずえば1.7たたは1.8に倉換するこずができなくなりたす。

Jythonの叀いバヌゞョンもあたり満足しおいたせんが、2.5.2はそれほど時代遅れではありたせん。



そうでなければ、Sikuliは玠晎らしいツヌルです、私は皆にアドバむスしたす



AutoIt-スクリプトにBasicを䜿甚したす。



フォヌラムのKulibinsは、画面䞊の画像を怜玢するこずを可胜にしたしたが、残念ながら、この機胜は䞍必芁な簡玠化ず倚くの制限のために私の芁件を満たしおいたせんでした。

Windowsでのみ動䜜し、むンストヌルが必芁です。



他のマシンで䜿甚するにはコンパむルが必芁です; AutoItがむンストヌルされおいない堎合、スクリプトをすばやく修正する方法はありたせん。



AutoHotKey-スクリプトを蚘述するために独自の構文を䜿甚したすが、これはただ長い間研究する必芁がありたすが、「恐ろしい」ず呌ぶ方が正しいでしょう。 適切な習慣なしに真に分岐したロゞックで䜕かを行うこずは困難です。 画面䞊の画像の怜玢が切り捚おられすぎおおり、私のニヌズに合っおいたせんでした。



Linux / Unixシステム甚のポヌトがいく぀かあり、むンストヌルが必芁です



Clickermann-独自の蚀語を䜿甚しおスクリプトを蚘述したすが、これはただ調査する必芁がありたす。 簡玠化により、たずえば、同じhttpリク゚ストなどの機胜がカットされたした。



ピクセルの基本的な怜玢はありたすが、画面䞊の画像の怜玢はありたせん。



UOPilot-私に合わなかったプロセスに付随しお、クリッカヌマンのような病気がありたす。 クロスプラットフォヌムはなく、倧きなスクリプトは曞くのに䟿利ではありたせん。



たた、倚くのマクロがあり、そのほずんどは「繰り返し実行」の原則に基づいお動䜜し、ナヌザヌアクションを蚘憶および繰り返したす。



そしお、残りのものには、画面䞊のアむコンの怜玢機胜がありたせんでした。



Habréのアナログ



私は蚘事を読みたした 、著者はレヌザヌ芖力矯正の割匕を埗るためにボットを䜜りたした。 この蚘事では、曞き蟌み䞭に発生する倚くの問題に぀いお説明したす。 これらの問題のほずんどは理解しやすい単玔化のために発生し、束葉杖は新しい束葉杖で解決されたした。この蚘事を読むこずをお勧めしたす。



自分の自転車のテクノロゞヌを遞択する



最初からJava SEを䜿甚しおカヌネル自䜓を䜜成するこずが決定されたした。これは、私がJavaを最も頻繁に䜿甚するため、時間の節玄になりたす。 さらに、私はRobotクラスに粟通しおいたため、マりスずキヌボヌドのコントロヌルを簡単にシミュレヌトできたす。



スクリプトむンタヌプリタヌを远加する堎合、Pythonは非垞にシンプルで人気のあるものずしお遞択されたした。 Javaの堎合、JVM䞊で実行され、むンストヌルを必芁ずしないJython実装がありたす。 さらに、スクリプトから盎接Javaクラスおよびオブゞェクトを操䜜できるようになり、ボットのコアにあるものを制限するこずなく、スクリプトの可胜性を倧幅に拡倧したす。



その埌、圌はOpenCLを䜿甚しおGPGPUを介しお画面䞊の写真の怜玢を远加したした。JavaにはJOCLの実装がありたしたが、それに぀いおは埌で詳しく説明したす。



Swingのグラフィカルむンタヌフェむス。シンプルであるず同時に、JREですぐに䜿甚できる機胜コンポヌネントです。



最初のステップ



Javaには、キヌボヌドのキヌストロヌク、マりスの動き、およびクリックをシミュレヌトできるRobotクラスがあり、特別な問題はありたせんでした。 そこで、mouseMove + mousePress + mouseReleaseを䜿甚しおmouseClickx、yのようなメ゜ッドでいく぀かの機胜を拡匵し、これらのアクションの間にThread.sleepmsを远加し、その埌、オヌバヌロヌドにより異なる匕数を持぀いく぀かのメ゜ッドを远加したした。 単䞀のメ゜ッドず同じドラッグアンドドロップ。



public void mouseClick(int x, int y) throws AWTException { mouseClick(x, y, InputEvent.BUTTON1_MASK, mouseDelay); } public void mouseClick(int x, int y, int button_mask) throws AWTException { mouseClick(x, y, button_mask, mouseDelay); } public void mouseClick(int x, int y, int button_mask, int sleepTime) throws AWTException { bot.mouseMove(x, y); bot.mousePress(button_mask); sleep(sleepTime); bot.mouseRelease(button_mask); } public void mouseClick(MatrixPosition mp) throws AWTException { mouseClick(mp.x, mp.y); } public void mouseClick(MatrixPosition mp, int button_mask) throws AWTException { mouseClick(mp.x, mp.y, button_mask); } public void mouseClick(MatrixPosition mp, int button_mask, int sleepTime) throws AWTException { mouseClick(mp.x, mp.y, button_mask, sleepTime); } public MatrixPosition mousePos() { return new MatrixPosition(MouseInfo.getPointerInfo().getLocation()); }
      
      





keyClickメ゜ッドも同じ方法で远加されたした。



 public void keyPress(int key_mask) { bot.keyPress(key_mask); } public void keyPress(int... keys) { for (int key : keys) bot.keyPress(key); } public void keyRelease(int key_mask) { bot.keyRelease(key_mask); } public void keyRelease(int... keys) { for (int key : keys) bot.keyRelease(key); } public void keyClick(int key_mask) { bot.keyPress(key_mask); sleep(keyboardDelay); bot.keyRelease(key_mask); } public void keyClick(int... keys) { keyPress(keys); sleep(keyboardDelay); keyRelease(keys); }
      
      





これらはすべお、スクリプトでのアクションの蚘述を容易にするために必芁でした。 スクリプティングのレベルをより抜象的な「あなたはそれを必芁ずし、あなたがそれをする」に䞊げたす。



読んで 䌑みたしたか そしお、さらに進んでいきたしょう



コアの目



次のステップは最も難しく、最も時間がかかりたした。画面のスクリヌンショットを撮り、事前に準備されたアむコンを芋぀ける機胜を远加したす。



たず、画面のスクリヌンショットを撮る方法ず保存する方法は

第二に、パタヌンアむコンを怜玢する方法は

第䞉に、このパタヌンアむコンはどこで入手できたすか



スクリヌンショットを撮るのがそれほど難しくない堎合



 public void grab() throws Exception { image = robot.createScreenCapture(screenRect); }
      
      





怜玢で特定の問題が発生したこず、怜玢のパタヌンをどこで取埗できるか 䜜成したすが、どのように

最初のパタヌンを䜜成するために、PrintScreenを䜿甚しお叀き良きペむントを䜿甚し、スクリヌンショットを゚ディタヌにアップロヌドし、スクリヌンショットから小さな断片を切り取り、別の.bmpファむル圢匏で保存したした。



さお、パタヌン自䜓はそこにあり、BufferedImageのコヌドからロヌドしたした。 BufferedImageにもスクリヌンショットが䜜成されたす。怜玢アルゎリズムを䜜成する必芁がありたす。 ネットワヌク䞊で、ブルヌトフォヌスを実行するオプションに出䌚いたした-小さな画像の最初のピクセル、倧きな画像の最初のピクセルを取埗し、それらを比范したす。ピクセルが同じカラヌコヌドを持っおいる堎合、その点に関連する残りのピクセルをチェックしたす。 すべおのピクセルが䞀臎する堎合、これは目的の画像が芋぀かったこずを意味したす。 䞀臎しない堎合は、倧きな画像から次のピクセルを取埗し、アクションを繰り返したす。



それはあたり良く聞こえたせんが、うたくいきたす。

 for (int y = 0; y < screenshot.getHeight() - fragment.getHeight(); y++) { __columnscan: for (int x = 0; x < screenshot.getWidth() - fragment.getWidth(); x++) { if (screenshot.getRGB(x, y) != fragment.getRGB(0, 0)) continue; for (int yy = 0; yy < fragment.getHeight(); yy++) { for (int xx = 0; xx < fragment.getWidth(); xx++) { if (screenshot.getRGB(x + xx, y + yy) != fragment.getRGB(xx, yy)) continue __columnscan; } } System.out.println(“found!”); } }
      
      





始めお...うたくいきたす 䞀臎するものが1぀芋぀かりたした しかし、かなりゆっくりであり、私たちにはたったく受け入れられたせん。 getRGBメ゜ッドが毎回呌び出され、プロセッサヌのキャッシュが非垞に非効率的に䜿甚され、私たちにずっおこれは玔粋なマトリックス怜玢ず蚀えたす。 ピクセルマトリックス そのため、スクリヌンショットを栌玍するBufferedImageオブゞェクトをint [] []マトリックスに倉換するこずにしたした。そのため、目的のフラグメントをint [] []マトリックスに倉換し、マトリックスを凊理するサむクルを修正したす。 開始しお...芋぀かりたせん。



怜玢゚ンゞンで回答を積極的に怜玢した結果、すべおの理由がBufferedImageデヌタが栌玍されるARGB / RGBA / RGB圢匏であるこずが明らかになりたした。 スクリヌンショットには、BGRフラグメントを含むARGBファむルがありたした。



画像



すべおを1぀の圢匏、぀たりARGBスクリヌンショット圢匏に瞮小しなければなりたせんでした。フラグメントをスクリヌンショット圢匏にするのは、毎回スクリヌンショット圢匏にするよりも高速です。 小さいものは倧きなフォヌマットに぀ながりたすが、これにはかなりの時間がかかりたしたが、最終的には動䜜し、パタヌンはスクリヌンショット䞊ではるかに速く、ほが2倍速く配眮され始めたした

 // USED FOR BMP/PNG BUFFERED_IMAGE private int[][] loadFromFile(BufferedImage image) { final byte[] pixels = ((DataBufferByte) image.getData().getDataBuffer()) .getData(); final int width = image.getWidth(); if (rgbData == null) rgbData = new int[image.getHeight()][width]; for (int pixel = 0, row = 0; pixel < pixels.length; row++) for (int col = 0; col < width; col++, pixel += 3) rgbData[row][col] = -16777216 + ((int) pixels[pixel] & 0xFF) + (((int) pixels[pixel + 1] & 0xFF) << 8) + (((int) pixels[pixel + 2] & 0xFF) << 16); // 255 // alpha, r // gb; return rgbData; }
      
      







さらに、条件のマトリックス行のキャッシュなどの小さな最適化により、怜玢結果の速床も向䞊したした。



 public MatrixPosition findIn(Frag b, int x_start, int y_start, int x_stop, int y_stop) { // precalculate all frequently used data final int[][] small = this.rgbData; final int[][] big = b.rgbData; final int small_height = small.length; final int small_width = small[0].length; final int small_height_minus_1 = small_height - 1; final int small_width_minus_1 = small_width - 1; final int first_pixel = small[0][0]; final int last_pixel = small[small_height_minus_1][small_width_minus_1]; int[] row_cache_big = null; int[] row_cache_big2 = null; int[] row_cache_small = null; for (int y = y_start; y < y_stop; y++) { row_cache_big = big[y]; __columnscan: for (int x = x_start; x < x_stop; x++) { if (row_cache_big[x] != first_pixel || big[y + small_height_minus_1][x + small_width_minus_1] != last_pixel) // if (row_cache_big[x] != first_pixel) continue __columnscan; // No first match // There is a match for the first element in small // Check if all the elements in small matches those in big for (int yy = 0; yy < small_height; yy++) { row_cache_big2 = big[y + yy]; row_cache_small = small[yy]; for (int xx = 0; xx < small_width; xx++) { // If there is at least one difference, there is no // match if (row_cache_big2[x + xx] != row_cache_small[xx]) { continue __columnscan; } } } // If arrived here, then the small matches a region of big return new MatrixPosition(x, y); } } return null; }
      
      





long vs intのマトリックスのタむプを詊しおみたしたが、最良の結果は、i7 4790の64/32ビットJVM構成の䞡方でint [] []マトリックスを䜿甚したたたでした。



ボットブレヌン



぀たり、スクリプト郚分は䟿利である必芁があり、構文はそれ以䞊説明するこずなく明確です。 理想的には、ボットカヌネルに埋め蟌むこずができ、ドキュメントが豊富な䞀般的な蚀語が適しおいたす。 カヌネルAPIの䜿甚は簡単で芚えやすいものでなければなりたせん。



遞択肢はPythonにあり、人気があり、孊習しやすく、よく文曞化されおおり、倚くの既補のラむブラリがあり、最も重芁なのは、スクリプトはテキスト゚ディタヌで簡単に線集できるこずです さらに、私はずっずそれを研究したいず思っおいたした。



Javaには、Jythonず呌ばれる組み蟌みPython実装がありたす。 JVM䞊で実行され、開始するのに特別なものは必芁ありたせん。文字通りすべおのクラス、Javaラむブラリ、および.jarパックを䜿甚できたす。 順番に、これは正しい遞択に察する自信を匷めるだけです。



Jythonをプロゞェクトに接続し、むンタヌプリタヌオブゞェクトを䜜成しお、スクリプトファむルを実行したす。



 class JythonVM { private boolean isJythonVMLoaded = false; private Object jythonLoad = new Object(); private PythonInterpreter pi = null; public JythonVM() { // TODO Auto-generated constructor stub } void load() { System.out.println("CORE: Loading JythonVM..."); pi = new PythonInterpreter(); isJythonVMLoaded = true; System.out.println("CORE: JythonVM loaded."); synchronized (jythonLoad) { jythonLoad.notify(); } } void run(String script) throws Exception { System.out.println("CODE: Waiting for JythonVM to load"); if (!isJythonVMLoaded) synchronized (jythonLoad) { jythonLoad.wait(); } System.out.println("CORE: Running " + script + "...\n\n"); pi.execfile(script); System.out.println("CORE: Script execution finished."); } }
      
      





スクリプトを芋おください



 # -*- coding: utf-8 -*- print("hello")
      
      





スクリプトから、必芁なカヌネルクラスをロヌドし、単玔にオブゞェクトを䜜成したす。 したがっお、クラスメ゜ッドをプルできたす。぀たり、カヌネルAPIを䜿甚しお必芁なアクションを実行できたす。



これらのクラスはActionおよびMatrixPositionでしたが、埌でタむプFragmentNotLoadedExceptionおよびScreenNotGrabbedExceptionの䟋倖クラスが远加されたした



アクション-カヌネル機胜にアクセスするためのメむンクラスずしお䜿甚されたす。 問題を解決するために必芁な䜙分な行の数を枛らすために、スクリプトを蚘述するプロセスを単玔化するように蚭蚈された䟿利なメ゜ッドが含たれおいたす。 同じmouseClick、keyClick、スクリヌンショット内のフラグメントの怜玢、スクリヌンショット自䜓の䜜成など。



さらに、このクラスの倚くのオブゞェクトを䜜成し、それに応じお䞀床に耇数のスレッドで独立しお䜿甚できたす



カヌネルAPIを䜿甚するために、数行でスクリプトを補完したしょう



 # -*- coding: utf-8 -*- from bot.penguee import Action a = Action() #      API  print("hello") #    ,     a.mouseMove(1000, 500) #       x 1000, y 500
      
      





MatrixPosition-画面䞊の座暙のラッパヌずしお䜿甚されたす。 この圢匏では、ボットAPIは座暙を返したす。 もちろん、すでに必芁な機胜を備えた既補のPointクラスを思い出したす。 ただし、すべおがそれほど単玔ではないため、XおよびYフィヌルドにはpos.getX pos.getYメ゜ッドを介しおのみアクセスできたす。これにより、スクリプト䜜成䞭に倚くの䞍䟿が生じたす。 pos.x pos.yを介しおフィヌルドにアクセスする方がはるかに䟿利です。 さらに、緎習では、䜍眮にも独自の名前が必芁であるこずが瀺されおいたす。これは、䜍眮を䞊べ替える画面の数字をアルファベット順に凊理するなどのタスクに必芁であるこずが刀明したした。



add、subメ゜ッドを䜿甚しお可胜性を広げ、珟圚のオブゞェクトの座暙を基準にした新しい䜍眮を䜜成できたす。



 # -*- coding: utf-8 -*- from bot.penguee import MatrixPosition, Action a = Action() print("hello") mp = MatrixPosition(1000, 500) print(mp.x, mp.y) a.mouseMove(mp)
      
      





その埌の改善



パタヌン座暙キャッシュ



怜玢の統蚈から、画面䞊の䜍眮を倉曎しない静的な画像をほずんど芋぀ける必芁があるこずが明らかになりたした。 このため、座暙付きのキャッシュが远加されたした。 画像がキャッシュにある堎合、たずキャッシュされた座暙に画像が存圚するかどうかがチェックされ、存圚しない堎合は画面の残りの郚分が怜玢されたす。 この小さな詳现により、スクリプトの実行速床が倧幅に向䞊したした。



サヌビス䞭のGPGPU



私は垞に、倧画面でパタヌンを芋぀けるプロセスを速くしたかったのですが、アルゎリズムの最適化には限界がありたす。 これに先立ち、怜玢プロセス党䜓がプロセッサ䞊で行われ、それを別々のスレッドに分割しおも、実際の速床は向䞊したせんが、負荷の問題が䞀桁増加したす。 GPGPUのカヌネルコヌドを蚘述した経隓があるため、OpenCLラむブラリをロヌドし、プロセッサヌで䜿甚されたものず同じ怜玢アルゎリズムを投げたしたビデオカヌドの最適な゜リュヌションではありたせん。



比范のために、画面解像床が1920 * 1080のIntel i7 4790では、プロセッサ怜玢は最悪の堎合画面の最も遠い隅で0-12msを費やし、Intel HD 4600では0-2msが安定しおいたす。 ただし、スクリヌンショットのマトリックスをビデオカヌドのメモリにロヌドする必芁があり、時間がかかるため、スクリヌンショット自䜓を䜜成するより長いプロセスで支払う必芁がありたす。 同時に、これは同じスクリヌンショットで倚くの異なる写真を怜玢できるずいう事実によっお補われ、最終的にプロセッサ怜玢の前にパフォヌマンスが向䞊したす。



スレッドセヌフ



ストリヌムを䜿甚し、フラグメントを盞互に独立しお怜玢できるようにするこずが特に重芁です。これにより、スクリプトを蚘述するずきにバグが発生しないように、バッファ、オブゞェクトがロヌカルになりたす。



クロスプラットフォヌム



スクリプトはどのプラットフォヌムでも同じように機胜したすが、䟋倖はコン゜ヌルに出力されるテキスト、オペレヌティングシステムごずに異なる゚ンコヌディングのみであるため、これは別の問題です。 JVMを䜿甚するず、むンストヌルする必芁なく、任意のプラットフォヌムでボットを実行できたす。 「Took and running。」



最終結果



スクリプトから呌び出すこずができるカヌネルメ゜ッドの数は70個以䞊あり、その数は垞に増加しおいたす。



このツヌルは、゜フトりェアおよびグラフィカルむンタヌフェむスのテストに適しおいたす。 䜕かを自動化する必芁がある堎合に圹立ちたすが、バむナリを遞択するための欲求や十分な知識がありたせんenikeesに適しおいたす。



実際の䜿甚䟋



明らかな理由から、名前や゜ヌスコヌドは教えたせん。





GUIボットのレビュヌ





解析する簡単なスクリプトを曞く





YouTubeプレむリスト





ビデオからのスクリプト
 # -*- coding: utf-8 -*- from bot.penguee import MatrixPosition, Action from java.awt.event import InputEvent, KeyEvent a = Action() p1 = MatrixPosition(630, 230) p2 = MatrixPosition(1230, 780) while True: a.grab(p1, p2) a.searchRect(630, 230, 1230, 780) #  if a.find("verstak.gui"): a.searchRect(760, 320, 960, 500) # emptyCells = a.findAllPos("cell_empty") a.searchRect(700, 520, 1220, 770) #  if a.findClick("coal.item"): coalRecentPos = a.recentPos() print(coalRecentPos.name) for i in range(len(emptyCells)): a.mouseClick(emptyCells[i], InputEvent.BUTTON3_MASK) a.sleep(50) a.mouseClick(coalRecentPos) a.searchRect(630, 230, 1230, 780) #  result = a.findPos("verstak.arrow").relative(70, 0) a.keyPress(KeyEvent.VK_SHIFT) a.sleep(100) a.mouseClick(result) a.sleep(100) a.keyRelease(KeyEvent.VK_SHIFT) elif a.find("pech.gui"): if a.find("pech.off"): a.searchRect(700, 520, 1220, 770) #  if a.findClick("coal.block"): coalBlockRecentPos = a.recentPos() a.searchRect(630, 230, 1230, 780) #  a.mouseClick(a.findPos("pech.off"), InputEvent.BUTTON3_MASK) a.mouseClick(coalBlockRecentPos) result = a.findPos("verstak.arrow").relative(70, 0) a.keyPress(KeyEvent.VK_SHIFT) a.sleep(100) a.mouseClick(result) a.sleep(100) a.keyRelease(KeyEvent.VK_SHIFT) if a.find("pech.empty"): a.searchRect(700, 520, 1220, 770) #  if a.findClick("gold.ore"): a.searchRect(630, 230, 1230, 780) #  a.mouseClick(a.findPos("pech.empty")) a.sleep(6000)
      
      









Githubリンク

APIリファレンス



UPD02/28/2019プレむリストが远加され、リンクが曎新されたした

将来のバヌゞョンでは

フラグメント圢匏は、䞋䜍互換性を備えお.bmpから.pngに倉換されたした。



859095など、類䌌の完党ではない䞀臎の怜玢が远加されたした...



透明性のあるフラグメントの怜玢が远加されたした。フラグメントはどのような圢状でもかたいたせん



クむックスタヌトを高速化するPython拡匵スクリプトを远加したした



All Articles