一、缺陷检测

我们知道,opencv是一个非常优秀的计算机视觉图像处理算法库,它给我们封装了好多基本的图像处理算法,免去了让我们重复造轮子的麻烦,今天我就用传统算法,根据实际工程项目,手把手教你做一个最典型的产品缺陷检测项目案例,虽然这个案例与实际生产还存在一定的差距,但是这个检测流程已经很接近实际生产了。

我们先看一下测试结果:

51c视觉~CV~合集1_取值

这个检测的主要需求就是,根据视频流中流水线上的产品,通过每一帧图像,检测出每个产品的缺陷属性,并把有缺陷的产品给标注出来,并注明缺陷类型。

项目开始之前,我先请大家安装一下opencv库

在项目开始之前,我们先思考一下整个检测流程的框架:

1,首先我们要抓取视频流中的每一帧图像

2,在抓取得每一张图像中,首先要定位出这张图片中的每一个产品,找出这件产品的边缘

3,分析所抓取每个产品的属性,分析其缺陷类型的特征,通过算法形式来对缺陷进行归类。

上面就是整个检测的基本流程,其实,在利用传统算法进行检测的时候,基本流程不会差别太大,所用算子也基本大致相同。在讲解这个检测方法之前,我先来讲解几个我们经常用到的算子:

findContours():

这个函数是查找整张图片中所有轮廓的函数,这个函数很重要,在处理图像分割尤其是查找整个图像中边缘分割时候少不了它,所以我重点说明一下,首先它的函数原型是这样的:

// 查找整张图片的所有轮廓
findContours(InputOutputArray image, 
    OutputArrayOfArrays contours,
    OutputArray hierarchy, 
    int mode,
    int method, 
    Point offset=Point());

我先来讲解一下这个函数的主要参数:


1),image代表输入的图像矩阵;

2),第二个参数contours代表输出的整张图片的所有轮廓,由于轮廓坐标是一组二维坐标构成的一组数据集,因此,它的声明是一个嵌套了一层坐标数据集向量的向量:

//参数contours的声明
vector<vector<Point>> contours;

3),第三个参数hierarchy顾名思义,字面的意思是层级关系,它代表各个轮廓的继承关系,也是一个向量,长度和contours相同,每个元素和contours的元素相对应,hierarchy的每一个元素是一个包含四个整形数的向量,也即:

//参数hierarchy的声明
//Vec4i 代表包含四个元素的整形向量
vector<Vec4i> hierarchy;

那么这里的hierarchy究竟代表什么意思呢,其实对于一张图片,findContours()查找出所有的 51c视觉~CV~合集1_取值_02 个轮廓后,每个轮廓都会有自己的索引, 51c视觉~CV~合集1_灰度_03 ,而hierarchy[i]里面的四个元素 51c视觉~CV~合集1_取值_04 分别代表第 51c视觉~CV~合集1_取值_05 个轮廓:后一个轮廓的序号、前一个轮廓的序号、子轮廓的序号、父轮廓的序号,有严格的顺序。

为了更加直观说明,请看下面一张图片:

51c视觉~CV~合集1_二值化_06

上面图片中,是一张图片中的所有轮廓,分别用序号0、1、2、3来表示,其中0有两个子轮廓,分别是1和3,而1则有一个父轮廓也有一个子轮廓,分别是0与2,它们的继承关系基本上就是这样,我们再做进一步的分析:

0号轮廓没有同级轮廓与父轮廓,但由两个子轮廓1与3,根据前面提到的四个元素的顺序分别是:下一轮廓、前一轮廓、子轮廓、父轮廓,所以其轮廓继承关系向量hierarchy为 51c视觉~CV~合集1_二值化_07 ,这里的-1表示无对应关系,而第三个元素1则代表0号轮廓的一个子轮廓为1;

同样,1号轮廓有同级轮廓3(按照索引是后一轮廓,但无前一轮廓),也有子轮廓2,父轮廓0,所以它的继承关系向量hierarchy为 51c视觉~CV~合集1_取值_08 ;

按照同样的方法,我们还可以得到2号轮廓的继承关系向量hierarchy为: 51c视觉~CV~合集1_二值化_09 ;3号轮廓继承关系为 51c视觉~CV~合集1_二值化_10 .

4),第四个参数mode,代表定义轮廓的检索方式:

取值一:CV_RETR_EXTERNAL,只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略。

取值二:CV_RETR_LIST,检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1.

取值三:CV_RETR_CCOMP,检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层。

取值四:CV_RETR_TREE,检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。

5),第五个参数method,用来定义轮廓的近似方法:

取值一:CV_CHAIN_APPROX_NONE,保存物体边界上所有连续的轮廓点到contours向量内。

取值二:CV_CHAIN_APPROX_SIMPLE,仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留。

取值三:CV_CHAIN_APPROX_TC89_L1;

取值四:CV_CHAIN_APPROX_TC89_KCOS,取值三与四两者使用teh-Chinl chain 近似算法。

6),第六个参数,Point类型的offset参数,这个参数相当于在每一个检测出的轮廓点上加上该偏移量,而且Point还可以是负数。

在实际使用中,我们需要根据实际情况来选定合适的参数,比如第五个参数,如果你使用CV_CHAIN_APPROX_SIMPLE这个参数,那么返回的坐标向量集是仅包含拐点信息的,但是在上一篇文章 手把手教你编写傅里叶动画 为了得到轮廓的完整坐标数据,我必须使用CV_CHAIN_APPROX_NONE参数。请大家在实际使用过程中根据自己的实际情况来选择合适的参数

再来说下moments这个函数,其实moment在物理学中表示“力矩”的感念,“矩”在物理学中是表示距离和物理量乘积的物理量,表示物体的空间分布(至于汉语为什么把它翻译成矩这个字,我也不太明白,不知道是不是李善兰翻译的)。而在数学中,“矩”是概率论中的一个名词,它的本质是数学期望,我们从它的定义中就可以看出:

假设 51c视觉~CV~合集1_取值_11 是离散随机变量, 51c视觉~CV~合集1_取值_12 为常数, 51c视觉~CV~合集1_灰度_13 为正整数,如果 51c视觉~CV~合集1_二值化_14存在,则称 51c视觉~CV~合集1_二值化_14 为 51c视觉~CV~合集1_取值_11 关于 51c视觉~CV~合集1_取值_12 的 51c视觉~CV~合集1_灰度_13如果 51c视觉~CV~合集1_取值_19 时,称它为 51c视觉~CV~合集1_灰度_13 阶原点矩;如果 51c视觉~CV~合集1_二值化_21 时,称它为 51c视觉~CV~合集1_灰度_13 阶中心距。我们知道数学期望的计算公式是 

