学习 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);
    }
}
相关推荐
每次的天空5 分钟前
Android-MVX技术总结
android
Digitally3 小时前
如何备份和恢复安卓设备2025
android
senator参议员4 小时前
【软件使用】Calibre部分提参
学习
ClassOps4 小时前
腾讯CODING Maven的aar制品添加上传流程
android·java·maven
ClassOps4 小时前
基于腾讯CODING Maven的Android库发布
android·java·maven
星期天要睡觉5 小时前
计算机视觉(opencv)——基于 dlib 关键点定位
人工智能·opencv·计算机视觉
鲸落落丶5 小时前
webpack学习
前端·学习·webpack
zhangrelay5 小时前
操作系统全解析:Windows、macOS与Linux的深度对比与选择指南(AI)
linux·笔记·学习
程序边界5 小时前
AI时代如何高效学习Python:从零基础到项目实战de封神之路(2025升级版)
人工智能·python·学习
路上^_^5 小时前
安卓基础组件024-底部导航栏
android