opencv
opencv
2023/6/1
➡️

图像的表示

但是计算机看来,图像只是一堆亮度各异的点。一副尺寸为 M × N 的图像可以用一个 M × N 的矩阵来 表示,如果是灰度图,每个像素点用一个数值表示亮度,如果是彩色图像每个像素点是 3 个数值一组进 行表示

一般来说,灰度图用 2 维矩阵表示,彩色(多通道)图像用 3 维矩阵(M × N × 3)表示。对于图像 显示来说,目前大部分设备都是用无符号 8 位整数(类型为 CV_8U)表示像素亮度。

Mat

Mat 是 Opencv 中的通用矩阵类型,我们通常将它作为图片的容器,它包含了矩阵头(包含矩阵尺寸, 储存方法,储存地址等信息)和指向储存所有点值的指针

 //创建一个 高度 300 像素,宽度 300 像素 , CV_8UC3 表示 8位无符号整数,表示位深,C3 表示三通道的
 // 8位无符号整数 范围是 0到255 ,通道是3,表示是 BGR, 下面的代码表示生成红色的图像
  cv::Mat mat=  cv::Mat(300, 300, CV_8UC3, cv::Scalar(0,0,255));

小矩阵直接赋值

当所需矩阵较小时,可以通过<<运算符对其直接初始化赋值,使用方法如下

cv::Mat mat= (cv::Mat_<double>(3,3) << 0,1,2,3,4,5,6,7,8);

通过已有矩阵赋值

通过 clone()函数对已有的矩阵或者数组进行深复制(不是新建信息头),将其赋值给待初始化的数组, 抽取已有图像的行或列的部分像素进行 clone

cv::Mat mat = cv::imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
auto newMat=  mat.colRange(0,100).clone();
cv::imshow("Image", newMat);

操作像素

mat 的分为固定的头部加可变的数据区域构成

uchar table[256];
for (int i = 0; i < 256; ++i) {
  //生成随机的颜色
  table[i]=(uchar)30*(i/16);
}

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
//通道数
int cn=mat.channels();
//深度
int depth= mat.depth();
//数据部分的指针
auto dataPtr= mat.data;

int height= mat.rows;
int width= mat.cols;

for (int i = 0; i < height; ++i) {
  //宽度方向需要考虑 cn
  for (int j = 0; j <width*cn ; ++j) {
      *dataPtr=table[*dataPtr];
      dataPtr++;
  }
}
std::cout<<"depth:"<< depth<< std::endl;
std::cout<<"cn:"<< cn<< std::endl;

imshow("Image", mat);

遍历像素

cv::Mat mat = cv::imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
for (int i=0;i<mat.rows;i++){
  for (int j=0;j<mat.cols/2;j++){
    cv::Vec3b& pixel =  mat.at<cv::Vec3b>(i, j);
    pixel[0] = 0;
    pixel[1] = 0;
    pixel[2] = 255;
  }
}

迭代器筛选

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
for (auto b=mat.begin<Vec3b>(); b!=mat.end<Vec3b>(); b++){
  (*b)[0] = rand()%255;
  (*b)[1] = rand()%255;
  (*b)[2] = rand()%255;
}

指针访问,性能好

cv::Mat mat = cv::imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");

for (int i=0;i<mat.rows;i++){
  //获取行指针,遍历指针
  //获取第i行首像素指针
  cv::Vec3b* it = mat.ptr<cv::Vec3b>(i);
  for (int j=0;j<mat.cols/2;j++){
    cv::Vec3b& pixel= it[j];
    pixel[0]=0;
    pixel[1]=0;
    pixel[2]=125;
  }
}

操作局部

Rect

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
//获取左上角,指定坐标,及宽度和高度的区域图像
Mat mat1=Mat(mat,Rect(0,0,100,100));

Range

Range::all() 表示所有行或列

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
//第一个  Range 指定行 第二个 Range 指定列
Mat mat1=mat(Range(0,100),Range(0,100));

像素运算

  1. 加、减、乘、除 OpenCV 在图像处理方面提供了很多基础的像素操作,其中一类操作主要是对像素 实现加、减、乘、除的算术运算。这种运算在两张图像之间完成,用于实现图像上每个像素的加、 减、乘、除操作,所以要求进行算术运算的两张图像的大小必须保持一致。
  2. 取反、位与、位或、位异或

调整图像亮度 加、减

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat const_img=Mat::zeros(mat.size(),mat.type());
const_img.setTo(Scalar(50,50,50));
Mat lightMat,darkMat;
//亮度加强
add(mat,const_img,lightMat);
//亮度减弱
subtract(mat,const_img,darkMat);
imshow("Image", mat);
imshow("lightMat", lightMat);
imshow("darkMat", darkMat);

调整图像对比度 乘、除

图像对比度主要用于描述图像亮度之间的感知差异,对比度越大,图像的每个像素与周围的差异性就越 大,整个图像的细节就越显著,反之亦然。图像乘法或者图像除法操作可分别用于扩大或者缩小图像像 素之间的差值,从而达到调整图像对比度的目的。

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat const_img=Mat::zeros(mat.size(),CV_32FC3);
const_img.setTo(Scalar(0.8,0.8,0.8));
Mat lowContrasMat,highContrasMat;
multiply(mat,const_img,lowContrasMat,1,CV_8UC3);
divide(mat,const_img,highContrasMat,1,CV_8UC3);
imshow("Image", mat);
imshow("lowContrasMat", lowContrasMat);
imshow("highContrasMat", highContrasMat);

addWeighted 调整对比度和亮度

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dst;
Mat light=Mat::zeros(mat.size(),mat.type());
light=Scalar(127,127,127);
// 第二个参数用来调整对比度,第三个参数 light,用来调整亮度,第四个参数用来给亮度加系数,第五个参数是伽马值
addWeighted(mat, 1.2, light, 0.3, 0.6, dst);
imshow("dst", dst);

截取图像指定的部分 位与

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");

//通过掩码获取指定的部分,cv::bitwise_and 要求 mask  是 ,CV_8UC1 类型的
//要和原图大小一致
Mat mask = Mat::zeros(mat.size(),CV_8UC1);
for (int i=0;i<mat.rows/2;i++){
  for (int j=0;j<mat.cols/2;j++){
    //需要获取的部分指定为 255
    mask.at<uchar>(i, j)=255;
  }
}

Mat dst;
cv::bitwise_and(mat,mat,dst,mask);
imshow("dst1", dst);

图像类型和通道

正如前文所述,Mat 的数据类型有很多种,而且各种 Mat 数据类型可以根据需要进行类型转换。 imread 默认加载的数据类型是 CV_8UC3 字节型数据,显示的时候使用 imshow 函数即可;但当其类型 转换为 CV_32FC3 浮点型数据时,直接通过 imshow 显示的图像将呈白色,无法正常显示。这是因为 imshow 对浮点型数据图像显示支持的取值范围为[0, 1],所以需要对转换之后的图像先除以 255,把 取值范围从 0 ~ 255 转换到 0 ~ 1,这样图像就能正常显示出来了

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat f;
mat.convertTo(f,CV_32FC3);
//操作符重载
f=f/255.0;
imshow("f", f);

通道操作

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat gray;
//cvtColor 通道转换 cv::COLOR_BGR2GRAY  表示三通道转换为单通道(彩色图像转换为灰度图像)
cvtColor(mat, gray, cv::COLOR_BGR2GRAY);

通道和分离和合并

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
std::vector<cv::Mat> channels;
cv::split(mat, channels);

//通道分离后,每个通道都是灰度图了
cv::imshow("Blue Channel", channels[0]);
cv::imshow("Green Channel", channels[1]);
cv::imshow("Red Channel", channels[2]);
Mat newMat;
merge(channels, newMat);
imshow("Image", newMat);

通道交换

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat newMat=Mat(mat.size(),CV_8UC3);

// {0, 2, 1, 1, 2, 0}  src 0 到 target 2 ,以此类推
mixChannels(mat, newMat, {0, 2, 1, 1, 2, 0});

imshow("lena", newMat);

色彩空间

常见的图像色彩空间主要有 RGB 色彩空间、HSV 色彩空间、LAB 色彩空间等。这些色彩空间之间是可 以相互转换的,在不同的色彩空间下,图像会以不一样的形态呈现出来

图中的黑色三角形区域称为 sRGB 色彩空间,曾广泛应用于个人计算机显示器、打印机、数码相机。到 了 20 世纪 90 年代, Adobe 公司提出了一个新的 RGB 色彩空间模型—Adobe RGB 色彩空间。它比 sRGB 色彩空间的取值范围更大,因此色彩也更加细腻、丰富。再后来陆续出现了 RGB 色彩空间的各种 变种,但是基本的 RGB 三色表达本质上并没有发生改变。至此, RGB 色彩空间已成为应用最广泛的色 彩空间。OpenCV 通过默认加载 imread 函数,使图像都是 RGB 色彩空间, 3 个通道的顺序依次为蓝 色、绿色、红色(即通道顺序为 BGR)