51c视觉~CV~合集1_灰度_23其中 51c视觉~CV~合集1_取值_24 是 51c视觉~CV~合集1_取值_11 的概率密度,从这个公式中看出,无论是在物理上,还是在数学上,矩都有种某个量与“距离”乘积的概念,因此,我们再直观一点,直接把“矩”抽象成为下面一个公式: 51c视觉~CV~合集1_二值化_26当 51c视觉~CV~合集1_二值化_27

有了这方面知识,我们再回顾一下 图像处理中的矩,对于图像中的矩是图像像素强度的某个特定加权平均(矩),或者是这样的矩的函数,因为图像是二维的,对于二元有界函数 51c视觉~CV~合集1_二值化_28 它的 51c视觉~CV~合集1_灰度_29 阶矩为: 51c视觉~CV~合集1_灰度_30如果把图像看成是一块质量密度不均匀的薄板,其图像的灰度分布函数 51c视觉~CV~合集1_二值化_28如零阶矩表示它的总质量;一阶矩表示它的质心;二阶矩又叫惯性矩,表示图像的大小和方向,当然,对于图像来说,它的像素坐标是离散的,因此积分可用求和来计算,图像的零阶矩: 

51c视觉~CV~合集1_取值_32显然,对于二值化图像,由于 51c视觉~CV~合集1_二值化_28 非 51c视觉~CV~合集1_二值化_34 即 51c视觉~CV~合集1_灰度_35图像的零阶矩表示区域内的像素点数,也即区域的面积

当 51c视觉~CV~合集1_二值化_36 时,它的一阶矩变为:51c视觉~CV~合集1_灰度_37它是横坐标 51c视觉~CV~合集1_取值_38

同样,当 51c视觉~CV~合集1_灰度_39 时,它的一阶矩变为:51c视觉~CV~合集1_二值化_40它是纵坐标 51c视觉~CV~合集1_二值化_41像素值的乘积,如果是二值化图像,每个像素点所有纵坐标的和。

根据物理意义,我们还可以得出轮廓的质心计算公式: 51c视觉~CV~合集1_灰度_42其中 51c视觉~CV~合集1_二值化_43

为了获得矩的不变特征,往往采用中心矩或者归一化的中心距,在图像中中心矩的定义为: 51c视觉~CV~合集1_二值化_44

Moments moments( 
    InputArray array, 
    bool binaryImage = false 
    );

第一个参数是输入一个数组,也就是我们上面计算出来的contours向量,第二个参数默认为false.

我们再来看整个程序的编写方法:

首先我们使用opencv中的VideoCapture类来进行读取视频流,并抓取视频中的每一帧图像:

Mat img; //定义图像矩阵
VideoCapture cap("1.mp4");
cap.read(img);

这样我们便把图像读取到img变量中了,当然,为了叙述方便,这只是其中的部分代码,实际你在运行过程中为了不断读取,外面还要嵌套个while(true)的循环。

读取到一帧图像后,我们需要对这张图像进行一些预处理,一般情况下,无论是传统算法还是深度学习算法,预处理的方法基本上都是一样的,比如灰度转换,图像滤波,二值化处理,边缘提取等等。在这里也一样,我们先对抓取到的图像进行灰度转换,灰度图像有诸多好处,比如它只包含亮度特征,而且一般情况下它是单通道的,256色调色板,一个像素占只用一个字节,储存起来非常整齐等等。

我们最终要的预处理结果是将其转化为二值化图像,二值化图像是灰度图像的一种特殊情况,它的像素亮度只有0与255两种情况,也就是非黑既白,在图像处理中,二值图像占有非常重要的地位,比如,图像的二值化有利于图像的进一步处理,使图像变得简单,使后面的计算量大大减少,而且还能能凸显出感兴趣的目标的轮廓。

灰度处理的算子接口为:

void cv::cvtColor 
     (InputArray src,
      OutputArray dst,
      int code,
      int dstCn = 0)

这个函数第一个参数src为输入图像,第二个参数dst为输出图像,第三个参数为需要转换的色彩空间,一般我们选取CV_BGR2GRAY即可,第四个参数默认为0.

灰度转化后,我们再进行二值化处理,这个算子比较简单,函数原型为:

void cv::threshold
     (InputArray src,
      OutputArray dst,
      double thresh,
      double maxval,
      int type)

同样,这个函数前两个参数分别为输入、输出参数;第三个参数thresh为设定的阈值;第四个参数maxval为设置的最大值,一般选取255;第五个参数type为阈值类型,表示当灰度值大于(或小于)阈值时将该灰度值赋成的值,其中最常用的有两个,分别是THRESH_BINARY,THRESH_BINARY_INV:

如果是THRESH_BINARY的话,表示当图像亮度值大于阈值thresh的话,将其设置为maxval,否则设置为0: 51c视觉~CV~合集1_灰度_45而 THRESH_BINARY_INV 则恰恰相反,当亮度大于阈值的时候,将其设置为0,否则设置为maxval: 51c视觉~CV~合集1_取值_46我们再回到视频中,先截取一张图片,我们使用下面梁行代码先后对其进行灰度、二值化处理:

//灰度处理
cvtColor(img, grayImage, CV_BGR2GRAY);  
 //二值化处理
threshold(grayImage, binImage, 
    128, 255, CV_THRESH_BINARY);

下面第二、第三分别是灰度、二值化处理后的结果,可见当原图经过二值化分割后,图像增强明显,明暗分离,更重要的是原本模糊的划痕变得非常清晰,特征非常明显,这对我们下一步提取划痕轮廓从而做进一步的分析带来非常大的方便:

51c视觉~CV~合集1_灰度_47

经过预处理后,下面我们就要想办法如何判定一件产品是具有划痕或者某种缺陷的,观察上面第三章图片,最直观的区别就是不良的产品中附带黑色长条斑点,如果我们对整个圆区域进行轮廓查找的话,它的轮廓数量应该大于0才对,反之等于0. 但是首先我们要知道如何定位每一个产品的位置,并找出它的轮廓,所以,我们先对每一张预处理后的binImage图像进行轮廓查找:

//定义轮廓点集
vector<vector<Point>> contours;
//定义继承关系
vector<Vec4i> hierarchy;
//进行轮廓查找
findContours(binImage, 
    contours, 
    hierarchy, 
    CV_RETR_EXTERNAL, 
    CV_CHAIN_APPROX_NONE, 
    Point(0, 0));

