news 2026/4/16 15:00:43

使用OpenCvSharp , Emgu.CV 手搓 视觉识别算法 以及 成果展示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用OpenCvSharp , Emgu.CV 手搓 视觉识别算法 以及 成果展示

先讲讲思路,图片是一组庞大的矩阵数据,每一个像素点有用数据为五个分别为RGB(三原色),以及XY坐标。也就是说我们能将整张图片每一个像素点的数据提取出来加以分析。那么就可以做到图片识别。

源代码会在最下方贴出。

首先整个流程思想是这杨

复制代码

#region 6孔混合鱼苗

VisionHelper.Two_Level(@"C:\Users\Administrator\Desktop\右上角三只鱼的提取\小鱼图片2-单个.png", @"C:\Users\Administrator\Desktop\3.1\二级化.png");

VisionHelper.Outline(@"C:\Users\Administrator\Desktop\3.1\二级化.png", @"C:\Users\Administrator\Desktop\3.1\轮廓检测.png");

VisionHelper.CutCircle(@"C:\Users\Administrator\Desktop\3.1\轮廓检测.png", @"C:\Users\Administrator\Desktop\3.1\圆形剪切.bmp");

VisionHelper.ExtractCircle(@"C:\Users\Administrator\Desktop\3.1\圆形剪切.bmp", @"C:\Users\Administrator\Desktop\3.1\圆形提取.jpg");

var data = VisionHelper.GetImagePixel(@"C:\Users\Administrator\Desktop\3.1\圆形提取.jpg");

data = VisionHelper.FishExtract(data, @"C:\Users\Administrator\Desktop\3.1\圆形提取.jpg");

var fish = VisionHelper.FishGroup(data);

fish = VisionHelper.FishDistinct(fish);

data = VisionHelper.FishCenter(fish);

#endregion

复制代码

先将抓取的图片二级化,效果如下所示

VisionHelper.Two_Level(@"C:\Users\Administrator\Desktop\右上角三只鱼的提取\小鱼图片2-单个.png", @"C:\Users\Administrator\Desktop\3.1\二级化.png");

原图左,处理图右

小鱼图片2-单个小鱼图片2-单个-二级化处理

二级化以后,这张图片的数据就只剩黑色和白色,如果二级化时没有损坏到目标特征像素点,那么接下来提取目标特征像素点会很容易,因为只有黑白两色

接下来做轮廓监测,将整个培养皿扫描出来并且去除,这杨就只剩培养皿内的鱼苗和食物或者排泄物

VisionHelper.Outline(@"C:\Users\Administrator\Desktop\3.1\二级化.png", @"C:\Users\Administrator\Desktop\3.1\轮廓检测.png");

效果如下所示

小鱼图片2-单个-二级化处理 小鱼图片2-单个-轮廓处理

在图片处理的算法中,我用红圈标注了培养皿内的区域,并且用蓝点打出了中心

接下来呢,可以将其他无用的图片区域全部剪切掉,就是图片内圆形切割

VisionHelper.CutCircle(@"C:\Users\Administrator\Desktop\3.1\轮廓检测.png", @"C:\Users\Administrator\Desktop\3.1\圆形剪切.bmp");

切割效果如下图所示

小鱼图片2-单个-轮廓处理image

因为当初代码里设定生成的图片是BMP,上传不了博客,所以这粗糙的截图一下。

可以看到圆形剪切.bmp里只剩培养皿内区域的图片了

之前轮廓处理和圆形剪切形成的红色,蓝色圆圈或者中心点代码里可以设置不写入

那么接下来就是对圆形剪切区域的有用像素进行提取和分析

复制代码

var data = VisionHelper.GetImagePixel(@"C:\Users\Administrator\Desktop\3.1\圆形提取.jpg");

data = VisionHelper.FishExtract(data, @"C:\Users\Administrator\Desktop\3.1\圆形提取.jpg");

var fish = VisionHelper.FishGroup(data);

fish = VisionHelper.FishDistinct(fish);

data = VisionHelper.FishCenter(fish);

复制代码

我先展示下最终的结果

image