在 OpenCV 中进行色彩空间的转换可以使用 cvtColor 函数。该函数可实现 RGB 色彩空间、HSV 色彩 空间和 LAB 色彩空间的相互转换。

  1. hsv 虽然 RGB 色彩空间的色彩比较丰富,但是它也有缺点。它最大的缺点就是无法直观地区分图像 的颜色、亮度和饱和度等值。我们需要采用更加直观的图像色彩空间,排在第一位的当属 HSV 色彩 空间。它足够直观,也很容易理解,在图像处理中非常有用。 HSV 色彩空间在 OpenCV 中的取值范 围具体如下:H 为 0 ~ 180、S 为 0 ~ 255、V 为 0 ~ 255

    • H(Hue)表示颜色通道,不同的值表示不同的颜色范围
    • S 表示饱和度通道,即色泽
    • V 表示亮度通道,代表了图像亮度高低的级别。
  2. 在 LAB 色彩空间中,3 个通道的解释如下。

    • L:表示亮度信息。
    • A:表示色度信息中的红色/绿色值。
    • B:表示色度信息中的蓝色/黄色值。
Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat  hsv;
// 颜色空间转转后,颜色呈现会完全不一样
cvtColor(mat, hsv, COLOR_BGR2HSV);
imshow("hsv", hsv);

inRange

常用于根据特定颜色取值范围生成对象掩膜(mask),然后结合其他操作完成对特定对象的提取

Mat red=Mat(300,300,CV_8UC3,Scalar(0,0,255));
Mat Green=Mat(300,300,CV_8UC3,Scalar(0,255,0));
Mat mat;
//图像左右拼接
hconcat(red,Green,mat);
Mat mask;
// 低于 lowerb 的值被设为 0,高于 upperb 的值被设为 0, 其他值被设为 255
//过滤出红色部分的掩码,被保留的部分为白色,其他部分为黑色
inRange(mat, Scalar(0, 0, 0), Scalar(0, 0, 255), mask);
Mat dst;
//过滤出红色部分,余下部分显示黑色
bitwise_and(mat, mat,dst,mask);

imshow("mask", dst);

图像直方图

RGB 色彩空间的图像 3 个通道的取值范围均为[0,255]。针对它们的取值范围计算出取值范围内每个数 值出现的次数,从而得到像素值的最终分布统计信息。该信息既是图像的基本特征之一,同时也是图像 数据的统计学特征,又称为图像直方图

RGB 色彩空间的图像 3 个通道的取值范围均为[0,255]。针对它们的取值范围计算出取值范围内每个数 值出现的次数,从而得到像素值的最终分布统计信息。该信息既是图像的基本特征之一,同时也是图像 数据的统计学特征,又称为图像直方图

均值和方差

方差 = sum((x - mean)^2) / n 在统计学和概率论中,方差是衡量随机变量或一组数据离散程度的一 个指标。方差表示数据集中各个数据点与数据集均值之间的差异程度。具体来说,方差越大,数据点相 对于均值的分散程度就越大;方差越小,数据点相对于均值的分散程度就越小。

Mat red=Mat(300,300,CV_8UC3,Scalar(0,0,255));
Mat Green=Mat(300,300,CV_8UC3,Scalar(0,255,0));
Mat mat;
//图像左右拼接
hconcat(red,Green,mat);
// BGR 颜色分布的均值 [0, 127.5, 127.5, 0]
Scalar s_mean=mean(mat);
cout<<"mean:"<<s_mean<<endl;

// BGR 颜色分布的标准差 [0, 127.5, 127.5, 0]
// G 和 R 通道的颜色刚好取值是在 0 和 255 俩个极限,故方差会很大
// Mat stddev 可以明确图像的通道
Scalar stddev;
meanStdDev(mat,s_mean,stddev);
cout<<"meanStdDev:"<<stddev<<endl;
 Mat red=Mat(300,300,CV_8UC3,Scalar(0,100,255));
Mat Green=Mat(300,300,CV_8UC3,Scalar(0,255,0));
Mat mat;
//图像左右拼接
hconcat(red,Green,mat);

Mat mean,stddev;
meanStdDev(mat,mean,stddev);

/*
  mean B:0
  mean G:177.5
  mean R:127.5

  stddev B:0
  stddev G:77.5
  stddev R:127.5
  * */
cout<<"mean B:"<< mean.at<double>(0)  <<endl;
cout<<"mean G:"<< mean.at<double>(1)  <<endl;
cout<<"mean R:"<< mean.at<double>(2)  <<endl;

cout<<"stddev B:"<< stddev.at<double>(0)  <<endl;
cout<<"stddev G:"<< stddev.at<double>(1)  <<endl;
cout<<"stddev R:"<< stddev.at<double>(2)  <<endl;

统计像素出现的次数

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg",IMREAD_GRAYSCALE);
vector<int> his(256);
for(int i=0;i<256;i++)his[i]=0;

for (int i = 0; i <mat.rows ; ++i) {
  for (int j =0; j <mat.cols ; ++j) {
    his[mat.at<uchar>(i,j)]++;
  }
}
for (int i = 0; i <his.size() ; ++i) {
  cout << "his-"  << i <<":" <<his[i]  <<endl;
}

直方图计算

OpenCV 中计算直方图、输出直方图统计信息的相关函数 calcHist 定义如下:

直方图的 x 轴是灰度值(0~255),y 轴是图片中具有同一个灰度值的点的数目

histImage

CV_EXPORTS 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 );

参数解释如下。

  1. images:输入图像,一张或者多张,通道与类型一致。
  2. nimages:输入图像的数目。
  3. channels:不同图像的通道索引,编号从 0 开始。
  4. mask:可选参数。
  5. hist:输出直方图。
  6. dims:必须是正整数,值不能大于 CV_MAX_DIMS(当前版本是 32)。
  7. histSize:直方图大小,可以理解为 X 轴上的直方图的取值范围,假设取值范围为 0 ~ 31,那么 这 32 个级别会将灰度的 256 个(0 ~ 255)灰度级别分为 32 等份,又称为 32 个 bin。
  8. ranges:表示通道的取值范围,RGB 的取值范围为 0 ~ 256,HSV 中 H 的取值范围为 0 ~ 180。
  9. uniform:表示一致性,对边界数据的处理方式,取值为 false 的时候表示不处理。
  10. accumulate:表示对图像计算累积直方图。
int main() {
  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
 showHistogram(&showHistogram,"mat")
}

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");

//直方图绘制
void showHistogram (const Mat &mat,string name) {
  vector<Mat> mv;
  split(mat, mv);
  Mat b_hist, g_hist,r_hist;
  int bins=256;// 0-255 分组
  int histSize[]= {bins};
  float rgb_range[]={0,255};
  const float* ranges[]= {rgb_range};
  int channels[]={0};

  calcHist(&mv[0], 1, channels, Mat(), b_hist, 1, histSize, ranges, true, false);
  calcHist(&mv[1], 1, channels, Mat(), g_hist, 1, histSize, ranges, true, false);
  calcHist(&mv[2], 1, channels, Mat(), r_hist, 1, histSize, ranges, true, false);


  Mat histImage(600, 900, CV_8UC3, Scalar(0, 0, 0));
  int padding=50;
  //上下左右留的边距
  int hist_w=histImage.cols-2*padding;
  int hist_h=histImage.rows-2*padding;
  int bin_w=cvRound((double)hist_w/bins);

  //归一化 为了在 histImage 幅面进行绘制需要归一化
  //因为 histImage 在高度方向是 900 ,所以要把 b_hist 的像素点出现的次数归一化到 0到900 之间
  normalize(b_hist,b_hist,0,hist_h,NORM_MINMAX,-1,Mat());
  normalize(g_hist,g_hist,0,hist_h,NORM_MINMAX,-1,Mat());
  normalize(r_hist,r_hist,0,hist_h,NORM_MINMAX,-1,Mat());

  for (int i = 0; i <bins ; ++i) {

    int x0= bin_w * (i)+padding;
    int x1= bin_w * (i+1)+padding;

    //hist_h-cvRound(b_hist.at<float>(i))+padding)  因为图像坐标原点在左上角
    // b_hist.at<float>(i) 因为计算直方图的时候使用 bins 作为x 轴上的划分
    line(histImage, Point(x0, hist_h-cvRound(b_hist.at<float>(i))+padding),
                         Point(x1, hist_h - cvRound(b_hist.at<float>(i+1))+padding),
         Scalar(255, 0, 0), 2, 8, 0);

    line(histImage, Point(x0, hist_h-cvRound(g_hist.at<float>(i))+padding),
         Point(x1, hist_h - cvRound(g_hist.at<float>(i+1))+padding),
         Scalar(0, 255, 0), 2, 8, 0);

    line(histImage, Point( x0, hist_h-cvRound(r_hist.at<float>(i))+padding),
         Point(x1, hist_h - cvRound(r_hist.at<float>(i+1))+padding),
         Scalar(0, 0, 255), 2, 8, 0);

  }

  imshow(name,histImage);
}

lena直方图

直方图均衡化

我们可以根据图像的像素分布生成直方图数据,假设我们对直方图数据进行相关的处理,改变像素数据 的分布,再重新映射到原图,这样就改变了原图的像素分布与显示。因此,通过调整图像直方图数据, 从而修改图像实现对比度调整,该操作称为直方图均衡化。

直方图均衡化,显而易见的能让图像的整体像素分布更均匀.对比度更好

直方图均衡化的函数定义如下:equalizeHist(src,dst)其中,src 表示输入参数,必须是 CV_8U 数 据类型的单通道图像;dst 表示输出图像,数据类型必须与输入图像的类型保持一致。相关的代码演示

直方图均衡化原理图

彩色图片直方图均衡化

通过 split 函数将彩色图像转换为单通道图像数组,然后分别调用直方图均衡化方法进行均衡化操作 ,最后通过 merge 方法合并输出,缺点是直方图均衡化改变的是对比度,而不是颜色,但是 RGB 色彩空 间把颜色和亮度进行混合表示,直接在此颜色空间进行直方图均衡化,会改变颜色

