前言
在自动计算图像中有几枚硬币的任务中,分离出前景和背景后是否就可以马上实现自动计件,如果可以,如何实现?如果不可以,为什么?
 答案是否定的。二值化之后我们的得到的只是前景总像素的多少,并不知道哪些像素属于同一枚硬币。想要实现自动计件功能还需要用到连通域标记的知识。
 连通域标记的方法这里我们使用种子填充法:
算法步骤:
1、遍历一幅图像。
 2、如果遇到前景且该点未被标记,说明在该点附近可能存在与该点相连通的像素点,即可能存在连通域,停止遍历。否则继续遍历。
 3、以该点为seed点,遍历seed点4邻域或者8邻域。如果同为前景,将坐标存到一个栈中,并将这点贴上label,表示已经访问过该像素,避免重复访问。
 4、将栈中的坐标取出,以该点为seed点,重复2操作。
 5、直到栈中的所有元素都取出,说明已经遍历完了该label的所有元素。
 6、label++;从一开始停止遍历的点继续遍历。
 7、重复2-6直到遍历到最后一个像素
代码实现:
*--------------------------【练习】连通域标记-------------------------------------*/
/*参数说明:
src_img:输入图像 
flag_img:作为标记的空间(在函数内部设置为单通道)
draw_img:作为输出的图像,不同的连通域的颜色不同
iFlag:作为判断属于连通域的像素目标值,一般来说我们是对二值图进行连通域分析,所以这个值为0或者255,物体是0/1,则iFlag是0/1
type:   type==4 :用4邻域     type==8 :用8邻域
nums: 设定的label像素个数截断值,被标记的连通域像素个数必须大于nums才算是正确的连通域。用来防止二值化后的效果并不好的情况。
*/
void seed_Connected_Component_labeling(Mat& src_img,Mat& flag_img,Mat& draw_img, int iFlag,int type, int nums)
{
  int img_row = src_img.rows;
  int img_col = src_img.cols;
  flag_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC1);//标志矩阵,为0则当前像素点未访问过
  draw_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC3);//绘图矩阵
  Point cdd[111000];                  //栈的大小可根据实际图像大小来设置
  long int cddi = 0;
  int next_label = 1;    //连通域标签
  int tflag = iFlag;
  long int nums_of_everylabel[100] = { 0 }; //存放每个区域的像素个数
  //Mat(纵坐标,横坐标)
  //Point(横坐标,纵坐标)
  for (int j = 0; j < img_row; j++)     //height
  {
    for (int i = 0; i < img_col; i++)   //width
    {
      //一行一行来
      if ((src_img).at<uchar>(j, i) == tflag && (flag_img).at<uchar>(j, i) == 0)   //满足条件且未被访问过
      {
        //将该像素坐标压入栈中
        cdd[cddi] = Point(i, j);
        cddi++;
        //将该像素标记
        (flag_img).at<uchar>(j, i) = next_label;
        //将栈中元素取出处理
        while (cddi != 0)
        {
          Point tmp = cdd[cddi - 1];
          cddi--;
          //对4邻域进行标记
          if (type == 4)
          {
            Point p[4];//邻域像素点,这里用的四邻域
            p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y);   //左
            p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右
            p[2] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上
            p[3] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下
            //顺时针
            //p[0] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上
            //p[1] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右
            //p[2] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下
            //p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y);   //左
            //逆时针
            //p[3] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上
            //p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右
            //p[1] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下
            //p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y);   //左
            for (int m = 0; m < 4; m++)
            {
              if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //满足条件且未被访问过
              {
                //将该像素坐标压入栈中
                cdd[cddi] = p[m];
                cddi++;
                //将该像素标记
                (flag_img).at<uchar>(p[m].y, p[m].x) = next_label;
              }
            }
          }
          //对8邻域进行标记
          else if (type == 8)
          {
            Point p[8];//邻域像素点,这里用的四邻域
            p[0] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y - 1 > 0 ? tmp.y - 1 : 0);   //左上
            p[1] = Point(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);//上
            p[2] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1,tmp.y - 1 > 0 ? tmp.y - 1 : 0);    //右上
            p[3] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y);   //左
            p[4] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y);//右
            p[5] = Point(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//左下
            p[6] = Point(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//下
            p[7] = Point(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_col - 1, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);//右下
            for (int m = 0; m < 7; m++)
            {
              if ((src_img).at<uchar>(p[m].y, p[m].x) == tflag && (flag_img).at<uchar>(p[m].y, p[m].x) == 0) //满足条件且未被访问过
              {
                //将该像素坐标压入栈中
                cdd[cddi] = p[m];
                cddi++;
                //将该像素标记
                (flag_img).at<uchar>(p[m].y, p[m].x) = next_label;
              }
            }
          }
          
        }
        next_label++;
      }
    }
  }
  next_label = next_label - 1;
  int all_labels = next_label;
  std::cout << "labels : " << next_label <<std::endl;
  //给不同连通域的涂色并且记录下每个连通域的像素个数
  for (int j = 0;j < img_row;j++) //行循环
  {
    for (int i = 0;i < img_col;i++) //列循环
    {
      int now_label = (flag_img).at<uchar>(j, i);   //当前像素的label
      nums_of_everylabel[now_label]++; 
      float scale = now_label * 1.0f / all_labels;
      //-------【开始处理每个像素】---------------
      draw_img.at<Vec3b>(j, i)[0] = 255 - 255 * scale;    //B通道
      draw_img.at<Vec3b>(j, i)[1] = 128 - 128 * scale;    //G通道
      draw_img.at<Vec3b>(j, i)[2] = 255 * scale;    //R通道
      //-------【处理结束】---------------
    }
  }
  std::cout << "初步结论 : " << std::endl;
  for (int i = 1;i <= next_label;i++)
  {
    std::cout << "labels : " << i<<"像素个数   " << nums_of_everylabel[i] <<std::endl;
  }
  std::cout << "最后结论 : " << std::endl;
  std::cout << "截断像素数目 : " << nums << std::endl;
  for (int i = 1;i <= next_label;i++)
  {
    if (nums_of_everylabel[i] <= nums)
    {
      all_labels--;
    }
  }
  std::cout << "labels : " << all_labels << std::endl;
}
int main()
{
  Mat flag_img;
  Mat draw_img;
  Mat srcImage = imread("D:\\opencv_picture_test\\阈值处理\\硬币.png", 0);  //读入的时候转化为灰度图
  //Mat srcImage = imread("D:\\opencv_picture_test\\阈值处理\\黑白.jpg", 0);  //读入的时候转化为灰度图
  namedWindow("原始图", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
  imshow("原始图", srcImage);
  cout << "srcImage.rows : " << srcImage.rows << endl;    //308
  cout << "srcImage.cols : " << srcImage.cols << endl;    //372
  Mat dstImage;
  dstImage.create(srcImage.rows, srcImage.cols, CV_8UC1);
  //阈值处理+二值化
  My_artificial(&srcImage, &dstImage, 84);
  //  flag_img = cv::Mat::zeros(src.size(), src.type());
  //cvtColor(src, src, COLOR_RGB2GRAY);    //这一句很重要,必须保证输入的是单通道的图,否则所读取的数据是错误的
  double time0 = static_cast<double>(getTickCount()); //记录起始时间
  seed_Connected_Component_labeling(dstImage,flag_img,draw_img,255,4,500);    //白色部分被标记
  time0 = ((double)getTickCount() - time0) / getTickFrequency();
  cout << "此方法运行时间为:" << time0 << "秒" << endl;  //输出运行时间
  imshow("dstImage", dstImage);
  namedWindow("draw_img", WINDOW_NORMAL);//WINDOW_NORMAL允许用户自由伸缩窗口
  imshow("draw_img", draw_img);
  waitKey(0);
  return 0;
}实现效果:
原图:
 二值图(可以看到有几个噪点,而且图像的右边和上边是白色的,这是因为原图我是截图的,边界并没有剪裁好,这点在下面的连通域标记会有影响)
 我给属于不同连通域的物体涂上不同的颜色。
 下面是打印出来的信息:初步得到的label是19个,其中label1就是我所说的截图边界问题。其他的几个像素个数小的就是噪点。
 通过设定门限,像素个数小于500的标签物体我们将它视为噪声。最后得到的label数目正好是10,也就是硬币的数目。
发现的问题
连通域标记函数代码部分,可以看到我还尝试了其他两种遍历seed周围元素的方式,分别是顺时针和逆时针。但是运算速度没有第一种快,至于原因我没有深究。希望有心人能给我讲解一波。此外,试了一下8邻域,运算速度也得到了下降。
 这就是我说的剪裁错误,嘿嘿。

此外,二值化的方法我是用的人工调整,原图受到非均匀光线的照射,全局大津阈值得到的效果并不是很好,反而由于直方图双峰性比较明显,迭代法看起来还不错。不过为了连通域标记的时候能够准确一点,我就用滑条调整阈值了。 滑动条调整阈值的代码在这儿: 迭代法、大津的代码在这儿:
3.15更新,加入形态学腐蚀操作
首先回顾之前遇到的问题:受到噪声影响,十个硬币竟然贴了19个labels,尽管利用限制像素个数的方法来限制,但这种方法有许多弊端。
 这几天学习了一些简单的形态学操作,其中腐蚀操作有个作用:去除黏连像素以及噪声。
 这不就正好能解决之前遇到的问题嘛!
 操作也很简单,加上两行代码就行。 、
、
 结果运行如下(把自己简陋的限制像素函数去掉了)
 效果很好啊!
 关于腐蚀的详细讲解请看这边:
                
                










