public static OpenFieldReaderResult FindBoxes(int[] imgData, int row, int col, OpenFieldReaderOptions options) { // Debug image. int[,] debugImg = null; if (options.GenerateDebugImage) { debugImg = new int[row, col]; for (int y = 0; y < row; y++) { for (int x = 0; x < col; x++) { debugImg[y, x] = 0; } } } // We are seaching for pattern! // We look for junctions. // This will help us make a decision. // Junction types: T, L, +. // Junctions allow us to find boxes contours. int width = options.JunctionWidth; int height = options.JunctionHeight; // Cache per line speed up the creation of various cache. Dictionary <int, List <Junction> > cacheListJunctionPerLine = new Dictionary <int, List <Junction> >(); List <Junction> listJunction = new List <Junction>(); // If there is too much junction near each other, maybe it's just a black spot. // We must ignore it to prevent wasting CPU and spend too much time. int maxProximity = 10; for (int y = 1; y < row - 1; y++) { List <Junction> listJunctionX = null; int proximityCounter = 0; for (int x = 1; x < col - 1; x++) { Junction?junction = GetJunction(imgData, row, col, height, width, y, x); if (junction != null) { if (listJunctionX == null) { listJunctionX = new List <Junction>(); } listJunctionX.Add(junction.Value); proximityCounter++; } else { if (listJunctionX != null) { if (proximityCounter < maxProximity) { if (!cacheListJunctionPerLine.ContainsKey(y)) { cacheListJunctionPerLine.Add(y, new List <Junction>()); } cacheListJunctionPerLine[y].AddRange(listJunctionX); listJunction.AddRange(listJunctionX); listJunctionX.Clear(); } else { listJunctionX.Clear(); } } proximityCounter = 0; } } if (proximityCounter < maxProximity && listJunctionX != null) { if (!cacheListJunctionPerLine.ContainsKey(y)) { cacheListJunctionPerLine.Add(y, new List <Junction>()); } cacheListJunctionPerLine[y].AddRange(listJunctionX); listJunction.AddRange(listJunctionX); } } if (options.Verbose) { Console.WriteLine("Junction.count: " + listJunction.Count); } if (listJunction.Count >= options.MaxJunctions) { // Something wrong happen. Too much junction for now. // If we continue, we would spend too much time processing the image. // Let's suppose we don't know. return(new OpenFieldReaderResult { // Too many junctions. The image seem too complex. You may want to increase MaxJunctions ReturnCode = 10 }); } // Let's check the list of points. // Search near same line. // TODO: should be parameters. (Can speed up process if you know what you are looking for.) int minX = 15; // Min estimated cell width (should not be less than 15.) int maxX = 80; // Max estimated cell width (should not be greater than 85. Most of the time, it can be reduced to 50 or 60.) int variationY = 3; // Variation of y to find next cell. (should be really small for faster result) // Prepare cache to speedup searching algo. Dictionary <int, Junction[]> cacheNearJunction = new Dictionary <int, Junction[]>(); Dictionary <int, Junction[]> cachePossibleNextJunctionRight = new Dictionary <int, Junction[]>(); Dictionary <int, Junction[]> cachePossibleNextJunctionLeft = new Dictionary <int, Junction[]>(); foreach (var junction in listJunction) { var listJunctionNearJunction = new List <Junction>(); for (int deltaY = -variationY; deltaY <= variationY; deltaY++) { if (cacheListJunctionPerLine.ContainsKey(junction.Y - deltaY)) { listJunctionNearJunction.AddRange(cacheListJunctionPerLine[junction.Y - deltaY]); } } var list = listJunctionNearJunction .Where(m => Math.Abs(m.X - junction.X) <= maxX ) .ToArray(); var id = junction.X | junction.Y << 16; cacheNearJunction.Add(id, list); var possibleNextJunction = list .Where(m => Math.Abs(m.X - junction.X) >= minX ) .ToList(); cachePossibleNextJunctionLeft.Add(id, possibleNextJunction.Where(m => m.X < junction.X).ToArray()); cachePossibleNextJunctionRight.Add(id, possibleNextJunction.Where(m => m.X > junction.X).ToArray()); } int numSol = 0; List <Line> possibleSol = new List <Line>(); // We use a dictionary here because we need a fast way to remove entry. // We reduce computation and we also merge solutions. var elements = listJunction.OrderBy(m => m.Y).ToDictionary(m => m.X | m.Y << 16, m => m); int skipSol = 0; while (elements.Any()) { var start = elements.First().Value; elements.Remove(start.X | start.Y << 16); Dictionary <int, List <int> > usedJunctionsForGapX = new Dictionary <int, List <int> >(); List <Line> listSolutions = new List <Line>(); var junctionsForGap = cacheNearJunction[start.X | start.Y << 16]; for (int iGap = 0; iGap < junctionsForGap.Length; iGap++) { var gap = junctionsForGap[iGap]; // Useless because it's already done with: cacheNearJunction. /* * var gapY = Math.Abs(gap.Y - start.Y); * if (gapY > 2) * { * continue; * }*/ var gapX = Math.Abs(gap.X - start.X); if (gapX <= minX || gapX > maxX) { continue; } // We will reduce list of solution by checking if the solution is already found. //if (listSolutions.Any(m => Math.Abs(m.GapX - gapX) < 2 && m.Junctions.Contains(start))) if (usedJunctionsForGapX.ContainsKey(gap.X | gap.Y << 16) && usedJunctionsForGapX[gap.X | gap.Y << 16].Any(m => Math.Abs(m - gapX) < 10)) { skipSol++; continue; } List <Junction> curSolution = new List <Junction>(); curSolution.Add(start); int numElementsRight = FindElementsOnDirection(cachePossibleNextJunctionRight, start, gap, gapX, curSolution); int numElementsLeft = FindElementsOnDirection(cachePossibleNextJunctionLeft, start, gap, -gapX, curSolution); int numElements = numElementsLeft + numElementsRight; if (numElements >= options.MinNumElements) { if (numSol == options.MaxSolutions) { // Something wrong happen. Too much solution for now. // If we continue, we would spend too much time processing the image. // Let's suppose we don't know. return(new OpenFieldReaderResult { // Too much solution. You may want to increase MaxSolutions. ReturnCode = 30 }); } numSol++; listSolutions.Add(new Line { GapX = gapX, Junctions = curSolution.ToArray() }); foreach (var item in curSolution) { List <int> listGapX; if (!usedJunctionsForGapX.ContainsKey(item.X | item.Y << 16)) { listGapX = new List <int>(); usedJunctionsForGapX.Add(item.X | item.Y << 16, listGapX); } else { listGapX = usedJunctionsForGapX[item.X | item.Y << 16]; } listGapX.Add(gapX); } } } Line bestSol = listSolutions.OrderByDescending(m => m.Junctions.Count()).FirstOrDefault(); if (bestSol != null) { // Too slow. (faster if we skip removal) // But, we have more solutions. foreach (var item in bestSol.Junctions) { elements.Remove(item.X | item.Y << 16); } possibleSol.Add(bestSol); } } if (options.Verbose) { Console.WriteLine("Skip solutions counter: " + skipSol); Console.WriteLine(numSol + " : Solution found"); Console.WriteLine(possibleSol.Count + " : Best solution found"); } // Let's merge near junctions. (vertical line) // We assign a group id for each clusters. Dictionary <int, int> junctionToGroupId = new Dictionary <int, int>(); int nextGroupId = 1; foreach (var curSolution in possibleSol) { if (curSolution.Junctions.First().GroupId == 0) { for (int i = 0; i < curSolution.Junctions.Length; i++) { ref var j = ref curSolution.Junctions[i]; j.GapX = curSolution.GapX; } // Not assigned yet. // Find near junction. int groupId = 0; foreach (var item in curSolution.Junctions) { var alreadyClassified = cacheNearJunction[item.X | item.Y << 16] .Where(m => // Doesn't work with struct. //m.GroupId != 0 && Math.Abs(m.X - item.X) <= 5 && Math.Abs(m.Y - item.Y) <= 3 // Doesn't work with struct. //Math.Abs(m.GapX - item.GapX) <= 2 ).Where(m => junctionToGroupId.ContainsKey(m.X | m.Y << 16)); if (alreadyClassified.Any()) { Junction junction = alreadyClassified.First(); groupId = junctionToGroupId[junction.X | junction.Y << 16]; //groupId = alreadyClassified.First().GroupId; break; } } if (groupId == 0) { // Not found. // Create a new group. nextGroupId++; groupId = nextGroupId; } for (int i = 0; i < curSolution.Junctions.Length; i++) { ref var j = ref curSolution.Junctions[i]; j.GroupId = groupId; int id = j.X | j.Y << 16; if (!junctionToGroupId.ContainsKey(id)) { junctionToGroupId.Add(id, groupId); } } }
private static void Run(OpenFieldReaderOptions options) { try { using (var image = Image.Load(options.InputFile)) { try { int row = image.Height; int col = image.Width; int[] imgData = new int[row * col]; for (int y = 0; y < row; y++) { for (int x = 0; x < col; x++) { var pixel = image[x, y]; var val = pixel.R | pixel.G | pixel.B; imgData[y + x * row] = val > 122 ? 0 : 255; } } var result = OpenFieldReader.FindBoxes(imgData, row, col, options); if (result.ReturnCode != 0) { if (options.Verbose) { Console.WriteLine("Exit with code: " + result.ReturnCode); } Environment.Exit(result.ReturnCode); } if (options.OutputFile == "std") { // Show result on the console. Console.WriteLine("Boxes: " + result.Boxes.Count); Console.WriteLine(); int iBox = 1; foreach (var box in result.Boxes) { Console.WriteLine("Box #" + iBox); foreach (var element in box) { Console.WriteLine(" Element: " + element.TopLeft + "; " + element.TopRight + "; " + element.BottomRight + "; " + element.BottomLeft); } iBox++; } Console.WriteLine("Press any key to continue..."); Console.ReadLine(); } else { // Write result to output file. var outputPath = options.OutputFile; var json = JsonSerializer.ToJsonString(result); File.WriteAllText(outputPath, json); } } catch (Exception ex) { Console.WriteLine("File: " + options.InputFile); Console.WriteLine("Something wrong happen: " + ex.Message + Environment.NewLine + ex.StackTrace); Environment.Exit(3); } } } catch (Exception ex) { Console.WriteLine("File: " + options.InputFile); Console.WriteLine("Something wrong happen: " + ex.Message + Environment.NewLine + ex.StackTrace); Environment.Exit(2); } }