计算机视觉(一)

概念和基础

Posted by Gavin on October 7, 2019

戍楼寒

梦长安

概念

基本概念

计算机视觉是研究用计算机来模拟生物外显或宏观视觉功能的科学和技术,其首要目标是用图像创建或恢复现实世界模型,然后认知现实世界,中心任务就是对图像进行理解。
核心问题

  1. 单幅图像:分割问题、识别问题
  2. 多幅图像:识别、三维重构、运动分割与跟踪

分割问题:

主要研究方向

  1. 语义分割(Semantic Segmentatio)
  2. 人像识别(Portrait Mattin)
  3. 多角度场景深度重建

研究内容

  1. 输入设备
    成像设备和数字化设备
  2. 低层视觉 对输入的原始图像进行处理,使用了大量图像处理技术和算法,如图像滤波、图像增强、边缘检测、纹理检测、运动检测,以便从图像中抽取出诸如角点、边缘、线条、边界、色彩、纹理、运动等关于场景的特征
  3. 中层视觉
    主要任务是恢复场景的深度、表面法线方向、轮廓等有关场景的2.5维信息,实现的 途径有立体视觉(stereo vision)、测距成像(rangefinder) 、从X恢复形状(Shape from X, X = 明暗、纹理、运动)。系统标定、系统成像模型等研究内容一般也是在这个层次上进行的。分割、拟合等
  4. 高层视觉
    主要任务是在以物体为中心的坐标系中,在原始输入图像、图像基本特征、2.5维图的基础上,恢复物体的完整三维图,建立物体三维描述,识别物体并确定物体的位置和方向。
  5. 体系结构
    高度抽象的层次上,研究系统结构

基础

原始图像

二值图像

二值图像仅是0,1矩阵,简单易处理。
二值化算法

  • Otsu算法
void BGR2Binary(const cv::Mat &img)
{
    cv::Mat out;
    cv::threshold(img, out, 60, 255, cv::THRESH_BINARY);
    cv::namedWindow("Binary", cv::WINDOW_AUTOSIZE);
    cv::imshow("Binary",out);
    cv::waitKey(0);
}

三通道二值化:

黑白二值化:

几何特性

  • 面积(零阶矩)
    \(A = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}B[i,j]\)
  • 区域中心(一阶矩)

    \[\overline{x} = { \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}jB[i,j] \over A }\] \[\overline{y} = - { \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}iB[i,j] \over A }\]
  • 方向(某些物体没有方向,假定物体是长形的,其长轴方向默认为其方向)

    \(\chi^2 = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}r_{ij}B[i,j]\)
    其中\(r_{i,j}\)是点[i,j]到直线的距离,因此求方向便转化成一个最小化问题,可以利用最小二乘法求解:
    其中备选直线的方程:\(\rho = xcos\theta + ysin\theta\)
    点到直线的距离:\(r^2 = (xcos\theta + ysin\theta - \rho)^2\)
    则:

    \[\chi^2 = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}(x_{ij}cos\theta + y_{ij}sin\theta - \rho)^2B[i,j]\]

    对\(\rho\)求导,使得其导数为0,则\(\rho = \overline{x}cos\theta + \overline{y}sin\theta \),将其代入原式得:

    \[\chi^2 = a{cos}^2\theta + bsin\theta cos\theta + c{sin}^2\theta\]

    其中:

    \[a = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}(x_{ij}-\overline{x})^2B[i,j]\] \[b = 2\sum_{i=0}^{n-1}\sum_{j=0}^{m-1}(x_{ij}-\overline{x})(y_{ij}-\overline{y})B[i,j]\] \[c = \sum_{i=0}^{n-1}\sum_{j=0}^{m-1}(y_{ij}-\overline{y})^2B[i,j]\]

    微分求解\(\theta\):

    \[sin2\theta = \pm { b \over \sqrt{b^2 + (a-c)^2} }\] \[cos2\theta = \pm { a-c \over \sqrt{b^2 + (a-c)^2} }\]
  • 伸长率

    \[E = {\chi_{max} \over \chi_{min} }\]
  • 密集度

    \[C = {A \over p^2}\]

    A是面积,p是周长

  • 形态比(区域最小外接矩形长宽比)
  • 欧拉数(联通分量减去洞数)
  • 距离度量
    • 欧几里得距离(Euclidean)
    • 街区距离(block)
    • 棋盘距离(chess)
    • Minkowski距离
