【android opencv学习笔记】Day 2: Mat类(图片数据结构体)

OpenCV 核心类 Mat

本文从原理、代码实现、函数详解、实战效果全方位讲解 OpenCV 中最核心的 Mat 类,适合 Android NDK + OpenCV 初学者系统学习。


什么是 OpenCV Mat

Mat(Matrix)是 OpenCV 中用于存储图像/矩阵数据的核心类 ,是所有图像处理操作的基础。

简单理解:Mat = 图像在内存中的容器,由两部分组成:

  1. 头部信息:图像宽、高、通道数、数据类型、尺寸等描述信息
  2. 像素数据指针:指向真实图像像素数组的内存地址

在 Android OpenCV 开发中,所有图像加载、处理、保存都必须依赖 Mat 实现


Mat 类核心成员函数

函数/用法 作用 适用场景
Mat(rows, cols, type, Scalar) 创建指定尺寸、类型、像素值的图像 创建纯色图/空矩阵
create(rows, cols, type) 重新分配 Mat 内存,释放旧数据 复用 Mat 对象,节省内存
clone() 深拷贝,创建独立的图像副本 原图与拷贝图互不影响
copyTo(Mat) 深拷贝,效果同 clone() 图像数据复制
imread(path) 从文件路径读取图像到 Mat 加载本地图片
imwrite(path, mat) 将 Mat 保存为图片文件 保存处理后的图像
flip(src, dst, flag) 图像翻转(水平/垂直/双向) 图像镜像操作
convertTo(dst, type, scale) 转换 Mat 数据类型(如8位转浮点型) 归一化、数值计算

核心知识点:浅拷贝 VS 深拷贝

本文代码中最重要的知识点,也是 Mat 最容易踩坑的地方:

  1. 浅拷贝

    cpp 复制代码
    cv::Mat image4(image3);
    image1 = image3;

    ✅ 共用同一块像素内存

    ✅ 无额外内存开销

    ❌ 一个对象修改,所有拷贝对象同步变化

  2. 深拷贝

    cpp 复制代码
    cv::Mat image5 = image3.clone();
    image3.copyTo(image2);

    ✅ 独立内存,互不影响

    ❌ 占用双倍内存

    ✅ 处理图像时推荐使用


完整实战工程代码

1. C++ 核心代码:native-lib.cpp

cpp 复制代码
// 引入 JNI 头文件,用于 Java 和 C++ 相互调用
#include <jni.h>
#include <string>
// 引入 OpenCV 核心头文件,所有图像处理功能都在这里
#include <opencv2/opencv.hpp>
// 引入 Android Bitmap 操作头文件,用于把 Mat 转成 Android 可用的图片
#include <android/bitmap.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <android/log.h>

#define LOG_TAG "OpenCV_Mat_Demo"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

namespace {
    cv::Mat createTestImage() {
        // 创建 500x500 单通道、值为 50 的灰度图
        cv::Mat ima(500, 500, CV_8U, cv::Scalar(50));
        return ima;
    }
}

/**
 * JNI 导出函数(供 Kotlin 调用)
 * 函数名规则:Java_包名_类名_方法名
 *
 * @param env    JNI 环境变量,用于操作 Java 对象
 * @param thiz   当前 Java 对象(MainActivity)的引用
 * @param imagePath  输入图片路径
 * @param outputDir  图片保存目录
 * @return     执行结果 0成功 -1失败
 */
