Beispiel #1
0
        private void ClearCache_Button_Click(object sender, RoutedEventArgs e)
        {
            StitchTask task = (StitchTask)((Button)sender).DataContext;

            task.ClearCache();
            Task.Run(() => task.Run());
        }
Beispiel #2
0
        // 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);
        }
Beispiel #3
0
        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
            });
        }
Beispiel #4
0
        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");
                }
            });
        }