欢迎光临
我们一直在努力

【android opencv学习笔记】Day 32:直线检测之霍夫变换

【android opencv学习笔记】Day 1: Switch类
在这里插入图片描述

霍夫变换直线/线段检测

霍夫变换(Hough Transform)是机器视觉领域经典的几何特征检测算法,尤其在直线、线段检测中表现突出,广泛应用于车道线识别、文档边缘校正、工业零件定位等场景。

本文将从霍夫变换原理、OpenCV 核心 API 解析、Android 完整工程实现三部分,手把手讲解霍夫直线检测。项目支持开发者传入自定义 2048×2048 本地图片,自动完成边缘检测、霍夫直线/线段提取与绘制,源码完整可编译、可直接运行,适合学习、项目集成与技术博文发布。


霍夫变换核心原理

霍夫变换的核心思想是**“空间映射与投票”**,将图像空间中的点映射到参数空间,通过投票统计找到满足同一参数方程的点集,从而检测出直线、圆等几何形状。

1. 直线的参数化表示

在图像空间中,一条直线可以用极坐标形式表示:
ρ=xcos⁡θ+ysin⁡θ
\\rho = x \\cos\\theta + y \\sin\\theta
ρ=xcosθ+ysinθ

  • ρ\\rhoρ:直线到图像原点(左上角)的距离;
  • θ\\thetaθ:直线与垂直方向的夹角,范围 [0,π][0, \\pi][0,π]

这种参数化表示的优势是:所有直线都能被唯一表示,且无需处理直线斜率无穷大的特殊情况。

2. 从图像空间到参数空间的映射

图像空间中的一个点 (x0,y0)(x_0, y_0)(x0,y0),在参数空间中对应一条正弦曲线:
ρ=x0cos⁡θ+y0sin⁡θ
\\rho = x_0 \\cos\\theta + y_0 \\sin\\theta
ρ=x0cosθ+y0sinθ

图像空间中同一条直线上的多个点,在参数空间中对应的正弦曲线会相交于同一点 (ρ,θ)(\\rho, \\theta)(ρ,θ)

3. 累加器投票机制

