ニューラルネットワークが必要ですか?

または、ニューラルネットワークなしで畳み込みニューラルネットワークを使用して画像認識を行った方法についての物語。 面白い? それから猫をお願いします。



背景



ある夏の夜、Dota 2をプレイして、ゲーム内のキャラクターを認識し、カウンターキャラクターの最も成功した選択に関する統計情報を提供するのがいいと思いました。 最初に考えたのは、何らかの方法で一致からデータを取得し、すぐに処理する必要があることです。 しかし、ゲームをハッキングした経験がないため、このベンチャーを拒否しました。 それから、ゲーム中にスクリーンショットを撮ってすぐに処理し、選択したキャラクターに関するデータを受け取ることができると決めました。



準備する



それでは始めましょう。 まず、スクリーンショットを撮ります。



スクリーンショット
画像



画像





誰もが画像の外観に似ており、画像からハッシュを取得して検索するだけです。



うん
画像



画像



「ああ、ガベ! まあ、そのような苦痛のために。」「いいえ、機能しません。 Gabeだけが、キャラクター画像が非常に曲がっている理由を知っています(彼だけでなく、画像はその場でサイズ変更(ts)され、サイズは小数で与えられると仮定しています)。 そのため、別の方法で画像を認識する必要があります。 現在、ニューラルネットワークが流行しています。 それで、それらを固定してみましょう。 畳み込みニューラルネットワークを使用します。 必要なのは:



  1. 畳み込みコア。
  2. ハールの兆候。
  3. 画像のセットをテストします。 1000は文字ごとに別の...
  4. ...


ちょっと待って、ああああああああああ!



画像



私はそのような基盤を蓄積するのに十分な人生を持っていません。 認識速度を犠牲にすることなく、どうにかして巨大なトレーニングサンプルなしでやらなければならないと思った。 Wikipediaの SNA(Convolutional Neural Network)に関する包括的な記事があります。 SNAの動作原理を簡単に説明すると、次のとおりです。 入力は、グレースケールのピクセル輝度のマトリックスです。 畳み込みコアを乗算すると、値が合計されて正規化されます。 畳み込みコアは通常-1と1であり、それぞれ黒と白の色、または1回転あたりを示します。



画像






Haarの兆候など、一般的なコアパターンがあります。 さらに記事では、Haarの兆候のいくつかを正確に使用します。 次に、畳み込みの値はニューロンの入力リンクを通過し、リンクの重みと乗算されて合計され、最終的に結果の値がニューロンの活性化関数に送られます。 しかし、これは純粋な比較、つまり畳み込みの違いです。 最初の畳み込みとキャラクターの畳み込みの差をとるだけで、差が小さければ小さいほど、画像は似ています。 これは最も単純なアーキテクチャです。 抽象パラメーターは必要ないため、隠されたレイヤーはありません。 上記のすべてを試してみましょう。



コードを書く



元のキャラクター画像のサイズは78x53ピクセルです。 グレースケール。 つまり、マトリックスのサイズは78x53で、値は0〜255です。サイズ10x10のカーネルを使用します。 カーネルのステップは、カーネルのサイズ-10です。xとyの両方。 (多くのパラメーターは必要ありません。結局のところ、画像は互いにそれほど違いはありません)。 文字ごとに合計48個の値。 それでは、コードに取りかかりましょう。 Haarの兆候に応じた番号でカーネルを初期化する必要があります。 次の標識に従ってください。



画像






畳み込みコアを初期化するConvolutionCoreクラスを作成しましょう。