注意,在首次查找的时候,我们关心的是整个产品的轮廓,或者产品的质心坐标在哪里,所以我们只需要抓取最外轮廓即可,所以第四个参数我们设置为CV_RETR_EXTERNAL,上面已经解释过了,意为只检查最外围轮廓,目的是为了方面后面从原图中截取产品轮廓。为了方便后面计算,也为了计数方便,我先定义了一个产品类型的结构体myproduct,里面包含外接Rect,质心cx,xy,还有对应索引index. 为了去除重复,每次计算每个轮廓的位置,再根据当前帧里面的所有产品坐标与上一帧图像中产品里面的坐标进行对比,如果有两个产品在上下两帧中偏移的距离很小,那么我们可以认为它是同一件产品,否则就是新出现的产品,就需要放入我们的product容器里面,这其实在实际抓拍检测过程中,比较常用的去重方法:

for (int i = 0; i < contours.size(); i++)
{
  double area = contourArea(contours[i]);
  //小于18000我们认为是干扰因素
  if (area > 18000)
  {
    Moments M = moments(contours[i]);  //计算矩
    double center_x = M.m10 / M.m00;
    double center_y = M.m01 / M.m00;
    Rect rect = boundingRect(contours.at(i));
    bool isNew = true;
    //必须保证产品完全出来
    if (center_x > 100)
    {
      for (int i = 0; i < product.size(); i++)
      {
        double h = abs(center_x - product[i].cx);
        double v = abs(center_y - product[i].cy);
        if(h < 25 && v < 25)
        {
          isNew = false;
          //说明不是新的,那就要更新一下
          product[i].cx = center_x;
          product[i].cy = center_y;
          product[i].rect = rect;
        }
      }
      if (isNew)
      {
        myproduct p;
        p.cx = center_x;
        p.cy = center_y;
        p.rect = rect;
        idx++;
        p.index = idx;
        product.push_back(p);
      }
    }
  }

函数计算后,product里面就包含了我们所要当前帧中的所有产品对象,包括质心、外接矩形等等,然后根据最大外接矩形的坐标,再回到原图中img中,把这个矩形包含的产品轮廓给裁剪出来,做进一步分析,裁剪的方法很简单,我们根据上面程序返回的rect,直接在原图像img中进行裁剪即可,即可得到右边小图像:

Mat roi = img(rect);


51c视觉~CV~合集1_二值化_48

拿到上图右边截取的小图像roi, 对其再做进一步的分析,从而能够抓取到缺陷划痕的轮廓,方法还是先灰度处理再二值化操作,然后查找轮廓。但注意,这次查找轮廓的对象是我们截取的roi区域,为了分析roi区域内所有的缺陷,我们必须要找出对象中的所有轮廓,所以findcontours参数中mode参数我们就不能使用CV_RETR_EXTERNAL了,而要改用CV_RETR_TREE:

Mat grayImage;
  Mat binImage;
  cvtColor(roi, grayImage, CV_BGR2GRAY);  //灰度处理
  threshold(grayImage, 
      binImage, 
      128, 255, 
      CV_THRESH_BINARY);  //二值化处理
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  //检测出roi内所有轮廓
  findContours(binImage, contours, 
      hierarchy, 
      CV_RETR_TREE, 
      CV_CHAIN_APPROX_NONE, 
      Point(0, 0));

这次返回的contours其实还包含单个产品的最外轮廓,显然不是我们想要的,对其进行适当过滤,过滤的方法很简单,还是根据面积来进行判断,进而再把缺陷部分给提取出来,我们可以用下面两行代码,将提取到的轮廓全部绘制出来:

//最后一个参数如果为负值CV_FILLED则为填充内部
//第三个参数如果为-1则绘制所有的轮廓
drawContours(roi, 
  contours, 
  -1, 
  Scalar(0, 0, 255), 0);

51c视觉~CV~合集1_二值化_49

下一步我们就要想办法把产品内部的一个划痕轮廓给单独提取出来,也就是找一把“剪刀”,将它最大外接矩形给剪切出来,同时为了防止划痕周围颜色的干扰,我们还需要将剩余部分涂成黑色,方法很简单,我们需要单独定义一个mask掩模,再使用fillPoly函数将这块区域填充到我们的mask上面即可。我们看一下这个函数的原型:

//填充多边形函数
void fillPoly(InputOutputArray img, 
InputArrayOfArrays pts, 
const Scalar& color, 
int lineType = LINE_8, 
int shift = 0,
Point offset = Point());

第一个参数img是输入的参数,也就是我们的mask, 第二个参数是轮廓点数据集,第三个参数为填充的颜色。但是这里边需要注意,第二个参数如果我们直接把上面那张提取轮廓得到的划痕轮廓数据集contours[index]输入进去是不行的,会报错,我尝试了多次,发现可能fillPoly函数第二个参数只接收vector<vector<Point>>类型的数据集,所以我们在使用之前需要把划痕的最大外接矩形使用findContours函数再做一次轮廓查找,得到缺陷轮廓contours_defect,方法跟上面的一样,事先先灰度处理,再阈值分割。

//定义掩膜,CV_8UC1代表单通道
Mat mask = Mat::zeros(grayImage.size(), 
CV_8UC1);
//把缺陷轮廓填充掩膜
fillPoly(mask, 
contours_defect, 
Scalar(255, 255, 255));

这里面在定义mask的时候,尽量避免使用白色背景,因为我发现在白色背景使用findContours的时候会把整个背景图片的外框也当成一个轮廓,无形之中给我们带来了不必要的麻烦。填充后的mask我们再与原图中的缺陷外接矩形轮廓做位与运算,最终将原图中的缺陷轮廓抠出来:

//图像按位与运算
bitwise_and(grayImage, mask, result);


51c视觉~CV~合集1_灰度_50

上图中第三幅就是我们要的抠图效果,里面的灰色图案就是划痕形状图像背景是纯黑色,为我们下一步分析免去了诸多干扰。

接下来就是如何分析这张缺陷了,并把缺陷类型给标注出来。我们再看一下另外一种缺陷,比如掉漆的缺陷:

51c视觉~CV~合集1_二值化_51

根据肉眼判断,显然左侧的掉漆缺陷背景颜色更黑一点,这是他们两种缺陷的最大区别,所以我们可以采用灰度直方图的算法对其进行分析,从而判断是何种类型。所谓灰度直方图,就是以横坐标为像素的亮度,纵坐标为对应像素的数量,将整张图片对应的像素分布给计算出来,显然,如果是掉漆缺陷的话,它的低亮度像素值(偏向黑色)占的比重更多一些,而如果是划痕的话,整体像素亮度区间会分布的高一些,我们就按照这个方法来定义缺陷的类型。在opencv中计算灰度直方图的算法已经给我们封装好了,我们来看一下函数的原型:

//计算灰度直方图
void calcHist(
const Mat* images, //输入图像指针
int nimages,  //输入图像的个数
const int* channels, //需要统计的第几通道
InputArray mask, //掩膜
OutputArray hist, //输出的直方图数组
int dims, //需要统计直方图通道的个数
const int* histSize, //直方图分成区间个数指针
const float** ranges, //统计像素值的区间
bool uniform = true, //是否对得到的直方图数组进行归一化处理
bool accumulate = false); //在多个图像时,是否累计计算像素值得个数

用这个函数计算后,我们需要稍微再加一点分析,将分布直方图对应像素出现的概率给计算出来即可:

MatND hist;
//256个,范围是0,255.
const int histSize = 256;
float range[] = { 0, 255 };
const float *ranges[] = { range };
const int channels = 0;
calcHist(&result, 1, &channels, 
cv::Mat(), hist, 1, 
&histSize, &ranges[0]);
float *h = (float*)hist.data;
double hh[256];
double sum = 0;
for (int i = 0; i < 256; ++i) 
{
  hh[i] = h[i];
  sum += hh[i];
}

最后根据出现的概率区间来对缺陷进行类型定义,值得一提的是,下面两个阈值需要根据实际情况进行调整:

double hist_sum_scratch = 0;
double hist_sum_blot = 0;
for (int i = 90; i < 135; i++)
{
  hist_sum_scratch += hh[i];
}
hist_sum_scratch /= sum;
for (int i = 15; i < 90; i++)
{
  hist_sum_blot += hh[i];
}
hist_sum_blot /= sum;
int type = 0;
if (hist_sum_scratch > 0.1)
{
  type = 1;  //划痕类型
}
if (hist_sum_blot > 0.3)
{
  type = 2;  //掉漆类型
}

好了,上面就是整个项目的检测流程,在这里我们使用的是传统算法,其实在利用传统算法进行缺陷检测基本流程都差不多,最终无非是根据像素的分布的特征进行分类,在实际场景比较单一的情况下使用起来它的鲁棒性还算可以,比如这个视频里面,流水线颜色背景是黑色的,场景模式也比较单一,用起来不会出大问题。但是如果一旦遇到实际场景比较复杂的情况下,传统算法用起来就它的局限性就非常大,举个例子,你敢保证所有的缺陷都是这两种,你敢保证实际生产情况下所有缺陷类型直方图分布严格按照自己规定的阈值进行的?显然实际场景的复杂程度要远远超过我们的所能想到的,这个时候就要用到深度学习算法进行检测了,深度学习算法相比传统算法有诸多优点,诸如它抛开了像素层面的繁琐分析,只要前期数据量够大,它就能适应各种复杂的环境等等。



二、背景去除

什么是背景消除?

    当您喜欢图像中的某个焦点部分,并希望在不影响要保留部分的情况下移除其中的干扰元素时,这称为背景移除。从技术上讲,背景移除并不像它所说的那样有效。

    有时您需要从图像中删除框架前面的某些内容。因此,背景移除并不总是指擦除框架后面的材料。

为什么需要背景消除?

    在数码摄影中,人们笨拙地点击数千张照片,完成后再整理出他们需要的图像。有时他们点击的照片很好,颜色、亮度和其他重要部分可能很完美,但一个错误,如背景中一些不必要的阴影或材料,就会毁掉照片!虽然所有电子商务网站都需要具有透明背景的图像,但其背后的原因是人们会将全部注意力集中在您可能想要销售的产品上。

    去除图片背景是如今人们最常使用的图像处理服务。让我们看看为什么你愿意花钱去做这件事。

    因此我们将重点讨论如何删除应用程序或网站背景中的背景。

    现在,我们将使用OpenC 库进行像素级背景去除

一、去除黑色背景:

    此方法仅侧重于去除黑色背景,因此我们将重点关注阈值技术和 alpha(不透明度参数)

    具体步骤:

  • 导入 opencv 库
  • 加载并读取图像
  • 将图像背景转换为灰色图像背景
  • 此外,应用阈值技术
  • 彩色图像 RGB 的分割通道
  • 将 rgba 合并为彩色/多通道图像
# Import the library OpenCV
import cv2


# Import the image
file_name = r"/home/vk/Desktop/CV_BackgroundRemoval/black_backgroundremoval/6.jpg"


# Read the image
src = cv2.imread(file_name, 1)


# Convert image to image gray
tmp = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)