经过我进行数据处理后的图片内提取出了三条鱼的中心点位数据

我们校验一下答案

下图1是原图

小鱼图片2-单个

三个小黑点是三条鱼数据正确,坐标是否正确?我用画图工具打开校验

如下三图所示,为了更直观的展示结果,用鼠标浮在指定坐标,手机拍摄的,不是很清楚但是看得清,大家可以双击图片放大

微信图片_20250928141219

第一条数据151,77,在图内鼠标右上角指向的小鱼苗内

微信图片_20250928141224

第二条数据22,88,在图内鼠标左侧指向的小鱼苗内

微信图片_20250928141228

第三条数据137,148,88,在图内鼠标右下角指向的小鱼苗内

接下来贴出我手搓的核心算法

整个VisionHelper运用了OpenCvSharp和Emgu.CV这两个第三方图片处理框架的算法,所有的方法都可以灵活运用,方法体内的参数可以随着实际需要识别的物体做调整

(源代码里有那么多注释应该就不用在讲解基础框架和算法应用了吧,嘻嘻)

复制代码

using Emgu.CV;

using Emgu.CV.CvEnum;

using Emgu.CV.Structure;

using OpenCvSharp;

using System.Drawing.Drawing2D;

using System.Drawing.Imaging;

using FishVision.Model;

namespace FishVision