void Features(const cv::Mat &img)
{
    cv::Moments temp =  cv::moments(img);
    std::cout << "Area:" << std::endl;
    std::cout << temp.m00 << std::endl;
    std::cout << "First Moment:" << std::endl;
    std::cout << temp.m01 << std::endl;
    std::cout << temp.m10 << std::endl;
    std::cout << "Center Point:" << std::endl;
    std::cout << temp.m01 / temp.m00 << std::endl;
    std::cout << temp.m10 / temp.m00 << std::endl;

};

投影计算

对角线投影计算:假设对角线的标号d用行和列的仿射变换,即\(d = ai + bj + c\),对角线投影对应n+m-1条,将右上角的像素映射为第一个像素,左下角像素映射为最后一个像素,则\(d = i - j + m\)

垂直投影:

void VerticalProjection(const cv::Mat &img)
{
    cv::Mat lineImg(img.rows, img.cols, CV_8UC1, 255);
    for (int i = 0; i < img.rows; i++)
    {
        int count = 0;
        for (int j = 0; j < img.cols; j++)
        {
            if (static_cast<int>(img.at<uchar>(i,j)) == 0) count++;
        }
        for (int j = img.cols - 1; j >=0 && count >= 0; j--, count--)
        {
            lineImg.at<uchar>(i,j) = static_cast<uchar>(0);
        }
    }
    cv::namedWindow("empty", cv::WINDOW_AUTOSIZE);
    cv::imwrite("VerticalProjection.jpg", lineImg);
    //cv::imshow("Empty",lineImg);
    //cv::waitKey(0);
};


水平投影:

void HorizontalProjection(const cv::Mat &img)
{
    cv::Mat lineImg(img.rows, img.cols, CV_8UC1, 255);
    for (int i = 0; i < img.cols; i++)
    {
        int count = 0;
        for (int j = 0; j < img.rows; j++)
        {
            if (static_cast<int>(img.at<uchar>(j,i)) == 0) count++;
        }
        for (int j = img.rows - 1; j >=0 && count >= 0; j--, count--)
        {
            lineImg.at<uchar>(j,i) = static_cast<uchar>(0);
        }
    }
    cv::namedWindow("empty", cv::WINDOW_AUTOSIZE);
    cv::imwrite("../Pics/HorizontalProjection.jpg", lineImg);
    //cv::imshow("Empty",lineImg);
    //cv::waitKey(0);
};

对角线投影:

void LineProjection(const cv::Mat &img)
{
    cv::Mat lineImg(img.rows, img.cols + img.rows - 1, CV_8UC1, 255);
    std::map<int, int> mark;
    for (int i = 0; i < img.cols; i++)
    {
        for (int j = 0; j < img.rows; j++) {
            if (static_cast<int>(img.at<uchar>(j, i)) == 0) mark[i - j + img.cols]++;
        }
    }
    for (int i = 0; i < img.cols; i++)
    {
        for (int j = img.rows - 1; j >=0 && mark[i - j + img.cols] >= 0; j--, mark[i - j + img.cols]--)
        {
            lineImg.at<uchar>(j,i) = static_cast<uchar>(0);
        }
    }

    cv::namedWindow("empty", cv::WINDOW_AUTOSIZE);
    cv::imwrite("../Pics/LineProjection.jpg", lineImg);
    //cv::imshow("Empty",lineImg);
    //cv::waitKey(0);
};

联通区域标记

void FillConnection(cv::Mat &img, std::vector<std::vector<bool>> &visited, int i = 0, int j = 0, int count = 0)
{
    if (i < 0 || j < 0) return;
    if (i >= img.rows || j >= img.cols) return;
    if (visited[i][j]) return;
    if (static_cast<int>(img.at<uchar>(i, j)) != 0) return;
    img.at<uchar>(i, j) = static_cast<uchar>(count);
    visited[i][j] = true;
    FillConnection(img, visited, i, j+1, count);
    FillConnection(img, visited, i, j-1, count);
    FillConnection(img, visited, i+1, j, count);
    FillConnection(img, visited, i-1, j, count);
};