# Applying thresholding technique
_, alpha = cv2.threshold(tmp, 0, 255, cv2.THRESH_BINARY)


# Using cv2.split() to split channels 
# of coloured image
b, g, r = cv2.split(src)


# Making list of Red, Green, Blue
# Channels and alpha
rgba = [b, g, r, alpha]


# Using cv2.merge() to merge rgba
# into a coloured/multi-channeled image
dst = cv2.merge(rgba, 4)


# Writing and saving to a new image
cv2.imwrite("/home/vk/Desktop/CV_BackgroundRemoval/black_backgroundremoval/opencv_bgrm.png", dst)

二. 背景去除 - 3 种方法(OpenCV)

    方法 1

    第一种方法,我们专注于去除前景、背景并将两者结合起来。

    具体步骤:

  • 执行高斯模糊以消除噪音
  • 通过将像素放入 RGB 空间中的六个等距箱中来简化我们的图像。换句话说,转换成 5 x 5 x 5 = 125 种颜色
  • 将图像转换为灰度并应用阈值处理以获得前景的蒙版
  • 将蒙版应用到我们的合并图像上,仅保留前景(本质上是去除背景)
def bgremove1(myimage=myimage):
    myimage =cv2.imread(myimage)
    # Blur to image to reduce noise
    myimage = cv2.GaussianBlur(myimage,(5,5), 0)


    # We bin the pixels. Result will be a value 1..5
    bins=np.array([0,51,102,153,204,255])
    myimage[:,:,:] = np.digitize(myimage[:,:,:],bins,right=True)*51


    # Create single channel greyscale for thresholding
    myimage_grey = cv2.cvtColor(myimage, cv2.COLOR_BGR2GRAY)


    # Perform Otsu thresholding and extract the background.
    # We use Binary Threshold as we want to create an all white background
    ret,background = cv2.threshold(myimage_grey,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)


    # Convert black and white back into 3 channel greyscale
    background = cv2.cvtColor(background, cv2.COLOR_GRAY2BGR)
    cv2.imwrite("method1_background_image.png",background)


    # Perform Otsu thresholding and extract the foreground.
    # We use TOZERO_INV as we want to keep some details of the foregorund
    ret,foreground = cv2.threshold(myimage_grey,0,255,cv2.THRESH_TOZERO_INV+cv2.THRESH_OTSU)  #Currently foreground is only a mask
    foreground = cv2.bitwise_and(myimage,myimage, mask=foreground)  # Update foreground with bitwise_and to extract real foreground
    cv2.imwrite("method1_foreground_image.png",foreground)


    # Combine the background and foreground to obtain our final image
    finalimage = background+foreground


    cv2.imwrite("method1_bm.png",finalimage)


    return finalimage

