【android opencv学习笔记】Day 21: 形态学开运算与闭运算

形态学开运算与闭运算详解

开运算与闭运算是腐蚀和膨胀的组合形态学运算,是图像处理中去噪、补洞、目标分割的核心工具。

本文将从原理讲解、API解析、Android完整源码实现三个维度,带你彻底掌握这两种运算。


核心原理:开运算与闭运算

1. 前置知识回顾

  • 腐蚀(Erosion):邻域有黑则中心变黑,收缩前景、去除亮噪声
  • 膨胀(Dilation):邻域有白则中心变白,扩张前景、填充暗孔洞

开运算与闭运算就是这两个基础操作的有序组合。


2. 开运算(Opening):先腐蚀后膨胀

定义开运算 = 腐蚀(Erosion) → 膨胀(Dilation)

  • 操作顺序:先对图像腐蚀,再用相同结构元素膨胀
  • 效果:
    1. 消除背景中的小白点噪声(腐蚀阶段直接吃掉无法被结构元素容纳的小物体)
    2. 平滑前景物体的边缘
    3. 不明显改变前景物体的整体大小(腐蚀后再膨胀,主体轮廓会恢复)
  • 典型场景:文本识别前去除背景噪点、工业质检中过滤微小亮点缺陷

3. 闭运算(Closing):先膨胀后腐蚀

定义闭运算 = 膨胀(Dilation) → 腐蚀(Erosion)

  • 操作顺序:先对图像膨胀,再用相同结构元素腐蚀
  • 效果:
    1. 填充前景物体内部的小黑孔洞/裂缝(膨胀阶段先把洞填满,再腐蚀恢复轮廓)
    2. 连接邻近的小目标(比如断裂的文字笔画)
    3. 不明显改变前景物体的整体大小(膨胀后再腐蚀,主体轮廓会恢复)
  • 典型场景:修复扫描文本中的断裂文字、填充目标物体的内部孔洞

4. 关键性质与对比

运算 操作顺序 核心作用 适用场景
开运算 腐蚀→膨胀 去亮噪、平滑边缘 背景噪点去除、前景物体分离
闭运算 膨胀→腐蚀 补暗洞、连接目标 物体内部孔洞填充、断裂目标连接

注意:开运算和闭运算都是幂等运算,即对同一图像重复执行相同运算,结果不会再发生变化。


OpenCV 核心 API 解析

OpenCV 提供了 cv::morphologyEx 函数,可直接实现所有高级形态学运算,无需手动组合腐蚀和膨胀。

1. 函数原型(C++)

cpp 复制代码
void morphologyEx(
    InputArray src,          // 输入图像(单通道,灰度/二值)
    OutputArray dst,         // 输出图像
    int op,                  // 形态学运算类型
    InputArray kernel,       // 结构元素(由getStructuringElement生成)
    Point anchor = Point(-1,-1), // 锚点(默认中心)
    int iterations = 1       // 重复次数(一般为1)
);

2. 关键参数 op 取值

  • MORPH_OPEN:开运算(先腐蚀后膨胀)
  • MORPH_CLOSE:闭运算(先膨胀后腐蚀)
  • MORPH_GRADIENT:形态学梯度(膨胀-腐蚀,提取边缘)
  • MORPH_TOPHAT:顶帽运算(原图-开运算,提取亮噪声)
  • MORPH_BLACKHAT:黑帽运算(闭运算-原图,提取暗孔洞)

3. Android/Java 版本 API

java 复制代码
Imgproc.morphologyEx(
    Mat src,                // 输入图像
    Mat dst,                // 输出图像
    int op,                 // 运算类型(如Imgproc.MORPH_OPEN)
    Mat kernel              // 结构元素
);

Android 完整项目实现

1. 布局文件:activity_main.xml

