学习 Android (二十二) 学习 OpenCV (七)

学习 Android (二十二) 学习 OpenCV (七)

在上一章节,我们对霍夫直线、霍夫圆、直线拟合、轮廓发现与绘制、轮廓面积与周长、轮廓外接多边形、判断轮廓的几何形状、凸包检测和凸缺陷有了一定的学习和了解,那我们接着继续学习 OpenCV 的相关知识。

37 模板匹配

37.1 什么是模板匹配

模板匹配(Template Matching)是一种在较大图像中搜索和查找模板图像位置的方法。其核心思想是通过滑动模板图像 over the source image,并在每个位置计算相似度度量,最终找到最匹配的位置。

数学原理:

模板匹配的数学本质是图像相关性和相似性计算 。对于源图像 III 和模板图像 TTT,在源图像的每个位置 (x,y)(x,y)(x,y) 计算相似度得分:

text 复制代码
R(x,y)=∑x′,y′​(T(x′,y′)−I(x+x′,y+y′))2

实际应用中,OpenCV 提供了多种匹配方法,每种方法有不同的计算公式和特性。

匹配方法

OpenCV 提供了 6 种不同的匹配方法:

方法 公式 最佳匹配 说明
TM_SQDIFF R=∑(T−I)2R = \sum(T - I)^2R=∑(T−I)2 最小值 平方差匹配
TM_SQDIFF_NORMED R=∑(T−I)2∑T2⋅∑I2R = \frac{\sum(T - I)^2}{\sqrt{\sum T^2 \cdot \sum I^2}}R=∑T2⋅∑I2 ∑(T−I)2 最小值 归一化平方差匹配
TM_CCORR R=∑(T⋅I)R = \sum(T \cdot I)R=∑(T⋅I) 最大值 相关匹配
TM_CCORR_NORMED R=∑(T⋅I)∑T2⋅∑I2R = \frac{\sum(T \cdot I)}{\sqrt{\sum T^2 \cdot \sum I^2}}R=∑T2⋅∑I2 ∑(T⋅I) 最大值 归一化相关匹配
TM_CCOEFF R=∑(T′⋅I′)R = \sum(T' \cdot I')R=∑(T′⋅I′) 最大值 相关系数匹配
TM_CCOEFF_NORMED R=∑(T′⋅I′)∑T′2⋅∑I′2R = \frac{\sum(T' \cdot I')}{\sqrt{\sum T'^2 \cdot \sum I'^2}}R=∑T′2⋅∑I′2 ∑(T′⋅I′) 最大值 归一化相关系数匹配

其中 T′=T−TˉT' = T - \bar{T}T′=T−Tˉ, I′=I−IˉI' = I - \bar{I}I′=I−Iˉ (Tˉ\bar{T}Tˉ 和 Iˉ\bar{I}Iˉ 是均值)

  • TM_SQDIFF(平方差匹配)

    • 计算模板和图像窗口的像素差的平方和。

    • 值越小,匹配越好

    • 缺点:对亮度变化非常敏感。

  • TM_SQDIFF_NORMED(归一化平方差匹配)

    • TM_SQDIFF 的归一化版本,结果在 [0,1] 之间。

    • 更稳定,但依然受光照影响。

  • TM_CCORR(相关匹配)

    • 计算模板和图像窗口的相关性。

    • 值越大,匹配越好

    • 适合亮度一致的情况,但不做归一化,受图像强度影响。

  • TM_CCORR_NORMED(归一化相关匹配)

    • TM_CCORR 的归一化版本。

    • 结果范围 [0,1],更健壮。

    • 常用方法之一。

  • TM_CCOEFF(相关系数匹配)

    • 考虑了均值的相关系数,能减弱亮度整体偏移的影响。

    • 值越大,匹配越好

  • TM_CCOEFF_NORMED(归一化相关系数匹配)

    • TM_CCOEFF 的归一化版本,范围 [-1,1]。

    • 1 表示完全匹配,-1 表示完全不匹配。

    • 最常用,鲁棒性较强

工作流程

  1. 准备阶段:加载源图像和模板图像

  2. 匹配阶段:使用选定的匹配方法计算相似度图

  3. 分析阶段:寻找相似度图的极值点(最小值或最大值)

  4. 结果可视化:在源图像上标记匹配区域

37.2 核心函数详解

核心函数

java 复制代码
// 模板匹配核心函数
Imgproc.matchTemplate(Mat image, Mat templ, Mat result, int method)

参数说明

  • image:源图像,必须是8位或32位浮点图像

  • templ:模板图像,不能大于源图像且类型相同

  • result:匹配结果图,单通道32位浮点图像

  • method:匹配方法(见上述6种方法)

辅助函数

java 复制代码
// 查找矩阵中的最小值和最大值位置
Core.MinMaxLocResult Core.minMaxLoc(Mat mat)

// 归一化图像
Core.normalize(Mat src, Mat dst, double alpha, double beta, int norm_type)

// 图像转换
Imgproc.cvtColor(Mat src, Mat dst, int code) // 颜色空间转换

MinMaxLocResult 结构

java 复制代码
Core.MinMaxLocResult result = Core.minMaxLoc(matchResult);
double minVal = result.minVal;    // 最小值
double maxVal = result.maxVal;    // 最大值
Point minLoc = result.minLoc;     // 最小值位置
Point maxLoc = result.maxLoc;     // 最大值位置

37.3 应用场景

  1. 工业视觉检测

    • 产品质量控制:检测产品上的标志、标签或缺陷

    • 零件定位:在装配线上精确定位零件

    • 印刷品检测:检查印刷质量和对齐精度

  2. 文档处理

    • 表单识别:识别和提取表单中的特定字段

    • 印章检测:检测文档中的印章或签名区域

    • 文档比对:查找不同版本文档间的差异

  3. 游戏与自动化

    • 游戏机器人:自动识别游戏界面元素

    • UI自动化测试:验证应用程序界面元素位置

    • 屏幕 scraping:从屏幕截图中提取特定信息

  4. 增强现实与计算机视觉

    • 标记跟踪:识别和跟踪视觉标记

    • 物体检测:在复杂场景中定位特定物体

    • 图像检索:在图像库中查找相似图像

  5. 移动应用开发

    • 实时相机处理:在相机预览中实时检测对象

    • 图像编辑工具:提供基于模板的编辑功能

    • 教育应用:开发视觉学习工具

