From 556d885614c466f4129386d84cf759041b252de4 Mon Sep 17 00:00:00 2001 From: Dor-sketch Date: Wed, 26 Jun 2024 20:37:40 +0300 Subject: [PATCH] Major performance upgrade - Using workers for efficient computation by offloading the heavy lifting to a separate thread - Improved preprocessing of the input data by computing only non-empty cells --- README.md | 3 + docs/css/styles.css | 4 +- docs/index.html | 672 ++++++++++++++++++++++++++++++++++++-------- docs/js/parser.js | 96 ------- docs/js/sudoku.js | 277 ------------------ docs/worker.js | 55 ++++ 6 files changed, 612 insertions(+), 495 deletions(-) delete mode 100644 docs/js/parser.js delete mode 100644 docs/js/sudoku.js create mode 100644 docs/worker.js diff --git a/README.md b/README.md index 9c9ad27..448d731 100644 --- a/README.md +++ b/README.md @@ -324,3 +324,6 @@ Any contributions you make are greatly appreciated. ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +............5.3.3...84.12...59...8..........6.23...57...53.7....1. +............5.3.3...84.12...59...8..........6.23...57...53.7....1.....9.....1... \ No newline at end of file diff --git a/docs/css/styles.css b/docs/css/styles.css index f645ba8..2ab395f 100644 --- a/docs/css/styles.css +++ b/docs/css/styles.css @@ -22,9 +22,9 @@ padding: 1rem; width: 70%; height: auto; } -#canvas { +/* #canvas { display: hidden; -} +} */ .parcer { display: block; margin: 0 auto; diff --git a/docs/index.html b/docs/index.html index 2e4b724..1bb281a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -24,31 +24,29 @@ - - +
-
- - - - +
+ + +
-

Built By Dor ©
- dorpascal.com +

Built By Dor © +
+ dorpascal.com

-
@@ -56,19 +54,15 @@
-
- Image to crop will be shown here - -

-        
-
Please crop the Sudoku board and click confirm
- Processed Image will be shown here -
-
- -
- -
+
+ +

+            
+
Please crop the Sudoku board and click confirm
+
+
+
+

Welcome to the Sudoku Solver!

