From f742cf32019cc17e9423c2a38d020814fba8a66d Mon Sep 17 00:00:00 2001 From: davpapp Date: Sat, 24 Feb 2018 10:49:49 -0500 Subject: [PATCH] Object tracking framework is working, but is too inaccurate. Also added some tests --- src/DetectedObject.java | 17 ++--- src/IronMiner.java | 71 ++++++++++++------ src/ObjectDetector.java | 29 ++++--- src/ObjectDetectorTest.java | 30 +++++++- src/ObjectTrackerTest.java | 96 ++++++++++++++++++++++++ src/main.java | 3 + target/classes/DetectedObject.class | Bin 2349 -> 2237 bytes target/classes/IronMiner.class | Bin 4510 -> 6828 bytes target/classes/ObjectDetector.class | Bin 6830 -> 7007 bytes target/classes/ObjectDetectorTest.class | Bin 1808 -> 2047 bytes target/classes/ObjectTrackerTest.class | Bin 0 -> 3875 bytes target/classes/main.class | Bin 657 -> 770 bytes 12 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 src/ObjectTrackerTest.java create mode 100644 target/classes/ObjectTrackerTest.class diff --git a/src/DetectedObject.java b/src/DetectedObject.java index fc334c3..6797d0f 100644 --- a/src/DetectedObject.java +++ b/src/DetectedObject.java @@ -7,7 +7,7 @@ import org.opencv.core.Rect2d; public class DetectedObject { - private Rectangle boundingBox; + private Rect2d boundingBox; private float detectionScore; private String detectionClass; @@ -17,14 +17,13 @@ public class DetectedObject { this.detectionClass = initializeLabel(detectionClass); } - private Rectangle initializeBoundingBox(float[] detectionBox) { + // TODO: migrate this all to a Rect2d data type + private Rect2d initializeBoundingBox(float[] detectionBox) { int offset_x = (int) (detectionBox[1] * Constants.GAME_WINDOW_WIDTH); int offset_y = (int) (detectionBox[0] * Constants.GAME_WINDOW_HEIGHT); int width = (int) (detectionBox[3] * Constants.GAME_WINDOW_WIDTH) - offset_x; int height = (int) (detectionBox[2] * Constants.GAME_WINDOW_HEIGHT) - offset_y; - - //System.out.println(detectionBox[0] + ", " + detectionBox[1] + ", " + detectionBox[2] + ", " + detectionBox[3]); - return new Rectangle(offset_x, offset_y, width, height); + return new Rect2d(offset_x, offset_y, width, height); } private String initializeLabel(float detectionClass) { @@ -37,16 +36,12 @@ public class DetectedObject { return detectionClass; } - public Rectangle getBoundingRectangle() { - return boundingBox; - } - public Rect2d getBoundingRect2d() { - return new Rect2d(boundingBox.x, boundingBox.y, boundingBox.x + boundingBox.width, boundingBox.y + boundingBox.height); + return new Rect2d(boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height); } public Point getCenterForClicking() { - return new Point(boundingBox.x + boundingBox.width / 2 + Constants.GAME_WINDOW_OFFSET_X, boundingBox.y + boundingBox.height / 2 + Constants.GAME_WINDOW_OFFSET_Y); + return new Point((int) (boundingBox.x + boundingBox.width / 2 + Constants.GAME_WINDOW_OFFSET_X), (int) (boundingBox.y + boundingBox.height / 2 + Constants.GAME_WINDOW_OFFSET_Y)); } public void display() { diff --git a/src/IronMiner.java b/src/IronMiner.java index f9cd964..f21b98a 100644 --- a/src/IronMiner.java +++ b/src/IronMiner.java @@ -1,4 +1,5 @@ import java.awt.AWTException; +import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; @@ -10,6 +11,8 @@ import java.util.ArrayList; import javax.imageio.ImageIO; +import org.opencv.core.Core; +import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.Rect2d; import org.opencv.tracking.Tracker; @@ -30,29 +33,25 @@ public class IronMiner { public IronMiner() throws AWTException, IOException { - //cursor = new Cursor(); - //cursorTask = new CursorTask(); - //inventory = new Inventory(); + cursor = new Cursor(); + cursorTask = new CursorTask(); + inventory = new Inventory(); objectDetector = new ObjectDetector(); robot = new Robot(); randomizer = new Randomizer(); } public void run() throws Exception { - int count = 0; - long mineStartTime = System.currentTimeMillis(); - - while (System.currentTimeMillis() - 60000 < mineStartTime) { - count++; + while (true) { BufferedImage screenCapture = objectDetector.captureScreenshotGameWindow(); ArrayList detectedObjects = objectDetector.getObjectsInImage(screenCapture); - //ArrayList ironOres = objectDetector.getObjectsOfClassInList(detectedObjects, "ironOre"); - System.out.println("Count: " + count); - System.out.println(detectedObjects.size()); - /*DetectedObject closestIronOre = getClosestObjectToCharacter(ironOres); + ArrayList ironOres = objectDetector.getObjectsOfClassInList(detectedObjects, "ironOre"); + + DetectedObject closestIronOre = getClosestObjectToCharacter(ironOres); if (closestIronOre != null) { - //Tracker objectTracker = TrackerKCF.create(); - //Rect2d boundingBox = closestIronOre.getBoundingRect2d(); + System.out.println("Found iron ore! Starting tracking!"); + Tracker objectTracker = TrackerKCF.create(); + Rect2d boundingBox = closestIronOre.getBoundingRect2d(); objectTracker.init(getMatFromBufferedImage(screenCapture), boundingBox); cursor.moveAndLeftClickAtCoordinatesWithRandomness(closestIronOre.getCenterForClicking(), 10, 10); @@ -61,17 +60,26 @@ public class IronMiner { int maxTimeToMine = randomizer.nextGaussianWithinRange(3500, 5000); // track until either we lose the object or too much time passes - while ((System.currentTimeMillis() - mineStartTime) < maxTimeToMine) { + int lostTrackCounter = 0; + while (((System.currentTimeMillis() - mineStartTime) < maxTimeToMine) && lostTrackCounter < 3) { + screenCapture = objectDetector.captureScreenshotGameWindow(); + detectedObjects = objectDetector.getObjectsInImage(screenCapture); + boolean ok = objectTracker.update(getMatFromBufferedImage(screenCapture), boundingBox); - if (!ok || !objectDetector.isObjectPresentInBoundingBoxInImage(screenCapture, boundingBox, "ironOre")) { - System.out.println("Lost track! Finding new ore."); - break; + if (!ok || !objectDetector.isObjectPresentInBoundingBoxInImage(detectedObjects, boundingBox, "ironOre")) { + System.out.println("Lost track for + " + lostTrackCounter + "! Finding new ore soon."); + lostTrackCounter++; } + else if (ok) { + lostTrackCounter = 0; + System.out.println("Tracking at " + boundingBox.x + ", " + boundingBox.y + ", " + boundingBox.width + ", " + boundingBox.height); + } + } } - dropInventoryIfFull();*/ + dropInventoryIfFull(); } } @@ -108,13 +116,32 @@ public class IronMiner { return null; } - public Mat getMatFromBufferedImage(BufferedImage image) { - Mat matImage = new Mat(); - byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + private Mat getMatFromBufferedImage(BufferedImage image) { + BufferedImage formattedImage = convertBufferedImage(image, BufferedImage.TYPE_3BYTE_BGR); + byte[] data = ((DataBufferByte) formattedImage.getData().getDataBuffer()).getData(); + bgr2rgb(data); + Mat matImage = new Mat(formattedImage.getWidth(), formattedImage.getHeight(), CvType.CV_8UC3); + byte[] pixels = ((DataBufferByte) formattedImage.getRaster().getDataBuffer()).getData(); matImage.put(0, 0, pixels); return matImage; } + private static BufferedImage convertBufferedImage(BufferedImage sourceImage, int bufferedImageType) { + BufferedImage image = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), bufferedImageType); + Graphics2D g2d = image.createGraphics(); + g2d.drawImage(sourceImage, 0, 0, null); + g2d.dispose(); + return image; + } + + private static void bgr2rgb(byte[] data) { + for (int i = 0; i < data.length; i += 3) { + byte tmp = data[i]; + data[i] = data[i + 2]; + data[i + 2] = tmp; + } + } + public int getDistanceBetweenPoints(Point startingPoint, Point goalPoint) { return (int) (Math.hypot(goalPoint.x - startingPoint.x, goalPoint.y - startingPoint.y)); } diff --git a/src/ObjectDetector.java b/src/ObjectDetector.java index dc02825..eb70c94 100644 --- a/src/ObjectDetector.java +++ b/src/ObjectDetector.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import javax.imageio.ImageIO; +import org.opencv.core.Core; import org.opencv.core.Rect2d; import org.tensorflow.SavedModelBundle; import org.tensorflow.Tensor; @@ -48,7 +49,7 @@ public class ObjectDetector { Robot robot; public ObjectDetector() throws AWTException { - this.model = SavedModelBundle.load("/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/results/checkpoint_22948/saved_model/", "serve"); + this.model = SavedModelBundle.load("/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/results/checkpoint_56749/saved_model/", "serve"); this.robot = new Robot(); } @@ -94,10 +95,26 @@ public class ObjectDetector { return detectedObjectsInImage; } - public boolean isObjectPresentInBoundingBoxInImage(BufferedImage image, Rect2d boundingBox, String objectClass) throws Exception { + /*public boolean isObjectPresentInBoundingBoxInImage(BufferedImage image, Rect2d boundingBox, String objectClass) throws Exception { BufferedImage subImage = image.getSubimage((int) boundingBox.x, (int) boundingBox.y, (int) boundingBox.width, (int) boundingBox.height); ArrayList detectedObjectsInSubImage = getObjectsInImage(subImage); return (getObjectsOfClassInList(detectedObjectsInSubImage, objectClass).size() != 0); + }*/ + + public boolean isObjectPresentInBoundingBoxInImage(ArrayList detectedObjects, Rect2d boundingBox, String objectClass) throws Exception { + for (DetectedObject detectedObject : detectedObjects) { + if (detectedObject.getDetectionClass().equals(objectClass)) { + //System.out.println(("Required bounding box: " + (int) boundingBox.x + ", " + (int) boundingBox.y + ", " + (int) boundingBox.width + ", " + (int) boundingBox.height)); + //System.out.println(("Detected bounding box: " + (int) detectedObject.getBoundingRect2d().x + ", " + (int) detectedObject.getBoundingRect2d().y + ", " + (int) detectedObject.getBoundingRect2d().width + ", " + (int) detectedObject.getBoundingRect2d().height) + "\n"); + if ((Math.abs(detectedObject.getBoundingRect2d().x - boundingBox.x) < 10) && + (Math.abs(detectedObject.getBoundingRect2d().y - boundingBox.y) < 10) && + (Math.abs(detectedObject.getBoundingRect2d().width - boundingBox.width) < 10) && + (Math.abs(detectedObject.getBoundingRect2d().height - boundingBox.height) < 10)) { + return true; + } + } + } + return false; } public ArrayList getObjectsOfClassInList(ArrayList detectedObjects, String objectClass) { @@ -111,17 +128,11 @@ public class ObjectDetector { } private static Tensor makeImageTensor(BufferedImage image) throws IOException { - /*if (image.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")); - }*/ - BufferedImage formattedImage = convertBufferedImage(image, BufferedImage.TYPE_3BYTE_BGR); byte[] data = ((DataBufferByte) formattedImage.getData().getDataBuffer()).getData(); bgr2rgb(data); - // ImageIO.read seems to produce BGR-encoded images, but the model expects RGB. + // BufferedImage and ImageIO.read() seems to produce BGR-encoded images, but the model expects RGB. final long BATCH_SIZE = 1; final long CHANNELS = 3; long[] shape = new long[] {BATCH_SIZE, formattedImage.getHeight(), formattedImage.getWidth(), CHANNELS}; diff --git a/src/ObjectDetectorTest.java b/src/ObjectDetectorTest.java index fb2e595..c03493a 100644 --- a/src/ObjectDetectorTest.java +++ b/src/ObjectDetectorTest.java @@ -1,15 +1,39 @@ import static org.junit.jupiter.api.Assertions.*; import java.awt.AWTException; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import javax.imageio.ImageIO; import org.junit.jupiter.api.Test; +import org.opencv.core.Rect2d; class ObjectDetectorTest { @Test - void testObjectDetection() throws AWTException { + void testObjectDetection() throws Exception { ObjectDetector objectDetector = new ObjectDetector(); - ArrayList detectedObjects1 = objectDetector.getObjectsInImage(loadimages here in bufferedimage format)); + BufferedImage testImage1 = ImageIO.read(new File("/home/dpapp/tensorflow-1.5.0/models/raccoon_dataset/test_images/ironOre_test_9.jpg")); + ArrayList detectedObjects1 = objectDetector.getObjectsInImage(testImage1); + ArrayList detectedIronOres1 = objectDetector.getObjectsOfClassInList(detectedObjects1, "ironOre"); + ArrayList detectedOres1 = objectDetector.getObjectsOfClassInList(detectedObjects1, "ore"); + + assertEquals(3, detectedIronOres1.size()); + assertEquals(2, detectedOres1.size()); + //assertDetectedObjectsAreEqual(); + } + + void assertDetectedObjectsAreEqual(DetectedObject obj1, DetectedObject obj2) { - }va + } + + void assertBoundingBoxesAreEqual(Rect2d bb1, Rect2d bb2) { + assertEquals(bb1.x, bb2.x, 3); + assertEquals(bb1.y, bb2.y, 3); + assertEquals(bb1.width, bb2.height, 3); + assertEquals(bb1.width, bb2.height, 3); + } } diff --git a/src/ObjectTrackerTest.java b/src/ObjectTrackerTest.java new file mode 100644 index 0000000..4ecd897 --- /dev/null +++ b/src/ObjectTrackerTest.java @@ -0,0 +1,96 @@ +import static org.junit.jupiter.api.Assertions.*; + +import java.awt.AWTException; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.util.ArrayList; + +import javax.imageio.ImageIO; + +import org.junit.jupiter.api.Test; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; +import org.opencv.core.Rect2d; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.tracking.Tracker; +import org.opencv.tracking.TrackerKCF; +import org.opencv.videoio.VideoCapture; + +class ObjectTrackerTest { + + @Test + void testObjectTracking() throws Exception { + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); + VideoCapture video = new VideoCapture("/home/dpapp/Videos/gameplay-2018-02-24_10.01.00.mp4"); + System.out.println("loaded video..."); + + ObjectDetector objectDetector = new ObjectDetector(); + + Mat frame = new Mat(); + boolean frameReadSuccess = video.read(frame); + assertTrue(frameReadSuccess); + + ArrayList detectedObjects = objectDetector.getObjectsInImage(Mat2BufferedImage(frame)); + System.out.println("Tracking " + detectedObjects.size() + " objects."); + ArrayList objectTrackers = new ArrayList(); + ArrayList boundingBoxes = new ArrayList(); + for (int i = 0; i < 3; i++) { + boundingBoxes.add(detectedObjects.get(i).getBoundingRect2d()); + objectTrackers.add(TrackerKCF.create()); + objectTrackers.get(i).init(frame, boundingBoxes.get(i)); + } + //System.out.println("bounding box: " + (int) boundingBoxes.get(0).x + ", " + (int) boundingBoxes.get(0).y + ", " + (int) boundingBoxes.get(0).width + ", " + (int) boundingBoxes.get(0).height); + + int counter = 0; + while (video.read(frame)) { + for (int i = 0; i < 3; i++) { + objectTrackers.get(i).update(frame, boundingBoxes.get(i)); + boolean trackingSuccess = objectTrackers.get(i).update(frame, boundingBoxes.get(i)); + + detectedObjects = objectDetector.getObjectsInImage(Mat2BufferedImage(frame)); + //System.out.println(detectedObjects.size()); + //System.out.println((int) boundingBoxes.get(i).x + ", " + (int) boundingBoxes.get(i).y + ", " + (int) boundingBoxes.get(i).width + ", " + (int) boundingBoxes.get(i).height); + //BufferedImage subImage = screencapture.getSubimage((int) boundingBoxes.get(i).x- 10, (int) boundingBoxes.get(i).y - 10, (int) boundingBoxes.get(i).width + 20, (int) boundingBoxes.get(i).height + 20); + + boolean ironOreDetected = objectDetector.isObjectPresentInBoundingBoxInImage(detectedObjects, boundingBoxes.get(i), "ironOre"); + boolean oreDetected = objectDetector.isObjectPresentInBoundingBoxInImage(detectedObjects, boundingBoxes.get(i), "ore"); + + //ImageIO.write(screencapture, "jpg", new File("/home/dpapp/Videos/frames/frame_" + counter + ".jpg")); + //ImageIO.write(subImage, "jpg", new File("/home/dpapp/Videos/sub_frames/frame_" + counter + "_sub.jpg")); + //System.out.println("wrote file..."); + if (i == 2) { + System.out.println(trackingSuccess + ", ironOre: " + ironOreDetected + ", ore:" + oreDetected); + } + } + counter++; + } + + /*Tracker objectTracker = TrackerKCF.create(); + Rect2d boundingBox = new Rect2d(405, 177, 38, 38); + + int counter = 0; + objectTracker.init(frame, boundingBox); + while (video.read(frame) && counter < 200) { + boolean trackingSuccess = objectTracker.update(frame, boundingBox); + BufferedImage screencapture = Mat2BufferedImage(frame); + boolean ironOreDetected = objectDetector.isObjectPresentInBoundingBoxInImage(screencapture, boundingBox, "ironOre", counter); + + ImageIO.write(screencapture, "jpg", new File("/home/dpapp/Videos/frames/frame_" + counter + ".jpg")); + + System.out.println(trackingSuccess + ", ironOre: " + ironOreDetected); + counter++; + }*/ + } + + private BufferedImage Mat2BufferedImage(Mat matrix)throws Exception { + MatOfByte mob=new MatOfByte(); + Imgcodecs.imencode(".jpg", matrix, mob); + byte ba[]=mob.toArray(); + + BufferedImage bi=ImageIO.read(new ByteArrayInputStream(ba)); + return bi; + } + +} diff --git a/src/main.java b/src/main.java index b90ccbc..e3d1fc2 100644 --- a/src/main.java +++ b/src/main.java @@ -3,11 +3,14 @@ import java.awt.Point; import java.io.IOException; import java.net.URL; +import org.opencv.core.Core; + public class main { public static void main(String[] args) throws Exception { System.out.println("Starting Iron Miner."); + System.loadLibrary(Core.NATIVE_LIBRARY_NAME); IronMiner ironMiner = new IronMiner(); ironMiner.run(); /*Cursor cursor = new Cursor(); diff --git a/target/classes/DetectedObject.class b/target/classes/DetectedObject.class index b4dce75e061bf1009142d8001f3c88c86bf32377..036eb1053a16d9412ae338c2f1fe3ac1d3217012 100644 GIT binary patch delta 748 zcmZ`#O=}ZT6g{s=W|DD~HYRP+tCZGX*Fz?=b&b#-XIh(<61Jci5U%vx5i+3B+ zR3uW=8;h#`pjy|Os-`!pYPqTzr|)judOtj@+%Clb$M1LIga2-oFph+b1DIeCl{_83 z%QpE|pL!-hR941C{BH#9X z2z-JBfmCGptT3@;j+kMu8TOeS#6J_-hbY~H@Hw|1L+D2Y$8ZEEaTKG3o_D)3hT~Mx z9GpN5B!;X=B1IGWpJIMEhyf2CnM!6!ESd6#C=;|D)VKgPSgYo%9 z8{sxCxZ#o;3T~L`(I+@Pilaw%`@4D=$WlTM(#!<}94@niV(;ui2y?m?LTO4{@7Xrf zCCb`*2RHs0;x0`7wJ^g!`lr*)V9P=+VWH8+V++mTQz|X2T6p<|6u53m6hZD3aEcEE K!lLOgZ+`>Pb&=}; delta 891 zcmZuw>uwTJ5dIFZu&ir?gsm8FrM3!`WwG(n7;kM6tX4&;CR$?_DIq9f4OwXG4eun4 ziD~u)^Z}Y^B#_wX1N4{A;3N0|#@StpP34^A%zX3B%r|>Z_WQ0izp(M=+YbQ8(OeZy z1p>1Va`l{&Thx_oA+P5u#d2Zw?n*Eio=-*kaE60*M*aWS2snbMh#eSX@Pub*8Nb+? z94Hp_DXmsnC{>ER{tScRzP;Z&##Ildh}Wt`MSE1JCK8rKo&5GtL9MG%%PIBBDaj+j<__(Fwpykj8>}X0e8;Qa_UZQz(O>>g|Ems}FpLF6+<95_hlAUHZD@Z|Zh8+#GNVLFE>de*K) z2*ZS^2M%Ebq>31AnL79g$H{Y2#UAqt;G*gQ^9rc1gQEK=e<#(>6XP^~x?|iB0tgw* zlWB}vDk5covTYTHGo)+RC}b@z+Jf|NWVaQJ^esMz^WEb65Ujk3bxhti8?-Rpv~9|^ z4NTj3M%^TAd|7RFaAmuoyhU)LH!ZzdSMUzqmN{)=9obF6Zhx+<`ig2-ysN>LM^xWU zwSybmg^q3!N*GZ-ov8Vj+9*ZasJ8I9jhg=n!8V??@!|_9+Ak@PHUF*@P8gqf$@A7? G-u(p;^Q372 diff --git a/target/classes/IronMiner.class b/target/classes/IronMiner.class index d626dc656086b2604942ad8fa780723db1ea1020..722072ee28824323e946b18d49960c74075f6ede 100644 GIT binary patch literal 6828 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 diff --git a/target/classes/ObjectDetector.class b/target/classes/ObjectDetector.class index 49cbb2cfd7e1ad5820739685ed7a0d84fc3404da..92603e3edc21cfd9fbf4f13c90074100e98676a1 100644 GIT binary patch delta 1409 zcmZXTc~I3=6vsdJZNJCw{T>s__D~Zk6%Ym&#CSOly`kJ7${Y1cGKx)}XTQNdMSNZO=5XHPaD8=N zX}Gb*;sst*IKvgo6t2wTtYS~m#l${4*UJ{K#IiKot5+=!*(>)gwN;HZHu9Rq>o(%4 ziPTotH2Ub^O^dg9TVd#IYm{y?OLV1`p+}etMUjfLisbr&!xTwXkwz&$QZ=WdPUdBE z7R_s38ZkJg*IGAuj$3@j=j;8~dA_vxim!F0KhyIq-y3|V+x%(HANWys`X{QN^l^Vi z-~_+eQ73Hz#0nM0nOQSU+WFU_pEKPH;twGLsoaf$mQTscGGzA*GI%%Wx3Fq*$C+kj8XIFoSevGL~87Fq;XKP(Ue#TuzWH zX`+lfxr(jKl{=j$QqAWuS96R7oTQvTxkkBJsM0A{$<@txxm6lrh1|z&j2Am7YzBEuJ=W_4 zGSANTM)|Zvn&FEczS+--9)2#(iSnx%O5~3zZey>n+-a7TyCmZDnzC}Y%>T&j5pP-0 z7^(;uIZ@?^DsPvhb0D{vE~gu^SS5|Nnnc!+%365`>ln!$OrupFN}WDZ-m4UC*RoP- zamhS-5AEgIl3&JYJwA|`a5o9uBd@4UcCP0lw5|*Z+${{ca^P!<3H delta 1280 zcmZ9Ldr;I>6vsbz-|YUDN#wO_2@xs_%Mj0Uhm42<0)j>>$;=Vi04t{gCebmR2`Xm{ zYi>0<(jPs{9)uLQp+ai!u(He^T4DCiGRt0$X?1>!`bWRt{La1iJih09&bhw>Mg4^X zXDNGL-pqmv2NRUbrK4;$1O3#uH8+*8}b;3pVhiLM@fA%+x<+@N`tu zVV<5f=rd3DJ6amsnoUi=!6sAVXb!hDHMhCx<#~e_7*u$g!fmtL8(MBz*c4WzXH?1) z&;ypK@op5G4YCNDyB7_H*`oJ(<|VnwVVl7SqY7JV%QA_xW&|pAi|5LKtF^tMastcO z7;XzMY6~~kHY}F4wQ`B%Ic00e(7P=&^a3N~Hql-;c!M|f&Y0v?TtciZh-4$KYQ^2Uo?ew|euikxwE8oJ*n3 zjVo0b4YtMASUO9va0zZM72{?2xtwz-CzT3jP)U$0$fb%Rs+ma{A!=x+methp2v^b1 zY?;O!c5yZ1T*Fc3a*TRq=Q@?jT$ROqwTBy+26W*CwU_CFERXEE=pHOdXi%LNJfg8l zQ2}CQw-txHke?MC;?W)v8%^$fL%IXknUC23E;d2Iu zm1S7D1_aK6j44&JJLRxcCeuMY%SdL0wEj*~=wucv^>SZ%iu@`<+PoSogo{nqY1`i&7g^82%3AM%+@$9ic4(wc(a6tyIz=*HBGOL7~Rt4L8XlEE9I)?xuh*;PW`+ z0MpL+06v6|;`p825FmwSCTIWK`TqO)=fAgq1DM5b6fFXs8-;yY^fJ=pX*;i^>qQX~ z=-4;j7<$F9%6ecS5SceE(_0W|O^oLSLaTO3#?gv44PitCZs$x(uGbC<(s^YRDw5zi zyJ%GMhGQzeE)02lrYq2$d*5?fpvz+fmo=ts3B=cqin8jFcB8mMtBdFw8i9e$n&p`X zGH<#j{ViLT?HT?hfuWr3l=b}@GvHY@J?ZF1)znqO=_q;x+An*xojAJD7Dc~+<~uWO z{kd6@F?=krsqfhbQZH4FYE}27<=W0}#Xg*#NzNu8=?AP!#nm07ShQ_xr(}4BD?Qq| z-i~=-l%=bij%{r?a>tjYlKa(i9DNv4g}f`!lgM4G;E>;Tbro8l$FU?YEjy;XH>E*{oWNcKgagag&+`ombs-cxQ+)5$r97^lFXqGRK<9J8({S!Ry}hk+{HGGRr0dd5CCbAUcOUa)aj=XC7_k0BtnM@`?+Sc%c zZ+)zAf70-?;=+e!$=g$s9Sy%KNn}r&5ZBij> zI?zcg5IHTyK~?agJ-z+a&qbfW9yUCorx1nJPVOXDk2;i z2%X^38J>m*!U3V8Y4xKSfhuh$&o1=vVHo5&hF(mgA5SrWMM_kp3xVHyq%q849$zrB z9!yfV$lM0;083aV?ihx!LMlX*F07IYBSVZ~>H@2^hSgfbYK>OFb9_lumFpt^s;r{9 zboc#$6d09_(g}QpEO{k+L8^t+*T~hu{_qP6q$dBu%PIc5v2}*;$h{`_y21TO?w9Mh siy(UaI7is4QRX>@3FbY=2U}Gs2rAVGDg^@tKd2Onq*_RqXx9SyKS+e{l>h($ literal 1808 zcmeHI&ubGw6n>MYO|nK))7DyzF!Yo{aW8r?A_QBpuvRcJ;&HP3l1$j0S!O4(`O5?a zy?gLa65no0o1~32;6?DTv-9J7KjzK%&HVWFWpt*1HBXzqI~BI3jB_(j67lK|GfE<16utDPj^`6iyqrdYN ziXW@aXDENqFcoT33_uw+vo=G)U8vFsV2f7QV4He*LhZmV^-}zO`d{_CcSFAn9+G_y GYQF&qn?4Ny diff --git a/target/classes/ObjectTrackerTest.class b/target/classes/ObjectTrackerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..759d4430a26707665c51712dec27339614b7262d GIT binary patch literal 3875 zcmai0X?q*h6@IU6c_ewVAdo>xNR$L8c4B*M;~0q2q?R$Qz)Na74t5~vSQ z?s@B-D{lZegnuZg6S!$~X5L6U6Skgy#IPrff}@~8VC%fTsA~&)Zcg(E1R9T+Iny~R zP~X}%DbO%rWsETD5mFIAqrjFSGiQtxvojQ<&ny@Mks&LsFHGvTDZl;A2Is6<5V(2h z2E_?-I5eQ*z|74Fga^;1jl5%8xq^b5$hvOZ+KE7HteA7mtTAa8Ols@TP5)W#x@rdQnSTwxJF1q2e~gsC1-%A~iX9 zW+;^$>mQpsGtxgiC_P-aW!$l8ZX%5BxLv`Hsu+$h6&xe0VkdS9G%Z+qX2_heb$dx5 z+Ipjmwn+uM1$I@~vuI`v%e1sf`5w^oPSG}k*eh_ScGk)oS|+dO^WJGeo71yKenDUA z=;`XdyQ8b8qo?;wcUNaucV|~uXExs(MjS0NZM#CWcE5^Nv@x$%(V;z_IaGC={y6l? z(6fmk4l={ia3d3UZRzan45I^@g3fYEs(Dm&NpF?B6tj?1&?B(f%k8~}L(Exr7~Kdd zI79&zuDYQg)}1hV5t63ft>PXWCgf;JM&PzqH=UJr=);~1UhN~{RZrp1xe1+D$d}80zt>}w6H{5 zZ`94`j!K}6BCR3=X*)wKE33*-4|)t5Tgqk+fkwvL3}Y6iig`TZr6*|>a~Vc$j417) z%iE~+dVqRou5K1~4i?qw84_0&w69cR^Fv_!vITZlNOEM`r6SGmzYh!$B@T)VcbZ^# z=*ymJ-8DGq(xU>~-$kqfHk#%N0=L9G55ic)hgEz8A7$!ghZpFojq!~_Ae~#pV=6u_ zWi%G^85$nMCt3TZZRJL7UK8E8pyJc`jKE&A;05KlZ4`{0lggE&owUxa6-3epOEp3h z8<}tejMxo=_^j+{Qv2ud1qGk4?#OPpR`EqVNjn$yg`zP!E0Z@}17JaXnQgm0?z?l? zt){0WuD>elp$1DC#zB0IA!%nnGT6WZZs!!(UFk=y&d#fFv*p%MxmD75|HdrukR~u| zv(w9-eSL0aLnMoPKE|#&UQDNr0`FZh%|4!S`)?Wj-1VFObYUHd zh4Bi0so>S>7GP%Q(wwBy1ua!xsVHHYZI9DTPM!%2*65uzb6#uHGse9iFX2}zu3(i3 zb1e5Fk=I_^sicC}iDdUG;VmwWcTZEfe9<2P!gvk8mNUau;;!{nQeKt6#qSmTj_{F; z=e(Pb?CzzO8kM*9o9>NSmRlh%;g{J)Hy*1dtFuPWv4*qO4Ex)=xF%3Hqw{epDOZH# zPA{<=3lEQ5MLTWWXUaLX*+Y_#ayb-sN{cypP&sF1xK9qb{8sp-@h$ic99Ty1WiILv z=2ISn!(0CVl~%d0LMxTs81yDxUax;VqQCocp=G}!q3wKrWEFVy#iF5=l(V>IN-ajCr}sXs<}wGaBj73R0c0@UGi zMLto&r}~Pf~6>=Xx2) zYI>Zi0(`fu;u696i{Z#i2isS0`DA+uDj?QtTA7 zHqE+w2z&6b8_`}*;9B*y;17%*J1R5#NBoHrqU8BA-lFO~{Qm`ib?fqPuJphE4}6*x AWB>pF literal 0 HcmV?d00001 diff --git a/target/classes/main.class b/target/classes/main.class index c0eaf8197ad678573d4f836837ec2bc34fba4d79..8388c35d31602c6d0380125d4a5367f869b5a39d 100644 GIT binary patch delta 269 zcmXYp-Ack>0EVA$WV*QuW=I(H|F^8xpGO^8crchAXapXOrDIHFi;oaI3c7$Gyb?XI zDCh#ZsOU@Lfd^jRm-jXEJa2see0%{FDr2K1o^&?Z&ax#~PG#hz4`I?7IGt36&W)1E z4!eRvqk8M!)qKyrZdRLZzfrB%1eVte27%KLlE`U2$~cLx3<~U<9I!6P_tT*3#rHCh z!z_ma-IZxlkCRXu*yw+nJ2p8P4aJMlM>);z`DhAQonTojQ%Vyy-r4)aZog6X%2STU ms@5=+i)55daxAc@D6&L}HS)7=c7wy||MHA;gyKSFjmuy7J~BoC delta 155 zcmZo-o5;#_>ff$?3=9mW3_2URf*EDl859{AI6aH<^L#V&Qj5446c{)`gbELX>f{HE zeT?jr^O-agLK&EVkbyyT6N8YD#5M-0-3$tm+ZmL6w6`%RZv;v(F>nD%AqEy8$;H6I rz|A1Qz{4QIAPr