37.4 示例

TemplateMatchingActivity.java

java 复制代码
public class TemplateMatchingActivity extends AppCompatActivity {

    private ActivityTemplateMatchingBinding mBinding;

    static {
        System.loadLibrary("opencv_java4"); // 加载 OpenCV 动态库
    }

    private Mat mOriginalMat, mTemplateMat; // 原始图像和模板图像

    // 匹配方法(字符串显示给用户看)
    private final String[] matchMethods = {
            "TM_SQDIFF - 平方差匹配法",
            "TM_SQDIFF_NORMED - 归一化平方差匹配法",
            "TM_CCORR - 相关匹配法",
            "TM_CCORR_NORMED - 归一化相关匹配法",
            "TM_CCOEFF - 相关系数匹配法",
            "TM_CCOEFF_NORMED - 归一化相关系数匹配法"
    };

    // 对应 OpenCV 内部的匹配方法常量
    private final int[] methodValues = {
            Imgproc.TM_SQDIFF, Imgproc.TM_SQDIFF_NORMED,
            Imgproc.TM_CCORR, Imgproc.TM_CCORR_NORMED,
            Imgproc.TM_CCOEFF, Imgproc.TM_CCOEFF_NORMED
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityTemplateMatchingBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        setupSpinner(); // 设置下拉框选择匹配方法

        try {
            // 加载原图和模板
            mOriginalMat = Utils.loadResource(this, R.drawable.wuping);
            mTemplateMat = Utils.loadResource(this, R.drawable.muban);

            // 显示在 UI 上
            OpenCVHelper.showMat(mBinding.ivSource, mOriginalMat);
            OpenCVHelper.showMat(mBinding.ivTemplate, mTemplateMat);

            // 单模板匹配按钮
            mBinding.btnProcess.setOnClickListener(view -> {
                if (checkImagesReady()) {
                    processSingleTemplateMatching();
                }
            });

            // 多目标匹配按钮
            mBinding.btnMultiMatch.setOnClickListener(view -> {
                if (checkImagesReady()) {
                    processMultiTemplateMatching();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 检查图像是否准备好
     */
    private boolean checkImagesReady() {
        if (mOriginalMat == null || mTemplateMat == null) {
            Toast.makeText(this, "请先选择源图像和模板图像", Toast.LENGTH_SHORT).show();
            return false;
        }

        // 模板尺寸不能大于源图
        if (mTemplateMat.cols() > mOriginalMat.cols() || mTemplateMat.rows() > mOriginalMat.rows()) {
            Toast.makeText(this, "模板图像不能大于源图像", Toast.LENGTH_SHORT).show();
            return false;
        }

        return true;
    }

    /**
     * 设置下拉框,选择模板匹配方法
     */
    private void setupSpinner() {
        ArrayAdapter<String> adapter = new ArrayAdapter<>(
                this, android.R.layout.simple_spinner_item, matchMethods);
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        mBinding.spMethod.setAdapter(adapter);
        mBinding.spMethod.setSelection(5); // 默认 TM_CCOEFF_NORMED
    }

    /**
     * 单次模板匹配(找出最佳位置)
     */
    private void processSingleTemplateMatching() {
        try {
            // 获取用户选择的匹配方法
            int selectedMethod = methodValues[mBinding.spMethod.getSelectedItemPosition()];

            // 结果矩阵大小 (W-w+1, H-h+1)
            int resultCols = mOriginalMat.cols() - mTemplateMat.cols() + 1;
            int resultRows = mOriginalMat.rows() - mTemplateMat.rows() + 1;
            Mat result = new Mat(resultRows, resultCols, CvType.CV_32FC1);

            // 执行模板匹配
            long startTime = System.currentTimeMillis();
            Imgproc.matchTemplate(mOriginalMat, mTemplateMat, result, selectedMethod);
            long endTime = System.currentTimeMillis();

            // 获取最小值/最大值以及对应的位置
            Core.MinMaxLocResult mmr = Core.minMaxLoc(result);

            Point matchLoc;
            double matchValue;
            boolean isMinBest = (selectedMethod == Imgproc.TM_SQDIFF ||
                    selectedMethod == Imgproc.TM_SQDIFF_NORMED);

            // 对于平方差,最小值才是最佳匹配;其余方法最大值才是最佳匹配
            if (isMinBest) {
                matchLoc = mmr.minLoc;
                matchValue = mmr.minVal;
            } else {
                matchLoc = mmr.maxLoc;
                matchValue = mmr.maxVal;
            }

            // 克隆原图,用于绘制匹配结果
            Mat displayMat = mOriginalMat.clone();

            // 绘制矩形框
            Point topLeft = matchLoc;
            Point bottomRight = new Point(matchLoc.x + mTemplateMat.cols(),
                    matchLoc.y + mTemplateMat.rows());
            Imgproc.rectangle(displayMat, topLeft, bottomRight, new Scalar(0, 255, 0), 4);

            // 绘制中心点
            Point center = new Point(matchLoc.x + mTemplateMat.cols() / 2,
                    matchLoc.y + mTemplateMat.rows() / 2);
            Imgproc.circle(displayMat, center, 8, new Scalar(255, 0, 0), -1);

            // 显示匹配置信度和位置
            String infoText = String.format("Confidence level: %.4f Position: (%.0f,%.0f)",
                    matchValue, matchLoc.x, matchLoc.y);
            Imgproc.putText(displayMat, infoText,
                    new Point(10, 30), Imgproc.FONT_HERSHEY_SIMPLEX,
                    1, new Scalar(0, 0, 255), 2);

            // 显示耗时
            String timeText = String.format("Processing Time: %dms", (endTime - startTime));
            Imgproc.putText(displayMat, timeText,
                    new Point(10, 70), Imgproc.FONT_HERSHEY_SIMPLEX,
                    1, new Scalar(0, 255, 255), 2);

            // 显示结果
            OpenCVHelper.showMat(mBinding.ivResult, displayMat);

            // 更新 TextView
            String resultText = String.format("匹配方法: %s\n最佳位置: (%.0f, %.0f)\n置信度: %.4f\n处理时间: %dms",
                    matchMethods[mBinding.spMethod.getSelectedItemPosition()],
                    matchLoc.x, matchLoc.y, matchValue,
                    (endTime - startTime));
            mBinding.tvResult.setText(resultText);

            result.release();
            displayMat.release();

        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "模板匹配失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    /**
     * 多目标模板匹配
     */
    private void processMultiTemplateMatching() {
        try {
            int selectedMethod = methodValues[mBinding.spMethod.getSelectedItemPosition()];
            boolean isMinBest = (selectedMethod == Imgproc.TM_SQDIFF ||
                    selectedMethod == Imgproc.TM_SQDIFF_NORMED);

            // 结果矩阵
            int resultCols = mOriginalMat.cols() - mTemplateMat.cols() + 1;
            int resultRows = mOriginalMat.rows() - mTemplateMat.rows() + 1;
            Mat result = new Mat(resultRows, resultCols, CvType.CV_32FC1);

            long startTime = System.currentTimeMillis();
            Imgproc.matchTemplate(mOriginalMat, mTemplateMat, result, selectedMethod);
            long endTime = System.currentTimeMillis();

            // 计算阈值(决定多少结果算匹配)
            Core.MinMaxLocResult mmr = Core.minMaxLoc(result);
            double threshold;
            if (isMinBest) {
                threshold = mmr.minVal * 1.5;
            } else {
                threshold = mmr.maxVal * 0.95;
            }

            // 扫描所有位置,找到超过阈值的点
            List<Point> matches = new ArrayList<>();
            for (int y = 0; y < result.rows(); y++) {
                for (int x = 0; x < result.cols(); x++) {
                    double[] value = result.get(y, x);
                    if (value != null && value.length > 0) {
                        if ((isMinBest && value[0] <= threshold) ||
                                (!isMinBest && value[0] >= threshold)) {
                            matches.add(new Point(x, y));
                        }
                    }
                }
            }

            // 使用非最大抑制,去掉重叠框
            List<Point> finalMatches = nonMaxSuppression(matches, mTemplateMat.cols() / 2);

            // 绘制结果
            Mat displayMat = mOriginalMat.clone();
            for (Point match : finalMatches) {
                Point topLeft = match;
                Point bottomRight = new Point(match.x + mTemplateMat.cols(),
                        match.y + mTemplateMat.rows());
                Imgproc.rectangle(displayMat, topLeft, bottomRight, new Scalar(0, 255, 0), 3);

                // 标号
                Imgproc.putText(displayMat, String.valueOf(finalMatches.indexOf(match) + 1),
                        new Point(match.x + 5, match.y + 20),
                        Imgproc.FONT_HERSHEY_SIMPLEX, 0.7,
                        new Scalar(255, 255, 0), 2);
            }

            // 显示检测信息
            String infoText = String.format("Found %d match Time: %dms",
                    finalMatches.size(), (endTime - startTime));
            Imgproc.putText(displayMat, infoText,
                    new Point(10, 30), Imgproc.FONT_HERSHEY_SIMPLEX,
                    1, new Scalar(0, 0, 255), 2);

            OpenCVHelper.showMat(mBinding.ivResult, displayMat);
            mBinding.tvResult.setText(String.format("找到 %d 个匹配对象\n处理时间: %dms",
                    finalMatches.size(), (endTime - startTime)));

            result.release();
            displayMat.release();

        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "多对象匹配失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    /**
     * 非最大抑制:避免多个检测框重叠
     */
    private List<Point> nonMaxSuppression(List<Point> matches, double minDistance) {
        List<Point> result = new ArrayList<>();
        for (Point match : matches) {
            boolean isOverlap = false;
            for (Point existing : result) {
                double distance = Math.sqrt(Math.pow(match.x - existing.x, 2) +
                        Math.pow(match.y - existing.y, 2));
                if (distance < minDistance) {
                    isOverlap = true;
                    break;
                }
            }
            if (!isOverlap) {
                result.add(match);
            }
        }
        return result;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);
        if (mTemplateMat != null) OpenCVHelper.safeRelease(mTemplateMat);
    }
}
  • 模板匹配核心

    • Imgproc.matchTemplate(src, template, result, method)

    • 结果矩阵 result 保存了模板在每个位置的匹配分数。

    • 不同 method 决定是取最小值(平方差)还是取最大值(相关系数)。

  • 单匹配

    • 找到唯一最佳位置(min 或 max)。

    • 用绿色矩形框框出模板在原图的位置。

    • 额外绘制中心点和匹配置信度。

  • 多匹配

    • 扫描 result 全图。

    • 根据阈值(不同方法不同)筛选多个候选点。

    • 非最大抑制(NMS) 去掉重叠结果。

    • 给每个检测到的对象画矩形并编号。

  • 关键点

    • checkImagesReady() 确保模板图像不能比原图大。

    • nonMaxSuppression() 避免多个检测框堆叠。

    • spinner 控制用户切换不同的匹配方法。



38 二维码 QR 检测与识别

38.1 什么是 QR

QR码(Quick Response Code)是一种矩阵二维码,由日本Denso Wave公司于1994年发明。其结构包含以下关键部分:

  • 位置探测图形:三个相同的位于角落的Finder Pattern,用于定位二维码

  • 对齐图案:帮助校正扭曲的二维码

  • 定时图案:黑白相间的线条,用于确定模块坐标

  • 格式信息:包含纠错等级和掩模模式信息

  • 版本信息:标识QR码的版本(1-40)

  • 数据和纠错码:实际存储的数据和Reed-Solomon纠错码

  • 安静区:二维码周围的空白区域,便于识别

QR码检测主要基于以下计算机视觉技术:

  1. 位置探测图形识别

    • 查找三个特定的Finder Pattern(黑白黑比例1:1:3:1:1)

    • 使用轮廓分析、几何特征匹配和比例验证

  2. 透视变换校正

    • 通过三个Finder Pattern的位置计算透视变换矩阵

    • 将倾斜的二维码校正为正面视角

  3. 模块解析

    • 根据版本信息确定模块数量和排列方式

    • 读取格式信息和版本信息

    • 解析数据模块并应用Reed-Solomon纠错

  4. 数据解码

    • 解析数据流(数字、字母数字、字节、汉字等模式)

    • 应用纠错算法修复损坏的数据

38.2 核心函数详解

OpenCV提供了专门的QR码检测与识别类:

java 复制代码
// QR码检测器主要类
org.opencv.objdetect.QRCodeDetector

核心方法:

java 复制代码
// 检测并解码QR码
boolean decode(Mat img, Mat points, String decodedInfo)

// 检测QR码(不解码)
boolean detect(Mat img, Mat points)

// 多二维码检测
boolean detectAndDecodeMulti(Mat img, List<String> decodedInfo, List<Mat> points)

// 同时检测和解码
String detectAndDecode(Mat img, Mat points, Mat straight_qrcode)

参数说明

  • img:输入图像,灰度或彩色

  • points:输出的QR码角点位置(4个点)

  • decodedInfo:解码后的字符串信息

  • straight_qrcode:校正后的二维码图像

辅助方法:

java 复制代码
// 绘制检测结果
Imgproc.line(Mat img, Point pt1, Point pt2, Scalar color) // 绘制边界线
Imgproc.putText(Mat img, String text, Point org, int fontFace, double fontScale, Scalar color) // 绘制文本

// 透视变换
Imgproc.getPerspectiveTransform(Mat src, Mat dst) // 获取透视变换矩阵
Imgproc.warpPerspective(Mat src, Mat dst, Mat M, Size dsize) // 应用透视变换

// 图像预处理
Imgproc.cvtColor(Mat src, Mat dst, int code) // 颜色空间转换
Imgproc.GaussianBlur(Mat src, Mat dst, Size ksize, double sigmaX) // 高斯模糊
Imgproc.threshold(Mat src, Mat dst, double thresh, double maxval, int type) // 阈值处理

38.3 应用场景

QR码在移动应用中有着广泛的应用场景:

  1. 商业与支付

    • 移动支付:支付宝、微信支付等扫码支付

    • 商品信息:扫描商品二维码获取详细信息

    • 促销活动:扫描二维码参与促销或获取优惠券

  2. 信息传递

    • 网址链接:快速访问网站或下载应用

    • 联系方式:vCard格式的联系人信息交换

    • WiFi连接:扫描二维码自动连接WiFi网络

  3. 票务与身份验证

    • 电子票务:演唱会、电影票、交通票务

    • 身份认证:登录验证、门禁系统

    • 会员管理:会员卡、积分系统

  4. 工业与物流

    • 库存管理:商品追踪和管理

    • 物流跟踪:包裹追踪和配送信息

    • 设备管理:设备标识和维护信息

  5. 教育与生活

    • 学习资源:链接到在线教育内容

    • 公共场所:博物馆、公园的导览信息

    • 疫情防控:健康码、行程码等

38.4 示例

QRCodeDetectionActivity.java

java 复制代码
public class QRCodeDetectionActivity extends AppCompatActivity {

    private ActivityQrcodeDetectionBinding mBinding;

    static {
        System.loadLibrary("opencv_java4"); // 加载 OpenCV 的 JNI 库
    }

    private Mat mOriginalMat, mGrayMat;
    private QRCodeDetector qrCodeDetector = new QRCodeDetector(); // OpenCV 自带的二维码检测器

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityQrcodeDetectionBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            // 从资源文件加载一张图片
            mOriginalMat = Utils.loadResource(this, R.drawable.byqc);
            // 显示原始图像
            OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);

            // 转灰度,检测/解码 QR 更稳定
            mGrayMat = new Mat();
            Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);

            detectQR();  // 检测 QR 二维码边界
            decodeQR();  // 解码 QR 二维码内容
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 检测二维码边界,并在图像上绘制检测结果
     */
    private void detectQR() {
        Mat points = new Mat();
        // detect() 只检测二维码是否存在,并返回角点
        boolean detect = qrCodeDetector.detect(mGrayMat, points);
        if (detect) {
            try {
                // 转换为彩色图,方便绘制彩色线条
                Mat result = new Mat();
                Imgproc.cvtColor(mGrayMat, result, Imgproc.COLOR_GRAY2BGR);

                // points 是 1x4x2 矩阵,保存二维码的四个角点
                if (points.cols() == 4) {
                    Point[] corners = new Point[4];
                    for (int i = 0; i < 4; i++) {
                        double[] pointData = points.get(0, i); // 获取单个角点 [x, y]
                        corners[i] = new Point(pointData[0], pointData[1]);
                    }
                    Log.e("detectQR", Arrays.toString(corners));

                    // 绘制二维码边界(闭合四边形)
                    for (int i = 0; i < 4; i++) {
                        Imgproc.line(result,
                                corners[i],                   // 当前角点
                                corners[(i + 1) % 4],         // 下一个角点(最后一个连回第一个)
                                new Scalar(255, 0, 0), 4);    // 蓝色线条,线宽4
                    }

                    // 在角点位置画绿色圆点
                    for (Point corner : corners) {
                        Imgproc.circle(result, corner, 4, new Scalar(0, 255, 0), 4);
                    }

                    // 显示结果图
                    OpenCVHelper.showMat(mBinding.ivDetect, result);
                    OpenCVHelper.safeRelease(result);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 解码二维码内容
     */
    private void decodeQR() {
        Mat points = new Mat();
        boolean detect = qrCodeDetector.detect(mGrayMat, points);
        if (detect) {
            // decode() 尝试解析二维码的内容
            String result = qrCodeDetector.decode(mGrayMat, points);
            if (result != null) {
                Log.e("ZJH", "解码结果为: " + result);
                mBinding.tvResult.setText("解码结果为:\n" + result);
            } else {
                mBinding.tvResult.setText("解码结果为: 无法解码");
                Toast.makeText(this, "无法解码", Toast.LENGTH_SHORT).show();
            }
        } else {
            mBinding.tvResult.setText("解码结果为: 未检测到QRCode");
            Toast.makeText(this, "未检测到QRCode", Toast.LENGTH_SHORT).show();
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);
        if (mGrayMat != null) OpenCVHelper.safeRelease(mGrayMat);
    }
}

39. 图像分割:漫水填充法

图像分割就是把图像分成若干个特定的、具有独特性质的区域并提出感兴趣目标的技术和过程。它是由图像处理到图像分析的关键步骤。现有的图像分割方法主要分以下几类:基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法以及基于特定理论的分割方法等。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号。

39.1 什么是漫水填充法

漫水填充法(Flood Fill)是一种基于区域生长的图像分割算法,其工作原理类似于画图软件中的"油漆桶"工具。算法从一个种子点开始,检查相邻像素的颜色相似性,将颜色相似的相邻像素填充为指定颜色。

算法原理

  1. 选择一个种子点(起始点)

  2. 检查种子点的颜色值

  3. 检查相邻像素(4连通或8连通)的颜色是否在允许的容差范围内

  4. 将符合条件的像素填充为指定颜色

  5. 递归或迭代地继续这个过程,直到没有符合条件的相邻像素

39.2 核心函数详解

OpenCV中的漫水填充函数是Imgproc.floodFill(),它有多个重载版本,最常用的如下:

java 复制代码
public static int floodFill(
    Mat image, 
    Mat mask, 
    Point seedPoint, 
    Scalar newVal, 
    Rect rect, 
    Scalar loDiff, 
    Scalar hiDiff, 
    int flags
)

参数说明:

  1. image:输入/输出图像,可以是单通道或多通道

  2. mask:操作掩码,必须比原图宽和高各大2个像素

  3. seedPoint:填充起始点(种子点)

  4. newVal:填充的新颜色值

  5. rect:可选参数,设置填充区域的最小边界矩形

  6. loDiff:当前像素与种子像素或相邻像素之间的最大下限差异

  7. hiDiff:当前像素与种子像素或相邻像素之间的最大上限差异

  8. flags:操作标志,控制填充行为

标志位(flags)详解 :

flags参数由三部分组成:

  • 连通性:4(默认)或8连通

  • 填充模式:FLOODFILL_FIXED_RANGE或FLOODFILL_MASK_ONLY

  • 掩码值:填充掩码的值(0-255),左移8位

常用组合:

java 复制代码
// 4连通,标准填充
int flags = 4;

// 8连通,固定范围填充
int flags = 8 | Imgproc.FLOODFILL_FIXED_RANGE;

// 只填充掩码,不修改原图
int flags = 4 | Imgproc.FLOODFILL_MASK_ONLY;

// 设置掩码填充值为128
int flags = 4 | (128 << 8);

39.3 应用场景

  1. 图像分割:将图像中颜色相似的区域分割出来

  2. 背景替换:填充背景区域以实现背景替换

  3. 目标提取:提取特定颜色的对象

  4. 图像编辑:实现类似油漆桶的填充功能

  5. 连通区域分析:标记和统计图像中的连通区域

39.4 示例

FloodFillActivity.java

java 复制代码
public class FloodFillActivity extends AppCompatActivity {

    private ActivityFloodFillBinding mBinding;

    static {
        System.loadLibrary("opencv_java4"); // 加载 OpenCV 库
    }

    private Mat mOriginalMat; // 原始图像
    private int connectionType = 4; // 连通性 (4-连通 / 8-连通)
    private int floodFillFlag = 0;  // floodFill 的标志位
    private double loDiff = 0.0;    // 灰度下界差值
    private double upDiff = 0.0;    // 灰度上界差值

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityFloodFillBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            // 加载测试图片
            mOriginalMat = Utils.loadResource(this, R.drawable.lxh);
            OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // loDiff 调节 (灰度差下限)
        mBinding.sbLo.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                mBinding.tvLo.setText("" + i);
                loDiff = i;       // 设置下界
                doFloodFill();    // 每次调整重新执行 floodFill
            }
            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onStopTrackingTouch(SeekBar seekBar) {}
        });

        // upDiff 调节 (灰度差上限)
        mBinding.sbUp.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
                mBinding.tvUp.setText("" + i);
                upDiff = i;       // 设置上界
                doFloodFill();    // 每次调整重新执行 floodFill
            }
            @Override public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override public void onStopTrackingTouch(SeekBar seekBar) {}
        });

        // 选择连通性:4 邻域 / 8 邻域
        mBinding.rgColor.setOnCheckedChangeListener((radioGroup, checkedId) -> {
            if (checkedId == R.id.rb_4) {
                connectionType = 4;
            } else if (checkedId == R.id.rb_8) {
                connectionType = 8;
            }
            doFloodFill();
        });

        // 勾选 "Fixed Range" 开启固定范围模式
        mBinding.cbRange.setOnCheckedChangeListener((compoundButton, b) -> {
            if (b) {
                floodFillFlag |= Imgproc.FLOODFILL_FIXED_RANGE;
            } else {
                floodFillFlag &= ~Imgproc.FLOODFILL_FIXED_RANGE;
            }
            doFloodFill();
        });

        // 勾选 "Mask Only" 只输出掩膜
        mBinding.cbOnly.setOnCheckedChangeListener((compoundButton, b) -> {
            if (b) {
                floodFillFlag |= Imgproc.FLOODFILL_MASK_ONLY;
            } else {
                floodFillFlag &= ~Imgproc.FLOODFILL_MASK_ONLY;
            }
            doFloodFill();
        });

        // 初始执行一次
        doFloodFill();
    }

    private void doFloodFill() {
        try {
            // 克隆一份原始图像,避免修改源图
            Mat result = mOriginalMat.clone();

            // 注意:mask 必须比原图大 2 个像素
            Mat maskers = new Mat(mOriginalMat.rows() + 2, mOriginalMat.cols() + 2,
                    CvType.CV_8UC1, new Scalar(0));

            // 种子点选在图像中心
            Point seedPoint = new Point(mOriginalMat.cols() / 2.0, mOriginalMat.rows() / 2.0);

            // 执行 FloodFill
            Imgproc.floodFill(
                    result,               // 输入图像(会被修改)
                    maskers,              // 掩膜(必须大 2)
                    seedPoint,            // 种子点
                    new Scalar(65.0, 105.0, 225.0), // 填充颜色 (BGR: RoyalBlue)
                    new Rect(),           // 输出填充区域的边界矩形(可忽略)
                    new Scalar(loDiff, loDiff, loDiff), // loDiff:允许的下界差值
                    new Scalar(upDiff, upDiff, upDiff), // upDiff:允许的上界差值
                    connectionType | floodFillFlag     // 标志位(连通性 + 选项)
            );

            // 判断是否只显示 MASK 掩膜
            if ((floodFillFlag & Imgproc.FLOODFILL_MASK_ONLY) == Imgproc.FLOODFILL_MASK_ONLY) {
                // mask 原始范围是 0/1,需要转化为 0/255 才能显示
                Mat displayMask = new Mat();
                Core.multiply(maskers, new Scalar(255), displayMask);
                OpenCVHelper.showMat(mBinding.ivFillResult, displayMask);
                OpenCVHelper.safeRelease(displayMask);
            } else {
                // 显示填充后的结果图
                OpenCVHelper.showMat(mBinding.ivFillResult, result);
            }

            // 释放内存
            OpenCVHelper.safeRelease(maskers);
            OpenCVHelper.safeRelease(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);
    }
}

40 图像分割:分水岭法

40.1 什么分水岭法

分水岭算法(Watershed Algorithm)是一种基于拓扑理论的数学形态学分割方法,其基本思想是将图像视为地理拓扑表面,其中像素的灰度值表示海拔高度。高灰度值对应山峰,低灰度值对应山谷。

算法原理

  1. 地理隐喻:将图像看作地形图,亮度值代表海拔

  2. "注水"过程:从局部最小值开始注水,水会逐渐填满盆地

  3. 分水岭形成:当来自不同盆地的水相遇时,形成分水岭(边界)

  4. 分割结果:分水岭将图像分割成不同的区域

数学基础

分水岭算法基于数学形态学中的距离变换和梯度计算,通过寻找图像的局部最小值和分水岭线来实现分割。

40.2 核心函数详解

OpenCV中的分水岭函数是Imgproc.watershed()

java 复制代码
public static void watershed(Mat image, Mat markers)

参数说明:

  1. image:输入图像,必须是8位3通道图像(CV_8UC3)

  2. markers:输入/输出标记矩阵,数据类型为32位单通道整型(CV_32SC1)

标记矩阵(markers)的含义

  • 0:未知区域(算法将确定这些区域的归属)

  • 正整数:表示不同的前景对象

  • -1:表示分水岭(边界)

40.3 应用场景

  1. 接触对象分离:分离相互接触的物体(如细胞、颗粒等)

  2. 图像分割:将图像分割成有意义的区域

  3. 医学图像处理:细胞计数、组织分割

  4. 工业检测:零件计数、缺陷检测

  5. 计算机视觉:对象识别和分割的预处理步骤

40.4 示例

WatershedActivity.java

java 复制代码
public class WatershedActivity extends AppCompatActivity {

    private ActivityWatershedBinding mBinding;

    static {
        System.loadLibrary("opencv_java4"); // 加载 OpenCV 库
    }

    private Mat mOriginalMat; // 原始图像 Mat

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityWatershedBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            // 加载原始图片
            mOriginalMat = Utils.loadResource(this, R.drawable.lena);
            OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);

            Mat grayMat = new Mat();
            // 1. 灰度化
            Imgproc.cvtColor(mOriginalMat, grayMat, Imgproc.COLOR_RGB2GRAY);
            // 2. 高斯滤波,去噪声
            Imgproc.GaussianBlur(grayMat, grayMat, new Size(3, 3), 2);
            // 3. 边缘检测 (Canny)
            Imgproc.Canny(grayMat, grayMat, 80, 150);

            // 4. 查找轮廓
            List<MatOfPoint> contours = new ArrayList<>();
            Mat hierarchy = new Mat(); // 层级关系
            Imgproc.findContours(
                    grayMat, contours, hierarchy,
                    Imgproc.RETR_TREE,       // 检测所有轮廓并重构层级
                    Imgproc.CHAIN_APPROX_SIMPLE, // 压缩水平、垂直、对角冗余点
                    new Point()
            );

            // 5. 创建空白 Mat
            Mat imageContours = new Mat(mOriginalMat.size(), CvType.CV_8UC1); // 用于绘制轮廓
            Mat marks = new Mat(mOriginalMat.size(), CvType.CV_32S);          // 用于分水岭标记
            marks.setTo(Scalar.all(0)); // 初始化为 0

            int index = 0, compCount = 0;

            // hierarchy 数据展开到数组
            int[] hierarchyData = new int[(int) hierarchy.total() * hierarchy.channels()];
            hierarchy.get(0, 0, hierarchyData);

            // 6. 遍历轮廓并绘制到 marks / imageContours
            for (; index >= 0; index = hierarchyData[index * 4], compCount++) {
                // 绘制到 marks (作为分水岭算法输入的 marker)
                Imgproc.drawContours(
                        marks, contours, index,
                        new Scalar(compCount + 1), // 每个区域标记不同整数
                        1, 8, hierarchy, Integer.MAX_VALUE, new Point()
                );

                // 绘制到 imageContours (仅用于显示)
                Imgproc.drawContours(
                        imageContours, contours, index,
                        new Scalar(255), // 白色
                        1, 8, hierarchy, Integer.MAX_VALUE, new Point()
                );
            }

            // 7. 分水岭算法执行
            Imgproc.watershed(mOriginalMat, marks);

            // 8. 转换标记矩阵为可视化图像
            Mat afterWatershed = new Mat();
            Core.convertScaleAbs(marks, afterWatershed);
            OpenCVHelper.showMat(mBinding.ivWater, afterWatershed);

            // 9. 生成随机颜色的分割结果
            Mat perspectiveImage = Mat.zeros(mOriginalMat.size(), CvType.CV_8UC3);
            for (int i = 0; i < marks.rows(); i++) {
                for (int j = 0; j < marks.cols(); j++) {
                    double[] pixel = marks.get(i, j); // 获取标记值
                    int value = (int) pixel[0];

                    if (value == -1) {
                        // 分界线(-1 标记):白色
                        perspectiveImage.put(i, j, 255, 255, 255);
                    } else {
                        // 其它区域:赋予随机颜色
                        double[] color = randomColor(value);
                        perspectiveImage.put(i, j, color);
                    }
                }
            }
            OpenCVHelper.showMat(mBinding.ivPush, perspectiveImage);

            // 10. 原图与分割结果融合显示
            Mat wshed = new Mat();
            Core.addWeighted(mOriginalMat, 0.4, perspectiveImage, 0.6, 0, wshed);
            OpenCVHelper.showMat(mBinding.ivResult, wshed);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 随机颜色生成函数
     * @param value 分区标记值
     * @return BGR 颜色数组
     */
    private double[] randomColor(int value) {
        value = value % 255;  // 控制范围 0-255
        Random rng = new Random();
        int aa = rng.nextInt(value + 1);  // 蓝通道
        int bb = rng.nextInt(value + 1);  // 绿通道
        int cc = rng.nextInt(value + 1);  // 红通道
        return new double[]{aa, bb, cc}; // BGR 顺序
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat); // 释放资源
    }
}

41 图像分割:Grabcut

41.1 什么是 Grabcut

GrabCut 是一种基于图割(graph cut)的交互式图像分割算法,由 Microsoft Research 的 Rother 等人提出。与漫水填充相比,GrabCut 能够处理更复杂的图像分割任务,特别适合前景-背景分离。

算法原理

  1. 初始化:用户指定一个包含前景的矩形区域或提供前景/背景的种子点

  2. 高斯混合模型(GMM):算法为前景和背景分别建立颜色分布模型

  3. 图割优化:将图像构建为图结构,通过最小化能量函数找到最佳分割

  4. 迭代优化:通过多次迭代不断优化GMM模型和分割结果

能量函数最小化

GrabCut 最小化的能量函数包括:

  • 数据项:衡量像素属于前景或背景的概率

  • 平滑项:鼓励相邻相似像素具有相同标签

41.2 核心函数详解

OpenCV中的GrabCut函数是Imgproc.grabCut()

java 复制代码
public static void grabCut(
    Mat img, 
    Mat mask, 
    Rect rect, 
    Mat bgdModel, 
    Mat fgdModel, 
    int iterCount, 
    int mode
)

参数说明:

  1. img:输入图像,必须是8位3通道图像(CV_8UC3)

  2. mask:输入/输出掩码,尺寸与图像相同,指定像素的初始标签

  3. rect:包含前景的矩形区域(如果使用矩形初始化)

  4. bgdModel:背景模型的临时数组

  5. fgdModel:前景模型的临时数组

  6. iterCount:算法迭代次数

  7. mode:操作模式,决定如何初始化掩码

掩码(mask)值含义:

java 复制代码
// 定义在Imgproc类中
public static final int GC_BGD = 0;    // 明确背景
public static final int GC_FGD = 1;    // 明确前景
public static final int GC_PR_BGD = 2; // 可能背景
public static final int GC_PR_FGD = 3; // 可能前景

操作模式(mode)

java 复制代码
public static final int GC_INIT_WITH_RECT = 0; // 使用矩形初始化
public static final int GC_INIT_WITH_MASK = 1; // 使用掩码初始化
public static final int GC_EVAL = 2;           // 重新评估

41.3 应用场景

  1. 人物抠图:从复杂背景中提取人物

  2. 物体提取:分离图像中的特定物体

  3. 背景替换:为图像更换背景

  4. 图像编辑:Photoshop-like的智能选择工具

  5. 计算机视觉预处理:为其他算法提供精确的前景掩码

41.4 示例

GrabCutActivity.java

java 复制代码
public class GrabCutActivity extends AppCompatActivity {
    private ActivityGrabCutBinding mBinding;

    static {
        System.loadLibrary("opencv_java4");
    }

    private Mat mOriginalMat;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityGrabCutBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            mOriginalMat = Utils.loadResource(this, R.drawable.lena);
            OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);
            new Thread(() -> doBgdGrabCut()).start();
            new Thread(() -> doFgdGrabCut()).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void doBgdGrabCut() {
        try {
            // 拷贝一份用于绘制矩形
            Mat rectMat = new Mat();
            mOriginalMat.copyTo(rectMat);

            // 定义矩形 ROI
            Rect rect = new Rect(80, 30, 340, 390);
            Imgproc.rectangle(rectMat, rect, Scalar.all(255.0), 2);

            // 在 UI 线程显示矩形框
            runOnUiThread(() -> OpenCVHelper.showMat(mBinding.ivOriginal, rectMat));

            // 创建背景/前景模型
            Mat bgdModel = Mat.zeros(1, 65, CvType.CV_64FC1);
            Mat fgdModel = Mat.zeros(1, 65, CvType.CV_64FC1);

            // 初始化 mask
            Mat mask = Mat.zeros(mOriginalMat.size(), CvType.CV_8UC1);

            // 调用 GrabCut
            Imgproc.grabCut(mOriginalMat, mask, rect, bgdModel, fgdModel,
                    5, Imgproc.GC_INIT_WITH_RECT);

            // 遍历 mask,把背景标记为 255,前景为 0
            for (int i = 0; i < mask.rows(); i++) {
                for (int j = 0; j < mask.cols(); j++) {
                    double[] value = mask.get(i, j);
                    int v = (int) value[0];
                    if (v == Imgproc.GC_BGD || v == Imgproc.GC_PR_BGD) {
                        mask.put(i, j, 255.0); // 背景
                    } else {
                        mask.put(i, j, 0.0);   // 前景
                    }
                }
            }

            // 通过 mask 提取背景
            Mat result = new Mat();
            Core.bitwise_and(mOriginalMat, mOriginalMat, result, mask);

            // 在 UI 线程显示结果
            runOnUiThread(() -> OpenCVHelper.showMat(mBinding.ivBgd, result));

            // 释放临时资源
            rectMat.release();
            mask.release();
            bgdModel.release();
            fgdModel.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void doFgdGrabCut() {
        try {
            // 拷贝一份用于绘制矩形
            Mat rectMat = new Mat();
            mOriginalMat.copyTo(rectMat);

            // 定义矩形 ROI
            Rect rect = new Rect(80, 30, 340, 390);
            Imgproc.rectangle(rectMat, rect, Scalar.all(255.0), 2);

            // 在 UI 线程显示矩形框
            runOnUiThread(() -> OpenCVHelper.showMat(mBinding.ivOriginal, rectMat));

            // 创建背景/前景模型
            Mat bgdModel = Mat.zeros(1, 65, CvType.CV_64FC1);
            Mat fgdModel = Mat.zeros(1, 65, CvType.CV_64FC1);

            // 初始化 mask
            Mat mask = Mat.zeros(mOriginalMat.size(), CvType.CV_8UC1);

            // 调用 GrabCut
            Imgproc.grabCut(mOriginalMat, mask, rect, bgdModel, fgdModel,
                    5, Imgproc.GC_INIT_WITH_RECT);

            // 遍历 mask,将前景和可能的前景标记为 255,其他设为 0
            for (int i = 0; i < mask.rows(); i++) {
                for (int j = 0; j < mask.cols(); j++) {
                    double[] value = mask.get(i, j);
                    int v = (int) value[0];
                    if (v == Imgproc.GC_FGD || v == Imgproc.GC_PR_FGD) {
                        mask.put(i, j, 255.0);
                    } else {
                        mask.put(i, j, 0.0);
                    }
                }
            }

            // 通过 mask 提取前景
            Mat result = new Mat();
            Core.bitwise_and(mOriginalMat, mOriginalMat, result, mask);

            // 在 UI 线程显示结果
            runOnUiThread(() -> OpenCVHelper.showMat(mBinding.ivFgd, result));

            // 释放临时资源
            rectMat.release();
            mask.release();
            bgdModel.release();
            fgdModel.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);
    }
}

42 图像修复

42.1 什么是图像修复

图像修复(Image Inpainting)是一种用于修复图像中损坏或缺失部分的技术,其目标是使修复后的区域看起来自然且与周围环境无缝融合。OpenCV提供了两种主要的图像修复算法:

基于快速行进方法(Telea算法)

  • 原理:基于快速行进方法(Fast Marching Method),从已知区域向未知区域传播信息

  • 工作方式:算法从损坏区域的边界开始,逐步向内填充,使用周围像素的加权平均值

  • 特点:速度较快,适合小到中等大小的修复区域

基于 Navier-Stokes 方程(NS算法):

  • 原理:基于流体动力学和偏微分方程,模拟"等照度线"(相同强度的线)的传播

  • 工作方式:将图像修复视为流体动力学问题,保持边缘连续性

  • 特点:通常产生更平滑的结果,适合较大的修复区域

42.2 核心函数详解

OpenCV中的图像修复函数是photo.inpaint()

java 复制代码
public static void inpaint(Mat src, Mat inpaintMask, Mat dst, double inpaintRadius, int flags)

参数说明

  1. src:输入图像,必须是8位单通道或3通道图像

  2. inpaintMask:修复掩码,8位单通道图像,非零像素表示需要修复的区域

  3. dst:输出图像,与输入图像相同的尺寸和类型

  4. inpaintRadius:修复半径,算法考虑的每个受损像素周围的圆形邻域的半径

  5. flags:修复方法,可以是:

    • INPAINT_TELEA:基于Telea的算法

    • INPAINT_NS:基于Navier-Stokes的算法

42.3 应用场景

  1. 老照片修复:去除照片中的划痕、污渍和折痕

  2. 水印去除:从图像中移除不需要的文字或标志

  3. 对象移除:移除图像中的不需要的物体或人物

  4. 图像编辑:修复红眼、瑕疵或其他缺陷

  5. 文档修复:修复扫描文档中的损坏部分

  6. 视频修复:修复视频帧中的损坏区域

42.4 示例

InpaintingActivity.java

java 复制代码
public class InpaintingActivity extends AppCompatActivity {

    private ActivityInpaintingBinding mBinding;

    static {
        // 加载 OpenCV 动态库(opencv_java4)
        System.loadLibrary("opencv_java4");
    }

    // 保存原始图像的 Mat 对象
    private Mat mOriginalMat;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityInpaintingBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        try {
            // 加载资源图片(R.drawable.lena2)并转换为 Mat 格式
            mOriginalMat = Utils.loadResource(this, R.drawable.lena2);

            // 显示原始图像
            OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);

            // 将原图转为灰度图
            Mat grayMat = new Mat();
            Imgproc.cvtColor(mOriginalMat, grayMat, Imgproc.COLOR_RGB2GRAY);

            // 创建掩膜图像(单通道,初始全黑)
            Mat imageMask = new Mat(mOriginalMat.size(), CvType.CV_8UC1, Scalar.all(0));

            // 将灰度图中亮度 > 240 的像素点设为白色(255),其余为黑色
            // 即提取出高亮区域作为修复的掩膜
            Imgproc.threshold(grayMat, imageMask, 240, 255, Imgproc.THRESH_BINARY);

            // 创建 3x3 的矩形结构元素(用于膨胀操作)
            Mat Kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));

            // 对掩膜进行膨胀,扩大白色区域范围,方便修复效果更明显
            Imgproc.dilate(imageMask, imageMask, Kernel);

            // 使用 OpenCV 的图像修复算法 (Inpainting)
            // 参数:
            //   mOriginalMat - 原图
            //   imageMask    - 掩膜图(白色区域为修复目标)
            //   mOriginalMat - 输出图像(直接覆盖原图)
            //   5            - 修复半径
            //   Photo.INPAINT_TELEA - Telea 算法(另一种是 Photo.INPAINT_NS)
            Photo.inpaint(mOriginalMat, imageMask, mOriginalMat, 5, Photo.INPAINT_TELEA);

            // 显示掩膜图
            OpenCVHelper.showMat(mBinding.ivMask, imageMask);

            // 显示修复后的图像
            OpenCVHelper.showMat(mBinding.ivFix, mOriginalMat);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 回收 Mat,避免内存泄漏
        if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);
    }
}
相关推荐
Digitally3 小时前
比较 iPhone:全面比较 iPhone 17 系列
android·ios·iphone
被开发耽误的大厨3 小时前
鸿蒙项目篇-22-项目功能结构说明-写子页面和导航页面
android·华为·harmonyos·鸿蒙
FlYFlOWERANDLEAF3 小时前
DevExpress中Word Processing Document API学习记录
学习·c#·word
半夏知半秋3 小时前
基于跳跃表的zset实现解析(lua版)
服务器·开发语言·redis·学习·lua
AnySpaceOne3 小时前
PDF转Word在线转换教程:多种实用方法分享
学习·pdf·word
xiaohouzi1122333 小时前
Python读取视频-硬解和软解
python·opencv·ffmpeg·视频编解码·gstreamer
安然~~~4 小时前
mysql多表联查
android·数据库·mysql
2501_915909067 小时前
HTTPS 错误解析,常见 HTTPS 抓包失败、443 端口错误与 iOS 抓包调试全攻略
android·网络协议·ios·小程序·https·uni-app·iphone
在路上`9 小时前
前端学习之后端java小白(四)之数据库设计
sql·学习