方法2 - 阈值技术

    显然,在方法 1 中,我们执行了大量图像处理。可以看出,高斯模糊和 Otsu 阈值需要大量处理。此外,在应用高斯模糊和分箱时,我们在图像中丢失了很多细节。因此,我们希望设计一种有望更快的替代策略。考虑到效率并知道 OpenCV 是一个高度优化的库,我们选择了一种以阈值为重点的方法:

    具体步骤:

  • 将我们的图像转换为灰度
  • 执行简单的阈值处理来为前景和背景构建蒙版
  • 根据掩码确定前景和背景
  • 结合前景和背景重建原始图像
def  bgremove2 ( myimage=myimage ): 
    myimage =cv2.imread(myimage) 


    # 首先转换为灰度
    myimage_grey = cv2.cvtColor(myimage, cv2.COLOR_BGR2GRAY) 


    ret,baseline = cv2.threshold(myimage_grey, 127 , 255 ,cv2.THRESH_TRUNC) 


    ret,background = cv2.threshold(baseline, 126 , 255 ,cv2.THRESH_BINARY) 


    ret,foreground = cv2.threshold(baseline, 126 , 255 ,cv2.THRESH_BINARY_INV) 


    foreground = cv2.bitwise_and(myimage,myimage, mask=foreground)   # 用 bitwise_and 更新前景以提取真实前景
    cv2.imwrite( "method2_foreground_image.png",foreground) 
    # 将黑白转换回 3 通道灰度
    background = cv2.cvtColor(background, cv2.COLOR_GRAY2BGR) 
    cv2.imwrite( "method2_background_image.png",background) 




    # 结合背景和前景以获得最终图像
    finalimage = background+foreground 
    cv2.imwrite( "method2_image.png",finalimage) 
    return finalimage

方法 3 — HSV 颜色空间

    到目前为止,我们一直在 BGR 颜色空间中工作。考虑到这一点,我们的图像容易出现光线不足和阴影的情况。毫无疑问,我们想知道在 HSV 颜色空间中工作是否会得到更好的结果。为了不丢失图像细节,我们还决定不执行高斯模糊或图像分级。而是专注于使用 Numpy 进行阈值处理和生成图像蒙版。一般来说,我们的策略如下:

    具体步骤:

  • 将我们的图像转换为 HSV 颜色空间
  • 执行简单的阈值处理,使用 Numpy 根据饱和度和值创建地图
  • 将 S 和 V 的映射组合成最终的蒙版
  • 根据组合掩码确定前景和背景
  • 结合提取的前景和背景重建原始图像
def bgremove3(myimage=myimage):
    myimage =cv2.imread(myimage)


    # BG Remover 3
    myimage_hsv = cv2.cvtColor(myimage, cv2.COLOR_BGR2HSV)


    #Take S and remove any value that is less than half
    s = myimage_hsv[:,:,1]
    s = np.where(s < 127, 0, 1) # Any value below 127 will be excluded


    # We increase the brightness of the image and then mod by 255
    v = (myimage_hsv[:,:,2] + 127) % 255
    v = np.where(v > 127, 1, 0)  # Any value above 127 will be part of our mask


    # Combine our two masks based on S and V into a single "Foreground"
    foreground = np.where(s+v > 0, 1, 0).astype(np.uint8)  #Casting back into 8bit integer


    background = np.where(foreground==0,255,0).astype(np.uint8) # Invert foreground to get background in uint8
    background = cv2.cvtColor(background, cv2.COLOR_GRAY2BGR)  # Convert background back into BGR space
    cv2.imwrite("method3_background_image.png",background)
    foreground=cv2.bitwise_and(myimage,myimage,mask=foreground) # Apply our foreground map to original image
    cv2.imwrite("method3_foreground_image.png",foreground)
    finalimage = background+foreground # Combine foreground and background
    cv2.imwrite("method3_image.png",finalimage)
    return finalimage

 以上三种方法是常用的,Thersholding,HSV,Pixel-wise Blur背景。

    这样才能精准的去除背景,所以我们将重点放在深度学习上。

    准确地去除背景,以便我们继续深度学习和两阶段过程。

    1. 在 U2net 中生成清晰的掩码

    2. 减去掩码和输入图像

    现在,我讨论一下 U2net 架构,

    第一阶段:U2Net

    U2net:我们设计了一个简单但功能强大的深度网络架构 U2-Net,用于显著对象检测 (SOD)。我们的 U2-Net 架构是一个两级嵌套 U 结构。该设计具有以下优点:(1) 由于我们提出的 ReSidual U 块 (RSU) 中混合了不同大小的接受场,因此能够从不同尺度捕获更多上下文信息;(2) 由于这些 RSU 块中使用了池化操作,它增加了整个架构的深度,而不会显著增加计算成本。这种架构使我们能够从头开始训练深度网络,而无需使用来自图像分类任务的主干。我们实例化了所提架构的两个模型,U2-Net(176.3 MB,GTX 1080Ti GPU 上 30 FPS)和 U2-Net†(4.7 MB,40 FPS),以方便在不同环境中使用。两种模型在六个 SOD 数据集上都实现了具有竞争力的性能。

  下载 — U2net 模型:

https://drive.google.com/file/d/1ao1ovG1Qtx4b7EoskHXmi2E9rp5CHLcZ/view
import os
from skimage import io, transfor
import torch
import torchvision
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms#, utils
# import torch.optim as optim


import numpy as np
from PIL import Image
import glob


from data_loader import RescaleT
from data_loader import ToTensor
from data_loader import ToTensorLab
from data_loader import SalObjDataset


from model import U2NET # full size version 173.6 MB
from model import U2NETP # small version u2net 4.7 MB


# normalize the predicted SOD probability map
def normPRED(d):
    ma = torch.max(d)
    mi = torch.min(d)


    dn = (d-mi)/(ma-mi)


    return dn


def save_output(image_name,pred,d_dir):


    predict = pred
    predict = predict.squeeze()
    predict_np = predict.cpu().data.numpy()


    im = Image.fromarray(predict_np*255).convert('RGB')
    img_name = image_name.split(os.sep)[-1]
    image = io.imread(image_name)
    imo = im.resize((image.shape[1],image.shape[0]),resample=Image.BILINEAR)


    pb_np = np.array(imo)


    aaa = img_name.split(".")
    bbb = aaa[0:-1]
    imidx = bbb[0]
    for i in range(1,len(bbb)):
        imidx = imidx + "." + bbb[i]


    imo.save(d_dir+imidx+'.png')