{

public class VisionHelper

{

/// <summary>

/// 1.二级化

/// </summary>

/// <param name="oldpath"></param>

/// <param name="newPath"></param>

public static void Two_Level(string oldpath,string newPath)

{

Emgu.CV.Mat image = CvInvoke.Imread(oldpath, Emgu.CV.CvEnum.ImreadModes.Grayscale);

Emgu.CV.Mat mid = new Emgu.CV.Mat();

CvInvoke.Threshold(image, mid, 125, 255, ThresholdType.Binary);//180 改

CvInvoke.Imwrite(newPath, mid);

}

/// <summary>

/// 2.轮廓检测

/// </summary>

public static void Outline(string oldpath, string newPath)

{

//读取图片

var img = Cv2.ImRead(oldpath);

//转换成灰度图

OpenCvSharp.Mat gray = img.CvtColor(ColorConversionCodes.BGR2GRAY);

//阈值操作 阈值参数可以用一些可视化工具来调试得到

OpenCvSharp.Mat ThresholdImg = gray.Threshold(135, 255, ThresholdTypes.Binary);

//Cv2.ImShow("Threshold", ThresholdImg);

//降噪 高斯变化

//Mat gaussImg= ThresholdImg.GaussianBlur(new Size(5, 5), 0.8);

//Cv2.ImShow("GaussianBlur", gaussImg);

//中值滤波降噪

//Mat medianImg = ThresholdImg.MedianBlur(5);

//Cv2.ImShow("MedianBlur", medianImg);

//膨胀+腐蚀

//Mat kernel = new Mat(15, 15, MatType.CV_8UC1);

//Mat DilateImg = ThresholdImg.Dilate(kernel);

////腐蚀处理

//Mat binary = DilateImg.Erode(kernel);

OpenCvSharp.Mat element = Cv2.GetStructuringElement(MorphShapes.Ellipse, new OpenCvSharp.Size(3, 3));

OpenCvSharp.Mat openImg = ThresholdImg.MorphologyEx(MorphTypes.Open, element);

//Cv2.ImShow("Dilate & Erode", openImg);

//设置感兴趣的区域

int x = 0, y = 0, w = img.Width, h = img.Height;

Rect roi = new Rect(x, y, w, h);

OpenCvSharp.Mat ROIimg = new OpenCvSharp.Mat(openImg, roi);

//Cv2.ImShow("ROI Image", ROIimg);

//寻找图像轮廓

OpenCvSharp.Point[][] contours;

HierarchyIndex[] hierachy;

Cv2.FindContours(ROIimg, out contours, out hierachy, RetrievalModes.List, ContourApproximationModes.ApproxTC89KCOS);

//根据找到的轮廓点,拟合椭圆

for (int i = 0; i < contours.Length; i++)

{

//拟合函数必须至少5个点,少于则不拟合

if (contours[i].Length < 150 || contours[i].Length > 200) continue;

//椭圆拟合

var rrt = Cv2.FitEllipse(contours[i]);

//ROI复原

rrt.Center.X += x;

rrt.Center.Y += y;

//画椭圆

Cv2.Ellipse(img, rrt, new Scalar(0, 0, 255), 2, LineTypes.AntiAlias);

//画圆心

Cv2.Circle(img, (int)(rrt.Center.X), (int)(rrt.Center.Y), 4, new Scalar(255, 0, 0), -1, LineTypes.Link8, 0);

}

//Cv2.ImShow("Fit Circle", img);

Cv2.ImWrite(newPath, img);

}

/// <summary>

/// 3.圆形剪切

/// </summary>

/// <param name="oldpath"></param>

/// <param name="newPath"></param>

public static void CutCircle(string oldpath, string newPath)

{

Image<Bgr, Byte> src = new Image<Bgr, byte>(oldpath);

int scale = 1;

if (src.Width > 500)

{

scale = 2;

}

if (src.Width > 1000)

{

scale = 10;

}

if (src.Width > 10000)

{

scale = 100;

}

var size = new System.Drawing.Size(src.Width / scale, src.Height / scale);

Image<Bgr, Byte> srcNewSize = new Image<Bgr, byte>(size);

CvInvoke.Resize(src, srcNewSize, size);

//将图像转换为灰度

Emgu.CV.UMat grayImage = new Emgu.CV.UMat();

CvInvoke.CvtColor(srcNewSize, grayImage, ColorConversion.Bgr2Gray);

//使用高斯滤波去除噪声

CvInvoke.GaussianBlur(grayImage, grayImage, new System.Drawing.Size(3, 3), 3);

//霍夫圆检测

CircleF[] circles = CvInvoke.HoughCircles(grayImage, Emgu.CV.CvEnum.HoughModes.Gradient, 2.0, 200.0, 100.0, 180.0, 5);

Rectangle rectangle = new Rectangle();

float maxRadius = 0;

foreach (CircleF circle in circles)

{

var center = circle.Center;//圆心

var radius = circle.Radius;//半径

if (radius > maxRadius)

{

maxRadius = radius;

rectangle = new Rectangle((int)(center.X - radius) * scale,

(int)(center.Y - radius) * scale,

(int)radius * 2 * scale + scale,

(int)radius * 2 * scale + scale);

}

srcNewSize.Draw(circle, new Bgr(System.Drawing.Color.Blue), 4);

}

//CvInvoke.Imwrite("原始图片.bmp", srcNewSize); //保存原始图片

if (maxRadius == 0)

{

//MessageBox.Show("没有圆形");

}

CvInvoke.cvSetImageROI(srcNewSize.Ptr, rectangle);//设置兴趣点—ROI(region of interest )

var clone = srcNewSize.Clone();

CvInvoke.Imwrite(newPath, clone); //保存结果图

src.Dispose();

srcNewSize.Dispose();

grayImage.Dispose();

}

/// <summary>

/// 4.圆形提取

/// </summary>

public static void ExtractCircle(string oldpath, string newPath)

{

// 加载原始图片

Bitmap originalImage = new Bitmap(oldpath);

int diameter = Math.Min(originalImage.Width, originalImage.Height); // 获取最小边长作为直径

int x = (originalImage.Width - diameter) / 2; // 计算起始x坐标

int y = (originalImage.Height - diameter) / 2; // 计算起始y坐标

// 创建与圆形大小相等的bitmap

Bitmap croppedImage = new Bitmap(diameter, diameter);

using (Graphics g = Graphics.FromImage(croppedImage))

{

g.Clear(Color.LightBlue); // 设置圆圈外的颜色

// 设置高质量插值法

g.InterpolationMode = InterpolationMode.HighQualityBicubic;

// 设置高质量,低速度呈现平滑程度

g.SmoothingMode = SmoothingMode.HighQuality;

g.PixelOffsetMode = PixelOffsetMode.HighQuality;

g.CompositingQuality = CompositingQuality.HighQuality;

// 创建一个圆形路径

using (GraphicsPath path = new GraphicsPath())

{

path.AddEllipse(0, 0, diameter, diameter);

// 设置裁剪区域为圆形路径

g.SetClip(path);

// 从原始图片中绘制圆形区域到新图片

g.DrawImage(originalImage, new Rectangle(0, 0, diameter, diameter), x, y, diameter, diameter, GraphicsUnit.Pixel);

}

}

// 保存剪切后的图片

croppedImage.Save(newPath, ImageFormat.Jpeg);

}

/// <summary>

/// 5.像素提取(默认黑像素)

/// </summary>

/// <param name="img"></param>

/// <returns></returns>

public static List<string> GetImagePixel(string oldpath)//过滤

{

// 加载原始图片

Bitmap img = new Bitmap(oldpath);

//0 黑色

//95 深灰

//240 浅灰

//255 白

List<int> R = new List<int>();

List<int> G = new List<int>();

List<int> B = new List<int>();

List<string> xyList = new List<string>();

for (int y = 0; y < img.Height; y++)

{

for (int x = 0; x < img.Width; x++)

{

var a = img.GetPixel(x, y);

if (a.R == 0 && a.G == 0 && a.B == 0)

{

R.Add(img.GetPixel(x, y).R);

G.Add(img.GetPixel(x, y).G);

B.Add(img.GetPixel(x, y).B);

xyList.Add(x + "|" + y);

}

}

}

return xyList;

}

/// <summary>

/// 6.鱼像素提取

/// </summary>

/// <returns></returns>

public static List<string> FishExtract(List<string> data ,string circleImg)

{

for (int i = 0; i < data.Count; i++)

{

var str = data[i].Split("|");

if (!string.IsNullOrWhiteSpace(data[i]))

{

Bitmap image2 = new Bitmap(circleImg);

//周边检测 6

var list = GetSurroundingPixels(image2, Convert.ToInt32(str[0]), Convert.ToInt32(str[1]));

if (list.Where(a => a.R == 0 && a.G == 0 && a.B == 0).Count() >= 2)//这是鱼像素特征

{

}

else

{

data[i] = string.Empty;

//非鱼

}

}

}

return data;

}

//像素周边检测

public static List<FishVision.Model.Pixel> GetSurroundingPixels(Bitmap bitmap, int x, int y)

{

var result = new List<FishVision.Model.Pixel>();

int width = bitmap.Width;

int height = bitmap.Height;

Color[,] surroundingPixels = new Color[3, 3]; // 3x3 grid including the center pixel

for (int i = -1; i <= 1; i++) // Loop through the 3x3 grid around the center pixel

{

for (int j = -1; j <= 1; j++)

{

int newX = x + i;

int newY = y + j;

// Check if the new coordinates are within the bounds of the image

if (newX >= 0 && newX < width && newY >= 0 && newY < height)

{

surroundingPixels[i + 1, j + 1] = bitmap.GetPixel(newX, newY);

}

else

{

// Optionally, set out-of-bounds pixels to a default color or handle them as needed

surroundingPixels[i + 1, j + 1] = Color.Transparent; // or any other color you prefer

}

}

}

// Use surroundingPixels as needed (e.g., print colors)

for (int i = 0; i < 3; i++) // Printing the surrounding pixels for demonstration purposes

{

for (int j = 0; j < 3; j++)

{

var model = new FishVision.Model.Pixel();

model.X = x + i - 1;

model.Y = y + j - 1;

model.R = surroundingPixels[i, j].R;

model.G = surroundingPixels[i, j].G;

model.B = surroundingPixels[i, j].B;

result.Add(model);

//Console.WriteLine($"Pixel ({x + i - 1}, {y + j - 1}): {surroundingPixels[i, j]}");

}

}

return result;

}

/// <summary>

/// 7.鱼像素去重

/// </summary>

/// <param name="listFish"></param>

/// <returns></returns>

public static List<FishModel> FishDistinct(List<FishModel> listFish)

{

if (listFish != null && listFish.Count() > 0)

{

for (int i = 0; i < listFish.Count; i++)

{

listFish[i].FishIndex = listFish[i].FishIndex.Distinct().ToList();

}

}

return listFish;

}

/// <summary>

/// 8.鱼像素分组

/// </summary>

public static List<FishModel> FishGroup(List<string> data)

{

var fish = new List<FishModel>();

//鱼像素分组

for (int i = 0; i < data.Count; i++)

{

for (int b = 0; b < data.Count; b++)

{

if (!string.IsNullOrWhiteSpace(data[i]) && !string.IsNullOrWhiteSpace(data[b]))

{

var data_i_xy = data[i].Split("|");

var data_b_xy = data[b].Split("|");

if (AreAdjacent(Convert.ToInt32(data_i_xy[0]), Convert.ToInt32(data_i_xy[1]), Convert.ToInt32(data_b_xy[0]), Convert.ToInt32(data_b_xy[1])))//相邻的鱼像素合并一组

{

var entity = fish.Where(a => a.FishIndex.Contains(data[i]) || a.FishIndex.Contains(data[b])).FirstOrDefault();

if (entity != null)

{

entity.FishIndex.Add(data[i]);

entity.FishIndex.Add(data[b]);

}

else

{

FishModel model = new FishModel();

model.FishIndex = new List<string>();

model.FishIndex.Add(data[i]);

model.FishIndex.Add(data[b]);

fish.Add(model);

}

}

}

}

}

return fish;

}

/// <summary>

/// 像素是否相邻

/// </summary>

/// <param name="x1"></param>

/// <param name="y1"></param>

/// <param name="x2"></param>

/// <param name="y2"></param>

/// <returns></returns>

public static bool AreAdjacent(int x1, int y1, int x2, int y2)

{

// 检查x和y坐标之差是否为1,这样可以确保像素是直接相邻的

return (Math.Abs(x1 - x2) <= 1 && Math.Abs(y1 - y2) <= 1) && !(x1 == x2 && y1 == y2);

}

/// <summary>

/// 9.每条鱼的像素群寻找中位值作为轨迹坐标

/// </summary>

/// <param name="listFish"></param>

/// <returns></returns>

public static List<string> FishCenter(List<FishModel> listFish)

{

var result = new List<string>();

if (listFish != null && listFish.Count() > 0)

{

for (int i = 0; i < listFish.Count; i++)

{

//取中位值

var index = -1;

if (listFish[i].FishIndex.Count() % 2 == 0) // 偶数长度

{

index = listFish[i].FishIndex.Count() / 2;

}

else // 奇数长度

{

index = (listFish[i].FishIndex.Count() + 1) / 2;

}

//这条鱼中心坐标

var center = listFish[i].FishIndex[index];

result.Add(center);

}

}

return result;

}

/// <summary>

/// 反向二级化

/// </summary>

/// <param name="oldpath"></param>

/// <param name="newPath"></param>

public static void Two_LevelReversal(string oldpath, string newPath)

{

OpenCvSharp.Mat src = Cv2.ImRead(oldpath, OpenCvSharp.ImreadModes.Grayscale);

OpenCvSharp.Mat dst = new OpenCvSharp.Mat();

// 反向二值化:大于 127 的像素设为 0,其他设为 255

Cv2.Threshold(src, dst, 135, 255, ThresholdTypes.BinaryInv);

Cv2.ImWrite(newPath, dst);

}

/// <summary>

/// 96孔鱼苗高光二级化处理

/// </summary>

/// <param name="oldpath"></param>

/// <param name="newPath"></param>

public static void Two_LevelHeight(string oldpath, string newPath)

{

Emgu.CV.Mat image = CvInvoke.Imread(oldpath, Emgu.CV.CvEnum.ImreadModes.Grayscale);

Emgu.CV.Mat mid2 = new Emgu.CV.Mat();

CvInvoke.Threshold(image, mid2, 180, 255, ThresholdType.Binary);

CvInvoke.Imwrite(newPath, mid2);

}

/// <summary>

/// 斑点检测(用不着)

/// </summary>

/// <param name="mat">图片</param>

/// <param name="resultMat">结果图片</param>

/// <returns>斑点中心点数据</returns>

public static KeyPoint[] SimpleblobDetector(OpenCvSharp.Mat mat, out OpenCvSharp.Mat resultMat)

{

// 转化为灰度图

OpenCvSharp.Mat gray = new OpenCvSharp.Mat();

Cv2.CvtColor(mat, gray, ColorConversionCodes.BGR2GRAY);

// 创建SimpleBlobDetector并设置参数

OpenCvSharp.SimpleBlobDetector.Params parameters = new OpenCvSharp.SimpleBlobDetector.Params();

parameters.BlobColor = 0;//斑点的亮度值,取值为0或255,默认为0,表示只检测黑色斑点。

parameters.FilterByArea = true; // 是否根据斑点的面积进行过滤,默认为true

parameters.MinArea = 10; // 最小的斑点面积,默认为25

parameters.MaxArea = 6000; // 最大的斑点面积,默认为5000

// 创建SimpleBlobDetector

OpenCvSharp.SimpleBlobDetector detector = OpenCvSharp.SimpleBlobDetector.Create(parameters);

// 检测斑点

KeyPoint[] keypoints = detector.Detect(gray);

// 在图像上绘制斑点

resultMat = new OpenCvSharp.Mat();

Cv2.DrawKeypoints(mat, keypoints, resultMat, Scalar.All(-1));

return keypoints;

}

}

}

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 12:59:26