垂直分布,依次显示原图、开运算结果、闭运算结果:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="8dp">

    <!-- 原图 -->
    <ImageView
        android:id="@+id/iv_original"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

    <!-- 开运算结果 -->
    <ImageView
        android:id="@+id/iv_opened"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="4dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

    <!-- 闭运算结果 -->
    <ImageView
        android:id="@+id/iv_closed"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="4dp"
        android:scaleType="fitCenter"
        android:adjustViewBounds="true"/>

</LinearLayout>

2. Kotlin 上层代码:MainActivity.kt

负责图片加载、调用 C++ 算法、结果展示:

kotlin 复制代码
package com.nicoli.hellomorphology

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 接口声明
    // 1. 二值化预处理
    private external fun binarizeImage(src: Bitmap, out: Bitmap, thresh: Int)
    // 2. 开运算(先腐蚀后膨胀)
    private external fun openImage(src: Bitmap, out: Bitmap, kernelSize: Int)
    // 3. 闭运算(先膨胀后腐蚀)
    private external fun closeImage(src: Bitmap, out: Bitmap, kernelSize: Int)

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

        // 1. 加载原图(带噪点和孔洞的二值图像)
        val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.noisy_binary)

        // 2. 创建输出位图
        val binaryBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
        val openedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)
        val closedBitmap = Bitmap.createBitmap(originalBitmap.width, originalBitmap.height, Bitmap.Config.ARGB_8888)

        // 3. 执行算法流程
        binarizeImage(originalBitmap, binaryBitmap, 127)  // 二值化预处理
        openImage(binaryBitmap, openedBitmap, 5)           // 5×5 结构元素开运算
        closeImage(binaryBitmap, closedBitmap, 5)         // 5×5 结构元素闭运算

        // 4. 显示结果
        findViewById<ImageView>(R.id.iv_original).setImageBitmap(originalBitmap)
        findViewById<ImageView>(R.id.iv_opened).setImageBitmap(openedBitmap)
        findViewById<ImageView>(R.id.iv_closed).setImageBitmap(closedBitmap)
    }
}

3. C++ 核心算法:native-lib.cpp(逐行注释)

cpp 复制代码
#include <jni.h>
#include <opencv2/opencv.hpp>
#include <android/bitmap.h>

using namespace cv;
using namespace std;

// ====================== 工具函数:Bitmap ↔ Mat 转换 ======================
Mat bitmapToMat(JNIEnv *env, jobject bitmap) {
    AndroidBitmapInfo info;
    void* pixels;
    AndroidBitmap_getInfo(env, bitmap, &info);
    AndroidBitmap_lockPixels(env, bitmap, &pixels);

    Mat rgba(info.height, info.width, CV_8UC4, pixels);
    Mat gray;
    cvtColor(rgba, gray, COLOR_RGBA2GRAY); // 转为灰度图
    AndroidBitmap_unlockPixels(env, bitmap);
    return gray;
}

void matToBitmap(JNIEnv *env, const Mat& srcMat, jobject dstBitmap) {
    AndroidBitmapInfo info;
    void* pixels;
    AndroidBitmap_getInfo(env, dstBitmap, &info);
    AndroidBitmap_lockPixels(env, dstBitmap, &pixels);

    Mat rgba;
    cvtColor(srcMat, rgba, COLOR_GRAY2RGBA); // 转为 RGBA
    memcpy(pixels, rgba.data, info.width * info.height * 4);
    AndroidBitmap_unlockPixels(env, dstBitmap);
}

// ====================== 预处理:图像二值化 ======================
void binarizeImpl(const Mat& src, Mat& out, int threshVal) {
    threshold(src, out, threshVal, 255, THRESH_BINARY);
}

// ====================== 核心:开运算(先腐蚀后膨胀) ======================
void openImpl(const Mat& src, Mat& out, int kernelSize) {
    // 创建结构元素:kernelSize×kernelSize 矩形
    Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
    // 直接调用 morphologyEx 实现开运算
    morphologyEx(src, out, MORPH_OPEN, element);
}

