/* * GNU AFFERO GENERAL PUBLIC LICENSE * Version 3, 19 November 2007 * Copyright (C) 2007 Free Software Foundation, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ using CommunityToolkit.HighPerformance; using Emgu.CV; using Emgu.CV.Cuda; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Text; using UVtools.Core.EmguCV; using UVtools.Core.Objects; namespace UVtools.Core.Extensions; public static class EmguExtensions { #region Constants /// /// White color: 255, 255, 255, 255 /// public static readonly MCvScalar WhiteColor = new(255, 255, 255, 255); /// /// Black color: 0, 0, 0, 255 /// public static readonly MCvScalar BlackColor = new(0, 0, 0, 255); //public static readonly MCvScalar TransparentColor = new(); public static readonly Point AnchorCenter = new (-1, -1); public static readonly Mat Kernel3x3Rectangle = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), AnchorCenter); #endregion #region Initializers methods /// /// Create a byte array of size of this /// /// /// Blank byte array public static byte[] CreateBlankByteArray(this Mat mat) => new byte[mat.GetLength()]; /// /// Creates a new empty with same size and type of the source /// /// /// public static Mat New(this Mat mat) => new(mat.Size, mat.Depth, mat.NumberOfChannels); /// /// Creates a new empty with same size and type of the source /// /// /// /// public static Mat New(this Mat src, MCvScalar color) => InitMat(src.Size, color, src.NumberOfChannels, src.Depth); /// /// Creates a new blanked (All zeros) with same size and type of the source /// /// /// Blanked public static Mat NewBlank(this Mat mat) => InitMat(mat.Size, mat.NumberOfChannels, mat.Depth); /// /// Creates a new blanked (All zeros) with same size and type of the source /// /// /// Blanked public static UMat NewBlank(this UMat mat) => InitUMat(mat.Size, mat.NumberOfChannels, mat.Depth); /// /// Creates a with same size and type of the source and set it to a color /// /// /// /// public static Mat NewSetTo(this Mat mat, MCvScalar color) => InitMat(mat.Size, color, mat.NumberOfChannels, mat.Depth); /// /// Creates a new and zero it /// /// /// /// /// public static Mat InitMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) => size.IsEmpty ? new() : Mat.Zeros(size.Height, size.Width, depthType, channels); /// /// Creates a new and zero it /// /// /// /// /// public static UMat InitUMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) { if (size.IsEmpty) return new(); var umat = new UMat(size.Height, size.Width, depthType, channels); umat.SetTo(BlackColor); return umat; } /// /// Creates a new and set it to a /// /// /// /// /// /// public static Mat InitMat(Size size, MCvScalar color, int channels = 1, DepthType depthType = DepthType.Cv8U) { if (size.IsEmpty) return new(); var mat = new Mat(size, depthType, channels); mat.SetTo(color); return mat; } /// /// Allocates a new array of mat's /// /// Array size /// public static Mat[] InitMats(uint count) => InitMats(count, Size.Empty); /// /// Allocates a new array of mat 's /// /// Array size /// Image size to create, use to create a empty Mat /// New mat array public static Mat[] InitMats(uint count, Size size) { var layers = new Mat[count]; for (var i = 0; i < layers.Length; i++) { layers[i] = InitMat(size); } return layers; } /// /// Create a new from /// /// /// public static GpuMat ToGpuMat(this Mat mat) { var gpuMat = new GpuMat(mat.Rows, mat.Cols, mat.Depth, mat.NumberOfChannels); gpuMat.Upload(mat); return gpuMat; } #endregion #region Memory accessors /// /// Gets the mat bytes as /// /// /// /// public static unsafe UnmanagedMemoryStream GetUnmanagedMemoryStream(this Mat mat, FileAccess accessMode) { var length = mat.GetLength(); return new UnmanagedMemoryStream(mat.GetBytePointer(), length, length, accessMode); } /// /// Gets the byte pointer of this /// /// /// public static unsafe byte* GetBytePointer(this Mat mat) => (byte*)mat.DataPointer.ToPointer(); /// /// Gets the whole data span to manipulate or read pixels, use this when possibly using ROI /// /// public static unsafe Span2D GetDataSpan2D(this Mat mat) { var step = mat.GetRealStep(); if (!mat.IsSubmatrix) return new(mat.DataPointer.ToPointer(), mat.Height, step, 0); return new(mat.DataPointer.ToPointer(), mat.Height, step, mat.Step / mat.ElementSize - step); } /// /// Gets the whole data span to manipulate or read pixels, use this when possibly using ROI /// /// public static Span2D GetDataByteSpan2D(this Mat mat) => mat.GetDataSpan2D(); /// /// Gets the whole data span to manipulate or read pixels /// /// /// public static Span GetDataByteSpan(this Mat mat) => mat.GetDataSpan(); /// /// Gets the whole data span to manipulate or read pixels /// /// public static Span GetDataByteSpan(this Mat mat, int length, int offset = 0) => GetDataSpan(mat, length, offset); /// /// Gets the data span to manipulate or read pixels given a length and offset /// /// /// /// /// /// public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int offset = 0) { if (length <= 0) { if (mat.IsSubmatrix) { length = mat.Step * (mat.Height - 1) + mat.GetRealStep(); } else { length = mat.GetLength(); } } return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length); } /// /// Gets a single pixel span to manipulate or read pixels /// /// /// /// /// /// public static Span GetPixelSpan(this Mat mat, int x, int y) => mat.GetDataSpan(mat.NumberOfChannels, mat.GetPixelPos(x, y)); /// /// Gets a single pixel span to manipulate or read pixels /// /// /// /// /// public static Span GetPixelSpan(this Mat mat, int pos) => mat.GetDataSpan(mat.NumberOfChannels, pos); /// /// Gets a row span to manipulate or read pixels /// /// /// /// /// /// /// public static unsafe Span GetRowSpan(this Mat mat, int y, int length = 0, int offset = 0) { var originalStep = mat.Step; if(length <= 0) length = mat.GetRealStep(); return new(IntPtr.Add(mat.DataPointer, y * originalStep + offset).ToPointer(), length); } /// /// Gets a row span to manipulate or read pixels /// /// /// /// /// /// public static Span GetRowByteSpan(this Mat mat, int y, int length = 0, int offset = 0) => mat.GetRowSpan(y, length, offset); /// /// Gets a col span to manipulate or read pixels /// /// /// /// /// /// /// public static unsafe Span GetColSpan(this Mat mat, int x, int length = 0, int offset = 0) { // Fix with Span2D var colMat = mat.Col(x); return new(IntPtr.Add(colMat.DataPointer, offset).ToPointer(), length <= 0 ? mat.Height : length); } #endregion #region Memory Fill /// /// Fill a mat span with a color /// /// Mat to fill /// Start position, this reference will increment by the /// Length to fill /// Color to fill with /// Ignore and fill to if is less than the threshold value public static void FillSpan(this Mat mat, ref int startPosition, int length, byte color, byte colorFillMinThreshold = 1) { if (length <= 0) return; if (color < colorFillMinThreshold) // Ignore threshold (mostly if blacks), spare cycles { startPosition += length; return; } mat.GetDataByteSpan(length, startPosition).Fill(color); startPosition += length; } /// /// Fill a mat span with a color /// /// Mat to fill /// /// /// Length to fill /// Color to fill with /// Ignore and set to if is less than the threshold value public static void FillSpan(this Mat mat, int x, int y, int length, byte color, byte colorFillMinThreshold = 1) { if (length <= 0 || color < colorFillMinThreshold) return; // Ignore threshold (mostly if blacks), spare cycles mat.GetDataByteSpan(length, mat.GetPixelPos(x, y)).Fill(color); } /// /// Fill a mat span with a color /// /// Mat to fill /// /// Length to fill /// Color to fill with /// Ignore and fill to if is less than the threshold value public static void FillSpan(this Mat mat, Point position, int length, byte color, byte colorFillMinThreshold = 1) => mat.FillSpan(position.X, position.Y, length, color, colorFillMinThreshold); #endregion #region Get/Set methods /// /// Step return the original Mat step, if ROI step still from original matrix which lead to errors. /// Use this to get the real step size /// /// /// public static int GetRealStep(this Mat mat) { return mat.Width * mat.NumberOfChannels; } /// /// Gets the total length of this /// /// /// The total length of this public static int GetLength(this Mat mat) { return mat.Total.ToInt32() * mat.NumberOfChannels; } /// /// Gets a pixel index position on a span given X and Y /// /// /// X coordinate /// Y coordinate /// The pixel index position public static int GetPixelPos(this Mat mat, int x, int y) { return y * mat.GetRealStep() + x * mat.NumberOfChannels; } /// /// Gets a pixel index position on a span given X and Y /// /// /// X and Y Location /// The pixel index position public static int GetPixelPos(this Mat mat, Point point) { return mat.GetPixelPos(point.X, point.Y); } /// /// Gets a byte array copy of this /// /// /// Byte array public static byte[] GetBytes(this Mat mat) { return mat.GetRawData(); } /// /// Gets a byte pixel at a position /// /// /// /// public static byte GetByte(this Mat mat, int pos) { //return new Span(IntPtr.Add(mat.DataPointer, pos).ToPointer(), mat.Step)[0]; var value = new byte[1]; Marshal.Copy(mat.DataPointer + pos, value, 0, value.Length); return value[0]; } /// /// Gets a byte pixel at a position /// /// /// /// /// public static byte GetByte(this Mat mat, int x, int y) => GetByte(mat, mat.GetPixelPos(x, y)); /// /// Gets a byte pixel at a position /// /// /// /// public static byte GetByte(this Mat mat, Point pos) => GetByte(mat, mat.GetPixelPos(pos.X, pos.Y)); /// /// Sets a byte pixel at a position /// /// /// /// public static void SetByte(this Mat mat, int pixel, byte value) => SetByte(mat, pixel, new[] { value }); /// /// Sets a byte pixel at a position /// /// /// /// public static void SetByte(this Mat mat, int pixel, byte[] value) => Marshal.Copy(value, 0, mat.DataPointer + pixel, value.Length); /// /// Sets a byte pixel at a position /// /// /// /// /// public static void SetByte(this Mat mat, int x, int y, byte value) => SetByte(mat, x, y, new[] { value }); /// /// Sets a byte pixel at a position /// /// /// /// public static void SetByte(this Mat mat, Point pos, byte value) => SetByte(mat, pos.X, pos.Y, new[] { value }); /// /// Sets a byte pixel at a position /// /// /// /// /// public static void SetByte(this Mat mat, int x, int y, byte[] value) => SetByte(mat, y * mat.GetRealStep() + x * mat.NumberOfChannels, value); /// /// Sets bytes /// /// /// public static void SetBytes(this Mat mat, byte[] value) { mat.SetTo(value); } /// /// Gets PNG byte array /// /// /// public static byte[] GetPngByes(this IInputArray mat) { return CvInvoke.Imencode(".png", mat); } public static Point GetCenterPoint(this Mat mat) => new(mat.Width / 2, mat.Height / 2); #endregion #region Create methods public static Mat CreateMask(this Mat src, VectorOfVectorOfPoint contours, Point offset = default) { var mask = src.NewBlank(); CvInvoke.DrawContours(mask, contours, -1, WhiteColor, -1, LineType.EightConnected, null, int.MaxValue, offset); return mask; } public static Mat CreateMask(this Mat src, Point[][] contours, Point offset = default) { using var vec = new VectorOfVectorOfPoint(contours); return src.CreateMask(vec, offset); } public static Mat CropByBounds(this Mat src, bool cloneInsteadRoi = false) { var rect = CvInvoke.BoundingRectangle(src); if (rect.Size == Size.Empty) return src.New(); if (src.Size == rect.Size) return cloneInsteadRoi ? src.Roi(src.Size) : src.Clone(); var roi = src.Roi(rect); if (cloneInsteadRoi) { var clone = roi.Clone(); roi.Dispose(); return clone; } return roi; } public static Mat CropByBounds(this Mat src, ushort margin) => src.CropByBounds(new Size(margin, margin)); public static Mat CropByBounds(this Mat src, Size margin) { var rect = CvInvoke.BoundingRectangle(src); if (rect.Size == Size.Empty) return src.New(); using var roi = src.Size == rect.Size ? src.Roi(src.Size) : src.Roi(rect); var numberOfChannels = roi.NumberOfChannels; var cropped = InitMat(new Size(roi.Width + margin.Width * 2, roi.Height + margin.Height * 2), numberOfChannels, roi.Depth); using var dest = new Mat(cropped, new Rectangle(margin.Width, margin.Height, roi.Width, roi.Height)); roi.CopyTo(dest); return cropped; } public static void CropByBounds(this Mat src, Mat dst) { using var mat = src.CropByBounds(); dst.Create(mat.Rows, mat.Cols, mat.Depth, mat.NumberOfChannels); src.CopyTo(dst); } #endregion #region Copy methods /// /// Copy a region from to center of other /// /// Source to be copied to /// Size of the center offset /// Target to paste the public static void CopyCenterToCenter(this Mat src, Size size, Mat dst) { var srcRoi = src.RoiFromCenter(size); CopyToCenter(srcRoi, dst); } /// /// Copy a region from to center of other /// /// Source to be copied to /// Region to copy /// Target to paste the public static void CopyRegionToCenter(this Mat src, Rectangle region, Mat dst) { var srcRoi = src.Roi(region); CopyToCenter(srcRoi, dst); } /// /// Copy a to center of other /// /// Source to be copied to /// Target to paste the public static void CopyToCenter(this Mat src, Mat dst) { var srcStep = src.GetRealStep(); var dstStep = dst.GetRealStep(); var dx = Math.Abs(dstStep - srcStep) / 2; var dy = Math.Abs(dst.Height - src.Height) / 2; if (src.Size == dst.Size) { src.CopyTo(dst); return; } if (dstStep > srcStep && dst.Height > src.Height) { using var dstRoi = dst.Roi(new Rectangle(dx, dy, src.Width, src.Height)); src.CopyTo(dstRoi); return; } if (dstStep < srcStep && dst.Height < src.Height) { using var srcRoi = src.Roi(new Rectangle(dx, dy, dst.Width, dst.Height)); srcRoi.CopyTo(dst); return; } throw new InvalidOperationException("Unable to copy, out of bounds"); } public static void CopyAreasSmallerThan(this Mat src, double threshold, Mat dst) { if (threshold <= 1) return; using var contours = src.FindContours(out var hierarchy, RetrType.Tree); var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); var mask = src.NewBlank(); uint drawContours = 0; foreach (var contourGroup in contourGroups) { using var selectedContours = new VectorOfVectorOfPoint(); foreach (var group in contourGroup) { var area = EmguContours.GetContourArea(group); if (area >= threshold) continue; drawContours++; selectedContours.Push(group); } if (selectedContours.Size == 0) continue; CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); } if (drawContours > 0) src.CopyTo(dst, mask); } public static void CopyAreasLargerThan(this Mat src, double threshold, Mat dst) { if (threshold <= 0) return; using var contours = src.FindContours(out var hierarchy, RetrType.Tree); var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); var mask = src.NewBlank(); uint drawContours = 0; foreach (var contourGroup in contourGroups) { using var selectedContours = new VectorOfVectorOfPoint(); foreach (var group in contourGroup) { var area = EmguContours.GetContourArea(group); if (area <= threshold) continue; drawContours++; selectedContours.Push(group); } if (selectedContours.Size == 0) continue; CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); } if(drawContours > 0) src.CopyTo(dst, mask); } #endregion #region Roi methods /// /// Gets a Roi, but return source when roi is empty or have same size as source /// /// /// /// public static Mat Roi(this Mat mat, Rectangle roi) { return roi.IsEmpty || roi.Size == mat.Size ? mat : new Mat(mat, roi); } /// /// Gets a Roi at x=0 and y=0 given a size, but return source when roi is empty or have same size as source /// /// /// /// public static Mat Roi(this Mat mat, Size size) { return size.IsEmpty || size == mat.Size ? mat : new Mat(mat, new(Point.Empty, size)); } /// /// Gets a Roi from a mat size, but return source when roi is empty or have same size as source /// /// /// /// public static Mat Roi(this Mat mat, Mat fromMat) => mat.Roi(fromMat.Size); /// /// Gets a Roi from center, but return source when have same size as source /// /// /// /// public static Mat RoiFromCenter(this Mat mat, Size size) { if(mat.Size == size) return mat; var newRoi = mat.Roi(new Rectangle( mat.Width / 2 - size.Width / 2, mat.Height / 2 - size.Height / 2, size.Width, size.Height )); return newRoi; } /// /// Gets a new mat obtained from it center at a target size and roi /// /// /// /// /// public static Mat NewMatFromCenterRoi(this Mat mat, Size targetSize, Rectangle roi) { if (targetSize == mat.Size) return mat.Clone(); var newMat = InitMat(targetSize); var roiMat = mat.Roi(roi); //int xStart = mat.Width / 2 - targetSize.Width / 2; //int yStart = mat.Height / 2 - targetSize.Height / 2; var newMatRoi = newMat.RoiFromCenter(roi.Size); /*var newMatRoi = new Mat(newMat, new Rectangle( targetSize.Width / 2 - roi.Width / 2, targetSize.Height / 2 - roi.Height / 2, roi.Width, roi.Height ));*/ roiMat.CopyTo(newMatRoi); return newMat; } #endregion #region Is methods /// /// Gets if a is all zeroed by a threshold /// /// /// Pixel brightness threshold /// Start pixel position /// Pixel span length /// public static bool IsZeroed(this Mat mat, byte threshold = 0, int startPos = 0, int length = 0) { return mat.FindFirstPixelGreaterThan(threshold, startPos, length) == -1; } #endregion #region Find methods /// /// Finds the first negative (Black) pixel /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstNegativePixel(this Mat mat, int startPos = 0, int length = 0) { return mat.FindFirstPixelEqualTo(0, startPos, length); } /// /// Finds the first positive pixel /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPositivePixel(this Mat mat, int startPos = 0, int length = 0) { return mat.FindFirstPixelGreaterThan(0, startPos, length); } /// /// Finds the first pixel that is /// /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPixelEqualTo(this Mat mat, byte value, int startPos = 0, int length = 0) { var span = mat.GetDataByteSpan(); if (length <= 0) length = span.Length; for (var i = startPos; i < length; i++) { if (span[i] == value) return i; } return -1; } /// /// Finds the first pixel that is at less than /// /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPixelLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) { var span = mat.GetDataByteSpan(); if (length <= 0) length = span.Length; for (var i = startPos; i < length; i++) { if (span[i] < value) return i; } return -1; } /// /// Finds the first pixel that is at less or equal than /// /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPixelEqualOrLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) { var span = mat.GetDataByteSpan(); if (length <= 0) length = span.Length; for (var i = startPos; i < length; i++) { if (span[i] <= value) return i; } return -1; } /// /// Finds the first pixel that is at greater than /// /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPixelGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) { var span = mat.GetDataByteSpan(); if (length <= 0) length = span.Length; for (var i = startPos; i < length; i++) { if (span[i] > value) return i; } return -1; } /// /// Finds the first pixel that is at equal or greater than /// /// /// /// Start pixel position /// Pixel span length /// Pixel position in the span, or -1 if not found public static int FindFirstPixelEqualOrGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) { var span = mat.GetDataByteSpan(); if (length <= 0) length = span.Length; for (var i = startPos; i < length; i++) { if (span[i] >= value) return i; } return -1; } #endregion #region Transform methods public static void Transform(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) { //var dst = new Mat(src.Size, src.Depth, src.NumberOfChannels); using var translateTransform = new Matrix(2, 3) { [0, 0] = xScale, // xScale [1, 1] = yScale, // yScale [0, 2] = xTrans, //x translation + compensation of x scaling [1, 2] = yTrans // y translation + compensation of y scaling }; CvInvoke.WarpAffine(src, src, translateTransform, dstSize.IsEmpty ? src.Size : dstSize, interpolation); } /// /// Rotates a Mat by an angle while keeping the image size /// /// /// Angle in degrees to rotate /// /// public static void Rotate(this Mat src, double angle, Size newSize = default, double scale = 1.0) => Rotate(src, src, angle, newSize, scale); /// /// Rotates a Mat by an angle while keeping the image size /// /// /// /// /// /// public static void Rotate(this Mat src, Mat dst, double angle, Size newSize = default, double scale = 1.0) { if (angle % 360 == 0 && Math.Abs(scale - 1.0) < 0.001) return; if (newSize.IsEmpty) { newSize = src.Size; } var halfWidth = src.Width / 2.0f; var halfHeight = src.Height / 2.0f; using var translateTransform = new Matrix(2, 3); CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); if (src.Size != newSize) { // adjust the rotation matrix to take into account translation translateTransform[0, 2] += newSize.Width / 2.0 - halfWidth; translateTransform[1, 2] += newSize.Height / 2.0 - halfHeight; } CvInvoke.WarpAffine(src, dst, translateTransform, newSize); } /// /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content /// /// /// /// public static void RotateAdjustBounds(this Mat src, double angle, double scale = 1.0) => RotateAdjustBounds(src, src, angle, scale); /// /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content /// /// /// /// /// public static void RotateAdjustBounds(this Mat src, Mat dst, double angle, double scale = 1.0) { if (angle % 360 == 0 && Math.Abs(scale - 1.0) < 0.001) return; var halfWidth = src.Width / 2.0f; var halfHeight = src.Height / 2.0f; using var translateTransform = new Matrix(2, 3); CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); var cos = Math.Abs(translateTransform[0, 0]); var sin = Math.Abs(translateTransform[0, 1]); // compute the new bounding dimensions of the image int newWidth = (int) (src.Height * sin + src.Width * cos); int newHeight = (int) (src.Height * cos + src.Width * sin); // adjust the rotation matrix to take into account translation translateTransform[0, 2] += newWidth / 2.0 - halfWidth; translateTransform[1, 2] += newHeight / 2.0 - halfHeight; CvInvoke.WarpAffine(src, dst, translateTransform, new Size(newWidth, newHeight)); } /// /// Scale image from it center, preserving src bounds /// https://stackoverflow.com/a/62543674/933976 /// /// to transform /// X scale factor /// Y scale factor /// X translation /// Y translation /// Destination size /// Interpolation mode public static void TransformFromCenter(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) { src.Transform(xScale, yScale, xTrans + (src.Width - src.Width * xScale) / 2.0, yTrans + (src.Height - src.Height * yScale) / 2.0, dstSize, interpolation); } /// /// Resize source mat proportional to a scale /// /// /// /// public static void Resize(this Mat src, double scale, Inter interpolation = Inter.Linear) { if (Math.Abs(scale - 1) < 0.001) return; CvInvoke.Resize(src, src, new Size((int) (src.Width * scale), (int) (src.Height * scale)), 0, 0, interpolation); } #endregion #region Draw Methods /// /// Correct openCV thickness which always results larger than specified /// /// Thickness to correct /// public static int CorrectThickness(int thickness) { if (thickness < 3) return thickness; return thickness - 1; } public static void DrawLineAccurate(this Mat src, Point pt1, Point pt2, MCvScalar color, int thickness, LineType lineType = LineType.EightConnected) { /*var deltaX = pt2.X - pt1.X; var deltaY = pt2.Y - pt1.Y; var deg = Math.Atan2(deltaY, deltaX) * (180 / Math.PI); src.DrawRotatedRectangle( new Size(Math.Abs(deltaX), thickness), new Point(pt1.X + deltaX / 2, pt1.Y + deltaY / 2), color, (int)deg, -1, lineType);*/ if (thickness >= 3) { thickness--; /*var lastNumber = thickness % 10; switch (lastNumber) { case 1: case 3: case 4: case 5: case 6: case 7: case 8: case 9: thickness--; break; }*/ } CvInvoke.Line(src, pt1, pt2, color, thickness, lineType); } /// /// Draw a rotated square around a center point /// /// /// /// /// /// /// /// public static void DrawRotatedSquare(this Mat src, int size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) => src.DrawRotatedRectangle(new(size, size), center, color, angle, thickness, lineType); /// /// Draw a rotated rectangle around a center point /// /// /// /// /// /// /// /// public static void DrawRotatedRectangle(this Mat src, Size size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) { if (angle == 0) { src.DrawCenteredRectangle(size, center, color, thickness, lineType); return; } var rect = new RotatedRect(center, size, angle); var vertices = rect.GetVertices(); var points = new Point[vertices.Length]; for (int i = 0; i < vertices.Length; i++) { points[i] = new( (int)Math.Round(vertices[i].X), (int)Math.Round(vertices[i].Y) ); } if (thickness <= 0) { using var vec = new VectorOfPoint(points); CvInvoke.FillConvexPoly(src, vec, color, lineType); } else { CvInvoke.Polylines(src, points, true, color, thickness, lineType); } } /// /// Draw a square around a center point /// /// /// /// /// /// /// public static void DrawCenteredSquare(this Mat src, int size, Point center, MCvScalar color, int thickness = -1, LineType lineType = LineType.EightConnected) => src.DrawCenteredRectangle(new Size(size, size), center, color, thickness, lineType); /// /// Draw a rectangle around a center point /// /// /// /// /// /// /// public static void DrawCenteredRectangle(this Mat src, Size size, Point center, MCvScalar color, int thickness = -1, LineType lineType = LineType.EightConnected) { CvInvoke.Rectangle(src, new Rectangle(center.OffsetBy(size.Width / -2, size.Height / -2), size), color, thickness, lineType); } /// /// Draw a polygon given number of sides and length /// /// /// Number of polygon sides, Special: use 1 to draw a line and >= 100 to draw a native OpenCV circle /// Radius /// Center position /// /// /// /// /// public static void DrawPolygon(this Mat src, int sides, int radius, Point center, MCvScalar color, double startingAngle = 0, int thickness = -1, LineType lineType = LineType.EightConnected, FlipType? flip = null) { if (sides == 1) { var point1 = center with {X = center.X - radius}; var point2 = center with {X = center.X + radius}; point1 = point1.Rotate(startingAngle, center); point2 = point2.Rotate(startingAngle, center); if (flip is FlipType.Horizontal or FlipType.Both) { var newPoint1 = new Point(point2.X, point1.Y); var newPoint2 = new Point(point1.X, point2.Y); point1 = newPoint1; point2 = newPoint2; } if (flip is FlipType.Vertical or FlipType.Both) { var newPoint1 = new Point(point1.X, point2.Y); var newPoint2 = new Point(point2.X, point1.Y); point1 = newPoint1; point2 = newPoint2; } CvInvoke.Line(src, point1, point2, color, thickness < 1 ? 1 : thickness, lineType); return; } if (sides >= 100) { CvInvoke.Circle(src, center, radius, color, thickness, lineType); return; } var points = DrawingExtensions.GetPolygonVertices(sides, radius, center, startingAngle, flip is FlipType.Horizontal or FlipType.Both, flip is FlipType.Vertical or FlipType.Both); if (thickness <= 0) { using var vec = new VectorOfPoint(points); CvInvoke.FillConvexPoly(src, vec, color, lineType); } else { CvInvoke.Polylines(src, points, true, color, thickness, lineType); } } #endregion #region Text methods public enum PutTextLineAlignment : byte { /// /// Left aligned without trimming, openCV default call /// None, /// /// Left aligned and trimmed /// Left, /// /// Center aligned and trimmed /// Center, /// /// Right aligned and trimmed /// Right } public static string PutTextLineAlignmentTrim(string line, PutTextLineAlignment lineAlignment) { switch (lineAlignment) { case PutTextLineAlignment.None: return line.TrimEnd(); case PutTextLineAlignment.Left: case PutTextLineAlignment.Center: case PutTextLineAlignment.Right: return line.Trim(); default: throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null); } } public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, ref int baseLine, PutTextLineAlignment lineAlignment = default) => GetTextSizeExtended(text, fontFace, fontScale, thickness, 0, ref baseLine, lineAlignment); public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, int lineGapOffset, ref int baseLine, PutTextLineAlignment lineAlignment = default) { text = text.TrimEnd('\n', '\r', ' '); var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); if (lines.Length is 0 or 1) return textSize; var lineGap = textSize.Height / 3 + lineGapOffset; var width = 0; var height = lines.Length * (lineGap + textSize.Height) - lineGap; for (var i = 0; i < lines.Length; i++) { lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); if (string.IsNullOrWhiteSpace(lines[i])) continue; int baseLineRef = 0; var lineSize = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); width = Math.Max(width, lineSize.Width); } return new(width, height); } public static void PutTextExtended(this IInputOutputArray src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) => src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, lineAlignment); /// /// Extended OpenCV PutText to accepting line breaks and line alignment /// public static void PutTextExtended(this IInputOutputArray src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) { text = text.TrimEnd('\n', '\r', ' '); var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); switch (lines.Length) { case 0: return; case 1: CvInvoke.PutText(src, text, org, fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); return; } // Get height of text lines in pixels (height of all lines is the same) int baseLine = 0; var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); var lineGap = textSize.Height / 3 + lineGapOffset; var linesSize = new Size[lines.Length]; int width = 0; // Sanitize lines for (var i = 0; i < lines.Length; i++) { lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); } // If line needs alignment, calculate the size for each line if (lineAlignment is not PutTextLineAlignment.Left and not PutTextLineAlignment.None) { for (var i = 0; i < lines.Length; i++) { if (string.IsNullOrWhiteSpace(lines[i])) continue; int baseLineRef = 0; linesSize[i] = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); width = Math.Max(width, linesSize[i].Width); } } for (var i = 0; i < lines.Length; i++) { if(string.IsNullOrWhiteSpace(lines[i])) continue; int x = lineAlignment switch { PutTextLineAlignment.None or PutTextLineAlignment.Left => org.X, PutTextLineAlignment.Center => org.X + (width - linesSize[i].Width) / 2, PutTextLineAlignment.Right => org.X + width - linesSize[i].Width, _ => throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null) }; // Find total size of text block before this line var lineYAdjustment = i * (lineGap + textSize.Height); // Move text down from original line based on line number int lineY = !bottomLeftOrigin ? org.Y + lineYAdjustment : org.Y - lineYAdjustment; CvInvoke.PutText(src, lines[i], new Point(x, lineY), fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); } } /// /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation /// public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default, double angle = 0) => src.PutTextRotated(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, lineAlignment, angle); /// /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation /// public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default, double angle = 0) { if (angle % 360 == 0) // No rotation needed, cheaper cycle { src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); return; } using var rotatedSrc = src.Clone(); rotatedSrc.RotateAdjustBounds(-angle); var sizeDifference = (rotatedSrc.Size - src.Size).Half(); org.Offset(sizeDifference.ToPoint()); org = org.Rotate(-angle, new Point(rotatedSrc.Size.Width / 2, rotatedSrc.Size.Height / 2)); rotatedSrc.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); using var mask = rotatedSrc.NewBlank(); mask.PutTextExtended(text, org, fontFace, fontScale, WhiteColor, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); rotatedSrc.Rotate(angle, src.Size); mask.Rotate(angle, src.Size); rotatedSrc.CopyTo(src, mask); } #endregion #region Other Images Types /// /// From gets the SVG path's. Tags are not included. /// /// /// Compression method for the contours /// True to binary threshold first /// Array of path's public static IEnumerable GetSvgPath(this Mat src, ChainApproxMethod compression = ChainApproxMethod.ChainApproxSimple, bool threshold = true) { var mat = src; if (threshold) { mat = new(); CvInvoke.Threshold(src, mat, 127, byte.MaxValue, ThresholdType.Binary); } using var contours = mat.FindContours(out var hierarchy, RetrType.Tree, compression); var sb = new StringBuilder(); for (int i = 0; i < contours.Size; i++) { if (hierarchy[i, EmguContour.HierarchyParent] == -1) // Top hierarchy { if (sb.Length > 0) { yield return sb.ToString(); sb.Clear(); } } else { sb.Append(" "); } sb.Append($"M {contours[i][0].X} {contours[i][0].Y} L"); for (int x = 1; x < contours[i].Size; x++) { sb.Append($" {contours[i][x].X} {contours[i][x].Y}"); } sb.Append(" Z"); } if (sb.Length > 0) { yield return sb.ToString(); sb.Clear(); } if (!ReferenceEquals(src, mat)) mat.Dispose(); } #endregion #region Utilities methods /// /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. /// The function modifies the source image content /// /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content /// Retrieval mode /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context /// The contour hierarchy public static VectorOfVectorOfPoint FindContours(this IInputOutputArray mat, RetrType mode = RetrType.List, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) { using var hierarchy = new Mat(); var contours = new VectorOfVectorOfPoint(); CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); return contours; } /* /// /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. /// The function modifies the source image content /// /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content /// Detected contours. Each contour is stored as a vector of points. /// Retrieval mode /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context /// The contour hierarchy public static int[,] FindContours(this Mat mat, IOutputArray contours, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) { using var hierarchy = new Mat(); CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); var numArray = new int[hierarchy.Cols, 4]; var gcHandle = GCHandle.Alloc(numArray, GCHandleType.Pinned); using (var mat2 = new Mat(hierarchy.Rows, hierarchy.Cols, hierarchy.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchy.Step)) hierarchy.CopyTo(mat2); gcHandle.Free(); return numArray; }*/ /// /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. /// The function modifies the source image content /// /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content /// The contour hierarchy /// Retrieval mode /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context /// Detected contours. Each contour is stored as a vector of points. public static VectorOfVectorOfPoint FindContours(this IInputOutputArray mat, out int[,] hierarchy, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) { var contours = new VectorOfVectorOfPoint(); using var hierarchyMat = new Mat(); CvInvoke.FindContours(mat, contours, hierarchyMat, mode, method, offset); hierarchy = new int[hierarchyMat.Cols, 4]; if (contours.Size == 0) return contours; var gcHandle = GCHandle.Alloc(hierarchy, GCHandleType.Pinned); using (var mat2 = new Mat(hierarchyMat.Rows, hierarchyMat.Cols, hierarchyMat.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchyMat.Step)) hierarchyMat.CopyTo(mat2); gcHandle.Free(); return contours; } /// /// Determine the area (i.e. total number of pixels in the image), /// initialize the output skeletonized image, and construct the /// morphological structuring element /// /// /// Number of iterations required to perform the skeletoize /// /// public static Mat Skeletonize(this Mat src, out int iterations, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) { if (ksize.IsEmpty) ksize = new Size(3, 3); var skeleton = src.NewBlank(); var kernel = Kernel3x3Rectangle; var image = src; using var temp = new Mat(); iterations = 0; while (true) { iterations++; // erode and dilate the image using the structuring element using var eroded = new Mat(); CvInvoke.Erode(image, eroded, kernel, AnchorCenter, 1, BorderType.Reflect101, default); CvInvoke.Dilate(eroded, temp, kernel, AnchorCenter, 1, BorderType.Reflect101, default); // subtract the temporary image from the original, eroded // image, then take the bitwise 'or' between the skeleton // and the temporary image CvInvoke.Subtract(image, temp, temp); CvInvoke.BitwiseOr(skeleton, temp, skeleton); image = eroded.Clone(); // if there are no more 'white' pixels in the image, then // break from the loop if (CvInvoke.CountNonZero(image) == 0) break; } return skeleton; } /// /// Determine the area (i.e. total number of pixels in the image), /// initialize the output skeletonized image, and construct the /// morphological structuring element /// /// /// /// public static Mat Skeletonize(this Mat src, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) => src.Skeletonize(out _, ksize, elementShape); #endregion #region Kernel methods /// /// Reduces iterations to 1 and generate a kernel to match the iterations effect /// /// /// /// public static Mat GetDynamicKernel(ref int iterations, ElementShape elementShape = ElementShape.Ellipse) { var size = Math.Max(iterations, 1) * 2 + 1; iterations = 1; return CvInvoke.GetStructuringElement(elementShape, new Size(size, size), AnchorCenter); } #endregion #region Disposes /// /// Dispose this if it's a sub matrix / roi /// /// Mat to dispose public static void DisposeIfSubMatrix(this Mat mat) { if(mat.IsSubmatrix) mat.Dispose(); } #endregion }