/// <summary> /// Method for solving the sudoku in a separate thread. /// </summary> public void Reworker() { TakePicture.Instance.solvedSudoku = ArrayHandling.Solve(TakePicture.Instance.unsolvedSudoku, ref TakePicture.Instance.success); if (TakePicture.Instance.success) { Popup.queued = new KeyValuePair <string, System.Action>(SolvedMessage, null); } TakePicture.Status = TakePicture.StatusDone; //triggers a check in TakePicture.Update to refill the sudoku with the new solution print("Recalculation done"); }
public bool CheckForWin() { var ArrayHandler = new ArrayHandling <BoardType>(); for (int i = 0; i <= 2; i++) { var array = ArrayHandler.GetRow(_board, i); var item = array.FirstOrDefault(); bool match = array.Skip(1).All(x => x == item && x != BoardType.UNFILLED); if (match) { return(true); } } for (int i = 0; i <= 2; i++) { var array = ArrayHandler.GetColumn(_board, i); var item = array.FirstOrDefault(); bool match = array.Skip(1).All(x => x == item && x != BoardType.UNFILLED); if (match) { return(true); } } if (_board[0, 0] != BoardType.UNFILLED && _board[0, 0] == _board[1, 1] && _board[1, 1] == _board[2, 2]) { return(true); } if (_board[0, 2] != BoardType.UNFILLED && _board[0, 2] == _board[1, 1] && _board[1, 1] == _board[2, 0]) { return(true); } return(false); }
/// <summary> /// Refreshes the sudoku view after a change is made. /// </summary> public void RecalculateSudoku() { print("Recalculating sudoku"); TakePicture.debugText.enabled = true; TakePicture.Status = "Solving Sudoku"; Sudoku.transform.parent.gameObject.SetActive(false); TakePicture.Instance.unsolvedSudoku = new int[9, 9]; TakePicture.Instance.solvedSudoku = new int[9, 9]; SudokuTile[] sudokuTiles = Sudoku.GetComponentsInChildren <SudokuTile>(true); //Reads the values from the sudoku tiles, see TakePicture.FillSudoku int x, y; for (int i = 0; i < 81; i++) { x = (i / 3) % 3 + (i / 27) * 3; y = 8 - (i % 3 + ((i / 9) % 3) * 3); if (sudokuTiles[i].Defined) { TakePicture.Instance.unsolvedSudoku[x, y] = sudokuTiles[i].Value; } } TakePicture.Instance.allZero = ArrayHandling.OnlyZeroes(TakePicture.Instance.unsolvedSudoku); //there should not be an active work thread at this point, but if there is, shut it down if (TakePicture.Instance.workThread != null && TakePicture.Instance.workThread.IsAlive) { TakePicture.Instance.workThread.Abort(); TakePicture.Instance.workThread.Join(200); } //start reworker in a new thread TakePicture.Instance.workThread = new System.Threading.Thread(Reworker); TakePicture.Instance.workThread.Start(); }
/// <summary> /// Uses a flood fill implementation to cut out an area of adjacently connected false elements. /// </summary> /// <param name="bitmap">The bitmap in which to look.</param> /// <param name="width">The width of the bitmap.</param> /// <param name="height">The height of the bitmap.</param> /// <param name="startIndex">The index of a false element from which to start the search.</param> /// <returns>The bitmap cutout.</returns> public static bool[,] ExtractDigit(bool[] bitmap, int width, int height, int startIndex) { int x = startIndex % width, xmin = x, xmax = x, y = startIndex / width, ymin = y, ymax = y; int widthLimit = width / 9, heightLimit = height / 9; ulong next; Queue <ulong> toVisit = new Queue <ulong>(512); HashSet <ulong> visited = new HashSet <ulong>(); toVisit.Enqueue(PackULong(x, y)); while (toVisit.Count > 0) { next = toVisit.Dequeue(); if (visited.Contains(next)) { continue; } visited.Add(next); UnpackULong(next, out x, out y); if (x >= 0 && y >= 0 && x < width && y < height && !bitmap[x + y * width]) { xmax = Mathf.Max(x, xmax); xmin = Mathf.Min(x, xmin); ymax = Mathf.Max(y, ymax); ymin = Mathf.Min(y, ymin); //make sure the selected area is not a significant portion of the image - //this would most likely mean we've accidentally started selecting the wireframe of the sudoku if (xmax - xmin > widthLimit || ymax - ymin > heightLimit) { return new bool[, ] { { false } } } ; toVisit.Enqueue(PackULong(x + 1, y)); toVisit.Enqueue(PackULong(x - 1, y)); toVisit.Enqueue(PackULong(x, y + 1)); toVisit.Enqueue(PackULong(x, y - 1)); } } #if WRITE_IMAGES_TO_DISK TakePicture.colorq.Enqueue(new KeyValuePair <int, Color>(xmin + ymin * width, Color.cyan)); TakePicture.colorq.Enqueue(new KeyValuePair <int, Color>(xmax + ymax * width, Color.cyan)); #endif return(ArrayHandling.CutAndRaise(bitmap, width, height, xmin, ymin, xmax - xmin + 1, ymax - ymin + 1)); } #endregion }
private static int FailCounter = 0; //counts how many times Identify() is called with a bitmap not resembling a digit /// <summary> /// Compares the cutout at a certain index to all stored digit bitmaps, and returns the best match. /// If the digit is different enough from its best match, it is inserted into the digit bitmap array. /// </summary> /// <param name="bitmap">The bitmap image of the whole sudoku.</param> /// <param name="width">The width of the bitmap.</param> /// <param name="height">The height of the bitmap.</param> /// <param name="index">The index at which the digit was found.</param> /// <param name="digits"> /// A length 10 array of arbitrarily sized arrays of bitmaps, to be used when identifying the digits. /// This array can be modified by the function call, inserting and removing new bitmaps. /// </param> /// <param name="instantMatch">A 0-100 threshold of when two bitmaps should be considered identical.</param> /// <param name="maxBitmapsPerDigit">The maximum number of bitmaps each digit should be allowed to store.</param> /// <param name="digitout">The extracted digit bitmap, which can be inserted into identifiedBitmaps.</param> /// <returns>The digit which was found at the indicated position.</returns> public static int Identify(bool[] bitmap, int width, int height, int index, bool[][][,] digits, float instantMatch, int maxBitmapsPerDigit, ref bool[,] digitout) { #if WRITE_IMAGES_TO_DISK TakePicture.colorq.Enqueue(new KeyValuePair <int, Color>(index, Color.blue)); #endif bool[,] digit = ExtractDigit(bitmap, width, height, index); //cut out the digit as a 2d bitmap digitout = digit; TakePicture.Visualize(digit); //check if every digit is invalid, and if so, alert the user if (digit.Length <= 1 || SingleColor(digit)) { if (++FailCounter == 81) { Popup.queued = new KeyValuePair <string, System.Action>(NoSudokuMessage, null); FailCounter = 0; } MonoBehaviour.print(FailCounter); return(0); } //identify the digit by checking it against all stored bitmaps bool[][,] memoryArray; bool[,] digitStretched = new bool[0, 0], memoryStretched = new bool[0, 0]; float matchpercent, bestPercent = 0; int bestmatch = 0; #if CREATE_NEW_DEFAULT bestmatch = orderedDigits.Dequeue(); #else for (int i = 1; i <= 9; i++) { memoryArray = digits[i]; foreach (var memMap in memoryArray) { ArrayHandling.StretchToMatch(digit, memMap, out digitStretched, out memoryStretched); matchpercent = MatchPercent(digitStretched, memoryStretched); if (matchpercent >= instantMatch) { return(i); } if (matchpercent > bestPercent) { bestmatch = i; bestPercent = matchpercent; } } } #endif //save the bitmap if it is different enough bool[][,] bitmaps = digits[bestmatch] ?? new bool[0][, ]; if (bitmaps.Length < maxBitmapsPerDigit) { MonoBehaviour.print("Adding digit " + bestmatch + " to storage"); var temp = new bool[bitmaps.Length + 1][, ]; for (int i = 0; i < bitmaps.Length; i++) { temp[i] = bitmaps[i]; } temp[bitmaps.Length] = digit; digits[bestmatch] = temp; } else { MonoBehaviour.print("Replacing random stored bitmap for digit " + bestmatch); bitmaps[Mathf.FloorToInt((float)random.NextDouble() * bitmaps.Length)] = digit; } return(bestmatch); }
/// <summary> /// Finds and identifies the digits, given the bitmap image and search settings. /// </summary> /// <param name="bitmap">The bitmap for which the search coordinates were calculated.</param> /// <param name="width">The width of the bitmap.</param> /// <param name="height">The height of the bitmap.</param> /// <param name="searchCoords">An array with 81 probable digit indexes. See FindDigitCoordinates().</param> /// <param name="searchRadius"> /// The maximum distance from a calculated digit index /// to its actual match.Too high values may result in accidentally selecting /// the entire sudoku wireframe, while too low values may result in the algorithm /// not actually finding anything, assuming a zero. /// </param> /// <param name="digitBitmaps"> /// A length 10 array of arbitrarily sized arrays of bitmaps, to be used when identifying the digits. /// This array can be modified by the function call, inserting and removing new bitmaps. /// </param> /// <param name="instantMatch">A 0-100 threshold of when two bitmaps should be considered identical.</param> /// <param name="maxBitmapsPerDigit">The maximum number of bitmaps each digit should be allowed to store.</param> /// <returns>A 9x9 sudoku where 0 represents an empty tile.</returns> public static int[,] GetSudoku(bool[] bitmap, int width, int height, int[] searchCoords, float searchRadius, bool[][][,] digitBitmaps, float instantMatch, int maxBitmapsPerDigit) { int[] result = new int[81]; int x, y, xscan = 0, yscan = 0; bool breakout = false; bool[,] digit = new bool[0, 0]; identifiedBitmaps = new bool[81][, ]; //try to find a hit in the desired area for each coordinate for (int i = 0; i < 81; i++) { if (!bitmap[searchCoords[i]]) { result[i] = Identify(bitmap, width, height, searchCoords[i], digitBitmaps, instantMatch, maxBitmapsPerDigit, ref digit); identifiedBitmaps[i] = digit; continue; } x = searchCoords[i] % width; y = searchCoords[i] / width; //Search in an octagonal pattern breakout = false; for (int j = 1; j <= searchRadius && !breakout; j++) { for (int k = 0; k < 8; k++) { switch (k) { case 0: xscan = x + j; yscan = y; break; case 1: xscan = x; yscan = y + j; break; case 2: xscan = x - j; yscan = y; break; case 3: xscan = x; yscan = y - j; break; case 4: xscan = x + j; yscan = y - j; break; case 5: xscan = x + j; yscan = y + j; break; case 6: xscan = x - j; yscan = y + j; break; case 7: xscan = x - j; yscan = y - j; break; } if (xscan >= 0 && xscan < width && yscan >= 0 && yscan < height) { if (!bitmap[xscan + yscan * width]) { result[i] = Identify(bitmap, width, height, xscan + yscan * width, digitBitmaps, instantMatch, maxBitmapsPerDigit, ref digit); identifiedBitmaps[i] = digit; breakout = true; break; } } } } } FailCounter = 0; return(ArrayHandling.Raise(result, 9, 9)); }
/// <summary> /// The main working method. Handles the flow from creating a bitmap /// from the scanned image to analysing its digits and solving the sudoku. /// </summary> /// <param name="pixels">The scanned image as a one-dimensional array of Colors.</param> public void Worker(Color[] pixels) { try { var timer = System.Diagnostics.Stopwatch.StartNew(); //cut out the sudoku part pixels = ArrayHandling.Cut(pixels, textureWidth, textureHeight, (textureWidth - side) / 2, (textureHeight - side) / 2, side, side); Status = "Creating Bitmap"; //the current status is displayed while the background thread is active bool[] bits = Bitify(pixels, side, side, HorizontalChunks, VerticalChunks); //generate a "black-and-white" bitmap from the picture int[] coords; int searchRadius; Status = "Finding Corners"; int[][] corners = OCR.FindCorners2(bits, side, side, CornerQueueLength); //find the corners of the sudoku if (corners == null) { Popup.queued = new KeyValuePair <string, System.Action>(OCR.NoSudokuMessage, null); corners = new int[][] { new int[] { 0, 0 }, new int[] { 0, 0 }, new int[] { 0, 0 }, new int[] { 0, 0 } }; } #if WRITE_IMAGES_TO_DISK foreach (int[] corner in corners) { colorq.Enqueue(new KeyValuePair <int, Color>(corner[0] + corner[1] * side, Color.yellow)); } #endif searchRadius = Mathf.RoundToInt(SearchRadiusFactor * Mathf.Sqrt( Mathf.Pow((corners[2][0] - corners[0][0]) / 9f, 2) + Mathf.Pow((corners[2][1] - corners[0][1]) / 9f, 2) )); Status = "Finding Digits"; coords = OCR.FindDigitCoordinates(corners, side); //calculate where the digits should be Status = "Identifying Digits"; unsolvedSudoku = OCR.GetSudoku(bits, side, side, coords, searchRadius, storedBitmaps, InstantMatchPercent, MaxStoredBitmapsPerDigit); //identify all digits allZero = ArrayHandling.OnlyZeroes(unsolvedSudoku); var sb = new System.Text.StringBuilder(); for (int y = 8; y >= 0; y--) { for (int x = 0; x < 9; x++) { sb.Append(unsolvedSudoku[x, y]); } sb.AppendLine(); } print(sb); Status = "Solving Sudoku"; solvedSudoku = ArrayHandling.Solve(unsolvedSudoku, ref success); //solve the sudoku if (!success) { Popup.queued = new KeyValuePair <string, System.Action>(CannotSolveMessage, null); } sb = new System.Text.StringBuilder(); for (int y = 8; y >= 0; y--) { for (int x = 0; x < 9; x++) { sb.Append(solvedSudoku[x, y]); } sb.AppendLine(); } print(sb); #if WRITE_IMAGES_TO_DISK Status = "Rendering Debug Picture"; for (int i = 0; i < pixels.Length; i++) { pixels[i] = bits[i] ? Color.white : Color.black; } while (colorq.Count > 0) { var kvp = colorq.Dequeue(); if (kvp.Key >= 0 && kvp.Key < pixels.Length) { pixels[kvp.Key] = kvp.Value; } } #endif debugPicture = pixels; timer.Stop(); print("Done"); print("Time: " + timer.ElapsedMilliseconds + " ms"); Status = StatusDone; //signal to the main thread that the solved sudoku is availible } catch (System.Exception e) { print(e); Status = StatusError + e.Message; } }
/// <summary> /// Main program loop. /// </summary> void Update() { if (!taken) { if ((TakePictureNow && texture != null && texture.isPlaying) || Input.GetKey(KeyCode.Return) || Input.GetKey(KeyCode.Space)) { //grab the image data from the WebCamTexture taken = true; original = texture.GetPixels(); Color[] pixels = new Color[original.Length]; System.Array.Copy(original, pixels, original.Length); texture.Stop(); print(string.Format("Took {0}x{1} picture", texture.width, texture.height)); //refresh the settings and rotate the texture if it is needed Status = "Loading Settings"; SettingManager.LoadSettings(); ArrayHandling.RotateTexture(ref pixels, texture.width, texture.height, texture.videoRotationAngle); //start a new worker thread debugText.enabled = true; if (workThread != null && workThread.IsAlive) { workThread.Abort(); workThread.Join(200); } workThread = new System.Threading.Thread(() => Worker(pixels)); workThread.Start(); PicturePanel.SetActive(false); CameraUI.transform.parent.gameObject.SetActive(false); } } //check if the worker thread has completed its tasks else if (Status == StatusDone) //this works, but an enum would probably be better suited for this kind of check { Status = ""; debugText.enabled = false; FillSudoku(solvedSudoku, unsolvedSudoku); print("Displayed the result"); #if WRITE_IMAGES_TO_DISK Texture2D originalTexture = new Texture2D(texture.width, texture.height), processedTexture = new Texture2D(side, side); originalTexture.filterMode = FilterMode.Point; originalTexture.SetPixels(original); originalTexture.Apply(); processedTexture.filterMode = FilterMode.Point; processedTexture.SetPixels(debugPicture); processedTexture.Apply(); File.WriteAllBytes(Path.Combine(BitmapEncoding.PersistentPath, "Output.png"), processedTexture.EncodeToPNG()); File.WriteAllBytes(Path.Combine(BitmapEncoding.PersistentPath, "Input.png"), originalTexture.EncodeToPNG()); print("Saved images to " + BitmapEncoding.PersistentPath); #endif } //update explanatory text if it is needed if (debugText.enabled && Status != oldStatus) { if (Status.Contains(StatusError)) { debugText.text = Status; } else { debugText.text = (Status.Length != 0 ? "Please Wait" + System.Environment.NewLine + Status : ""); } oldStatus = Status; } //trigger popup if needed if (Popup.queued != null) { Popup.ActivatePopup(Popup.queued.Value.Key, Popup.queued.Value.Value); Popup.queued = null; } //if the user presses escape (back button on Android), go back/abort calculation/exit the app if (Input.GetKeyDown(KeyCode.Escape)) { var eb = PicturePanel.GetComponentsInChildren <ExtraButtons>(true)[0]; if (PicturePanel.activeInHierarchy || (debugText.enabled && workThread != null && workThread.IsAlive)) { Application.Quit(); } else if (infoPopup.gameObject.activeInHierarchy) { infoPopup.Close(); } else if (QuestionPopup.Instance.gameObject.activeInHierarchy) { QuestionPopup.Instance.Close(false); } else if (digitPrompt.gameObject.activeInHierarchy) { digitPrompt.Choose(-1); } else if (SudokuPanel.activeInHierarchy) { Reset(false); } else if (SettingManager.AdvancedSettingsPanel != null && SettingManager.AdvancedSettingsPanel.activeInHierarchy) { eb.DirectlyFromAdvanced(); } else if (eb.HelpPanel != null && eb.HelpPanel.activeInHierarchy) { eb.ReturnFromHelp(); } } }