Skip to content

Commit

Permalink
Sudoku Release 1.8: simplified algorithm implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rladstaetter committed Jun 12, 2016
1 parent 51ba720 commit 8e6f583
Show file tree
Hide file tree
Showing 17 changed files with 74 additions and 57 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -60,7 +60,7 @@
<plugin>
<groupId>com.simpligility.maven.plugins</groupId>
<artifactId>android-maven-plugin</artifactId>
<version>4.3.0</version>
<version>4.4.1</version>
<extensions>true</extensions>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class SudokuCapturer extends Activity with CvCameraViewListener2 with CanLog {
var currState: SudokuState = AndroidOpenCV.DefaultAndroidState

def initAssets(): Unit = {
AndroidOpenCV.init()
TemplateLibrary.getResourceAsStream = getAssets().open
TemplateLibrary.templateResource = "templates.csv"
}
Expand Down Expand Up @@ -142,9 +143,16 @@ class SudokuCapturer extends Activity with CvCameraViewListener2 with CanLog {
val frame = inputFrame.rgba()
frameNr = frameNr + 1

val (sudokuResult, nextState) = Await.result(SCandidate(frameNr, frame, currState, SParams()).calc, Duration.Inf)
currState = nextState
sudokuResult
val pipeline: FramePipeline = FramePipeline(frame, SParams())

pipeline.detectRectangle match {
case None => SFailure("No rectangle detected",SCandidate(frameNr,pipeline, SRectangle(frame, pipeline.corners,pipeline.corners),currState))
case Some(r) =>
val rectangle: SRectangle = SRectangle(pipeline)
val (sudokuResult, nextState) = Await.result(SCandidate(frameNr, pipeline, rectangle, currState).calc, Duration.Inf)
currState = nextState
sudokuResult
}
}

def execOnUIThread(f: => Unit): Unit = {
Expand Down
5 changes: 2 additions & 3 deletions sudoku-core/src/main/scala/net/ladstatt/opencv/OpenCV.scala
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,8 @@ object OpenCV extends CanLog {
* @param pattern
* @return
*/
def copySrcToDestWithMask(source: Mat, destination: Mat, pattern: Mat): Future[Mat] =
Future {
source.copyTo(destination, pattern)
def copySrcToDestWithMask(source: Mat, destination: Mat, pattern: Mat): Mat = {
source.copyTo(destination, pattern)
destination
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ object FramePipeline {

import net.ladstatt.opencv.OpenCV._

def apply(frame: Mat, params: SParams): FramePipeline = {
def apply(frame: Mat, params: SParams = SParams()): FramePipeline = {
val start = System.nanoTime()
Await.result(for {
working <- copySrcToDestWithMask(frame, new Mat, frame)
working <- Future(copySrcToDestWithMask(frame, new Mat, frame))
grayed <- toGray(working)
blurred <- gaussianblur(grayed)
thresholdApplied <- adaptiveThreshold(blurred)
inverted <- bitwiseNot(thresholdApplied)
dilated <- dilate(inverted, OpenCV.Kernel)
eroded <- erode(inverted)
corners: MatOfPoint2f = OpenCV.mkCorners(frame.size)
} yield FramePipeline(start, frame, working, grayed, blurred, thresholdApplied, inverted, dilated, eroded, corners, params), Duration.Inf)
} yield FramePipeline(start, frame, working, grayed, blurred, thresholdApplied, inverted, dilated, eroded, params), Duration.Inf)
}

}
Expand All @@ -42,9 +41,9 @@ case class FramePipeline(start: Long,
thresholded: Mat,
inverted: Mat,
dilated: Mat, eroded: Mat,
corners: MatOfPoint2f,
params: SParams) extends SResult {

val corners = OpenCV.mkCorners(frame.size)
/**
* a sequence of point lists which give the recognized contour lines
*/
Expand Down
13 changes: 11 additions & 2 deletions sudoku-core/src/main/scala/net/ladstatt/sudoku/Parameters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package net.ladstatt.sudoku
import net.ladstatt.opencv.OpenCV._
import org.opencv.core.Mat

import scala.concurrent.Future


object SudokuState {

Expand Down Expand Up @@ -49,6 +51,9 @@ case class SudokuState(hitCounts: HitCounters,

val detections: Int = hitCounts.values.flatMap(filterHits(_, cap)).size

def merge(sRectangle: SRectangle) : SudokuState = {
merge(sRectangle.normalized, sRectangle.cells, sRectangle.cellValues)
}
def merge(normalized: Mat,
detectedCells: Seq[SCell],
detectedCellValues: Seq[Int]): SudokuState = {
Expand Down Expand Up @@ -82,16 +87,20 @@ object Parameters {

// least number of matches necessary to identify one number
// if you have a good camera, take 1 to get fast response
val cap = 30
val cap = 3
//val cap = 30

// number of different values a cell can have before the cell is label 'ambiguous'
val ambiguitiesCount = 5
//val ambiguitiesCount = 5

// how many cells are allowed to have ambiguous information before number detection process is restarted
val ambiCount = 5
//val ambiCount = 5

// numbers won't get any larger in the status matrix than this number
val topCap = 50
val topCap =5
//val topCap = 50


assert(topCap - cap > 0)
Expand Down
14 changes: 5 additions & 9 deletions sudoku-core/src/main/scala/net/ladstatt/sudoku/SCandidate.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package net.ladstatt.sudoku

import net.ladstatt.core.CanLog
import net.ladstatt.opencv.{Debug, OpenCV}
import org.opencv.core._

Expand Down Expand Up @@ -43,18 +42,15 @@ case class SCandidate(nr: Int,
* This function uses an input image and a detection method to calculate the sudoku.
*/
lazy val calc: Future[(SudokuResult, SudokuState)] = {
val currentState = oldState.merge(sRectangle)
for {
detectedCells <- Future(sRectangle.cells)
detectedCellValues = sRectangle.cells.map(_.value)
currentState = oldState.merge(sRectangle.normalized, detectedCells, detectedCellValues)
solvedState <- Future(currentState.solve())
withSolution <- sRectangle.paintSolution(detectedCellValues, solvedState.someCells, currentState.library)
annotatedSolution <- SudokuUtils.paintCorners(withSolution, sRectangle.cellRois, solvedState.someCells, currentState.hitCounts, oldState.cap)
withSolution <- Future(sRectangle.paintSolution(solvedState.someCells, currentState.library))
annotatedSolution <- Future(SudokuUtils.paintCorners(withSolution, sRectangle.cellRois, solvedState.someCells, currentState.hitCounts, oldState.cap))
warped = OpenCV.warp(annotatedSolution, pipeline.corners, sRectangle.detectedCorners)
solutionMat <- OpenCV.copySrcToDestWithMask(warped, pipeline.frame, warped) // copy solution mat to input mat
sudokuFrame = SudokuFrame(sRectangle.normalized, detectedCells.toArray, sRectangle.detectedCorners.toList.toList)
solutionMat <- Future(OpenCV.copySrcToDestWithMask(warped, pipeline.frame, warped)) // copy solution mat to input mat
} yield {
(SSuccess(this, sudokuFrame, solvedState.someResult.map(s => SolutionFrame(s, solutionMat))), currentState)
(SSuccess(this, sRectangle, solvedState.someResult.map(s => SolutionFrame(s, solutionMat))), currentState)
}
}

Expand Down
1 change: 1 addition & 0 deletions sudoku-core/src/main/scala/net/ladstatt/sudoku/SCell.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import scala.concurrent.{Await, Future}
* @param roi
*/
case class SCell(cellMat: Mat, roi: Rect) {

val contour: Option[Mat] = Await.result(extractContour(cellMat), Duration.Inf)

val (value, quality) = Await.result(contour.map(TemplateLibrary.detectNumber).getOrElse(Future.successful((0, 0.0))),Duration.Inf)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object SParams {
def apply(): SParams = {
SParams(Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE, 30)
}

}


Expand Down
45 changes: 25 additions & 20 deletions sudoku-core/src/main/scala/net/ladstatt/sudoku/SRectangle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,38 @@ import net.ladstatt.opencv.OpenCV
import net.ladstatt.opencv.OpenCV._
import org.opencv.core._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.collection.JavaConversions ._

object SRectangle {

def apply(pipeline: FramePipeline) : SRectangle = {
SRectangle(pipeline.frame, pipeline.detectRectangle.get,pipeline.corners)
}
}
/**
* Created by lad on 01.05.16.
*/
case class SRectangle(frame: Mat, detectedCorners: MatOfPoint2f, destCorners: MatOfPoint2f) {

//val analysisCorners = OpenCV.mkCorners(TemplateLibrary.templateCanvasSize)
/**
* This mat contains an 'unstretched' version of the detected sudoku outer rectangle.
*
* In this representation it is easier to paint upon. After painting this Mat will be retransformed
* to the original appearance again.
*/
val normalized: Mat = OpenCV.warp(frame, detectedCorners, destCorners)

/**
* the cellRois denote the region of interests for every sudoku cell (there are 81 of them for every sudoku)
*/
val cellRois: Seq[Rect] = Parameters.cellRange.map(OpenCV.mkRect(_, OpenCV.mkCellSize(normalized.size)))

val cells: Seq[SCell] = cellRois.map(r => SCell(normalized.submat(r), r))

// lazy val detectedCells: Future[Seq[SCell]] = Future.fold(warpedCells)(Seq[SCell]())((cells, c) => cells ++ Seq(c))
val cellValues: Seq[Int] = cells.map(_.value)

lazy val detectedCells: Seq[SCell] = cells.filter(_.value != 0)

lazy val corners = detectedCorners.toList.toList
/**
* paints the solution to the canvas.
*
Expand All @@ -33,23 +46,15 @@ case class SRectangle(frame: Mat, detectedCorners: MatOfPoint2f, destCorners: Ma
*
* uses digitData as lookup table to paint onto the canvas, thus modifying the canvas.
*/
def paintSolution(detectedCells: Seq[Int],
someSolution: Option[Cells],
digitLibrary: DigitLibrary): Future[Mat] = {

Future {
for (solution <- someSolution) {
val values: Array[Int] = solution.map(_.value)
for ((s, r) <- values zip cellRois) {
if (values.sum == 405) {
copyTo(digitLibrary(s)._2.getOrElse(SudokuUtils.mkFallback(s, digitLibrary).get), normalized, r)
} else {
logTrace("values.sum was not 405 in paintsolution")
}
}
def paintSolution(someSolution: Option[Cells],
digitLibrary: DigitLibrary): Mat = {
for (solution <- someSolution) {
val values: Array[Int] = solution.map(_.value)
for ((s, r) <- values zip cellRois if s != 0) {
copyTo(digitLibrary(s)._2.getOrElse(SudokuUtils.mkFallback(s, digitLibrary).get), normalized, r)
}
normalized
}
normalized
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package net.ladstatt.sudoku

import org.opencv.core.{Mat, Point}

import scala.collection.JavaConversions._

case class InputFrame(nr: Int, framePipeline: FramePipeline)

case class SudokuFrame(in: Mat, cells: Cells, corners: List[Point]) {
lazy val detectedCells = cells.filter(_.value != 0)
}


case class SolutionFrame(solution: SudokuDigitSolution, solutionMat: Mat) {
def solutionAsString: String = solution.sliding(9, 9).map(new String(_)).mkString("\n")
Expand All @@ -24,7 +24,7 @@ sealed trait SudokuResult {
* @param someSolution
*/
case class SSuccess(inputFrame: SCandidate,
sudokuFrame: SudokuFrame,
sudokuFrame: SRectangle,
someSolution: Option[SolutionFrame]) extends SudokuResult

case class SFailure(msg: String, inputFrame: SCandidate) extends SudokuResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ object SudokuUtils {
rects: Seq[Rect],
someSolution: Option[Cells],
hitCounts: HitCounters,
cap: Int): Future[Mat] = {
cap: Int): Mat = {


// TODO update colors
Expand All @@ -96,7 +96,7 @@ object SudokuUtils {
new Scalar(0, (n % cap) * 255 / cap, r, 255.0)
}

Future {

for (solution <- someSolution) {
CollectionUtils.traverseWithIndex(rects)((cell, i) => {
paintRect(canvas, rects(i), color(hitCounts(i), cap), 1)
Expand All @@ -105,7 +105,6 @@ object SudokuUtils {
}

canvas
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ object TemplateLibrary extends CanLog {
* @return
*/
def detectNumber(candidate: Mat): Future[(Int, Double)] = {
//println(candidate.size.width + "/" + candidate.size.height)
val resizedCandidate = OpenCV.resize(candidate, TemplateLibrary.templateSize) // since templates are 25 x 50
val matchHaystack: (Int, Mat) => Future[(Int, Double)] = OpenCV.matchTemplate(resizedCandidate, _: Int, _: Mat)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package object sudoku {
* records for each number from 0 to 9 the best hit (quality) along with its digital data
*/
type DigitLibrary = Map[Int, (Double, Option[Mat])]

// TODO solution should be an array of Int or a string with 81 entries
// the graphical representation should be a single mat with the matrixes edited inline
// (meaning the matrix represents the whole canvas and the app is working with submatrixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class SudokuStateTest extends FunSuite with GeneratorDrivenPropertyChecks
for {nr <- Gen.choose(0, 10000)
frame <- Gen.const(SudokuTestContext.frameSudoku_1)
cap <- Gen.choose(8, 15)
minHits <- Gen.choose(20, 30)} yield SCandidate(nr, frame,FramePipeline(frame,SParams()),SudokuState())
minHits <- Gen.choose(20, 30)} yield SCandidate(nr, frame, FramePipeline(frame), SudokuState())
// SudokuState(cap = cap, minHits = minHits)

def printState(s: SCandidate): Unit = logInfo(s"${s}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object SudokuTestContext {
lazy val (emptySudoku, (emptySudokuResult, _)) = calculate(emptyFrame)

def calculate(frame: Mat): (SCandidate, (SudokuResult, SudokuState)) = {
val c = SCandidate(0, frame,FramePipeline(frame,SParams()), SudokuState.DefaultState)
val c = SCandidate(0, frame, FramePipeline(frame), SudokuState.DefaultState)
val state: SudokuState = SudokuState.DefaultState.copy(cap = 1, minHits = 17, maxSolvingDuration = 5000L)
(c, Await.result(c.calc, Duration.Inf))
}
Expand Down
4 changes: 2 additions & 2 deletions sudoku-javafx/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxjava</artifactId>
<version>1.0.1</version>
<version>1.1.5</version>
</dependency>
<dependency>
<groupId>io.reactivex</groupId>
<artifactId>rxscala_2.11</artifactId>
<version>0.23.1</version>
<version>0.26.1</version>
</dependency>
</dependencies>
<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ class SudokuFXController extends Initializable with OpenCVJfxUtils with CanLog w
displayHitCounts(getCurrentSudokuState().hitCounts, as[FlowPane](statsFlowPane.getChildren))

sudokuResult match {
case SSuccess(SCandidate(nr, framePipeline, sr, ss), SudokuFrame(sudokuCanvas, detectedCells, corners), someSolution) =>
case SSuccess(SCandidate(nr, framePipeline, sr, ss), SRectangle(sudokuCanvas, detectedCells, corners), someSolution) =>
if (someSolution.isDefined) {
val sol = someSolution.get
updateVideo(stage, framePipeline, sol.solutionMat)
Expand Down

0 comments on commit 8e6f583

Please sign in to comment.