int ConnectionCount(cv::Mat &img)
{
    int count = 0;
    std::vector<std::vector<bool>> visited(img.rows,std::vector<bool>(img.cols, false));
    for (int i = 0; i < img.rows; i++)
    {
        for (int j = 0; j < img.cols; j++)
        {
            if (static_cast<int>(img.at<uchar>(i,j) == 0 && !visited[i][j])) {
                count++;
                FillConnection(img, visited, i, j, count);
            }
        }
    }
    cv::namedWindow("empty", cv::WINDOW_AUTOSIZE);
    cv::imwrite("../Pics/Connection.jpg", img);
    return count;
};


一共4183个四连通分量

区域边界跟踪

void findEdge(cv::Mat &img, cv::Mat &edge, std::vector<std::vector<bool>> &visited, int i, int j, int pi, int pj)
{
    if (i == pi && j == pj && visited[i][j]) return;
    if (visited[i][j]) return;
    visited[i][j] = true;
    if (j - 1 >= 0 && static_cast<int>(img.at<uchar>(i,j-1) == 0))
    {
        edge.at<uchar>(i, j-1) = static_cast<uchar>(255);
        //visited[i][j-1] = true;
        findEdge(img, edge, visited, i, j-1, pi, pj);
        return;
    }
    if (i + 1 < img.rows && j - 1 >= 0 && static_cast<int>(img.at<uchar>(i+1,j-1) == 0))
    {
        edge.at<uchar>(i+1, j-1) = static_cast<uchar>(255);
        //visited[i+1][j-1] = true;
        findEdge(img, edge, visited, i+1, j-1, pi, pj);
        return;
    }
    if (i + 1 < img.rows && static_cast<int>(img.at<uchar>(i+1,j) == 0))
    {
        edge.at<uchar>(i+1, j) = static_cast<uchar>(255);
        //visited[i+1][j] = true;
        findEdge(img, edge, visited, i+1, j, pi, pj);
        return;
    }
    if (i + 1 < img.rows && j + 1 < img.cols && static_cast<int>(img.at<uchar>(i+1,j+1)) == 0)
    {
        edge.at<uchar>(i+1, j+1) = static_cast<uchar>(255);
        //visited[i+1][j+1] = true;
        findEdge(img, edge, visited, i+1, j+1, pi, pj);
        return;
    }
    if (j + 1 < img.cols && static_cast<int>(img.at<uchar>(i,j+1)) == 0)
    {
        edge.at<uchar>(i, j+1) = static_cast<uchar>(255);
        //visited[i][j+1] = true;
        findEdge(img, edge, visited, i, j+1, pi, pj);
        return;
    }
    if (i - 1 >= 0 && j + 1 < img.cols && static_cast<int>(img.at<uchar>(i-1,j+1)) == 0)
    {
        edge.at<uchar>(i-1, j+1) = static_cast<uchar>(255);
        //visited[i-1][j+1] = true;
        findEdge(img, edge, visited, i-1, j+1, pi, pj);
        return;
    }
    if (i - 1 >= 0 && static_cast<int>(img.at<uchar>(i-1,j)) == 0)
    {
        edge.at<uchar>(i-1, j) = static_cast<uchar>(255);
        //visited[i-1][j] = true;
        findEdge(img, edge, visited, i-1, j, pi, pj);
        return;
    }
    if (i - 1 >= 0 && j - 1 >= 0 && static_cast<int>(img.at<uchar>(i-1,j-1)) == 0)
    {
        edge.at<uchar>(i-1, j-1) = static_cast<uchar>(255);
        //visited[i-1][j-1] = true;
        findEdge(img, edge, visited, i-1, j-1, pi, pj);
        return;
    }
}

void EdgeFollow(cv::Mat &img)
{
    cv::Mat edge(img.rows, img.cols, CV_8UC1);
    std::vector<std::vector<bool>> visited(img.rows,std::vector<bool>(img.cols, false));
    for (int i = 0; i < img.rows; i++)
    {
        for (int j = 0; j < img.cols; j++)
        {
            if (static_cast<int>(img.at<uchar>(i,j) == 0 && !visited[i][j])) {
                findEdge(img, edge, visited, i, j, i, j);
            }
        }
    }
    //findEdge(img, edge, visited, 370, 243, 370, 243);
    cv::namedWindow("Edge", cv::WINDOW_AUTOSIZE);
    //cv::imshow("Edge", edge);
    //cv::waitKey(0);
    cv::imwrite("../Pics/Edge.jpg", edge);
};

