Mat
OpenCV C++ n-dimensional dense array class (cv::Mat) OpenCV c++ n维稠密数组类(cv::Mat)
1.矩阵类型:
例:
MatType 的 MatType.CV_8UC1 等
U 是无符号整数(uint) ,8在前则是8位无符号整数,C是通道(channel), 1是通道数,
MatType.CV_8UC1 的可能是 0~255数值的,透明通道 或 灰度图通道的
还有 MatType.CV_32FC3 等
F 是浮点数(float)
S 是有符号整数(short integer)
U 是无符号整数(uint)
C是通道(channel)
2.构造函数
15个重载
new Mat( 指针)
new Mat()
new Mat(Mat对象)
new Mat(高,宽,矩阵的类型)
new Mat(地址,读取模式)
构造二维矩阵并用指定的标量值填充它:
new Mat(高,宽, MatType type, Scalar s)
指向用户分配数据的矩阵头的构造函数:
Mat(高,宽, MatType type, Array data, 开始的位置index)
。。。。。。。。。
预处理:
按训练好的模型的输入张量需要,要和训练时用的数据一样处理
范围:
有些-1~1 范围的 推理结果比较好,
var frame_size = new OpenCvSharp.Size(frame_w, frame_h);
Mat new_background = new Mat("models/bg.jpg").Resize(frame_size);
new_background.ConvertTo(new_bg_frame, MatType.CV_32FC3, 1 / 255.0f,0);
转对应维度输入张量......
有些是0~1(归一化),
var frame_size = new OpenCvSharp.Size(frame_w, frame_h);
Mat new_background = new Mat("models/bg.jpg").Resize(frame_size);
new_background.ConvertTo(new_bg_frame, MatType.CV_32FC3, 1 / 127.5.0f,-1);
转对应维度输入张量......
有些归一化 的同时还
均值(Mean)中心化: 对于每个通道,你将图像中的每个像素值减去该通道的均值。这一步的目的是将数据的中心移到0点,基本上是在去除数据中的偏置。
标准差(Standard Deviation)缩放: 经过第一步处理后的值将再除以该通道的标准差。这一步的目的是在通道间规范化数据的尺度,即让不同的特征具有相似的尺度。
input_tensor = new DenseTensor<float>(new[] { 1, 3, 512, 512 });
for (int y = 0; y < resize_image.Height; y++)
{
for (int x = 0; x < resize_image.Width; x++)
{
input_tensor[0, 0, y, x] = (resize_image.At<Vec3b>(y, x)[0] / 255f - 0.485f) / 0.229f;
input_tensor[0, 1, y, x] = (resize_image.At<Vec3b>(y, x)[1] / 255f - 0.456f) / 0.224f;
input_tensor[0, 2, y, x] = (resize_image.At<Vec3b>(y, x)[2] / 255f - 0.406f) / 0.225f;
}
}这样预处理后,图像在训练过程中效果更好;然后这样训练出的模型,这样预处理再去推理就效果更好
有些直接0~255
精度优化:
如果要精度更高, 大于的输入张量的宽高维度,则等比缩放 后 填充,小于则填充
int height = srcimg.Rows;
int width = srcimg.Cols;
Mat temp_image = srcimg.Clone();
if (height > input_height || width > input_width)
{
float scale = Math.Min((float)input_height / height, (float)input_width / width);
Size new_size = new Size((int)(width * scale), (int)(height * scale));
Cv2.Resize(srcimg, temp_image, new_size);
ratio_height = ratio_width = scale;
Trace.WriteLine("scale:"+ scale);
}
else
ratio_height = ratio_width = 1;//填充,参数3到6 分别是上下左右填充的像素,BorderTypes.Constant常量,恒定填充参数8
Cv2.CopyMakeBorder(temp_image, temp_image, 0, input_height - temp_image.Rows, 0, input_width - temp_image.Cols, BorderTypes.Constant, 0);
处理范围....
/*全局变量ratio_height ,ratio_width ,记下后,在推理完毕后,计算在原图数据*/
处理好的Mat图像矩阵 转 输入张量:
//转NCHW维度的张量
var channel_size = 640 * 640;
Mat[] bgrChannels = Cv2.Split(temp_image);
Parallel.For(0, 3, c =>
{// 按实际要的范围计算
bgrChannels[c].ConvertTo(bgrChannels[c], MatType.CV_32FC1, 1 / 127.5f, -1);
Marshal.Copy(bgrChannels[c].Data, srcInputArray, ((c + 3) % 3) * channel_size, channel_size);
});
my_input_image = srcInputArray;Tensor<float> input_tensor = new DenseTensor<float>(my_input_image, new[] { 1, 3, input_height, input_width });
另外
Cv2.Merge(new Mat[]{bgrChannels[2],bgrChannels[1],bgrChannels[0]}, result); 无法再多线程里运行,速度慢1ms~3ms,所以不用这样的方式排序
后处理:
根据模型输出张量信息处理
只列举遇到过的:
1. pix2pix.onnx 输入输出都是NHWC ,后处理
float[] pdata = 输出张量.AsTensor<float>().ToArray();
float[] floatArray = new float[512 * 512 * 3];
float[] rArray = pdata.Take(size).ToArray();
float[] gArray = pdata.Skip(size).Take(size).ToArray();
float[] bArray = pdata.Skip(size * 2).Take(size).ToArray();Mat r = new Mat(height, width, MatType.CV_32F, rArray);
Mat g = new Mat(height, width, MatType.CV_32F, gArray);
Mat b = new Mat(height, width, MatType.CV_32F, bArray);
实践
ppMattingv2 抠图后处理:
实践1:加入精度96.63%、FPS 63,SOTA人像分割方案PP-HumanSeg v2开箱即用!-CSDN博客
最后一步 高斯模糊 不对
实践代码:
// 对于当前的输出张量,无改良效果
// 边缘检测以生成边缘掩码
Mat edges = new Mat();
Cv2.Canny(mask_out, edges, 100, 200); // 阈值根据需要调整// 生成距离变换图
Mat distTrans = new Mat();
Cv2.DistanceTransform(edges, distTrans, DistanceTypes.L2, DistanceTransformMasks.Mask5);
Cv2.Normalize(distTrans, distTrans, 0, 255, NormTypes.MinMax);// 高斯模糊平滑距离变换图
Cv2.GaussianBlur(distTrans, distTrans, new OpenCvSharp.Size(3, 3), 0);
Cv2.ImShow("GaussianBlur", distTrans);
// 结合边缘渐变到原始掩码
Mat blend = new Mat();
distTrans.ConvertTo(distTrans, MatType.CV_8UC1,255.0f,0);//保持mask_out, distTrans 统一类型才能AddWeighted函数传参
Cv2.AddWeighted(mask_out, 0.5, distTrans, 0.5, 0, blend);
Cv2.ImShow("blend", blend);
Mat eMat = new Mat();
Cv2.Add(mask_out, blend, eMat);
//这个可以去掉外部部分细线,或小的杂物,内部填充少许但边缘处理不行
// 阈值操作
Cv2.Threshold(mask_out, mask_out, 0, 255, ThresholdTypes.Binary);// mask_out.ConvertTo(mask_out, MatType.CV_8UC1);
// 创建一个结构元素,用来腐蚀 膨胀等操作的精度
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 15),new OpenCvSharp.Point(-1,-1));
//Mat element2 = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(25, 5), new OpenCvSharp.Point(-1, -1));// 闭运算 获取去除内部细节的图
Mat neiBiHeMat= new Mat();
Cv2.MorphologyEx(mask_out, neiBiHeMat, MorphTypes.Close, Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(3, 3)) );
// Cv2.MorphologyEx(neiBiHeMat, neiBiHeMat, MorphTypes.Close, Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 1)));
//Cv2.ImShow("neiBiHeMat", neiBiHeMat);
// 执行顶帽运算 获取去除内部细节的图的外部细节
Mat topHat = new Mat();
Cv2.MorphologyEx(neiBiHeMat, topHat, MorphTypes.TopHat, element);
Cv2.ImShow("外部细节", topHat);// 获取去除内部细节的图 去掉 去除 内部细节的图》的外部细节
Cv2.Subtract(neiBiHeMat, topHat, mask_out);
Cv2.GaussianBlur(mask_out, mask_out, new OpenCvSharp.Size(5, 5), 0);
//mask_out = neiBiHeMat;if (mask_out.Type() != MatType.CV_32FC1)
{
mask_out.ConvertTo(mask_out, MatType.CV_32FC1,1/255.0f,0);
}
Cv2.ImShow("mask handler11111111", mask_out);
int size = mask_out.Rows * mask_out.Cols;
//float[] mask_float_array = new float[size];
unsafe
{
System.Runtime.InteropServices.Marshal.Copy(new IntPtr(mask_out.DataPointer), phaArray, 0, size);
}去掉了部分非主体细节,但是方块化了,不平滑了
=========================================
3. 函数
1. 转换函数
(1)mat转其它数据/通道的mat
mat对象.ConvertTo()
///通过可选缩放将数组转换为另一种数据类型, 转换时第三参数还可以乘(也可以乘分数1/255.0f), 第四参数可以 偏移(减)
mat对象.ConvertTo(OutputArray m, MatType rtype, double alpha = 1, double beta = 0)
应用:
var frame_size = new OpenCvSharp.Size(frame_w, frame_h);
Mat new_background = new Mat("models/bg.jpg").Resize(frame_size);
new_background.ConvertTo(new_bg_frame, MatType.CV_32FC3, 1 / 255.0f,0);
其中可以
0~1 第三/四参数 1/255.0f 0
-1~1 第三/四参数 1/127.5f -1
(2)张量 转 数组
float[] pdata = 张量.AsTensor<float>().ToArray();
(3)Mat 转 数组
Marshal.Copy(mat.Data,arr, 开始复制的起点索引, 复制长度);
(4) 数组 转 Mat
var mat= new Mat(高,宽, MatType type, Array data, 开始的位置index)
① rbg数组 转 Mat
var mat= new Mat(高,宽, MatType.8UC3,rbg数组)
② Point2f 数组 转 Mat 及 反转
var mat= new Mat(Point2f 数组长度, 1, MatType.CV_32FC2, Point2f 数组 );
和
Point2f[] face68landmarks;
mat.GetArray<Point2f>(out face68landmarks);
需要注意的是,输出张量和mat 的排列方式,
rrrrrrrrrrrrrrrrrrrrrrrrgggggggggggggggggbbbbbbbbbbbbbbbb 是输出张量 转数组的排列方式
而bgrbgrbgrbgrbgrbgrbgrbgrbgrbgrbgrbgrbgr 是mat图像的排列方式
组合 输出张量 转 数组 ------ 数组 转 Mat
(1)
//pdata是输出张量转的数组,
float[] temp_r = new float[channel_step];
float[] temp_g = new float[channel_step];
float[] temp_b = new float[channel_step];
Array.Copy(pdata, temp_b, channel_step);
Array.Copy(pdata, channel_step, temp_g, 0, channel_step);
Array.Copy(pdata, channel_step * 2, temp_r, 0, channel_step);
Mat rmat = new Mat(128, 128, MatType.CV_32FC1, temp_r);
Mat gmat = new Mat(128, 128, MatType.CV_32FC1, temp_g);
Mat bmat = new Mat(128, 128, MatType.CV_32FC1, temp_b);
Mat result = new Mat();
Cv2.Merge(new Mat[] { bmat * 255.0f, gmat * 255.0f, rmat * 255.0f }, result);
(5) 转换Mat到NDArray
// 转换Mat到NDArray
byte[] imageData = new byte[image.Rows* image.Cols* 3];
Marshal.Copy(mat.Data, imageData,0,imageData.Length);
var ndArray = (np.array(imageData).astype(np.float32)/ p_number).reshape(image.Height, image.Width, 3);// NDArray ndArray = image.ToNdArray(); //0.20.5 版本可以0.30.0报错不全
封装,利用NumSharp库转换顺序(速度非最快):
public DenseTensor<float> ProcessImage(Mat image, float p_number=255.0f) { // 确保图像是8位无符号整数类型 if (image.Type() != MatType.CV_8UC3) { throw new InvalidOperationException("图像类型必须是8位无符号整数(CV_8UC3)"); } Mat mat=new Mat(); // 将图像从BGR转换为RGB Cv2.CvtColor(image, mat, ColorConversionCodes.BGR2RGB); // 转换Mat到NDArray byte[] imageData = new byte[image.Rows* image.Cols* 3]; Marshal.Copy(mat.Data, imageData,0,imageData.Length); var ndArray = (np.array(imageData).astype(np.float32)/ p_number).reshape(image.Height, image.Width, 3); ; // NDArray ndArray = image.ToNdArray(); //0.20.5 版本可以0.30.0报错不全 // 调整维度顺序从[H, W, C]到[C, H, W](256x456是示例尺寸) int[] dims = { 2, 0, 1 }; ndArray = ndArray.transpose(dims); // 展平NDArray到1D var flattenedArray = ndArray.flatten().ToArray<float>(); //float[] flattenedArray = ndArray.ToArray<float>(); // 创建DenseTensor var inputTensor = new DenseTensor<float>(flattenedArray, new[] { 1, 3, image.Height, image.Width }); return inputTensor; }
2.重新设置宽高函数 Cv2.Resize()
Cv2.Resize(image_rgb, resize_image, new OpenCvSharp.Size(宽, 高));
2. 过滤 0~255 或 其它范围
public Mat ClampMatValues(Mat mat,float sV=0,float eV=255) { Mat result = mat.Clone(); // 使用Cv2.Min和Cv2.Max限制矩阵内的值 // 首先,确保所有值都大于等于sV Cv2.Max(result, new Scalar(sV), result); // 然后,确保所有值都小于等于eV Cv2.Min(result, new Scalar(eV), result); return result; }
3.映射 Cv2.Normalize()
//把原来的值 缩放到对应范围,比如0,10,510 映射到 0 ,5,255
Cv2.Normalize(mat, out_mat, 0, 255, NormTypes.MinMax);
4. mat[] 合成 mat Cv2.Merge()
//mat[] 组成mat ,多个单通道 组合成 多通道
Cv2.Merge(bgrChannels, crop_img);
5. 变换矩阵 相关
例如facefusion 的过程用到:
Mat affine_matrix = new Mat(2, 3, MatType.CV_32FC1,
new float[] { scale, 0.0f, translation[0], 0.0f, scale, translation[1] } );
[a b tx] [c d ty] /* [scale 0.0f translation[0]] [0.0f scale translation[1]]*/ a 和 d 是水平和竖直方向的缩放因子,分别控制图像的水平和垂直缩放。 b 和 c 是错切因子,控制图像的变形。 tx 和 ty 是位移因子,控制图像的平移。tx 控制水平平移,ty 控制垂直平移。
(1) Cv2.WarpAffine()
a 和 d 是水平和竖直方向的缩放因子,分别控制图像的水平和垂直缩放。
b 和 c 是错切因子,控制图像的变形。
tx 和 ty 是位移因子,控制图像的平移。tx 控制水平平移,ty 控制垂直平移。
6. 运算
参考图解:
Mat src = Cv2.ImRead("test.jpg");
Mat element = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(3, 3));
Mat dst = new Mat();
// 腐蚀
Mat erodedImage = new Mat();
Cv2.Erode(src, erodedImage, element);
// 膨胀
Mat dilatedImage = new Mat();
Cv2.Dilate(src, dilatedImage, element);
// 开运算
Cv2.MorphologyEx(src, dst, MorphTypes.Open, element);
// 闭运算
Cv2.MorphologyEx(src, dst, MorphTypes.Close, element);
// 顶帽运算
Cv2.MorphologyEx(src, dst, MorphTypes.TopHat, element);
// 黑帽运算
Cv2.MorphologyEx(src, dst, MorphTypes.BlackHat, element);
// 梯度运算
Cv2.MorphologyEx(src, dst, MorphTypes.Gradient, element);
1. Mat 对象通道排序转换
Cv2.CvtColor(mat_image, mat_image, ColorConversionCodes.BGR2RGB);
mat_image输入, 改变默认的BGR 排序 到 BGR, 输出mat_image
也可以 ColorConversionCodes.BGR2其它
2. OpenCVSharp4 的加减乘除
除:
Cv2.Divide(resize_image, div, buf, 1.0f, MatType.CV_32FC3);
Cv2.Divide(被除数, 除数, 结果, 缩放因子, 输出矩阵类型);
3. 预处理的 Mat图像数据归一化
NCHW 的输入张量 数据归一化(0~1)
(1) OpenCV函数来计算
Mat buf = new Mat(inpHeight, inpWidth, MatType.CV_32FC3, new Scalar(0.0f)); Mat div = new Mat(inpHeight, inpWidth, resize_image.Type(), new Scalar(255, 255, 255)); float[] rgbArray = new float[inpWidth* inpHeight * 3];
//Console.WriteLine( $"{ div.Channels()} {resize_image.Channels()}");
获取并输出resize_image的宽度和高度
//Console.WriteLine("resize_image的宽度: " + resize_image.Width);
//Console.WriteLine("resize_image的高度: " + resize_image.Height);
获取并输出div的宽度和高度
//Console.WriteLine("div的宽度: " + div.Width);
//Console.WriteLine("div的高度: " + div.Height);// 上面输出信息保证一致后,可以执行除, 参数3 Cv2.Divide(resize_image, div, buf, 1.0f, MatType.CV_32FC3); Mat[] channels = Cv2.Split(buf); //B,G,R Marshal.Copy(channels[2].Data, rgbArray, 0, channel_size);//R Marshal.Copy(channels[1].Data, rgbArray, channel_size, channel_size);//G Marshal.Copy(channels[0].Data, rgbArray, channel_size * 2, channel_size);//B Marshal.Copy(resize_image.Data, rgbArray, 0,512*512*3);
/*resize_image: 被除数,通常是一个Mat对象,此处看起来像是一个已经缩放的图像。
div: 除数,可以是一个标量、向量或与resize_image相同尺寸的Mat对象。
buf: 输出结果,通常也是一个Mat对象。在这里,div操作的结果将会存放在这个变量中。
1.0f: 缩放因子,除法的结果会乘以这个因子。在这里设置为1.0,意味着不改变计算的结果。
MatType.CV_32FC3: 输出矩阵的类型。这里指定了三通道浮点数Mat,每个通道32位浮点数。*/
(2) 多线程+指针
unsafe //NCHW
{
byte* resizeImagePtr = (byte*)resize_image.Data;
Parallel.For(0, 256, y =>
{
//逐行获取
for (int x = 0; x < 456; x++)
{
int pixelOffset = (x + y * 456) * 3;//x + y * 512 即第y行横向第x个(每多一列就对1个宽度数量的像素),再存RBG就乘3
// 假设模型在训练时使用了[0, 1]范围的归一化
rgbArray[x + y * 456 + 456 * 256 * 2] = *(resizeImagePtr + pixelOffset) / 255.0f; // B
rgbArray[x + y * 456 + 456 * 256] = *(resizeImagePtr + pixelOffset + 1) / 255.0f; // G
rgbArray[x + y * 456 + 0] = *(resizeImagePtr + pixelOffset + 2) / 255.0f; // R
}
});
}
(3) 易懂易看的写法(无加速,操作张量)
input_tensor = new DenseTensor<float>(new[] { inpHeight, inpWidth, 3 });
for (int i = 0; y< inpHeight; y++)
{
for (int j = 0; x < inpWidth; x++)
{
// 获取像素值
Vec3b pixel = resize_image.Get<Vec3b>(x, y);
// 分别提取 R、G、B 值,在 OpenCV 中,像素值的存储顺序是 BGR,而不是 RGB
byte r = pixel.Item2;
byte g = pixel.Item1;
byte b = pixel.Item0;
input_tensor[j, i, 0] = r / 255.0f;
input_tensor[j, i, 1] = g / 255.0f;
input_tensor[j, i, 2] = b / 255.0f;}
}
4. 后处理
(1)张量转Bitmap图片
static Bitmap TensorToImage_katong(DenseTensor<float> tensor)
{int height = tensor.Dimensions[2];
int width = tensor.Dimensions[3];
创建图像对象
Bitmap image = new Bitmap(width, height);
将张量的值转换为像素值
unsafe
{
/*在C#中,使用unsafe关键字可以打开不安全代码块。在不安全代码块中,可以使用指针来直接访问内存,并且不受到C#的内存安全限制。
* 上述示例中,我们使用了unsafe关键字来访问位图的像素数据。当处理大型图像或需要高性能的图像处理操作时,
使用指针操作可以提高性能,因为它绕过了C#中的边界检查和安全性检查。*/
for (int y = 0; y < height; y++)
{for (int x = 0; x < width; x++)
{// 将像素值作为灰度值应用于R、G和B通道
System.Drawing.Color pixel = System.Drawing.Color.FromArgb(r, g, b);
image.SetPixel(x, y, pixel);
}
}
}image.Save("models/output_katong_linshi.png", ImageFormat.Png);
return image;
}
(2) 张量转 Mat图片