ConvolutionCoreクラス
package com.kuldiegor.recognize; /** * Created by aeterneus on 17.03.2017. */ public class ConvolutionCore { public int unitMin; // -1 public int unitMax; // 1 public int matrix[][]; public ConvolutionCore(int width,int height,int haar){ matrix = new int[height][width]; unitMax=0; unitMin=0; switch (haar){ case 0:{ // -1 =   // 1 =   /* -1 -1 -1 1 1 1 -1 -1 -1 1 1 1 -1 -1 -1 1 1 1 */ for (int y=0;y<height;y++){ for (int x=0;x<(width/2);x++){ matrix[y][x]=-1; unitMin++; } for (int x=width/2;x<width;x++){ matrix[y][x]=1; unitMax++; } } break; } case 1:{ // -1 =   // 1 =   /* 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 */ for (int y=0;y<(height/2);y++){ for (int x=0;x<width;x++){ matrix[y][x]=1; unitMax++; } } for (int y=(height/2);y<height;y++){ for (int x=0;x<width;x++){ matrix[y][x]=-1; unitMin++; } } break; } case 2:{ // -1 =   // 1 =   /* 1 1 -1 -1 -1 1 1 1 1 -1 -1 -1 1 1 1 1 -1 -1 -1 1 1 */ for (int y=0;y<height;y++){ for (int x=0;x<(width/3);x++){ matrix[y][x]=1; unitMax++; } for (int x=(width/3);x<(width*2/3);x++){ matrix[y][x]=-1; unitMin++; } for (int x=(width*2/3);x<width;x++){ matrix[y][x]=1; unitMax++; } } break; } case 3:{ // -1 =   // 1 =   /* 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 */ for (int y=0;y<(height/3);y++){ for (int x=0;x<width;x++){ matrix[y][x]=1; unitMax++; } } for (int y=(height/3);y<(height*2/3);y++){ for (int x=0;x<width;x++){ matrix[y][x]=-1; unitMin++; } } for (int y=(height*2/3);y<height;y++){ for (int x=0;x<width;x++){ matrix[y][x]=1; unitMax++; } } break; } case 4:{ // -1 =   // 1 =   /* 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 1 1 -1 -1 -1 1 1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 */ for (int y=0;y<(height/3);y++){ for (int x=0;x<width;x++){ matrix[y][x]=1; unitMax++; } } for (int y=(height/3);y<(height*2/3);y++){ for (int x=0;x<(width/3);x++){ matrix[y][x]=1; unitMax++; } for (int x=(width/3);x<(width*2/3);x++){ matrix[y][x]=-1; unitMin++; } for (int x=(width*2/3);x<width;x++){ matrix[y][x]=1; unitMax++; } } for (int y=(height*2/3);y<height;y++){ for (int x=0;x<width;x++){ matrix[y][x]=1; unitMax++; } } break; } case 5:{ // -1 =   // 1 =   /* 1 1 1 -1 -1 -1 1 1 1 -1 -1 -1 -1 -1 -1 1 1 1 -1 -1 -1 1 1 1 */ for (int y=0;y<(height/2);y++){ for (int x=0;x<(width/2);x++){ matrix[y][x]=1; unitMax++; } for (int x=width/2;x<width;x++){ matrix[y][x]=-1; unitMin++; } } for (int y=(height/2);y<height;y++){ for (int x=0;x<(width/2);x++){ matrix[y][x]=-1; unitMin++; } for (int x=width/2;x<width;x++){ matrix[y][x]=1; unitMax++; } } break; } } } }
      
      







カーネルのサイズに関係なく、動的なスタッフィングと初期化を行いました。 それでは、畳み込みクラスを作成しましょう。その中で畳み込みを行います。



畳み込みクラス
 package com.kuldiegor.recognize; import java.awt.image.BufferedImage; import java.util.ArrayList; /** * Created by aeterneus on 17.03.2017. */ public class Convolution { static ConvolutionCore convolutionCores[]; //  static { //    convolutionCores = new ConvolutionCore[6]; for (int i=0;i<6;i++){ convolutionCores[i] = new ConvolutionCore(10,10,i); } } private int matrixx[][]; //   0 .. 255 public Convolution(BufferedImage image){ matrixx = getReadyMatrix(image); } private int[][] getReadyMatrix(BufferedImage bufferedImage){ //     int width = bufferedImage.getWidth(); int heigth = bufferedImage.getHeight(); int[] lineData = new int[width * heigth]; bufferedImage.getRaster().getPixels(0, 0, width, heigth, lineData); int[][] res = new int[heigth][width]; int shift = 0; for (int row = 0; row < heigth; ++row) { System.arraycopy(lineData, shift, res[row], 0, width); shift += width; } return res; } private double[] ColapseMatrix(int[][] matrix,ConvolutionCore convolutionCore){ //     int cmh=convolutionCore.matrix.length; //   int cmw=convolutionCore.matrix[0].length; //   int mh=matrix.length; //   int mw=matrix[0].length; //   int addWidth = cmw - (mw%cmw); //      ,      int addHeight = cmh - (mh%cmh); int nmatrix[][]=new int[mh+addHeight][mw+addWidth]; for (int row = 0; row < mh; row++) { System.arraycopy(matrix[row], 0, nmatrix[row], 0, mw); } int nw = nmatrix[0].length/cmw; int nh = nmatrix.length/cmh; double result[] = new double[nh*nw]; int dmin = -convolutionCore.unitMin*255; //   int dm = convolutionCore.unitMax*255-dmin; int q=0; for (int ny=0;ny<nh;ny++){ for (int nx=0;nx<nw;nx++){ int sum=0; for (int y=0;y<cmh;y++){ for (int x=0;x<cmw;x++){ sum += nmatrix[ny*cmh+y][nx*cmw+x]*convolutionCore.matrix[y][x]; } } result[q++]=((double)sum-dmin)/dm; } } return result; } public ArrayList<double[]> getConvolutionMatrix(){ //       ArrayList<double[]> result = new ArrayList<>(); for (int i=0;i<convolutionCores.length;i++){ result.add(ColapseMatrix(matrixx,convolutionCores[i])); } return result; } }
      
      







クラスで、静的ブロックで、コンボリューションカーネルをロードします。これらは定数であり、変化しないため、多くのコンボリューションを行い、カーネルを一度初期化し、これを行いません。 使用可能な画像のコンストラクターで値のマトリックスを取得します。これはアルゴリズムを高速化するために必要です。画像から毎回1ピクセルを取得しないように、グレーのグラデーションマトリックスをすぐに作成します。 入力のColapseMatrixメソッドには、画像行列と畳み込みコアがあります。 最初に、カーネルがマトリックスと一致しない場合、ゼロがマトリックスの最後に追加されます。 次に、マトリックスのコアを調べて、畳み込みをカウントします。 すべてを配列に保存します。 また、文字の畳み込みを保存する必要があります。 したがって、ヒーロークラスを作成し、次のフィールドを追加します。



  1. バンドルの配列。
  2. キャラクター名;


ヒーロークラス
 package com.kuldiegor.recognize; import java.util.ArrayList; /** * Created by aeterneus on 17.03.2017. */ public class Hero { public String name; public ArrayList<double[]> convolutions; public Hero(String Name){ name = Name; } }
      
      







また、畳み込みサンプルをロードして、文字を比較し、DefaultHeroクラスを作成する必要があります。



クラスDefaultHero
 package com.kuldiegor.recognize; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; /** * Created by aeterneus on 17.03.2017. */ public class DefaultHero { public ArrayList<Hero> heroes; public String path; public DefaultHero(String path,int tload){ heroes = new ArrayList<>(); this.path = path; switch (tload){ case 0:{ LoadFromFolder(path); break; } case 1:{ LoadFromFile(path); } } } private void LoadFromFolder(String path){ //        File folder = new File(path); File[] folderEntries = folder.listFiles(); for (File entry : folderEntries) { if (!entry.isDirectory()) { BufferedImage image = null; try { image = ImageIO.read(entry); } catch (IOException e) { e.printStackTrace(); } Hero hero = new Hero(StringTool.parse(entry.getName(),"",".png")); hero.convolutions = new Convolution(image).getConvolutionMatrix(); heroes.add(hero); } } } private void LoadFromFile(String name){ //     try { BufferedReader bufferedReader = new BufferedReader(new FileReader(name)); String str; while ((str = bufferedReader.readLine())!= null){ Hero hero =new Hero(StringTool.parse(str,"",":")); String s = StringTool.parse(str,":",""); String mas[] = s.split(";"); int n=mas.length/48; hero.convolutions = new ArrayList<>(n); for (int c=0;c<n;c++) { double dmas[] = new double[48]; for (int i = 0; i < 48; i++) { dmas[i] = Double.parseDouble(mas[i+c*48]); } hero.convolutions.add(dmas); } heroes.add(hero); } } catch (IOException e){ e.printStackTrace(); } } public void SaveToFile(String name){ //    Collections.sort(heroes, Comparator.comparing(o -> o.name)); FileWriter fileWriter = null; try { fileWriter = new FileWriter(name); } catch (IOException e){ e.printStackTrace(); } for (int i=0;i<heroes.size();i++){ Hero hero = heroes.get(i); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(hero.name).append(":"); for (int i2=0;i2<hero.convolutions.size();i2++){ double matrix[] = hero.convolutions.get(i2); for (int i3=0;i3<matrix.length;i3++){ stringBuilder.append(matrix[i3]).append(";"); } } try { fileWriter.write(stringBuilder.append("\r\n").toString()); } catch (IOException e){ e.printStackTrace(); } } try { fileWriter.close(); } catch (IOException e){ e.printStackTrace(); } } public String getSearhHeroName(Hero hero){ //     for (int i=0;i<heroes.size();i++){ if (equalsHero(hero,heroes.get(i))){ return heroes.get(i).name; } } return "0"; } public boolean equalsHero(Hero hero1,Hero hero2){ // 2  int min=0; int max=0; for (int i=0;i<hero1.convolutions.size();i++){ double average=0; for (int i1=0;i1<hero1.convolutions.get(i).length;i1++){ // 2    average += Math.abs(hero1.convolutions.get(i)[i1]-hero2.convolutions.get(i)[i1]); } average /=hero1.convolutions.get(0).length; if (average<0.02){ //            min++; } else { max++; } } return (min>=max); } }
      
      







ここではすべてが非常に簡単です。すべての畳み込みを調べて、ファイルに保存します。 同様に、ファイルからの読み込み。 カタログからのダウンロードは、文字画像のデータベースを蓄積するときに必要になる画像からの畳み込みを考慮するという点で異なります。



さて、比較自体。 コアあたりのコンボリューションの算術平均差を考慮し、0.02未満の場合、画像はほぼ同じであると仮定します。「画像が98%似ている場合、それらは同じと見なします」 次に、少なくとも半分、またはそれ以上の記号が肯定的な結果を示した場合、文字が等しいことを示します。



次に、スクリーンショットを撮ります。



スクリーンショットのコード
 try { //  image = new Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); } catch (AWTException e) { e.printStackTrace(); }
      
      







そのため、キャラクターの画像を見ないたびに、10個の画像の境界がすぐに決定されます。 次に、スクリーンショットから10個の画像をコピーし、グレースケールに変換します。 結論として、画像の畳み込みを取得します。 これを10枚の画像で行います。 そして、既存のキャラクターと比較してください。



HRecognizeクラス
 package com.kuldiegor.recognize; import java.awt.image.BufferedImage; import java.util.ArrayList; /** * Created by aeterneus on 17.03.2017. */ public class HRecognize { private DefaultHero defaultHero; public String heroes[]; //    public HRecognize(BufferedImage screen, DefaultHero defaultHero){ heroes = new String[10]; ArrayList<Hero> heroArrayList = new ArrayList<>(); this.defaultHero = defaultHero; for (int i=0;i<5;i++){ //       BufferedImage bufferedImage = new BufferedImage(78,53,BufferedImage.TYPE_BYTE_GRAY); //    bufferedImage.getGraphics().drawImage(screen.getSubimage(43+i*96,6,78,53),0,0,null); Hero hero = new Hero(""); //  hero.convolutions = new Convolution(bufferedImage).getConvolutionMatrix(); heroArrayList.add(hero); } for (int i=0;i<5;i++) { //       BufferedImage bufferedImage = new BufferedImage(78, 53, BufferedImage.TYPE_BYTE_GRAY); //    bufferedImage.getGraphics().drawImage(screen.getSubimage(777 + i * 96, 6, 78, 53), 0, 0, null); Hero hero = new Hero(""); //  hero.convolutions = new Convolution(bufferedImage).getConvolutionMatrix(); heroArrayList.add(hero); } for (int i=0;i<10;i++){ //     heroes[i] = defaultHero.getSearhHeroName(heroArrayList.get(i)); } } }
      
      







次に、画像のデータベースを蓄積する必要があります。 dotabuffで写真を撮ろうとしましたが、dota(vskih)とはまったく異なる画像がありました。 したがって、半自動モードでそれらを収集することが決定されました。 畳み込みウィザードのコードをわずかに書き直して、「スクリーンショットを撮る」ボタンを追加しました。 畳み込みの比較は、カタログからサンプルをロードするたびに行われ、サンプルがない場合は、カタログに保存します。



GitHubの畳み込みウィザード



行こう! ロビーでDotAを起動し、113文字すべてを順番に選択します。



ロビーのスクリーンショット
画像



ベースが蓄積されました。 次に、各キャラクターに名前を付ける必要があります。



キャラクターカタログのスクリーンショット
画像



そして、すべての畳み込みをファイルに保存します。 これで、アプリケーションをテストできます



認識スクリーンショット
画像



実用的な認識エラーはありません。 ゲームが開始し、黒い背景の周りでアプリケーションがそれに反応してShadow Fiendキャラクターを与えると、キャラクターが現れてもエラーはありません。 毎回ゲームを最小化しないように、認識されたキャラクターに関するデータをネットワーク経由でAndroidアプリケーションに送信することは残っています。



ここではすべてが簡単です。



ブロードキャスト受け入れコード
 try { DatagramSocket socket = new DatagramSocket(6001); byte buffer[] = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, 1024); InetAddress localIP= InetAddress.getLocalHost(); while (!Thread.currentThread().isInterrupted()) { //   socket.receive(packet); String s=new String(packet.getData(),0,packet.getLength()); if (StringTool.parse(s,"",":").equals("BroadCastFastDefinition")){ String str = "OK:"+localIP.getHostAddress(); byte buf[] = str.getBytes(); DatagramPacket p = new DatagramPacket(buf,buf.length,packet.getAddress(),6001); //     socket.send(p); } } }catch (Exception e) { e.printStackTrace(); }
      
      







データ提出コード
 try { //  Socket client = socket.accept(); final String ipclient = client.getInetAddress().getHostAddress(); Platform.runLater(new Runnable() { @Override public void run() { label1.setText(""); label1.setTextFill(Color.GREEN); textfield2.setText(ipclient); } }); DataOutputStream streamWriter = new DataOutputStream(client.getOutputStream()); BufferedImage image = null; while (client.isConnected()){ try { //  image = new Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); } catch (AWTException e) { e.printStackTrace(); } //  HRecognize hRecognize = new HRecognize(image,defaultHero); StringBuilder stringBuilder = new StringBuilder(); for (int i=0;i<5;i++){ stringBuilder.append(hRecognize.heroes[i]).append(";"); } stringBuilder.append(":"); for (int i=5;i<10;i++){ stringBuilder.append(hRecognize.heroes[i]).append(";"); } stringBuilder.append("\n"); String str = stringBuilder.toString(); //    streamWriter.writeUTF(str); try { Thread.sleep(300); } catch (InterruptedException e){ threadSocket.interrupt(); } } }catch (IOException e){ }
      
      







ブロードキャスト要求を受け入れます。 私たちはそれに答え、さらにリクエストであなたのIPを送信しますが、これは必要ではありませんが、そうさせてください。 その後、彼らは私たちとのtcp接続を開き、300ミリ秒ごとにデータの送信を開始します。 接続が切断されるとすぐに、文字の認識を停止します。 これは、プロセッサの負荷を軽減するために行いました。 ゲームが既に開始されている場合、クライアントの接続を切断するだけで、プロセッサはロードされなくなります。



Androidアプリ。 これは非常に単純なアプリケーションであるため、ここではコードを提供しません。その仕組みを簡単に説明します。 そして、GitHubへのリンクを提供します。



アプリケーションは、ネットワークを介してブロードキャスト要求を送信し(特定のサーバーIPアドレスを入力できます)、アプリケーションのサーバー部分を決定します。 いずれかのサーバーから応答が届くとすぐに、サーバーに接続してデータを受信し始めます。 カウンターピークを決定するために、dotabuff.comを選択しました。 各キャラクターのリストを見て、「強い」と「弱い」のデータを引き出し、リンクリストを作成しています。 次に、送信されたキャラクターを取得し、選択した5人より弱いすべてのキャラクターをリストします。 途中で、5つのうちの1つに「対抗」する文字がリストにないこと、つまり、リスト内の文字が選択した5つの文字に対して絶対に弱いことを確認します。



未解決のタスク



1280x1024の解像度でのみ動作します。他の解像度では試していません。



おわりに



認識は非常に高速で、Intel Celeron 1.5 GHzプロセッサを搭載したLenovo B570eラップトップにプロセッサが負担をかけることはありません。 1秒間に3回の認識サイクルで、6%をロードします。 トピックが興味深い場合は、HealthBarで番号を認識した方法(e)、どのような困難に遭遇し、どのように解決したかを説明します。



最後まで読んでくれてありがとう。



PSすべては、コンボリューションウィザードと認識プログラム自体を使用したgithub、およびコンボリューションを含む画像のアーカイブへのリンクです。



コンボリューションウィザードへのGitHubリンク

GitHub-アプリケーション自体へのリンク

AndroidアプリケーションへのGitHubリンク

画像を含むアーカイブへのリンク

畳み込みファイルへのリンク



All Articles