实现的不好,可以改进

形态学算子

  1. 膨胀
    把当前像素替换成所定义的结构像素集合中最大的值,物体尺寸会放大
  2. 腐蚀
    把当前像素替换成所定义的结构像素集合中最小的值,物体尺寸会缩小
  3. 开启
    先腐蚀,后膨胀
  4. 闭合
    先膨胀,后腐蚀

     void Structure(cv::Mat &img)
     {
         cv::Mat img_e, img_d, img_o, img_c;
         cv::erode(img, img_e, cv::Mat());
         cv::dilate(img, img_d, cv::Mat());
         cv::morphologyEx(img, img_c, cv::MORPH_CLOSE, cv::Mat());
         cv::morphologyEx(img, img_o, cv::MORPH_OPEN, cv::Mat());
         cv::imwrite("../Pics/Erode.jpg", img_e);
         cv::imwrite("../Pics/Dilate.jpg", img_d);
         cv::imwrite("../Pics/Close.jpg", img_c);
         cv::imwrite("../Pics/Open.jpg", img_o);
     }
    

边缘检测

一阶导数的局部极大值,二阶导数的过零点

  • 基于一阶导数的检测
    • Roberts交叉算子

      \[G(i,j) = |f(i,j) - f(i+1,j+1)| + |f(i+1,j) - f(i,j+1)|\] \[G_x = \left[ \begin{matrix} 1 & 0 \\\\ 0 & -1 \end{matrix} \right]\] \[G_y = \left[ \begin{matrix} 0 & -1 \\\\ 1 & 0 \end{matrix} \right]\]

        void Roberts(cv::Mat &img)
        {
            cv::Mat res(img);
            for (int i = 0; i < img.rows-1; i++)
            {
                for (int j = 0; j < img.cols-1; j++)
                {
                    res.at<uchar>(i,j) = abs(img.at<uchar>(i,j)-img.at<uchar>(i+1,j+1)) + abs(img.at<uchar>(i+1,j)-img.at<uchar>(i,j+1));
                }
            }
            cv::imwrite("../Pics/Roberts.jpg", res);
        }
      
    • Sobel算子

      \[G_x = \left[ \begin{matrix} -1 & 0 & 1 \\\\ -2 & 0 & 2 \\\\ -1 & 0 & 1 \end{matrix} \right]\] \[G_y = \left[ \begin{matrix} 1 & 2 & 1 \\\\ 0 & 0 & 0 \\\\ -1 & -2 & -1 \end{matrix} \right]\]
    • Prewitt算子

      \[G_x = \left[ \begin{matrix} -1 & 0 & 1 \\\\ -1 & 0 & 1 \\\\ -1 & 0 & 1 \end{matrix} \right]\] \[G_y = \left[ \begin{matrix} 1 & 1 & 1 \\\\ 0 & 0 & 0 \\\\ -1 & -1 & -1 \end{matrix} \right]\]
  • 基于二阶导数的检测
    • Laplacian(拉普拉斯)算子
    \[{ {\partial^2 f} \over {\partial x^2} } = { f[i,j+1]-2f[i,j]+f[i,j-1] }\] \[{ {\partial^2 f} \over {\partial y^2} } = { f[i+1,j]-2f[i,j]+f[i-1,j] }\] \[\nabla^2 = \left[ \begin{matrix} 0 & 1 & 0 \\\\ 1 & -4 & 1 \\\\ 0 & 1 & 0 \end{matrix} \right]\]
    • LoG(高斯+拉普拉斯)
    \[h(x,y) = \nabla^2(g(x,y)*f(x,y))\]

    等效方法:

      1. 先高斯卷积,再求卷积的拉普拉斯微分
      2. 先求高斯函数的拉普拉斯微分,再卷积
    

      void LoG(cv::Mat &img) {
          cv::Mat res(img);
          cv::GaussianBlur(img, res, cv::Size(3,3), 1, 1);
          cv::Laplacian(res, res, 0);
          cv::imwrite("../Pics/LoG.jpg", res);
      }
    
  • Canny算子

    1. 用高斯滤波平滑图像
    2. 用一阶偏导有限差分计算梯度幅值和方向
    3. 对梯度幅值进行非极大值抑制
    4. 用双阈值算法检测和连接边缘