【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, \\pi0,π

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

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. 累加器投票机制

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

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

4. 概率霍夫变换(HoughLinesP)

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

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

OpenCV 核心 API 解析

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

cpp 复制代码
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=1theta=CV_PI/180threshold 根据图像调整(一般 50~150);
  • 输出结果:仅包含直线参数,需额外计算直线与图像边界的交点才能绘制。

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

cpp 复制代码
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 复制代码
<?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 目录,修改资源名即可使用:

kotlin 复制代码
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 边缘检测、概率霍夫线段检测与绘制,附带完整注释:

cpp 复制代码
#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 复制代码
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})

运行说明与效果解读

使用步骤

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

运行效果

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

参数调优指南

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

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

总结与拓展

  1. 算法核心:霍夫变换通过"参数空间投票"实现直线检测,概率霍夫变换是工程中更常用的线段检测方案;
  2. API 要点HoughLinesP 需传入二值边缘图,minLineLengthmaxLineGap 是控制检测效果的关键参数;
  3. 工程优势:本项目完全基于 Android NDK + OpenCV 实现,支持自定义大图输入,源码注释完整,可直接用于车道线检测、文档校正等项目集成;
  4. 拓展方向 :可在此基础上增加霍夫圆检测(HoughCircles)、滑动条动态调整参数、相机实时霍夫检测等功能。
相关推荐
提子拌饭1333 小时前
Column 嵌套布局:多级 Column 实现复杂纵向结构——鸿蒙 HarmonyOS ArkTS 原生学习应用
学习·华为·harmonyos·鸿蒙·鸿蒙系统
xqqxqxxq3 小时前
树结构技术学习笔记
数据结构·笔记·学习
十月的皮皮4 小时前
C语言学习笔记202606008- 三角形判断(3种方法)
c语言·笔记·学习
XGeFei4 小时前
【Fastapi学习笔记(6)】—— Fastapi文件上传、请求头自动转换
笔记·学习·fastapi
一口吃俩胖子5 小时前
【脉宽调制DCDC功率变换学习笔记024】频域性能
笔记·学习
吃着火锅x唱着歌5 小时前
深度探索C++对象模型 学习笔记 第五章 构造、解构、拷贝语意学(2)
c++·笔记·学习
中小企业实战军师刘孙亮5 小时前
快消纺织五金怎么融合?三大业态协同发展战略思路-佛山鼎策创局破局增长咨询
学习·面试·创业创新·制造·学习方法
Upsy-Daisy5 小时前
Hermes Agent 学习笔记 04:工具调用系统,让 Agent 从“会说”变成“会做”
java·笔记·学习
楼田莉子5 小时前
C++20新特性:协程
开发语言·c++·后端·学习·c++20