当前位置: 首页 > backend >正文

学习 Android (十九) 学习 OpenCV (四)

学习 Android (十九) 学习 OpenCV (四)

在上一章节,我们介绍了OpenCV中椒盐噪声的处理方法。椒盐噪声表现为图像中随机出现的黑白像素点,可通过随机生成掩膜来模拟。文章详细阐述了椒盐噪声的数学表达和生成原理,并提供了四种去噪方法:中值滤波(首选)、双边滤波、形态学操作(开/闭运算)和非局部均值滤波(NLM)。每种方法都给出了参数设置建议和使用场景分析。最后通过代码示例展示了如何在Android中实现椒盐噪声的添加和去除,对比了不同去噪方法的效果。这些技术在图像处理算法评估、OCR预处理、数据增强等领域有广泛应用。这一章节我们将进行边缘检测相关知识的学习。

21. 边缘检测

21.1 什么是边缘检测

边缘是图像中亮度或颜色发生显著变化的地方,它通常对应着物体的轮廓、不同材质的边界等。边缘检测是图像处理中提取图像中这种不连续性(边缘)的技术。

边缘的本质是图像中像素值发生剧烈变化的区域。例如,从白色的纸张突然变为深色的桌子,此处的像素强度(在灰度图中)会有很大的差异。

我们的目标是找到这些“变化很大”的地方。在数学上,“变化”可以通过导数梯度来衡量。图像是离散的二维函数,因此我们使用卷积来计算其近似梯度。

  1. 卷积 (filter2D)

    • 卷积是一种数学运算,它使用一个称为卷积核(或滤波器)的小矩阵,在图像上滑动并计算其覆盖区域的加权和。

    • 对于边缘检测,我们设计的卷积核可以近似计算图像在某个方向上的导数

    • filter2D(src, dst, ddepth, kernel) 是执行该操作的核心函数。

      • src: 输入图像(如灰度图)。

      • dst: 输出图像,将存储卷积结果。

      • ddepth: 输出图像的深度(例如,CvType.CV_16S 表示16位有符号整数,用于存储可能为负的卷积结果)。

      • kernel: 承载我们意图的卷积核。

  2. 绝对值与缩放 (convertScaleAbs)

    • 卷积计算梯度时,在边缘的一侧,像素值从高到低变化,梯度为负;在另一侧,从低到高变化,梯度为正。但我们通常只关心变化的强度,而不关心方向。

    • 卷积后的图像可能包含负数值,如果直接用8位无符号整数(0-255)格式显示,这些负值会被截断,导致信息丢失。

    • convertScaleAbs(src, dst) 函数解决了这个问题:

      1. 计算绝对值 (Abs):将图像中的每个值取绝对值。这样,无论是正梯度还是负梯度,都变为正数,代表了边缘的强度。

      2. 缩放转换 (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 |
    
  • 计算过程

    1. 分别卷积:使用 Gx 和 Gy 分别与图像进行卷积,得到水平梯度分量 grad_x 和垂直梯度分量 grad_y

    2. 计算梯度幅度grad = √(grad_x² + grad_y²)(实际应用中常用近似计算:|grad_x| + |grad_y|

    3. 阈值处理:对梯度幅度应用阈值,提取显著的边缘

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 算子的区别

    1. 更高的精度:Scharr 算子在 3×3 核大小下提供了更精确的梯度近似

    2. 更好的旋转对称性:对不同方向的边缘响应更加一致

    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 |
      
  • 与一阶导数算子的区别

    1. 二阶导数特性:Laplacian检测的是像素强度的二阶变化,而不是一阶变化

    2. 过零检测:边缘对应于Laplacian响应的过零点(从正到负或从负到正的过渡)

    3. 各向同性:Laplacian算子是旋转不变的,对所有方向的边缘响应相同

    4. 对噪声更敏感:由于是二阶导数,对噪声比一阶算子更敏感

  • 计算过程

    1. 图像预处理:通常先应用高斯模糊减少噪声影响

    2. Laplacian卷积:使用Laplacian核与图像进行卷积

    3. 过零检测:寻找Laplacian响应中从正到负或从负到正的过渡点

    4. 阈值处理:可选步骤,用于提取显著的边缘

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 边缘检测包含四个主要步骤

    1. 高斯滤波降噪

      • 使用高斯滤波器平滑图像,减少噪声对边缘检测的影响

      • 高斯核的大小和标准差影响平滑程度,较大的核会模糊更多细节但能更好地抑制噪声

    2. 计算梯度幅值和方向

      • 使用 Sobel 算子计算图像在 x 和 y 方向的梯度

      • 计算梯度幅值:G = √(Gx² + Gy²)

      • 计算梯度方向:θ = arctan(Gy / Gx)

      • 将方向近似到 0°、45°、90° 或 135° 四个方向之一

    3. 非极大值抑制

      • 遍历梯度幅值矩阵中的所有点

      • 检查每个点在梯度方向上的邻接点

      • 如果当前点的梯度幅值不是梯度方向上的局部最大值,则将其抑制(置为零)

      • 保留细化的边缘,去除非边缘点

    4. 双阈值检测和边缘连接

      • 使用两个阈值:高阈值和低阈值

      • 梯度幅值大于高阈值的点被确定为强边缘点

      • 梯度幅值低于低阈值的点被抑制

      • 梯度幅值在两个阈值之间的点被标记为弱边缘点

      • 弱边缘点只有在连接到强边缘点时才会被保留为最终边缘

  • 算法特点

    1. 低错误率:尽可能少地检测非边缘点

    2. 高定位精度:检测到的边缘点尽可能接近真实边缘

    3. 最小响应:对单一边缘只响应一次,避免多个响应

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);}
}