extern "C"
JNIEXPORT jint  JNICALL
Java_com_nicoli_hellomat_MainActivity_runMatDemo(
        JNIEnv *env,
        jobject thiz,
        jstring imagePath, jstring outputDir
) {
    const char* inputPath = env->GetStringUTFChars(imagePath, nullptr);
    const char* outDir = env->GetStringUTFChars(outputDir, nullptr);

    LOGI("输入图片路径: %s", inputPath);
    LOGI("输出目录: %s", outDir);

    // ==================== Mat 创建与初始化 ====================
    // 创建240x320灰度图,像素值100(中灰色)
    cv::Mat image1(240, 320, CV_8U, cv::Scalar(100));
    cv::imwrite(std::string(outDir)+"/image1_gray_100.png", image1);
    LOGI("已保存 image1_gray_100.png");

    // create() 重新分配内存,尺寸改为200x200
    image1.create(200,200,CV_8U);
    image1 = 200;
    cv::imwrite(std::string(outDir)+"/image1_gray_200.png", image1);
    LOGI("已保存 image1_gray_200.png");

    // 创建240x320彩色图,纯红色(BGR格式:0,0,255)
    cv::Mat image2(240, 320, CV_8UC3, cv::Scalar(0,0,255));
    cv::imwrite(std::string(outDir) + "/image2_red.png", image2);
    LOGI("已保存 image2_red.png");

    // ==================== 图像读取 ====================
    cv::Mat image3 = cv::imread(inputPath);
    if(image3.empty()){
        LOGE("读取图片失败: %s", inputPath);
        env->ReleaseStringUTFChars(imagePath, inputPath);
        env->ReleaseStringUTFChars(outputDir, outDir);
        return -1;
    }
    LOGI("成功读取图片,大小: %dx%d, 通道数: %d", image3.cols, image3.rows, image3.channels());

    // ==================== 浅拷贝(共享内存) ====================
    cv::Mat image4(image3);
    image1 = image3;

    // ==================== 深拷贝(独立内存) ====================
    cv::Mat image5 = image3.clone();
    image3.copyTo(image2);

    // ==================== 图像翻转 ====================
    cv::flip(image3, image3, 1);
    cv::imwrite(std::string(outDir) + "/image3_flipped.png", image3);
    LOGI("已保存 image3_flipped.png");

    // 保存拷贝结果,验证浅/深拷贝差异
    cv::imwrite(std::string(outDir) + "/image1_shared.png", image1);
    cv::imwrite(std::string(outDir) + "/image4_shared.png", image4);
    cv::imwrite(std::string(outDir) + "/image5_cloned.png", image5);
    cv::imwrite(std::string(outDir) + "/image2_copied.png", image2);

    // ==================== 函数返回Mat ====================
    cv::Mat gray = createTestImage();
    cv::imwrite(std::string(outDir) + "/gray_from_func.png", gray);
    LOGI("已保存 gray_from_func.png");

    // ==================== 数据类型转换 ====================
    cv::Mat image1_gray = cv::imread(inputPath, cv::IMREAD_GRAYSCALE);
    cv::Mat image2_float;
    // 转为32位浮点型,归一化到0~1
    image1_gray.convertTo(image2_float, CV_32F, 1.0 / 255.0, 0.0);
    // 转回8位用于保存
    cv::Mat image2_float_8u;
    image2_float.convertTo(image2_float_8u, CV_8U, 255.0);
    cv::imwrite(std::string(outDir) + "/image2_float.png", image2_float_8u);
    LOGI("已保存 image2_float.png");

    // 释放JNI字符串资源
    env->ReleaseStringUTFChars(imagePath, inputPath);
    env->ReleaseStringUTFChars(outputDir, outDir);

    LOGI("所有 OpenCV Mat 示例执行完成!");
    return 0;
}

2. Kotlin 界面代码:MainActivity.kt

kotlin 复制代码
package com.nicoli.hellomat

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.io.FileOutputStream

class MainActivity : AppCompatActivity() {

    private lateinit var imageContainer: LinearLayout

    // 声明JNI函数,调用C++代码
    private external fun runMatDemo(imagePath: String, outputDir: String): Int

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

        // 加载本地SO库
        System.loadLibrary("native-lib")

