点集直线拟合
在霍夫变换检测出直线后,我们往往需要更精确的直线位置与方向,例如车道线矫正、工业零件边缘校准、文档透视校正等场景。
此时就需要用到点集直线拟合,通过边缘点集计算出最优直线,实现亚像素级精度的直线定位。
点集直线拟合核心原理
点集直线拟合的核心目标,是找到一条直线,使点集中所有点到直线的距离误差最小。
1. 最小二乘法(L2 距离)
最经典的拟合方法,通过最小化点到直线的欧几里得距离平方和求解最优直线:
min∑i=1ndi2 \min \sum_{i=1}^{n} d_i^2mini=1∑ndi2
其中did_idi是点(xi,yi)(x_i,y_i)(xi,yi)到拟合直线的垂直距离。这种方法计算简单、速度快,但对离群点(噪声点)非常敏感,一个异常点就会严重影响拟合结果。
2. M 估计(鲁棒拟合)
OpenCV 的fitLine函数默认采用基于 M 估计的鲁棒拟合算法,核心改进是:
- 引入权重系数,权重与点到直线的距离成反比;
- 迭代计算加权最小二乘,逐步降低离群点的影响;
- 支持多种距离函数(L1、L2、Huber、Tukey 等),适配不同噪声场景。
3. 完整流程:从图像边缘到拟合直线
- 边缘提取:用 Canny 算法获取图像边缘二值图;
- 点集筛选:根据霍夫检测的直线,提取其邻域内的所有边缘点,构建点集;
- 直线拟合:使用
cv::fitLine对筛选出的点集进行鲁棒拟合,得到最优直线参数; - 结果绘制:根据拟合出的直线参数,在原图上绘制精确的直线。
OpenCV 核心 API 解析
cv::fitLine函数
这是 OpenCV 中实现点集直线拟合的核心函数,支持二维/三维点集,返回直线的方向向量与过点坐标。
函数原型
voidfitLine(InputArray points,// 输入点集(std::vector<Point> / std::vector<Point3f>)OutputArray line,// 输出直线参数:Vec4f(2D) 或 Vec6f(3D)intdistType,// 距离类型(决定鲁棒性)doubleparam,// 距离函数参数,0表示默认值doublereps,// 精度参数,坐标的回归精度doubleaeps// 精度参数,角度的回归精度);参数解读
distType:距离函数类型,决定算法的鲁棒性:CV_DIST_L2:欧几里得距离,标准最小二乘法,速度快但不抗噪;CV_DIST_L1:L1 距离,对离群点有一定鲁棒性;CV_DIST_HUBER:Huber 距离,对离群点的影响进行衰减;CV_DIST_Tukey:Tukey 距离,完全忽略离群点,鲁棒性最强。
line:输出结果,二维场景下为Vec4f,包含:line[0], line[1]:单位方向向量(dx,dy)(dx, dy)(dx,dy);line[2], line[3]:直线上的一个点坐标(x0,y0)(x_0, y_0)(x0,y0)。
Android 完整工程实现
布局文件 activity_main.xml
页面分为原图展示区、Canny 边缘图、点集筛选图、直线拟合结果区,使用滚动布局适配大图预览:
<?xml version="1.0" encoding="utf-8"?><ScrollViewxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#f5f5f5"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:padding="10dp"android:gap="10dp"><!-- 原始图片展示 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="原始图片"android:textSize="16sp"android:textStyle="bold"/><ImageViewandroid:id="@+id/iv_origin"android:layout_width="match_parent"android:layout_height="220dp"android:scaleType="fitCenter"android:background="#ffffff"/></LinearLayout><!-- Canny 边缘图 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Canny 边缘图"android:textSize="16sp"android:textStyle="bold"/><ImageViewandroid:id="@+id/iv_canny"android:layout_width="match_parent"android:layout_height="220dp"android:scaleType="fitCenter"android:background="#ffffff"/></LinearLayout><!-- 直线拟合结果 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="直线拟合结果"android:textSize="16sp"android:textStyle="bold"/><ImageViewandroid:id="@+id/iv_fitline"android:layout_width="match_parent"android:layout_height="220dp"android:scaleType="fitCenter"android:background="#ffffff"/></LinearLayout></LinearLayout></ScrollView>上层 Kotlin 代码 MainActivity.kt
负责加载本地图片、创建位图、调用 JNI 原生方法、展示结果。开发者只需将自己的 2048×2048 图片放入res/drawable目录,修改资源名即可使用:
packagecom.example.linefitimportandroid.graphics.Bitmapimportandroid.graphics.BitmapFactoryimportandroid.os.Bundleimportandroid.widget.ImageViewimportandroidx.appcompat.app.AppCompatActivityclassMainActivity:AppCompatActivity(){companionobject{init{System.loadLibrary("native-lib")}}/** * JNI 原生方法:执行 Canny 边缘检测 + 点集筛选 + 直线拟合 * @param srcBitmap 输入原图 Bitmap * @param outCanny 输出 Canny 边缘图 Bitmap * @param outPoints 输出点集筛选图 Bitmap * @param outFitLine 输出直线拟合结果 Bitmap */privateexternalfunprocessLineFit(srcBitmap:Bitmap,outCanny:Bitmap,outFitLine:Bitmap)overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// ========== 1. 加载你自己的 2048*2048 图片 ==========valsrcBitmap=BitmapFactory.decodeResource(resources,R.drawable.test_image)// 创建输出位图,尺寸与原图保持一致valcannyBitmap=Bitmap.createBitmap(srcBitmap.width,srcBitmap.height,Bitmap.Config.ARGB_8888)valfitLineBitmap=Bitmap.createBitmap(srcBitmap.width,srcBitmap.height,Bitmap.Config.ARGB_8888)// ========== 2. 调用原生算法 ==========processLineFit(srcBitmap,cannyBitmap,pointsBitmap,fitLineBitmap)// ========== 3. 展示图片 ==========findViewById<ImageView>(R.id.iv_origin).setImageBitmap(srcBitmap)findViewById<ImageView>(R.id.iv_canny).setImageBitmap(cannyBitmap)findViewById<ImageView>(R.id.iv_fitline).setImageBitmap(fitLineBitmap)}}底层 C++ JNI 代码 native-lib.cpp
核心逻辑:Bitmap与 OpenCVMat互转、Canny 边缘检测、霍夫线段检测、点集筛选、直线拟合与绘制,附带完整注释:
#include<jni.h>#include<opencv2/core.hpp>#include<opencv2/imgproc.hpp>#include<opencv2/highgui.hpp>#include<android/bitmap.h>usingnamespacecv;usingnamespacestd;// ------------------------------// Bitmap → Mat// ------------------------------MatbitmapToMat(JNIEnv*env,jobject bitmap){AndroidBitmapInfo info;void*pixels=nullptr;AndroidBitmap_getInfo(env,bitmap,&info);AndroidBitmap_lockPixels(env,bitmap,&pixels);Matrgba(info.height,info.width,CV_8UC4,pixels);Mat bgr;cvtColor(rgba,bgr,COLOR_RGBA2BGR);AndroidBitmap_unlockPixels(env,bitmap);returnbgr;}// ------------------------------// Mat → Bitmap// ------------------------------voidmatToBitmap(JNIEnv*env,constMat&mat,jobject bitmap){AndroidBitmapInfo info;void*pixels=nullptr;AndroidBitmap_getInfo(env,bitmap,&info);AndroidBitmap_lockPixels(env,bitmap,&pixels);Mat rgba;if(mat.channels()==1){cvtColor(mat,rgba,COLOR_GRAY2RGBA);}else{cvtColor(mat,rgba,COLOR_BGR2RGBA);}memcpy(pixels,rgba.data,info.width*info.height*4);AndroidBitmap_unlockPixels(env,bitmap);}// ------------------------------// 核心:拟合 + 画线// ------------------------------voidfitLineProcess(constMat&src,Mat&cannyOut,Mat&fitLineOut){Mat gray;cvtColor(src,gray,COLOR_BGR2GRAY);GaussianBlur(gray,gray,Size(3,3),0);// Canny 边缘Canny(gray,cannyOut,50,150,3);// 收集所有边缘点vector<Point>points;findNonZero(cannyOut,points);fitLineOut=src.clone();if(!points.empty()){Vec4f lineParams;fitLine(points,lineParams,2,0,0.01,0.01);floatvx=lineParams[0];floatvy=lineParams[1];floatx0=lineParams[2];floaty0=lineParams[3];Pointp1(cvRound(x0-vx*2000),cvRound(y0-vy*2000));Pointp2(cvRound(x0+vx*2000),cvRound(y0+vy*2000));// --------------------------// ✅ line 函数已修复// --------------------------line(fitLineOut,p1,p2,Scalar(0,255,0),// 绿色4// 线宽);}}// ------------------------------// JNI 入口// ------------------------------extern"C"JNIEXPORTvoidJNICALLJava_com_nicoli_hellolinesfitline_MainActivity_processFitLine(JNIEnv*env,jobject thiz,jobject src,jobject outCanny,jobject outFitLine){Mat srcMat=bitmapToMat(env,src);Mat cannyMat,fitMat;fitLineProcess(srcMat,cannyMat,fitMat);matToBitmap(env,cannyMat,outCanny);matToBitmap(env,fitMat,outFitLine);}总结与拓展
- 算法核心:点集直线拟合通过最小化点到直线的距离误差求解最优直线,OpenCV 的
fitLine函数支持多种鲁棒距离函数; - 工程流程:霍夫检测获取初始直线 → 邻域点集筛选 → 鲁棒直线拟合,解决霍夫直线精度不足的问题;
- 工程优势:本项目完全基于 Android NDK + OpenCV 实现,支持自定义大图输入,源码注释完整,可直接用于车道线矫正、文档边缘校准等项目集成;
- 拓展方向:可在此基础上实现多直线拟合、椭圆拟合(
fitEllipse)、三维点集直线拟合等功能。