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 的两种值的图像,这是形态学降噪前的操作。
  • 降噪处理
    • 目前学到的三种方式:
      1. 高斯降噪
        • 函数:cv::GaussianBlur()
        • 原理:需要定义一个核(也称元素结构),核的作用是计算周围(具体看核大小)像素值的加权平均。
      2. 椒盐降噪(中值滤波)
        • 函数:cv::medianBlur()
        • 原理:核范围内将像素值进行排序,取中位数。
      3. 形态学降噪(本程序使用)
        • 函数: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()(计算轮廓面积)
  • 在轮廓质心位置标记面积
    1. 获取轮廓的矩
      • 函数:cv::Moments mu = cv::moments()
      • 说明:矩(Moments)是一组描述图像区域形状、位置、方向等特性的统计量。
    2. 计算质心
      • 公式:
        • 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);
    3. 面积转字符串
      • 函数:std::to_string()
    4. 放置面积文本
      • 函数: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})

 

原图:

image

 效果图:

image

 

  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 }

 

posted @ 2026-03-28 15:08  freeyang8  阅读(8)  评论(0)    收藏  举报