private void ClearCache_Button_Click(object sender, RoutedEventArgs e) { StitchTask task = (StitchTask)((Button)sender).DataContext; task.ClearCache(); Task.Run(() => task.Run()); }
// TODO: Name of this method + signature is a mess private SearchResult FindTemplateInImage(IMagickImage needleImage, Gravity needleGravity, IMagickImage haystack, StitchTask task, Point anchor) { // Resize needle var pixelMagnification = 8.0; var magnification = Math.Min(1 / (double)pixelMagnification, 1.0); var progress = (IProgress <double>)task; var resizeAmount = new Percentage(magnification * 100); var template = needleImage.Clone(); template.Resize(resizeAmount); template.RePage(); IMagickImage searchArea = null; var resized = false; while (!resized) { try { lock (haystack) { searchArea = haystack.Clone(); searchArea.Resize(resizeAmount); searchArea.RePage(); } resized = true; } catch (AccessViolationException) { Console.WriteLine("Corrupt Memory, trying again"); Thread.Sleep(500); } } // We need to get the actual values here, since Resize() has unexpected rounding logic // (only supports whole digit percentages) and if we don't use the actual values then // scaling math won't work properly. var oldWidth = searchArea.Width; var oldHeight = searchArea.Height; var bounds = new MagickGeometry(0, 0, searchArea.Width, searchArea.Height); var candidates = FindTemplateCandidates(searchArea, template, bounds, progress, 2000, new HashSet <Point>()); if (candidates.Any()) { var bestScore = candidates.First().Key; this.initialCandidates = candidates.Where(x => x.Key < bestScore * 1.05).Select(x => { return(new Point(x.Value.X / magnification, x.Value.Y / magnification)); }).ToList(); } while (pixelMagnification > 1 && candidates.Any()) { var newCandidates = new SortedList <double, Point>(new DuplicateKeyComparer <double>()); var newPixelMagnification = pixelMagnification / 2; var newMagnification = Math.Min(1 / (double)newPixelMagnification, 1.0); var newResizeAmount = new Percentage(newMagnification * 100); var threshold = 2000.0; var bestSeen = threshold; var bestScore = candidates.First().Key; var toLoop = candidates.Where(x => x.Key < bestScore * 1.05); Console.WriteLine("Considering {0} candidates at {1}", toLoop.Count(), newMagnification); IMagickImage newHaystack = null; lock (haystack) { newHaystack = haystack.Clone(); newHaystack.Resize(newResizeAmount); newHaystack.RePage(); } var t2 = needleImage.Clone(); t2.Resize(newResizeAmount); t2.RePage(); var cache = new HashSet <Point>(); foreach (var candidate in toLoop) { var point = new Point(candidate.Value.X / oldWidth * newHaystack.Width, candidate.Value.Y / oldHeight * newHaystack.Height); var margin = newPixelMagnification; var clampedBounds = new MagickGeometry( (int)(point.X - margin), (int)(point.Y - margin), (int)(NeedleSize * newMagnification + margin * 2), (int)(NeedleSize * newMagnification + margin * 2) ); clampedBounds.X = Math.Max(0, clampedBounds.X); clampedBounds.Y = Math.Max(0, clampedBounds.Y); clampedBounds.Width = Math.Min(newHaystack.Width - clampedBounds.X, clampedBounds.Width); clampedBounds.Height = Math.Min(newHaystack.Height - clampedBounds.Y, clampedBounds.Height); var toAdd = FindTemplateCandidates(newHaystack, t2, clampedBounds, this, threshold, cache); foreach (var add in toAdd) { newCandidates.Add(add.Key, add.Value); if (add.Key < bestSeen) { bestSeen = add.Key; Console.WriteLine("Updating best score: {0}", bestSeen); } } } candidates = newCandidates; magnification = newMagnification; pixelMagnification = newPixelMagnification; oldWidth = newHaystack.Width; oldHeight = newHaystack.Height; } Console.WriteLine("============ Final: {0}", candidates.Count); if (candidates.Any()) { var bestCandidate = candidates.First(); return(new SearchResult() { Distance = bestCandidate.Key, HaystackPoint = bestCandidate.Value, NeedlePoint = anchor }); } return(SearchResult.Null); }
private NeedleResult FindHighEntropyStrip(IMagickImage image, Gravity gravity, double NeedleSize, StitchTask task) { IProgress <double> progress = task; var pixels = image.GetPixels(); var t1 = DateTime.UtcNow; IEnumerable <int> rows = null; IEnumerable <int> columns = null; Debug.Assert(image.Height > 1 && image.Width > 1, "Assumes non-empty image"); Debug.Assert(image.Width >= NeedleSize, "Assumes image is at least as big as needle size"); var searchArea = NeedleSearchArea(image, gravity); rows = searchArea.Item1; columns = searchArea.Item2; var minY = rows.Min(); var maxY = rows.Max(); var minX = columns.Min(); var maxX = columns.Max(); var imageDimensions = Tuple.Create(image.Width, image.Height); List <List <Pixel> > pixelGrid = LinqExtensions.FromTo(minY, maxY).Select(y => LinqExtensions.FromTo(minX, maxX).Select(x => pixels.GetPixel(x, y)).ToList()).ToList(); List <List <float> > brightnessGrid = pixelGrid.Select(xs => xs.Select(p => p.ToColor().ToColor().GetBrightness()).ToList()).ToList(); var gridWidth = maxX - minX; var gridHeight = maxY - minY; var bestNeedleStddev = 0.0; Point bestNeedle = default(Point); double totalCycles = rows.Count() * columns.Count(); double currentCycle = 0; Console.WriteLine(brightnessGrid); foreach (var y in rows) { foreach (var x in columns) { progress.Report(currentCycle / totalCycles); currentCycle++; if (y - minY + NeedleSize >= gridHeight) { continue; } if (x - minX + NeedleSize >= gridWidth) { continue; } var count = 0; var mean = 0.0; var m2 = 0.0; double blackCount = 0.0; for (var x2 = x - minX; x2 < x - minX + NeedleSize; x2++) { for (var y2 = y - minY; y2 < y - minY + NeedleSize; y2++) { var b = brightnessGrid[y2][x2]; var p = pixelGrid[y2][x2].ToColor(); if (b < 0.08) { blackCount++; } count++; var delta = b - mean; mean = mean + delta / count; var delta2 = b - mean; m2 = m2 + delta * delta2; } } var variance = m2 / (count - 1); var stddev = variance; //Console.WriteLine("{0}, {1}, {2}", blackCount, NeedleSize * NeedleSize, blackCount / (NeedleSize * NeedleSize)); if (stddev > bestNeedleStddev && blackCount / (NeedleSize * NeedleSize) < 0.5) { bestNeedleStddev = stddev; bestNeedle = new Point(x, y); } } } return(new NeedleResult() { Point = bestNeedle, Entropy = bestNeedleStddev }); }
public async Task DoNetwork() { OpenCL.IsEnabled = false; var cacheFile = System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\stitch_cache.json"; State state = new State(); try { state = JsonConvert.DeserializeObject <State>(File.ReadAllText(cacheFile)); Console.WriteLine($"Using state from {cacheFile}"); } catch { Console.WriteLine("Couldn't load cache"); } //state = new State(); AppState = state; var workerPool = new LimitedConcurrencyLevelTaskScheduler(Math.Max(Environment.ProcessorCount - 2, 1)); // / 2, 1)); //var workerPool = new LimitedConcurrencyLevelTaskScheduler(1); var snapshotState = new ActionBlock <State>((s) => { var content = ""; s.Lock(lockedState => content = JsonConvert.SerializeObject(lockedState)); WriteAllTextWithBackup(cacheFile, content); this.Dispatcher.Invoke(() => Joins.ItemsSource = AppState.Joins); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 }); state.ChangeListener = snapshotState; var blockOptions = new ExecutionDataflowBlockOptions { TaskScheduler = workerPool, MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded // Handled by underlying scheduler }; //var sourceDir = "C:/Users/Xavier/Source/ori-tracker/MapStitcher/Screenshots"; var sourceDir = "C:/Users/Xavier/Source/ori-tracker/MapStitcher/4KScreenshots"; /* * IMagickImage image1 = new MagickImage(System.IO.Path.GetFullPath($"{sourceDir}/../Temp/forlorn-1.png")); * image1 = image1.Clone(); * var settings = new MorphologySettings * { * Channels = Channels.Alpha, * Method = MorphologyMethod.Distance, * Kernel = Kernel.Euclidean, * KernelArguments = "1,50!" * }; * * image1.Alpha(AlphaOption.Set); * image1.VirtualPixelMethod = VirtualPixelMethod.Transparent; * image1.Morphology(settings); * image1.Write(System.IO.Path.GetFullPath($"{sourceDir}/../Temp/forlorn-test.png")); */ /* * MagickImage image1 = new MagickImage($"{sourceDir}/sorrow-1.png"); * MagickImage image2 = new MagickImage($"{sourceDir}/sorrow-2.png"); */ /* * var sourceFiles = new List<string> * { * $"{sourceDir}\\387290_20180314160604_1.png", * }; */ var sourceFiles = Directory.GetFiles(sourceDir, "*.png"); /* * var sourceFiles = new List<string> * { * $"{sourceDir}\\forlorn-1.png", * $"{sourceDir}\\forlorn-2.png", * $"{sourceDir}\\forlorn-3.png", * }; */ //state.ClearJoins(); /* * state.ClearNeedle(new NeedleKey { Key = $"{sourceDir}/forlorn-3.png", Gravity = Gravity.West }); * state.ClearJoin($"{sourceDir}/forlorn-2.png", $"{sourceDir}/forlorn-3.png"); * state.ClearJoin($"{sourceDir}/forlorn-1.png", $"{sourceDir}/forlorn-3.png"); * state.ClearJoin($"{sourceDir}/forlorn-2.png", $"{sourceDir}/forlorn-1.png"); */ this.Dispatcher.Invoke(() => SourceImages.ItemsSource = SourceImages2.ItemsSource = sourceFiles); this.Dispatcher.Invoke(() => Joins.ItemsSource = AppState.Joins); UpdateUI(); var loadFromDiskBlock = new TransformBlock <string, string>(path => { // TODO: Make this a load and crop task var task = new StitchTask($"Load and crop {System.IO.Path.GetFileName(path)}"); this.Dispatcher.Invoke(() => Tasks.Add(task)); state.GetOrAddImage(path, () => { var image = new MagickImage(path); var originalSize = new Size(image.Width, image.Height); int sideMargin = (int)(image.Width * 0.15); // The sides have a subtle animated mask over them. 280px wide on 1920px resolution. Crop them out. int topMargin = (int)(image.Height * 0.17); int bottomMargin = (int)(image.Height * 0.15); var bounds = new MagickGeometry(sideMargin, topMargin, image.Width - sideMargin * 2, image.Height - bottomMargin - topMargin); image.Crop(bounds); image.RePage(); //image.Write("C:\\Users\\Xavier\\Source\\ori-tracker\\MapStitcher\\Temp\\" + System.IO.Path.GetFileName(path)); return(image); }); task.Complete("Done", false); return(path); }, blockOptions); var gravities = new TransformManyBlock <string, NeedleKey>(path => { return(allGravities.Select(g => new NeedleKey() { Key = path, Gravity = g })); }, blockOptions); var findNeedleBlock = new TransformBlock <NeedleKey, NeedleKey>(needle => { var task = new FindNeedleTask(state, needle); this.Dispatcher.Invoke(() => Tasks.Add(task)); task.Run(); return(needle); }, blockOptions); var findJoinBlock = new TransformBlock <SearchKey, string>(t => { var haystack = t.Item1; var needle = t.Item2; var task = new SearchTask(state, haystack, needle); this.Dispatcher.Invoke(() => Tasks.Add(task)); task.Run(); completedSearchTasks++; return(haystack); // TODO: Figure out best thing to propagate. Maybe when match found? }, blockOptions); var broadcaster = new BroadcastBlock <string>(null); var cartesian = new CartesianProductBlock <string, NeedleKey>(); var propagate = new DataflowLinkOptions { PropagateCompletion = true }; var headBlock = loadFromDiskBlock; headBlock.LinkTo(broadcaster, propagate); broadcaster.LinkTo(gravities, propagate); gravities.LinkTo(findNeedleBlock, propagate); // Don't propagate completion from left/right sources for cartesian join. It should // complete when _both_ are done (which is it's default behaviour) broadcaster.LinkTo(cartesian.Left, propagate); findNeedleBlock.LinkTo(cartesian.Right, propagate); var countTotals = new TransformManyBlock <Tuple <string, NeedleKey>, SearchKey>(t => { var haystack = t.Item1; var needle = t.Item2; var none = Enumerable.Empty <SearchKey>(); if (haystack == needle.Key || !state.GetNeedle(needle).MeetsThreshold()) { return(none); } var existingJoins = state.Joins; var connectedJoins = new HashSet <HashSet <string> >(); foreach (var join in existingJoins) { var found = false; foreach (var connectedSubset in connectedJoins) { if (connectedSubset.Contains(join.Image1) || connectedSubset.Contains(join.Image2)) { connectedSubset.Add(join.Image1); connectedSubset.Add(join.Image2); found = true; break; } } if (!found) { var newSubset = new HashSet <string>(); newSubset.Add(join.Image1); newSubset.Add(join.Image2); connectedJoins.Add(newSubset); } } connectedJoins.Aggregate(new HashSet <HashSet <string> >(), (acc, x) => { var found = false; foreach (var connectedSubset in acc) { if (connectedSubset.Overlaps(x)) { connectedSubset.UnionWith(x); found = true; break; } } if (!found) { acc.Add(x); } return(acc); }); if (connectedJoins.Any(x => x.Contains(haystack) && x.Contains(needle.Key))) { Console.WriteLine("Two images already connected via transitive joins, skipping"); return(none); } totalSearchTasks++; return(Enumerable.Repeat(SearchKey.Create(t.Item1, t.Item2), 1)); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 }); cartesian.LinkTo(countTotals, propagate); countTotals.LinkTo(findJoinBlock, propagate); var sink = new ActionBlock <string>(s => { }); findJoinBlock.LinkTo(sink, propagate); foreach (var file in sourceFiles) { headBlock.Post(file); } headBlock.Complete(); await sink.Completion.ContinueWith(_ => { Console.WriteLine("Pipeline Finished"); /* * this.Dispatcher.Invoke(() => TaskGrid.ItemsSource = Tasks.Where(x => * { * if (x is SearchTask) * { * var task = (SearchTask)x; * return task.Name.Contains("Found"); * return task.searchResult.MeetsThreshold(); * return task.searchResult.Distance < 2500; * } * return false; * })); */ Dictionary <string, Point> completedJoins = new Dictionary <string, Point>(); var remainingJoins = new List <State.Join>(state.Joins); var rejects = new List <State.Join>(); var images = new MagickImageCollection(); var lastCycleCount = 0; var morphologySettings = new MorphologySettings { Channels = Channels.Alpha, Method = MorphologyMethod.Distance, Kernel = Kernel.Euclidean, KernelArguments = "1,50!" }; while (remainingJoins.Count > 0 && remainingJoins.Count != lastCycleCount) { lastCycleCount = remainingJoins.Count; foreach (var join in remainingJoins) { if (completedJoins.Count == 0) { var tempPath = System.IO.Path.GetTempFileName(); var tempPath2 = System.IO.Path.GetTempFileName(); // Initial seed var i1 = state.Image(join.Image1).Clone(); var i2 = state.Image(join.Image2).Clone(); i1.Alpha(AlphaOption.Set); i1.VirtualPixelMethod = VirtualPixelMethod.Transparent; i1.Morphology(morphologySettings); i1.Write(tempPath); i1.Dispose(); i1 = new MagickImage(tempPath); i1.BackgroundColor = new MagickColor(18, 18, 18); i2.Alpha(AlphaOption.Set); i2.VirtualPixelMethod = VirtualPixelMethod.Transparent; i2.Morphology(morphologySettings); i2.Write(tempPath2); i2.Dispose(); i2 = new MagickImage(tempPath2); i2.Page = new MagickGeometry($"{ToOffset(join.JoinPoint.X)}{ToOffset(join.JoinPoint.Y)}"); images.Add(i1); images.Add(i2); completedJoins.Add(join.Image1, new Point(0, 0)); completedJoins.Add(join.Image2, join.JoinPoint); File.Delete(tempPath); File.Delete(tempPath2); } else { Point offset = join.JoinPoint; if (completedJoins.ContainsKey(join.Image1) && completedJoins.ContainsKey(join.Image2)) { // NOOP } else if (completedJoins.ContainsKey(join.Image1)) { completedJoins.TryGetValue(join.Image1, out offset); var tempPath = System.IO.Path.GetTempFileName(); var i2 = state.Image(join.Image2).Clone(); var joinPoint = new Point(join.JoinPoint.X + offset.X, join.JoinPoint.Y + offset.Y); i2.Alpha(AlphaOption.Set); i2.VirtualPixelMethod = VirtualPixelMethod.Transparent; i2.Morphology(morphologySettings); i2.Write(tempPath); //i2.Dispose(); //i2 = new MagickImage(tempPath); i2.Page = new MagickGeometry($"{ToOffset(joinPoint.X)}{ToOffset(joinPoint.Y)}"); images.Add(i2); File.Delete(tempPath); completedJoins.Add(join.Image2, joinPoint); } else if (completedJoins.ContainsKey(join.Image2)) { completedJoins.TryGetValue(join.Image2, out offset); var tempPath = System.IO.Path.GetTempFileName(); var i1 = state.Image(join.Image1).Clone(); var joinPoint = new Point(offset.X - join.JoinPoint.X, offset.Y - join.JoinPoint.Y); i1.Alpha(AlphaOption.Set); i1.VirtualPixelMethod = VirtualPixelMethod.Transparent; i1.Morphology(morphologySettings); i1.Write(tempPath); //i1.Dispose(); //i1 = new MagickImage(tempPath); i1.Page = new MagickGeometry($"{ToOffset(joinPoint.X)}{ToOffset(joinPoint.Y)}"); images.Add(i1); File.Delete(tempPath); completedJoins.Add(join.Image1, joinPoint); } else { rejects.Add(join); } } } remainingJoins = rejects.ToList(); rejects.Clear(); } if (images.Any()) { var merged = images.Merge(); //merged.BackgroundColor = new MagickColor(0, 0, 0); //merged.Alpha(AlphaOption.Remove); //merged.Alpha(AlphaOption.Off); merged.Write("C:\\Users\\Xavier\\Source\\ori-tracker\\MapStitcher\\Temp\\map.png"); DisplayImage(Viewer, merged); Console.WriteLine("Done Compositing"); } }); }