学习 Android (十九) 学习 OpenCV (四)
学习 Android (十九) 学习 OpenCV (四)
在上一章节,我们介绍了OpenCV中椒盐噪声的处理方法。椒盐噪声表现为图像中随机出现的黑白像素点,可通过随机生成掩膜来模拟。文章详细阐述了椒盐噪声的数学表达和生成原理,并提供了四种去噪方法:中值滤波(首选)、双边滤波、形态学操作(开/闭运算)和非局部均值滤波(NLM)。每种方法都给出了参数设置建议和使用场景分析。最后通过代码示例展示了如何在Android中实现椒盐噪声的添加和去除,对比了不同去噪方法的效果。这些技术在图像处理算法评估、OCR预处理、数据增强等领域有广泛应用。这一章节我们将进行边缘检测相关知识的学习。
21. 边缘检测
21.1 什么是边缘检测
边缘是图像中亮度或颜色发生显著变化的地方,它通常对应着物体的轮廓、不同材质的边界等。边缘检测是图像处理中提取图像中这种不连续性(边缘)的技术。
边缘的本质是图像中像素值发生剧烈变化的区域。例如,从白色的纸张突然变为深色的桌子,此处的像素强度(在灰度图中)会有很大的差异。
我们的目标是找到这些“变化很大”的地方。在数学上,“变化”可以通过导数或梯度来衡量。图像是离散的二维函数,因此我们使用卷积来计算其近似梯度。
-
卷积 (
filter2D
)-
卷积是一种数学运算,它使用一个称为卷积核(或滤波器)的小矩阵,在图像上滑动并计算其覆盖区域的加权和。
-
对于边缘检测,我们设计的卷积核可以近似计算图像在某个方向上的导数。
-
filter2D(src, dst, ddepth, kernel)
是执行该操作的核心函数。-
src
: 输入图像(如灰度图)。 -
dst
: 输出图像,将存储卷积结果。 -
ddepth
: 输出图像的深度(例如,CvType.CV_16S
表示16位有符号整数,用于存储可能为负的卷积结果)。 -
kernel
: 承载我们意图的卷积核。
-
-
-
绝对值与缩放 (
convertScaleAbs
)-
卷积计算梯度时,在边缘的一侧,像素值从高到低变化,梯度为负;在另一侧,从低到高变化,梯度为正。但我们通常只关心变化的强度,而不关心方向。
-
卷积后的图像可能包含负数值,如果直接用8位无符号整数(0-255)格式显示,这些负值会被截断,导致信息丢失。
-
convertScaleAbs(src, dst)
函数解决了这个问题:-
计算绝对值 (Abs):将图像中的每个值取绝对值。这样,无论是正梯度还是负梯度,都变为正数,代表了边缘的强度。
-
缩放转换 (ConvertScale):将数值缩放到 0-255 的范围内,并转换为标准的 8 位无符号整数(
CvType.CV_8UC1
)格式,以便正确显示。
-
-
简单流程:
原始图像 -> (用特殊核进行 filter2D
卷积) -> 包含正负梯度的图像 -> (应用 convertScaleAbs
) -> 可视化的边缘强度图
21.2 应用场景
-
教学与理解:这是理解边缘检测和图像卷积原理最直观的方式。你可以通过改变卷积核来亲眼看到不同的核如何产生不同的效果。
-
自定义滤波器:当你有一个特定的、非标准的边缘提取需求,而现成算法(如Canny)无法满足时,你可以自己设计卷积核来实现。
-
轻量级处理:在某些性能受限的场景下,一个简单的卷积核可能比复杂的Canny算法更快。
-
方向性边缘检测:可以分别检测水平方向或垂直方向的边缘,这在某些图像分析中非常有用。
特点:
-
透明可控:每一步操作都非常清晰,你可以完全控制卷积过程。
-
基础性强:这是所有高级边缘检测算法的基石。
-
需要调参:效果很大程度上依赖于你选择的卷积核和后续的阈值处理。
22.2 示例
这里我们将进行简单的边缘检测实例,针对X、Y方向的边缘检测,不直接使用那些高级算法,专注于使用最基础的图像卷积 (filter2D
) 和数值转换 (convertScaleAbs
) 操作来从头构建和理解边缘检测的原理。
EdgeDetectionActivity.java
public class EdgeDetectionActivity extends AppCompatActivity {private ActivityEdgeDetectionBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat;private Mat mGrayMat; // 添加灰度图缓存// 预定义卷积核private static final short[] KERNEL_X = new short[]{-1, 0, 1};private static final short[] KERNEL_Y = new short[]{-1, 0, 1};private static final short[] KERNEL_XY = new short[]{2, 1, 0,1, 0, -1,0, -1, -2};private static final short[] KERNEL_YX = new short[]{-2, -1, 0,-1, 0, 1,0, 1, 2};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityEdgeDetectionBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 加载原图mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();return;}// 转换为灰度图并缓存mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);// 显示原图showMat(mBinding.ivOriginal, mOriginalMat);// 执行各种边缘检测executeEdgeDetections();} catch (Exception e) {e.printStackTrace();Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show();}}/*** 执行所有边缘检测方法*/private void executeEdgeDetections() {edgeDetectionX();edgeDetectionY();edgeDetectionXAndY();edgeDetectionXY();edgeDetectionYX();}/*** 通用的边缘检测方法* @param kernel 卷积核数组* @param rows 卷积核行数* @param cols 卷积核列数* @param targetView 显示结果的ImageView* @param useBlur 是否使用高斯模糊预处理* @return 处理后的Mat对象(需要调用者释放)*/private Mat applyEdgeDetection(short[] kernel, int rows, int cols,ImageView targetView, boolean useBlur) {if (mGrayMat == null || mGrayMat.empty()) return null;Mat processed = new Mat();Mat kernelMat = new Mat(rows, cols, CvType.CV_16S);Mat result = new Mat();try {// 预处理:高斯模糊if (useBlur) {Imgproc.GaussianBlur(mGrayMat, processed, new Size(3, 3), 0);} else {processed = mGrayMat;}// 设置卷积核kernelMat.put(0, 0, kernel);// 卷积操作Imgproc.filter2D(processed, result, CvType.CV_16S, kernelMat);// 转换为绝对值Core.convertScaleAbs(result, result);// 显示结果showMat(targetView, result);return result.clone(); // 返回克隆,原始result会被释放} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "Error in applyEdgeDetection: " + e.getMessage());return null;} finally {// 释放临时资源safeRelease(kernelMat);if (useBlur) safeRelease(processed);// 注意:result在返回克隆后,原始对象会在方法结束时释放safeRelease(result);}}/*** X 轴方向边缘检测*/private void edgeDetectionX() {Mat result = applyEdgeDetection(KERNEL_X, 1, 3, mBinding.ivResultX, false);safeRelease(result); // 释放返回的结果}/*** Y 轴方向边缘检测*/private void edgeDetectionY() {Mat result = applyEdgeDetection(KERNEL_Y, 3, 1, mBinding.ivResultY, false);safeRelease(result);}/*** X、Y 轴方向边缘检测*/private void edgeDetectionXAndY() {// 分别检测X和Y方向Mat resultX = applyEdgeDetection(KERNEL_X, 1, 3, null, false);Mat resultY = applyEdgeDetection(KERNEL_Y, 3, 1, null, false);if (resultX != null && resultY != null && !resultX.empty() && !resultY.empty()) {// 合并结果Mat combined = new Mat();Core.add(resultX, resultY, combined);showMat(mBinding.ivResultXY, combined);safeRelease(combined);}// 释放临时结果safeRelease(resultX);safeRelease(resultY);}/*** 由左上到右下方向边缘检测*/private void edgeDetectionXY() {Mat result = applyEdgeDetection(KERNEL_XY, 3, 3, mBinding.ivResultLeftToRight, true);safeRelease(result);}/*** 由右上到左下方向边缘检测*/private void edgeDetectionYX() {Mat result = applyEdgeDetection(KERNEL_YX, 3, 3, mBinding.ivResultRightToLeft, true);safeRelease(result);}/*** 安全释放Mat资源*/private void safeRelease(Mat mat) {if (mat != null && !mat.empty()) {mat.release();}}/*** 将 Mat 显示到 ImageView*/private void showMat(ImageView view, Mat mat) {if (view == null || mat == null || mat.empty()) return;try {// 创建临时Mat用于转换Mat displayMat = new Mat();// 根据通道数进行转换if (mat.channels() == 1) {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_GRAY2RGBA);} else {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_BGR2RGBA);}// 创建Bitmap并显示Bitmap bmp = Bitmap.createBitmap(displayMat.cols(), displayMat.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(displayMat, bmp);view.setImageBitmap(bmp);// 释放临时MatsafeRelease(displayMat);} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "Error in showMat: " + e.getMessage());}}@Overrideprotected void onDestroy() {super.onDestroy();// 释放所有Mat资源safeRelease(mOriginalMat);safeRelease(mGrayMat);}
}
22. Sobel 算子边缘检测
22.1 什么是 Sobel 算子边缘检测
Sobel 算子是一种离散微分算子,用于计算图像亮度的近似梯度,从而检测边缘区域。其核心思想是通过卷积操作来强调图像中像素值剧烈变化的区域。
-
数学基础
Sobel 算子的本质是一阶导数计算。在图像处理中:
-
一阶导数的峰值对应图像中的边缘
-
梯度指向变化最剧烈的方向
对于图像函数
f(x, y)
,其在点(x, y)
的梯度是一个向量:∇f = [∂f/∂x, ∂f/∂y]ᵀ
梯度幅度(边缘强度)为:
M = √[(∂f/∂x)² + (∂f/∂y)²]
梯度方向为:
θ = arctan(∂f/∂y / ∂f/∂x)
-
-
Sobel 卷积核
Sobel 算子使用两个 3×3 的卷积核分别计算水平和垂直方向的梯度:
水平方向核 (Gx) - 检测垂直边缘:
| -1 0 1 | | -2 0 2 | | -1 0 1 |
垂直方向核 (Gy) - 检测水平边缘:
| -1 -2 -1 | | 0 0 0 | | 1 2 1 |
-
计算过程
-
分别卷积:使用 Gx 和 Gy 分别与图像进行卷积,得到水平梯度分量
grad_x
和垂直梯度分量grad_y
-
计算梯度幅度:
grad = √(grad_x² + grad_y²)
(实际应用中常用近似计算:|grad_x| + |grad_y|
) -
阈值处理:对梯度幅度应用阈值,提取显著的边缘
-
22.2 应用场景
Sobel 边缘检测在移动端应用中广泛使用:
-
文档扫描应用:检测文档边缘以便进行透视校正
-
物体轮廓提取:作为物体识别和形状分析的前置步骤
-
手势识别:提取手部轮廓特征
-
图像增强:创建素描效果或艺术化处理
-
工业检测:在产品质量检测中识别缺陷边缘
-
辅助驾驶系统:简单车道线检测(在资源受限的移动设备上)
特点:
-
计算简单,效率较高
-
对噪声有一定的抑制能力(相比简单梯度算子)
-
只能检测特定方向的边缘
-
边缘较粗,定位精度不如一些高级算法
22.3 示例
SobelActivity.java
public class SobelEdgeDetectionActivity extends AppCompatActivity {// ViewBinding 对象,用于绑定布局中的视图组件private ActivitySobelEdgeDetectionBinding mBinding;// 加载 OpenCV 本地库static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat; // 原始彩色图像@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 初始化 ViewBindingmBinding = ActivitySobelEdgeDetectionBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 加载资源图片(R.drawable.lena)mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "加载图像失败", Toast.LENGTH_SHORT).show();return;}// 显示原始图像showMat(mBinding.ivOriginal, mOriginalMat);// 执行 Sobel 边缘检测(X、Y、XY)executeEdgeDetections();} catch (Exception e) {e.printStackTrace();Toast.makeText(this, "发生错误: " + e.getMessage(), Toast.LENGTH_SHORT).show();}}/*** 执行所有方向的 Sobel 边缘检测*/private void executeEdgeDetections() {edgeDetectionX(); // X 方向edgeDetectionY(); // Y 方向edgeDetectionXAndY(); // XY 方向叠加}/*** X 方向的 Sobel 边缘检测*/private void edgeDetectionX() {Mat resultX = new Mat();// Sobel 处理,dx=2 表示对 X 方向二阶导(不常见)Imgproc.Sobel(mOriginalMat, resultX, CvType.CV_16S, 2, 0, 1);// 转换为可视化图像(8位)Core.convertScaleAbs(resultX, resultX);showMat(mBinding.ivResultX, resultX);// 释放资源safeRelease(resultX);}/*** Y 方向的 Sobel 边缘检测*/private void edgeDetectionY() {Mat resultY = new Mat();// Sobel 处理,dy=1 表示一阶导(常见)Imgproc.Sobel(mOriginalMat, resultY, CvType.CV_16S, 0, 1, 3);Core.convertScaleAbs(resultY, resultY);showMat(mBinding.ivResultY, resultY);safeRelease(resultY);}/*** X 和 Y 方向的边缘检测结果叠加*/private void edgeDetectionXAndY() {Mat resultX = new Mat();Imgproc.Sobel(mOriginalMat, resultX, CvType.CV_16S, 2, 0, 1);Core.convertScaleAbs(resultX, resultX);Mat resultY = new Mat();Imgproc.Sobel(mOriginalMat, resultY, CvType.CV_16S, 0, 1, 3);Core.convertScaleAbs(resultY, resultY);// 将 X 和 Y 的边缘结果相加Mat resultXY = new Mat();Core.add(resultX, resultY, resultXY);showMat(mBinding.ivResultXY, resultXY);safeRelease(resultX);safeRelease(resultY);safeRelease(resultXY);}/*** 安全释放 Mat 对象,避免内存泄漏*/private void safeRelease(Mat mat) {if (mat != null && !mat.empty()) {mat.release();}}/*** 将 OpenCV 的 Mat 图像转换为 Bitmap 并显示到 ImageView 上*/private void showMat(ImageView view, Mat mat) {if (view == null || mat == null || mat.empty()) return;try {Mat displayMat = new Mat();// 若是单通道图像(灰度图),转换为 RGBA 显示if (mat.channels() == 1) {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_GRAY2RGBA);} else {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_BGR2RGBA);}// 创建 Bitmap 并从 Mat 转换Bitmap bmp = Bitmap.createBitmap(displayMat.cols(), displayMat.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(displayMat, bmp);// 显示到 ImageView 上view.setImageBitmap(bmp);// 释放转换后的 MatsafeRelease(displayMat);} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "显示图像时出错: " + e.getMessage());}}@Overrideprotected void onDestroy() {super.onDestroy();// 在销毁 Activity 时释放所有 Mat 资源safeRelease(mOriginalMat);}
}
23. Scharr 算子边缘检测
23.1 什么是 Scharr 算子边缘检测
Scharr 算子是一种用于图像边缘检测的离散微分算子,是 Sobel 算子的优化版本。它通过计算图像灰度函数的梯度近似来检测边缘区域。
-
数学基础
Scharr 算子与 Sobel 算子类似,但使用了不同的卷积核系数,提供了更好的旋转对称性和更准确的梯度近似。
Scharr 卷积核:
水平方向核 (Gx) - 检测垂直边缘:
| -3 0 3 | | -10 0 10 | | -3 0 3 |
垂直方向核 (Gy) - 检测水平边缘:
| -3 -10 -3 | | 0 0 0 | | 3 10 3 |
-
与 Sobel 算子的区别
-
更高的精度:Scharr 算子在 3×3 核大小下提供了更精确的梯度近似
-
更好的旋转对称性:对不同方向的边缘响应更加一致
-
更强的边缘响应:由于系数更大,对边缘的响应比 Sobel 更强
-
23.2 应用场景
Scharr 边缘检测在以下场景中特别有用:
-
需要高精度边缘检测:当图像中的边缘比较微弱或需要更精确的边缘方向时
-
医学图像处理:对边缘精度要求高的应用,如细胞边界检测
-
工业检测:需要精确测量物体尺寸或形状的应用
-
实时性要求较高的场景:与更大核的 Sobel 相比,Scharr 在相同核大小下提供更好性能
-
图像分析:用于需要准确梯度信息的计算机视觉任务
特点:
-
在 3×3 核大小下提供最优的精度
-
对边缘的响应比 Sobel 更好
-
旋转对称性更优
-
计算效率高(与 Sobel 相当)
23.3 示例
ScharrActivity.java
public class ScharrActivity extends AppCompatActivity {// ViewBinding 对象,用于直接访问布局中的视图组件(如 ImageView)private ActivityScharrBinding mBinding;// 加载 OpenCV 所需的本地库(JNI)static {System.loadLibrary("opencv_java4");}// 存储原始图像(Mat 格式)private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 初始化 ViewBinding,替代 findViewByIdmBinding = ActivityScharrBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 加载资源图片(比如 lena.jpg),转为 Mat 对象mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "加载图像失败", Toast.LENGTH_SHORT).show();return;}// 显示原图到 ImageView(mBinding.ivOriginal)showMat(mBinding.ivOriginal, mOriginalMat);// 执行边缘检测(X方向、Y方向、XY合并)executeEdgeDetections();} catch (Exception e) {e.printStackTrace(); // 捕捉并打印错误信息}}/*** 执行所有方向的 Scharr 边缘检测*/private void executeEdgeDetections() {edgeDetectionX(); // X 方向edgeDetectionY(); // Y 方向edgeDetectionXAndY(); // X + Y 合并}/*** 使用 Scharr 算子计算 X 方向的边缘*/private void edgeDetectionX() {Mat resultX = new Mat();// 使用 Scharr 计算 X 方向梯度(dx=1, dy=0)Imgproc.Scharr(mOriginalMat, resultX, CvType.CV_16S, 1, 0);// 将结果转换为 8-bit 范围的图像,便于显示Core.convertScaleAbs(resultX, resultX);// 显示处理结果showMat(mBinding.ivResultX, resultX);// 释放内存safeRelease(resultX);}/*** 使用 Scharr 算子计算 Y 方向的边缘*/private void edgeDetectionY() {Mat resultY = new Mat();// 使用 Scharr 计算 Y 方向梯度(dx=0, dy=1)Imgproc.Scharr(mOriginalMat, resultY, CvType.CV_16S, 0, 1);Core.convertScaleAbs(resultY, resultY);showMat(mBinding.ivResultY, resultY);safeRelease(resultY);}/*** 合并 X 和 Y 两个方向的边缘图像*/private void edgeDetectionXAndY() {Mat resultX = new Mat();Imgproc.Scharr(mOriginalMat, resultX, CvType.CV_16S, 1, 0);Core.convertScaleAbs(resultX, resultX);Mat resultY = new Mat();Imgproc.Scharr(mOriginalMat, resultY, CvType.CV_16S, 0, 1);Core.convertScaleAbs(resultY, resultY);// 使用 add() 合并两个方向的边缘图像Mat resultXY = new Mat();Core.add(resultX, resultY, resultXY);showMat(mBinding.ivResultXY, resultXY);safeRelease(resultX);safeRelease(resultY);safeRelease(resultXY);}/*** 释放 OpenCV 的 Mat 对象,防止内存泄露*/private void safeRelease(Mat mat) {if (mat != null && !mat.empty()) {mat.release();}}/*** 将 Mat 图像显示到指定的 ImageView 中*/private void showMat(ImageView view, Mat mat) {if (view == null || mat == null || mat.empty()) return;try {Mat displayMat = new Mat();// 灰度图需转换为 RGBA 格式以便正确显示if (mat.channels() == 1) {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_GRAY2RGBA);} else {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_BGR2RGBA);}// 创建对应的 Bitmap 容器Bitmap bmp = Bitmap.createBitmap(displayMat.cols(),displayMat.rows(),Bitmap.Config.ARGB_8888);// 将 Mat 数据转换为 BitmapUtils.matToBitmap(displayMat, bmp);// 显示图像到 ImageViewview.setImageBitmap(bmp);// 释放临时 MatsafeRelease(displayMat);} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "显示图像时出错: " + e.getMessage());}}/*** 活动销毁时释放原始图像 Mat*/@Overrideprotected void onDestroy() {super.onDestroy();safeRelease(mOriginalMat);}
}
24. Laplacian 算子边缘检测
24.1 什么是 Laplacian 算子边缘检测
Laplacian 算子是一种基于二阶导数的边缘检测方法,它通过计算图像的二阶导数来检测边缘区域。与一阶导数方法(如Sobel和Scharr)不同,Laplacian能够同时检测边缘的方向和强度变化率。
-
数学基础
Laplacian 算子是二阶微分算子,定义为函数f(x,y)的拉普拉斯变换:
∇²f = ∂²f/∂x² + ∂²f/∂y²
在离散图像处理中,Laplacian 算子通常使用以下卷积核实现:
-
4邻域Laplacian核:
| 0 -1 0 | | -1 4 -1 | | 0 -1 0 |
-
8邻域Laplacian核(包含对角线):
| -1 -1 -1 | | -1 8 -1 | | -1 -1 -1 |
-
-
与一阶导数算子的区别
-
二阶导数特性:Laplacian检测的是像素强度的二阶变化,而不是一阶变化
-
过零检测:边缘对应于Laplacian响应的过零点(从正到负或从负到正的过渡)
-
各向同性:Laplacian算子是旋转不变的,对所有方向的边缘响应相同
-
对噪声更敏感:由于是二阶导数,对噪声比一阶算子更敏感
-
-
计算过程
-
图像预处理:通常先应用高斯模糊减少噪声影响
-
Laplacian卷积:使用Laplacian核与图像进行卷积
-
过零检测:寻找Laplacian响应中从正到负或从负到正的过渡点
-
阈值处理:可选步骤,用于提取显著的边缘
-
24.2 应用场景
Laplacian 边缘检测在以下场景中特别有用:
-
斑点检测:Laplacian对图像中的斑点(blob)结构特别敏感
-
图像锐化:通过从原图像中减去Laplacian结果可以实现图像锐化
-
医学图像分析:用于检测细胞边界、血管结构等
-
工业检测:检测产品表面的缺陷或不规则区域
-
零交叉边缘检测:利用Laplacian响应的过零点进行精确边缘定位
特点:
-
能够同时检测所有方向的边缘
-
对细线和孤立点特别敏感
-
对噪声非常敏感,通常需要先进行平滑处理
-
产生双边缘效应(正负响应)
-
边缘定位精度高
24.3 示例
LaplacianActivity.java
public class LaplacianActivity extends AppCompatActivity {// ViewBinding,用于访问布局中的视图组件(如 ImageView)private ActivityLaplacianBinding mBinding;// 静态代码块:加载 OpenCV 动态库(必须加载)static {System.loadLibrary("opencv_java4");}// 原始图像的 Mat 表示(BGR 彩色图)private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 初始化 ViewBinding,绑定 XML 布局mBinding = ActivityLaplacianBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 使用 OpenCV 工具加载资源图像(R.drawable.lena)mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "加载图像失败", Toast.LENGTH_SHORT).show();return;}// 显示原始图像(彩色)showMat(mBinding.ivOriginal, mOriginalMat);// 执行边缘检测executeEdgeDetections();} catch (Exception e) {e.printStackTrace(); // 打印异常,避免程序崩溃}}/*** 执行两种 Laplacian 边缘检测方法*/private void executeEdgeDetections() {LaplacianEdgeDetection(); // 直接使用 LaplacianGaussianLaplacianEdgeDetection(); // 高斯模糊后再用 Laplacian(LOG)}/*** 普通 Laplacian 边缘检测(不进行预处理)*/private void LaplacianEdgeDetection() {Mat result = new Mat();// Laplacian(src, dst, depth, kernelSize, scale, delta)// 直接对彩色图像做二阶导数边缘检测Imgproc.Laplacian(mOriginalMat, result, CvType.CV_16S, 3, 1.0, 0.0);// 将结果缩放回 CV_8U 范围,适合显示Core.convertScaleAbs(result, result);// 显示到 ImageViewshowMat(mBinding.ivResultLaplacian, result);// 释放资源safeRelease(result);}/*** 高斯模糊后再进行 Laplacian 检测(增强边缘)*/private void GaussianLaplacianEdgeDetection() {Mat resultG = new Mat(); // 存储模糊图Mat result = new Mat(); // 最终边缘图// 先进行高斯模糊,滤除细节和噪声Imgproc.GaussianBlur(mOriginalMat, resultG,new Size(3.0, 3.0), // 卷积核大小5.0, 0.0 // σx=5.0,σy=0.0);// 对模糊图进行 Laplacian 处理(同上)Imgproc.Laplacian(resultG, result, CvType.CV_16S, 3, 1.0, 0.0);Core.convertScaleAbs(result, result);// 显示图像showMat(mBinding.ivResultGaussianLaplacian, result);// 释放内存safeRelease(resultG);safeRelease(result);}/*** 安全释放 Mat 对象,防止内存泄漏*/private void safeRelease(Mat mat) {if (mat != null && !mat.empty()) {mat.release();}}/*** 将 Mat 图像显示到指定的 ImageView 中*/private void showMat(ImageView view, Mat mat) {if (view == null || mat == null || mat.empty()) return;try {Mat displayMat = new Mat();// 若是灰度图,需转换为 RGBA 才能正常显示if (mat.channels() == 1) {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_GRAY2RGBA);} else {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_BGR2RGBA);}// 创建 Bitmap 并从 Mat 转换Bitmap bmp = Bitmap.createBitmap(displayMat.cols(),displayMat.rows(),Bitmap.Config.ARGB_8888);Utils.matToBitmap(displayMat, bmp);// 显示到 ImageViewview.setImageBitmap(bmp);// 释放中间 MatsafeRelease(displayMat);} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "显示图像时出错: " + e.getMessage());}}/*** Activity 销毁时释放原始图像内存*/@Overrideprotected void onDestroy() {super.onDestroy();safeRelease(mOriginalMat);}
}
25. Canny 算法边缘检测
25.1 什么是 Canny
Canny 边缘检测算法是 John F. Canny 于 1986 年提出的多阶段边缘检测算法,被广泛认为是最优的边缘检测算法之一。它通过多个步骤来检测图像中的边缘,具有低错误率、良好定位和最小响应的特点。
-
Canny 边缘检测包含四个主要步骤:
-
高斯滤波降噪
-
使用高斯滤波器平滑图像,减少噪声对边缘检测的影响
-
高斯核的大小和标准差影响平滑程度,较大的核会模糊更多细节但能更好地抑制噪声
-
-
计算梯度幅值和方向
-
使用 Sobel 算子计算图像在 x 和 y 方向的梯度
-
计算梯度幅值:
G = √(Gx² + Gy²)
-
计算梯度方向:
θ = arctan(Gy / Gx)
-
将方向近似到 0°、45°、90° 或 135° 四个方向之一
-
-
非极大值抑制
-
遍历梯度幅值矩阵中的所有点
-
检查每个点在梯度方向上的邻接点
-
如果当前点的梯度幅值不是梯度方向上的局部最大值,则将其抑制(置为零)
-
保留细化的边缘,去除非边缘点
-
-
双阈值检测和边缘连接
-
使用两个阈值:高阈值和低阈值
-
梯度幅值大于高阈值的点被确定为强边缘点
-
梯度幅值低于低阈值的点被抑制
-
梯度幅值在两个阈值之间的点被标记为弱边缘点
-
弱边缘点只有在连接到强边缘点时才会被保留为最终边缘
-
-
-
算法特点
-
低错误率:尽可能少地检测非边缘点
-
高定位精度:检测到的边缘点尽可能接近真实边缘
-
最小响应:对单一边缘只响应一次,避免多个响应
-
25. 2 应用场景
Canny 边缘检测在以下场景中特别有用:
-
精确边缘检测:需要高质量边缘信息的应用
-
计算机视觉:作为物体识别、图像分割等任务的前处理步骤
-
医学影像:检测组织边界、血管结构等
-
工业检测:产品质量控制中的缺陷检测
-
自动驾驶:车道线检测、障碍物边界识别
-
文档分析:文档边界检测、文字分割
特点:
-
提供高质量、连续的边缘
-
对噪声有较好的鲁棒性
-
参数可调,适应不同应用需求
-
计算复杂度相对较高
25.3 示例
CannyActivity.java
public class CannyActivity extends AppCompatActivity {// 用于自动绑定布局中的视图组件(如 ImageView)private ActivityCannyBinding mBinding;// 静态代码块:加载 OpenCV 所需的本地库(必须在使用任何 OpenCV 功能前加载)static {System.loadLibrary("opencv_java4");}// 存储原始图像(OpenCV 的 Mat 格式,默认是 BGR 彩色)private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 初始化 ViewBinding,替代 findViewById 的写法,更安全mBinding = ActivityCannyBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 加载资源图片(res/drawable 中的 lena.jpg)mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "加载图像失败", Toast.LENGTH_SHORT).show();return;}// 显示原图(左上角 ImageView)showMat(mBinding.ivOriginal, mOriginalMat);// 调用 3 种不同方式的 Canny 边缘检测executeEdgeDetections();} catch (Exception e) {e.printStackTrace(); // 打印异常信息,方便调试}}/*** 执行所有 Canny 边缘检测方案*/private void executeEdgeDetections() {lowCannyEdgeDetection(); // 较低阈值(20-40)highCannyEdgeDetection(); // 较高阈值(100-200)edgeDetectionAfterBlur(); // 模糊后再做高阈值 Canny}/*** 低阈值的 Canny 边缘检测:更敏感,能检测更多细节,但容易有噪点*/private void lowCannyEdgeDetection() {Mat result = new Mat();// 参数解释:threshold1=20,threshold2=40,kernel size = 3Imgproc.Canny(mOriginalMat, result, 20.0, 40.0, 3);// 显示结果showMat(mBinding.ivLowCanny, result);// 释放内存safeRelease(result);}/*** 高阈值的 Canny 边缘检测:更保守,只检测强边缘,适合去噪后使用*/private void highCannyEdgeDetection() {Mat result = new Mat();// 参数解释:threshold1=100,threshold2=200Imgproc.Canny(mOriginalMat, result, 100.0, 200.0, 3);showMat(mBinding.ivHighCanny, result);safeRelease(result);}/*** 高斯模糊后再进行 Canny:可以减少噪声干扰,提高边缘稳定性*/private void edgeDetectionAfterBlur() {Mat resultG = new Mat(); // 模糊后的图像Mat result = new Mat(); // Canny 结果图像// 对原图应用高斯模糊(降噪)Imgproc.GaussianBlur(mOriginalMat, resultG, new Size(3.0, 3.0), 5.0);// 在模糊图像上进行 Canny 边缘检测Imgproc.Canny(resultG, result, 100.0, 200.0, 3);showMat(mBinding.ivFilterCanny, result);safeRelease(resultG);safeRelease(result);}/*** 安全释放 Mat 对象资源,避免内存泄漏*/private void safeRelease(Mat mat) {if (mat != null && !mat.empty()) {mat.release();}}/*** 将 Mat 图像渲染到指定的 ImageView 上*/private void showMat(ImageView view, Mat mat) {if (view == null || mat == null || mat.empty()) return;try {Mat displayMat = new Mat();// 灰度图像(单通道)需要转换为 RGBA 才能显示if (mat.channels() == 1) {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_GRAY2RGBA);} else {Imgproc.cvtColor(mat, displayMat, Imgproc.COLOR_BGR2RGBA);}// 创建 Bitmap 显示图像Bitmap bmp = Bitmap.createBitmap(displayMat.cols(),displayMat.rows(),Bitmap.Config.ARGB_8888);Utils.matToBitmap(displayMat, bmp);// 设置到 ImageViewview.setImageBitmap(bmp);safeRelease(displayMat);} catch (Exception e) {e.printStackTrace();Log.e("EdgeDetection", "显示图像时出错: " + e.getMessage());}}/*** 活动销毁时释放资源*/@Overrideprotected void onDestroy() {super.onDestroy();safeRelease(mOriginalMat);}
}