前言
前一篇《C OpenCV透视变换综合练习》中针对透视变换做了一个小练习,上篇中我们用多边形拟合的点集来计算离最小旋转矩形最近的点来定义为透视变换的点,效果是有,无意间又想了一个新的思路,在原来的点的基础上效果会更好一点,其中就用到了直线拟合的方法,今天这篇就说一下优化的思路及直线拟合的函数。
实现效果
放大图
直线拟合函数
微卡智享
代码语言:javascript复制 void cv::fitLine(
cv::InputArray points, // 二维点的数组或vector
cv::OutputArray line, // 输出直线,Vec4f (2d)或Vec6f (3d)的vector
int distType, // 距离类型,要使输入点到拟合直线的距离和最小化
double param, // 距离参数,一般设为0
double reps, // 径向的精度参数,通常情况下两个值均被设定为1e-2
double aeps // 角度精度参数
);
参数说明:
- points: 用于拟合直线的输入点集,可以是二维点的cv::Mat数组,也可以是二维点的STL vector。
- line: 输出的直线,对于二维直线而言类型为cv::Vec4f,对于三维直线类型则是cv::Vec6f,输出参数的前半部分给出的是直线的方向,而后半部分给出的是直线上的一点(即通常所说的点斜式直线)。
- distType: 距离类型,拟合直线时,要使输入点到拟合直线的距离和最小化(即下面公式中的cost最小化),可供选的距离类型如下表所示,ri表示的是输入的点到直线的距离。
CV_DIST_USER =-1, /* User defined distance */
CV_DIST_L1 =1, /* distance = |x1-x2| |y1-y2| */
CV_DIST_L2 =2, /* the simple euclidean distance */
CV_DIST_C =3, /* distance = max(|x1-x2|,|y1-y2|) */
CV_DIST_L12 =4, /* L1-L2 metric: distance = 2(sqrt(1 x*x/2) - 1)) */
CV_DIST_FAIR =5, /* distance = c^2(|x|/c-log(1 |x|/c)), c = 1.3998 */
CV_DIST_WELSCH =6, /* distance = c^2/2(1-exp(-(x/c)^2)), c = 2.9846 */
CV_DIST_HUBER =7 /* distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345 */
- param:距离参数,跟所选的距离类型有关,值可以设置为0,cv::fitLine()函数本身会自动选择最优化的值。
- reps:拟合直线所需要的径向精度,一般设置为0.01或1e-2。
- aeps:拟合直线所需要的角度精度,一般设置为0.01或1e-2。
实现思路
微卡智享
# | 步骤 |
---|---|
1 | 旋转矩形的点和上一步获取的最近点设置一个阈值距离,在距离内的都列入当前区域的直线拟合点,超过阈值的用最近点加上阈值重新算为计算点来进行拟合 |
2 | 根据不同区域计算直线拟合 |
3 | 求到的直线拟合点实现每两条求交点 |
4 | 得到的4个交点做为透视变换的坐标点 |
01
阈值范围内的直线拟合
先以左边区域为例,首先我们设定了一个距离为15的阈值,白色的是我们上一篇中求到的最近的点(点1和2),蓝色为最小旋转矩形的角点(点3和4),我们通过计算点1到点3的距离,还有点2到点4的距离都小于15,那我们把这4个点都列入到左边区域,用来计算直接拟合。
紫色线即为上面4个点采用直线拟合后的结果
左边的区域拟合直线,因为都在阈值内,所以拟合出的直线比原来只求最近点连起来的效果要更好一点。接下来我们看看超过阈值的处理。
02
超出阈值的直线拟合
上图中可以看到,右下的区域点在阈值范围内是无问题了,右上的旋转矩形角点(点4)与最近点(点2)距离挺远,肯定超出阈值了,如果还把点4也加入到拟合点计算的话,直线会多出来不少,所以我们就在根据(点2)的坐标,在X轴和Y轴都加上阈值的范围,计算出新的拟合点,即上图红圈标识的,用点1,点2,点3和红色拟合点来进行直线拟合,得到的效果如下:
03
每两条直线拟合求交点
直线拟合的函数,输出的参数line里面有说到了是Vec4f的类型,输出参数的前半部分给出的是直线的方向,而后半部分给出的是直线上的一点(即通常所说的点斜式直线)。
方程式:y-y1=k(x-x1)
其中(x1,y1)为坐标系上过直线的一点的坐标,k为该直线的斜率。
推导:若直线L1经过点P1(x1,y1),且斜率为k,求L1方程。
设点P(x,y)是直线上不同于点P1的任意一点,直线PP1的斜率应等于直线L1的斜率,根据经过两点的直线的斜率公式得k=(y-y1)/(x-x1) (且:x≠x1)
所以,直线L1:y-y1=k(x-x1)
说明:
(1)这个方程是由直线上一点和斜率确定的,这一点必须在直线上,否则点斜式方程不成立;
(2)当直线l的倾斜角为0°时,直线方程为y=y1;
(3)当直线倾斜角为90°时,直线没有斜率,它的方程不能用点斜式表示,这时直线方程为x=x1。
我们直线拟合的得到的4个Vec4f就需要每两个求交点最后得到上图中红圈的1,2,3,4的4个交点。根据斜率和点的计算
kk1*x b1=kk2*x b2
可以得到:x=(b2-b1)/(kk1-kk2)
进而得到:y=(kk1*(b2-b1)/(kk1-kk2)) b1
核心代码
微卡智享
重新计算区域及直线拟合的函数
定义的默认阈值是15。
代码语言:javascript复制//采用重新定义点做直线拟合后找到的对应点
void GetPointsFromFitline(Point2f newPoints[], Point2f vetPoints[], Point2f rectPoints[], float dist = 15.0f);
//重新计算距离变换的4个坐标点
//思路:旋转矩形的点和上一步获取的临近点判断距离,如果小于阈值都列入,大于阈值按最近距离的阈值处理
void GetPointsFromFitline(Point2f newPoints[], Point2f vetPoints[], Point2f rectPoints[], float dist)
{
//1.重新规划区域点
float curdist = CalcPointDistance(rectPoints[0], vetPoints[0]);
newPoints[0] = curdist <= dist ? rectPoints[0] : vetPoints[0] Point2f(-dist, -dist);
curdist = CalcPointDistance(rectPoints[1], vetPoints[1]);
newPoints[1] = curdist <= dist ? rectPoints[1] : vetPoints[1] Point2f(dist, -dist);
curdist = CalcPointDistance(rectPoints[2], vetPoints[2]);
newPoints[2] = curdist <= dist ? rectPoints[2] : vetPoints[2] Point2f(dist, dist);
curdist = CalcPointDistance(rectPoints[3], vetPoints[3]);
newPoints[3] = curdist <= dist ? rectPoints[3] : vetPoints[3] Point2f(-dist, dist);
//左侧区域点为最小旋转矩形的左上左下和离的最近的左上左下组成
vector<Point2f> lArea;
lArea.push_back(newPoints[0]);
lArea.push_back(vetPoints[0]);
lArea.push_back(vetPoints[3]);
lArea.push_back(newPoints[3]);
//顶部区域点为最小外接矩形左上右上和最近点的左上右上组成
vector<Point2f> tArea;
tArea.push_back(newPoints[0]);
tArea.push_back(newPoints[1]);
tArea.push_back(vetPoints[1]);
tArea.push_back(vetPoints[0]);
//右侧区域点为最近点的右上右下和最小外接矩形的右上和右下组成
vector<Point2f> rArea;
rArea.push_back(vetPoints[1]);
rArea.push_back(newPoints[1]);
rArea.push_back(newPoints[2]);
rArea.push_back(vetPoints[2]);
//底部区域点为最近点的左下右下和最小外接矩形的左下右下组成
vector<Point2f> bArea;
bArea.push_back(vetPoints[3]);
bArea.push_back(vetPoints[2]);
bArea.push_back(newPoints[2]);
bArea.push_back(newPoints[3]);
//2.根据区域做直线拟合
//左边区域
Vec4f lLine;
fitLine(lArea, lLine, DIST_L2, 0, 0.01, 0.01);
//顶部区域
Vec4f tLine;
fitLine(tArea, tLine, DIST_L2, 0, 0.01, 0.01);
//右边区域
Vec4f rLine;
fitLine(rArea, rLine, DIST_L2, 0, 0.01, 0.01);
//顶部区域
Vec4f bLine;
fitLine(bArea, bLine, DIST_L2, 0, 0.01, 0.01);
//3.根据直线拟合的求每两条直线的交叉点为我们的多边形顶点
//左上顶点
newPoints[0] = GetCrossPoint(lLine, tLine);
//右上顶点
newPoints[1] = GetCrossPoint(tLine, rLine);
//右下顶点
newPoints[2] = GetCrossPoint(rLine, bLine);
//左下顶点
newPoints[3] = GetCrossPoint(bLine, lLine);
}
根据两点及斜率求交点
代码语言:javascript复制//求两条直线的交叉点
Point2f GetCrossPoint(Vec4f Line1, Vec4f Line2)
{
double k1, k2;
//Line1的斜率
k1 = Line1[1] / Line1[0];
//Line2的斜率
k2 = Line2[1] / Line2[0];
Point2f crossPoint;
crossPoint.x = (k1 * Line1[2] - Line1[3] - k2 * Line2[2] Line2[3]) / (k1 - k2);
crossPoint.y = (k1 * k2 * (Line1[2] - Line2[2]) k1 * Line2[3] - k2 * Line1[3]) / (k1 - k2);
return crossPoint;
}
透视变换的新坐标代码
上一篇中透视变换的新坐标我们直接是用的最小外接矩形的4个点,不过个别图中会矩形特别大,整个透视变换后的拉伸有点太夸张了,所以这里我们改了方法,先求出最小旋转矩形中最左和最上的坐标,然后计算出最小旋转矩形的长和高,来定义一个新的矩形进行透视变换。
代码语言:javascript复制 //根据最小矩形和多边形拟合的最大四个点计算透视变换矩阵
Point2f rectPoint[4];
//计算旋转矩形的宽和高
float rWidth = CalcPointDistance(vertices[0], vertices[1]);
float rHeight = CalcPointDistance(vertices[1], vertices[2]);
//计算透视变换的左上角起始点
float left = gray.cols;
float top = gray.rows;
for (int i = 0; i < 4; i) {
if (left > newPoints[i].x) left = newPoints[i].x;
if (top > newPoints[i].y) top = newPoints[i].y;
}
rectPoint[0] = Point2f(left, top);
rectPoint[1] = rectPoint[0] Point2f(rWidth, 0);
rectPoint[2] = rectPoint[1] Point2f(0, rHeight);
rectPoint[3] = rectPoint[0] Point2f(0, rHeight);
完