def main():


    # --------- 1. get image path and name ---------
    model_name='u2net'#u2netp






    image_dir=os.path.join(os.getcwd(),'test_data','images','input')
    prediction_dir=os.path.join(os.getcwd(),'test_data','images',model_name+'_results'+os.sep)
    model_dir = os.path.join(os.getcwd(),'saved_models',model_name,model_name+'.pth')


    img_name_list = glob.glob(image_dir + os.sep + '*')
    print(img_name_list)


    # --------- 2. dataloader ---------
    #1. dataloader
    test_salobj_dataset = SalObjDataset(img_name_list = img_name_list,
                                        lbl_name_list = [],
                                        transform=transforms.Compose([RescaleT(320),
                                                                      ToTensorLab(flag=0)])
                                        )
    test_salobj_dataloader = DataLoader(test_salobj_dataset,
                                        batch_size=1,
                                        shuffle=False,
                                        num_workers=1)


    # --------- 3. model define ---------
    if(model_name=='u2net'):
        print("...load U2NET---173.6 MB")
        net = U2NET(3,1)
    elif(model_name=='u2netp'):
        print("...load U2NEP---4.7 MB")
        net = U2NETP(3,1)
    net.load_state_dict(torch.load(model_dir))
    if torch.cuda.is_available():
        net.cuda()
    net.eval()


    # --------- 4. inference for each image ---------
    for i_test, data_test in enumerate(test_salobj_dataloader):


        print("inferencing:",img_name_list[i_test].split(os.sep)[-1])


        inputs_test = data_test['image']
        inputs_test = inputs_test.type(torch.FloatTensor)


        if torch.cuda.is_available():
            inputs_test = Variable(inputs_test.cuda())
        else:
            inputs_test = Variable(inputs_test)


        d1,d2,d3,d4,d5,d6,d7= net(inputs_test)


        # normalization
        pred = d1[:,0,:,:]
        pred = normPRED(pred)


        # save results to test_results folder
        if not os.path.exists(prediction_dir):
            os.makedirs(prediction_dir, exist_ok=True)
        save_output(img_name_list[i_test],pred,prediction_dir)


        del d1,d2,d3,d4,d5,d6,d7


if __name__ == "__main__":
    main()

第 2 阶段:减去输入图像和掩码

import cv2


#subimage
subimage=Image.open('/content/drive/MyDrive/background_removal_DL/test_data/images/u2net_results/4.png')
#originalimage
original=Image.open('/content/drive/MyDrive/background_removal_DL/test_data/images/input/4.jpeg')


subimage=subimage.convert("RGBA")
original=original.convert("RGBA")


subdata=subimage.getdata()
ogdata=original.getdata()


newdata=[]
for i in range(subdata.size[0]*subdata.size[1]):
  if subdata[i][0]==0 and subdata[i][1]==0 and subdata[i][2]==0:
    newdata.append((255,255,255,0))
  else:
    newdata.append(ogdata[i])
subimage.putdata(newdata)
subimage.save('/content/drive/MyDrive/background_removal_DL/test_data/images/output/output_bgrm.png',"PNG")

在这个 U2net 架构中生成 mask 输出。并减去输入和 mask 图像。但它并没有明显去除背景。所以现在新引入了 Rembg 方法。

    引用:

  • Rembg 是深度学习方法。
  • Rembg 是骨干架构——U2Net。
  • 显然,背景已被删除。
from rembg import remove
import cv2


input_path = '/home/vk/Desktop/CV_BackgroundRemoval/rembg/6.jpg'
output_path = '/home/vk/Desktop/CV_BackgroundRemoval/rembg/rembg_output2.png'


input = cv2.imread(input_path)
output = remove(input)
cv2.imwrite(output_path, output)

  主干比较:

  完整代码:

https://github.com/VK-Ant/CV_BackgroundRemoval

    参考链接:

https://github.com/Nkap23/background_removal_DL
https://nisargkapkar.hashnode.dev/image-and-video-background-removal-using-deep-learning
https://www.freedomvc.com/index.php/2022/01/17/basic-background-remover-with-opencv/
https://clipingchoice.com/what-is-background-removing/
https://arxiv.org/abs/2005.09007v3


三、OpenCV和K-Means聚类实现颜色分割

   颜色分割是计算机视觉中使用的一种技术,可根据颜色识别和区分图像中的不同物体或区域。聚类算法可以自动将相似的颜色归为一组,而无需为每种颜色指定阈值。当处理颜色范围很广的图像或事先不知道确切阈值时,此功能非常有用。

    本文中,我们将探索如何使用 K-Means聚类算法进行颜色分割,并计算每种颜色的对象数量。我们将使用“泡泡射击”游戏中的图像作为示例,根据轮廓查找和过滤气泡对象,并应用 K 均值算法将颜色相似的气泡分组在一起。这将使我们能够计算和提取颜色相似的气泡的蒙版,以供进一步的下游应用使用。我们将使用OpenCV和scikit-learn库进行图像分割和颜色聚类。

from matplotlib import pyplot as plt
import cv2
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
%matplotlib inlin
image = cv2.imread(r'bubbles.jpeg', cv2.IMREAD_UNCHANGED)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.axis('off')

使用阈值提取二进制掩码

    第一步是从背景中提取所有气泡。为此,我们将首先使用cv2.cvtColor()函数将图像转换为灰度,然后使用cv2.threshold()它将其转换为二进制图像,其中像素为 0 或 255。阈值设置为 60,因此所有低于 60 的像素都设置为 0,其他像素设置为 255。由于一些气泡在二进制图像上略有重叠,我们使用函数cv2.erode()将它们分开。腐蚀是一种形态学操作,可减小图像中对象的大小。它可用于去除小的白噪声,以及分离连接的对象。

image = cv2.imread(r'bubbles.jpeg', cv2.IMREAD_UNCHANGED)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_ , mask = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
mask = cv2.erode(mask, np.ones((7, 7), np.uint8))

使用轮廓提取对象边界

    下一步是在二值图像中查找对象。我们cv2.findContours()在二值图像上使用函数来检测对象的边界。轮廓定义为构成图像中对象边界的连续曲线。当使用cv2.RETR_EXTERNAL标志时,仅返回最外层的轮廓。该算法输出轮廓列表,每个轮廓代表图像中单个对象的边界。

contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

过滤轮廓并提取平均颜色

    为了去除不代表气泡的轮廓,我们将迭代生成的轮廓并仅选择面积较大的轮廓(大于 3000 像素)。这将使我们能够隔离气泡的轮廓并丢弃任何较小的物体,例如字母或背景的一部分。