int main() {
  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  vector<Mat> channels;
  split(mat, channels);

  //equalizeHist 只能对单通道的图
  vector<Mat> dstChannels(3);
  equalizeHist(channels[0],dstChannels[0]);
  equalizeHist(channels[1],dstChannels[1]);
  equalizeHist(channels[2],dstChannels[2]);

  Mat dst;
  merge(dstChannels,dst);

  imshow("equalizeHistSrc",mat);
  imshow("equalizeHistDst",dst);

  showHistogram(mat, "src");
  showHistogram(dst, "dst");

  cv::waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

直方图均衡化

如下图右侧明显提高了对必度,但是也改变了颜色'

直方图均衡化效果

hsv 色彩空间直方图均衡化

int main() {
  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  Mat hsv;
  //转换为HSV 色彩空间
  cvtColor(mat, hsv, COLOR_BGR2HSV);

  vector<Mat> channels;
  split(hsv, channels);

  equalizeHist(channels[2],channels[2]);
  Mat dst;
  merge(channels,dst);

  //转换为BGR 色彩空间
  cvtColor(dst, dst, COLOR_HSV2BGR);

  imshow("equalizeHistSrc",mat);
  imshow("equalizeHistDst",dst);

  showHistogram(mat, "src");
  showHistogram(dst, "dst");

  cv::waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

hsv色彩空间直方图均衡化

图像颜色替换

快速的对颜色进行替换

int main() {
  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  Mat dst1, dst2;
  //只要是包含256个像素值就可以,无论是一行还是一列,只要能提供256个像素点分别进行替换的值就可以
  Mat lut=Mat::zeros(256,1,CV_8UC3);
  for (int i=0 ;i<256 ;i++) {
    cv::Vec3b& pixel = lut.at<Vec3b>(i,0);
    pixel[0]=i;
    pixel[1]=0;
    pixel[2]=i;
  }

  LUT(mat, lut, dst1);

  // 12 中 ColorMap进行颜色替换
  applyColorMap(mat,dst2,COLORWAY_COOL);

  imshow("LUT",dst1);
  imshow("applyColorMap",dst2);
  cv::waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

图像卷积

卷积,有时也叫算子。用一个模板去和另一个图片对比,进行卷积运算。目的是使目标与目标之间的差 距变得更大。卷积在数字图像处理中最常见的应用为锐化和边缘提取和模糊。

大概原理

定义如下的卷

111
111
111

待处理的矩阵

1234
5678
9101112
13141516

扩展矩阵 x,为了是经过卷积计算后的得到的矩阵和原来的矩阵的长度一致

将卷积核 h 的中心对准 x 的第一个元素,然后 h 和 x 重叠的元素相乘,h 中不与 x 重叠的地方 x 用 0 代替(是为了产生,同样大小的卷积和的特征图),再将相乘后 h 对应的元素相加,得到结果矩 阵中 Y 的第一个元素。如图灰色背景是扩展的常数 0

1*0 1*0 1*0 0 0 0
1*0 1*1 1*2 3 4 0
1*0 1*5 1*6 7 8 0
0 9 10 11 12 0
0 13 14 15 16 0
0 0 0 0 0 0

所以结果矩阵中的第一个元素,中心的元素也要加 Y11 = 1 * 0 + 1 * 0 + 1 * 0 + 1 * 0 + 1 * 1 + 1 * 2 + 1 * 0 + 1 *5 + 1 * 6 = 14

14234
5678
9101112
13141516

依次类推,x 中的每一个元素都用这样的方法来计算,得到的卷积结果矩阵为下图很明显经过卷积计算 后像素值的数值差异变得很大了

14243022
33546345
57579969
46727854

3*3 的卷积核填充 1 的边界,5*5 填充 3 个边界,依次类推,最好是奇书,这样好找锚点,

在 OpenCV 中,Point(-1, -1)通常被用作卷积核的中心点位置的默认值。在计算卷积操作时,通常是 根据卷积核的大小来确定中心点的位置假设有一个大小为 (m, n) 的卷积核,其中 m 和 n 分别表示核 的行数和列数。中心点的坐标可以计算为 (m // 2, n // 2),其中 // 表示整除运算符。

例如,对于一个大小为 3x3 的卷积核,其中心点的坐标可以计算为 (3 // 2, 3 // 2),即 (1, 1)。 对于一个大小为 5x5 的卷积核,其中心点的坐标可以计算为 (5 // 2, 5 // 2),即 (2, 2)。

因此,在 OpenCV 中,当卷积核的中心点位置被指定为 Point(-1, -1)时,实际上是使用了卷积核大小 来自动计算中心点的位置。这种设计使得代码更加简洁和易读,无需手动计算中心点的位置。

borderType 边框填充

BORDER_DEFAULT 是最好的方式可以让最新相机近的像素挨在一起,拓宽图片的边缘像素,图像又不会失 去

enum BorderTypes {
    BORDER_CONSTANT    = 0, //!< `iiiiii|abcdefgh|iiiiiii`  with some specified `i`
    BORDER_REPLICATE   = 1, //!< `aaaaaa|abcdefgh|hhhhhhh`
    BORDER_REFLECT     = 2, //!< `fedcba|abcdefgh|hgfedcb`
    BORDER_WRAP        = 3, //!< `cdefgh|abcdefgh|abcdefg`
    BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba`
    BORDER_TRANSPARENT = 5, //!< `uvwxyz|abcdefgh|ijklmno`

    BORDER_REFLECT101  = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
    BORDER_DEFAULT     = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
    BORDER_ISOLATED    = 16 //!< do not look outside of ROI
};
Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dst,dst1,dst2,dst3;
int border=18;
copyMakeBorder(mat, dst, border, border, border, border, BORDER_CONSTANT, Scalar(0, 0, 255));
copyMakeBorder(mat, dst1, border, border, border, border, BORDER_DEFAULT);
copyMakeBorder(mat, dst2, border, border, border, border, BORDER_REPLICATE);
copyMakeBorder(mat, dst3, border, border, border, border, BORDER_WRAP);

imshow("dst", dst);

imshow("dst1", dst1)
imshow("dst2", dst2);
imshow("dst3", dst3);

blur 均值模糊

blur 给默认给出卷积核心数字是 1,blur 处理一副图片后,图片会模糊

均值模糊是卷积核系数相同的一种图像卷积计算方式,卷积核越大,模糊程度越厉害。 OpenCV 中的均 值模糊函数为 blur

  1. src:表示输入图像,支持任意通道数目。
  2. dst:表示输出图像,类型与输入图像类型相同
  3. size:表示卷积核大小,卷积核系数为,ksize 的值越大,表示均值模糊的程度越高。当相邻像素之 间的差异较大时,图像通常会看起来更模糊。这是因为较大的像素值差异可能导致边缘变得模糊或 失真,从而影响图像的清晰度。相反,当相邻像素之间的差异较小时,图像通常会更清晰。
  4. anchor:表示锚定,默认情况下,OpenCV 卷积计算输出值为中心像素点,但是可以通过 anchor 参 数来修改默认位置。默认的 anchor 参数值为 Point(-1,-1),表示中心位置。
  5. borderType:表示对边缘的处理方式,卷积在进行图像处理时,因为卷积窗口无法移动最边缘的像 素,实现对边缘像素的卷积计算,所以通常需要借助一些边缘填充方法来实现对边缘的填充。
// 定义待处理的矩阵x
cv::Mat matrix_x = (cv::Mat_<uchar>(4, 4) << 1, 2, 3, 4,
    5, 6, 7, 8,
    9, 10, 11, 12,
    13, 14, 15, 16);
cv::Mat result;
// BORDER_CONSTANT 常数 0
cv::blur(matrix_x, result, cv::Size(3, 3),Point(-1,-1),BORDER_CONSTANT);
std::cout << "卷积结果:" << std::endl;
std::cout << result << std::endl;

//卷积结果:
[  2,   3,   3,   2;
  4,   6,   7,   5;
  6,  10,  11,   8;
  5,   8,   9,   6]

高斯模糊

均值模糊在进行图像模糊的时候没有考虑中心像素与周围像素的空间位置关系,采用均值系数的卷积核输出, 输出位置的像素点没有被权重提升。而高斯模糊考虑了输出位置像素点与周围像素点的空间位置关系,空间位置不同, 使用的卷积核系数也不同,卷积核系数是通过一个2D的高斯函数生成的,因此这种卷积模糊方式也称为高斯模

f(x,y)=12πσxσyexp(12((xμx)2σx2+(yμy)2σy2))f(x, y) = \frac{1}{2\pi\sigma_x\sigma_y} \exp\left(-\frac{1}{2}\left(\frac{(x-\mu_x)^2}{\sigma_x^2} + \frac{(y-\mu_y)^2}{\sigma_y^2}\right)\right)

其中,x、y的取值范围决定了卷积核的大小,σ表示方差取值范围(为正整数),通常把 $\frac{1}{2\pi\sigma_x\sigma_y}$ 称为归一化因子。 实际的程序实现通常会根据x、y的值计算生成σ参数,或者根据σ参数计算得到x、y的值。OpenCV中的高斯模糊函数如下

其中,src、dst、ksize、borderType参数与blur函数类似。所以这里重点解释一下sigmaX与sigmaY参数。当ksize的值不为0的时候, 表示根据ksize参数的值计算sigmaX参数的值;当ksize设置为Size(0,0)时,表示根据sigmaX的值计算ksize的值。它们之间的计算公式如下: σ=0.3×((Size-1)×0.5-1)+0.8 ,sigmaY的值默认为0,表示与sigmaX的值保持相同

CV_EXPORTS_W void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
                                double sigmaX, double sigmaY = 0,
                                int borderType = BORDER_DEFAULT );

注意:在高斯模糊中,ksize必须设置为奇数,因为偶数无法完成中心化对称,如果设置为偶数,OpenCV就会出现运行错误。 同时高斯模糊的抛锚点必须在中心位置

3*3 的高斯核

121
242
121

在上图中看到的,高斯核中心数值高边缘低,符合正态分布

filter2D 自定义滤波

// 定义卷积核h
cv::Mat kernel_h = (cv::Mat_<float>(3, 3) << 1, 1, 1,
      1, 1, 1,
      1, 1, 1);

// 定义待处理的矩阵
cv::Mat matrix_x = (cv::Mat_<uchar>(4, 4) << 1, 2, 3, 4,
      5, 6, 7, 8,
      9, 10, 11, 12,
      13, 14, 15, 16);

cv::Mat result;
// 约定的卷积核心的中心位置为 Point(-1, -1),
// 参数 0, BORDER_CONSTANT   边界扩充方式用常数 0 ,-1 参数表示输入输出图像深度一致
cv::filter2D(matrix_x, result, -1,
               kernel_h, Point(-1, -1), 0, BORDER_CONSTANT);

std::cout << "卷积结果:" << std::endl;
std::cout << result << std::endl;

//计算结果是:
[ 14,  24,  30,  22;
  33,  54,  63,  45;
  57,  90,  99,  69;
  46,  72,  78,  54]

为了缩小计算后的值,可以归一化

kernel_h /= 9; // 将卷积核进行归一化

//卷积结果:
[  2,   3,   3,   2;
   4,   6,   7,   5;
   6,  10,  11,   8;
   5,   8,   9,   6]

自定义均值滤波

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");

//因为  15,15 之后的数字很大 进行 kernel_h/(15*15) 归一化后又是小数,故这里的数据类型是 CV_32FC1
cv::Mat kernel_h =Mat::ones(15,15,CV_32FC1);
kernel_h= kernel_h/(15*15);
cv::Mat result;
// 约定的卷积核心的中心位置为 Point(-1, -1),
// 参数 0, BORDER_CONSTANT   边界扩充方式用常数 0
cv::filter2D(mat, result, -1,
            kernel_h, Point(-1, -1), 0, BORDER_CONSTANT);

imshow("result",result);

自定义的非均值滤波

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat kernel_h= (Mat_<int>(2,2) << 1,0,0,-1);
Mat dst;

filter2D(mat,dst,CV_32F,kernel_h,Point(-1,-1),0,BORDER_DEFAULT);
// kernel_h  进行过卷积后,像素值反差太大, 像素范围在 -255 到 255 之间, 显示图片后效果不好
imshow("dst",dst);
// convertScaleAbs 这个 api ,可以把像素缩放到 0-255 之间 
convertScaleAbs(dst,dst);
imshow("convertScaleAbs",dst);

自定义非均值滤波

水平和垂直模糊

  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  cv::Mat kernel_h =Mat::ones(1,20,CV_32FC1);
  kernel_h= kernel_h/(1*20);
  cv::Mat result;
// 约定的卷积核心的中心位置为 Point(-1, -1),
// 参数 0, BORDER_CONSTANT   边界扩充方式用常数 0
  cv::filter2D(mat, result, -1,
               kernel_h, Point(-1, -1), 0, BORDER_CONSTANT);

  imshow("result",result);
  Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  cv::Mat kernel_h =Mat::ones(20,1,CV_32FC1);
  kernel_h= kernel_h/(20*1);
  cv::Mat result;
// 约定的卷积核心的中心位置为 Point(-1, -1),
// 参数 0, BORDER_CONSTANT   边界扩充方式用常数 0
  cv::filter2D(mat, result, -1,
               kernel_h, Point(-1, -1), 0, BORDER_CONSTANT);

  imshow("result",result);

图片边缘发黑是因为 0, BORDER_CONSTANT 参数导致

水平垂直模糊

对角线模糊

 Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  cv::Mat kernel_h =Mat::eye(20,20,CV_32FC1);
  cout<<kernel_h<<endl;
  // 这里  kernel_h/(1*20) 是因为只要20个1 ,Mat::eye 只在左右对象线出现 1
  kernel_h= kernel_h/(1*20);
  cout<<kernel_h<<endl;
  
  cv::Mat result;
// 约定的卷积核心的中心位置为 Point(-1, -1),
// 参数 0, 表示图像增强 BORDER_DEFAULT 
  cv::filter2D(mat, result, -1,
               kernel_h, Point(-1, -1), 0, BORDER_DEFAULT);
               
imshow("result",result);

对角线模糊

梯度提取

图像梯度本质上就是图像中不同像素值之间的差异,即差异越大,梯度越大.像素值依次增大,所以它在X轴方向(水平方向)有梯度值且不为0, 而对于从上到下的垂直方向来说,每一列的像素值均保持不变,因此它的梯度值为0. 简单的来说,梯度就是求导数,找出图像像素急剧变化的部分,用以绘制轮廓,或显示边缘特征. opencv中有三种不同滤波器,或者说成高通滤波器。分别是Sobel,Scharr 和 LaplacianSobel,

灰度梯度

如下图所示,左图红色圈的地方头发到脸的过度地方,像素值有接近(黑色),急剧变化到 接近 (白色),这就是明显的梯度变化,也就是导数最大的地方 ,中间图的红圈部分表达的是头发到脸部过度过程中像素的变化过程,有黑色0 到白色 255 的变化, 右边图表达的是梯度变化的过程,其中最大变化点在图一红圈

卷积一阶导数滤波

如下卷积核

a0a1a2
a7[i,j]a3
a6a5a4

[i,j]表示当前像素点的坐标,周围是它的8个相邻像素点。X轴方向与Y轴方向的梯度计算公式如下:

Mx =(a2+ca3+a4)-(a0+ca7+a6)

My =(a6+ca5+a4)-(a0+ca1+a2)

让左右 或上下的像素值形成大的差异,以进行梯度的提取

当系数c=1时,卷积核称为Prewitt算子:

Mx=[101101101]My=[111000111]Mx = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{bmatrix} My = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \\ \end{bmatrix}

Prewitt 算子由美国计算机科学家 J. Prewitt 在 1970 年代初提出,用于计算图像中每个像素点的梯度,从而实现边缘检测 OpenCV中没有Prewitt算子的函数,但是我们可以通过filter2D函数实现自定义的卷积核Prewitt滤波。

当系数c=2时,卷积核称为Sobel算子:

Mx=[101202101]My=[121000121]Mx = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix} My = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix}

Sobel 算子由美国计算机科学家 I.R. Sobel 在 1970 年提出 Sobel 算子中的权重值更加关注核心像素的周围像素, 因此 Sobel 算子对噪声具有较强的抵抗能力,可以产生较为平滑的边缘检测结果。

OpenCV中的梯度计算函数除了Sobel之外,还有一个基于Sobel的增强卷积核,称为Scharr算子:

Mx=[30310010303]My=[31030003103]Mx = \begin{bmatrix} -3 & 0 & 3 \\ -10 & 0 & 10 \\ -3 & 0 & 3 \\ \end{bmatrix} My = \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ 3 & 10 & 3 \\ \end{bmatrix}

Scharr 算子是由德国科学家 Reinhard Scharr 在 2000 年提出的。Scharr 算子是对 Sobel 算子的改进,旨在提高边缘检测的准确性和稳定性 与 Sobel 算子和 Prewitt 算子相比,Scharr 算子的权重值更加平衡,这使得它在边缘检测中表现更加稳定,尤其是在处理噪声较多的图像时

Sobel算子

OpenCV中的Sobel算子函数与参数解释如下:

CV_EXPORTS_W void Sobel( InputArray src, OutputArray dst, int ddepth,
                         int dx, int dy, int ksize = 3,
                         double scale = 1, double delta = 0,
                         int borderType = BORDER_DEFAULT );

其中,src与dst分别表示输入图像与输出图像。ddepth表示输出图像dst的深度类型。ddepth=-1时表示输出图像与 输入图像的类型保持一致。 dx表示计算X轴(水平)方向的梯度(x方向的求导阶数)。dy表示计算Y轴(垂直)方向的梯度(y 方向的求导阶数)。ksize的取值必须 是1、3、5、7,以此类推。scale表示对求取的一阶导数值是否进行放缩, 默认值为1。delta表示是否在求得一阶导数的基础上加 上常量值delta,默认值为0。 对于输入的一张图像,使用Sobel算子分别求取该图像在X轴方向与Y轴方向的梯度代码实现如下:

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dstX , dstY ,dst;
//Sobel卷积计算的结果值已经超过了输入图像的CV_8U字节的取值范围,所以这里输出图像使用了CV_32F的数据类型
Sobel(mat, dstX, CV_32F, 1, 0);
Sobel(mat, dstY, CV_32F, 0, 1);
//lpha:归一化的最小值  ,beta:归一化的最大值。 
//归一化到 0到1  是因为Sobel 处理后 像素的数据范围差异很大,统一到 0 到 1 ,方便查看轮廓
normalize(dstX, dstX, 0, 1, NORM_MINMAX);
normalize(dstY, dstY, 0, 1, NORM_MINMAX);

// 自定义滤波实现 Sobel算子
filter2D(mat, dst, CV_32F, Matx33f(-1, 0, 1,-2,0,2,-1,0,1));
normalize(dst, dst, 0, 1, NORM_MINMAX);

imshow("mat", mat);
imshow("dstX", dstX);
imshow("dstY", dstY);
imshow("dst", dst);

Sobel提取轮廓

Scharr算子

Scharr函数的参数意义与解释与Sobel函数类似,使用Scharr算子实现对图像梯度提取的效果要比Sobel更强,可以获得更明显的梯度信息

CV_EXPORTS_W void Scharr( InputArray src, OutputArray dst, int ddepth,
                          int dx, int dy, double scale = 1, double delta = 0,
                          int borderType = BORDER_DEFAULT );
Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dstX , dstY ,dst;
Scharr(mat, dstX, CV_32F, 1, 0);
Scharr(mat, dstY, CV_32F, 0, 1);
//lpha:归一化的最小值  ,beta:归一化的最大值。 
//Sobel卷积计算的结果值已经超过了输入图像的CV_8U字节的取值范围,所以这里输出图像使用了CV_32F的数据类型
normalize(dstX, dstX, 0, 1, NORM_MINMAX);
normalize(dstY, dstY, 0, 1, NORM_MINMAX);

// 自定义滤波实现 Sobel算子
filter2D(mat, dst, CV_32F, Matx33f(-3, 0, 3,-10,0,10,-3,0,3));
normalize(dst, dst, 0, 1, NORM_MINMAX);

imshow("mat", mat);
imshow("dstX", dstX);
imshow("dstY", dstY);
imshow("dst", dst);
Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dstX , dstY ,dst;
Scharr(mat, dstX, CV_32F, 1, 0);
Scharr(mat, dstY, CV_32F, 0, 1);
//lpha:归一化的最小值  ,beta:归一化的最大值。
normalize(dstX, dstX, 0, 1, NORM_MINMAX);
normalize(dstY, dstY, 0, 1, NORM_MINMAX);
addWeighted(dstX,0.5, dstY,0.5, 0,dst);
cv::namedWindow("dst", WINDOW_NORMAL);
imshow("dst", dst);

Laplacian算子

Sobel与Scharr等算子都是在同一个方向完成图像梯度的提取和计算,要得到图像两个方向的梯度,还需要进一步计算, 那么有没有一个卷积核可以同时直接计算得到X轴方向与Y轴方向的梯度,并且实现图像梯度的提取,拉普拉普,对一阶导数继续求导,二阶导数 ,缺点是会产生噪点.

拉普拉普算子

拉普拉斯算子通常用以下的离散算子表示:

f(x,y)=2fx2+2fy2f(x,y) = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2}

其中,f(x,y) 表示图像在 x 方向和 y 方向的二阶导数

在离散图像中,这个算子可以通过卷积操作来实现。具体来说,对于一个大小为 n*n 拉普拉斯核

Mx=[010141010]Mx = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \\ \end{bmatrix}

将这个核应用于图像的每个像素,相当于在该像素周围进行了二阶微分运算,从而可以找到图像中的边缘。 这个卷积核中心的值为 -4,而周围的值为 1。这种设置是为了在进行拉普拉斯变换时强调中心像素与周围像素之间的差异,从而检测出图像中的边缘或特征。 这个卷积核的设计是经过数学推导和实验验证的,目的是使拉普拉斯算子能够有效地捕捉到图像中的特征

Mat mat = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
Mat dst;
Laplacian(mat, dst, -1, 3,1,127);
normalize(dst, dst, 0, 1, NORM_MINMAX);
imshow("Laplacian", dst);

canny 边缘提取

标准的边缘检测算法包括如下几步。

  1. 将图像转换为灰度图像。

  2. 通过高斯模糊卷积实现降噪。

  3. 计算图像梯度的大小与角度。 首先,对图像进行梯度计算,通常使用Sobel算子或Prewitt算子。这些算子可以分别计算图像在水平和垂直方向上的梯度。 在梯度方向上,角度通常以弧度表示,可以在0到π之间。为了便于理解,通常将角度映射到0到180度之间。

  4. 非最大信号压制。

  5. 双阈值边缘连接。

其中,需要特别强调的是,第3步中的计算图像梯度的大小和角度是在X轴方向与Y轴方向梯度的基础上进一步计算的。 根据X轴和Y轴方向的梯度可以计算出图像中像素点的梯度幅值G与角度θ:

  • 非最大抑制得到图像梯度的大小和角度之后就可以完成非最大抑制操作了。在理想情况下,只有边缘像素的梯度是大于阈值T的, 但是在实际情况下,局部也会出现多个高梯度阈值,所以需要每个像素根据自身角度方向与两侧像素的梯度值进行比较。 如果当前像素点的梯度值小于两侧像素的梯度值,则将当前像素点的值设置为0(黑色);如果大于两侧像素的梯度值,则保留。 这步操作称为非最大抑制。

  • 双阈值连接边缘检测算法的双阈值连接是保证边缘连续的关键步骤。在双阈值中,一个是高阈值(H),一个是低阈值(L)。 双阈值连接操作首先使用低阈值L对梯度图像进行处理,高于L的值都保留,低于L的值都丢弃,并将值设置为0。然后使用高阈值H对图像进行处理, 高于H的都视为边缘像素点。梯度值在[L, H]之间的:如果从低阈值像素点出发,最终可以通过相邻的像素点连接到高阈值像素点, 而且整个连线上的像素点梯度值都大于L,则保留;否则设置为0(黑色)。 最终显示输出的边缘图像。OpenCV已经实现了边缘检测算法, 通过Canny函数就可以直接调用该算法,从而得到输入图像的边缘图像。

int t1=50;
Mat src;
void canny_demo(int,void*){
   Mat edges;
   //t1,t1*2 高低阀值
  Canny(src,edges,t1,t1*2,3, false);
  imshow("Canny",edges);
}
int main() {
  src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
  imshow("src",src);
  //"src" 是附加到的窗口名  canny_demo 是回调,
  createTrackbar("xxx:","src",&t1,100,canny_demo);
  canny_demo(0,0);
  cv::waitKey(0);
  //释放窗口和图像资源  
  cv::destroyAllWindows();

  return 0;
}

canny边缘提取

二值图像

二值图像在图像处理、对象检测与测量、缺陷检测、模式识别、机器人视觉等方面都有很重要的应用 二值对象是单通道,取值只能是 0(黑)和255(白)

CV_EXPORTS_W double threshold( InputArray src, OutputArray dst,
                               double thresh, double maxval, int type );
  • src与dst分别表示输入图像和输出图像,支持单通道与多通道,数据类型支持CV_8U与CV_32F。
  • thresh表示阈值。
  • maxval表示最大值,当图像数据类型为CV_8U时,其最大值为255,当图像数据类型为CV_32F时,其最大值为1.0。
  • type表示阈值化方法。 OpenCV中支持5种阈值化方法,它们的解释分别如下。
    1. THRESH_BINARY 二值化是指将阈值T与每个像素点的像素值进行比较,结果大于阈值T的赋值为最大值maxval;其他情况下赋值为0。
    2. THRESH_BINARY_INV 二值化反同样也是比较阈值T与每个像素点的像素值,结果大于阈值T的赋值为0;其他情况下赋值为maxval,所以形象地称它为二值化反。
    3. THRESH_TRUNC 阈值截断是指将阈值T与每个像素点的像素值进行比较,结果大于阈值T的赋值为T;其他情况下赋值为0,由此可以看出超过阈值的数值会被截取,只保留阈值大小。
    4. THRESH_TOZERO 阈值取零是指将阈值T与每个像素点的像素值进行比较,只要结果是不大于阈值的像素点就都赋值为0。
    5. THRESH_TOZERO_INV 阈值取零反是指将阈值T与每个像素点的像素值进行比较,只要结果是大于阈值的像素点就都赋值为0.

阈值

图像二值化的阈值计算方法有很多,单从算法类型上来说,可以分为全局阈值计算和局部阈值计算(自适应阈值计算)两大类。 OpenCV支持两种经典的全局阈值计算方法,分别是大津法与三角法。 自适应阈值计算方法支持均值与高斯自适应两种。

阈值的选取很重要,最简单的可以使用像素的均值

Mat src ,gray, dst;
src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
cvtColor(src, gray, cv::COLOR_BGR2GRAY);

//取像素的均值作为阈值
Scalar item= mean(gray);
threshold(gray,dst,item[0] , 255,THRESH_BINARY);
imshow("dst", dst);

全局阈值_OTSU 大津法

该方法最早是由大津展之提出的,所以通常称为大津法。该方法主要基于图像灰度直方图信息,类内方差最小,类间方差最大的像素值 输入图像必须是单通道灰度图像

大津法(Otsu's method)是一种自动确定图像阈值的方法,其基本原理是通过最小化类内方差和最大化类间方差来确定最佳的图像阈值。 通过循环 0到 255,循环计算获取阈值,通过每个循环像素分割成前景色(白色),背景色(黑)

  • 类内方差(Within-Class Variance)表示类内像素的差异程度,即在当前阈值下背景和前景之间的差异程度。
  • 类间方差(Between-Class Variance)表示类间像素的差异程度,即在当前阈值下背景和前景之间的相似程度。
Mat src ,gray, dst;
src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
cvtColor(src, gray, cv::COLOR_BGR2GRAY);

//此时的参数 0 不重要了,阈值是自己计算的,函数的返回值是 计算到的阈值
double t1=threshold(gray,dst,0 , 255,THRESH_OTSU);

imshow("dst", dst);

自适应阈值分割

全局阈值计算方法对光照方向一致的图像会有很好的二值化效果,但是当光照方向不一致的时候, 全局阈值计算会破坏图像的原有信息,导致二值图像的大量信息丢失,此时就需要一个更加智能的二值化分割算法, 可以在亮度不均匀的情况下,抵消亮度对图像的影响,完成二值化分割。这个算法就是OpenCV中的自适应阈值分割。 OpenCV中的自适应阈值分割并不会真正产生一个局部阈值, 而是先对输入的图像进行模糊处理,然后使用原图减去模糊图像得到一个差值图像, 再使用常量阈值C来与每个差值进行比较,大于-C的则赋值为白色,否则为0。

根据选择的模糊方式不同,自适应阈值分割可以分为均值自适应分割与高斯自适应分割。自适应阈值分割的函数定义如下:

只支持单通道的图像

void adaptiveThreshold( InputArray src, OutputArray dst,
                                     double maxValue, int adaptiveMethod,
                                     int thresholdType, int blockSize, double C );
  • src与dst分别是输入图像与输出图像,而且它们必须是8位单通道的。
  • maxValue是赋值给满足自适应条件的前景图像灰度值,一般将maxValue设置为255即可。
  • adaptiveMethod自适应方法当前支持均值自适应与高斯自适应两种方法
  • thresholdType是阈值化类型,
  • blockSize表示计算时候的窗口大小,而且必须是奇数,
  • 参数C为常量。用自适应阈值分割图像的示例代码如下
Mat src ,gray, dst1,dst2;
src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\lena.jpg");
cvtColor(src, gray, cv::COLOR_BGR2GRAY);

adaptiveThreshold(gray,dst1,255,THRESH_BINARY,ADAPTIVE_THRESH_MEAN_C,15,10);
//高斯自适应
adaptiveThreshold(gray,dst2,255,THRESH_BINARY,ADAPTIVE_THRESH_GAUSSIAN_C,15,10);
imshow("dst1", dst1);
imshow("dst2", dst2);
cv::waitKey(0);

二值分析

对比不同方法的二值化输出,我们可以根据观察到的结果,总结出在不同应用场景下的首选方法。常见的二值化方法如下

  1. 基于全局阈值(threshold函数)得到的二值图像。
  2. 基于自适应阈值(adaptiveThreshold函数)得到的二值图像。
  3. 基于边缘检测(Canny函数)得到的二值图像。
  4. 基于像素值范围(inRange函数)得到的二值图像。

要进行二值化以得到轮廓,就需要 根据上述4种常用的图像二值化方法进行处理,然后选择保留信息最多的那种二值化方法。 其中,边缘检测与像素值范围能够支持从彩色图像中直接获取二值图像的输出。threshold方法在手动设置阈值时,如果输入的是三通道的图像, 那么返回的就是三通道的图像,如果输入的是单通道的图像,那么返回的就是单通道的图像。自适应阈值只支持单通道图像输入。

注意:OpenCV中的二值分析算法,默认二值图像的背景为黑色(0),前景为白色(255)

CCL 连通组件标记

CCL(Connected Component Labeling,连通组件标记)算法是图像分析中最常用的算法之一。 CCL算法的实质是扫描一幅图像的每个像素,将位置相邻且值相同的像素点归为相同的组(group),最终得到图像中所有像素的连通组件。

对二值图像来说,CCL算法就是对图像中的每个前景对象的像素点进行扫描与分类,进而完成对每个前景对象的合并与定位,为后续的分析与测量做好准备。 常见的二值图像的连通组件扫描是通过如下两步来完成的。第一步,首先对二值图像的每个前景像素点进行标记。 假设当前的像素点为前景像素P(x,y),上方相邻的像素点为P(x,y-1),左侧相邻的像素点为P(x-1,y),那么标记方法如下。

  1. 当上方相邻的像素点与左侧相邻的像素点均未标记过时,则将当前的像素点标记为label+1。
  2. 当上方相邻的像素点与左侧相邻的像素点只有一个已标记过时,则将当前的像素点赋予与已标记像素点相同的标签值。
  3. 当上方相邻的像素点与左侧相邻的像素点均已标记过时,则选择二者中的较小值作为当前的像素点的标签值。

上述步骤的整个标记过程如图

ccl算法

仔细观察该图像可以发现:图像标记为1与2的前景对象是同一个连通区域;标记为3与7的前景对象是同一个连通区域;标记为4与6的前景对象是同一个连通区域。 接下来需要对这些等价的标记进行合并。第二步,执行等价标签类合并操作,得到最终的连通组件并标记输出,合并部分的图示如下图。

ccl算法

OpenCV中的连通组件扫描算法是在以上两步法的基础上改进的快速版本,实现了快速扫描算法BBDT(基于块的决策树)。相关的函数有两个:

  1. 基础版本的连通组件扫描,不包含每个组件的相关统计信息
  2. 带有统计信息的连通组件扫描,在完成连通组件扫描的同时,输出每个扫描组件的统计信息
int connectedComponents(InputArray image, OutputArray labels,
                        int connectivity, int ltype, int ccltype);
  • labels 是输出的标签图像,每个像素点都有一个标签值。在正常情况下,标签值大于0且相同的像素点属于同一个连通组件。
  • ccltype标签的数据类型默认为整数类型(CV_32S)。
  • connectivity 联通性 默认是8 联通, 8联通是元素周围的8个位置标记一样, 4联通是只有上下左右标记一样
int connectedComponentsWithStats(InputArray image, OutputArray labels,
                                 OutputArray stats, OutputArray centroids,
                                 int connectivity, int ltype, int ccltype);

在上述代码中,centroids是每个连通组件的中心坐标, stats参数表示的是统计信息,这些统计信息包含以下内容:

  • CC_STAT_LEFT:连通组件的外接矩形左上角坐标的x值。
  • CC_STAT_TOP:连通组件的外接矩形左上角坐标的y值。
  • CC_STAT_WIDTH:连通组件的外接矩形宽度。
  • CC_STAT_HEIGHT:连通组件的外接矩形高度。
  • CC_STAT_AREA:像素总和/连通组件的面积。

使用这些参数可以获取每个连通组件的中心位置、外接矩形、像素总数(连通组件的面积)等信息,从而很方便地得到每个连通组件的定位与统计信息

连通组件

using namespace cv;
using namespace std;

RNG rng(12345678);
void labelColor(int  connectedNum, Mat& labelImg, Mat& dst)
{
  
  //一张全黑的图片
  dst = Mat::zeros(labelImg.size(), CV_8UC3);
  
  //相同标记的给相同的颜色
  vector<Vec3b>  coloTable(connectedNum);
  //背景色
  coloTable[0]=Vec3b(0, 0, 0);
  for (int i = 1; i < connectedNum; ++i) {
    coloTable[i]=Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
  }
  
  int cols = labelImg.cols;
  int rows = labelImg.rows;
  
  for (int i = 0; i < rows; i++)
  {
    for (int j = 0; j < cols; j++)
    {
        int label=labelImg.at<int>(i,j);
        //把相同标记的区域标记成相同的颜色
        dst.at<Vec3b>(i, j) = coloTable[label];
    }
  }
}


int main() {
  Mat src, gaussianBlurImag, gray, thresholdImag, dst2;
  src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\cicle.jpg");
  
  GaussianBlur(src, gaussianBlurImag, Size(3, 3), 0);
  cvtColor(gaussianBlurImag, gray, COLOR_BGR2GRAY);
  threshold(gray, thresholdImag, 0, 255, THRESH_OTSU);
  imshow("thresholdImag", thresholdImag);
  
  Mat labels;
  //labels 输出的是连通域的标记信息,nums_conects 0 是黑色部分
  int nums_conects = connectedComponents(thresholdImag, labels, 8,CV_32S,CCL_DEFAULT);
  
  labelColor(nums_conects,labels, dst2);
  imshow("dst2aaa", dst2);
  cout << "nums_conects = " << nums_conects << endl;
  cv::waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

俩个黑色的圆圈分割形成的二值图像白色区域形成三个联通的区域,在下图给三个联通的区域赋值不同的颜色

二值CCL

带状态的连通组件

int main() {
  Mat src, gaussianBlurImag, gray, thresholdImag, dst2;
  src = imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\cicle.jpg");
  
  GaussianBlur(src, gaussianBlurImag, Size(3, 3), 0);
  cvtColor(gaussianBlurImag, gray, COLOR_BGR2GRAY);
 
  threshold(gray, thresholdImag, 0, 255, THRESH_OTSU);
  imshow("thresholdImag", thresholdImag);
  
  Mat labels,status,centroids;
  //labels 输出的是连通域的标记信息,nums_conects 0 是黑色部分
  int nums_conects = connectedComponentsWithStats(thresholdImag, labels,status,centroids, 8,CV_32S,CCL_DEFAULT);
  
  labelColor(nums_conects,labels, dst2);
  for (int i = 2; i < nums_conects; ++i){
     int cx = centroids.at<double>(i, 0) ;
     int cy = centroids.at<double>(i, 1) ;
     int x= status.at<int>(i, CC_STAT_LEFT);
     int y= status.at<int>(i, CC_STAT_TOP);
     int width= status.at<int>(i, CC_STAT_WIDTH);
     int height= status.at<int>(i, CC_STAT_HEIGHT);
     int area=status.at<int>(i, CC_STAT_AREA);

    circle(dst2,Point(cx, cy), 5, Scalar(0, 0, 255),2,8,0);
    rectangle(dst2, Point(x, y), Point(x + width, y + height), Scalar(0, 255, 0), 2, 8, 0);
    auto cicleText="cicle pixel radius:"+ to_string((int)round(height/2));
    putText(dst2, cicleText, 
            Point(x, y-20), FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 0, 0), 1, 8, 0);
  }
  
  
  imshow("dst2", dst2);
  cout << "nums_conects = " << nums_conects << endl;
  cv::waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

connectedComponentsWithStats.jpg

轮廓发现及绘制

二值图像的连通组件扫描可以发现并定位每个连通组件的位置,进而统计出像素总数, 但是无法给出连通组件之间的层级关系、 拓扑结构以及各个连通组件的轮廓信息, 想要获取这些信息,还需要应用OpenCV中轮廓发现的相关知识。

void findContours( InputArray image, OutputArrayOfArrays contours,
                              OutputArray hierarchy, int mode,
                              int method, Point offset = Point());

参数含义:

  1. image:图像必须是8位单通道图像,可以是灰度图像,但更常用的是二值图像,一般是经过Canny,拉普拉斯等边缘检测算子处理过的二值图像; (函数运行时,所有值为非0的像素都当成前景(255),所有值为0的像素都保持不变,因此输入图像会被当成二值图像 , 这个图像会被直接涂改,因此如果是将来还有用的图像,应该复制之后再传给该函数)
  2. contours:定义为vector<vector> contours;向量,向量内每个元素保存了一组由连续的Point点构成的点的集合的向量, 每一组Point点集就是一个轮廓,有多少轮廓,向量contours就有多少元素
  3. hierarchy:定义为vector hierarchy;,表示向量内每一个元素包含了4个int型变量——hierarchy[i][0] —hierarchy[i][3], 分别表示第i个轮廓的后一个轮廓、前一个轮廓、第一条子轮廓、父轮廓的索引编号,如果当前轮廓没有对应的后一个轮廓,前一个轮廓、 第一条子轮廓或父嵌轮廓的话,则hierarchy[i][0] —hierarchy[i][3]的相应位被设置为默认值-1;
  4. mode:轮廓提取方式
    • cv::RETR_EXTERNAL:只检测最外围轮廓;
    • cv::RETR_LIST:检测所有的轮廓,但是不建立等级关系;
    • cv::RETR_CCOMP:检测所有的轮廓,但所有轮廓只建立两种等级关系,外围为顶层
    • cv::RETR_TREE:检测所有的轮廓,所有轮廓建立一个等级树结构
  5. method:轮廓的近似方法
    • CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量中
    • CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存到contours向量中
  6. Point:偏移量,默认为(0,0)没有偏移
void drawContours( InputOutputArray image, InputArrayOfArrays contours,
                   int contourIdx, const Scalar& color,
                   int thickness = 1, int lineType = LINE_8,
                   InputArray hierarchy = noArray(),
                   int maxLevel = INT_MAX, Point offset = Point() );

参数含义:

  1. image:待绘制轮廓的图像;
  2. contours:要绘制的轮廓组,与findContours中输出的contours相同;
  3. contourIdx:指明画第几个轮廓,如果该参数为负值(通常设为-1),则画全部轮廓,
  4. color:指定绘制的颜色或亮度(灰度图像),如scalar(255,0,255)
  5. thickness:指定线段的宽度
  6. lineType:边框线型,可以是4或8,4代表绘制的线是四连通线(不美观),8代表绘制的线是八连通线(较美观)
  7. hierarchy:对应findContours中输出的hierarchy
  8. maxLevel:限制将在图上绘制的轮廓层次深度,当maxLevel=0时,表示只绘制当前的轮廓。当maxLevel=1时,表示绘制当前轮廓与它的嵌套轮廓;当maxLevel=2时, 表示绘制当前轮廓与它嵌套轮廓的嵌套轮廓。以此类推,设置maxLevel的值。
  9. offset:偏移量,默认为(0,0)没有偏移

只有设置了层次信息参数hierarchy时,maxLevel的设置才有效

int main() {
  Mat src,dst1,gray,dst2;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\轮廓发现.jpg");
 
  src.copyTo(dst1);

  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  cvtColor(dst1,gray,COLOR_BGR2GRAY);
 
  imshow("gray",gray);
  
  findContours(gray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_NONE, Point());
  dst2 = Mat::zeros(src.size(), src.type());
  drawContours(dst2,contours,-1,Scalar(255,0,0),2,8,hierarchy,2);
  imshow("dst2",dst2);
 
  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

轮廓发现

轮廓测量

轮廓测量是指对二值图像的每个轮廓的弧长和面积进行测量,根据轮廓的面积和弧长对大小不同的对象实现查找、过滤与处理的操作, 以寻找到感兴趣的RoI(Region of Interest)区域,这是图像二值分析的核心任务之一。 下面就是轮廓测量中计算面积、弧长/周长、 轮廓外接矩形等相关函数支持的介绍与说明。

计算面积

double contourArea( InputArray contour, bool oriented = false );

其中,参数contour表示输入的轮廓点集;参数oriented的默认值为false,表示返回的面积,且为正数。如果方向参数为true, 则表示会根据轮廓编码点的顺时针或者逆时针方向返回正值或者负值的面积。

计算周长和弧长

double arcLength( InputArray curve, bool closed );

其中,参数curve表示输入的轮廓点集;参数closed表示是否为闭合区域。

计算轮廓外接矩形

与连通组件扫描相似,轮廓发现的每个对象的轮廓都可以计算并生成一个外接矩形。OpenCV根据一个点集生成外接矩形的函数定义如下:

最大外接矩形

Rect boundingRect( InputArray array );

其中,参数array表示输入点集。

最小外接矩形

RotatedRect minAreaRect( InputArray points );
int main() {
  Mat src,gray,contoursDst;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\轮廓发现.jpg");
  GaussianBlur(src,src,Size(3,3),0,0);
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  cvtColor(src,gray,COLOR_BGR2GRAY);
  Mat binary;
  threshold(gray, binary,0,255,THRESH_BINARY | THRESH_OTSU);
  imshow("binary",binary);
  
  findContours(binary,contours,hierarchy,RETR_TREE,CHAIN_APPROX_NONE, Point());
  contoursDst = Mat::zeros(src.size(), src.type());
  //-1 是不填充
  drawContours(contoursDst,contours,-1,Scalar(255,0,0),1,8);
  imshow("contoursDst",contoursDst);

   for (int i = 0; i < contours.size(); ++i) {
     if(i==0 || i==3 || i==4 || i==6 ) {
       continue;
     }
    auto box= boundingRect((Mat)contours[i]);
    double area= contourArea(contours[i]);
    double arc = arcLength(contours[i],true);
    putText(contoursDst,"area:"+ to_string((int)round(area)),box.tl(),FONT_HERSHEY_SIMPLEX,0.6,Scalar(0,0,255),1,8);
    putText(contoursDst,"arc:"+ to_string((int)round(arc)),Point(box.x,box.y+20),FONT_HERSHEY_SIMPLEX,0.6,Scalar(0,255,0),1,8);
  }

  imshow("contoursDst",contoursDst);
  
  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

获取最大最小外接矩形

  Mat src,gray,contoursDst;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\轮廓发现.jpg");

  GaussianBlur(src,src,Size(3,3),0,0);
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  cvtColor(src,gray,COLOR_BGR2GRAY);
  
  Mat binary;
  threshold(gray, binary,0,255,THRESH_OTSU);
  imshow("binary",binary);
  
  findContours(binary,contours,hierarchy,RETR_TREE,CHAIN_APPROX_NONE, Point());
  contoursDst = Mat::zeros(src.size(), src.type());
  
   for (int i = 0; i < contours.size(); ++i) {
     //通过点获取绑定的最大矩形
    auto box= boundingRect((Mat)contours[i]);
     //通过点获取绑定的最小矩形,还可以输出角度
    auto rrt =  minAreaRect(contours[i]);
    vector<Point2f> points;
    rrt.points(points);
    for (int j = 0; j < 4; ++j) {
      // (j+1)%4 刚好满足 0->1  1->2  2->3  3->0 围一圈
      line(contoursDst,points[j],points[(j+1)%4],Scalar(0,0,255),2,8);
    }
    ellipse(contoursDst,rrt,Scalar(rng.uniform(0,255),rng.uniform(0,255),rng.uniform(0,255)),2,8);
    //rectangle(contoursDst,box,Scalar(0,255,0),2,8,0);
  }

轮廓计算

轮廓分析

可根据轮廓发现所得到的每个对象轮廓的最大外接矩形或最小外接矩形计算其横纵比,以及周长与面积大小。 根据实际图像分析的需要进行过滤,得到符合过滤条件大小的轮廓对象, 最后对这些轮廓对象进行更进一步的细化分析。

  1. 横纵比 轮廓分析需要计算轮廓外接矩形的横纵比(宽度/高度)
  2. 延展度 延展度是指轮廓面积(Contour Area)与最大外接矩形(Bounding Rect)面积的比值
  3. 实密度 是指轮廓面积与凸包面积的比值
  4. 对象像素均值 在进行轮廓绘制时,将thickness的值设置为-1就能完成轮廓填充,并生成轮廓对象所对应的掩膜, 然后用mean函数实现对掩膜区域的均值求解,

轮廓拟合和逼近

对于轮廓发现所得到的每个对象轮廓,除了测量它的面积与周长之外,有一些应用场景还要求对找到的轮廓点进行拟合,生成拟合的圆、椭圆或者直线。 而另外一些应用场景则要求尽可能逼近真实形状或者轮廓的大致形状,以便获得轮廓的几何信息。OpenCV提供了相关的函数与方法来实现对轮廓的拟合和逼近。

拟合椭圆

RotatedRect fitEllipse( InputArray points );

其中,参数points是输入的轮廓点,RotatedRect的输出包含如下信息:拟合之后椭圆的中心位置、椭圆的长轴与短轴的直径、椭圆倾斜角度。 然后,我们就可以根据得到的拟合信息绘制椭圆。当椭圆的长轴和短轴大小相等的时候,它就是圆。

在使用fitEllipse函数进行椭圆拟合时,最少需要5个轮廓编码点

int main() {
  Mat src,edges;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\stuff.jpg");
  
  int t=80;
  //高低阈值处理,提取二值的轮廓
  Canny(src,edges,t,t*2,3,false);
  
  //形态学膨胀让原来靠经的边缘轮廓相连
  auto k= getStructuringElement(MORPH_RECT, Size(3,3) ,Point(-1,-1));
  dilate( edges, edges, k );
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  //提取最外边的边缘
  findContours(edges,contours,hierarchy,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE, Point());
  
  //绘制椭圆
  for (int i = 0; i < contours.size(); ++i) {
     if (contours[i].size() > 5) {
       auto tl= fitEllipse(contours[i]);
       ellipse(src,tl,Scalar(0,0,255),2,8);
     }
  }

  imshow("src",src);

  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

椭圆拟合

拟合直线

void fitLine( InputArray points, OutputArray line, int distType,
                           double param, double reps, double aeps );
  1. points表示待拟合的输入点集合。
  2. line在二维拟合时,输出的是Vec4f类型的数据;line在三维拟合时,输出的是Vec6f类型的数据。
  3. distType表示拟合时使用的距离计算公式。OpenCV支持如下6种距离计算设置:DIST_L1=1、DIST_L2=2、DIST_L12=4、DIST_FAIR=5、 DIST_WELSCH=6、DIST_HUBER=7。
  4. param表示对模型进行拟合距离计算的公式是否需要用到该参数。当distType参数设置为5、6、7时表示需要用到该参数,否则该参数不参与拟合距离 计算。
  5. reps与aeps是指对拟合结果的精度要求
int main() {
  Mat src,edges;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\stuff.jpg");
  
  int t=80;
  //高低阈值处理,提取二值的轮廓
  Canny(src,edges,t,t*2,3,false);
  
  //形态学膨胀让原来靠经的边缘轮廓相连
  auto k= getStructuringElement(MORPH_RECT, Size(3,3) ,Point(-1,-1));
  dilate( edges, edges, k );
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  //提取最外部的边缘
  findContours(edges,contours,hierarchy,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE, Point());
  
  //绘制直线
  for (int i = 0; i < contours.size(); ++i) {
    if (contours[i].size() > 5) {
      auto tl= fitEllipse(contours[i]);
      ellipse(src,tl,Scalar(0,0,255),2,8);
    }
      Vec4f oneLine;
      fitLine(contours[i], oneLine, DIST_L2, 0, 0.01, 0.01);
      //轮廓的斜率
      float vx = oneLine[0];
      float vy = oneLine[1];
      //轮廓的原点
      float x0 = oneLine[2];
      float y0 = oneLine[3];
    
      circle(src,Point(x0,y0),3,Scalar(255,0,0),-1);
      float k = vy/vx;
      float b = y0-k*x0;
      //找到轮廓最大和最小点进行绘制
      int minX=0,minY=100000,maxX=0,maxY=0;
      for (int j = 0; j < contours[i].size(); ++j) {
          Point p=contours[i][j];
          if(p.y < minY) minY=p.y;
          if(p.y > maxY) maxY=p.y;
      }
      maxX =(maxY-b)/k;
      minX=(minY-b)/k;
    
      //绘制直线
      line(src,Point(minX,minY),Point(maxX,maxY),Scalar(0,255,0),2,8,0);
    
  }

  imshow("src",src);

  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

直线拟合

轮廓逼近

OpenCV中的几何形状拟合有时并不能顺利地得到轮廓的几何形状信息,而需要通过轮廓逼近才能实现。轮廓逼近是对轮廓真实形状的编码,根据编码点的数目可以大致判断出轮廓的几何形状。OpenCV轮廓逼近函数定义如下

void approxPolyDP( InputArray curve,
                    OutputArray approxCurve,
                    double epsilon, bool closed );

参数解释如下。

  1. curve表示轮廓曲线。
  2. approxCurve表示轮廓逼近输出的顶点数目。
  3. epsilon表示轮廓逼近的顶点距离真实轮廓曲线的最大距离,该值越小表示越逼近真实轮廓。 推荐将epsilon值设置在10~100之间
  4. closed表示是否为闭合区域。
int main() {
  Mat src,edges;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\stuff.jpg");
  
  int t=80;
  //高低阈值处理,提取二值的轮廓
  Canny(src,edges,t,t*2,3,false);
  
  //形态学膨胀让原来靠经的边缘轮廓相连
  auto k= getStructuringElement(MORPH_RECT, Size(3,3) ,Point(-1,-1));
  dilate( edges, edges, k );
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  //提取最外部的边缘
  findContours(edges,contours,hierarchy,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE, Point());
  
  for (int i = 0; i < contours.size(); ++i) {
     vector<Point> pts;
     //轮廓逼近,找出逼近的点
     approxPolyDP(contours[i],pts,20,true);
     //使用逼近的点绘制原点,利用这些原点可以看出轮廓的形状
     for (auto pt : pts) {
        circle(src,pt,3,Scalar(0,0,255),-1,8);
     }
  }

  imshow("src",src);

  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

轮廓逼近

凸包

在平面上能包含所有给定点的最小凸多边形叫做凸包。它明显特点是任意一条边都是凸边,选取任意俩个点绘制的直线都在凸包内部 opencv 使用 graham 扫描算法完成凸包发现

参考

 void convexHull( InputArray points, OutputArray hull,
                              bool clockwise = false, bool returnPoints = true );

参数解释

  1. points 输入的轮廓点
  2. 输出的凸包点
  3. 顺时针或逆时针进行查找
  4. 是否返回点集
int main() {
  Mat src,edges;
  src=imread("F:\\CPulsePulseStudy\\opencvLib\\sources\\samples\\data\\stuff.jpg");
  
  int t=80;
  //高低阈值处理,提取二值的轮廓
  Canny(src,edges,t,t*2,3,false);
  
  //形态学膨胀让原来靠经的边缘轮廓相连
  auto k= getStructuringElement(MORPH_RECT, Size(3,3) ,Point(-1,-1));
  dilate( edges, edges, k );
  
  vector<vector<Point>> contours;
  vector<Vec4i> hierarchy;
  //提取最外部的边缘
  findContours(edges,contours,hierarchy,RETR_EXTERNAL,CHAIN_APPROX_SIMPLE, Point());
  
  for (int i = 0; i < contours.size(); ++i) {
   

    vector<Point> pts;
    convexHull(contours[i],pts);
    if(isContourConvex(pts)) {
       cout<<"是凸包"<<endl;
    }
    //把检测出来的凸包点绘制成直线
    for (int j=0;j < pts.size();j++) {
      circle(src,pts[j],5,Scalar(0,0,255),-1);
      line(src,pts[j%pts.size()],pts[(j+1)%pts.size()],Scalar(0,255,0),1);
    }
  }

  imshow("src",src);

  waitKey(0);
  //释放窗口和图像资源
  cv::destroyAllWindows();

  return 0;
}

凸包检测

直线检测

直线检测使用了或霍夫算法

直角坐标和极坐标转换的关系是,来个坐标系关注的点不一样 直角坐标 坐标系关注 k,b 极坐标系关注 θ ,r 用不同的坐标系表达不同的关注 https://zhuanlan.zhihu.com/p/531356100

r=x×cos(θ)+y×sin(θ)r = x \times cos(θ) + y \times sin(θ)

推导过程是

首先,我们知道直角坐标系中的点 (x,y) 的极坐标 r 和 θ的关系是:

r=x2+y2;cos(θ)=x÷r;sin(θ)=y÷rr = \sqrt{x^2 + y^2}; \\ cos(θ)=x \div r ; sin(θ)=y \div r \\

将 r 的表达式代入,我们得到:

cos(θ)=x÷(x2+y2);sin(θ)=y÷(x2+y2);r=x×(x÷(x2+y2));r=y×(y÷(x2+y2))进而得到r=x×cos(θ)+y×sin(θ)cos(θ)=x \div (\sqrt{x^2 + y^2}); sin(θ)=y \div (\sqrt{x^2 + y^2}); r=x \times (x \div(\sqrt{x^2 + y^2})) ; r=y \times (y \div (\sqrt{x^2 + y^2})) \\ 进而得到 r = x \times cos(θ) + y \times sin(θ)

Hough变换: 核心思想:直线 每一条直线 y = kx + b ,对应一个k , b,极坐标下对应一个点 (θ,r)

极坐标

圆检测

👍🎉🎊