【android opencv学习笔记】Day 31:提取轮廓之Canny算法
本文介绍了Canny边缘检测算法的原理及Android实现。Canny算法通过高斯滤波降噪、Sobel梯度计算、非极大值抑制和双阈值筛选四个步骤,实现精准的边缘检测。OpenCV提供了简洁的Canny()函数,支持参数调节。文章详细展示了Android工程的实现过程,包括布局设计和Kotlin代码,开发者只需替换本地图片即可运行。该方案适用于图像处理、目标识别等场景,具有抗噪性强、边缘定位精准的特
【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 算法最具特色的设计,通过高低两个阈值区分强边缘、弱边缘与噪声,解决弱边缘保留和噪声剔除的矛盾:
- 高阈值:像素梯度值高于高阈值 → 判定为强边缘,直接保留;
- 低阈值:像素梯度值低于低阈值 → 判定为噪声,直接剔除;
- 中间区间:梯度介于两个阈值之间的像素,判定为弱边缘;仅当弱边缘与强边缘相连时才保留,否则当作噪声删除。
经验参数:高阈值一般设置为低阈值的
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范数(精度高)
);
参数解读
- image:必须传入灰度图像,彩色图需要提前做色彩空间转换;
- threshold1 / threshold2:双阈值,控制边缘检出数量,阈值越大,检出边缘越少;
- apertureSize:Sobel 核尺寸,常用
3、5,大图/高噪声图像可适当调大; - 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使用步骤
- 将 2048×2048 图片 放入项目
res/drawable文件夹; - 在 Kotlin 代码中修改图片资源名
R.drawable.test_image; - 同步 NDK 配置,编译运行项目;
- 页面自动展示原图与Canny 边缘图。
4运行效果
- 原图:你自定义的 2048×2048 彩色图片;
- 边缘结果图:黑色背景 + 白色单像素细边缘,轮廓连续、无多余噪点。
4阈值调优指南
根据图片画质、噪声强度调整双阈值,适配不同场景:
- 普通图片(通用):低阈值
50,高阈值150(比例 1:3); - 高噪声图片:提高双阈值(例:
80, 240),减少噪点被误检为边缘; - 低对比度图片(弱边缘):降低双阈值(例:
30, 90),保留更多弱边缘; - 边缘断裂严重:适当降低低阈值,增强弱边缘连通性。
总结
- 算法核心:Canny 由「高斯降噪 → Sobel 梯度 → 非极大值抑制 → 双阈值筛选」四步组成,是目前综合表现最优的边缘检测算法;
- API 要点:
Canny函数必须传入灰度图,高低阈值推荐2~3倍比例; - 工程优势:本项目完全基于 Android NDK + OpenCV 实现,支持自定义大图输入,源码注释完整,可直接用于学习、二次开发与项目集成;
- 拓展方向:可在此基础上增加滑动条动态调整阈值、相机实时边缘检测、边缘轮廓绘制等功能。

更多推荐



所有评论(0)