Package net.hearthstats.game.imageanalysis

Source Code of net.hearthstats.game.imageanalysis.ScreenAnalyser$PartialResult

package net.hearthstats.game.imageanalysis;

import net.hearthstats.game.Screen;
import net.hearthstats.game.ScreenGroup;
import net.hearthstats.util.Coordinate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.image.BufferedImage;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;

/**
* An analyser that identifies Hearthstone screens from screenshots.
*
* @author gtch
*/
public class ScreenAnalyser {

  private final static Logger log = LoggerFactory.getLogger(ScreenAnalyser.class);

  private int expectedWidth = 0;
  private int expectedHeight = 0;

  private Map<PixelLocation, Coordinate> pixelMap;

  /**
   * <p>
   * Identifies the screen in the given image. If possible it will perform an
   * 'exact' match, meaning that it all the primary pixels specified in
   * {@link Screen} were within range. An exact match is very accurate, however
   * sometimes an exact match is not possible due to effects (eg partial
   * effects) or obstructing objects (eg a card being dragged over certain
   * pixels) so a partial match is performed. If a partial match doesn't
   * identify a screen with high enough confidence, no screen is returned.
   * </p>
   * <p>
   * It is considered normal that some screens can't be identified. Only the
   * screens that indicate important events have been defined in Screen so far;
   * some Hearthstone screens can't be identified but this is silently ignored
   * by the HearthstoneAnalyser which expects some unknown screens.
   * </p>
   *
   * @param image
   *          The image to identify a Hearthstone screen from.
   * @param previousScreen
   *          The last screen that was identified; optional, but specify this to
   *          narrow down the search, reduce the risk of false positives (eg
   *          jumping out of a game unexpectedly) and generally make the
   *          analysis faster.
   * @return The Screen that was identified, or null if no screen could be
   *         identified with reasonable confidence.
   */
  public Screen identifyScreen(BufferedImage image, Screen previousScreen) {

    log.trace("Identifying screen");

    // ProgramHelpers may return a null image if the window is minimised or still loading, so ignore those
    if (image == null) {
      return null;
    }

    if (expectedWidth != image.getWidth() || expectedHeight != image.getHeight()) {
      pixelMap = calculatePixelPositions(image.getWidth(), image.getHeight());
      expectedWidth = image.getWidth();
      expectedHeight = image.getHeight();
    }

    // If we have a previous screen, check only those screens which follow from
    // this one
    EnumSet<Screen> possibleScreens;
    if (previousScreen == null) {
      possibleScreens = EnumSet.allOf(Screen.class);
    } else {
      possibleScreens = previousScreen.nextScreens;
      if (possibleScreens.size() == 0) {
        throw new IllegalStateException("Unable to identify screen because previous screen "
            + previousScreen + " has no nextScreens parameter");
      }
    }

    Screen match = null;

    // Try to perform and exact match on the screen we were last on -- it's the
    // most likely one to match, of course!
    if (previousScreen != null) {
      if (checkForExactMatch(image, previousScreen)) {
        // This screen matches
        log.trace("Exact match on previous screen {}", previousScreen);
        match = previousScreen;
      }
    }

    // Try to find an exact match for the screens, based on the primary pixels
    // only
    if (match == null) {
      for (Screen screen : possibleScreens) {
        if (checkForExactMatch(image, screen)) {
          // This screen matches
          if (log.isDebugEnabled()) {
            if (match == null) {
              log.trace("Exact match on new screen {}", screen);
            } else {
              log.warn(
                  "More that one screen matched! Matched screen {}, but have already matched {}",
                  screen, match);
            }
          }

          match = screen;

          // If not running in debug mode, we can skip the rest of the loop for
          // efficiency
          if (!log.isDebugEnabled())
            break;
        }
      }
    }

    if (match == null) {
      // A check of the primary pixels did not find an exact match, so try for a
      // partial match
      log.debug("Did not find exact screen match, attempting partial match");

      Map<Screen, PartialResult> screenMatchesMap = new HashMap<>();
      int maxMatchedCount = 0;
      int maxUnmatchedCount = 0;
      Screen bestMatch = null;

      EnumSet<Screen> possibleScreensIncludingPrevious = EnumSet.copyOf(possibleScreens);
      if (previousScreen != null) {
        possibleScreensIncludingPrevious.add(previousScreen);
      }

      for (Screen screen : possibleScreensIncludingPrevious) {
        PartialResult partialResult = checkForPartialMatch(image, screen);

        if (partialResult.matchedCount >= maxMatchedCount) {
          maxMatchedCount = partialResult.matchedCount;
          bestMatch = screen;
        }
        if (partialResult.unmatchedCount > maxUnmatchedCount) {
          maxUnmatchedCount = partialResult.unmatchedCount;
        }

        log.debug("Test of screen {} matched={} unmatched={}", screen, partialResult.matchedCount,
            partialResult.unmatchedCount);
        screenMatchesMap.put(screen, partialResult);
      }

      // A partial match is defined as the screen that:
      // - has no more than two pixels unmatched
      // - has more matched pixels than any other screen
      // - has fewer unmatched pixels than any other screen
      assert (bestMatch != null);

      PartialResult bestMatchResult = screenMatchesMap.get(bestMatch);
      boolean acceptBestMatch = true;

      if (bestMatchResult.unmatchedCount > 2) {
        log.debug("Partial match failed because best match {} has {} unmatched pixels", bestMatch,
            bestMatchResult.unmatchedCount);
        acceptBestMatch = false;
      } else {
        // Check whether other screens are too close to the best-matched screen,
        // but ignore any screens considered to be equivalent (ie the playing
        // screen for each board is considered equivalent)
        ScreenGroup ignoreGroup;
        if (bestMatch.group == ScreenGroup.MATCH_PLAYING
            || bestMatch.group == ScreenGroup.MATCH_END) {
          ignoreGroup = bestMatch.group;
        } else {
          ignoreGroup = null;
        }
        for (Screen screen : possibleScreens) {
          if (screen != bestMatch && (ignoreGroup == null || screen.group != ignoreGroup)) {
            // This screen is not the best match, and it's not from the same
            // group (for those groups considered equivalent) so we need to
            // ensure it's not too close to the best match
            PartialResult currentResult = screenMatchesMap.get(screen);
            if (bestMatchResult.matchedCount <= currentResult.matchedCount) {
              log.debug(
                  "Partial match failed because best match {} has {} matched pixels whereas {} has {}",
                  bestMatch, bestMatchResult.matchedCount, screen, currentResult.matchedCount);
              acceptBestMatch = false;
              break;
            } else if (bestMatchResult.unmatchedCount >= currentResult.unmatchedCount) {
              log.debug(
                  "Partial match failed because best match {} has {} unmatched pixels whereas {} has {}",
                  bestMatch, bestMatchResult.unmatchedCount, screen, currentResult.unmatchedCount);
              acceptBestMatch = false;
              break;
            }
          }
        }
      }

      if (acceptBestMatch) {
        log.trace("Partial match on screen {}", bestMatch);
        match = bestMatch;
      }
    }

    return match;
  }