直接给各位上点轨迹跟踪的干货。这次咱们玩个能自定义参考轨迹的二自由度MPC控制器,重点说说怎么让这铁疙瘩在不同路况下都跟得稳当。先整杯咖啡,咱们边调参边唠

可自定义期望轨迹的二自由动力学 MPC 跟踪控制 可以外部导入轨迹 知道x y s 即纵向位置 横向位置 位移量即可 请注意 要跟踪不同的轨迹&#xff0c;同一参数可能效果不一样 因此需要自己调参数保证控制效果最佳&#xff1a; Q矩阵增大可以保证侧向位置跟踪效果变好&#xff0c…

作者头像 李华
网站建设 2026/4/16 12:25:14

33、利用TiMidity搭建卡拉OK系统全攻略

利用TiMidity搭建卡拉OK系统全攻略 1. TiMidity基础介绍 TiMidity本质上是一款MIDI播放器,并非专门的卡拉OK播放器,不过它具备一定的可扩展性,经过配置后也能用于卡拉OK场景。默认情况下,它仅播放MIDI音乐并打印歌词。例如,运行以下命令: $timidity ../54154.mid执行…

作者头像 李华
网站建设 2026/4/16 12:26:54

骨髓来源抑制细胞(MDSC)

骨髓来源抑制细胞(Myeloid-derived suppressor cells, MDSC)分为粒形/多核形MDSCs&#xff08;G-MDSC或PMN-MDSC&#xff09;与MNP样MDSCs&#xff08;M-MDSC&#xff09;。单核吞噬细胞&#xff08;Mononuclear phagocytes (MNPs)&#xff09;包括单核细胞、巨噬细胞和树突状细…

作者头像 李华
网站建设 2026/4/16 12:15:30

14、邮件系统的插件、安全及配置全解析

邮件系统的插件、安全及配置全解析 在当今数字化的时代,邮件系统是我们日常工作和生活中不可或缺的一部分。无论是个人用户收发邮件,还是企业进行业务沟通,一个稳定、安全且功能丰富的邮件系统至关重要。下面将详细介绍邮件系统相关的插件、安全防护以及配置方法。 一、Sq…

作者头像 李华
网站建设 2026/4/16 12:22:08

22、Procmail 正则表达式及高级应用全解析

Procmail 正则表达式及高级应用全解析 正则表达式简介 正则表达式是处理数据的强大工具。在 Procmail 中,正则表达式的实现与其他 UNIX 实用程序略有不同。Procmail 的匹配默认情况下不区分大小写,除非使用 D 标志,并且默认使用多行匹配。 简单来说,正则表达式可以理解为…

作者头像 李华