霍夫变换使用一个二维累加器(投票矩阵)统计参数空间中各点的投票数:

  • 遍历二值边缘图像中的所有非零像素点;
  • 对每个点,遍历所有可能的 θ\\thetaθ 值,计算对应的 ρ\\rhoρ
  • 累加器中 (ρ,θ)(\\rho, \\theta)(ρ,θ) 位置的投票数加1;
  • 投票数超过预设阈值的 (ρ,θ)(\\rho, \\theta)(ρ,θ),即为检测到的直线。
  • 4. 概率霍夫变换(HoughLinesP)

    标准霍夫变换只能检测无限长直线,而实际应用中我们通常需要线段(带端点)。OpenCV 提供的 HoughLinesP(概率霍夫变换)是其优化版本,核心改进:

    • 随机选取边缘点进行投票,减少计算量;
    • 引入线段最小长度、最大间隙两个参数,仅保留连续线段;
    • 直接返回线段的两个端点坐标,无需额外计算直线与图像边界的交点。

    OpenCV 核心 API 解析

    1. 标准霍夫直线检测:cv::HoughLines

    void HoughLines(
    InputArray image, // 输入二值边缘图像(如 Canny 输出)
    OutputArray lines, // 输出直线参数向量,每个元素为 Vec2f(ρ, θ)
    double rho, // ρ 方向步长(像素级精度)
    double theta, // θ 方向步长(弧度级精度)
    int threshold, // 最低投票数
    double srn = 0, // 多尺度霍夫变换的 rho 除数
    double stn = 0, // 多尺度霍夫变换的 theta 除数
    double min_theta = 0, // θ 范围最小值
    double max_theta = CV_PI // θ 范围最大值
    );

    • 常用参数:rho=1,theta=CV_PI/180,threshold 根据图像调整(一般 50~150);
    • 输出结果:仅包含直线参数,需额外计算直线与图像边界的交点才能绘制。

    2. 概率霍夫线段检测:cv::HoughLinesP

    void HoughLinesP(
    InputArray image, // 输入二值边缘图像
    OutputArray lines, // 输出线段向量,每个元素为 Vec4i(x1,y1,x2,y2)
    double rho, // ρ 方向步长
    double theta, // θ 方向步长
    int threshold, // 最低投票数
    double minLineLength = 0, // 线段最小长度(像素)
    double maxLineGap = 0 // 线段间允许的最大间隙(像素)
    );

    • 关键参数:minLineLength 过滤过短线段,maxLineGap 合并断裂的同一直线段;
    • 输出结果:直接返回线段端点坐标,可直接用 cv::line 绘制。

    Android 完整工程实现

    环境说明

    • 开发环境:Android Studio + NDK 27 + OpenCV Android
    • 图片要求:支持开发者自行传入 2048×2048 本地图片
    • 技术栈:Kotlin + JNI + C++ + OpenCV

    布局文件 activity_main.xml

    页面分为原图展示区、Canny 边缘图、霍夫直线检测结果区,使用滚动布局适配大图预览:

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#f5f5f5">

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10dp"
    android:gap="10dp">

    <!– 原始图片展示 –>
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="原始图片"
    android:textSize="16sp"
    android:textStyle="bold"/>

    <ImageView
    android:id="@+id/iv_origin"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    android:scaleType="fitCenter"
    android:background="#ffffff"/>

    </LinearLayout>

    <!– Canny 边缘图 –>
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Canny 边缘图"
    android:textSize="16sp"
    android:textStyle="bold"/>

    <ImageView
    android:id="@+id/iv_canny"
    android:layout_width="match_parent"
    android:layout_height="220dp"
    android:scaleType="fitCenter"
    android:background="#ffffff"/>

    </LinearLayout>

    <!– 霍夫线段检测结果 –>
    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="霍夫线段检测结果"
    android:textSize="16sp"
    android:textStyle="bold"/>

    <ImageView
    android:id="@+id/iv_hough"
    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 目录,修改资源名即可使用:

    package com.example.houghline

    import android.graphics.Bitmap
    import android.graphics.BitmapFactory
    import android.os.Bundle
    import android.widget.ImageView
    import androidx.appcompat.app.AppCompatActivity

    class MainActivity : AppCompatActivity() {

    companion object {
    init {
    System.loadLibrary("native-lib")
    }
    }

    /**
    * JNI 原生方法:执行 Canny 边缘检测 + 霍夫线段检测
    * @param srcBitmap 输入原图 Bitmap
    * @param outCanny 输出 Canny 边缘图 Bitmap
    * @param outHough 输出霍夫线段检测结果 Bitmap
    */

    private external fun processHoughLine(
    srcBitmap: Bitmap,
    outCanny: Bitmap,
    outHough: Bitmap
    )

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // ========== 1. 加载你自己的 2048*2048 图片 ==========
    val srcBitmap = BitmapFactory.decodeResource(resources, R.drawable.test_image)

    // 创建输出位图,尺寸与原图保持一致
    val cannyBitmap = Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)
    val houghBitmap = Bitmap.createBitmap(srcBitmap.width, srcBitmap.height, Bitmap.Config.ARGB_8888)

    // ========== 2. 调用原生算法 ==========
    processHoughLine(srcBitmap, cannyBitmap, houghBitmap)

    // ========== 3. 展示图片 ==========
    findViewById<ImageView>(R.id.iv_origin).setImageBitmap(srcBitmap)
    findViewById<ImageView>(R.id.iv_canny).setImageBitmap(cannyBitmap)
    findViewById<ImageView>(R.id.iv_hough).setImageBitmap(houghBitmap)
    }
    }

    底层 C++ JNI 代码 native-lib.cpp

    核心逻辑:Bitmap 与 OpenCV Mat 互转、Canny 边缘检测、概率霍夫线段检测与绘制,附带完整注释:

    #include <jni.h>
    #include <opencv2/opencv.hpp>
    #include <android/bitmap.h>

    using namespace cv;
    using namespace std;

    /**
    * Bitmap 转 OpenCV Mat
    * @param bitmap Android 上层传入的 Bitmap
    * @return 转换后的 BGR 格式 Mat
    */

    Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
    AndroidBitmapInfo info;
    void* pixels = nullptr;
    AndroidBitmap_getInfo(env, bitmap, &info);
    AndroidBitmap_lockPixels(env, bitmap, &pixels);

    Mat rgba(info.height, info.width, CV_8UC4, pixels);
    Mat bgr;
    cvtColor(rgba, bgr, COLOR_RGBA2BGR);
    AndroidBitmap_unlockPixels(env, bitmap);
    return bgr;
    }

    /**
    * OpenCV Mat 转 Bitmap,用于回传给 Android 上层展示
    * @param srcMat OpenCV 图像矩阵
    * @param dstBitmap 目标 Bitmap
    */

    void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
    AndroidBitmapInfo info;
    void* pixels = nullptr;
    AndroidBitmap_getInfo(env, dstBitmap, &info);
    AndroidBitmap_lockPixels(env, dstBitmap, &pixels);

    Mat rgba;
    if (srcMat.channels() == 1) {
    cvtColor(srcMat, rgba, COLOR_GRAY2RGBA);
    } else {
    cvtColor(srcMat, rgba, COLOR_BGR2RGBA);
    }
    memcpy(pixels, rgba.data, info.width * info.height * 4);
    AndroidBitmap_unlockPixels(env, dstBitmap);
    }

    /**
    * 霍夫线段检测核心逻辑
    * @param srcBgr 输入彩色图像
    * @param outCanny 输出 Canny 边缘图像
    * @param outHough 输出霍夫线段检测结果图像
    */

    void houghLineDetection(const Mat& srcBgr, Mat& outCanny, Mat& outHough) {
    // 1. 转灰度图
    Mat srcGray;
    cvtColor(srcBgr, srcGray, COLOR_BGR2GRAY);

    // 2. Canny 边缘检测(霍夫变换输入为二值边缘图)
    Canny(srcGray, outCanny, 50, 150, 3);

    // 3. 概率霍夫线段检测
    vector<Vec4i> lines;
    HoughLinesP(
    outCanny, lines,
    1, // ρ 步长
    CV_PI/180, // θ 步长
    60, // 最低投票数
    100, // 线段最小长度
    20 // 线段间最大间隙
    );

    // 4. 在原图上绘制检测到的线段(绿色线条,线宽 2)
    outHough = srcBgr.clone();
    for (size_t i = 0; i < lines.size(); i++) {
    Vec4i l = lines[i];
    line(outHough, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 255, 0), 2);
    }
    }

    /**
    * JNI 入口函数:供 Kotlin 调用
    */

    extern "C" JNIEXPORT void JNICALL
    Java_com_example_houghline_MainActivity_processHoughLine(
    JNIEnv *env, jobject thiz,
    jobject srcBitmap,
    jobject outCanny,
    jobject outHough)
    {
    // 1. Bitmap 转 Mat
    Mat srcBgr = bitmapToMat(env, srcBitmap);
    Mat matCanny, matHough;

    // 2. 执行 Canny + 霍夫线段检测
    houghLineDetection(srcBgr, matCanny, matHough);

    // 3. 结果回转为 Bitmap,返回上层
    matToBitmap(env, matCanny, outCanny);
    matToBitmap(env, matHough, outHough);
    }

    3.4 CMake 配置(CMakeLists.txt)

    NDK 编译核心配置,关联 OpenCV 库,按需修改 OpenCV 路径即可:

    cmake_minimum_required(VERSION 3.22.1)

    project("houghline")

    # 引入 OpenCV 头文件目录
    include_directories(E:/xxx/opencv-native/include)

    # 配置原生库
    add_library(
    native-lib
    SHARED
    native-lib.cpp)

    # 链接系统库与 OpenCV 库
    find_library(
    log-lib
    log)

    target_link_libraries(
    native-lib
    ${log-lib})

    在这里插入图片描述


    运行说明与效果解读

    使用步骤

  • 将 2048×2048 图片 放入项目 res/drawable 文件夹;
  • 在 Kotlin 代码中修改图片资源名 R.drawable.test_image;
  • 同步 NDK 配置,编译运行项目;
  • 页面自动展示原图、Canny 边缘图与霍夫线段检测结果图。
  • 运行效果

    • 原图:你自定义的 2048×2048 彩色图片;
    • Canny 边缘图:黑色背景 + 白色单像素细边缘,为霍夫变换提供输入;
    • 霍夫结果图:在原图上用绿色线条标注出检测到的所有线段,仅保留长度达标、连续的线段。

    参数调优指南

    根据图片场景调整霍夫变换参数,适配不同场景:

  • 道路线检测:提高 minLineLength(如 150),过滤过短的干扰线段;
  • 文档边缘检测:降低 maxLineGap(如 10),保证断裂的文档边缘被合并;
  • 高噪声图片:提高 threshold(如 80),减少噪声产生的伪直线;
  • 低对比度图片:降低 threshold(如 40),同时降低 Canny 低阈值(如 30),保留更多弱边缘。

  • 总结与拓展

  • 算法核心:霍夫变换通过“参数空间投票”实现直线检测,概率霍夫变换是工程中更常用的线段检测方案;
  • API 要点:HoughLinesP 需传入二值边缘图,minLineLength 和 maxLineGap 是控制检测效果的关键参数;
  • 工程优势:本项目完全基于 Android NDK + OpenCV 实现,支持自定义大图输入,源码注释完整,可直接用于车道线检测、文档校正等项目集成;
  • 拓展方向:可在此基础上增加霍夫圆检测(HoughCircles)、滑动条动态调整参数、相机实时霍夫检测等功能。
  • 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 【android opencv学习笔记】Day 32:直线检测之霍夫变换
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址