OpenCV__006_识别硬币数量简易版
运行环境:
vscode 1.112.0
编译环境:
Visual Studio Community 2026 Release - amd64_x86
思路:
1. 图像加载与预处理
- 加载图片
- 函数:
cv::imread()
- 函数:
- 克隆图片
- 目的:避免源图像受到后续操作的污染
- 函数:
cv::image.clone()(image为存储图像的变量)
- 转灰度
- 目的:降低颜色对后续处理和操作的影响
- 函数:
cv::cvtColor()
2. 二值化与降噪
- 二值化
- 说明:将图像转为像素值只有 0 或 255 的两种值的图像,这是形态学降噪前的操作。
- 降噪处理
- 目前学到的三种方式:
- 高斯降噪
- 函数:
cv::GaussianBlur() - 原理:需要定义一个核(也称元素结构),核的作用是计算周围(具体看核大小)像素值的加权平均。
- 函数:
- 椒盐降噪(中值滤波)
- 函数:
cv::medianBlur() - 原理:核范围内将像素值进行排序,取中位数。
- 函数:
- 形态学降噪(本程序使用)
- 函数:
cv::morphologyEx() - 核定义:
cv::Mat kernel = cv::getStructuringElement()(核的类型是Mat,即矩阵) - 两种方式:
- 开运算:参数为
cv::MORPH_OPEN,用于去除白噪点。 - 闭运算:参数为
cv::MORPH_CLOSE,用于填黑孔洞。
- 开运算:参数为
- 函数:
- 高斯降噪
- 目前学到的三种方式:
3. 距离变换与归一化
- 距离变换
- 目的:分离粘连/重叠的硬币
- 函数:
cv::distanceTransform() - 原理:计算每个前景像素(非0像素)到最近的背景像素(0像素)的最短距离(欧氏距离)。这里需要用到“掩膜”,作用是在图像中通过扩大像素计算范围,提高欧式距离的准确度。
- 归一化
- 目的:将距离变换结果归一化到 [0, 1] 区间,消除图像尺寸/分辨率的影响。0 是最短距离变换结果,1 是最大距离变换结果。
- 函数:
cv::normalize()
4. 获取前景与标记
- 获取前景(硬币)
- 函数:
cv::threshold() - 说明:传入存放距离变换结果的变量。值越接近 1.0 越靠近物体中心,值越接近 0.0 越靠近物体边缘。函数中的阈值参数(假设为 0.1)会保留所有距离值 > 0.1 的像素,其余设为 0。
- 函数:
- 标记前景
- 说明:获取前景后,重新创建一个跟源图像一样大小的图,并对硬币的位置进行标记。
- 函数:
cv::drawContours()
- 标记背景
- 说明:进行分水岭算法(区分重叠硬币)的重要步骤。
- 函数:
cv::circle()(告诉算法,跟一块一样的区域是背景)
5. 分水岭算法与边缘处理
- 分水岭算法
- 目的:将重叠硬币分开计算,同时保留前景和背景的区分。背景和前景之间的边界会被设为 -1。
- 函数:
cv::watershed()
- 边缘设置
- 操作:将边缘设置为绿色。
6. 轮廓过滤与面积标记
- 过滤小轮廓
- 函数:
cv::contourArea()(计算轮廓面积)
- 函数:
- 在轮廓质心位置标记面积
- 获取轮廓的矩
- 函数:
cv::Moments mu = cv::moments() - 说明:矩(Moments)是一组描述图像区域形状、位置、方向等特性的统计量。
- 函数:
- 计算质心
- 公式:
mu.m00:0阶矩,表示面积mu.m10:x方向一阶矩mu.m01:y方向的一阶矩int centerX = static_cast<int>(mu.m10 / mu.m00);int centerY = static_cast<int>(mu.m01 / mu.m00);
- 公式:
- 面积转字符串
- 函数:
std::to_string()
- 函数:
- 放置面积文本
- 函数:
cv::putText()
- 函数:
- 获取轮廓的矩
CMakelists.txt
cmake_minimum_required(VERSION 3.10.0) project(opencvtest VERSION 0.1.0 LANGUAGES CXX) include(CTest) enable_testing() find_package(OpenCV REQUIRED) add_executable(opencvtest test05.cpp) target_link_libraries(opencvtest ${OpenCV_LIBS}) set(CPACK_PROJECT_NAME ${PROJECT_NAME})
原图:

效果图:

