openCV学习指南

本文最后更新于:2023年9月23日 下午

一些opencv的函数积累,还有传统opencv处理项目的流程整理

坐标转换

pnp解算

1
2
3
4
bool solvePnP(InputArray objectPoints, InputArray imagePoints, 
InputArray cameraMatrix, InputArray distCoeffs,
OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess=false,
int flags=ITERATIVE )

函数solvepnp接收一组对应的3D坐标和2D坐标,计算得到两组坐标对应的几何变换(旋转矩阵rvec,平移矩阵tvec),从而建立相机拍摄2D图像中物体坐标和3D世界坐标系中物体坐标的映射关系。

重映射

1
2
3
4
void projectPoints(InputArray objectPoints, InputArray rvec, InputArray tvec,
InputArray cameraMatrix, InputArray distCoeffs,
OutputArray imagePoints, OutputArray jacobian=noArray(),
double aspectRatio=0 )

根据所给的3D坐标和已知的几何变换来求解投影后的2D坐标

相机标定

同时标定两个摄像头

1
2
3
4
5
6
7
8
double stereoCalibrate(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints1,
             InputArrayOfArrays imagePoints2,
InputOutputArray cameraMatrix1,InputOutputArray distCoeffs1,
InputOutputArray cameraMatrix2, InputOutputArray distCoeffs2,
Size imageSize,OutputArray R,OutputArray T, OutputArray E,
OutputArray F,
TermCriteria criteria=TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6),
int flags=CALIB_FIX_INTRINSIC )

能够求出两个摄像头的内外参数矩阵,还能够得出两个摄像头的位置关系R,T

立体矫正

1
2
3
4
5
6
7
void stereoRectify(InputArray cameraMatrix1, InputArray distCoeffs1, 
InputArray cameraMatrix2, InputArray distCoeffs2, Size imageSize,
InputArray R, InputArray T,OutputArray R1, OutputArray R2,
OutputArray P1, OutputArray P2,OutputArray Q,
int flags=CALIB_ZERO_DISPARITY, double alpha=-1,
Size newImageSize=Size(), Rect* validPixROI1=0,
Rect* validPixROI2=0 )

计算每个摄像机(实际上)的旋转矩阵,从而使两个摄像机图像平面成为同一平面。因此,这使得所有的外极线平行,从而简化了稠密立体对应问题。

图像处理

预处理流程

常规步骤:

  • 图像颜色空间转换:RGB2HSV

    • 原因:HSV中的H表示色调,S表示饱和度,V表示亮度。不同颜色在HSV空间有严格的分量范围,从而将颜色进行量化。
    • 代码:
      1
      2
      Mat imgHSV;
      cvtColor(imgOriginal, imgHSV, COLOR_BGR2HSV);
  • 直方图均衡化(三个通道各自均衡化再组合)

    • 目的:通过拉伸像素强度分布范围来增强图像对比度,利于后面的二值化处理。
    • 效果对比:直方图均衡化效果对比
    • 代码:
      1
      2
      3
      4
      vector<Mat> hsvSplit;
      split(imgHSV, hsvSplit); //通道分离
      equalizeHist(hsvSplit[2], hsvSplit[2]); //通道各自均衡化
      merge(hsvSplit, imgHSV); //通道合并
  • 阈值处理

    • 目的:图像进行目标分割,可用于目标检测、图像增强等。
    • 常用的阈值处理办法:
      二值化阈值处理:threshold函数,大于阈值设为最大值,小于就是0。(最简单但是感觉不太好用,轮廓提取不明显。)
      自适应阈值处理:根据图像的局部特征,自动确定每个像素点的阈值,能够在不同光照条件下得到更好的效果。(但是不能筛选特定颜色的轮廓目标,可以区分背景和前景)
      双阈值化操作:将在两个阈值内的像素值设置为白色(255),而不在阈值区间内的像素值设置为黑色(0),可以用来筛选指定颜色的物体。
    • 代码:
      1
      2
      Mat imgThresholded;
      inRange(imgHSV, LOWERB, UPPERB, imgThresholded); //双阈值化操作
  • 开闭操作:

    • 目的:
      开运算:先腐蚀后膨胀,用于消除细小物体、在窄区域分离物体、平滑大物体边界。
      闭运算:先膨胀后腐蚀,用于填充物体空洞、消除噪声、连接邻近物体、平滑边界。
    • 代码:
      1
      2
      3
      4
      5
      6
      7
      //得到一个矩形卷积核
      Mat element = getStructuringElement(MORPH_RECT, Size(8, 8));
      //闭操作,填充物体空洞
      morphologyEx(imgThresholded, imgThresholded, MORPH_CLOSE, element);
      dilate(imgThresholded, imgThresholded, 300 * 300, Point(-1, -1), 1);
      //开操作,去除噪点
      morphologyEx(imgThresholded, imgThresholded, MORPH_OPEN, element);

轮廓修复

预处理的逻辑比较通用,但是在实际光线和物体呈现角度的因素影响下,预处理得到的轮廓是不太好的,比如圆可能识别出来是个月牙、形状被分割成好几个物体、正方形可能只识别了一个多边形的轮廓,并没有完整的被抠出来等等,这样的预处理效果不足以用于后续的轮廓判断,因此需要进行轮廓修复。

  • 修复效果图:轮廓修复