filtered_contours = []
df_mean_color = pd.DataFrame()
for idx, contour in enumerate(contours):
    area = int(cv2.contourArea(contour))


    # if area is higher than 3000:
    if area > 3000:
        filtered_contours.append(contour)
        # get mean color of contour:
        masked = np.zeros_like(image[:, :, 0])  # This mask is used to get the mean color of the specific bead (contour), for kmeans
        cv2.drawContours(masked, [contour], 0, 255, -1)


        B_mean, G_mean, R_mean, _ = cv2.mean(image, mask=masked)
        df = pd.DataFrame({'B_mean': B_mean, 'G_mean': G_mean, 'R_mean': R_mean}, index=[idx])
        df_mean_color = pd.concat([df_mean_color, df])

    为了找到每个气泡的平均颜色,我们首先通过在黑色图像上用白色绘制气泡轮廓来为每个气泡创建一个蒙版。然后,我们将使用该cv2.mean()函数使用原始图像和气泡的蒙版计算气泡的平均蓝色、绿色和红色 (BGR) 通道值。每个气泡的平均 BGR 值存储在 pandas DataFrame 中。

使用 K-means 算法对相似颜色进行聚类

    最后,我们将应用 K 均值聚类算法将颜色相似的气泡分组在一起。我们将使用轮廓的平均颜色值作为库KMeans中算法的输入数据sklearn。n_clusters超参数指定算法要创建的聚类数。在本例中,由于气泡有 6 种颜色,我们将该值设置为 6。

    K 均值算法是一种流行的聚类方法,可用于将相似的数据点分组在一起。该算法的工作原理是将一组数据点作为输入,并将它们分成指定数量的聚类,每个聚类由一个质心表示。质心被初始化为数据空间内的随机位置,算法迭代地将每个数据点分配给由最接近的质心所代表的聚类。一旦所有数据点都被分配到一个聚类,质心就会更新为数据点在其聚类中的平均位置。这个过程重复进行,直到质心收敛到稳定的位置,数据点不再被重新分配到不同的聚类。通过使用 K 均值算法并以每个气泡的平均 BGR 值作为输入,我们可以将具有相似颜色的气泡分组在一起。

    一旦KMeans类初始化完毕,fit_predict就会调用该方法执行聚类。该fit_predict方法返回每个对象的聚类标签,然后将其分配给数据集中的新“标签”列。这使我们能够识别哪些数据点属于哪个聚类。

km = KMeans( n_clusters=6)
df_mean_color['label'] = km.fit_predict(df_mean_color)

    draw_segmented_objects然后定义函数来创建一个新的蒙版图像,其中包含相同颜色的气泡。首先创建一个二进制蒙版:在黑色图像上用白色绘制所有具有相同标签的气泡的轮廓。然后,使用来自的函数将原始图像与蒙版组合,bitwise_and得到cv2一个只有具有相同标签的气泡可见的图像。为方便起见,使用函数在图像上绘制每种颜色的气泡数量cv2.putText()。

def draw_segmented_objects(image, contours, label_cnt_idx, bubbles_count):
    mask = np.zeros_like(image[:, :, 0])
    cv2.drawContours(mask, [contours[i] for i in label_cnt_idx], -1, (255), -1)
    masked_image = cv2.bitwise_and(image, image, mask=mask)
    masked_image = cv2.putText(masked_image, f'{bubbles_count} bubbles', (200, 1200), cv2.FONT_HERSHEY_SIMPLEX,
                        fontScale = 3, color = (255, 255, 255), thickness = 10, lineType = cv2.LINE_AA)
    return masked_image

    对每组具有相同标签的气泡调用该draw_segmented_objects函数,以生成每种颜色的蒙版图像。可以通过计算按颜色分组后的 DataFrame 中的行数来确定每种颜色的珠子数量。

img = image.copy()
for label, df_grouped in df_mean_color.groupby('label'):
    bubbles_amount = len(df_grouped)
    masked_image = draw_segmented_objects(image, contours, df_grouped.index, bubbles_amount)
    img = cv2.hconcat([img, masked_image])


plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB) )

总 结

    使用 K-means 聚类进行颜色分割可以成为一种强大的工具,可根据颜色识别和量化图像中的对象。在本教程中,我们演示了如何使用 K-means 算法以及 OpenCV 和 scikit-learn 来执行颜色分割并计算图像中每种颜色的对象数量。此技术可应用于各种需要根据颜色分析和分类图像中对象的场景。



四、Python和MediaPipe~检测系统

疲劳驾驶的危害不堪设想,据了解,21%的交通事故都因此而生,尤其是高速路上,大多数车辆都是长途驾驶,加之速度快,危害更加严重。

    相关部门一般都会建议司机朋友及时休息调整后再驾驶,避免酿成惨

    作为视觉开发人员,我们可否帮助驾驶人员设计一套智能检测嗜睡的系统,及时提醒驾驶员注意休息?如下图所示,本文将详细介绍如何使用Python和MediaPipe来实现一个嗜睡检测系统。 天皓智联 开发板商城

实现步骤

思路:疲劳驾驶的司机大部分都有打瞌睡的情形,所以我们根据驾驶员眼睛闭合的频率和时间来判断驾驶员是否疲劳驾驶(或嗜睡)。详细实现步骤【1】眼部关键点检测。关于MediaPipe前面已经介绍过,具体可以查看下面链接的文章:------MediaPipe介绍与手势识别------

我们使用Face Mesh来检测眼部关键点,Face Mesh返回了468个人脸关键点

    由于我们专注于驾驶员睡意检测,在468个点中,我们只需要属于眼睛区域的标志点。眼睛区域有 32 个标志点(每个 16 个点)。为了计算 EAR,我们只需要 12 个点(每只眼睛 6 个点)。

    以上图为参考,选取的12个地标点如下:

  1. 对于左眼:  [362, 385, 387, 263, 373, 380]
  2. 对于右眼:[33, 160, 158, 133, 153, 144]

    选择的地标点按顺序排列:P 1、 P 2、 P 3、 P 4、 P 5、 P 6 

import cv2
import numpy as np
import matplotlib.pyplot as plt
import mediapipe as mp


mp_facemesh = mp.solutions.face_mesh
mp_drawing  = mp.solutions.drawing_utils
denormalize_coordinates = mp_drawing._normalized_to_pixel_coordinates


%matplotlib inline

获取双眼的地标(索引)点。

# Landmark points corresponding to left eye
all_left_eye_idxs = list(mp_facemesh.FACEMESH_LEFT_EYE)
# flatten and remove duplicates
all_left_eye_idxs = set(np.ravel(all_left_eye_idxs)) 


# Landmark points corresponding to right eye
all_right_eye_idxs = list(mp_facemesh.FACEMESH_RIGHT_EYE)
all_right_eye_idxs = set(np.ravel(all_right_eye_idxs))


# Combined for plotting - Landmark points for both eye
all_idxs = all_left_eye_idxs.union(all_right_eye_idxs)


# The chosen 12 points:   P1,  P2,  P3,  P4,  P5,  P6
chosen_left_eye_idxs  = [362, 385, 387, 263, 373, 380]
chosen_right_eye_idxs = [33,  160, 158, 133, 153, 144]
all_chosen_idxs = chosen_left_eye_idxs + chosen_right_eye_idx


