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

Canny 边缘检测算法

边缘检测是图像处理、机器视觉领域最基础也最重要的技术之一,广泛应用于轮廓提取、目标识别、图像分割等场景。

在众多边缘检测算法中,Canny 算法凭借定位精准、抗噪性强、边缘线条纤细连续等优势,成为工业界与开发中使用最广泛的边缘检测方案。


Canny 算法核心原理

Canny 边缘检测是多步骤组合型算法,整套流程分为高斯降噪、梯度计算、非极大值抑制、双阈值滞后筛选四大核心环节,层层递进实现高质量边缘提取。

1. 高斯滤波降噪

边缘检测对图像噪声极度敏感,微小噪声都会被误判为边缘。因此 Canny 算法第一步,使用高斯滤波器对图像做平滑处理,抑制高频噪声。
高斯滤波属于线性平滑滤波,通过加权平均的方式模糊图像,在降噪的同时最大程度保留原有边缘信息。

2. 梯度计算(基于 Sobel 算子)

降噪完成后,使用经典 Sobel 算子计算图像水平、垂直两个方向的梯度,以此表征像素亮度变化强度与方向。

  • 梯度幅值:描述当前像素处亮度变化的剧烈程度,幅值越大,越接近边缘
    G=Gx2+Gy2G = \sqrt{G_x^2 + G_y^2}G=Gx2+Gy2
  • 梯度方向:描述边缘的走向
    θ=arctan⁡(GyGx)\theta = \arctan\left(\frac{G_y}{G_x}\right)θ=arctan(GxGy)

3. 非极大值抑制(NMS)

经过梯度计算后,边缘区域会出现较粗的亮带。非极大值抑制的作用就是“细化边缘”:
沿着梯度方向遍历像素,仅保留局部梯度最大值的像素,其余像素置为 0。
经过该步骤后,边缘会被压缩为单像素细线,保证边缘定位精度。

4. 双阈值滞后筛选(Canny 核心)

这是 Canny 算法最具特色的设计,通过高低两个阈值区分强边缘、弱边缘与噪声,解决弱边缘保留和噪声剔除的矛盾:

  1. 高阈值:像素梯度值高于高阈值 → 判定为强边缘,直接保留;
  2. 低阈值:像素梯度值低于低阈值 → 判定为噪声,直接剔除;
  3. 中间区间:梯度介于两个阈值之间的像素,判定为弱边缘;仅当弱边缘与强边缘相连时才保留,否则当作噪声删除。

经验参数:高阈值一般设置为低阈值的 2~3 倍,可根据图像明暗、噪声情况灵活调整。


OpenCV 核心 API 详解

OpenCV 封装了成熟的 Canny 函数,一行代码即可实现整套边缘检测逻辑,下面对函数参数逐一说明。

函数原型

void Canny(
    InputArray image,        // 输入图像,要求为 8 位灰度图
    OutputArray edges,      // 输出边缘图像,二值图(仅 0 和 255)
    double threshold1,      // 低阈值
    double threshold2,      // 高阈值(必须大于低阈值)
    int apertureSize = 3,   // Sobel 算子卷积核大小,默认 3×3
    bool L2gradient = false // 梯度计算方式:false=L1范数(速度快),true=L2范数(精度高)
);

参数解读

  1. image:必须传入灰度图像,彩色图需要提前做色彩空间转换;
  2. threshold1 / threshold2:双阈值,控制边缘检出数量,阈值越大,检出边缘越少;
  3. apertureSize:Sobel 核尺寸,常用 35,大图/高噪声图像可适当调大;
  4. L2gradient:普通场景默认 false 即可,追求高精度场景设为 true

Android 完整工程实现

环境说明

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

3.1 布局文件 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>
</ScrollView>

3.2 上层 Kotlin 代码 MainActivity.kt

负责加载本地图片、创建位图、调用 JNI 原生方法、展示结果。
开发者只需将自己的 2048×2048 图片 放入 res/drawable 目录,修改资源名即可使用。

package com.example.canny

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

class MainActivity : AppCompatActivity() {

    // 加载 OpenCV 原生库
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

