// Standard public CWatchImage(string name, int index, IMagickImage magickImage, double metricUpperBound) { Name = name; Index = index; MagickImage = magickImage; HasAlpha = MagickImage != null && MagickImage.HasAlpha; TransparencyRate = MagickImage != null?MagickImage.GetTransparencyRate() : 0; AlphaChannel = null; MetricUpperBound = metricUpperBound; _PauseTicks = DateTime.MinValue.Ticks; if (HasAlpha) { // Does Separate() clone the channels? If so, does it dispose of them during this? var tmpMi = MagickImage.Separate().Last().Clone(); tmpMi.Negate(); AlphaChannel = new MagickImage(_AlphaReplacement, MagickImage.Width, MagickImage.Height); AlphaChannel.Composite(tmpMi, CompositeOperator.CopyAlpha); AlphaChannel.RePage(); tmpMi.Dispose(); MagickImage.ColorAlpha(_AlphaReplacement); MagickImage.RePage(); } }
private void Distort(IMagickImage output, PointD[] corners, MagickGeometry inputDimensions, MagickGeometry trimmedDimensions, MagickColor backgroundColor) { MagickGeometry outputDimensions = GetDimensions(output, corners, inputDimensions, trimmedDimensions); double[] arguments = new double[16] { corners[0].X, corners[0].Y, 0, 0, corners[1].X, corners[1].Y, 0, outputDimensions.Height, corners[2].X, corners[2].Y, outputDimensions.Width, outputDimensions.Height, corners[3].X, corners[3].Y, outputDimensions.Width, 0 }; output.VirtualPixelMethod = VirtualPixelMethod.Background; output.BackgroundColor = backgroundColor; if (!DisableViewportCrop) { output.SetArtifact("distort:viewport", GetViewport(arguments, corners).ToString()); } var distortSettings = new DistortSettings() { Bestfit = true }; output.Distort(DistortMethod.Perspective, distortSettings, arguments); output.BorderColor = backgroundColor; output.Border(2); output.ColorFuzz = ColorFuzz; output.Trim(); output.RePage(); }
private void TrimImage(IMagickImage result) { if (!Trim) { return; } result.Trim(); result.RePage(); }
private MagickGeometry TrimImage(IMagickImage image, MagickColor backgroundColor, int borderSize) { image.BorderColor = backgroundColor; image.Border(borderSize); image.ColorFuzz = ColorFuzz; image.Trim(); image.RePage(); return(new MagickGeometry(image.Width, image.Height)); }
private void ExecuteOuterTrim(IMagickImage image, MagickColor borderColor) { image.BackgroundColor = borderColor; image.ColorFuzz = ColorFuzz; image.Trim(); image.RePage(); var geometry = new MagickGeometry(0, 0, image.Width, image.Height); ShiftGeometry(geometry); Crop(image, geometry); }
public IMagickImage Execute(IMagickImage image, ValueItemCollection values) { Variables = values; if (!CropVisible) { return(image); } var dw = image.Width / 1000.0; var dh = image.Height / 1000.0; image.Crop((int)(CropX * dw), (int)(CropY * dh), (int)(CropWidth * dw), (int)(CropHeight * dh)); image.RePage(); return(image); }
private void Crop(IMagickImage image, MagickGeometry area) { ShiftGeometry(area); image.Crop(area.X, area.Y, area.Width, area.Height); image.RePage(); }
// 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); }
public static void Run([BlobTrigger("inbox/{inputFilename}", Connection = "StorageAccountConnString")] Stream inputStream, string inputFilename, [Blob("outbox/out-{inputFilename}", FileAccess.Write, Connection = "StorageAccountConnString")] Stream outputStream, ILogger log) { log.LogInformation($"STEP: trigger fired for file:{inputFilename}, Size: {inputStream.Length} Bytes"); using (MagickImage image = new MagickImage(inputStream)) { log.LogInformation($"STEP: Read image {inputFilename} with {image.BaseWidth} x {image.BaseHeight} in {image.Format.ToString()}"); // Step 0 - does nothing in the sample images I used image.AutoOrient(); // Step 1 - Deskew image.BackgroundColor = MagickColor.FromRgb(0, 0, 0); image.Deskew(new Percentage(1)); // documentation suggests "A threshold of 40% works for most images" but 1% seems to work well IMagickImage rotatedImage = image.Clone(); log.LogInformation($"STEP: Deskewed {inputFilename}"); // Step 2 - Apply threshold to transform in black and white image image.AutoThreshold(AutoThresholdMethod.OTSU); log.LogInformation($"STEP: Applied OTSU to {inputFilename}"); // Step 3 - find the regions (blocs) in the image and group them if large enough IEnumerable <ConnectedComponent> components = null; ConnectedComponentsSettings ccs = new ConnectedComponentsSettings(); ccs.AreaThreshold = 500 * 500.0; // 500x500 -- seems to be pointless, many more regions are returned ccs.Connectivity = 8; components = image.ConnectedComponents(ccs); // if there are multiple blocs, consolidate them in a larger block if (components != null && components.Count() > 0) { log.LogInformation($"STEP: Looked for regions in {inputFilename}, there are {components.Count()}"); // filter out the smaller rectangles, as the AreaThreshold parameter seems not to be working List <ConnectedComponent> biggerComponents = components.Where(cc => cc.Height * cc.Width >= 250000 && cc.Height * cc.Width != image.Width * image.Height) /*.OrderByDescending(i => i.Height * i.Width)*/.ToList(); int topLeftX = biggerComponents[0].X, topLeftY = biggerComponents[0].Y, bottomRightX = biggerComponents[0].Width + topLeftX, bottomRightY = biggerComponents[0].Height + topLeftY; foreach (ConnectedComponent cc in biggerComponents) { #region Debug -- draw the regions on the image //DrawableStrokeColor strokeColor = new DrawableStrokeColor(new MagickColor("yellow")); //DrawableStrokeWidth stokeWidth = new DrawableStrokeWidth(3); //DrawableFillColor fillColor = new DrawableFillColor(new MagickColor(50, 50, 50, 128)); //DrawableRectangle dr = new DrawableRectangle(cc.X, cc.Y, cc.X + cc.Width, cc.Y + cc.Height); //rotatedImage.Draw(dr, strokeColor, stokeWidth, fillColor); #endregion if (cc.X < topLeftX) { topLeftX = cc.X; } if (cc.Y < topLeftY) { topLeftY = cc.Y; } if (cc.X + cc.Width > bottomRightX) { bottomRightX = cc.X + cc.Width; } if (cc.Y + cc.Height > bottomRightY) { bottomRightY = cc.Y + cc.Height; } } #region Debug -- draw the bounding box on the image //DrawableStrokeColor strokeColor2 = new DrawableStrokeColor(new MagickColor("purple")); //DrawableStrokeWidth stokeWidth2 = new DrawableStrokeWidth(3); //DrawableFillColor fillColor2 = new DrawableFillColor(new MagickColor(50, 50, 50, 128)); //DrawableRectangle dr2 = new DrawableRectangle(topLeftX, topLeftY, bottomRightX, bottomRightY); //rotatedImage.Draw(dr2, strokeColor2, stokeWidth2, fillColor2); #endregion // Step 4 - Crop the image MagickGeometry mg = new MagickGeometry(topLeftX, topLeftY, bottomRightX - topLeftX, bottomRightY - topLeftY); rotatedImage.RePage(); // this is needed because otherwise the crop is relative to the page information and sometimes this leads to an incorrect crop rotatedImage.Crop(mg); log.LogInformation($"STEP: Cropped {inputFilename} to fit existing large regions"); } else { log.LogInformation($"STEP: Looked for large regions in {inputFilename}, none were found, skipping crop"); } // Step 5 - Resize the image to 1200px width (todo: move to configuration) int originalWidth = rotatedImage.BaseWidth; int originalHeight = rotatedImage.BaseHeight; rotatedImage.Resize(1200, 0); // make width 1200, height proportional log.LogInformation($"STEP: Resized {inputFilename} from {originalWidth}x{originalHeight} to {image.BaseWidth}x{image.BaseHeight}"); // Step 6 - write out as Jpeg with 70% quality rotatedImage.Format = MagickFormat.Jpeg; rotatedImage.Quality = 70; rotatedImage.Write(outputStream); log.LogInformation($"STEP: Wrote out {inputFilename} as JPEG"); } log.LogInformation($"STEP: Processing of {inputFilename} done"); }