【2】检测眼睛是否闭合——计算眼睛纵横比(EAR)。要检测眼睛是否闭合,我们可以使用眼睛纵横比(EAR) 公式:EAR 公式返回反映睁眼程度的单个标量:    1. 我们将使用 Mediapipe 的 Face Mesh 解决方案来检测和检索眼睛区域中的相关地标(下图中的点P 1 - P 6)。    2. 检索相关点后,会在眼睛的高度和宽度之间计算眼睛纵横比 (EAR)。    当眼睛睁开并接近零时,EAR 几乎是恒定的,而闭上眼睛是部分人,并且头部姿势不敏感。睁眼的纵横比在个体之间具有很小的差异。它对于图像的统一缩放和面部的平面内旋转是完全不变的。由于双眼同时眨眼,所以双眼的EAR是平均的。

上图:检测到地标P i的睁眼和闭眼。

底部:为视频序列的几帧绘制的眼睛纵横比 EAR。存在一个闪烁。

首先,我们必须计算每只眼睛的 Eye Aspect Ratio:

|| 表示L2范数,用于计算两个向量之间的距离。

为了计算最终的 EAR 值,作者建议取两个 EAR 值的平均值。

    一般来说,平均 EAR 值在 [0.0, 0.40] 范围内。在“闭眼”动作期间 EAR 值迅速下降。

    现在我们熟悉了 EAR 公式,让我们定义三个必需的函数:distance(…)、get_ear(…)和calculate_avg_ear(…)。

def distance(point_1, point_2):
    """Calculate l2-norm between two points"""
    dist = sum([(i - j) ** 2 for i, j in zip(point_1, point_2)]) ** 0.5
    return dist

get_ear (…)函数将.landmark属性作为参数。在每个索引位置,我们都有一个NormalizedLandmark对象。该对象保存标准化的x、y和z坐标值。

def get_ear(landmarks, refer_idxs, frame_width, frame_height):
    """
    Calculate Eye Aspect Ratio for one eye.


    Args:
        landmarks: (list) Detected landmarks list
        refer_idxs: (list) Index positions of the chosen landmarks
                            in order P1, P2, P3, P4, P5, P6
        frame_width: (int) Width of captured frame
        frame_height: (int) Height of captured frame


    Returns:
        ear: (float) Eye aspect ratio
    """
    try:
        # Compute the euclidean distance between the horizontal
        coords_points = []
        for i in refer_idxs:
            lm = landmarks[i]
            coord = denormalize_coordinates(lm.x, lm.y, 
                                             frame_width, frame_height)
            coords_points.append(coord)


        # Eye landmark (x, y)-coordinates
        P2_P6 = distance(coords_points[1], coords_points[5])
        P3_P5 = distance(coords_points[2], coords_points[4])
        P1_P4 = distance(coords_points[0], coords_points[3])


        # Compute the eye aspect ratio
        ear = (P2_P6 + P3_P5) / (2.0 * P1_P4)


    except:
        ear = 0.0
        coords_points = None


    return ear, coords_points

最后定义了calculate_avg_ear(…)函数:

def calculate_avg_ear(landmarks, left_eye_idxs, right_eye_idxs, image_w, image_h):
    """Calculate Eye aspect ratio"""


    left_ear, left_lm_coordinates = get_ear(
                                      landmarks, 
                                      left_eye_idxs, 
                                      image_w, 
                                      image_h
                                    )
    right_ear, right_lm_coordinates = get_ear(
                                      landmarks, 
                                      right_eye_idxs, 
                                      image_w, 
                                      image_h
                                    )
    Avg_EAR = (left_ear + right_ear) / 2.0


    return Avg_EAR, (left_lm_coordinates, right_lm_coordinates)

    让我们测试一下 EAR 公式。我们将计算先前使用的图像和另一张眼睛闭合的图像的平均 EAR 值。

image_eyes_open  = cv2.imread("test-open-eyes.jpg")[:, :, ::-1]
image_eyes_close = cv2.imread("test-close-eyes.jpg")[:, :, ::-1]


for idx, image in enumerate([image_eyes_open, image_eyes_close]):
   
    image = np.ascontiguousarray(image)
    imgH, imgW, _ = image.shape


    # Creating a copy of the original image for plotting the EAR value
    custom_chosen_lmk_image = image.copy()


    # Running inference using static_image_mode
    with mp_facemesh.FaceMesh(refine_landmarks=True) as face_mesh:
        results = face_mesh.process(image).multi_face_landmarks


        # If detections are available.
        if results:
            for face_id, face_landmarks in enumerate(results):
                landmarks = face_landmarks.landmark
                EAR, _ = calculate_avg_ear(
                          landmarks, 
                          chosen_left_eye_idxs, 
                          chosen_right_eye_idxs, 
                          imgW, 
                          imgH
                      )


                # Print the EAR value on the custom_chosen_lmk_image.
                cv2.putText(custom_chosen_lmk_image, 
                            f"EAR: {round(EAR, 2)}", (1, 24),
                            cv2.FONT_HERSHEY_COMPLEX, 
                            0.9, (255, 255, 255), 2
                )                
             
                plot(img_dt=image.copy(),
                     img_eye_lmks_chosen=custom_chosen_lmk_image,
                     face_landmarks=face_landmarks,
                     ts_thickness=1, 
                     ts_circle_radius=3, 
                     lmk_circle_radius=3
                )

结果:

如您所见,睁眼时的 EAR 值为0.28,闭眼时(接近于零)为 0.08

【3】设计一个实时检测系统。

  1. 首先,我们声明两个阈值和一个计数器。
  1. EAR_thresh: 用于检查当前EAR值是否在范围内的阈值。
  2. D_TIME:一个计数器变量,用于跟踪当前经过的时间量EAR < EAR_THRESH.
  3. WAIT_TIME:确定经过的时间量是否EAR < EAR_THRESH超过了允许的限制。 
  1. 当应用程序启动时,我们将当前时间(以秒为单位)记录在一个变量中t1并读取传入的帧。
  2. 接下来,我们预处理并frame通过Mediapipe 的 Face Mesh 解决方案管道。 
  3. 如果有任何地标检测可用,我们将检索相关的 ( Pi )眼睛地标。否则,在此处重置t1 和重置以使算法一致)。D_TIME (D_TIME
  4. 如果检测可用,则使用检索到的眼睛标志计算双眼的平均EAR值。
  5. 如果是当前时间,则加上当前时间和to之间的差。然后将下一帧重置为。EAR < EAR_THRESHt2t1D_TIMEt1 t2
  6. 如果D_TIME >= WAIT_TIME,我们会发出警报或继续下一帧。

参考链接:

https://learnopencv.com/driver-drowsiness-detection-using-mediapipe-in-python/