    /**
     * JNI 原生方法:执行 Canny 边缘检测
     * @param srcBitmap 输入原图 Bitmap
     * @param outCanny 输出边缘结果 Bitmap
     * @param lowThreshold 低阈值
     * @param highThreshold 高阈值
     */
    private external fun processCanny(
        srcBitmap: Bitmap,
        outCanny: Bitmap,
        lowThreshold: Int,
        highThreshold: Int
    )

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

        // ========== 1. 加载你自己的 2048*2048 图片 ==========
        // 将图片放入 res/drawable,修改此处资源名即可
        val srcBitmap = BitmapFactory.decodeResource(resources, R.drawable.test_image)

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

        // ========== 2. 调用原生算法,可自由调整双阈值 ==========
        // 推荐比例:高阈值 = 低阈值 * (2 ~ 3)
        processCanny(srcBitmap, cannyBitmap, 50, 150)

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

3.3 底层 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);

    // Android Bitmap 默认 RGBA 四通道
    Mat rgba(info.height, info.width, CV_8UC4, pixels);
    Mat bgr;
    // 转换为 OpenCV 标准 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;
    // 区分灰度图 / 彩色图,统一转为 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);
}

/**
 * Canny 边缘检测核心逻辑
 * @param srcBgr 输入彩色图像
 * @param outEdges 输出二值边缘图像
 * @param lowThresh 低阈值
 * @param highThresh 高阈值
 */
void cannyEdgeDetection(const Mat& srcBgr, Mat& outEdges, int lowThresh, int highThresh) {
    // 1. 彩色图转为灰度图(Canny 要求输入灰度图)
    Mat srcGray;
    cvtColor(srcBgr, srcGray, COLOR_BGR2GRAY);

    // 2. 前置高斯滤波,强化降噪效果
    GaussianBlur(srcGray, srcGray, Size(3, 3), 0);

    // 3. 执行 Canny 边缘检测
    Canny(srcGray, outEdges, lowThresh, highThresh, 3, false);

    // 反转黑白:黑色背景 + 白色边缘(视觉更直观)
    bitwise_not(outEdges, outEdges);
}

/**
 * JNI 入口函数:供 Kotlin 调用
 */
extern "C" JNIEXPORT void JNICALL
Java_com_example_canny_MainActivity_processCanny(
        JNIEnv *env, jobject thiz,
        jobject srcBitmap,
        jobject outCanny,
        jint lowThreshold,
        jint highThreshold)
{
    // 1. Bitmap 转 Mat
    Mat srcBgr = bitmapToMat(env, srcBitmap);
    Mat matEdges;

    // 2. 执行边缘检测算法
    cannyEdgeDetection(srcBgr, matEdges, lowThreshold, highThreshold);

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

3.4 CMake 配置(CMakeLists.txt)

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

cmake_minimum_required(VERSION 3.22.1)

project("canny")

# 引入 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})

在这里插入图片描述


运行说明与效果解读

4使用步骤

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

4运行效果

  • 原图:你自定义的 2048×2048 彩色图片;
  • 边缘结果图:黑色背景 + 白色单像素细边缘,轮廓连续、无多余噪点。

4阈值调优指南

根据图片画质、噪声强度调整双阈值,适配不同场景:

  1. 普通图片(通用):低阈值 50,高阈值 150(比例 1:3);
  2. 高噪声图片:提高双阈值(例:80, 240),减少噪点被误检为边缘;
  3. 低对比度图片(弱边缘):降低双阈值(例:30, 90),保留更多弱边缘;
  4. 边缘断裂严重:适当降低低阈值,增强弱边缘连通性。

总结

  1. 算法核心:Canny 由「高斯降噪 → Sobel 梯度 → 非极大值抑制 → 双阈值筛选」四步组成,是目前综合表现最优的边缘检测算法;
  2. API 要点Canny 函数必须传入灰度图,高低阈值推荐 2~3 倍比例;
  3. 工程优势:本项目完全基于 Android NDK + OpenCV 实现,支持自定义大图输入,源码注释完整,可直接用于学习、二次开发与项目集成;
  4. 拓展方向:可在此基础上增加滑动条动态调整阈值、相机实时边缘检测、边缘轮廓绘制等功能。

在这里插入图片描述

Logo

CANN开发者社区旨在汇聚广大开发者,围绕CANN架构重构、算子开发、部署应用优化等核心方向,展开深度交流与思想碰撞,携手共同促进CANN开放生态突破!

更多推荐