【18】OpenCV C++实战篇——【项目实战】OpenCV C++ 精准定位“十字刻度尺”中心坐标,过滤图片中的干扰,精准获取十字交点坐标

文章目录

  • [1 问题及分析](#1 问题及分析)
  • [2 多尺度霍夫直线 与 渐进概率霍夫线段 细节对比](#2 多尺度霍夫直线 与 渐进概率霍夫线段 细节对比)
    • [2.1 多尺度霍夫直线 HoughLines](#2.1 多尺度霍夫直线 HoughLines)
    • [2.2 渐进概率霍夫线段 HoughLinesP](#2.2 渐进概率霍夫线段 HoughLinesP)
    • [2.3 HoughLines 和 HoughLinesP 所求结果细节对比](#2.3 HoughLines 和 HoughLinesP 所求结果细节对比)
    • [2.4 为什么 HoughLinesP 直线两端没有呈放射状态呢?直线总是平行吗?](#2.4 为什么 HoughLinesP 直线两端没有呈放射状态呢?直线总是平行吗?)
    • [2.5 HoughLines 直线角度,输出有顺序?](#2.5 HoughLines 直线角度,输出有顺序?)
  • [3 获取十字交点坐标](#3 获取十字交点坐标)
    • [3.1 找到两直线](#3.1 找到两直线)
      • [3.1.1 获得 HoughLines 横竖两条线](#3.1.1 获得 HoughLines 横竖两条线)
      • [3.1.2 获得HoughLinesP 横竖两条线](#3.1.2 获得HoughLinesP 横竖两条线)
    • [3.2 计算交点坐标原理 及实现](#3.2 计算交点坐标原理 及实现)
  • [4 项目源码](#4 项目源码)
    • [4.1 .h文件](#4.1 .h文件)
    • [4.2 .cpp文件](#4.2 .cpp文件)
    • [4.3 main文件](#4.3 main文件)
  • [5 将 以上两种封装成一个函数------新增方法切换,转换大图坐标等功能](#5 将 以上两种封装成一个函数——新增方法切换,转换大图坐标等功能)

1 问题及分析

该问题,是由实际工业项目中玻璃出来的,实验详细记录分析,便于日后查看;

(虽然使用的知识点简单,但要把简单的知识点 用于解决好 实际工业问题 还是要做很多优化与改进的)

问题:
2000万像素的工业相机 ,要对拍摄的画面中"十字刻度尺 "精准定位 (改刻度尺在原图中占比很小);

原图很大,下面截取需要定位的ROI区域如下图 。

难点:图像较暗,有红色黄色干扰,刻度尺上有记号笔涂抹干扰,图像细节模糊;

定位结果

思路 :

  • 先对原图灰度化,
  • 再进行反色使刻度尺部分凸显出来;
  • 二值化,找直线,直线筛选,求两线交点。


2 多尺度霍夫直线 与 渐进概率霍夫线段 细节对比

2.1 多尺度霍夫直线 HoughLines

csharp 复制代码
int getCrossScale_HoughL(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow)
{
	cv::Mat grayImg, binaryImg, grayImg2;
	cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
	//反色
	grayImg2 = 255 - grayImg;
	cv::threshold(grayImg2, binaryImg, 200, 255, THRESH_BINARY);

	vector<Vec2f> lines;//极坐标(r,theta)

	HoughLines(binaryImg, lines, 1, CV_PI / 180, 260, 0, 0);//累加器阈值threshold,越大线条越少,反之越多

	drawHoughLine(srcImg, lines,Scalar(0,0,255),1);
	//getLineCross(srcImg, lines, crossPoint, isShow);

	if (isShow)
	{
		cv::imshow("灰度图", grayImg);
		cv::imshow("反色", grayImg2);
		cv::imshow("二值化", binaryImg);

		cv::imshow("getCrossScale_HoughL", srcImg);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return 0;
}

横竖各找到3条线,

倾斜较为明显,线段两端呈放射状;

再看中间交点 横向占2个像素,竖向占3个像素

2.2 渐进概率霍夫线段 HoughLinesP

csharp 复制代码
int getCrossScale_HoughLP(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow)
{
	cv::Mat grayImg, binaryImg, grayImg2;
	cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
	//反色
	grayImg2 = 255 - grayImg;
	cv::threshold(grayImg2, binaryImg, 200, 255, THRESH_BINARY);

	//利用渐进概率式霍夫变换提取 直线段
	vector<Vec4i> linesP;//每条线有四个参数,分别是选段两端点坐标(x1,y1,x2,y2)
	HoughLinesP(binaryImg, linesP, 1, CV_PI / 180, 260, srcImg.cols * 0.6, 5);  //两个点连接最大距离10,CV_PI / 180表示1度对应的弧度

	for (size_t i = 0; i < linesP.size(); i++)
	{
		line(srcImg, Point(linesP[i][0], linesP[i][1]), Point(linesP[i][2], linesP[i][3]), Scalar(255,0,0), 1);
		//测试坐标点
		//cout << "linesP1[" << i << "][0]" << linesP1[i][0] << "," << "linesP1[" << i << "][1]" << linesP1[i][1] << endl;
	}

	//getLineCross(srcImg, lines, crossPoint, isShow);

	if (isShow)
	{
		cv::imshow("灰度图", grayImg);
		cv::imshow("反色", grayImg2);
		cv::imshow("二值化", binaryImg);

		cv::imshow("getCrossScale_HoughLP", srcImg);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return 0;
}

横向找到3条线,竖向找到1条线;

横向线段 3条线非常贴合,看似平行;(线段两端没有呈放射状;)

再看中间交点 横向占3个像素,竖向占1个像素

2.3 HoughLines 和 HoughLinesP 所求结果细节对比

对比两函数参数,发现HoughLinesP 似乎比 HoughLines 有更多的约束条件;
HoughLinesP 多了最先线段长度minLineLength ,最小间隙maxLineGap

csharp 复制代码
void HoughLines( InputArray image, OutputArray lines,
                              double rho, double theta, int threshold,
                              double srn = 0, double stn = 0,
                              double min_theta = 0, double max_theta = CV_PI );
csharp 复制代码
void HoughLinesP( InputArray image, OutputArray lines,
                               double rho, double theta, int threshold,
                               double minLineLength = 0, double maxLineGap = 0 );
  • 在使用上的区别,HoughLinesP设定 最小那线段长度为 图片高度的0.6倍 ;两线之间最小间隙为5
  • 此外在使用上HoughLinesP,似乎更方便
    • HoughLines, 每条线返回2个参数,(r,theta);
    • HoughLinesP,每条线返回4个参数,分别是选段两端点坐标(x1,y1,x2,y2);
csharp 复制代码
	vector<Vec2f> lines;////每条线有2个参数,极坐标(r,theta)
	HoughLines(binaryImg, lines, 1, CV_PI / 180, 260, 0, 0);//累加器阈值threshold,越大线条越少,反之越多

	//利用渐进概率式霍夫变换提取 直线段
	vector<Vec4i> linesP;//每条线有四个参数,分别是选段两端点坐标(x1,y1,x2,y2)
	HoughLinesP(binaryImg, linesP, 1, CV_PI / 180, 260, srcImg.cols * 0.6, 5);  //两个点连接最大距离10,CV_PI / 180表示1度对应的弧度

红色 HoughLines :横竖各找到3条线;线段两端呈放射状;
蓝色 HoughLinesP :横向找到3条线,竖向找到1条线;横向线段 3条线非常贴合,看似平行,线段两端没有呈放射状;

红色 HoughLines : 再看中间交点 横向占2个像素,竖向占3个像素;
蓝色 HoughLinesP :再看中间交点 横向占3个像素,竖向占1个像素

对比来看,HoughLinesP 效果更好一下;

2.4 为什么 HoughLinesP 直线两端没有呈放射状态呢?直线总是平行吗?

在观察发现 HoughLinesP ,返回的直线vector linesP是 int型,而HoughLines 返回的直线是flaot类型;

难道是HoughLinesP 直线精度不高,稍微的歪斜,放射状没表现出来吗?

csharp 复制代码
	vector<Vec2f> lines;////每条线有2个参数,极坐标(r,theta)
	HoughLines(binaryImg, lines, 1, CV_PI / 180, 260, 0, 0);//累加器阈值threshold,越大线条越少,反之越多

	//利用渐进概率式霍夫变换提取 直线段
	vector<Vec4i> linesP;//每条线有四个参数,分别是选段两端点坐标(x1,y1,x2,y2)
	HoughLinesP(binaryImg, linesP, 1, CV_PI / 180, 260, srcImg.cols * 0.6, 5);  //两个点连接最大距离10,CV_PI / 180表示1度对应的弧度

HoughLinesP的直线类型 vector<Vec4i> linesP;改为float类型vector<Vec4f> linesP;

更换倾斜的十字测试

发现,线集是平行的,没有放射状;

csharp 复制代码
linesP.size() = 5
[262, 390, 319, 28]
[107, 215, 475, 215]
[108, 216, 474, 216]
[260, 391, 318, 23]
[107, 214, 474, 214]

更换倾斜的十字测试

发现,线集是平行的,还是没有放射状;

csharp 复制代码
linesP.size() = 16
[25, 193, 392, 206]
[203, 365, 215, 20]
[196, 363, 208, 17]
[200, 365, 212, 20]
[206, 365, 218, 14]
[202, 365, 214, 19]
[204, 365, 217, 15]
[26, 192, 397, 205]
[26, 195, 387, 207]
[198, 364, 210, 18]
[26, 190, 398, 203]
[27, 7, 286, 48]
[26, 196, 385, 209]
[62, 190, 398, 202]
[197, 365, 208, 51]
[22, 198, 385, 211]

2.5 HoughLines 直线角度,输出有顺序?

一依此绘制出HoughLines的每一条直线,发现还是有规律的;

如下图所示:

第一条直线 倾斜 9度,竖向;(OpenCV里HoughLines的角度应该是与Y轴的夹角)

第二条直线 倾斜90度,横向;

依次竖向,横向,竖向,横向。。。循环输出所有直线 (输出顺序似乎是碰巧)

csharp 复制代码
lines 0[319, 0.15708]   角度 9
lines 1[215, 1.5708]    角度 90
lines 2[315, 0.139626]  角度 8
lines 3[221, 1.55334]   角度 89
lines 4[323, 0.174533]  角度 10
lines 5[211, 1.58825]   角度 91

输出顺序似乎是碰巧,再换一个图像试试;

这次就么那么规律了,横向和竖向的线 "群居";

csharp 复制代码
lines 0[192, 1.6057]    角度 92
lines 1[196, 1.58825]   角度 91
lines 2[189, 1.62316]   角度 93
lines 3[209, 0.0174533] 角度 1
lines 4[185, 1.64061]   角度 94
lines 5[206, 0] 角度 0
lines 6[215, 0.0349066] 角度 2
lines 7[213, 0.0349066] 角度 2
lines 8[216, 0.0523599] 角度 3
lines 9[218, 0.0523599] 角度 3
lines 10[219, 0.0698132]        角度 4
lines 11[222, 0.0698132]        角度 4
lines 12[224, 0.0872665]        角度 5
lines 13[199, 1.5708]   角度 90


3 获取十字交点坐标

前面只是获得了 横竖直线,视觉上交点找到了,但交点坐标是多少?还不知道

思路:横竖方向各选取一条直线,然后求两线交点坐标;

两线交点坐标函数如下,为了方便,每条线各取两个点带入;

csharp 复制代码
bool get2linesIntersectionPoint3(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD, cv::Point2f& crossPoint);

3.1 找到两直线

3.1.1 获得 HoughLines 横竖两条线

注意 : HoughLines 水平线90度,竖直线0度;

OpenCV里HoughLines的角度应该是与Y轴的夹角;

思路:

输入HoughLines直线lines,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来;

csharp 复制代码
bool getPointOn2Line(Mat& img, vector<Vec2f> lines, vector<cv::Point2f>& ptsOnLine,bool isShow)
{
	vector<Vec2f> linesHV;
	float rho, theta;
	bool linesH = false, linesV = false;

	//找水平、垂直,两条线 
	//水平线90度,竖直线0度;OpenCV里HoughLines的角度应该是与Y轴的夹角
	for (size_t i = 0; i < lines.size(); i++)
	{
		theta = lines[i][1];  //直线过坐标原点垂线与x轴夹角

		if (!linesH && (theta > CV_PI / 2 - 0.2 && theta < CV_PI / 2 + 0.2))//横线theta接近π/2,±δ,
		{
			linesHV.push_back(lines[i]);
			linesH = true;
		}
		else if (!linesV && ((theta > -0.2 && theta < 0.2) || theta > CV_PI - 0.2 && theta < CV_PI + 0.2))//竖线theta接近π或0,±δ
		{
			linesHV.push_back(lines[i]);
			linesV = true;
		}

		//横线竖线都找到了,跳出循环
		if (linesH && linesV)
		{
			break;
		}
	}

	//求两条直线上的四个点
	if (linesHV.size() == 2)
	{
		drawHoughLine(img, linesHV, ptsOnLine, Scalar(0, 0, 255), 1, isShow);
	}

	else
	{
		cout << "没找到两条直线" << endl;
		return false;
	}

	cv::Rect rect;
	return true;
}

交点坐标为(248.8,215.0)

3.1.2 获得HoughLinesP 横竖两条线

思路:

输入HoughLinesP直线linesP,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来

注意:

因为HoughLinesP线集是平行的,当同一方向有多条线时,随便选一条会导致中心不准,
当同一方向有多条线时,端点坐标取均值;

csharp 复制代码
//输入HoughLinesP直线linesP,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来
bool getPointOn2LineP(Mat& img, vector<Vec4i> linesP, vector<cv::Point2f>& ptsOnLine, bool isShow)
{
	float rho, theta;
	Point2f pt1, pt2;
	double angle;
	Point2f pts1_H(0,0), pts2_H(0, 0), pts1_V(0, 0), pts2_V(0, 0);
	int h = 0, v = 0;

	//找水平、垂直,两条线
	//因为HoughLinesP线集是平行的,当同一方向有多条线时,随便选一条会导致中心不准,
	//当同一方向有多条线端点坐标取均值;
	for (size_t i = 0; i < linesP.size(); i++)
	{
		pt1.x = linesP[i][0];
		pt1.y = linesP[i][1];
		pt2.x = linesP[i][2];
		pt2.y = linesP[i][3];

		//linesP的每条直线与y轴的夹角
		angle = getLineAngle(pt1, pt2, Point2f(0,0), Point2f(0,img.rows));
		
		//水平线90度,竖直线0度;;;直线与Y轴的夹角(与HoughLines的夹角保持一致)
		if (angle > 70 && angle < 110)//如果横线90°±20°
		{
			pts1_H.x += pt1.x;
			pts1_H.y += pt1.y;
			pts2_H.x += pt2.x;
			pts2_H.y += pt2.y;
			h++;
		}

		else if (angle > -20 && angle < 20)//如果竖线0°±20°
		{
			pts1_V.x += pt1.x;
			pts1_V.y += pt1.y;
			pts2_V.x += pt2.x;
			pts2_V.y += pt2.y;
			v++;
		}
	}

	pts1_H.x /= h;
	pts1_H.y /= h;
	pts2_H.x /= h;
	pts2_H.y /= h;

	pts1_V.x /= v;
	pts1_V.y /= v;
	pts2_V.x /= v;
	pts2_V.y /= v;

	ptsOnLine.push_back(pts1_H);
	ptsOnLine.push_back(pts2_H);
	ptsOnLine.push_back(pts1_V);
	ptsOnLine.push_back(pts2_V);


	line(img, pts1_H, pts2_H, Scalar(255, 0, 0), 1);
	line(img, pts1_V, pts2_V, Scalar(255, 0, 0), 1);

	if(isShow)
	{
		cv::imshow("img", img);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return true;
}

交点坐标为(249.1,215.0)

3.2 计算交点坐标原理 及实现

csharp 复制代码
////****************************************************************************************
//  求二条直线的交点的公式
//  有如下方程 (x-x1)/(y-y1) = (x2-x1)/(y2-y1) ==> a1*x+b1*y=c1
//             (x-x3)/(y-y3) = (x4-x3)/(y4-y3) ==> a2*x+b2*y=c2
//  则交点为
//          x= D1/D =| c1 b1|  / | a1 b1 |      y= D2/D= | a1 c1| / | a1 b1 |  //当两条线平行或重合时,分母为零
//                   | c2 b2|  / | a2 b2 |               | a2 c2| / | a2 b2 |
// 
// 注:D是其次ax+by=0,的行列式,Di是把第i列换成等式右边的列(常数),i是所求未知数所在的列,如x在第一列,D1 
//
//   a1= y2-y1
//   b1= x1-x2
//   c1= x1*y2-x2*y1,这里 a1*x+b1*y=c1,和Ax+By+C=0,的C是一个负号关系
//   a2= y4-y3
//   b2= x3-x4
//   c2= x3*y4-x4*y3
// 
////****************************************************************************************
//// 
////行列式法,x= D1/D, y= D2/D,求两直线的交点, //适合任意情况(斜率存在,不存在)
bool get2linesIntersectionPoint(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD, cv::Point2f& crossPoint)
{
	float a1 = pointB.y - pointA.y;
	float b1 = pointA.x - pointB.x;
	float c1 = pointA.x * pointB.y - pointB.x * pointA.y;//这里 a1*x+b1*y=c1,和Ax+By+C=0,的C是一个负号关系

	float a2 = pointD.y - pointC.y;
	float b2 = pointC.x - pointD.x;
	float c2 = pointC.x * pointD.y - pointD.x * pointC.y;

	float det = a1 * b2 - a2 * b1;

	// 直线平行:A1/A2=B1/B2≠C1/C2 (A2B2C2≠0); 重合:A1/A2=B1/B2=C1/C2(A2B2C2≠0)
	// 直线平行:A1B2=A2B1; 重合:A1B2=A2B1=A1C2

	if (det == 0) return false;//平行或重合

	//Now this is cross point of lines
	crossPoint.x = (b2 * c1 - b1 * c2) / det;//这里和公式法(ABC)也是负号关系
	crossPoint.y = (a1 * c2 - a2 * c1) / det;

	return true;
}

由上面两种方式获取的直线,分别带入交点坐标函数,求得交点如图:

红色 HoughLines : 交点坐标为(248.8,215.0)
蓝色 HoughLinesP :交点坐标为(249.1,215.0)

二者横坐标,相差0.3像素,纵坐标相等,

该工业相机为2000万像素,可见精度还是很高的

比较细节,看看哪个更准?

比较发现,由于刻度尺上端被黑色的记号笔涂抹记号,对直线检测结果有一定影响
但比较细节,蓝色受影响更小一些; 如果想要更加精准,ROI范围可以再小一些,把记号笔涂抹部分去掉;


4 项目源码

4.1 .h文件

csharp 复制代码
#pragma once
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

//获得黑十字刻度尺 交点坐标(转换为在B2大ROI中的位置),显示在srcImgBigROI上
int getCrossScale_HoughL(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow);
int getCrossScale_HoughLP(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow);

bool getPointOn2Line( Mat& img, vector<Vec2f> lines, vector<cv::Point2f>& ptsOnLine, bool isShow);
bool getPointOn2LineP(Mat& img, vector<Vec4i> linesP, vector<cv::Point2f>& ptsOnLine, bool isShow);


//返回直线上两坐标点,isDraw设置是否绘制直线
void drawHoughLine(Mat& img, vector<Vec2f> lines, vector<cv::Point2f> &ptsOnLine, const Scalar& color, int thickness, bool isDraw);//要标记直线的图像,检测的直线数据


bool get2linesIntersectionPoint(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD, cv::Point2f& crossPoint);
//已知每条直线的两个点求夹角,,返回角度
double getLineAngle(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD);

4.2 .cpp文件

csharp 复制代码
#include "getCross.h"

int getCrossScale_HoughL(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow)
{
	cv::Mat grayImg, binaryImg, grayImg2;
	cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
	//反色
	grayImg2 = 255 - grayImg;
	cv::threshold(grayImg2, binaryImg, 200, 255, THRESH_BINARY);

	vector<Vec2f> lines;////每条线有2个参数,极坐标(r,theta)
	HoughLines(binaryImg, lines, 1, CV_PI / 180, 260, 0, 0);//累加器阈值threshold,越大线条越少,反之越多

	vector<cv::Point2f> ptsOnLine;
	//获得两直线
	getPointOn2Line(srcImg, lines, ptsOnLine, false);
	//获得两直线交点
	get2linesIntersectionPoint(ptsOnLine[0], ptsOnLine[1], ptsOnLine[2], ptsOnLine[3], crossPoint);

	if (isShow)
	{
		//绘制交点坐标
		char buf[50];
		memset(buf, '\0', 50);
		sprintf_s(buf, "X = %.1f; Y = %.1f;", crossPoint.x, crossPoint.y);
		int font_face = FONT_HERSHEY_COMPLEX;
		double font_scale = 0.5;
		int thickness = 1;
		putText(srcImg, buf, Point(crossPoint.x + 20, crossPoint.y - 20), font_face, font_scale, Scalar(0, 0, 255), thickness, 8, 0);

		imshow("十字刻度尺 交点:", srcImg);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return 0;
}


int getCrossScale_HoughLP(cv::Mat srcImg, cv::Point2f& crossPoint, bool isShow)
{
	cv::Mat grayImg, binaryImg, grayImg2;
	cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
	//反色
	grayImg2 = 255 - grayImg;
	cv::threshold(grayImg2, binaryImg, 200, 255, THRESH_BINARY);

	//利用渐进概率式霍夫变换提取 直线段
	vector<Vec4i> linesP;//每条线有四个参数,分别是选段两端点坐标(x1,y1,x2,y2)
	HoughLinesP(binaryImg, linesP, 1, CV_PI / 180, 260, srcImg.cols * 0.6, 5);  //两个点连接最大距离10,CV_PI / 180表示1度对应的弧度


	////绘制所有线段查看效果
	//cout << "linesP.size() = " << linesP.size() << endl;
	//for (size_t i = 0; i < linesP.size(); i++)
	//{
	//	//linesP1[i][0]第i条线段的x坐标、linesP1[i][1]第i条线段的y坐标
	//	line(srcImg, Point(linesP[i][0], linesP[i][1]), Point(linesP[i][2], linesP[i][3]), Scalar(255,0,0), 1);
	//	cout << linesP[i]<< endl;//测试坐标点
	//}

	vector<cv::Point2f> ptsOnLine;
	//获得两直线
	getPointOn2LineP(srcImg, linesP, ptsOnLine, false);
	//获得两直线交点
	get2linesIntersectionPoint(ptsOnLine[0], ptsOnLine[1], ptsOnLine[2], ptsOnLine[3], crossPoint);
	
	if (isShow)
	{
		//绘制交点坐标
		char buf[50];
		memset(buf, '\0', 50);
		sprintf_s(buf, "X = %.1f; Y = %.1f;", crossPoint.x, crossPoint.y);
		int font_face = FONT_HERSHEY_COMPLEX;
		double font_scale = 0.5;
		int thickness = 1;
		putText(srcImg, buf, Point(crossPoint.x + 20, crossPoint.y - 20), font_face, font_scale, Scalar(0, 0, 255), thickness, 8, 0);

		imshow("十字刻度尺 交点:", srcImg);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return 0;
}

//输入HoughLines直线lines,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来
bool getPointOn2Line(Mat& img, vector<Vec2f> lines, vector<cv::Point2f>& ptsOnLine,bool isShow)
{
	vector<Vec2f> linesHV;
	float rho, theta;
	bool linesH = false, linesV = false;

	//找水平、垂直,两条线 
	//水平线90度,竖直线0度;OpenCV里HoughLines的角度应该是与Y轴的夹角
	for (size_t i = 0; i < lines.size(); i++)
	{
		theta = lines[i][1];  //直线过坐标原点垂线与x轴夹角

		if (!linesH && (theta > CV_PI / 2 - 0.2 && theta < CV_PI / 2 + 0.2))//横线theta接近π/2,±δ,
		{
			linesHV.push_back(lines[i]);
			linesH = true;
		}
		else if (!linesV && ((theta > -0.2 && theta < 0.2) || theta > CV_PI - 0.2 && theta < CV_PI + 0.2))//竖线theta接近π或0,±δ
		{
			linesHV.push_back(lines[i]);
			linesV = true;
		}

		//横线竖线都找到了,跳出循环
		if (linesH && linesV)
		{
			break;
		}
	}

	//求两条直线上的四个点
	if (linesHV.size() == 2)
	{
		drawHoughLine(img, linesHV, ptsOnLine, Scalar(0, 0, 255), 1, isShow);
	}

	else
	{
		cout << "没找到两条直线" << endl;
		return false;
	}

	cv::Rect rect;
	return true;
}

//输入HoughLinesP直线linesP,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来
bool getPointOn2LineP(Mat& img, vector<Vec4i> linesP, vector<cv::Point2f>& ptsOnLine, bool isShow)
{
	float rho, theta;
	Point2f pt1, pt2;
	double angle;
	Point2f pts1_H(0,0), pts2_H(0, 0), pts1_V(0, 0), pts2_V(0, 0);
	int h = 0, v = 0;

	//找水平、垂直,两条线
	//因为HoughLinesP线集是平行的,当同一方向有多条线时,随便选一条会导致中心不准,
	//当同一方向有多条线端点坐标取均值;
	for (size_t i = 0; i < linesP.size(); i++)
	{
		pt1.x = linesP[i][0];
		pt1.y = linesP[i][1];
		pt2.x = linesP[i][2];
		pt2.y = linesP[i][3];

		//linesP的每条直线与y轴的夹角
		angle = getLineAngle(pt1, pt2, Point2f(0,0), Point2f(0,img.rows));
		
		//水平线90度,竖直线0度;;;直线与Y轴的夹角(与HoughLines的夹角保持一致)
		if (angle > 70 && angle < 110)//如果横线90°±20°
		{
			pts1_H.x += pt1.x;
			pts1_H.y += pt1.y;
			pts2_H.x += pt2.x;
			pts2_H.y += pt2.y;
			h++;
		}

		else if (angle > -20 && angle < 20)//如果竖线0°±20°
		{
			pts1_V.x += pt1.x;
			pts1_V.y += pt1.y;
			pts2_V.x += pt2.x;
			pts2_V.y += pt2.y;
			v++;
		}
	}

	pts1_H.x /= h;
	pts1_H.y /= h;
	pts2_H.x /= h;
	pts2_H.y /= h;

	pts1_V.x /= v;
	pts1_V.y /= v;
	pts2_V.x /= v;
	pts2_V.y /= v;

	ptsOnLine.push_back(pts1_H);
	ptsOnLine.push_back(pts2_H);
	ptsOnLine.push_back(pts1_V);
	ptsOnLine.push_back(pts2_V);


	line(img, pts1_H, pts2_H, Scalar(255, 0, 0), 1);
	line(img, pts1_V, pts2_V, Scalar(255, 0, 0), 1);

	if(isShow)
	{
		cv::imshow("img", img);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return true;
}


////****************************************************************************************
//  求二条直线的交点的公式
//  有如下方程 (x-x1)/(y-y1) = (x2-x1)/(y2-y1) ==> a1*x+b1*y=c1
//             (x-x3)/(y-y3) = (x4-x3)/(y4-y3) ==> a2*x+b2*y=c2
//  则交点为
//          x= D1/D =| c1 b1|  / | a1 b1 |      y= D2/D= | a1 c1| / | a1 b1 |  //当两条线平行或重合时,分母为零
//                   | c2 b2|  / | a2 b2 |               | a2 c2| / | a2 b2 |
// 
// 注:D是其次ax+by=0,的行列式,Di是把第i列换成等式右边的列(常数),i是所求未知数所在的列,如x在第一列,D1 
//
//   a1= y2-y1
//   b1= x1-x2
//   c1= x1*y2-x2*y1,这里 a1*x+b1*y=c1,和Ax+By+C=0,的C是一个负号关系
//   a2= y4-y3
//   b2= x3-x4
//   c2= x3*y4-x4*y3
// 
////****************************************************************************************
//// 
////行列式法,x= D1/D, y= D2/D,求两直线的交点, //适合任意情况(斜率存在,不存在)
bool get2linesIntersectionPoint(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD, cv::Point2f& crossPoint)
{
	float a1 = pointB.y - pointA.y;
	float b1 = pointA.x - pointB.x;
	float c1 = pointA.x * pointB.y - pointB.x * pointA.y;//这里 a1*x+b1*y=c1,和Ax+By+C=0,的C是一个负号关系

	float a2 = pointD.y - pointC.y;
	float b2 = pointC.x - pointD.x;
	float c2 = pointC.x * pointD.y - pointD.x * pointC.y;

	float det = a1 * b2 - a2 * b1;

	// 直线平行:A1/A2=B1/B2≠C1/C2 (A2B2C2≠0); 重合:A1/A2=B1/B2=C1/C2(A2B2C2≠0)
	// 直线平行:A1B2=A2B1; 重合:A1B2=A2B1=A1C2

	if (det == 0) return false;//平行或重合

	//Now this is cross point of lines
	crossPoint.x = (b2 * c1 - b1 * c2) / det;//这里和公式法(ABC)也是负号关系
	crossPoint.y = (a1 * c2 - a2 * c1) / det;

	return true;
}

//*********************************************************
//求两直线的夹角

//已知两点坐标求向量:A(a1, b1), B(a2, b2, ), 则向量AB为:B点坐标减A点坐标,即:向量AB = (a2-a1, b2-b1);
//已知两向量坐标,求两向量夹角:设两个向量分别为a = (x1,y1), b = (x2, y2),其夹角为α,因为ab = |a||b| cosα,所以cosα = ab/|a||b|= (x1x2+y1y2) / (根号(x1^2 + y1^2)根号(x2^2 + y2^2));
//两向量内积:已知两向量a = [a1, a2, ..., an]和b = [b1, b2, ..., bn]的点积定义为:内积就是点积 a·b=a1b1+a2b2+......+anbn;
//向量的模,即向量的长度,设向量a = (x, y),则向量a的模 = 根号(x方 + y方)

//夹角为α = arccos(∑(xiyi) / sqrt((∑(xixi)∑(yiyi)))
//cosα = 两个向量的内积 / 向量的模("长度")的乘积

//*********************************************************

//已知每条直线的两个点求夹角
double getLineAngle(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD) 
{
	//向量AB,CD
	auto v1 = pointB - pointA;
	auto v2 = pointD - pointC;

	//向量AB,CD的模
	double n1 = cv::norm(v1);
	double n2 = cv::norm(v2);

	//cosα = ab/|a||b|
	double cosv = (v1.x * v2.x + v1.y * v2.y) / n1 / n2;
	double angle_rad = acos(cosv);

	//弧度转角度
	return angle_rad * 180 / CV_PI;
}

//返回直线上两坐标点,有时只需要两个点的坐标,并不需要绘制显示直线
void drawHoughLine(Mat& img, vector<Vec2f> lines, vector<cv::Point2f> &ptsOnLine ,const Scalar& color, int thickness,bool isShow )//要标记直线的图像,检测的直线数据
{
	double length = max(img.rows, img.cols);  //图像高宽的最大值
	Point2f pt1, pt2;
	float rho, theta;
	double a, b, x0, y0;

	for (size_t i = 0; i < lines.size(); i++)
	{
		rho = lines[i][0];    //直线距离坐标原点的距离
		theta = lines[i][1];  //直线过坐标原点垂线与x轴夹角

		a = cos(theta);  //夹角的余弦值
		b = sin(theta);  //夹角的正弦值
		//x = r*cos(θ),y = r*sin(θ)
		x0 = a * rho, y0 = b * rho;  //直线与过坐标原点的垂线的交点

		//计算直线上的一点
		pt1.x = x0 + length * (-b);
		pt1.y = y0 + length * (a);
		//计算直线上另一点
		pt2.x = x0 - length * (-b);
		pt2.y = y0 - length * (a);

		////若想获得整数点,可用cvRound()四舍五入;
		//pt1.x = cvRound(x0 + length * (-b));//返回跟参数最接近的整数值,即四舍五入;
		//pt1.y = cvRound(y0 + length * (a));
		//pt2.x = cvRound(x0 - length * (-b));
		//pt2.y = cvRound(y0 - length * (a));

		//两点绘制一条直线
		line(img, pt1, pt2, color, thickness);
		if (isShow)
		{
			cout << "lines " << i << lines[i] << "\t角度 " << lines[i][1] * 180 / CV_PI << endl;

			cv::imshow("img", img);
			cv::waitKey(0);
			destroyAllWindows();
		}

		ptsOnLine.push_back(pt1);
		ptsOnLine.push_back(pt2);	
	}
}

4.3 main文件

csharp 复制代码
#include "getCross.h"

void main()
{
	char imgPath[] = "D:\\C_test\\images\\8.bmp";
	cv::Mat srcImg = imread(imgPath);
	cv::Point2f crossPoint;

	//getCrossScale_HoughL(srcImg, crossPoint, true);
	getCrossScale_HoughLP(srcImg, crossPoint, true);
}

5 将 以上两种封装成一个函数------新增方法切换,转换大图坐标等功能

获得黑十字刻度尺交点坐标,有HoughLP、HoughL两种方法可选用,(ptsROIltop可将交点转换为大图中的位置,显示在ROI所在的图像上)

csharp 复制代码
bool getLineCrossPoint2f(cv::Mat srcImg, Mat ImgDraw, string getLineMethod , Point ptsROIltop, cv::Point2f& crossPoint, bool isShow)

新增参数解释:

  • Mat ImgDraw,传入用来绘制线条等信息的图像(有时并不需要将线条绘制在处理的图像上);
  • string getLineMethod ,有HoughLP、HoughL两种方法可选用;
  • Point ptsROIltop 是ROI左上角坐标,用于将交点转换为大图中的位置,显示在ROI所在的图像上;
csharp 复制代码
// 获得黑十字刻度尺交点坐标,有HoughLP、HoughL两种方法可选用,(ptsROIltop可将交点转换为大图中的位置,显示在ROI所在的图像上)
bool getLine::getLineCrossPoint2f(cv::Mat srcImg, Mat ImgDraw, string getLineMethod , Point ptsROIltop, cv::Point2f& crossPoint, bool isShow)
{
	cv::Mat grayImg, binaryImg, grayImg2;
	cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
	//反色
	grayImg2 = 255 - grayImg;
	cv::threshold(grayImg2, binaryImg, 200, 255, THRESH_BINARY);

	vector<cv::Point2f> ptsOnLine;//获取直线上的两点

	if (getLineMethod == "HoughLP")
	{
		//利用渐进概率式霍夫变换提取 直线段
		vector<Vec4i> linesP;//每条线有四个参数,分别是选段两端点坐标(x1,y1,x2,y2)
		HoughLinesP(binaryImg, linesP, 1, CV_PI / 180, 260, srcImg.cols * 0.6, 5);  //两个点连接最大距离10,CV_PI / 180表示1度对应的弧度

		////绘制所有线段查看效果
		//drawHoughLineP(srcImg, linesP, Scalar(0, 0, 255), 1, isShow);

		//获得两相交直线
		get2CrossHoughLP(srcImg, linesP,ptsOnLine, false);
	}
	
	else if (getLineMethod == "HoughL")
	{
		vector<Vec2f> lines;////每条线有2个参数,极坐标(r,theta)
		HoughLines(binaryImg, lines, 1, CV_PI / 180, 260, 0, 0);//累加器阈值threshold,越大线条越少,反之越多
		//绘制所有直线查看效果
		//drawHoughLine(srcImg, lines,ptsOnLine, Scalar(0, 0, 255), 1, isShow);

		vector<cv::Point2f> ptsOnLine;
		//获得两相交直线,
		get2CrossHoughL(srcImg, lines,ptsOnLine, false);
	}
	if (ptsOnLine.size() == 4)
	{
		//将直线坐标点转换为大图中的位置,ptsROIltop是ROI左上角坐标, 直线显示在srcImgBigROI上
		//当ptsROIltop为(0,0)时,绘制在ROI图上;当为ROI左上角坐标时,直线绘制在ROI所在的大图上;
		ptsOnLine[0].x += ptsROIltop.x;  ptsOnLine[0].y += ptsROIltop.y;
		ptsOnLine[1].x += ptsROIltop.x;  ptsOnLine[1].y += ptsROIltop.y;
		ptsOnLine[2].x += ptsROIltop.x;  ptsOnLine[2].y += ptsROIltop.y;
		ptsOnLine[3].x += ptsROIltop.x;  ptsOnLine[3].y += ptsROIltop.y;

		//获得两直线交点
		get2linesIntersectionPoint3(ptsOnLine[0], ptsOnLine[1], ptsOnLine[2], ptsOnLine[3], crossPoint);
	}

	else
	{
		cout << "没找到两条直线" << endl;
		return false;
	}

	if (isShow)
	{	
		//绘制两交线
		line(ImgDraw, ptsOnLine[0], ptsOnLine[1], Scalar(255, 0, 0), 1);
		line(ImgDraw, ptsOnLine[2], ptsOnLine[3], Scalar(255, 0, 0), 1);

		//绘制交点坐标
		char buf[50];
		memset(buf, '\0', 50);
		sprintf_s(buf, "X = %.1f; Y = %.1f;", crossPoint.x, crossPoint.y);
		int font_face = FONT_HERSHEY_COMPLEX;
		double font_scale = 0.5;
		int thickness = 1;
		putText(ImgDraw, buf, Point(crossPoint.x + 20, crossPoint.y - 20), font_face, font_scale, Scalar(0, 0, 255), thickness, 8, 0);

		imshow("十字刻度尺 交点:", ImgDraw);
		cv::waitKey(0);
		destroyAllWindows();
	}
	return true;
}

绘制两种霍夫直线的函数

csharp 复制代码
//返回每条直线上两坐标点
void getLine::drawHoughLine(Mat& img, vector<Vec2f> lines,  vector<cv::Point2f>& ptsOnLine, const Scalar& color, int thickness, bool isShow)//要标记直线的图像,检测的直线数据
{
	double length = max(img.rows, img.cols);  //图像高宽的最大值
	Point2f pt1, pt2;
	float rho, theta;
	double a, b, x0, y0;

	for (size_t i = 0; i < lines.size(); i++)
	{
		rho = lines[i][0];    //直线距离坐标原点的距离
		theta = lines[i][1];  //直线过坐标原点垂线与x轴夹角

		a = cos(theta);  //夹角的余弦值
		b = sin(theta);  //夹角的正弦值
		//x = r*cos(θ),y = r*sin(θ)
		x0 = a * rho, y0 = b * rho;  //直线与过坐标原点的垂线的交点

		//计算直线上的一点
		pt1.x = x0 + length * (-b);
		pt1.y = y0 + length * (a) ;
		//计算直线上另一点
		pt2.x = x0 - length * (-b);
		pt2.y = y0 - length * (a);

		////若想获得整数点,可用cvRound()四舍五入;
		//pt1.x = cvRound(x0 + length * (-b));//返回跟参数最接近的整数值,即四舍五入;
		//pt1.y = cvRound(y0 + length * (a));
		//pt2.x = cvRound(x0 - length * (-b));
		//pt2.y = cvRound(y0 - length * (a));

	
		//实践发现:
		//V方向,y轴向上超出img约 -img.rows,H方向,x轴向左超出img约 -img.cols,
		//为了将绘制的直线 限制在img范围内,可将V方向,y轴上端 +img.rows,H方向,x轴左 +img.cols 
		//这样实际是不准的,因有倾斜,最准的方法用直线表达式,给定x,y,获取指定位置坐标;

		//绘制的直线超出img范围,以后在修改
		if (isShow)
		{
			//两点绘制一条直线
			line(img, pt1, pt2, color, thickness);

			cout << "lines " << i << lines[i] << "\t角度 " << lines[i][1] * 180 / CV_PI << endl;

			cv::imshow("img", img);
			cv::waitKey(0);
			destroyAllWindows();
		}
		ptsOnLine.push_back(pt1);
		ptsOnLine.push_back(pt2);
	}
}

void getLine::drawHoughLineP(Mat& img, vector<Vec4i> linesP, const Scalar& color, int thickness, bool isShow)//要标记直线的图像,检测的直线数据
{

	cout << "linesP.size() = " << linesP.size() << endl;
	for (size_t i = 0; i < linesP.size(); i++)
	{
		//linesP1[i][0]第i条线段的x坐标、linesP1[i][1]第i条线段的y坐标
		line(img, Point(linesP[i][0], linesP[i][1]), Point(linesP[i][2], linesP[i][3]), color, thickness);
		cout << linesP[i]<< endl;//测试坐标点
	}
	if (isShow)
	{			
		cv::imshow("img", img);
		cv::waitKey(0);
		destroyAllWindows();
	}
}

下面的三个函数,前面已经介绍过,函数内容没变,只是名字改了;

csharp 复制代码
//行列式法,x= D1/D, y= D2/D,求两直线的交点, //适合任意情况(斜率存在,不存在)
bool get2linesIntersectionPoint3(cv::Point2f pointA, cv::Point2f pointB, cv::Point2f pointC, cv::Point2f pointD, cv::Point2f& crossPoint);
//输入HoughLines直线lines,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来;
bool get2CrossHoughL( Mat& img, vector<Vec2f> lines,  vector<cv::Point2f>& ptsOnLine, bool isShow);
//输入HoughLinesP直线linesP,选取横竖两条直线,在选取的直线上各取两个点,由ptsOnLine带出来;
bool get2CrossHoughLP(Mat& img, vector<Vec4i> linesP,  vector<cv::Point2f>& ptsOnLine, bool isShow);