  /**
   * Calculates the relative positions of our standard pixel locations given the
   * specified screen width and height. Hearthstone can run in many different
   * screen sizes so all pixel locations need to be adjusted accordingly.
   *
   * @param width
   *          the screen width to calculate positions for
   * @param height
   *          the screen height to calculate positions for
   */
  Map<PixelLocation, Coordinate> calculatePixelPositions(int width, int height) {

    log.trace("Recalculating pixel position for width {} height {}", width, height);

    Map<PixelLocation, Coordinate> result;

    if (width == PixelLocation.REFERENCE_SIZE.x() && height == PixelLocation.REFERENCE_SIZE.y()) {
      // The screen size is exactly what our reference pixels are based on, so
      // we can use their coordinates directly
      result = new HashMap<>();
      for (PixelLocation pixelLocation : PixelLocation.values()) {
        Coordinate coordinate = new Coordinate(pixelLocation.x(), pixelLocation.y());
        log.debug("Stored position of {} as {}", pixelLocation, coordinate);
        result.put(pixelLocation, coordinate);
      }

    } else {
      // The screen size is different to our reference pixels, so coordinates
      // need to be adjusted
      float ratioX = (float) width / (float) PixelLocation.REFERENCE_SIZE.x();
      float ratioY = (float) height / (float) PixelLocation.REFERENCE_SIZE.y();
      // ratioY is normally the correct ratio to use, but occasionally ratioX is
      // smaller (usually during screen resizing?)
      float ratio = Math.min(ratioX, ratioY);
      float screenRatio = (float) width / (float) height;

      int xOffset;
      if (screenRatio > 1.4) {
        xOffset = (int) (((float) width - (ratio * PixelLocation.REFERENCE_SIZE.x())) / 2);
      } else {
        xOffset = 0;
      }

      log.debug("ratio={} screenRatio={}, xOffset={}", ratio, screenRatio, xOffset);

      result = new HashMap<>();
      for (PixelLocation pixelLocation : PixelLocation.values()) {
        int x = (int) (pixelLocation.x() * ratio) + xOffset;
        int y = (int) (pixelLocation.y() * ratio);
        Coordinate coordinate = new Coordinate(x, y);
        log.debug("Calculated position of {} as {}", pixelLocation, coordinate);
        result.put(pixelLocation, coordinate);
      }
    }

    return result;
  }