@@ -120,52 +114,52 @@
- - - + diff --git a/docs/js/parser.js b/docs/js/parser.js deleted file mode 100644 index d605ee4..0000000 --- a/docs/js/parser.js +++ /dev/null @@ -1,96 +0,0 @@ - - -const fileInput = document.getElementById('fileInput'); -const canvas = document.getElementById('canvas'); -const ctx = canvas.getContext('2d'); -const inputBoard = document.getElementById('input-board'); -const processedImageElement = document.getElementById('processedImage'); -function processImage(src) { - return new Promise((resolve, reject) => { - if (!src) { - alert("Please upload an image first."); - reject(new Error("No image source provided.")); - return; - } - - const img = new Image(); - img.onload = async function () { - // Resize the image to make processing faster - const MAX_WIDTH = 500; - const scale = MAX_WIDTH / img.width; - const scaledHeight = img.height * scale; - - canvas.width = MAX_WIDTH; - canvas.height = scaledHeight; - ctx.drawImage(img, 0, 0, MAX_WIDTH, scaledHeight); - - // Binarize the image - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const data = imageData.data; - for (let i = 0; i < data.length; i += 4) { - const avg = data[i]; // Since the image is greyscale, we can just use the red channel - data[i] = avg < 128 ? 0 : 255; - data[i + 1] = data[i]; - data[i + 2] = data[i]; - } - ctx.putImageData(imageData, 0, 0); - const processedImageSrc = canvas.toDataURL(); - processedImageElement.src = processedImageSrc; - - try { - const boardStr = await extractSudokuBoard(ctx, canvas.width, canvas.height); - console.log(boardStr); - inputBoard.value = boardStr; - - if (boardStr.length !== 81) { - resultElement.value += "\n\nFailed to parse Sudoku board. Please try again with a clearer image."; - } - resolve(); // Resolve the promise after all processing is done - } catch (error) { - reject(error); // Reject the promise if there's an error - } - }; - img.src = src; - }); -} -function progressCallback(progress) { - document.querySelector('.processingMessage').textContent = `Extraction progress: ${progress.toFixed(2)}%`; -} -async function extractSudokuBoard(ctx, width, height) { - const cellWidth = width / 9; - const cellHeight = height / 9; - let boardStr = ''; - const totalCells = 9 * 9; // Total number of cells in a Sudoku board - - for (let row = 0; row < 9; row++) { - for (let col = 0; col < 9; col++) { - const cellCanvas = document.createElement('canvas'); - const cellCtx = cellCanvas.getContext('2d'); - cellCanvas.width = cellWidth; - cellCanvas.height = cellHeight; - cellCtx.drawImage(ctx.canvas, col * cellWidth, row * cellHeight, cellWidth, cellHeight, 0, 0, cellWidth, cellHeight); - - const cellImageSrc = cellCanvas.toDataURL(); - const { data: { text } } = await Tesseract.recognize(cellImageSrc, 'eng', { - tessedit_char_whitelist: '123456789', - tessedit_pageseg_mode: Tesseract.PSM.SINGLE_CHAR - }); - - const digit = text.trim(); - boardStr += digit.length === 1 && '123456789'.includes(digit) ? digit : '.'; - - // Calculate and report progress - const cellsProcessed = row * 9 + col + 1; // Calculate the number of cells processed so far - const progressPercent = (cellsProcessed / totalCells) * 100; - progressCallback(progressPercent); - } - } - - return boardStr; -} - -extractSudokuBoard(ctx, width, height, (progress) => { - console.log(`Extraction progress: ${progress}%`); -}).then((boardStr) => { - console.log('Sudoku board extracted:', boardStr); -}); \ No newline at end of file diff --git a/docs/js/sudoku.js b/docs/js/sudoku.js deleted file mode 100644 index 7810c54..0000000 --- a/docs/js/sudoku.js +++ /dev/null @@ -1,277 +0,0 @@ - -const sudokuBoard = document.getElementById('sudoku-board'); - -// Initialize empty Sudoku board -const board = Array.from({ length: 9 }, () => Array(9).fill('.')); - -// Create the grid elements -for (let row = 0; row < 9; row++) { - for (let col = 0; col < 9; col++) { - const cell = document.createElement('div'); - cell.className = 'cell'; - cell.contentEditable = true; - cell.dataset.row = row; - cell.dataset.col = col; - - // Add event listener for handling input - cell.addEventListener('input', function (e) { - const value = e.target.textContent.trim(); - if (value.match(/[1-9]/)) { // If a number between 1 and 9 is entered - e.target.textContent = value; // Set the cell's content to the entered number - moveToNextCell(row, col); // Move to the next cell - } else { - e.target.textContent = ''; // Clear the cell if the input is not a number - } - synchronizeInputBoard(); - }); - // Add event listener for handling keydown - cell.addEventListener('keydown', function (e) { - // Check if the key pressed is a number or navigation/functional key - if ((e.key >= '0' && e.key <= '9') || e.key === 'Backspace' || e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); // Prevent the default action - - if (e.key === 'Backspace') { - e.target.textContent = ''; // Clear the cell - moveToPreviousCell(row, col); // Move to the previous cell - } else if (e.key === ' ' || e.key === 'Enter') { - e.target.textContent = ''; // Clear the cell - moveToNextCell(row, col); // Move to the next cell - } else { - // Ensure the cell is empty before setting a new number - e.target.textContent = ''; // Clear existing content - e.target.textContent = e.key; // Set new number - moveToNextCell(row, col); // Then move to the next cell - } - - synchronizeInputBoard(); - } - }); - sudokuBoard.appendChild(cell); - } -} - -function moveToPreviousCell(row, col) { - let prevCol = col - 1; - let prevRow = row; - if (prevCol < 0) { // If the current cell is the first in its row, move to the last cell of the previous row - prevCol = 8; - prevRow--; - } - if (prevRow >= 0) { // If the previous row exists - const prevCell = document.querySelector(`.cell[data-row='${prevRow}'][data-col='${prevCol}']`); - if (prevCell) { - prevCell.focus(); // Move focus to the previous cell - } - } -} - -// Function to move focus to the next cell, skipping cells that already contain a number -function moveToNextCell(row, col) { - let nextCol = col; - let nextRow = row; - - do { - nextCol++; - if (nextCol > 8) { // If the current cell is the last in its row, move to the first cell of the next row - nextCol = 0; - nextRow++; - } - if (nextRow >= 9) { // If there are no more rows, stop the loop - return; - } - - var nextCell = document.querySelector(`.cell[data-row='${nextRow}'][data-col='${nextCol}']`); - } while (nextCell && nextCell.value); // Continue if the next cell exists and has a value - - if (nextCell) { - nextCell.focus(); // Move focus to the next empty cell - } -} - -// Function to synchronize the input board with the grid -function synchronizeInputBoard() { - let boardStr = ''; - for (let row = 0; row < 9; row++) { - for (let col = 0; col < 9; col++) { - const cell = document.querySelector(`.cell[data-row='${row}'][data-col='${col}']`); - boardStr += cell.textContent.trim() || '.'; - } - } - inputBoard.value = boardStr; -} - -// Synchronize text input with board cells -inputBoard.addEventListener('input', function (e) { - const value = e.target.value.trim(); - for (let i = 0; i < 81; i++) { - const row = Math.floor(i / 9); - const col = i % 9; - const cell = document.querySelector(`.cell[data-row='${row}'][data-col='${col}']`); - cell.textContent = (value[i] && value[i].match(/[1-9]/)) ? value[i] : ''; - } -}); - -// Function to convert 2D array to board string -function arrayToBoardString(array) { - // change 0 to . if there is any - array = array.map(row => row.map(cell => cell === 0 ? '.' : cell)); - return array.flat().join(''); -} - -// Event listener for pasting on the sudoku-board -sudokuBoard.addEventListener('paste', function (e) { - e.preventDefault(); // Prevent the default paste action - const pastedText = (e.clipboardData || window.clipboardData).getData('text'); - console.log('Pasted on sudoku-board:', pastedText); - distributeTextAcrossCells(pastedText, 'sudoku-board'); // Assuming this function handles the distribution of text across cells for the sudoku board -}); - -// Placeholder for the function to get the current cell's position -function getCurrentCellPosition() { - const cell = document.activeElement; - return { - row: Number(cell.dataset.row), - col: Number(cell.dataset.col) - }; -} - -// Placeholder for the function to set the value of a cell -function setCellValue(row, col, value) { - const cell = document.querySelector(`.cell[data-row='${row}'][data-col='${col}']`); - cell.textContent = value; - // update the input board - synchronizeInputBoard(); -} - -function distributeTextAcrossCells(text, target) { - const splitText = text.split(''); - let { row, col } = getCurrentCellPosition(); - for (let i = 0; i < splitText.length; i++) { - setCellValue(row, col, splitText[i]); - if (target === 'sudoku-board') { - moveToNextCell(row, col); - ({ row, col } = getCurrentCellPosition()); - } - } -} - -inputBoard.addEventListener('paste', function (e) { - e.preventDefault(); - const pastedText = (e.clipboardData || window.clipboardData).getData('text'); - try { - let parsedArray; - // Check if pastedText contains "." - const containsDot = pastedText.includes('.'); - - // Attempt to parse as JSON first - try { - parsedArray = JSON.parse(pastedText); - } catch { - // If JSON parsing fails, process as a plain string - if (pastedText.length === 81) { // Ensure it's the right length for a Sudoku board - parsedArray = []; - for (let i = 0; i < 9; i++) { - let row; - if (containsDot) { - // If pastedText contains ".", split directly without converting "0" to "." - row = pastedText.slice(i * 9, (i + 1) * 9).split('').map(cell => cell === '.' ? 0 : Number(cell)); - } else { - // If pastedText does not contain ".", proceed as before - row = pastedText.slice(i * 9, (i + 1) * 9).split('').map(Number); - } - parsedArray.push(row); - } - } - } - // Validate the parsed array - if (Array.isArray(parsedArray) && parsedArray.length === 9 && parsedArray.every(row => Array.isArray(row) && row.length === 9)) { - const boardString = arrayToBoardString(parsedArray); - inputBoard.value = boardString; - updateBoard(boardString); - } - // check if cant trim white spaces and commas - else if (pastedText.length === 81) { - const boardString = pastedText.replace(/[^0-9]/g, ''); - inputBoard.value = boardString; - updateBoard(boardString); - } - - else { - console.error('Pasted content is not a valid Sudoku board.'); - } - } catch (error) { - console.error('Error parsing pasted content:', error); - } -}); - -// Function to read board from DOM -function readBoard() { - return inputBoard.value.trim(); -} - -// Function to update board in DOM -function updateBoard(boardStr) { - let index = 0; - for (let row = 0; row < 9; row++) { - for (let col = 0; col < 9; col++) { - const cell = document.querySelector(`.cell[data-row='${row}'][data-col='${col}']`); - cell.textContent = boardStr[index] === '.' ? '' : boardStr[index]; - index++; - } - } -} - -// Load the WebAssembly module -var Module = { - onRuntimeInitialized: () => { - console.log('WASM module loaded'); - } -}; - -function isValidPlacement(boardStr, row, col, num) { - for (let i = 0; i < 9; i++) { - if (boardStr[row * 9 + i] === num || boardStr[i * 9 + col] === num) { - return false; - } - } - const startRow = Math.floor(row / 3) * 3; - const startCol = Math.floor(col / 3) * 3; - for (let i = startRow; i < startRow + 3; i++) { - for (let j = startCol; j < startCol + 3; j++) { - if (boardStr[i * 9 + j] === num) { - return false; - } - } - } - return true; -} - -function isValidSudoku(boardStr) { - for (let i = 0; i < 9; i++) { - for (let j = 0; j < 9; j++) { - if (boardStr[i * 9 + j] !== '.') { - const num = boardStr[i * 9 + j]; - boardStr = boardStr.substr(0, i * 9 + j) + '.' + boardStr.substr(i * 9 + j + 1); - if (!isValidPlacement(boardStr, i, j, num)) { - return false; - } - boardStr = boardStr.substr(0, i * 9 + j) + num + boardStr.substr(i * 9 + j + 1); - } - } - } - return true; -} - - - -function celebrate() { - const cells = document.querySelectorAll('.cell'); - cells.forEach(cell => { - cell.style.backgroundColor = "#8E8E93"; // Adjusted to a lighter Apple-esque gray - }); - setTimeout(() => { - cells.forEach(cell => { - cell.style.backgroundColor = ''; - }); - }, 2000); -} diff --git a/docs/worker.js b/docs/worker.js new file mode 100644 index 0000000..575248a --- /dev/null +++ b/docs/worker.js @@ -0,0 +1,55 @@ +importScripts('https://cdn.jsdelivr.net/npm/tesseract.js@4/dist/tesseract.min.js'); + +self.onmessage = async function(e) { + const { width, height, imageData, row, col } = e.data; + const result = await extractSudokuNumber(width, height, imageData); + self.postMessage({ result }); +}; +async function extractSudokuNumber(cellWidth, cellHeight, imageData) { + const cellImageData = new ImageData(new Uint8ClampedArray(imageData), cellWidth, cellHeight); + + // Check for black pixels in the center of the cell + if (!hasBlackPixelsInCenter(cellImageData)) { + return '.'; // Return '.' immediately if no black pixels are found + } + + // Convert ImageData to Blob as Tesseract.js can work with Blob for recognition + const blob = await new Promise(resolve => { + const canvas = new OffscreenCanvas(cellWidth, cellHeight); + const ctx = canvas.getContext('2d'); + ctx.putImageData(cellImageData, 0, 0); + canvas.convertToBlob().then(resolve); + }); + + const cellImageSrc = URL.createObjectURL(blob); + + const { data: { text } } = await Tesseract.recognize(cellImageSrc, 'eng', { + tessedit_char_whitelist: '123456789', + tessedit_pageseg_mode: Tesseract.PSM.SINGLE_CHAR + }); + + URL.revokeObjectURL(cellImageSrc); + + const digit = text.trim(); + const validDigit = digit.length === 1 && '123456789'.includes(digit) ? digit : '.'; + console.log('Extracted Digit:', validDigit); + return validDigit; +} + +function hasBlackPixelsInCenter(imageData) { + const { width, height, data } = imageData; + const centerX = Math.floor(width / 2); + const centerY = Math.floor(height / 2); + const radius = Math.floor(Math.min(width, height) / 4); // Check a central square of the cell + + for (let y = centerY - radius; y <= centerY + radius; y++) { + for (let x = centerX - radius; x <= centerX + radius; x++) { + const index = (y * width + x) * 4; + if (data[index] === 0 && data[index + 1] === 0 && data[index + 2] === 0 && data[index + 3] === 255) { + // Found a black pixel (RGBA) + return true; + } + } + } + return false; // No black pixels found +} \ No newline at end of file