/*
* 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
}