diff --git a/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Detector.java b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Detector.java index 0a4c0c4..7d55b21 100644 --- a/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Detector.java +++ b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Detector.java @@ -2,7 +2,7 @@ import net.sourceforge.tess4j.ITessAPI; import net.sourceforge.tess4j.Tesseract; -import net.sourceforge.tess4j.TesseractException; +import net.sourceforge.tess4j.Word; import org.apache.commons.text.similarity.LevenshteinDistance; import java.awt.*; @@ -12,21 +12,11 @@ import java.util.ArrayList; import java.util.Arrays; -class OCRResult { - ArrayList> values; - ArrayList regions; - - public OCRResult(ArrayList> values, ArrayList regions) { - this.values = values; - this.regions = regions; - } -} - class DetectionResult { - OCRResult matrix, sequences; + OCRArray2D matrix, sequences; int bufferSize; - public DetectionResult(OCRResult matrix, OCRResult sequences, int bufferSize) { + public DetectionResult(OCRArray2D matrix, OCRArray2D sequences, int bufferSize) { this.matrix = matrix; this.sequences = sequences; this.bufferSize = bufferSize; @@ -121,12 +111,6 @@ private void initTesseract() { tess.setTessVariable("user_defined_dpi", "300"); } - private static void offsetRegions(ArrayList regions, Point origin) { - for (Rectangle r : regions) { - r.translate(origin.x, origin.y); - } - } - /** * Find a black outlined rectangle in the image. The algorithm first searches * left until it finds a black pixel (leftmost bounds of the box), searches @@ -189,7 +173,7 @@ private int calcBufferSize(Rectangle bufferBoundingBox) { return (int)(innerWidth * 18/23 / innerHeight); } - private OCRResult doOCR(BufferedImage img, Rectangle boundingBox) { + private OCRArray2D doOCR(BufferedImage img, Rectangle boundingBox) { try { img = img.getSubimage( boundingBox.x, boundingBox.y, boundingBox.width, boundingBox.height @@ -197,30 +181,20 @@ private OCRResult doOCR(BufferedImage img, Rectangle boundingBox) { } catch (RasterFormatException e) { return null; } - try { - String text = tess.doOCR(img); - ArrayList regions = (ArrayList) tess.getSegmentedRegions( - img, ITessAPI.TessPageIteratorLevel.RIL_WORD - ); - offsetRegions(regions, boundingBox.getLocation()); - return new OCRResult(parseText(text), regions); - } catch (NullPointerException | TesseractException e) { - return null; - } - } - private static ArrayList> parseText(String text) { - ArrayList> rows = new ArrayList<>(); - for (String rowText : text.split("\n")) { - // Split up lines - ArrayList row = new ArrayList<>(); - for (String word : rowText.split(" ")) { - // Parse each word in the line as a hex value - row.add(parseWord(word)); - } - rows.add(row); + OCRArray2D array = new OCRArray2D(); + Rectangle lastBounds = null; + for (Word word : tess.getWords(img, ITessAPI.TessPageIteratorLevel.RIL_WORD)) { + Rectangle bounds = word.getBoundingBox(); + bounds.translate(boundingBox.x, boundingBox.y); + if (lastBounds == null || bounds.y > lastBounds.y + lastBounds.height) + // The bounding box is below the last one, so we're on a new row + array.addRow(); + array.add(parseWord(word.getText()), bounds); + lastBounds = bounds; } - return rows; + + return array; } private static Integer parseWord(String word) { @@ -288,15 +262,17 @@ public DetectionResult detect() { // Detect the matrix BufferedImage captureMatrix; - OCRResult matrix = null; + OCRArray2D matrix = null; for (int thresh=MATRIX_THRESHOLD; thresh<=MATRIX_THRESHOLD_MAX; thresh+=MATRIX_THRESHOLD_DELTA) { + // TODO it'd probably be nice to optimize the image processing by + // cropping rather than full copies ;) captureMatrix = ImageProcessing.copy(captureMaster); ImageProcessing.threshold(captureMatrix, thresh); ImageProcessing.invert(captureMatrix); matrix = doOCR(captureMatrix, matrixBox); - if (matrix != null && Utils.isGridUniform(matrix.values)) + if (matrix != null && matrix.isGrid()) // the OCR successfully found a well-formed grid break; } @@ -308,7 +284,7 @@ public DetectionResult detect() { ImageProcessing.threshold(captureSequences, SEQUENCES_THRESHOLD); ImageProcessing.invert(captureSequences); - OCRResult sequences = doOCR(captureSequences, sequencesBox); + OCRArray2D sequences = doOCR(captureSequences, sequencesBox); if (sequences == null) return null; diff --git a/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Main.java b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Main.java index 4620551..df17626 100644 --- a/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Main.java +++ b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/Main.java @@ -10,8 +10,8 @@ import java.awt.AWTException; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.logging.LogManager; public class Main implements NativeKeyListener { @@ -72,8 +72,6 @@ public void nativeKeyReleased(NativeKeyEvent e) { } public void runSuite() { - StringBuilder sb; - overlay.clearSolution(); try { Thread.sleep(100); @@ -88,54 +86,27 @@ public void runSuite() { return; } - if (logger.isDebugEnabled()) { - sb = new StringBuilder(); - for (ArrayList row : detection.matrix.values) { - for (Integer cell : row) { - sb.append(String.format("%02X ", cell)); - } - sb.append("\n"); - } - logger.debug("Matrix:\n{}", sb); - - sb = new StringBuilder(); - for (ArrayList row : detection.sequences.values) { - for (Integer i : row) { - sb.append(String.format("%02X ", i)); - } - sb.append("\n"); - } - logger.debug("Sequences:\n{}", sb); - - logger.debug("Buffer size: {}", detection.bufferSize); - } - - Integer[][] matrixArr = Utils.tryGet2DSubarray(detection.matrix.values); - if (matrixArr == null) { - overlay.clearSolution(); - return; - } + logger.debug("Matrix:\n{}", detection.matrix); + logger.debug("Sequences:\n{}", detection.sequences); + logger.debug("Buffer size: {}", detection.bufferSize); - solver.setAll(matrixArr, detection.sequences.values, detection.bufferSize); + solver.setAll(detection); solver.solve(); - ArrayList solution = solver.getSolution(); + List solution = solver.getSolution(); if (solution == null) { overlay.clearSolution(); return; } if (logger.isDebugEnabled()) { - sb = new StringBuilder(); - for (GridNode s : solver.getSolution()) { - sb.append(String.format("%02X (%d, %d)\n", s.value, s.x, s.y)); + StringBuilder sb = new StringBuilder(); + for (OCRArrayNode node : solution) { + sb.append(String.format("\n(%d, %d) %02X", node.x, node.y, node.value)); } - sb.append("\n"); - logger.debug("Solution:\n{}", sb); + logger.debug("Solution:{}", sb); } - int matrixWidth = matrixArr[0].length; - overlay.setRegions(detection.matrix.regions); - overlay.setSolution(solution, matrixWidth); + overlay.setSolution(solution); System.gc(); } diff --git a/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/OCRArray2D.java b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/OCRArray2D.java new file mode 100644 index 0000000..e81d28d --- /dev/null +++ b/src/main/java/com/github/hawkpath/cyberpunk_breach_protocol_solver/OCRArray2D.java @@ -0,0 +1,146 @@ +package com.github.hawkpath.cyberpunk_breach_protocol_solver; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +class OCRArrayNode { + public int x; + public int y; + public Integer value; + public Rectangle boundingBox; + + public OCRArrayNode(int x, int y, Integer value, Rectangle boundingBox) { + this.x = x; + this.y = y; + this.value = value; + this.boundingBox = boundingBox; + } + + public boolean equals(int other) { + return value.equals(other); + } + + public boolean equals(OCRArrayNode other) { + return value.equals(other.value); + } + + public String toString() { + return String.format("", value, x, y); + } +} + +public class OCRArray2D implements Iterable> { + + private List> rows; + private List lastRow; + private Boolean isGrid = true; + + public OCRArray2D() { + rows = new ArrayList<>(); + } + + public Iterator> iterator() { + return rows.iterator(); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int y=0; y(); + rows.add(lastRow); + } + + public void add(Integer value, Rectangle boundingBox) { + makeFieldsDirty(); + lastRow.add(new OCRArrayNode(lastRow.size(), rows.size() - 1, value, boundingBox)); + } + + private void makeFieldsDirty() { + isGrid = null; + } + + public OCRArrayNode get(int x, int y) throws IndexOutOfBoundsException { + return rows.get(y).get(x); + } + + public List getRow(int y) throws IndexOutOfBoundsException { + return rows.get(y); + } + + public boolean isGrid() { + if (isGrid != null) + return isGrid; + + if (rows.size() <= 1) { + isGrid = true; + return isGrid; + } + + int width = rows.get(0).size(); + for (int i=1; i regions) { - overlayComponent.regions = regions; - forceOnTop(); - overlayComponent.repaint(); - } - - public void setSolution(ArrayList solution, int matrixWidth) { + public void setSolution(List solution) { overlayComponent.solution = solution; - overlayComponent.matrixWidth = matrixWidth; forceOnTop(); overlayComponent.repaint(); } @@ -85,7 +78,7 @@ public void forceOnTop() { } public void clearSolution() { - setSolution(null, -1); + setSolution(null); } } @@ -101,23 +94,13 @@ class OverlayComponent extends JComponent { static final Stroke strokeFirst = new BasicStroke( 2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND ); - static final String regionNullMsg = "Region out of bounds. Matrix width is possibly wrong"; - protected ArrayList regions = null; - protected ArrayList solution = null; - protected int matrixWidth = -1; - - private Rectangle getRegion(GridNode node) { - int index = node.y * matrixWidth + node.x; - if (index < regions.size()) - return regions.get(index); - return null; - } + protected List solution = null; @Override public void paintComponent(Graphics g0) { super.paintComponent(g0); - if (solution == null || regions == null || matrixWidth == -1) { + if (solution == null) { return; } @@ -129,30 +112,23 @@ public void paintComponent(Graphics g0) { g.setStroke(stroke); g.setColor(strokeColor); - GridNode node, nodeNext; - Rectangle rect, rectNext; + OCRArrayNode node, nodeNext; for (int i=0; i", value, x, y); - } -} - -class Grid2D { - - private GridNode[][] data; - private int width; - private int height; - - public Grid2D(Integer[][] data) { - width = data[0].length; - height = data.length; - this.data = new GridNode[height][]; - - int lastRowLen = width; - for (int y=0; y>> { +class SequencePermutator implements Iterable>> { /* * We're assuming that the sequences are ordered in ascending priority * @@ -99,19 +28,22 @@ class SequencePermutator implements Iterable>> { * 4 */ - private List> sequences; + private List> sequences; private int maxBufferSize; - private List>> permutations; + private List>> permutations; - public SequencePermutator(List> sequences, int maxBufferSize) { - Collections.reverse(sequences); - this.sequences = sequences; + public SequencePermutator(OCRArray2D sequences, int maxBufferSize) { + int seqCount = sequences.getHeight(); + this.sequences = new ArrayList<>(seqCount); + for (int y = seqCount - 1; y >= 0; y--) { + this.sequences.add(sequences.getRow(y)); + } this.maxBufferSize = maxBufferSize; generatePermutations(); } @Override - public Iterator>> iterator() { + public Iterator>> iterator() { return permutations.iterator(); } @@ -133,9 +65,9 @@ private static long rowsWithItemInFirstPosition(long total, long select, long in private void generatePermutations() { int total = sequences.size(); - Combinator> combinator; - Permutator> permutator; - List>> permutations = new ArrayList<>(); + Combinator> combinator; + Permutator> permutator; + List>> permutations = new ArrayList<>(); int[] startPositions = new int[total]; for (int element=0; element(combinator.get(i), select); - for (List> permutation : permutator) { + for (List> permutation : permutator) { // Iterate through each permutation of this combination permutations.add(permutation); } @@ -170,35 +102,35 @@ private void generatePermutations() { public class Solver { - private Grid2D data = null; + private OCRArray2D matrix = null; private SequencePermutator sequencePermutator = null; private int bufferSize = -1; - private ArrayList solution = null; + private ArrayList solution = null; public Solver() {} - public Solver(Integer[][] data, List> sequences, int bufferSize) { - setAll(data, sequences, bufferSize); + public Solver(DetectionResult detection) { + setAll(detection); } - public void setAll(Integer[][] data, List> sequences, int bufferSize) { - this.data = new Grid2D(data); - this.sequencePermutator = new SequencePermutator(sequences, bufferSize); - this.bufferSize = bufferSize; + public void setAll(DetectionResult detection) { + this.matrix = detection.matrix; + this.sequencePermutator = new SequencePermutator(detection.sequences, bufferSize); + this.bufferSize = detection.bufferSize; solution = null; } - public ArrayList getSolution() { + public ArrayList getSolution() { return solution; } public void solve() { - if (data == null || sequencePermutator == null || bufferSize == -1) + if (matrix == null || sequencePermutator == null || bufferSize == -1) return; solution = null; - ArrayDeque stack = new ArrayDeque<>(bufferSize); + ArrayDeque stack = new ArrayDeque<>(bufferSize); - for (List> sequences : sequencePermutator) { + for (List> sequences : sequencePermutator) { if (solveRecursive(stack, sequences)) { solution = new ArrayList<>(stack); break; @@ -206,14 +138,14 @@ public void solve() { } } - private static int overlapSize(List> sequences, int secondSeqIndex) { + private static int overlapSize(List> sequences, int secondSeqIndex) { if (secondSeqIndex == 0) return 0; - List seq1 = sequences.get(secondSeqIndex - 1); - List seq2 = sequences.get(secondSeqIndex); + List seq1 = sequences.get(secondSeqIndex - 1); + List seq2 = sequences.get(secondSeqIndex); int overlap = 0; - for (Integer value : seq1) { + for (OCRArrayNode value : seq1) { if (overlap == seq2.size()) // We have another value in seq1 but we're at the end of seq2; try again! overlap = 0; @@ -231,21 +163,21 @@ private static int overlapSize(List> sequences, int seco return overlap; } - private boolean solveRecursive(Deque stack, List> sequences) { + private boolean solveRecursive(Deque stack, List> sequences) { return solveRecursive(stack, sequences, 0, 0, 0, null); } private boolean solveRecursive( - Deque deque, List> sequences, - int bufferIndex, int seqIndex, int seqValueIndex, GridNode lastNode + Deque deque, List> sequences, + int bufferIndex, int seqIndex, int seqValueIndex, OCRArrayNode lastNode ) { boolean solved; - int width = data.getWidth(); - int height = data.getHeight(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); boolean horizontal = bufferIndex % 2 == 0; if (lastNode == null) - lastNode = new GridNode(0, 0, 0); - List seq = sequences.get(seqIndex); + lastNode = new OCRArrayNode(0, 0, 0, null); + List seq = sequences.get(seqIndex); while (seqValueIndex == seq.size()) { // We're at the end of this sequence; move to the next. @@ -268,13 +200,13 @@ private boolean solveRecursive( return false; for ( - GridNode node = horizontal - ? data.findInRow(lastNode.y, seq.get(seqValueIndex), 0) - : data.findInColumn(lastNode.x, seq.get(seqValueIndex), 0); + OCRArrayNode node = horizontal + ? matrix.findInRow(lastNode.y, seq.get(seqValueIndex), 0) + : matrix.findInColumn(lastNode.x, seq.get(seqValueIndex), 0); node != null; node = horizontal - ? data.findInRow(lastNode.y, seq.get(seqValueIndex), node.x + 1) - : data.findInColumn(lastNode.x, seq.get(seqValueIndex), node.y + 1) + ? matrix.findInRow(lastNode.y, seq.get(seqValueIndex), node.x + 1) + : matrix.findInColumn(lastNode.x, seq.get(seqValueIndex), node.y + 1) ) { // Iterate through all instances of value in this row or col if (deque.contains(node)) @@ -299,13 +231,13 @@ private boolean solveRecursive( // Do NOT increment seqValueIndex because we didn't actually find the // value here for ( - GridNode node = horizontal - ? data.get(0, lastNode.y) - : data.get(lastNode.x, 0); + OCRArrayNode node = horizontal + ? matrix.get(0, lastNode.y) + : matrix.get(lastNode.x, 0); horizontal ? node.x < width - 1 : node.y < height - 1; node = horizontal - ? data.get(node.x + 1, lastNode.y) - : data.get(lastNode.x, node.y + 1) + ? matrix.get(node.x + 1, lastNode.y) + : matrix.get(lastNode.x, node.y + 1) ) { // Try every row/column to get to the next required value if (bufferIndex != 0 && node == lastNode) @@ -331,21 +263,21 @@ public void print() { return; } - for (GridNode s : solution) { + for (OCRArrayNode s : solution) { System.out.println(String.format("%H (%d, %d)", s.value, s.x, s.y)); } - ArrayList solutionSorted = new ArrayList<>(solution); - solutionSorted.sort((GridNode a, GridNode b) -> { + ArrayList solutionSorted = new ArrayList<>(solution); + solutionSorted.sort((OCRArrayNode a, OCRArrayNode b) -> { if (a.y != b.y) return Integer.compare(a.y, b.y); return Integer.compare(a.x, b.x); }); int nextIdx = 0; - GridNode next = solutionSorted.get(0); - int width = data.getWidth(); - int height = data.getHeight(); + OCRArrayNode next = solutionSorted.get(0); + int width = matrix.getWidth(); + int height = matrix.getHeight(); for (int y=0; y loadConfig(String path) { return map; } - public static boolean isGridUniform(ArrayList> list) { - if (list.size() <= 1) - return true; - - int rowSize = list.get(0).size(); - for (int i=1; i> list) { - if (list.size() == 0) - return null; - - Integer[][] outArr = new Integer[list.size()][]; - for (int i=0; i> list) { - if (list.size() == 0) - return null; - - int rowLen = list.get(0).size(); - int sameSizedRows = 0; - for ( ; sameSizedRows 2) - return null; - - Integer[][] outArr = new Integer[sameSizedRows][]; - for (int i=0; i