From 343c984d5ecaed2ea99b54bbc506a4a0e4c084e1 Mon Sep 17 00:00:00 2001 From: davpapp Date: Thu, 22 Feb 2018 09:28:21 -0500 Subject: [PATCH] Object detection is now fully functional. --- src/ColorAnalyzer.java | 4 +- src/IronMiner.java | 72 ++++++++++------ src/ObjectDetector.java | 124 +++++----------------------- src/WillowChopper.java | 8 +- src/main.java | 6 +- target/classes/ColorAnalyzer.class | Bin 6117 -> 6117 bytes target/classes/IronMiner.class | Bin 2899 -> 4326 bytes target/classes/ObjectDetector.class | Bin 5321 -> 5241 bytes target/classes/WillowChopper.class | Bin 1594 -> 1278 bytes target/classes/main.class | Bin 673 -> 657 bytes 10 files changed, 78 insertions(+), 136 deletions(-) diff --git a/src/ColorAnalyzer.java b/src/ColorAnalyzer.java index 3205281..f0b5c90 100644 --- a/src/ColorAnalyzer.java +++ b/src/ColorAnalyzer.java @@ -124,7 +124,7 @@ public class ColorAnalyzer { { ColorAnalyzer colorAnalyzer = new ColorAnalyzer(); //colorAnalyzer.showColorDistribution("screenshot21.jpg"); - //colorAnalyzer.printCursorColor(); - colorAnalyzer.colorImage(); + colorAnalyzer.printCursorColor(); + //colorAnalyzer.colorImage(); } } diff --git a/src/IronMiner.java b/src/IronMiner.java index be4be6f..3054cef 100644 --- a/src/IronMiner.java +++ b/src/IronMiner.java @@ -1,75 +1,99 @@ import java.awt.AWTException; import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.util.ArrayList; +import javax.imageio.ImageIO; + public class IronMiner { - public static final int IRON_ORE_MINING_TIME_MILLISECONDS = 650; - public static final int MAXIMUM_DISTANCE_TO_WALK_TO_IRON_ORE = 650; - public static final Point GAME_WINDOW_CENTER = new Point(200, 300); + public static final int IRON_ORE_MINING_TIME_MILLISECONDS = 2738; + public static final int MAXIMUM_DISTANCE_TO_WALK_TO_IRON_ORE = 400; + public static final Point GAME_WINDOW_CENTER = new Point(510 / 2, 330 / 2); Cursor cursor; CursorTask cursorTask; Inventory inventory; + ObjectDetector objectDetector; + Robot robot; + Randomizer randomizer; public IronMiner() throws AWTException, IOException { cursor = new Cursor(); cursorTask = new CursorTask(); inventory = new Inventory(); + objectDetector = new ObjectDetector(); + robot = new Robot(); + randomizer = new Randomizer(); } public void run() throws Exception { while (true) { - Thread.sleep(250); + //Thread.sleep(250); - mineClosestIronOre(); + String filename = "/home/dpapp/Desktop/RunescapeAI/temp/screenshot.jpg"; + BufferedImage image = captureScreenshotGameWindow(); + ImageIO.write(image, "jpg", new File(filename)); + mineClosestIronOre(filename); - inventory.update(); // TODO: add iron ore to inventory items - if (inventory.isInventoryFull()) { - System.out.println("Inventory is full! Dropping..."); - cursorTask.optimizedDropAllItemsInInventory(cursor, inventory); - } + dropInventoryIfFull(); } } - private void mineClosestIronOre() throws Exception { - Point ironOreLocation = getClosestIronOre(); - if (ironOreLocation == null) { + private void dropInventoryIfFull() throws Exception { + inventory.update(); // TODO: add iron ore to inventory items + if (inventory.isInventoryFull()) { + cursorTask.optimizedDropAllItemsInInventory(cursor, inventory); + } + } + + private void mineClosestIronOre(String filename) throws Exception { + Point ironOreLocation = getClosestIronOre(filename); + /*if (ironOreLocation == null) { Thread.sleep(1000); + }*/ + if (ironOreLocation != null) { + System.out.println("Mineable iron at (" + (ironOreLocation.x + 103) + "," + (ironOreLocation.y + 85) + ")"); + Point actualIronOreLocation = new Point(ironOreLocation.x + 103, ironOreLocation.y + 85); + cursor.moveAndLeftClickAtCoordinatesWithRandomness(actualIronOreLocation, 12, 12); + Thread.sleep(randomizer.nextGaussianWithinRange(IRON_ORE_MINING_TIME_MILLISECONDS - 350, IRON_ORE_MINING_TIME_MILLISECONDS + 150)); } - cursor.moveAndLeftClickAtCoordinatesWithRandomness(ironOreLocation, 20, 20); - Thread.sleep(IRON_ORE_MINING_TIME_MILLISECONDS); + } - private Point getClosestIronOre() { - ArrayList ironOreLocations = getIronOreLocations(); + private Point getClosestIronOre(String filename) throws IOException { + ArrayList ironOreLocations = objectDetector.getIronOreLocationsFromImage(filename); + System.out.println(ironOreLocations.size()); int closestDistanceToIronOreFromCharacter = Integer.MAX_VALUE; Point closestIronOreToCharacter = null; for (Point ironOreLocation : ironOreLocations) { int distanceToIronOreFromCharacter = getDistanceBetweenPoints(GAME_WINDOW_CENTER, ironOreLocation); if (distanceToIronOreFromCharacter < closestDistanceToIronOreFromCharacter) { closestDistanceToIronOreFromCharacter = distanceToIronOreFromCharacter; - closestIronOreToCharacter = ironOreLocation; + closestIronOreToCharacter = new Point(ironOreLocation.x, ironOreLocation.y); } } - if (closestDistanceToIronOreFromCharacter < MAXIMUM_DISTANCE_TO_WALK_TO_IRON_ORE) { + if (closestIronOreToCharacter != null && closestDistanceToIronOreFromCharacter < MAXIMUM_DISTANCE_TO_WALK_TO_IRON_ORE) { return closestIronOreToCharacter; } return null; } - private ArrayList getIronOreLocations() { - // TODO: Use trained DNN here - return new ArrayList(); - } public int getDistanceBetweenPoints(Point startingPoint, Point goalPoint) { return (int) (Math.hypot(goalPoint.x - startingPoint.x, goalPoint.y - startingPoint.y)); } - + private BufferedImage captureScreenshotGameWindow() throws IOException { + Rectangle area = new Rectangle(103, 85, 510, 330); + return robot.createScreenCapture(area); + + } } diff --git a/src/ObjectDetector.java b/src/ObjectDetector.java index c55c6a7..95d0240 100644 --- a/src/ObjectDetector.java +++ b/src/ObjectDetector.java @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ +import java.awt.Point; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.File; @@ -22,6 +23,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; @@ -41,10 +43,11 @@ public class ObjectDetector { model = SavedModelBundle.load("/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/results/checkpoint_23826/saved_model/", "serve"); } - public void getIronOreLocationsFromImage(BufferedImage image) throws IOException { + public ArrayList getIronOreLocationsFromImage(String filename) throws IOException { List> outputs = null; + ArrayList ironOreLocations = new ArrayList(); - try (Tensor input = makeImageTensor(image)) { + try (Tensor input = makeImageTensor(filename)) { outputs = model .session() @@ -71,117 +74,27 @@ public class ObjectDetector { // Print all objects whose score is at least 0.5. boolean foundSomething = false; for (int i = 0; i < scores.length; ++i) { - if (scores[i] < 0.5) { + if (scores[i] < 0.75) { continue; } foundSomething = true; - System.out.printf("\tFound %-20s (score: %.4f)\n", "ironOre", 1.0000); //labels[(int) classes[i]], scores[i]); - System.out.println("Location:"); - System.out.println("X:" + 510 * boxes[i][1] + ", Y:" + 330 * boxes[i][0] + ", width:" + 510 * boxes[i][3] + ", height:" + 330 * boxes[i][2]); + //System.out.printf("\tFound %-20s (score: %.4f)\n", "ironOre", scores[i]); + //System.out.println("X:" + 510 * boxes[i][1] + ", Y:" + 330 * boxes[i][0] + ", width:" + 510 * boxes[i][3] + ", height:" + 330 * boxes[i][2]); + ironOreLocations.add(getCenterPointFromBox(boxes[i])); } if (!foundSomething) { System.out.println("No objects detected with a high enough score."); } } + return ironOreLocations; } - /*public void test() { - try (SavedModelBundle model = SavedModelBundle.load("/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/results/checkpoint_23826/saved_model/", "serve")) { - // printSignature(model); + private Point getCenterPointFromBox(float[] box) { + int x = (int) (510 * (box[1] + box[3]) / 2); + int y = (int) (330 * (box[0] + box[2]) / 2); + return new Point(x, y); + } - final String filename = "/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/test_images/ironOre_test_9.jpg"; - List> outputs = null; - - try (Tensor input = makeImageTensor(filename)) { - System.out.println("Image was converted to tensor."); - long startTime = System.currentTimeMillis(); - outputs = - model - .session() - .runner() - .feed("image_tensor", input) - .fetch("detection_scores") - .fetch("detection_classes") - .fetch("detection_boxes") - .run(); - System.out.println("Object detection took " + (System.currentTimeMillis() - startTime)); - } - - try (Tensor scoresT = outputs.get(0).expect(Float.class); - Tensor classesT = outputs.get(1).expect(Float.class); - Tensor boxesT = outputs.get(2).expect(Float.class)) { - // All these tensors have: - // - 1 as the first dimension - // - maxObjects as the second dimension - // While boxesT will have 4 as the third dimension (2 sets of (x, y) coordinates). - // This can be verified by looking at scoresT.shape() etc. - int maxObjects = (int) scoresT.shape()[1]; - float[] scores = scoresT.copyTo(new float[1][maxObjects])[0]; - float[] classes = classesT.copyTo(new float[1][maxObjects])[0]; - float[][] boxes = boxesT.copyTo(new float[1][maxObjects][4])[0]; - // Print all objects whose score is at least 0.5. - System.out.printf("* %s\n", filename); - boolean foundSomething = false; - for (int i = 0; i < scores.length; ++i) { - if (scores[i] < 0.5) { - continue; - } - foundSomething = true; - System.out.printf("\tFound %-20s (score: %.4f)\n", "ironOre", 1.0000); //labels[(int) classes[i]], scores[i]); - System.out.println("Location:"); - System.out.println("X:" + 510 * boxes[i][1] + ", Y:" + 330 * boxes[i][0] + ", width:" + 510 * boxes[i][3] + ", height:" + 330 * boxes[i][2]); - } - if (!foundSomething) { - System.out.println("No objects detected with a high enough score."); - } - } - - } - }*/ - - private static void printSignature(SavedModelBundle model) throws Exception { - /*MetaGraphDef m = MetaGraphDef.parseFrom(model.metaGraphDef()); - SignatureDef sig = m.getSignatureDefOrThrow("serving_default"); - int numInputs = sig.getInputsCount(); - int i = 1; - System.out.println("MODEL SIGNATURE"); - System.out.println("Inputs:"); - for (Map.Entry entry : sig.getInputsMap().entrySet()) { - TensorInfo t = entry.getValue(); - System.out.printf( - "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n", - i++, numInputs, entry.getKey(), t.getName(), t.getDtype()); - } - int numOutputs = sig.getOutputsCount(); - i = 1; - System.out.println("Outputs:"); - for (Map.Entry entry : sig.getOutputsMap().entrySet()) { - TensorInfo t = entry.getValue(); - System.out.printf( - "%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n", - i++, numOutputs, entry.getKey(), t.getName(), t.getDtype()); - }*/ - System.out.println("-----------------------------------------------"); - } - - /*private static String[] loadLabels(String filename) throws Exception { - String text = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8); - StringIntLabelMap.Builder builder = StringIntLabelMap.newBuilder(); - TextFormat.merge(text, builder); - StringIntLabelMap proto = builder.build(); - int maxId = 0; - for (StringIntLabelMapItem item : proto.getItemList()) { - if (item.getId() > maxId) { - maxId = item.getId(); - } - } - String[] ret = new String[maxId + 1]; - for (StringIntLabelMapItem item : proto.getItemList()) { - ret[item.getId()] = item.getDisplayName(); - } - String[] label = {"ironOre"}; - return label; - }*/ private static void bgr2rgb(byte[] data) { for (int i = 0; i < data.length; i += 3) { @@ -191,14 +104,17 @@ public class ObjectDetector { } } - private static Tensor makeImageTensor(BufferedImage img) throws IOException { - //BufferedImage img = ImageIO.read(new File(filename)); + private static Tensor makeImageTensor(String filename) throws IOException { + BufferedImage img = ImageIO.read(new File(filename)); if (img.getType() != BufferedImage.TYPE_3BYTE_BGR) { throw new IOException( String.format( "Expected 3-byte BGR encoding in BufferedImage, found %d (file: %s). This code could be made more robust")); } + //System.out.println("Image is of type RGB? " + (img.getType() == BufferedImage.TYPE_INT_RGB)); + //System.out.println("Image is of type RGB? " + (img.getType() == BufferedImage.TYPE_3BYTE_BGR)); byte[] data = ((DataBufferByte) img.getData().getDataBuffer()).getData(); + // ImageIO.read seems to produce BGR-encoded images, but the model expects RGB. bgr2rgb(data); final long BATCH_SIZE = 1; diff --git a/src/WillowChopper.java b/src/WillowChopper.java index 9d98747..efeca52 100644 --- a/src/WillowChopper.java +++ b/src/WillowChopper.java @@ -17,8 +17,8 @@ public class WillowChopper { public WillowChopper() throws AWTException, IOException { - cursor = new Cursor(); - cursorTask = new CursorTask(); + //cursor = new Cursor(); + //cursorTask = new CursorTask(); inventory = new Inventory(); objectDetector = new ObjectDetector(); robot = new Robot(); @@ -27,8 +27,10 @@ public class WillowChopper { public void run() throws Exception { System.out.println("Starting ironMiner..."); while (true) { + String filename = "/home/dpapp/Desktop/RunescapeAI/temp/screenshot.jpg"; BufferedImage image = captureScreenshotGameWindow(); - objectDetector.getIronOreLocationsFromImage(image); + ImageIO.write(image, "jpg", new File(filename)); + ArrayList ironOreLocations = objectDetector.getIronOreLocationsFromImage(filename); System.out.println("--------------------------------\n\n"); /* if (character.isCharacterEngaged()) { diff --git a/src/main.java b/src/main.java index e219074..b90ccbc 100644 --- a/src/main.java +++ b/src/main.java @@ -7,9 +7,9 @@ import java.net.URL; public class main { public static void main(String[] args) throws Exception { - System.out.println("Starting Willow Chopper."); - WillowChopper willowChopper = new WillowChopper(); - willowChopper.run(); + System.out.println("Starting Iron Miner."); + IronMiner ironMiner = new IronMiner(); + ironMiner.run(); /*Cursor cursor = new Cursor(); CursorTask cursorTask = new CursorTask(); Inventory inventory = new Inventory(); diff --git a/target/classes/ColorAnalyzer.class b/target/classes/ColorAnalyzer.class index 6cfc7e5a12d001644017a6385156e05222c5de96..239d38d1a011f738f7a914351e7f550f21924a2f 100644 GIT binary patch delta 20 ccmaE=|5Sg&Y+lCWo9FQIaWU3UUM+3~09^zJ%m4rY delta 20 ccmaE=|5Sg&Y+lA+o9FQIaWOVbUM+3~0AFng00000 diff --git a/target/classes/IronMiner.class b/target/classes/IronMiner.class index afd6be8f9988e134d4bfaf9442eb286039f85025..9a6918f6c888194af7d756f86c905df5acc1ca15 100644 GIT binary patch literal 4326 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 diff --git a/target/classes/ObjectDetector.class b/target/classes/ObjectDetector.class index 29e757455690eeee99d4f453285ebeb2abb9fea6..09d73a99b91e65f901b5a2a48964d9a6146e9291 100644 GIT binary patch literal 5241 zcmb7I349dg75{(PV>X*ffHe^our94gPBtchkZ?$Vz+wU^iD*bMPIf1oh25R)>?|Z| zt!>rXdi1ci)=JS$L_ws7*lKTWZR<~axAs15)%q*_e>0n7L!vG0&V0vv-@E?r zy>Iixe;z&tU^!k^P$E#VePE9swKnP&pN6TxEiikJwoeNuwRAk}>3ut!y|tq(}E=j6~Y% zZ)#rIv?839I`rFIVFj~_4CuDZL^|%n9NLi8&3)A5G*l_56gcyBOi^(<=2HKpp~VEY zRCi3c#W}UL@6@H5*L6@S1cE9&@CwZ8NTl^`xzvDe_Gkl1T_Dh5M788j%}mI7VbN_3 zC9*WDqc~Xf^}@JrMNA{T-P9$cl`zuTcGE~jQd*p+)l8#wjiYqVN+iSUO;a1`NMtP@ z<=-?M)id@93f?Ginr&{v2uHS$?+BE3C*o<%%9%X9^?yBmwIfp7Z-u*L+*;SRDmZ&g zM6o7*EJm51KB`esi#qx!rR~*idwOix3Diw1cSZh?XXE&eK=P@O~{b+JE`NxD$oTj z0%uGwq`<0THP#SR8+$e|Y;^k3 ziR}uu2~^grf;hPhRfC0*DfXOQLz^+ z*xsm-8R;6e|&H<(E3X)VP71SZ*4px_DAI+L^P%3X{dmfP8h zDP?=yzT$dFQcohyV+7{TknFOu+4{_*qz@Ww=G|=3Y&_{WnRfA;U`Z_QP9vGI4pr~iKc2Cxr&o1#?ON7;fhCj3$9vE; zT|!{_3lQac-M;__}wi|*~zGIqaJaI<^EN$}!w z7kFd2Yb^7}f$hhgI3zDKpQg*IBN2%O7hOjm$?phVIuJLT%s4aLQ{C5Q--6^VMmoHW zv{ouZI*uZf^I;5GGig4h@)cXG-~sj+8)ZauJAvuP{m4r#ukm9P4=Z?x2$KZEVY?N{ z&MsS0WV?zZILfSNIx$C=7IZ2cGgc&|^)_F)EjKu*n|jRVX1_d!;|d<-UBs3?1D%R5 z;mdRk>qiemMJH6(M11(FKzx&31DUVQ4Fe;V9%|dXgXtPIVuW5Okq#Bfs}I@O3@wg@ zs%84MgcfIOmWFzGK?;#Y=OdSlg$DFcO5-fWwiYstfn1h#w*n_rJdUq10R{~-rC9>2 zrU}60TirB;vJfUe79lTuSsUJ_{8L8(FonFxd>&u$I7J>V zY0L8}ej>wEN(Hv)tef=OG5k!$&*iF*s~09>){uf<@(wjK9{u<=UQqCxvHF@OmZTbv zJt}^S-%*eKrp7b~$^>!BK`|Mf)rmi-_#^%#+Ze%V5jN{52h3@deOsqxpQ(QmdWjTk z+8Z3Mi4YXwj77@dw!UY>mj3R@u1(C=t+ao`mi61VZR+67qNI;=f47k{qqOI8^_W4nF_}UevL9e^2^fAOs@<<#<+||_L4=Pia%7Q*F zDUTuLvdK~;Q(Y^{T`MXMVM)+mRnd4uC5&5cgZB_B-7Cr)YJ+N3MblxtHILkdS5=6r zilG149VefuD!&zF_hBUP_B`GZ^qs)`pwG5EK+8Gx*;ASpR8C-7P_g&wU(*dF^x1@x zi_YsV`11VM~OR$olTg9JDEnI2ksF|bH$YCw6$2uIqdAJ+x z>_40FB(~srBKvPRA1|T<|3;_q@k(EYE>XeT`a*PwCAdJ;^LoAv7l~)lYs2|Qf}8*? zz*&yojcWmVe@1<^j@f5D3JQwv_gBx%o?Y&+!;C=S!0E^N4IZ~*}L41HCAN9E$ zAH+>efhUo`hj25kd!ByzFh^z7unix<5EJP|+Hea#igLU}FWgFw3Ve(nSxX6y{M*Lm z9~hVa@p1VFIdbG59G8EP@^9xEcVfXwx?u!Qw0uUpkPBR8E2XV~hJjBq;dKq#KBs{*zSqbZ<474=4 z0{7rvuH4Rb{Xy#CH&t zN3f_j@D1-R#)eM8E<`K2wZ+nvTrS7K0T(%^zYGQ*%F4c9=;z zLI7Wi-MGxQUxJwm+i4}c$8P)?f1&-&Olj$p5^^-+uS^RU`Red@jy$B2zd`>&zuZo4 cHm+ACe23z(bmk39 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 KU@}J;?*IVn`4)Tt delta 85 zcmbQpx{!4PGozS9a7kiONoHQULU?9QPJX$9b4Gqa!DLZJUmjkF2t*)@(M*=N9ION^ QkXppZ!0&^sVk_ev0JMP}Z~y=R