package cx.prutser.sudoku.ocr; import cx.prutser.sudoku.solver.ClassicSolver; import cx.prutser.sudoku.solver.ClassicSudokuUtils; import cx.prutser.sudoku.solver.SolutionsCollector; import cx.prutser.sudoku.solver.SolverContext; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * @author Erik van Zijst */ public class GraphicalSolver { private final static String CONFIG_FILENAME = "config.net"; private final SudokuDigitRecognizer ocr; public GraphicalSolver() { InputStream in = this .getClass() .getClassLoader() .getResourceAsStream(CONFIG_FILENAME); if (in == null) { throw new RuntimeException("Could not find neural network " + "configuration in classpath (" + CONFIG_FILENAME + ")."); } else { try { ocr = new SudokuDigitRecognizer(in); } catch (IOException e) { throw new RuntimeException("Error reading neural network: " + e.getMessage(), e); } finally { try { in.close(); } catch (IOException e) {} } } } /** * The supplied image must exactly contain the puzzle and be square in size. * * @param image * @return the same image, with the missing digits superimposed on the empty * tiles. * @throws IllegalArgumentException when the image is not squared. */ public BufferedImage solve(BufferedImage image, long timeout) throws IllegalArgumentException { if (image.getWidth() != image.getHeight()) { throw new IllegalArgumentException(String.format( "The supplied image must be a square; not %dx%d", image.getWidth(), image.getHeight())); } else { final Integer[] board = readTiles(image); System.out.println(ClassicSudokuUtils.format(board)); final Integer[] solution = solve(board, timeout); if (solution == null) { System.out.println("Puzzle cannot be solved."); } else { System.out.println(String.format( "Solution found :\n%s", ClassicSudokuUtils.format(solution))); // cut out the numbers that were in the puzzle, so we only burn the missing ones for (int i = 0; i < board.length; i++) { if (board[i] != null) { solution[i] = null; } } image = burnSolution(image, solution); } return image; } } /** * Reads the puzzle's photo, extracts the numerical values and returns them * as a board array. Tiles that are not recognized properly are left as * blank tiles. * * @param image * @return */ private Integer[] readTiles(BufferedImage image) { final Integer[] board = new Integer[81]; final List<BufferedImage> tiles = new AdaptiveThresholdingExtractor( new LoggingTileExtractor( new SimpleTileExtractor(), "snapshots")) .extractTiles(image); for (int index = 0; index < tiles.size(); index++) { int digit = ocr.testAndClassify(OCRUtils.pixelsToPattern(OCRUtils.getPixels(tiles.get(index)))); board[index] = digit <= 0 ? null : digit; if (digit < 0) { System.err.println(String.format("Warning: tile %d not recognized!", index)); } } return board; } /** * Solves the sudoku using a brute-force algorithm. Only returns the first * solution found. Returns <code>null</code> if no solution could be found. * * @param board * @return */ private Integer[] solve(Integer[] board, long timeout) { final Integer[] solution = new Integer[81]; final AtomicBoolean unsolvable = new AtomicBoolean(false); // the glass is half full new ClassicSolver(board, timeout).solve(new SolutionsCollector<Integer>() { public void newSolution(Integer[] sol, SolverContext ctx) { System.arraycopy(sol, 0, solution, 0, sol.length); ctx.cancel(); } public void searchComplete(long evaluations) { unsolvable.set(true); } public void timeoutExceeded(long millis) { System.out.println("Could not solve within " + millis + "ms"); unsolvable.set(true); } }); return unsolvable.get() ? null : solution; } /** * This method modifies its parameter. * * @param image * @return */ private BufferedImage burnSolution(BufferedImage image, Integer[] solution) { final int TILE_SIZE = image.getWidth() / 9; final int FONT_HEIGHT = (int)(TILE_SIZE * 0.8); final int FONT_WIDTH = (int)(FONT_HEIGHT * 0.75); // approximate Graphics2D g2 = image.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setFont(new Font("lucida sans unicode", Font.PLAIN, FONT_HEIGHT)); g2.setColor(Color.BLUE); for (int i = 0; i < solution.length; i++) { if (solution[i] != null) { int x = TILE_SIZE * (i % 9) + (TILE_SIZE - FONT_WIDTH) / 2; int y = TILE_SIZE * (i / 9) + (TILE_SIZE - FONT_HEIGHT) / 2 + FONT_HEIGHT; g2.drawString(String.valueOf(solution[i]), x, y); } } g2.dispose(); return image; } }