// ====================== 核心:闭运算(先膨胀后腐蚀) ======================
void closeImpl(const Mat& src, Mat& out, int kernelSize) {
    // 创建结构元素:kernelSize×kernelSize 矩形
    Mat element = getStructuringElement(MORPH_RECT, Size(kernelSize, kernelSize));
    // 直接调用 morphologyEx 实现闭运算
    morphologyEx(src, out, MORPH_CLOSE, element);
}

// ====================== JNI 接口 ======================
extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_binarizeImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint thresh) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    binarizeImpl(src, out, thresh);
    matToBitmap(env, out, outBitmap);
}

extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_openImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    openImpl(src, out, kernelSize);
    matToBitmap(env, out, outBitmap);
}

extern "C" JNIEXPORT void JNICALL
Java_com_nicoli_hellomorphology_MainActivity_closeImage
(JNIEnv *env, jobject thiz, jobject srcBitmap, jobject outBitmap, jint kernelSize) {
    Mat src = bitmapToMat(env, srcBitmap);
    Mat out;
    closeImpl(src, out, kernelSize);
    matToBitmap(env, out, outBitmap);
}

效果与参数详解

1. 运行效果

  • 原图:带有背景小白噪点和前景内部小黑洞的二值图像
  • 开运算结果:背景中的小白噪点被完全消除,前景物体边缘平滑,整体大小无明显变化
  • 闭运算结果:前景物体内部的小黑洞被填充,邻近的断裂目标被连接,整体大小无明显变化

2. 关键参数说明

  • 结构元素大小(kernelSize)
    • 3×3:轻微去噪/补洞,适合小瑕疵处理
    • 5×5/7×7:效果更明显,可去除较大噪点或填充较大孔洞
  • 结构元素形状
    • MORPH_RECT:矩形(默认,适合大多数场景)
    • MORPH_CROSS:十字形(适合线性目标处理)
    • MORPH_ELLIPSE:椭圆形(适合圆形目标处理)

扩展应用:形态学滤波组合使用

在实际项目中,通常会先开运算再闭运算,实现"去噪+补洞"的完整图像预处理流程:

cpp 复制代码
// 先开运算去除背景噪点
morphologyEx(src, opened, MORPH_OPEN, element);
// 再闭运算填充前景孔洞
morphologyEx(opened, finalResult, MORPH_CLOSE, element);

这种组合可以同时消除背景噪声和前景瑕疵,得到干净的二值图像,为后续的轮廓检测、连通组件分析等步骤打下基础。


总结

开运算与闭运算是形态学滤波的进阶操作,它们通过组合腐蚀和膨胀,解决了单一操作无法同时处理背景噪声和前景瑕疵的问题。在文本识别、工业质检、医学影像处理等场景中,它们是图像预处理的核心工具。

相关推荐
zhangfeng11334 小时前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
_李小白4 小时前
【Android车载学习笔记】第四天:AAOS系统架构
android·笔记·学习
Upsy-Daisy4 小时前
AI Agent 项目学习笔记(十):文件操作、终端执行与 PDF 生成工具
笔记·学习·pdf
nashane4 小时前
HarmonyOS 6学习:动画流畅与截图性能的双重优化实战
学习·华为·harmonyos
ゆづき4 小时前
AI能否替代小说作家?
人工智能·笔记·学习·其他·生活
_李小白4 小时前
【android opencv学习笔记】Day 20: 形态学滤波的腐蚀与膨胀
笔记·学习
圆粥綠4 小时前
【保姆级】国内Windows用户Android Studio下载+安装+配置完整教程(2026最新版,避坑指南)
android·windows·android studio
User_芊芊君子4 小时前
一条命令搞定 mysql_exporter 部署,Shell 脚本把重复配置这件事自动化了
android·mysql·自动化
rosemary5124 小时前
推理框架负责人 — 学习路线 (inference-framework-learning-path)
学习