  @SuppressWarnings("unchecked")
  EnumSet<Screen>[] matchScreensForTesting(BufferedImage image) {

    if (expectedWidth != image.getWidth() || expectedHeight != image.getHeight()) {
      pixelMap = calculatePixelPositions(image.getWidth(), image.getHeight());
      expectedWidth = image.getWidth();
      expectedHeight = image.getHeight();
    }

    EnumSet<Screen> primaryMatches = EnumSet.noneOf(Screen.class);
    EnumSet<Screen> secondaryMatches = EnumSet.noneOf(Screen.class);

    for (Screen screen : Screen.values()) {
      if (checkForExactMatch(image, screen)) {
        primaryMatches.add(screen);
        if (checkForMatchSecondary(image, screen)) {
          secondaryMatches.add(screen);
        }
      }
    }

    return new EnumSet[] { primaryMatches, secondaryMatches };
  }

  boolean checkForExactMatch(BufferedImage image, Screen screen) {

    // Skip screens that haven't yet been defined
    if (screen.primary.size() == 0) {
      return false;
    }

    for (Pixel pixel : screen.primary) {
      Coordinate coordinate = pixelMap.get(pixel.pixelLocation);
      int x = coordinate.x();
      int y = coordinate.y();

      int rgb = image.getRGB(x, y);
      int red = (rgb >> 16) & 0xFF;
      int green = (rgb >> 8) & 0xFF;
      int blue = (rgb & 0xFF);

      if (red < pixel.minRed || red > pixel.maxRed || green < pixel.minGreen
          || green > pixel.maxGreen || blue < pixel.minBlue || blue > pixel.maxBlue) {
        // This pixel is outside the expected range
        return false;
      }
    }

    // All pixels matched
    return true;
  }

  PartialResult checkForPartialMatch(BufferedImage image, Screen screen) {

    // Skip screens that haven't yet been defined
    if (screen.primary.size() == 0) {
      return new PartialResult(0, 0);
    }

    int matchedCount = 0;
    // Boost the unmatched count on the Starting Hand screen because it doesn't have sufficient pixels for a reliable partial match
    int unmatchedCount = screen == Screen.MATCH_STARTINGHAND ? 1 : 0;

    for (Pixel pixel : screen.primaryAndSecondary) {
      Coordinate coordinate = pixelMap.get(pixel.pixelLocation);
      int x = coordinate.x();
      int y = coordinate.y();

      int rgb = image.getRGB(x, y);
      int red = (rgb >> 16) & 0xFF;
      int green = (rgb >> 8) & 0xFF;
      int blue = (rgb & 0xFF);

      if (red < pixel.minRed || red > pixel.maxRed || green < pixel.minGreen
          || green > pixel.maxGreen || blue < pixel.minBlue || blue > pixel.maxBlue) {
        // This pixel is outside the expected range: it's not a match
        unmatchedCount++;
      } else {
        // This pixel is inside the expected range: it's a match
        matchedCount++;
      }
    }

    return new PartialResult(matchedCount, unmatchedCount);
  }

  boolean checkForMatchSecondary(BufferedImage image, Screen screen) {

    // Skip screens that haven't yet been defined
    if (screen.primary.size() == 0) {
      return false;
    }

    for (Pixel pixel : screen.secondary) {
      Coordinate coordinate = pixelMap.get(pixel.pixelLocation);
      int x = coordinate.x();
      int y = coordinate.y();

      int rgb = image.getRGB(x, y);
      int red = (rgb >> 16) & 0xFF;
      int green = (rgb >> 8) & 0xFF;
      int blue = (rgb & 0xFF);

      if (red < pixel.minRed || red > pixel.maxRed || green < pixel.minGreen
          || green > pixel.maxGreen || blue < pixel.minBlue || blue > pixel.maxBlue) {
        // This pixel is outside the expected range
        return false;
      }
    }

    // All pixels matched
    return true;
  }

  class PartialResult {
    final int matchedCount;
    final int unmatchedCount;

    PartialResult(int matchedCount, int unmatchedCount) {
      this.matchedCount = matchedCount;
      this.unmatchedCount = unmatchedCount;
    }
  }

}
TOP

Related Classes of net.hearthstats.game.imageanalysis.ScreenAnalyser$PartialResult

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.