1 #include <opencv2/opencv.hpp> 2 #include <iostream> 3 // 创建一个函数,方便控制输出框的大小 4 void my_imshow(const std::string& name, cv::Mat& photo) { 5 // WINDOW_NORMAL 允许用户手动调节窗口大小 6 cv::namedWindow(name, cv::WINDOW_NORMAL); 7 // 设置初始窗口大小 8 cv::resizeWindow(name, 600, 400); 9 cv::imshow(name, photo); 10 } 11 int main() { 12 // ================== 1. 读取图像 ================== 13 cv::Mat image = cv::imread("./photo/coins.jpg"); 14 if (image.empty()) { 15 std::cout << "错误:无法加载图像!" << std::endl; 16 return -1; 17 } 18 my_imshow("image", image); 19 cv::Mat src = image.clone(); 20 21 // ================== 2. 灰度 + 二值化 ================== 22 cv::Mat gray; 23 cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); 24 25 cv::Mat binary; 26 cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU); 27 my_imshow("Binary", binary); 28 29 // ================== 3. 形态学去噪(闭运算) ================== 30 //创建核 31 cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(1, 1)); 32 33 cv::Mat opening; 34 35 // 锚点位置:(-1,-1) 表示使用结构元素的中心作为锚点(默认行为) 36 //2表示去噪两次 37 cv::morphologyEx(binary, opening, cv::MORPH_OPEN, kernel, cv::Point(-1, -1), 2); 38 my_imshow("opening",opening); 39 40 41 // ================== 4. 距离变换 ================== 42 cv::Mat dist; 43 //计算每个前景像素(非0像素)到最近的背景像素(0像素)的最短距离,也叫欧氏距离 44 //第三个参数:cv::DIST_L2,表示距离的类型,这里是欧式距离 45 //第四个参数:5,表示定义一个5*5的掩膜 46 //掩膜的作用是在图像中,通过扩大像素计算范围,提高欧式距离的准确度 47 cv::distanceTransform(opening, dist, cv::DIST_L2, 5); 48 49 my_imshow("01.dist",dist); 50 //将距离变换结果归一化到 [0, 1] 区间,归一化是为了消除图像尺寸/分辨率的影响, 51 // 使后续阈值(如 dist > 0.5)具有通用性 52 cv::normalize(dist, dist, 0, 1.0, cv::NORM_MINMAX); 53 my_imshow("02.dist",dist); 54 55 double minVal, maxVal; 56 cv::minMaxLoc(dist, &minVal, &maxVal); 57 std::cout << "Max distance: " << maxVal << std::endl; 58 // ================== 5. 获取前景 ================== 59 cv::Mat dist_binary; 60 cv::threshold(dist, dist_binary, 0.1, 1.0, cv::THRESH_BINARY); 61 62 // 转换为8位图 63 cv::Mat dist_8u; 64 //将每个像素从 float 转为 uchar,OpenCV 会自动将 [0.0, 1.0] 映射到 [0, 255] 65 dist_binary.convertTo(dist_8u, CV_8U); 66 my_imshow("dist_binary",dist_binary); 67 // ================== 6. 查找轮廓(前景) ================== 68 std::vector<std::vector<cv::Point>> contours; 69 //①这里输入图像必须是单通道8位二值图 70 //③cv::RETR_EXTERNAL 表示 只检测最外层轮廓(忽略内部孔洞) 71 //④轮廓近似方法:cv::CHAIN_APPROX_SIMPLE 表示 压缩水平、垂直和对角线段,只保留端点 72 cv::findContours(dist_8u, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); 73 74 // ================== 7. 创建 markers ================== 75 //创建一个dist相同大小的矩阵,像素值位0,类型为浮点型 76 cv::Mat markers = cv::Mat::zeros(dist.size(), CV_32S); 77 78 for (size_t i = 0; i < contours.size(); i++) { 79 //cv::Scalar(static_cast<int>(i) + 1),填充颜色 80 //-1,负值表示填充轮廓内部(实心),而非只画边界 81 cv::drawContours(markers, contours, static_cast<int>(i), cv::Scalar(static_cast<int>(i) + 1), -1); 82 } 83 //my_imshow("markers",markers); 84 // 标记背景 85 cv::circle(markers, cv::Point(5, 5), 3, cv::Scalar(255), -1); 86 87 // ================== 8. 分水岭 ================== 88 //分割结果中属于区域之间边界的像素被设为 -1,边界在后续会被涂成绿色 89 cv::watershed(src, markers); 90 91 // ================== 9. 可视化结果 ================== 92 cv::Mat result = src.clone(); 93 94 for (int i = 0; i < markers.rows; i++) { 95 for (int j = 0; j < markers.cols; j++) { 96 97 //如果是边界,就涂成绿色 98 if (markers.at<int>(i, j) == -1) { 99 result.at<cv::Vec3b>(i, j) = cv::Vec3b(0, 255, 0); // 边界:绿色 100 } 101 } 102 } 103 my_imshow("1.result",result); 104 105 // 过滤小轮廓:只保留面积大于 300 的 106 std::vector<std::vector<cv::Point>> validContours; 107 std::vector<float>Area; 108 for (const auto& cnt : contours) { 109 double area = cv::contourArea(cnt); 110 if (area > 300) { // 根据图像调整阈值 111 Area.push_back(area); 112 validContours.push_back(cnt); 113 } 114 } 115 std::sort(Area.begin(),Area.end()); 116 for(auto area:Area) 117 std::cout << "area:" <<area << std::endl; 118 contours = validContours; 119 int coinCount = static_cast<int>(contours.size()); 120 std::cout << "有效硬币数量: " << coinCount << std::endl; 121 122 //在轮廓中心标记面积 123 for (size_t i = 0; i < validContours.size(); i++) { 124 // 计算轮廓的矩,矩(Moments) 是一组描述图像区域形状、位置、方向等特性的统计量 125 cv::Moments mu = cv::moments(validContours[i]); 126 127 // 获取轮廓的质心(中心点),质心≠外接矩形的中心 128 //mu.m00,0阶矩,表示面积 129 //mu.m10, 表示x方向一阶矩 130 //mu.m01,表示y方向的一阶矩 131 int centerX = static_cast<int>(mu.m10 / mu.m00); 132 int centerY = static_cast<int>(mu.m01 / mu.m00); 133 134 // 将面积转换为字符串 135 std::string areaStr = std::to_string(static_cast<int>(Area[i])); 136 137 // 在图像上放置面积文本 138 //其中:cv::FONT_HERSHEY_SIMPLEX, 表示字体类型 139 //0.5,表示缩放比例 140 cv::putText(result, areaStr, cv::Point(centerX, centerY), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255), 1); 141 } 142 143 // 展示结果图像 144 my_imshow("Result with Area Labels", result); 145 146 cv::waitKey(0); 147 return 0; 148 }

浙公网安备 33010602011771号