容器处理 vector, map等1. 容器转换0. 在有序容器中给cv::Point排序 struct ComparePoints { bool operator()(const cv::Point lhs, const cv::Point rhs) const { if (lhs.x rhs.x) { return true; } else if (lhs.x rhs.x lhs.y rhs.y) { return true; } else { return false; } } }; 1. vectorcv::Point 转换到 setcv::Point, ComparePoints std::setcv::Point, ComparePoints pointSet(points.begin(), points.end()); 2. vectorcv::Point 和 vectorPoint2f 的快速互相转化 - vectorPoint2f -- vectorcv::Point (四舍五入) vectorPoint2f vd{ { 1.1, 2.2 }, { 3.3, 4.4 }, {5.5, 6.6} }; vectorPoint v(vd.begin(), vd.end()); - vectorcv::Point -- vectorPoint2f vectorPoint vP{ { 1, 2 }, { 3, 4 }, {5, 6} }; vectorPoint2f v(vP.begin(), vP.end()); 3. [vectorcv::Point系列] -- [cv::Mat] | vectorT | MatType | Mat.cols | |:-------------------:|:--------:|:--------:| | vectorcv::Point | CV_32SC1 | 2 | | vectorcv::Point3i | CV_32SC1 | 3 | | vectorcv::Point2f | CV_32FC1 | 2 | | vectorcv::Point3f | CV_32FC1 | 3 | | vectorcv::Point2d | CV_64FC1 | 2 | | vectorcv::Point3d | CV_64FC1 | 3 | template typename T cv::Mat transVecPt2Mat(const vectorT ptlist, bool clonetrue) { if (ptlist.empty()) return Mat(); int col 0; int type 0; if (std::is_same_vT, cv::Point){ col 2; type CV_32SC1; }else if (std::is_same_vT, cv::Point3i){ col 3; type CV_32SC1; }else if (std::is_same_vT, cv::Point2f){ col 2; type CV_32FC1; }else if (std::is_same_vT, cv::Point3f){ col 3; type CV_32FC1; }else if (std::is_same_vT, cv::Point2d){ col 2; type CV_64FC1; }else if (std::is_same_vT, cv::Point3d){ col 3; type CV_64FC1; }else{ return cv::Mat(); } Mat mat(int(ptlist.size()), col, type, const_castvoid*(static_castconst void*(ptlist.data()))); if (clone){ return mat.clone(); }else{ return mat; } } 4. [cv::Mat] -- [vectorcv::Point系列] | MatType | Mat.cols | vectorT | |:--------:|:--------:|:-------------------:| | CV_32SC1 | 2 | vectorcv::Point | | CV_32SC1 | 3 | vectorcv::Point3i | | CV_32FC1 | 2 | vectorcv::Point2f | | CV_32FC1 | 3 | vectorcv::Point3f | | CV_64FC1 | 2 | vectorcv::Point2d | | CV_64FC1 | 3 | vectorcv::Point3d | template typename T void transMat2VecPt(const cv::Mat mat, vectorT ptlist) { ptlist.clear(); if (mat.empty()){ return; } if ((mat.cols!2)(mat.cols!3)){ return; } int type mat.type(); if (typeCV_32SC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point){ return; } }else{ if (!std::is_same_vT, cv::Point3i){ return; } } }else if (typeCV_32FC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point2f){ return; } }else{ if (!std::is_same_vT, cv::Point3f){ return; } } }else if (typeCV_64FC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point2d){ return; } }else{ if (!std::is_same_vT, cv::Point3d){ return; } } }else{ return; } ptlist.resize(size_t(mat.rows)); cv::Mat tmp mat.reshape(mat.cols, mat.rows); tmp.copyTo(cv::Mat(ptlist, false)); return; }2. vector1. cv::convexHull() 凸包 函数原型: void cv::convexHull( InputArray points, // 输入点集 OutputArray hull, // 输出的凸包 bool clockwise false, // 是否按顺时针方向输出凸包点 bool returnPoints true // 是否返回凸包点的坐标 ) - 输入点集 vectorcv::Point - 输出点集 vectorcv::Point - 功能: 输入的若干点集中找到n个点其围成的多边形能囊括住所有输入点 - 备注: 输出的点是有顺序的 std::vectorcv::Point2i hullPtsOut; cv::convexHull(std::vectorcv::Point2i{lumenKeyPtsPre.begin(), lumenKeyPtsPre.end()}, hullPtsOut); 2. 计算轮廓中心/重心 vectorcv::Point2f lumenPts; cv::Moments moments cv::moments(lumenPts); cv::Point2f center(float(moments.m10 / moments.m00), float(moments.m01 / moments.m00)); 3. 快速计算中位数 vectorint collist{ 2,3, 1, 2, 9, 10, 111, 8,1, 4, 5, 5, 5, 5, 5, 5, 5, 5,5,5,5,5,5,5,5,5,5,5,5,1,3,4,90 }; size_t mid collist.size() / 2; std::nth_element(collist.begin(), collist.begin() int(mid), collist.end()); cout collist[mid] endl; 4. 对一个结构体组成的vector进行排序 ## 方法一: 自定义比较函数 bool compare(int a, int b) { return a b; // 如果 a b返回 true用于降序排序 } std::sort(vec.begin(), vec.end(), compare); ## 方法二: Lambda 表达式 std::sort(vec.begin(), vec.end(), [](int a, int b) { return a b; // 用于降序排序 }); 5. 循环索引, 防越界 对一组数据按照某个索引取值时担心索引越界, 并希望越界的索引能从头开始取时, 可以这样 假设一组数据 vector... data 的元素个数 (即data.size()) 是 N 那么对于某个索引 idx (idx0), 如果希望 idx N 时能够循环地从data的起点开始取值 则 实际索引值 idx_real idx % N 例如, 元素一共有 5 个 (N5) 则 对于 idx 0, idx_real 0 则 对于 idx 4, idx_real 4 则 对于 idx 5, idx_real 0 则 对于 idx 6, idx_real 1 以此类推3. set1. set插入cv::Point std::setcv::Point, ComparePoints pointSet; pointSet.insert(cv::Point(1, 2)); pointSet.insert(cv::Point(3, 4)); 2. 返回一个set的首个元素 cv::Point firstPoint *pointSet.begin();4. map1. 两个map合并 (1) map2 合并到 map1中, 有重复key不覆盖, 且map2中被merge到map1的元素会被移除 (最高效方案) map1.merge(map2); (2) map2 合并到 map1中, 有重复key不覆盖, 且map2中的元素不会受影响 map1.insert(map2.begin(), map2.end()); (3) map2 合并到 map1中, 有重复key覆盖, 且map2中的元素不会受影响 for (const auto pair : map2) { map1[pair.first] pair.second; }矩阵处理 cv::Mat1. cv::Mat - vector1. 二值图提取轮廓 cv::findContours返回的轮廓点是有序的,轮廓之间是无序的 std::vectorstd::vectorcv::Point contours; cv::findContours(binaryImage, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); cv::findContours(sqrImg, contours, hierarchy, cv::RETR_CCOMP, CHAIN_APPROX_NONE); - 当使用上述函数时, contours中每一个contour都可以表示成std::vectorcv::Point, cv::Point之间是有序的,12点方向,逆时针转,但contour和contour之间彼此是无序的 - cv::RETR_EXTERNAL: 只提取外轮廓 - cv::RETR_CCOMP: 内外轮廓都提取,含父轮廓子轮廓关系 - cv::CHAIN_APPROX_NONE: 全部轮廓点 - cv::CHAIN_APPROX_SIMPLE: 多边形关键轮廓点 2. 二值图非零点坐标提取 cv::findNonZero void cv::findNonZero(const cv::Mat mask, vectorcv::Point idxlist); - mask: 必须是单通道, 但数据类型可以是CV_8UC1,CV_32SC1,CV_64FC1 - idxlist: 输出结果,非零点坐标,cv::Point.x非零点列坐标,cv::Point.x非零点行坐标2. cv::Mat 处理0. 矩阵初始化 cv::Mat mat (cv::Mat_float(5, 3) 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f); cv::Mat mat(512,512,CV_8UC1,cv::Scalar(255)); 1. 形态学操作 * 生成核 // 矩形核 cv::Mat element cv::getStructuringElement(cv::MORPH_RECT, cv::Size(15, 15)); // 圆形核 cv::Mat element cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(15, 15)); * 膨胀 cv::dilate(image, out, element); * 侵蚀 cv::erode(image, out, element) * 闭操作 cv::morphologyEx(srcImage, dstImage, cv::MORPH_CLOSE, element); * 开操作 * cv::morphologyEx(srcImage, dstImage, cv::MORPH_OPEN, element); * 翻转 InputArray src:要处理的原始图像 OutputArray dst:是和src具有相同大小、类型的目标图像 int flipCode:旋转类型 flipCode 0x轴方向旋转 上下翻转 flipCode 0y轴方向旋转 左右翻转 flipCode 0x轴y轴方向同时旋转 void cv::flip(InputArray src,OutputArray dst,int flipCode); * 旋转-特殊角度 ROTATE_180, ROTATE_90_CLOCKWISE ROTATE_90_COUNTERCLOCKWISE void cv::rotate(InputArray src, OutputArray dst, int rotateCode); * 对角线翻转 cv::transpose(maskM, maskM); 2. 查看cv::Mat数据类型 | 数据类型 | C1 (单通道) | C3(三通道) | C4(四通道) | | :------: | : ------- : | : ------ : | : ------ : | | CV_8U (uchar) | 0 | 16 | 24 | | CV_32S (int) | 4 | 20 | 28 | | CV_32F (float) | 5 | 21 | 29 | | CV_64F (double)| 6 | 22 | 30 | 详见: https://blog.csdn.net/m0_58709899/article/details/160217850?spm1001.2014.3001.5501 3. 填充多边形 cv::Mat showMat cv::Mat::zeros(oct.idp.points * 2, oct.idp.points * 2, CV_8UC1); * 方法1: cv::fillPoly(showMat , std::vectorcv::Point2i{tempState.curvePts.begin(), tempState.curvePts.end()}, 255); * 方法2 (一次填充多个多边形): cv::drawContours(showMat, contours, -1, 255, cv::FILLED); 4. 连通域相关 连通域问题 cv::Mat labels, stats, centroids; int nccomps cv::connectedComponentsWithStats(biImg, labels, stats, centroids); labels数据类型: CV_32SC1 (int) stats数据类型: CV_32SC1 (int) centroids数据类型: CV_64FC1 (double) int x stats.atint(i, 0); int y stats.atint(i, 1); int w stats.atint(i, 2); int h stats.atint(i, 3); int s stats.atint(i, 4); * 找到最大连通域 stats.atint(0, 4) -1; cv::Point componentMaxLoc; cv::minMaxLoc(stats.col(4), nullptr, nullptr, nullptr, componentMaxLoc); cv::Mat bigestCpn cv:: Mat::zeros(biImg.rows, biImg.cols, CV_8UC1); bigestCpn.setTo(255, labels componentMaxLoc.y); 5. 改变矩阵形状/维度 reshape函数 * 注意reshape的第一参数表示通道数(0表示通道数不变) 第二个参数表示行数 没有参数表示列数 cv::Mat mat cv::Mat::ones(10, 10, CV_32FC1) int newChannels 1; int newRows 5; mat mat.reshape(newChannels, newRows); // 【注意reshape的第一参数表示通道数 第二个参数表示行数 没有参数表示列数】 6. 矩阵按行/列计算平均值/最大值/最小值/求和 src输入矩阵 (注意:输入矩阵的数据类型不能是int型即不能是CV_32S !!!) dst输出矩阵 dim: 维度: dim0,矩阵被处理成一行(y方向/纵向进行计算); dim1, 矩阵被处理成一列(x方向/横向进行计算); dim-1时维数将根据输出向量的大小自动选择 rtype: ------- cv::REDUCE_SUM 输出是矩阵的所有行/列的和 ------- cv::REDUCE_AVG 输出是矩阵的所有行/列的平均向量 ------- cv::REDUCE_MAX 输出是矩阵的所有行/列的最大值 ------- cv::REDUCE_MIN 输出是矩阵的所有行/列的最小值 dtype: 设置输出矩阵的数据类型,默认等于输入矩阵数据类型 (推荐: 32F or 64F) void cv::reduce(InputArray src, OutputArray dst, int dim, int rtype, int dtype -1); 示例 矩阵按行取平均,并将结果投影到显示矩阵screen上 cv::Mat screen(sqrImgMap_preProced.begin()-second.rows, int(sqrImgMap_preProced.size()), CV_32FC1); for (const auto ele : sqrImgMap_preProced) { cv::reduce(ele.second, screen.col(ele.first), 1, cv::REDUCE_AVG, CV_32FC1); } 7. 多个单通道矩阵压缩成一个多通道矩阵 和 一个多通道矩阵分拆成多个单通道矩阵 * 多个单通道 -- 一个多通道矩阵 vectorcv::Mat vec_mat_list; // vector内包含了n个单通道矩阵; 矩阵的尺寸和数据类型必须相同 cv::Mat mergedMat(m, n, CV_32FC(n)); // 初始化一个空的多通道矩阵: 尺寸、通道数、数据类型必须和vector包含的矩阵一致 cv::merge(vec_mat_list, mergedMat); // vector -- cv::Mat * 一个多通道矩阵 -- 多个单通道 vec_mat_list.clear(); cv::split(mergedMat, vec_mat_list); // cv::Mat -- vector 8. 矩阵的横向拼接(等高) 和 矩阵的纵向拼接(等宽) cv::hconcat(...) // 横向拼接 cv::vconcat(...) // 纵向拼接 9. 从多通道矩阵中抽出某个通道的矩阵 cv::Mat matChns; // 输入:多通道矩阵 cv::Mat matObj; // 输出:抽取得到的单通道矩阵 int n 1; // 抽取第1个通道 cv::extractChannel(matChns, matObj, n); 10. 三通道矩阵访问/赋值 cv::Vec3b对应由uchar组成的三通道矩阵 cv::Vec3i对应由int组成的三通道矩阵 cv::Vec3f对应由float组成的三通道矩阵 cv::Vec3d对应由double组成的三通道矩阵 for (int y 0; y image.rows; y) { for (int x 0; x image.cols; x) { cv::Vec3b intensity image.atcv::Vec3b(y, x); for (int c 0; c image.channels(); c) { // 处理/访问每个intensity[c]; // 对应第y行第x列第c层 } } } 11. 矩阵的位操作 注意: 位操作可以与Mask掩膜搭配使用,确保只在局部区域进行位操作 cv::bitwise_and() 与 cv::bitwise_or() 或 cv::bitwise_not() 非 cv::bitwise_xor() 异或 12. 矩阵的拷贝、截取、复制粘贴 详见: https://blog.csdn.net/m0_58709899/article/details/146070176?spm1001.2014.3001.5502 13. 自动二值化算法 - 大津 二值化 double thres cv::threshold(img, img_Thr_O, 0, 255, THRESH_BINARY | THRESH_OTSU); - 三角 二值化 double thres cv::threshold(img, img_Thr_O, 0, 255, THRESH_BINARY | THRESH_TRIANGLE); - 自适应 二值化 -- int adaptiveMethod: ADAPTIVE_THRESH_GAUSSIAN_C, ADAPTIVE_THRESH_MEAN_C -- int blockSize: 奇数, 对某个位置计算二值化阈值时所选取的观察窗的尺寸 -- 参考: https://blog.csdn.net/weixin_42272768/article/details/110817275 cv::adaptiveThreshold(Rou, binary2, 255, adaptiveMethod, THRESH_BINARY, blockSize, 0); 14. 将矩阵沿着x轴或/和y轴重复堆叠 cv::repeat(src, ny, nx) * e.g. src为1行N列的矩阵时, 设置nx1, nyM, 则src沿y轴堆叠,生成矩阵M行N列 * e.g. src为N行1列的矩阵时, 设置nxM, ny1, 则src沿x轴堆叠,生成矩阵N行M列 15. 矩阵的push_back() 可以通过push_back()函数对一个矩阵添加新的行 (只能是行) cv::Mat mat; mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*1); mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*2); mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*3); cout mat endl endl; 16. 同尺寸的两个矩阵图像逐像素比较,输出掩膜 cv::compare void compare(const cv::Mat src1, const cv::Mat src2, cv::Mat dst, int cmpop); src1和src2逐元素比较,若符合cmpop对应的比较标准则对输出掩膜dst相应位置置为255,否则置0 要求: - src1和src2必须type相同,size相同 - src1和src2可以不是CV_8U, 可以是CV_32S或者CV_32F甚至CV_64F - dst为掩膜,类型为CV_8U, 且必为二值化结果 0-255 - cmpop参数表 枚举常量 英文全称 中文含义 判断逻辑 CMP_GE Greater or Equal 大于等于 src1(i,j) ≥ src2 CMP_GT Greater Than 大于 src1(i,j) src2 CMP_LE Less or Equal 小于等于 src1(i,j) ≤ src2 CMP_LT Less Than 小于 src1(i,j) src2 CMP_EQ Equal 等于 src1(i,j) src2 CMP_NE Not Equal 不等于 src1(i,j) ≠ src2 17. 根据某灰度范围, 基于图像矩阵生成一个掩膜 cv::inRange void cv::inRange(const cv::Mat src, cv::Scalar lowerb, cv::Scalar upperb, cv::Mat dstMask); - src 可以为单通道或三通道 - lowerb 下限, 灰度大于等于该值的像素会被接受 - upperb 上线, 灰度小于等于该值的像素会被接受 - dstMask 输出掩膜, CV_8UC1 - 示例: cv::inRange(grayMat, cv::Scalar(20), cv::Scalar(200), resMat); 18. 基于输入矩形框,分割出其中的前景,生成相应的掩膜 cv::grabCut void cv::grabCut(const cv::Mat colorImg, cv::Mat mask, cv::Rect rect, cv::Mat bgdModel, cv::Mat fgdModel, int iterCount, int modecv::GC_INIT_WITH_RECT); - 函数功能: 在输入彩色图像上,参考给定的矩形框rect (必须确保rect完全包裹住前景), 函数对图像进行GMM智能分割, 给出掩膜: 前景,背景,疑似前景,疑似背景 - colorImg: 输入图像,必须为三通道 CV_8UC3 - mask: 输出掩膜, CV_8UC1, cv::Size等于colorImg, 值域存在四个值: 0 (GC_BGD): 背景 1 (GC_FGD): 前景 2 (GC_PR_BGD): 疑似背景 3 (GC_PR_FGD): 疑似前景 - rect: 矩形框,必须确保rect完全包裹住前景,即被分割对象 - bgdModel, fgdModel: 函数内部迭代使用,不必理会,传入空两个矩阵即可 - iterCount: 迭代次数, 一般3-5次, 但我自测1次即可 - mode: 模式, 默认cv::GC_INIT_WITH_RECT, 即使用矩形框初始化 * 该函数实测: - 代码: cv::Mat imgC cv::imread(, cv::IMREAD_COLOR); cv::Rect roi(220,218,74,75); cv::Mat mask; cv::Mat bgdModel; cv::Mat fgdModel; CC_BFs::Timer timer; grabCut(imgC, mask, roi, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT); mask.setTo(255, maskGC_FGD); mask.setTo(100, maskGC_PR_FGD); mask.setTo(200, maskGC_PR_FGD); - 执行效率 512尺寸饼图,矩形框边长75,迭代1次耗时: 114ms 512尺寸饼图,矩形框边长75,迭代3次耗时: 262ms - 效果 从mask上看还是有一定效果的, 但几乎分割不出纯前景(GC_FGD), 因此实际使用时应该: (1) 把 GC_PR_FGD 和 GC_FGD 都视为分割结果的前景部分 (置255) (2) 把 GC_BGD 和 GC_PR_BGD 都视为分割结果的背景部分 (GC_PR_BGD置0, GC_BGD本就是0) 19. 使用指针来访问cv::Mat矩阵中的元素 float* datamat.ptrfloat(i) 无论矩阵是否连续(mat.isContinuous()true)都可以使用以下方式访问: cv::Mat mat cv::Mat::ones(M,N,CV_64FC1); int rows mat.rows; int cols mat.cols; for (int i 0; i rows; i) { double* row_ptr mat.ptrdouble(i); // 获取矩阵第i行数据的指针 for (int j 0; j cols; j) { double val row_ptr[j]; // row_ptr[j] 等价于 mat.atdouble(i,j) } } * 代码等价: mat.atdouble(i,j) 等价于 mat.ptrdouble(i)[j] * 但后者的速度优势只在大量连续访问/修改矩阵内存数据时才会体现,只是取一个值at就足够了 * 无论矩阵是否连续,矩阵的每一行数据都是连续的 * 矩阵若经历了转置/切片等操作,往往不再连续,也就是说矩阵的行与行之间内存并不连续 * 如果矩阵是三通道的,则应按照如下方式访问 cv::Mat mat cv::Mat::ones(M,N,CV_8UC3); int rows mat.rows; int cols mat.cols; for (int i 0; i rows; i) { uchar* row_ptr mat.ptruchar(i); for (int j 0; j cols; j) { uchar B row_ptr[j * 3 0]; uchar G row_ptr[j * 3 1]; uchar R row_ptr[j * 3 2]; } } 20. 构造矩阵时用到的 static_cast..., const_cast... 以及 void* # static_castxxx(data): 数据类型转换, 将data的原数据类型转换为xxx数据类型,但不能消除const属性 - 例1 int data 0; double data2 static_castdouble(data); - 例2 const vectorPoint pts {....}; // 已赋值 const void* ptsPtr static_castconst void*(pts.data()); // 注:pts.data() 数据类型为cv::Point* # const_castxxx(data): 强制消除data数据的const属性 - 例1 const int* data; // 已赋值 int* dataPtr const_castint*(data); - 例2 const void* ptsPtr; // 已赋值 void* ptsPtr2 const_castvoid* ptsPtr; # 要解决的问题: 已知数据并构造cv::Mat矩阵时,后者的构造方式如下: cv::Mat(int rows, int cols, int type, void* data); 【注】通过data写入函数的是纯二进制数据,具体读入几个字节以及矩阵如何理解数据完全取决于参数rows,cols,type 最后参数传入的应该是void* 但是, 我们也可以传入int*, float*, double*, uchar*, 因为函数内会自发地将其转换为void*, 其只是数据地址的记录 当然,我们也可以使用static_castvoid*在外部手动转化(纯粹多余) 但是, 无论如何, cv::Mat构造时最后参数传入的不能有const属性, 因此我们必须使用 const_castxxx 将其const属性消除掉 - 例1 对于数据 const int* data, 应该: cv::Mat(rows, cols, CV_32SC1, const_castint*(data)); - 例2 对于数据 const vectorPoint pts, 应该: cv::Mat(rows, cols, CV_32SC1, const_castcv::Point*(pts.data())); # 进阶, 【通用解决手段】 - 对于任何const的数据类型的【指针】 const Type dataPtr 都可以使用如下方式 const_castvoid*(static_castconst void*(dataPtr))将其传入矩阵构造函数, 即 cv::Mat(rows, cols, type, const_castvoid*(static_castconst void*(dataPtr))); - 对于任何非const的数据类型的【指针】 Type dataPtr 都可以使用如下方式 static_castvoid*(dataPtr)将其传入矩阵构造函数, 即 cv::Mat(rows, cols, type, static_castvoid*(dataPtr)); - 更多详见: 其他--函数说明备注参数计算1. 对点的运算cv::Point1. 判断多边形与点之间的关系 double cv::pointPolygonTest(const cv::InputArray contour, cv::Point2f pt, bool measureDist) * 参数说明 - contour输入的多边形可以是一个 std::vectorcv::Point,std::vectorcv::Point2f 或者 cv::Mat 类型的数组。 注如果输入的是cv::Mat, 那么必须确保cv::Mat是一个N行2列的矩阵2行N列不可以, 且应该为CV_32FC1(推荐)或CV_32SC1或CV_64FC1 - pt待判断的点类型为 cv::Point2f。 - measureDist是否计算点到多边形的距离默认为 false。 * 返回值说明 如果 measureDist 为 false返回值表示点与多边形之间的关系 --大于 0 表示点在多边形内部 --等于 0 表示点在多边形的边界上 --小于 0 表示点在多边形外部。 如果 measureDist 为 true返回值表示点到多边形的最短距离。2. 对数字的处理1. 基于已知上下界对数字范围进行限制钳位 std::clamp 逻辑: 输出 std::clamp(输入, 下限, 上限) - 若输入在上下限界内(闭区间), 则输出输入 - 若输入小于下限, 则输出下限 - 若输入大于上限, 则输出上限 cout std::clamp(0.0, 0.1, 0.8) endl; // 0.1 cout std::clamp(0.5, 0.1, 0.8) endl; // 0.5 cout std::clamp(0.8, 0.1, 0.8) endl; // 0.8 cout std::clamp(1.0, 0.1, 0.8) endl; // 0.8其他1. 函数说明备注1. 画直线 cv::line cv::line(image, pt1, pt2, cv::Scalar(0, 0, 255), thickness); thickness 指的是线的半宽包含线的中心 因此线无论thickness的奇偶都是对称的 2. 右移,左移,位操作, (,,) a N 即对一个变量的二进制结果数字右移N位, 移除的低N位弃之不用, 高位补0 a N 即对一个变量的二进制结果数字左移N位并造成升位, 如uchar类型左移会变成int类型, 高位低位都补0 配和位操作且“”, 可以取出特定位数段上的结果 * 应用: 四个uchar值转 和 一个int值 互相转换, 即用一个int值表示四个uchar值 // uchar值1个字节, sizeof(uchar) 1 // int值4个字节, sizeof(int) 4 * uchar 转 int, u1-低八位; u4-高八位 void transUchar2Int(uchar u1, uchar u2, uchar u3, uchar u4, int data) { data (u424) | (u316) | (u28) | u1; } * int 转 uchar, u1-低八位; u4-高八位 备注: 十六进制数F对应4位, 因此 0xFF 表示取低八位 void transInt2Uchar(int data, uchar u1, uchar u2, uchar u3, uchar u4) { u1 data 0xFF; u2 (data 8) 0xFF; u3 (data 16) 0xFF; u4 (data 24) 0xFF; } 3. 数组, int* data, int data[] * 数组的初始化: 栈初始化, 堆初始化 - 栈初始化, 特点: 不必手动释放 可以计算数组长度 可以在初始化时赋值 int data[] {10,20,30,40,50}; // 栈初始化一个数组 data[1] 99; // 对数组进行改值 for (int i0; i5; i){ cout data of i data[i] endl; // 读出数组结果 } int len sizeof(data) / sizeof(int); // 计算数组长度 - 堆初始化, 特点: 必须手动释放 不可以计算数组长度(因为其本质是一个指针) 不可以在初始化时赋值 int* data new int[5]; // 堆初始化一个数组 data[0] 10; // 对数组进行赋值 data[1] 20; data[2] 30; data[3] 40; data[4] 50; for (int i0; i5; i){ cout data of i data[i] endl; // 读出数组结果 } if (data!nullptr){ delete[] data; datanullptr; } - 共同点: 无论是哪种初始化方式, 当数组传入一个函数时都会变成一个指针; 另一方面,当我们想构建一个函数来接收数组时, 接收的也只能是指针; 此外由于退化成指针, 无论哪种初始化的数组都不能在函数内部再计算尺寸了, 需要外界传入数组尺寸或约定好尺寸; void calData(int* data, int Size) { for (int i0; iSize; i){ cout data[i] endl; } } 两种初始化方式的数组在传入上述函数时,方式是一样的: calData(data,5); // int data[] {10,20,30,40,50}; calData(data,5); // int* data new int[5]; 4. 借尸还魂法 QByteArray -- int* reinterpret_castint*( ... ) // 隐式转换 数据没变 只是改变了被读方式 * 需求场景: 我的数据容器是QByteArray, 但函数的输出参数是int* (1) 定义一个尺寸大小数据类型确定的变量: QByteArray byteArrayData; byteArrayData.reserve(rows*cols*channels); (2) 比如某个函数的传出参数要求数据类型是int* 而我们刚刚定义的是 QByteArray (3) 此时可以将byteArrayData按照如下方式传入,可以借尸还魂,取到结果放在byteArrayData中: void myFunction(int* result); // 函数声明 myFunction(reinterpret_castint*(byteArrayData.data())); // 函数调用 5. unsigned char* 和 uchar* 是等价的 6. 构建函数时, 将另一个函数作为参数传入 int multiply(int a, int b) { return a * b; } void compute(int x, int y, std::functionint(int, int) func) { // 使用 std::function 接收函数对象 std::cout Result: func(x, y) std::endl; } 7. static_cast..., const_cast... 说明 static_cast...: 数据类型转换(无论是否为指针), 但不改变const属性 const_cast... : 去除const属性(无论是否为指针), 但不能改变数据类型 - 例1 int data1 0; double data2 static_castdouble(data1); - 例2 const int data1 0; const double data2 static_castconst double(data1); double data3 const_castdouble(data2); - 例3 int* data1 new int[5]; void* data2 static_castvoid*(data1); - 例4 const int* data1 new int[5]; const void* data2 static_castconst void*(data1); void* data3 const_castvoid*(data2); - 例5 const int data1[] {10,20,30,40,50}; // 注: data1[]是数组, data1是指针 const void* data2 static_castconst void*(data1); void* data3 const_castvoid*(data2); 8. reinterpret_cast... 说明 对于数据(指针数据),按照指定的方式解读 比如对于一个uchar数组 内存中的二进制数据: 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 0 1 0 0 1 0 1 1 0 1 1 1 1 1 0 0 0 按uchar*解读 : |----uchar----|----uchar----|----uchar----|----uchar----| 按int*解读 : |-----------------------------int-----------------------------| 因此,reinterpret_cast...只是改变了数据的解读方式, 比如有的函数要求传入的是int*, 但我们的变量是uchar*, 这时可以使用该方法将函数外uchar*变量按照int*解读再传入进函数 而函数内对int*这个指针指向的内存会进行数据修改,以此来影响函数外uchar*指向的结果 例: int data[] {10,20,30,40,50}; // 注: data[]是数组, data是指针 cv::Mat mat(5,1,CV_32SC1, data); uchar* d mat.data; // 注: 无论cv::Mat是什么类型,.data返回的数据指针都是uchar* // 将uchar*指向的数据按照int*解读 // 也就是说, uchar*是将二进制数据按照每8位换算成一个数(0-255) // 而int*是将二进制数据按照每32位换算成一个数 int* data2 reinterpret_castint*(d); for (int i0; i5; i){ cout to_string(i) of data2 data2[i] endl; if (i2){ data2[i] 333; } } // 由于我们是对指针指向的内存里的数据进行修改(data2[i] 333),因此改变会影响到外部 cout mat endl; for (int i0; i5; i){ cout to_string(i) of data data[i] endl; } 输出: 0 of data2 10 1 of data2 20 2 of data2 30 3 of data2 40 4 of data2 50 [10; 20; 333; 40; 50] 0 of data 10 1 of data 20 2 of data 333 3 of data 40 4 of data 502. 模板1. 场景1: 函数的实现写在.h文件中 // 以下内容需写在.h文件中 template typename Key, typename Value void eraseKey(std::mapKey, Value map, const Key key) { auto it map.find(key); if (it! map.end()) { map.erase(it); } } 2. 场景2函数的声明写在.h文件中,实现写在.cpp文件中 // 以下内容需写在.h文件中 template typename T vectorT scalingPts(const vectorT srcPts, int srcSize, int dstSize); // 以下内容需写在.cpp文件中 template vectorcv::Point scalingPtscv::Point(const vectorcv::Point,int,int); template vectorcv::Point2f scalingPtscv::Point2f(const vectorcv::Point2f,int,int); template vectorcv::Point2d scalingPtscv::Point2d(const vectorcv::Point2d,int,int); template typename T vectorT scalingPts(const vectorT srcPts, int srcSize, int dstSize) { vectorT dstPts; dstPts.reserve(srcPts.size()); if (std::is_same_vT, cv::Point2f){ // 注:std::is_same_v可以获悉传入模板数据类型是否为目标类型 float scale dstSize/float(srcSize); for (const cv::Point2f pt: srcPts){ float x pt.x*scale; float y pt.y*scale; dstPts.push_back(cv::Point2f(x,y)); } }else{ double scale dstSize/double(srcSize); if (std::is_same_vT, cv::Point){ for (const cv::Point pt: srcPts){ int x cvRound(pt.x*scale); int y cvRound(pt.y*scale); dstPts.push_back(cv::Point(x,y)); } }else{ for (const cv::Point2d pt: srcPts){ double x pt.x*scale; double y pt.y*scale; dstPts.push_back(cv::Point2d(x,y)); } } } return dstPts; }3. 头文件#include opencv2/opencv.hpp 这是一个整体的OpenCV头文件, 它包含了几乎所有OpenCV模块的声明包含了core、highgui和imgproc这三个模块的声明。 如果你只想使用OpenCV的基本功能这个头文件足以满足需求。 #include opencv2/core/core.hpp 这个头文件包含了OpenCV核心模块的声明其中包括图像数据结构、矩阵操作、像素访问等基本功能。 如果你只需要使用这些基本功能可以只包含这个头文件。 #include opencv2/highgui/highgui.hpp 这个头文件包含了OpenCV的高级图形用户界面GUI模块的声明其中包括图像显示、窗口管理、鼠标和键盘事件处理等功能。 如果你需要在图形界面中显示图像或与用户交互需要包含这个头文件。 #include opencv2/imgproc/imgproc.hpp 这个头文件包含了OpenCV的图像处理模块的声明其中包括图像滤波、边缘检测、形态学操作等功能。 如果你需要进行图像处理操作需要包含这个头文件。4. 显示/标记5. 计时/报错/便捷小记录(变量)1. 计时 // #include time.h clock_t start,end; //定义clock_t变量 start clock(); //开始时间 end clock(); //结束时间 cout time double(end - start) / CLOCKS_PER_SEC s endl; 2. 获取无穷大极值 double/float std::numeric_limitsdouble::max(); std::numeric_limitsfloat::max(); * 获取一个大于0且最接近0的浮点数 std::numeric_limitsdouble::min() std::numeric_limitsfloat::min() 3. pair用法 pairint, double p1; p1.first 1; p1.second 2.5; p1 make_pair(1, 1.2); cout p1.first p1.second endl; 4. 打印/输出当前时间 C实现 void coutCurrentTime() { time_t now time(nullptr); // 获取当前时间的秒数 tm ltm; localtime_s(ltm, now); // 转换为本地时间 std::cout 1900 ltm.tm_year - std::setw(2) std::setfill(0) 1 ltm.tm_mon - std::setw(2) ltm.tm_mday std::setw(2) ltm.tm_hour : std::setw(2) ltm.tm_min : std::setw(2) ltm.tm_sec std::endl; } std::string getCurrentTime() { time_t now time(nullptr); // 获取当前时间的秒数 tm ltm; localtime_s(ltm, now); // 转换为本地时间 std::ostringstream oss; oss 1900 ltm.tm_year - std::setw(2) std::setfill(0) 1 ltm.tm_mon - std::setw(2) ltm.tm_mday std::setw(2) ltm.tm_hour : std::setw(2) ltm.tm_min : std::setw(2) ltm.tm_sec; std::string outputString oss.str(); return outputString; } 5. 打印/输出当前时间 Qt实现 #include QDateTime QString getCurrentTime() { QDateTime currentTime QDateTime::currentDateTime(); return currentTime.toString(yyyy-MM-dd hh:mm:ss.zzz); } 报错, 输出错误信息 cerr sssssssss endl; 红色字符 cout At File: __FILE__ endl; cout At Function: __FUNCTION__ endl; cout At Line: __LINE__ endl; cout Wrong at File: __FILE__ endl; cout Wrong at Function: __FUNCTION__ endl; cout Wrong at Line: __LINE__ endl; 报错方案 CV_Assert(src.rows 1 src.type() CV_8UC1); CV_Assert(T 0); CV_Assert 会在运行时检查给定的条件表达式 - 如果条件为真程序继续正常执行 - 如果条件为假它会抛出一个 cv::Exception 异常其中包含错误信息
C++小技巧汇总(更新)
容器处理 vector, map等1. 容器转换0. 在有序容器中给cv::Point排序 struct ComparePoints { bool operator()(const cv::Point lhs, const cv::Point rhs) const { if (lhs.x rhs.x) { return true; } else if (lhs.x rhs.x lhs.y rhs.y) { return true; } else { return false; } } }; 1. vectorcv::Point 转换到 setcv::Point, ComparePoints std::setcv::Point, ComparePoints pointSet(points.begin(), points.end()); 2. vectorcv::Point 和 vectorPoint2f 的快速互相转化 - vectorPoint2f -- vectorcv::Point (四舍五入) vectorPoint2f vd{ { 1.1, 2.2 }, { 3.3, 4.4 }, {5.5, 6.6} }; vectorPoint v(vd.begin(), vd.end()); - vectorcv::Point -- vectorPoint2f vectorPoint vP{ { 1, 2 }, { 3, 4 }, {5, 6} }; vectorPoint2f v(vP.begin(), vP.end()); 3. [vectorcv::Point系列] -- [cv::Mat] | vectorT | MatType | Mat.cols | |:-------------------:|:--------:|:--------:| | vectorcv::Point | CV_32SC1 | 2 | | vectorcv::Point3i | CV_32SC1 | 3 | | vectorcv::Point2f | CV_32FC1 | 2 | | vectorcv::Point3f | CV_32FC1 | 3 | | vectorcv::Point2d | CV_64FC1 | 2 | | vectorcv::Point3d | CV_64FC1 | 3 | template typename T cv::Mat transVecPt2Mat(const vectorT ptlist, bool clonetrue) { if (ptlist.empty()) return Mat(); int col 0; int type 0; if (std::is_same_vT, cv::Point){ col 2; type CV_32SC1; }else if (std::is_same_vT, cv::Point3i){ col 3; type CV_32SC1; }else if (std::is_same_vT, cv::Point2f){ col 2; type CV_32FC1; }else if (std::is_same_vT, cv::Point3f){ col 3; type CV_32FC1; }else if (std::is_same_vT, cv::Point2d){ col 2; type CV_64FC1; }else if (std::is_same_vT, cv::Point3d){ col 3; type CV_64FC1; }else{ return cv::Mat(); } Mat mat(int(ptlist.size()), col, type, const_castvoid*(static_castconst void*(ptlist.data()))); if (clone){ return mat.clone(); }else{ return mat; } } 4. [cv::Mat] -- [vectorcv::Point系列] | MatType | Mat.cols | vectorT | |:--------:|:--------:|:-------------------:| | CV_32SC1 | 2 | vectorcv::Point | | CV_32SC1 | 3 | vectorcv::Point3i | | CV_32FC1 | 2 | vectorcv::Point2f | | CV_32FC1 | 3 | vectorcv::Point3f | | CV_64FC1 | 2 | vectorcv::Point2d | | CV_64FC1 | 3 | vectorcv::Point3d | template typename T void transMat2VecPt(const cv::Mat mat, vectorT ptlist) { ptlist.clear(); if (mat.empty()){ return; } if ((mat.cols!2)(mat.cols!3)){ return; } int type mat.type(); if (typeCV_32SC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point){ return; } }else{ if (!std::is_same_vT, cv::Point3i){ return; } } }else if (typeCV_32FC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point2f){ return; } }else{ if (!std::is_same_vT, cv::Point3f){ return; } } }else if (typeCV_64FC1){ if (mat.cols2){ if (!std::is_same_vT, cv::Point2d){ return; } }else{ if (!std::is_same_vT, cv::Point3d){ return; } } }else{ return; } ptlist.resize(size_t(mat.rows)); cv::Mat tmp mat.reshape(mat.cols, mat.rows); tmp.copyTo(cv::Mat(ptlist, false)); return; }2. vector1. cv::convexHull() 凸包 函数原型: void cv::convexHull( InputArray points, // 输入点集 OutputArray hull, // 输出的凸包 bool clockwise false, // 是否按顺时针方向输出凸包点 bool returnPoints true // 是否返回凸包点的坐标 ) - 输入点集 vectorcv::Point - 输出点集 vectorcv::Point - 功能: 输入的若干点集中找到n个点其围成的多边形能囊括住所有输入点 - 备注: 输出的点是有顺序的 std::vectorcv::Point2i hullPtsOut; cv::convexHull(std::vectorcv::Point2i{lumenKeyPtsPre.begin(), lumenKeyPtsPre.end()}, hullPtsOut); 2. 计算轮廓中心/重心 vectorcv::Point2f lumenPts; cv::Moments moments cv::moments(lumenPts); cv::Point2f center(float(moments.m10 / moments.m00), float(moments.m01 / moments.m00)); 3. 快速计算中位数 vectorint collist{ 2,3, 1, 2, 9, 10, 111, 8,1, 4, 5, 5, 5, 5, 5, 5, 5, 5,5,5,5,5,5,5,5,5,5,5,5,1,3,4,90 }; size_t mid collist.size() / 2; std::nth_element(collist.begin(), collist.begin() int(mid), collist.end()); cout collist[mid] endl; 4. 对一个结构体组成的vector进行排序 ## 方法一: 自定义比较函数 bool compare(int a, int b) { return a b; // 如果 a b返回 true用于降序排序 } std::sort(vec.begin(), vec.end(), compare); ## 方法二: Lambda 表达式 std::sort(vec.begin(), vec.end(), [](int a, int b) { return a b; // 用于降序排序 }); 5. 循环索引, 防越界 对一组数据按照某个索引取值时担心索引越界, 并希望越界的索引能从头开始取时, 可以这样 假设一组数据 vector... data 的元素个数 (即data.size()) 是 N 那么对于某个索引 idx (idx0), 如果希望 idx N 时能够循环地从data的起点开始取值 则 实际索引值 idx_real idx % N 例如, 元素一共有 5 个 (N5) 则 对于 idx 0, idx_real 0 则 对于 idx 4, idx_real 4 则 对于 idx 5, idx_real 0 则 对于 idx 6, idx_real 1 以此类推3. set1. set插入cv::Point std::setcv::Point, ComparePoints pointSet; pointSet.insert(cv::Point(1, 2)); pointSet.insert(cv::Point(3, 4)); 2. 返回一个set的首个元素 cv::Point firstPoint *pointSet.begin();4. map1. 两个map合并 (1) map2 合并到 map1中, 有重复key不覆盖, 且map2中被merge到map1的元素会被移除 (最高效方案) map1.merge(map2); (2) map2 合并到 map1中, 有重复key不覆盖, 且map2中的元素不会受影响 map1.insert(map2.begin(), map2.end()); (3) map2 合并到 map1中, 有重复key覆盖, 且map2中的元素不会受影响 for (const auto pair : map2) { map1[pair.first] pair.second; }矩阵处理 cv::Mat1. cv::Mat - vector1. 二值图提取轮廓 cv::findContours返回的轮廓点是有序的,轮廓之间是无序的 std::vectorstd::vectorcv::Point contours; cv::findContours(binaryImage, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE); cv::findContours(sqrImg, contours, hierarchy, cv::RETR_CCOMP, CHAIN_APPROX_NONE); - 当使用上述函数时, contours中每一个contour都可以表示成std::vectorcv::Point, cv::Point之间是有序的,12点方向,逆时针转,但contour和contour之间彼此是无序的 - cv::RETR_EXTERNAL: 只提取外轮廓 - cv::RETR_CCOMP: 内外轮廓都提取,含父轮廓子轮廓关系 - cv::CHAIN_APPROX_NONE: 全部轮廓点 - cv::CHAIN_APPROX_SIMPLE: 多边形关键轮廓点 2. 二值图非零点坐标提取 cv::findNonZero void cv::findNonZero(const cv::Mat mask, vectorcv::Point idxlist); - mask: 必须是单通道, 但数据类型可以是CV_8UC1,CV_32SC1,CV_64FC1 - idxlist: 输出结果,非零点坐标,cv::Point.x非零点列坐标,cv::Point.x非零点行坐标2. cv::Mat 处理0. 矩阵初始化 cv::Mat mat (cv::Mat_float(5, 3) 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f); cv::Mat mat(512,512,CV_8UC1,cv::Scalar(255)); 1. 形态学操作 * 生成核 // 矩形核 cv::Mat element cv::getStructuringElement(cv::MORPH_RECT, cv::Size(15, 15)); // 圆形核 cv::Mat element cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(15, 15)); * 膨胀 cv::dilate(image, out, element); * 侵蚀 cv::erode(image, out, element) * 闭操作 cv::morphologyEx(srcImage, dstImage, cv::MORPH_CLOSE, element); * 开操作 * cv::morphologyEx(srcImage, dstImage, cv::MORPH_OPEN, element); * 翻转 InputArray src:要处理的原始图像 OutputArray dst:是和src具有相同大小、类型的目标图像 int flipCode:旋转类型 flipCode 0x轴方向旋转 上下翻转 flipCode 0y轴方向旋转 左右翻转 flipCode 0x轴y轴方向同时旋转 void cv::flip(InputArray src,OutputArray dst,int flipCode); * 旋转-特殊角度 ROTATE_180, ROTATE_90_CLOCKWISE ROTATE_90_COUNTERCLOCKWISE void cv::rotate(InputArray src, OutputArray dst, int rotateCode); * 对角线翻转 cv::transpose(maskM, maskM); 2. 查看cv::Mat数据类型 | 数据类型 | C1 (单通道) | C3(三通道) | C4(四通道) | | :------: | : ------- : | : ------ : | : ------ : | | CV_8U (uchar) | 0 | 16 | 24 | | CV_32S (int) | 4 | 20 | 28 | | CV_32F (float) | 5 | 21 | 29 | | CV_64F (double)| 6 | 22 | 30 | 详见: https://blog.csdn.net/m0_58709899/article/details/160217850?spm1001.2014.3001.5501 3. 填充多边形 cv::Mat showMat cv::Mat::zeros(oct.idp.points * 2, oct.idp.points * 2, CV_8UC1); * 方法1: cv::fillPoly(showMat , std::vectorcv::Point2i{tempState.curvePts.begin(), tempState.curvePts.end()}, 255); * 方法2 (一次填充多个多边形): cv::drawContours(showMat, contours, -1, 255, cv::FILLED); 4. 连通域相关 连通域问题 cv::Mat labels, stats, centroids; int nccomps cv::connectedComponentsWithStats(biImg, labels, stats, centroids); labels数据类型: CV_32SC1 (int) stats数据类型: CV_32SC1 (int) centroids数据类型: CV_64FC1 (double) int x stats.atint(i, 0); int y stats.atint(i, 1); int w stats.atint(i, 2); int h stats.atint(i, 3); int s stats.atint(i, 4); * 找到最大连通域 stats.atint(0, 4) -1; cv::Point componentMaxLoc; cv::minMaxLoc(stats.col(4), nullptr, nullptr, nullptr, componentMaxLoc); cv::Mat bigestCpn cv:: Mat::zeros(biImg.rows, biImg.cols, CV_8UC1); bigestCpn.setTo(255, labels componentMaxLoc.y); 5. 改变矩阵形状/维度 reshape函数 * 注意reshape的第一参数表示通道数(0表示通道数不变) 第二个参数表示行数 没有参数表示列数 cv::Mat mat cv::Mat::ones(10, 10, CV_32FC1) int newChannels 1; int newRows 5; mat mat.reshape(newChannels, newRows); // 【注意reshape的第一参数表示通道数 第二个参数表示行数 没有参数表示列数】 6. 矩阵按行/列计算平均值/最大值/最小值/求和 src输入矩阵 (注意:输入矩阵的数据类型不能是int型即不能是CV_32S !!!) dst输出矩阵 dim: 维度: dim0,矩阵被处理成一行(y方向/纵向进行计算); dim1, 矩阵被处理成一列(x方向/横向进行计算); dim-1时维数将根据输出向量的大小自动选择 rtype: ------- cv::REDUCE_SUM 输出是矩阵的所有行/列的和 ------- cv::REDUCE_AVG 输出是矩阵的所有行/列的平均向量 ------- cv::REDUCE_MAX 输出是矩阵的所有行/列的最大值 ------- cv::REDUCE_MIN 输出是矩阵的所有行/列的最小值 dtype: 设置输出矩阵的数据类型,默认等于输入矩阵数据类型 (推荐: 32F or 64F) void cv::reduce(InputArray src, OutputArray dst, int dim, int rtype, int dtype -1); 示例 矩阵按行取平均,并将结果投影到显示矩阵screen上 cv::Mat screen(sqrImgMap_preProced.begin()-second.rows, int(sqrImgMap_preProced.size()), CV_32FC1); for (const auto ele : sqrImgMap_preProced) { cv::reduce(ele.second, screen.col(ele.first), 1, cv::REDUCE_AVG, CV_32FC1); } 7. 多个单通道矩阵压缩成一个多通道矩阵 和 一个多通道矩阵分拆成多个单通道矩阵 * 多个单通道 -- 一个多通道矩阵 vectorcv::Mat vec_mat_list; // vector内包含了n个单通道矩阵; 矩阵的尺寸和数据类型必须相同 cv::Mat mergedMat(m, n, CV_32FC(n)); // 初始化一个空的多通道矩阵: 尺寸、通道数、数据类型必须和vector包含的矩阵一致 cv::merge(vec_mat_list, mergedMat); // vector -- cv::Mat * 一个多通道矩阵 -- 多个单通道 vec_mat_list.clear(); cv::split(mergedMat, vec_mat_list); // cv::Mat -- vector 8. 矩阵的横向拼接(等高) 和 矩阵的纵向拼接(等宽) cv::hconcat(...) // 横向拼接 cv::vconcat(...) // 纵向拼接 9. 从多通道矩阵中抽出某个通道的矩阵 cv::Mat matChns; // 输入:多通道矩阵 cv::Mat matObj; // 输出:抽取得到的单通道矩阵 int n 1; // 抽取第1个通道 cv::extractChannel(matChns, matObj, n); 10. 三通道矩阵访问/赋值 cv::Vec3b对应由uchar组成的三通道矩阵 cv::Vec3i对应由int组成的三通道矩阵 cv::Vec3f对应由float组成的三通道矩阵 cv::Vec3d对应由double组成的三通道矩阵 for (int y 0; y image.rows; y) { for (int x 0; x image.cols; x) { cv::Vec3b intensity image.atcv::Vec3b(y, x); for (int c 0; c image.channels(); c) { // 处理/访问每个intensity[c]; // 对应第y行第x列第c层 } } } 11. 矩阵的位操作 注意: 位操作可以与Mask掩膜搭配使用,确保只在局部区域进行位操作 cv::bitwise_and() 与 cv::bitwise_or() 或 cv::bitwise_not() 非 cv::bitwise_xor() 异或 12. 矩阵的拷贝、截取、复制粘贴 详见: https://blog.csdn.net/m0_58709899/article/details/146070176?spm1001.2014.3001.5502 13. 自动二值化算法 - 大津 二值化 double thres cv::threshold(img, img_Thr_O, 0, 255, THRESH_BINARY | THRESH_OTSU); - 三角 二值化 double thres cv::threshold(img, img_Thr_O, 0, 255, THRESH_BINARY | THRESH_TRIANGLE); - 自适应 二值化 -- int adaptiveMethod: ADAPTIVE_THRESH_GAUSSIAN_C, ADAPTIVE_THRESH_MEAN_C -- int blockSize: 奇数, 对某个位置计算二值化阈值时所选取的观察窗的尺寸 -- 参考: https://blog.csdn.net/weixin_42272768/article/details/110817275 cv::adaptiveThreshold(Rou, binary2, 255, adaptiveMethod, THRESH_BINARY, blockSize, 0); 14. 将矩阵沿着x轴或/和y轴重复堆叠 cv::repeat(src, ny, nx) * e.g. src为1行N列的矩阵时, 设置nx1, nyM, 则src沿y轴堆叠,生成矩阵M行N列 * e.g. src为N行1列的矩阵时, 设置nxM, ny1, 则src沿x轴堆叠,生成矩阵N行M列 15. 矩阵的push_back() 可以通过push_back()函数对一个矩阵添加新的行 (只能是行) cv::Mat mat; mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*1); mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*2); mat.push_back(cv::Mat::ones(1,5,CV_8UC1)*3); cout mat endl endl; 16. 同尺寸的两个矩阵图像逐像素比较,输出掩膜 cv::compare void compare(const cv::Mat src1, const cv::Mat src2, cv::Mat dst, int cmpop); src1和src2逐元素比较,若符合cmpop对应的比较标准则对输出掩膜dst相应位置置为255,否则置0 要求: - src1和src2必须type相同,size相同 - src1和src2可以不是CV_8U, 可以是CV_32S或者CV_32F甚至CV_64F - dst为掩膜,类型为CV_8U, 且必为二值化结果 0-255 - cmpop参数表 枚举常量 英文全称 中文含义 判断逻辑 CMP_GE Greater or Equal 大于等于 src1(i,j) ≥ src2 CMP_GT Greater Than 大于 src1(i,j) src2 CMP_LE Less or Equal 小于等于 src1(i,j) ≤ src2 CMP_LT Less Than 小于 src1(i,j) src2 CMP_EQ Equal 等于 src1(i,j) src2 CMP_NE Not Equal 不等于 src1(i,j) ≠ src2 17. 根据某灰度范围, 基于图像矩阵生成一个掩膜 cv::inRange void cv::inRange(const cv::Mat src, cv::Scalar lowerb, cv::Scalar upperb, cv::Mat dstMask); - src 可以为单通道或三通道 - lowerb 下限, 灰度大于等于该值的像素会被接受 - upperb 上线, 灰度小于等于该值的像素会被接受 - dstMask 输出掩膜, CV_8UC1 - 示例: cv::inRange(grayMat, cv::Scalar(20), cv::Scalar(200), resMat); 18. 基于输入矩形框,分割出其中的前景,生成相应的掩膜 cv::grabCut void cv::grabCut(const cv::Mat colorImg, cv::Mat mask, cv::Rect rect, cv::Mat bgdModel, cv::Mat fgdModel, int iterCount, int modecv::GC_INIT_WITH_RECT); - 函数功能: 在输入彩色图像上,参考给定的矩形框rect (必须确保rect完全包裹住前景), 函数对图像进行GMM智能分割, 给出掩膜: 前景,背景,疑似前景,疑似背景 - colorImg: 输入图像,必须为三通道 CV_8UC3 - mask: 输出掩膜, CV_8UC1, cv::Size等于colorImg, 值域存在四个值: 0 (GC_BGD): 背景 1 (GC_FGD): 前景 2 (GC_PR_BGD): 疑似背景 3 (GC_PR_FGD): 疑似前景 - rect: 矩形框,必须确保rect完全包裹住前景,即被分割对象 - bgdModel, fgdModel: 函数内部迭代使用,不必理会,传入空两个矩阵即可 - iterCount: 迭代次数, 一般3-5次, 但我自测1次即可 - mode: 模式, 默认cv::GC_INIT_WITH_RECT, 即使用矩形框初始化 * 该函数实测: - 代码: cv::Mat imgC cv::imread(, cv::IMREAD_COLOR); cv::Rect roi(220,218,74,75); cv::Mat mask; cv::Mat bgdModel; cv::Mat fgdModel; CC_BFs::Timer timer; grabCut(imgC, mask, roi, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT); mask.setTo(255, maskGC_FGD); mask.setTo(100, maskGC_PR_FGD); mask.setTo(200, maskGC_PR_FGD); - 执行效率 512尺寸饼图,矩形框边长75,迭代1次耗时: 114ms 512尺寸饼图,矩形框边长75,迭代3次耗时: 262ms - 效果 从mask上看还是有一定效果的, 但几乎分割不出纯前景(GC_FGD), 因此实际使用时应该: (1) 把 GC_PR_FGD 和 GC_FGD 都视为分割结果的前景部分 (置255) (2) 把 GC_BGD 和 GC_PR_BGD 都视为分割结果的背景部分 (GC_PR_BGD置0, GC_BGD本就是0) 19. 使用指针来访问cv::Mat矩阵中的元素 float* datamat.ptrfloat(i) 无论矩阵是否连续(mat.isContinuous()true)都可以使用以下方式访问: cv::Mat mat cv::Mat::ones(M,N,CV_64FC1); int rows mat.rows; int cols mat.cols; for (int i 0; i rows; i) { double* row_ptr mat.ptrdouble(i); // 获取矩阵第i行数据的指针 for (int j 0; j cols; j) { double val row_ptr[j]; // row_ptr[j] 等价于 mat.atdouble(i,j) } } * 代码等价: mat.atdouble(i,j) 等价于 mat.ptrdouble(i)[j] * 但后者的速度优势只在大量连续访问/修改矩阵内存数据时才会体现,只是取一个值at就足够了 * 无论矩阵是否连续,矩阵的每一行数据都是连续的 * 矩阵若经历了转置/切片等操作,往往不再连续,也就是说矩阵的行与行之间内存并不连续 * 如果矩阵是三通道的,则应按照如下方式访问 cv::Mat mat cv::Mat::ones(M,N,CV_8UC3); int rows mat.rows; int cols mat.cols; for (int i 0; i rows; i) { uchar* row_ptr mat.ptruchar(i); for (int j 0; j cols; j) { uchar B row_ptr[j * 3 0]; uchar G row_ptr[j * 3 1]; uchar R row_ptr[j * 3 2]; } } 20. 构造矩阵时用到的 static_cast..., const_cast... 以及 void* # static_castxxx(data): 数据类型转换, 将data的原数据类型转换为xxx数据类型,但不能消除const属性 - 例1 int data 0; double data2 static_castdouble(data); - 例2 const vectorPoint pts {....}; // 已赋值 const void* ptsPtr static_castconst void*(pts.data()); // 注:pts.data() 数据类型为cv::Point* # const_castxxx(data): 强制消除data数据的const属性 - 例1 const int* data; // 已赋值 int* dataPtr const_castint*(data); - 例2 const void* ptsPtr; // 已赋值 void* ptsPtr2 const_castvoid* ptsPtr; # 要解决的问题: 已知数据并构造cv::Mat矩阵时,后者的构造方式如下: cv::Mat(int rows, int cols, int type, void* data); 【注】通过data写入函数的是纯二进制数据,具体读入几个字节以及矩阵如何理解数据完全取决于参数rows,cols,type 最后参数传入的应该是void* 但是, 我们也可以传入int*, float*, double*, uchar*, 因为函数内会自发地将其转换为void*, 其只是数据地址的记录 当然,我们也可以使用static_castvoid*在外部手动转化(纯粹多余) 但是, 无论如何, cv::Mat构造时最后参数传入的不能有const属性, 因此我们必须使用 const_castxxx 将其const属性消除掉 - 例1 对于数据 const int* data, 应该: cv::Mat(rows, cols, CV_32SC1, const_castint*(data)); - 例2 对于数据 const vectorPoint pts, 应该: cv::Mat(rows, cols, CV_32SC1, const_castcv::Point*(pts.data())); # 进阶, 【通用解决手段】 - 对于任何const的数据类型的【指针】 const Type dataPtr 都可以使用如下方式 const_castvoid*(static_castconst void*(dataPtr))将其传入矩阵构造函数, 即 cv::Mat(rows, cols, type, const_castvoid*(static_castconst void*(dataPtr))); - 对于任何非const的数据类型的【指针】 Type dataPtr 都可以使用如下方式 static_castvoid*(dataPtr)将其传入矩阵构造函数, 即 cv::Mat(rows, cols, type, static_castvoid*(dataPtr)); - 更多详见: 其他--函数说明备注参数计算1. 对点的运算cv::Point1. 判断多边形与点之间的关系 double cv::pointPolygonTest(const cv::InputArray contour, cv::Point2f pt, bool measureDist) * 参数说明 - contour输入的多边形可以是一个 std::vectorcv::Point,std::vectorcv::Point2f 或者 cv::Mat 类型的数组。 注如果输入的是cv::Mat, 那么必须确保cv::Mat是一个N行2列的矩阵2行N列不可以, 且应该为CV_32FC1(推荐)或CV_32SC1或CV_64FC1 - pt待判断的点类型为 cv::Point2f。 - measureDist是否计算点到多边形的距离默认为 false。 * 返回值说明 如果 measureDist 为 false返回值表示点与多边形之间的关系 --大于 0 表示点在多边形内部 --等于 0 表示点在多边形的边界上 --小于 0 表示点在多边形外部。 如果 measureDist 为 true返回值表示点到多边形的最短距离。2. 对数字的处理1. 基于已知上下界对数字范围进行限制钳位 std::clamp 逻辑: 输出 std::clamp(输入, 下限, 上限) - 若输入在上下限界内(闭区间), 则输出输入 - 若输入小于下限, 则输出下限 - 若输入大于上限, 则输出上限 cout std::clamp(0.0, 0.1, 0.8) endl; // 0.1 cout std::clamp(0.5, 0.1, 0.8) endl; // 0.5 cout std::clamp(0.8, 0.1, 0.8) endl; // 0.8 cout std::clamp(1.0, 0.1, 0.8) endl; // 0.8其他1. 函数说明备注1. 画直线 cv::line cv::line(image, pt1, pt2, cv::Scalar(0, 0, 255), thickness); thickness 指的是线的半宽包含线的中心 因此线无论thickness的奇偶都是对称的 2. 右移,左移,位操作, (,,) a N 即对一个变量的二进制结果数字右移N位, 移除的低N位弃之不用, 高位补0 a N 即对一个变量的二进制结果数字左移N位并造成升位, 如uchar类型左移会变成int类型, 高位低位都补0 配和位操作且“”, 可以取出特定位数段上的结果 * 应用: 四个uchar值转 和 一个int值 互相转换, 即用一个int值表示四个uchar值 // uchar值1个字节, sizeof(uchar) 1 // int值4个字节, sizeof(int) 4 * uchar 转 int, u1-低八位; u4-高八位 void transUchar2Int(uchar u1, uchar u2, uchar u3, uchar u4, int data) { data (u424) | (u316) | (u28) | u1; } * int 转 uchar, u1-低八位; u4-高八位 备注: 十六进制数F对应4位, 因此 0xFF 表示取低八位 void transInt2Uchar(int data, uchar u1, uchar u2, uchar u3, uchar u4) { u1 data 0xFF; u2 (data 8) 0xFF; u3 (data 16) 0xFF; u4 (data 24) 0xFF; } 3. 数组, int* data, int data[] * 数组的初始化: 栈初始化, 堆初始化 - 栈初始化, 特点: 不必手动释放 可以计算数组长度 可以在初始化时赋值 int data[] {10,20,30,40,50}; // 栈初始化一个数组 data[1] 99; // 对数组进行改值 for (int i0; i5; i){ cout data of i data[i] endl; // 读出数组结果 } int len sizeof(data) / sizeof(int); // 计算数组长度 - 堆初始化, 特点: 必须手动释放 不可以计算数组长度(因为其本质是一个指针) 不可以在初始化时赋值 int* data new int[5]; // 堆初始化一个数组 data[0] 10; // 对数组进行赋值 data[1] 20; data[2] 30; data[3] 40; data[4] 50; for (int i0; i5; i){ cout data of i data[i] endl; // 读出数组结果 } if (data!nullptr){ delete[] data; datanullptr; } - 共同点: 无论是哪种初始化方式, 当数组传入一个函数时都会变成一个指针; 另一方面,当我们想构建一个函数来接收数组时, 接收的也只能是指针; 此外由于退化成指针, 无论哪种初始化的数组都不能在函数内部再计算尺寸了, 需要外界传入数组尺寸或约定好尺寸; void calData(int* data, int Size) { for (int i0; iSize; i){ cout data[i] endl; } } 两种初始化方式的数组在传入上述函数时,方式是一样的: calData(data,5); // int data[] {10,20,30,40,50}; calData(data,5); // int* data new int[5]; 4. 借尸还魂法 QByteArray -- int* reinterpret_castint*( ... ) // 隐式转换 数据没变 只是改变了被读方式 * 需求场景: 我的数据容器是QByteArray, 但函数的输出参数是int* (1) 定义一个尺寸大小数据类型确定的变量: QByteArray byteArrayData; byteArrayData.reserve(rows*cols*channels); (2) 比如某个函数的传出参数要求数据类型是int* 而我们刚刚定义的是 QByteArray (3) 此时可以将byteArrayData按照如下方式传入,可以借尸还魂,取到结果放在byteArrayData中: void myFunction(int* result); // 函数声明 myFunction(reinterpret_castint*(byteArrayData.data())); // 函数调用 5. unsigned char* 和 uchar* 是等价的 6. 构建函数时, 将另一个函数作为参数传入 int multiply(int a, int b) { return a * b; } void compute(int x, int y, std::functionint(int, int) func) { // 使用 std::function 接收函数对象 std::cout Result: func(x, y) std::endl; } 7. static_cast..., const_cast... 说明 static_cast...: 数据类型转换(无论是否为指针), 但不改变const属性 const_cast... : 去除const属性(无论是否为指针), 但不能改变数据类型 - 例1 int data1 0; double data2 static_castdouble(data1); - 例2 const int data1 0; const double data2 static_castconst double(data1); double data3 const_castdouble(data2); - 例3 int* data1 new int[5]; void* data2 static_castvoid*(data1); - 例4 const int* data1 new int[5]; const void* data2 static_castconst void*(data1); void* data3 const_castvoid*(data2); - 例5 const int data1[] {10,20,30,40,50}; // 注: data1[]是数组, data1是指针 const void* data2 static_castconst void*(data1); void* data3 const_castvoid*(data2); 8. reinterpret_cast... 说明 对于数据(指针数据),按照指定的方式解读 比如对于一个uchar数组 内存中的二进制数据: 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 0 1 0 0 1 0 1 1 0 1 1 1 1 1 0 0 0 按uchar*解读 : |----uchar----|----uchar----|----uchar----|----uchar----| 按int*解读 : |-----------------------------int-----------------------------| 因此,reinterpret_cast...只是改变了数据的解读方式, 比如有的函数要求传入的是int*, 但我们的变量是uchar*, 这时可以使用该方法将函数外uchar*变量按照int*解读再传入进函数 而函数内对int*这个指针指向的内存会进行数据修改,以此来影响函数外uchar*指向的结果 例: int data[] {10,20,30,40,50}; // 注: data[]是数组, data是指针 cv::Mat mat(5,1,CV_32SC1, data); uchar* d mat.data; // 注: 无论cv::Mat是什么类型,.data返回的数据指针都是uchar* // 将uchar*指向的数据按照int*解读 // 也就是说, uchar*是将二进制数据按照每8位换算成一个数(0-255) // 而int*是将二进制数据按照每32位换算成一个数 int* data2 reinterpret_castint*(d); for (int i0; i5; i){ cout to_string(i) of data2 data2[i] endl; if (i2){ data2[i] 333; } } // 由于我们是对指针指向的内存里的数据进行修改(data2[i] 333),因此改变会影响到外部 cout mat endl; for (int i0; i5; i){ cout to_string(i) of data data[i] endl; } 输出: 0 of data2 10 1 of data2 20 2 of data2 30 3 of data2 40 4 of data2 50 [10; 20; 333; 40; 50] 0 of data 10 1 of data 20 2 of data 333 3 of data 40 4 of data 502. 模板1. 场景1: 函数的实现写在.h文件中 // 以下内容需写在.h文件中 template typename Key, typename Value void eraseKey(std::mapKey, Value map, const Key key) { auto it map.find(key); if (it! map.end()) { map.erase(it); } } 2. 场景2函数的声明写在.h文件中,实现写在.cpp文件中 // 以下内容需写在.h文件中 template typename T vectorT scalingPts(const vectorT srcPts, int srcSize, int dstSize); // 以下内容需写在.cpp文件中 template vectorcv::Point scalingPtscv::Point(const vectorcv::Point,int,int); template vectorcv::Point2f scalingPtscv::Point2f(const vectorcv::Point2f,int,int); template vectorcv::Point2d scalingPtscv::Point2d(const vectorcv::Point2d,int,int); template typename T vectorT scalingPts(const vectorT srcPts, int srcSize, int dstSize) { vectorT dstPts; dstPts.reserve(srcPts.size()); if (std::is_same_vT, cv::Point2f){ // 注:std::is_same_v可以获悉传入模板数据类型是否为目标类型 float scale dstSize/float(srcSize); for (const cv::Point2f pt: srcPts){ float x pt.x*scale; float y pt.y*scale; dstPts.push_back(cv::Point2f(x,y)); } }else{ double scale dstSize/double(srcSize); if (std::is_same_vT, cv::Point){ for (const cv::Point pt: srcPts){ int x cvRound(pt.x*scale); int y cvRound(pt.y*scale); dstPts.push_back(cv::Point(x,y)); } }else{ for (const cv::Point2d pt: srcPts){ double x pt.x*scale; double y pt.y*scale; dstPts.push_back(cv::Point2d(x,y)); } } } return dstPts; }3. 头文件#include opencv2/opencv.hpp 这是一个整体的OpenCV头文件, 它包含了几乎所有OpenCV模块的声明包含了core、highgui和imgproc这三个模块的声明。 如果你只想使用OpenCV的基本功能这个头文件足以满足需求。 #include opencv2/core/core.hpp 这个头文件包含了OpenCV核心模块的声明其中包括图像数据结构、矩阵操作、像素访问等基本功能。 如果你只需要使用这些基本功能可以只包含这个头文件。 #include opencv2/highgui/highgui.hpp 这个头文件包含了OpenCV的高级图形用户界面GUI模块的声明其中包括图像显示、窗口管理、鼠标和键盘事件处理等功能。 如果你需要在图形界面中显示图像或与用户交互需要包含这个头文件。 #include opencv2/imgproc/imgproc.hpp 这个头文件包含了OpenCV的图像处理模块的声明其中包括图像滤波、边缘检测、形态学操作等功能。 如果你需要进行图像处理操作需要包含这个头文件。4. 显示/标记5. 计时/报错/便捷小记录(变量)1. 计时 // #include time.h clock_t start,end; //定义clock_t变量 start clock(); //开始时间 end clock(); //结束时间 cout time double(end - start) / CLOCKS_PER_SEC s endl; 2. 获取无穷大极值 double/float std::numeric_limitsdouble::max(); std::numeric_limitsfloat::max(); * 获取一个大于0且最接近0的浮点数 std::numeric_limitsdouble::min() std::numeric_limitsfloat::min() 3. pair用法 pairint, double p1; p1.first 1; p1.second 2.5; p1 make_pair(1, 1.2); cout p1.first p1.second endl; 4. 打印/输出当前时间 C实现 void coutCurrentTime() { time_t now time(nullptr); // 获取当前时间的秒数 tm ltm; localtime_s(ltm, now); // 转换为本地时间 std::cout 1900 ltm.tm_year - std::setw(2) std::setfill(0) 1 ltm.tm_mon - std::setw(2) ltm.tm_mday std::setw(2) ltm.tm_hour : std::setw(2) ltm.tm_min : std::setw(2) ltm.tm_sec std::endl; } std::string getCurrentTime() { time_t now time(nullptr); // 获取当前时间的秒数 tm ltm; localtime_s(ltm, now); // 转换为本地时间 std::ostringstream oss; oss 1900 ltm.tm_year - std::setw(2) std::setfill(0) 1 ltm.tm_mon - std::setw(2) ltm.tm_mday std::setw(2) ltm.tm_hour : std::setw(2) ltm.tm_min : std::setw(2) ltm.tm_sec; std::string outputString oss.str(); return outputString; } 5. 打印/输出当前时间 Qt实现 #include QDateTime QString getCurrentTime() { QDateTime currentTime QDateTime::currentDateTime(); return currentTime.toString(yyyy-MM-dd hh:mm:ss.zzz); } 报错, 输出错误信息 cerr sssssssss endl; 红色字符 cout At File: __FILE__ endl; cout At Function: __FUNCTION__ endl; cout At Line: __LINE__ endl; cout Wrong at File: __FILE__ endl; cout Wrong at Function: __FUNCTION__ endl; cout Wrong at Line: __LINE__ endl; 报错方案 CV_Assert(src.rows 1 src.type() CV_8UC1); CV_Assert(T 0); CV_Assert 会在运行时检查给定的条件表达式 - 如果条件为真程序继续正常执行 - 如果条件为假它会抛出一个 cv::Exception 异常其中包含错误信息