在这里插入图片描述
在这里插入图片描述

http://www.xdnf.cn/news/19838.html

相关文章:

  • sql项目总结
  • 无人机报警器8G信号技术解析
  • npm install 报错问题解决 npm install --ignore-scripts
  • 嵌入式学习---(单片机)
  • 【Kubernetes知识点】监控升级,备份及Kustomize管理
  • Python 基础语法与控制流程学习笔记
  • 学习笔记:MYSQL(3)(常用函数和约束)
  • 嵌入式人别再瞎折腾了!这8个开源项目,解决按键/队列/物联网所有痛点,小白也能抄作业
  • 【JVS更新日志】低代码、物联网、无忧企业计划9.3更新说明!
  • GitLab Boards 深度解析:选型、竞品、成本与资源消耗
  • 上下文记忆力媲美Genie3,且问世更早:港大和可灵提出场景一致的交互式视频世界模型!
  • MindNode AI:AI辅助思维导图工具,高效整理思路快速搭框架
  • React学习教程,从入门到精通, React 组件语法知识点(9)
  • 【108】基于51单片机智能输液监测系统【Proteus仿真+Keil程序+报告+原理图】
  • 浅谈linux内存管理 的RMAP机制的作用和原理
  • 指针高级(1)
  • leetcode 38 外观数列
  • 线程通信机制
  • 【程序人生】有梦想就能了不起,就怕你没梦想
  • BurpSuite_Pro_V2024.6使用教程-Burp Suite代理设置详解
  • (Me)Adobe Media Encoder 2025音视频格式转码软件及视频编码软件,全新版免激活,安装即永久使用!
  • HTTP协议——理解相关概念、模拟实现浏览器访问自定义服务器
  • 优化程序性能 | 《深入理解计算机系统》第五章笔记
  • React实现列表拖拽排序
  • LiteFlow:国产流程编排引擎体验
  • DAY20-新世纪DL(DeepLearning/深度学习)战士:终(目标检测/YOLO)3
  • 【医疗行业案例】基于 React 的预约系统:DHTMLX 助力高效排班与预约管理
  • CAD/BIM软件产品技术深度分析文章写作计划
  • 全渠道 + 低代码:如何打造 “内外协同” 的客服管理系统体系?
  • 【FastDDS】Layer DDS之Domain ( 02-DomainParticipant )