凸包检测+多边形填充+闭运算

1
2
3
4
5
6
7
8
9
10
11
findContours(imgThresholded, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_TC89_KCOS);
for (int i = 0; i < contour.size(); i++)
{
//凸包检测
convexHull(contour[i], hull, false, true);
//凸包填充
fillPoly(imgThresholded, hull, Scalar(255, 255, 255));
//闭操作,填充空洞
morphologyEx(imgThresholded, imgThresholded, MORPH_CLOSE, element);
dilate(imgThresholded, imgThresholded, 770 * 770, Point(-1, -1), 1);
}

轮廓判断

整体逻辑很简单,使用多边形拟合得到轮廓的边数信息,再与目标形状的边数进行比对判断是否为所求形状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vector<vector<Point>> contour;
vector<Vec4i> hierarchy;
findContours(imgPre, contour, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_TC89_KCOS);

vector<vector<Point>> conPoly(contour.size());
vector<Rect> boundRect(contour.size());

for (int i = 0; i < contour.size(); i++)
{
string objectType;//形状
int area = contourArea(contour[i]);
if (area < 6000)
continue;

float peri = arcLength(contour[i], true);
approxPolyDP(contour[i], conPoly[i], 0.02 * peri, true);//光滑曲线折线化
boundRect[i] = boundingRect(conPoly[i]);

int objCor = (int)conPoly[i].size();
if (objCor != SHAPE && SHAPE !=5) continue;//不找圆形的时候,边数非目标
if (objCor < SHAPE && SHAPE == 5)continue;//找圆形的时候,排除其余形状

cout << "FIND!!!"<<objCor<<endl;
}
  • 但是现实情况往往更复杂,以下为笔者在轮廓识别的过程中进行的优化:
    • 圆度检测
      其他图像可能在轮廓修复和开闭操作之后,棱角不明显,接近圆形,从而导致误识别,因此引入圆度检测。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      bool ifCircle(vector<Point>contour) {
      int area = contourArea(contour); //计算轮廓面积
      float len = arcLength(contour, true); //计算轮廓周长
      float roundness = (4 * CV_PI * area) / (len * len); //计算圆度
      // cout << "未确定圆的圆度:" << roundness << endl;
      if (roundness < 0.92) {
      //cout << "没过检测,圆度:" << roundness << endl;
      return false;
      }
      return true;
      }
    • 矩形检测:圆形在预处理之后,有概率被误识别为矩形;为更好的区分矩形和圆形,引入矩形检测。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      bool ifRect(vector<Point>contour) {
      float rectangularity;
      //计算最小外接矩形的面积:
      RotatedRect minrect = minAreaRect(contour); //最小外接矩形
      int area = contourArea(contour); //计算轮廓面积
      int Sminrect = minrect.size.height * minrect.size.width;
      if (Sminrect == 0) rectangularity = 0;
      else rectangularity = (float)area / Sminrect;
      // cout << "矩形度:"<<rectangularity<<"最小外接矩形面积:"<< Sminrect<<"轮廓面积:" << area << endl;
      if (rectangularity <0.86)return false;
      return true;
      }

      *************在轮廓函数中,采用矩形度和圆度双重检测,有效提高矩形的识别精确度****************

      if (objCor == 4) {
      //矩形度检测
      if (ifCircle(contour[i])|| ! ifRect(contour[i])) {
      cout << "矩形没过检测!!" << endl;
      continue;
      };

在图片上进行标记

绘制轮廓

1
2
3
4
5
6
7
8
9
10
void drawContours(
InputOutputArray image,
InputArrayOfArrays contours,
int contourIdx,
const Scalar& color,
int thickness=1,
int lineType=8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX,
Point offset = Point() );

框出目标物体

1
2
3
void cv::rectangle (InputOutputArray img, Point pt1, Point pt2, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)
***或者****
void cv::rectangle (InputOutputArray img, Rect rec, const Scalar &color, int thickness=1, int lineType=LINE_8, int shift=0)

标注点

1
2
3
//标注矩形顶点
for(int j=0;j<4;j++)
circle(img, conPoly[i][j], 3, Scalar(0, 255, 120), -1);

计算点的坐标

找圆点

  • 霍夫检测(我感觉很难调参而且精度不太行)
  • 最小二乘法拟合圆(没试过,但是据说可以,就是得自己造轮子)
  • 先识别得到一个圆度大于0.9的圆,再找最小外接矩形,矩形中点即为圆心(有点偷奸耍滑,但是几何上说得通嘿嘿)
    1
    2
    3
    4
    5
    6
    7
    void CircleCenter(Mat img, Rect rect) {
    int x = rect.width / 2;
    int y = rect.height / 2;
    Point Radius = Point(rect.x + x, rect.y + y);
    circle(img, Radius, 3, Scalar(0, 255, 120), -1);
    return;
    }

找矩形的四个顶点

只要通过矩形检测,四边形就是矩形啦。多边形拟合之后的那个conpoly就是矩形的角点点集。


openCV学习指南
http://zoechen04616.github.io/2023/07/26/openCV学习指南/
作者
Yunru Chen
发布于
2023年7月26日
许可协议