        // 复制Assets图片到APP私有目录
        copyAssetsToFiles("puppy.png")
        // 执行OpenCV图像处理
        runNativeDemo()
    }

    /**
     * 从Assets文件夹复制图片到应用私有目录
     * 解决Native无法直接读取Assets资源的问题
     */
    private fun copyAssetsToFiles(fileName: String) {
        val outFile = File(filesDir, fileName)
        if (outFile.exists()) return

        try {
            val input = assets.open(fileName)
            val output = FileOutputStream(outFile)
            val buffer = ByteArray(1024)
            var length: Int
            while (input.read(buffer).also { length = it } > 0) {
                output.write(buffer, 0, length)
            }
            input.close()
            output.close()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    /**
     * 执行OpenCV逻辑并显示结果
     */
    private fun runNativeDemo() {
        val inputFile = File(filesDir, "puppy.png")
        val outputDir = File(filesDir, "opencv_output")
        if (!outputDir.exists()) outputDir.mkdirs()

        // 调用JNI函数
        val result = runMatDemo(inputFile.absolutePath, outputDir.absolutePath)
        if (result == 0) {
            Toast.makeText(this, "处理完成,正在显示图片...", Toast.LENGTH_SHORT).show()
            // 显示所有生成的图片+文件名
            displayGeneratedImages(outputDir)
        } else {
            Toast.makeText(this, "执行失败", Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * 遍历目录,显示所有图片 + 文件名
     */
    private fun displayGeneratedImages(dir: File) {
        val files = dir.listFiles() ?: return
        for (file in files) {
            if (file.extension.lowercase() in setOf("png", "jpg", "bmp")) {

                // 垂直布局:图片 + 文件名
                val wrapper = LinearLayout(this)
                wrapper.orientation = LinearLayout.VERTICAL
                wrapper.layoutParams = LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                ).apply {
                    bottomMargin = 40
                }

                // 加载并显示图片
                val bitmap = BitmapFactory.decodeFile(file.absolutePath)
                val imageView = ImageView(this)
                imageView.setImageBitmap(bitmap)
                imageView.layoutParams = LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
                )

                // 显示文件名
                val textView = TextView(this)
                textView.text = "📄 " + file.name
                textView.textSize = 14f
                textView.gravity = Gravity.CENTER
                textView.setPadding(0, 8, 0, 8)

                // 组合控件
                wrapper.addView(imageView)
                wrapper.addView(textView)
                imageContainer.addView(wrapper)
            }
        }
    }
}

3. 布局文件:activity_main.xml

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

    <!-- 图片展示容器 -->
    <LinearLayout
        android:id="@+id/imageContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp"/>

</ScrollView>

代码逐模块实现详解

模块1:Mat 对象创建

  1. 纯色灰度图创建:指定尺寸、类型、像素值
  2. create() 函数:重新分配内存,不初始化数据
  3. 纯色彩色图创建:OpenCV 默认使用 BGR 颜色空间

模块2:图像读写

  • imread():加载本地图片到 Mat
  • imwrite():将 Mat 保存为 PNG 图片
  • 适配 Android 私有目录,无需申请存储权限

模块3:浅拷贝与深拷贝实战

通过代码对比验证:

  • 浅拷贝对象会随原图同步变化
  • 深拷贝对象完全独立
    这是 Mat 最核心的内存机制。

模块4:图像处理操作

  • flip():实现图像水平翻转
  • convertTo():实现图像数据类型转换(8位 ↔ 32位浮点型)

模块5:Android 端逻辑

  1. 复制 Assets 图片到私有目录(解决读取权限问题)
  2. 调用 JNI 函数执行 C++ 图像处理
  3. 遍历生成的图片,带文件名展示在界面上

总结

  1. Mat 是 OpenCV 图像的唯一载体,由头部信息 + 像素数据组成
  2. 核心函数:create/clone/copyTo/imread/imwrite/flip/convertTo
  3. 浅拷贝共享内存,深拷贝独立内存
  4. 本文代码是 Android + OpenCV Mat 学习的完整实战模板,可直接编译运行
  5. 适配 Android 私有目录机制,无权限、无崩溃、无兼容问题
相关推荐
harder3212 小时前
RMP模式的创新突破
开发语言·学习·ios·swift·策略模式
jinanwuhuaguo2 小时前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
程序猿乐锅3 小时前
【Tilas|第三篇】多表SQL语句
数据库·经验分享·笔记·学习·mysql
徐某人..3 小时前
基于i.MX6ULL平台的智能网关系统开发
arm开发·c++·单片机·qt·物联网·学习·arm
AOwhisky4 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes
小怪吴吴4 小时前
idea 开发Android
android·java·intellij-idea
光影少年5 小时前
大屏页面,一次多个请求,请求加密导致 点击 全局时间选择器 时出现卡顿咋解决(面板收起会延迟1~2秒)
前端·javascript·vue.js·学习·前端框架·echarts·reactjs
sakiko_5 小时前
UIKit学习笔记2-组件嵌套、滚动视图等
笔记·学习·objective-c·swift·uikit
知识分享小能手5 小时前
R语言入门学习教程,从入门到精通,R语言类别比较数据可视化- 完整知识点与案例代码(4)
学习·信息可视化·r语言