简介这套VS2008环境下的OpenCVMFC工程用于统计图像中米粒的数量并计算最大米粒的面积与周长适合具备一定C基础、正在学习图像处理轮廓分析和几何测量的开发者。资源包共29个文件以核心源代码.cpp/.h、MFC工程配置.sln/.vcproj、调试运行产物.exe/.pdb以及多幅示例图片.jpg/.png/.bmp为主压缩包大小约9.42MB文件划分清晰便于按工程结构查找和复用。作者在描述中特别记录了Release版编译不通过的典型问题Unicode环境下LPCTSTR与const char*不兼容而将项目字符集改为“使用多字节字符集”即可顺利通过这一排错过程对初学者解决MFC字符串转换问题很有参考价值。整体已获得1112人学习下载适合用来练习MFC界面结合OpenCV完成目标计数和特征测量也可作为相关课程设计的起点方案。1. 一个 MFC 对话框搞定米粒质检不用深度学习也能数清颗粒一台工控机上跑着一个看起来有点老的 MFC 对话框程序界面上只有三个按钮和三行结果米粒个数、最大面积、最大周长。这就是 VS2008 OpenCV MFC 版的“计算米粒的个数及最大米粒的面积及周长”项目一个非常典型的颗粒计数型图像处理 Demo。它不依赖深度学习也没有复杂的前后端核心链路就是读图、转灰度、阈值二值化、找轮廓、按面积过滤。跑完一张图不到一秒结果直接显示在对话框里老代码的调性一目了然。适合两类人一是做机器视觉课程设计或毕业设计的学生需要一套能交差、能讲清楚原理的完整工程二是还在维护老式质检设备的开发者想在不上新框架的前提下把散料计数需求落地。这篇文章我按“结构、代码、踩坑、跑通、改造”的顺序把它拆给你看。2. 项目拆解VS2008 OpenCV 2.x MFC 的最小图像处理框架2.1 技术选型为什么是 VS2008 OpenCV 2.x先说为什么这么老的组合还有人用。我拆到的这套工程目标运行环境是车间里一台 XP 或 Windows 7 的工控机上面已经装了 VC9 运行库和一堆老驱动升级到新版本 VS 意味着整个系统要跟着动风险完全不可控。这类项目通常不是“选型选出来的”而是“环境逼出来的”。在 VS2008 时代OpenCV 的稳定版本是 2.x 系列2.3.1 算是这个系列后期比较完整的版本同时保留了两套接口一套是 OpenCV 1.x 时代的 C 接口IplImage*、cvThreshold、cvFindContours这一批函数名直白和 MFC 的老式CStatic显示控件配合非常顺手另一套是 C 接口cv::Mat、cv::findContours类型安全更好但把结果显示到 MFC 控件时还是得绕回HBITMAP多一层转换。这套工程的主体用的是 C 接口只有少量地方混用了 C 的std::vector存轮廓索引。我个人的判断是在 VS2008 MFC 这个场景下用IplImage加cv*函数反而是最省事的路径因为cvLoadImage出来就是IplImage*cvShowImage直接对应老式图像内存布局调试时也能一眼看出内存结构。如果你拿到手的是 OpenCV 2.3 以上的版本C 接口头文件依然放在build/include/opencv目录下引用方式不变。依赖库也很简单核心就三个opencv_core基础数据结构、opencv_imgproc图像处理算法、opencv_highgui图像读写和摄像头采集。MFC 部分只负责界面事件和结果显示不参与算法这一点是这套工程模块划分做得比较干净的地方。2.2 工程目录结构与模块划分整个工程的目录结构不强求花哨功能边界却很清楚。我按拆解后看到的样子重新整理一份RiceCounter/ |-- RiceCounter.sln |-- RiceCounter/ | |-- RiceCounter.h / RiceCounter.cpp // 应用入口与主对话框的启动 | |-- RiceCounterDlg.h / RiceCounterDlg.cpp // 主对话框按钮事件与结果显示 | |-- ImageProcess.h / ImageProcess.cpp // 图像处理核心与 UI 完全解耦 | |-- StdAfx.h // 预编译头统一 include opencv 头文件 | |-- resource.h // MFC 资源定义 | -- res/ | -- rice_sample.jpg // 默认测试样张米粒平铺拍摄三个模块的职责分得很明确主对话框模块RiceCounterDlg负责三件事响应“打开图片”按钮、调用图像处理模块、把结果显示在 Edit 控件里。图像处理模块ImageProcess是核心暴露了三个函数给 UI 层调用PreprocessImage预处理、GetMaxRiceInfo计数和计算最大米粒信息、ShowIplImage在 Picture 控件上显示结果图。StdAfx.h里统一写入了 OpenCV 的头文件引用和必要的#pragma comment(lib)这样每个 cpp 文件不用重复配置链接库。这种分法值得借鉴算法函数全部接收IplImage*返回计算结果不依赖任何 MFC 类型所以你想把它搬到一个控制台工程或者换成 Qt 界面ImageProcess.cpp一行都不用改。2.3 MFC 界面与 OpenCV 图像的显示管道MFC 的CStatic控件本身不认识IplImage中间必须经过CImage转成HBITMAP再交给SetBitmap。显示管道的完整链路是IplImage - CImage逐行拷贝像素 - Detach 得到 HBITMAP - CStatic::SetBitmap这里最容易踩的坑是内存对齐。IplImage的每一行字节数widthStep不一定等于width * nChannels因为 OpenCV 默认按 4 字节对齐而CImage::Create分配的内存虽然也做了对齐但两者的行宽计算方式不同。最稳妥的做法是逐行memcpy不要图省事一次性拷贝整块内存。下面是核心转换代码// RiceCounterDlg.cpp - 显示一张 IplImage 到 IDC_STATIC_RICE 控件 BOOL CRiceCounterDlg::ShowIplImage(IplImage* img) { if (NULL img) return FALSE; CImage cImage; int nBpp img-nChannels * 8; // 灰度图 8 位彩色图 24 位 cImage.Create(img-width, img-height, nBpp); // 逐行拷贝IplImage 的 widthStep 带字节对齐不能整块 memcpy for (int y 0; y img-height; y) { memcpy(cImage.GetPixelAddress(0, y), img-imageData y * img-widthStep, img-width * img-nChannels); } HBITMAP hBmp cImage.Detach(); // 从 CImage 分离出 HBITMAP 交给控件 m_pictureRice.SetBitmap(hBmp); // 控件接管位图内部会刷新显示 return TRUE; }参数说明img-nChannels是通道数灰度图为 1三通道 BGR 图为 3cImage.Create的第三个参数传 8 或 24 表示每像素位数和IplImage的深度相对应。注意Detach之后HBITMAP的所有权转移给了CStatic函数返回后不要手动DeleteObject否则显示会花屏。还有一点需要在DoDataExchange里把 Picture 控件绑定成成员变量void CRiceCounterDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_STATIC_RICE, m_pictureRice); // 图片显示区 DDX_Control(pDX, IDC_EDIT_RESULT, m_editResult); // 结果文本区 }这段代码把资源 ID 和成员变量绑定在一起之后m_pictureRice.SetBitmap()、m_editResult.SetWindowText()才能直接操作控件。这一步漏掉编译时不会报错运行时就崩在空指针上属于老 MFC 程序员都遇到过的低级翻车。3. 核心模块拆解从灰度图到轮廓统计的完整数据链路图像处理模块是整套工程的心脏它按一条流水线工作灰度化、二值化、形态学去噪、轮廓提取、轮廓筛选、面积周长统计。下面按函数逐个拆开讲。3.1 图像预处理灰度化、二值化与形态学去噪输入的原图是车间里用普通 USB 摄像头拍的背景可能是深色托盘光照不均匀是常态。预处理的目标是把米粒从背景里干净地分离出来变成一张前景是白色、背景是黑色的二值图方便后续找轮廓。预处理函数如下// ImageProcess.cpp - 预处理灰度化 Otsu 二值化 开运算 IplImage* PreprocessImage(IplImage* src) { IplImage* gray cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1); IplImage* binary cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1); // 单通道直接复制三通道先转灰度 if (src-nChannels 3) cvCvtColor(src, gray, CV_BGR2GRAY); else cvCopy(src, gray); // Otsu 自动阈值阈值由算法根据灰度直方图算出来 cvThreshold(gray, binary, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU); // 开运算先腐蚀后膨胀去掉背景上的孤立噪点 cvMorphologyEx(binary, binary, NULL, NULL, CV_MOP_OPEN, 2); cvReleaseImage(gray); return binary; }逻辑说明cvCvtColor只负责通道转换不做任何增强cvThreshold的第二个参数传 0 表示“阈值由 Otsu 自动计算”此时传入的 0 会被忽略算法会遍历灰度直方图找一个让类间方差最大的分割点CV_THRESH_BINARY | CV_THRESH_OTSU这个组合的意思是“用 Otsu 算出的阈值做正向二值化”——灰度大于阈值的像素置为 255小于阈值的置为 0。cvMorphologyEx里的CV_MOP_OPEN是开运算结构元用默认的 3x3迭代 2 次能把背景上几个像素大小的亮斑点滤掉同时基本不影响米粒的轮廓形状。这里的参数需要根据实际拍摄环境微调如果拍出来的米粒发暗、背景发亮把最后一个标志换成CV_THRESH_BINARY_INV | CV_THRESH_OTSU就能完成反转如果背景纹理比较重开运算迭代次数可以从 2 加到 4但代价是米粒边缘会轻微收缩。工程里这两个参数都是写死的二次开发时建议把它们提成界面上的可调项。3.2 米粒计数连通域分析与轮廓筛选二值图做好之后米粒就变成了白色连通域。数米粒本质上是数连通域OpenCV 里最直接的工具是cvFindContours它会从二值图上提取所有轮廓并把它们组织成链表。计数代码如下// ImageProcess.cpp - 统计米粒个数按最小面积过滤噪声 int CountRice(IplImage* binary) { CvMemStorage* storage cvCreateMemStorage(0); CvSeq* contours NULL; // 只取外轮廓避免米粒表面的纹理产生内层轮廓干扰计数 int nContours cvFindContours(binary, storage, contours, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cvPoint(0, 0)); int nRice 0; for (CvSeq* c contours; c ! NULL; c c-h_next) { double area fabs(cvContourArea(c)); if (area 50.0) // 面积小于 50 像素的碎片不算米粒 nRice; } cvReleaseMemStorage(storage); return nRice; }几个参数要说明一下CV_RETR_EXTERNAL表示只提取最外层轮廓米粒表面如果有裂缝或纹理内部会形成子轮廓这个标志直接把它们忽略掉了CV_CHAIN_APPROX_SIMPLE是轮廓压缩模式只保留拐点不保留线段中间的点内存占用更小对面积计算也没有影响。cvContourArea返回的是带符号面积轮廓方向不同符号也不同fabs取绝对值是必备操作。面积阈值 50 不是随便定的——米粒在二值图上的面积一般在几千像素而灰尘和反光噪点只有几十像素50 这个值能有效过滤掉后者。有一个隐性坑必须提醒cvFindContours会直接修改传入的binary图像把找到的轮廓画上去。如果后续还想基于原二值图做别的分析必须在调用前先cvCloneImage一份副本不然二值图被改得面目全非后面所有步骤都会跟着出错。3.3 最大米粒的面积与周长计算计数和最大米粒统计在工程里被合并成了一个函数遍历一遍轮廓链表同时完成三件事计数、找最大面积轮廓、取它的周长。这样只做一次轮廓提取不重复扫描图像效率更高。// ImageProcess.cpp - 统计米粒个数并计算最大米粒面积、周长 int GetMaxRiceInfo(IplImage* binary, double* maxArea, double* maxPerimeter) { CvMemStorage* storage cvCreateMemStorage(0); CvSeq* contours NULL; cvFindContours(binary, storage, contours, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, cvPoint(0, 0)); int nRice 0; double dMaxArea 0.0; CvSeq* maxContour NULL; // 记住面积最大的那条轮廓 for (CvSeq* c contours; c ! NULL; c c-h_next) { double area fabs(cvContourArea(c)); if (area 50.0) continue; // 和 CountRice 保持同样的过滤口径 nRice; if (area dMaxArea) { dMaxArea area; maxContour c; } } if (maxContour ! NULL) { *maxArea dMaxArea; *maxPerimeter cvArcLength(maxContour, CV_WHOLE_SEQ, 0); } cvReleaseMemStorage(storage); return nRice; }逻辑说明maxContour是一个指针指向当前遍历过的所有轮廓中面积最大的那条。这个临时指针的生命周期由storage管理在cvReleaseMemStorage之前都用得着所以计算周长必须在释放内存之前完成。cvArcLength的第二个参数传CV_WHOLE_SEQ表示计算整条轮廓的长度第三个参数传 0 表示把轮廓当作闭合曲线处理返回值的单位是像素和面积dMaxArea的像素平方单位一致两者可以一起作为特征值输出。为了保证结果可人工核验工程里还加了一个可视化辅助函数把最大米粒的轮廓用红线画回原图处理完直接显示在界面上。这样工人一眼就能确认“程序选出的最大米粒”和“肉眼看到的最大米粒”是不是同一颗。// 把面积最大的轮廓叠加显示到原图上便于人工核验结果 void DrawMaxContour(IplImage* src, CvSeq* maxContour) { CvScalar red CV_RGB(255, 0, 0); cvDrawContours(src, maxContour, red, red, 0, 2, CV_AA, cvPoint(0, 0)); }cvDrawContours的第一个red是外轮廓颜色第二个red是内轮廓颜色CV_RETR_EXTERNAL模式下内轮廓本就不存在填同一个颜色即可2是线宽CV_AA是抗锯齿标志。轮廓画在原图上不影响数值结果画完再做显示。4. 避坑指南环境配置与图像算法两类常见问题排查4.1 编译与运行环境的三类翻车现场现象一编译时报几十条 C4996 警告说什么sprintf、strcpy是 deprecated 的。原因VS2008 对 C 运行时库里的不安全函数做了废弃标记而 OpenCV 的 C 接口内部大量用了这类函数。解决在StdAfx.h顶部加上#define _CRT_SECURE_NO_DEPRECATE或者在项目属性页的“预处理器定义”里统一加_CRT_SECURE_NO_WARNINGS。这个操作纯属消音不影响任何逻辑。现象二Debug 版程序一跑就崩Release 版却正常。原因链接了 OpenCV 的 Release 版 lib如opencv_imgproc231.lib但运行时去加载了同名 Debug 版 DLL或者反过来。OpenCV 2.x 的 Debug 库文件名会带一个d后缀如opencv_imgproc231d.lib它们依赖的运行时库也不同Debug 工程默认用/MDdRelease 工程用/MD。解决先确认“工程配置里 lib 文件名”和“OpenCV bin 目录下实际拷入的 DLL 文件名”属于同一套再确认“项目属性 - C/C - 代码生成 - 运行库”与 lib 版本匹配Debug 版就全用带d的库Release 版就全用不带d的库。现象三cvLoadImage返回的路径明明正确但程序在AfxMessageBox或SetWindowText时提示无法从const char*转换为CString。原因工程默认使用 Unicode 字符集而 OpenCV 2.x 的 C 接口函数参数都是char*两者在代码里直接混用就会编译不过。解决在项目属性里把“字符集”改为“使用多字节字符集”。这是 VS2008 跑 OpenCV 最典型的配置坑比前面的内存问题出现频率高得多。4.2 图像算法效果的三类玄学问题现象四二值图显示在 Picture 控件上出现斜纹或者整幅画面向右下角错位。原因前文提过的widthStep对齐问题——IplImage每行字节数按 4 字节对齐直接用memcpy(img-imageData, dst, size)整块拷贝时错位会逐行累积。解决用 2.3 节ShowIplImage里的逐行拷贝方式不要迷信整块拷贝。现象五同一批图片上午跑计数正常下午换了个光照角度就数错米粒和背景大面积连成一片。原因cvThreshold的固定阈值只适用于光照稳定的环境车间自然光变化会让灰度直方图整体偏移。解决使用CV_THRESH_OTSU标志让阈值自动适应如果光照还是不均建议先在cvCvtColor之后加一步cvSmooth(gray, gray, CV_GAUSSIAN, 3, 3)把高光噪点压下去再二值化。现象六第二次调用同一个函数时二值图变了轮廓数量和第一次不一样。原因cvFindContours会在输入图上填充轮廓信息把原图当作输出空间的一部分使用调用一次之后原图结构已经被破坏。解决每个函数入口先cvCloneImage(binary)对副本做cvFindContours原图保持干净。这个坑几乎是每个用 OpenCV C 接口的人都踩过的属于典型的“没有后悔药”只能靠克隆副本这个习惯救场。5. 运行与验证从编译配置到样张跑通的全流程5.1 环境配置与编译步骤第一步是装好 VS2008 和 OpenCV 2.x。OpenCV 2.3.1 的安装包解压后会得到以下目录结构注意 VC9 就是 VS2008 对应的编译器版本号选择vc9目录而不是vc10、vc11D:\OpenCV2.3\opencv\ |-- build\ | |-- include\ | | |-- opencv\ // cv.h, cvaux.h 等 C 接口头文件 | | -- opencv2\ // 新版 C 接口头文件 | -- x86\ | -- vc9\ | |-- lib\ // 静态链接库 | -- bin\ // 运行所需 DLL在 VS2008 里新建一个 MFC 对话框工程工程名随意然后按下表逐项配置。这张表是整个环境配置里最值得抄的部分配置项设置值说明项目属性 - C/C - 常规 - 附加包含目录D:\OpenCV2.3\opencv\build\include;D:\OpenCV2.3\opencv\build\include\opencv两个头文件目录都要加项目属性 - 链接器 - 常规 - 附加库目录D:\OpenCV2.3\opencv\build\x86\vc9\liblib 搜索路径项目属性 - 链接器 - 输入 - 附加依赖项opencv_core231.lib;opencv_imgproc231.lib;opencv_highgui231.lib三个核心库版本号以实际安装为准项目属性 - 链接器 - 输入 - 忽略特定库留空不需要额外忽略库项目属性 - 常规 - 字符集使用多字节字符集不改成多字节会有一堆类型转换错误项目属性 - C/C - 代码生成 - 运行库Debug 用/MDdRelease 用/MD与 OpenCV DLL 版本严格对应配置完还要把bin目录下的 DLL 拷贝到工程的Debug或Release输出目录或者把D:\OpenCV2.3\opencv\build\x86\vc9\bin加进系统PATH。我建议直接拷贝 DLL 到 exe 同目录省得换机器部署时漏配环境变量。编译顺序上先编一个空对话框工程确认 MFC 环境没问题再往StdAfx.h里加 OpenCV 的头文件包含分两步走把“MFC 问题”和“OpenCV 问题”隔离开来。都编译通过后在主对话框里放上“打开图片”“开始处理”“退出”三个按钮再加一个 Picture 控件和一个 Edit 控件资源绑定按 2.3 节来做。5.2 跑通样张与人工核验方法工程自带的res/rice_sample.jpg是一张米粒平铺在深色托盘上的样张先不接摄像头直接拿这张图验证程序行为和预期一致。核心调用代码集中在按钮事件里// RiceCounterDlg.cpp - “开始处理”按钮的完整处理链 void CRiceCounterDlg::OnBnClickedBtnProcess() { IplImage* src cvLoadImage(res\\rice_sample.jpg, CV_LOAD_IMAGE_UNCHANGED); if (NULL src) { AfxMessageBox(_T(加载图片失败请检查文件路径)); return; } IplImage* binary PreprocessImage(src); // 3.1 节预处理得到二值图 double maxArea 0.0, maxPerimeter 0.0; int nRice GetMaxRiceInfo(binary, maxArea, maxPerimeter); CString strInfo; strInfo.Format(_T(米粒个数%d\r\n最大米粒面积%.1f 像素²\r\n最大米粒周长%.1f 像素), nRice, maxArea, maxPerimeter); m_editResult.SetWindowText(strInfo); // 结果输出到编辑框 ShowIplImage(binary); // 显示二值图便于对照检查 cvReleaseImage(binary); cvReleaseImage(src); }这段代码把“读图、预处理、统计、显示”四个环节串成了完整的调用链。CV_LOAD_IMAGE_UNCHANGED表示保留原图的通道数和深度如果你确定输入是灰度图改成CV_LOAD_IMAGE_GRAYSCALE会让后续预处理少一步转换。跑通之后的验证口径我建议按下面几步走第一把程序输出的米粒个数和人工数出来的结果对比如果样张里大约有几十粒米误差在 1 粒以内才算合格第二看界面上的二值图确认每颗米都是独立的白色连通域米粒之间没有粘连、米粒内部没有黑色空洞第三用 3.3 节的DrawMaxContour把最大米粒轮廓画在原图上肉眼判断画出的红线是否贴合最大米粒的边缘。这三步都过了算法链路才算真正跑通而不是“程序不崩就完事”。5.3 从本地图片扩展到实时摄像头采集样张验证没问题之后可以把输入源从本地文件换成 USB 摄像头。工程里用的是cvCaptureFromCAM加定时器轮询的方式不用额外开线程省掉一整套线程同步逻辑// RiceCounterDlg.cpp - 定时器采集摄像头帧并缓存 void CRiceCounterDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent ! 1 || m_capture NULL) { CDialogEx::OnTimer(nIDEvent); return; } IplImage* frame cvQueryFrame(m_capture); // 返回内部缓冲指针 if (frame) { cvCopy(frame, m_frameCache); // 深拷贝到自己的缓存防覆盖 ShowIplImage(frame); // 实时预览画面 } CDialogEx::OnTimer(nIDEvent); }有一个关键点cvQueryFrame返回的指针指向CvCapture内部缓冲区下一次再调用这个函数时缓冲区内容会被新帧覆盖。所以必须用cvCopy把它拷贝到自己的m_frameCache里否则处理线程还没开始图像数据已经被下一帧冲掉了。定时器间隔一般设 30 到 50 毫秒就能获得比较流畅的预览效果如果你要把“统计米粒”也放进定时器里注意处理逻辑不要和预览抢 CPU否则界面会卡顿。6. 二次开发把单张计数改成批量质检与粘连分割6.1 批量目录处理与结果导出单张图片验证完成后最常见的改造需求是把“一张一张点按钮”变成“对一个文件夹里的所有图片自动统计”做完再把结果写进 CSV 文件。MFC 里用CFileFind遍历目录是现成方案处理逻辑写在循环里// BatchProcess.cpp - 遍历指定目录下的所有 jpg 并汇总结果到 CSV void BatchProcess(LPCTSTR dir, const CString outFile) { CFileFind finder; CString pattern CString(dir) _T(\\*.jpg); BOOL bMore finder.FindFile(pattern); CStdioFile file; file.Open(outFile, CFile::modeCreate | CFile::modeWrite); while (bMore) { bMore finder.FindNextFile(); if (finder.IsDots() || finder.IsDirectory()) continue; IplImage* src cvLoadImage(finder.GetFilePath(), CV_LOAD_IMAGE_UNCHANGED); IplImage* binary PreprocessImage(src); double maxArea 0.0, maxPerimeter 0.0; int nRice GetMaxRiceInfo(binary, maxArea, maxPerimeter); CString line; line.Format(_T(%s,%d,%.2f,%.2f\r\n), finder.GetFileName(), nRice, maxArea, maxPerimeter); file.WriteString(line); // 每处理完一张立即写一行 cvReleaseImage(binary); cvReleaseImage(src); } file.Close(); }CFileFind::FindFile配合FindNextFile是 MFC 的标准遍历姿势IsDots用来跳过.和..两个特殊目录项。这里每处理完一张图就立刻写一行 CSV而不是全部处理完再统一写入——这样程序跑到一半崩了已处理图片的结果也不会丢批量跑几百张图时这个习惯能省很多重跑的时间。6.2 深度改进粘连米粒的分割方向如果拍摄时米粒铺得不够散或者米粒湿度偏大导致互相粘在一起二值图上的多颗米会连成一个连通域计数结果会明显偏少。这时候在当前代码基础上直接调阈值没多大用问题出在分割策略上。两个可行的改进方向第一个是预处理阶段加腐蚀操作把两颗米之间很窄的连接处腐蚀断然后再膨胀回去恢复原尺寸。实现上就是在cvMorphologyEx之后再加一次cvErode(binary, binary, NULL, 1)。这个方案改动最小十行以内就能验证效果缺点是把较大颗粒的米粒边缘也磨掉一部分面积统计会偏小。第二个是上距离变换加分水岭算法先对二值图做cvDistTransform得到每个前景像素到背景的距离距离图的局部极大值点就是每颗米的“种子”再用水岭分割把粘连区域沿着米粒之间的谷线切开。这个方案对粘连严重的图像准确率更高但计算明显慢不建议在 UI 线程里跑最好丢到后台工作线程算完再通过PostMessage把结果传回主界面。每个颗粒计数的项目最终都会卡在同一堵墙前面图像质量决定算法上限。分得越开、光照越均匀现在的代码就越准。从那以后我每次拿到类似的颗粒计数需求都会先去确认拍摄时的光源稳定性和背景颜色再谈算法参数怎么调——这两个条件不满足任何分割算法都是白搭。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?