
OpenCV 核心类 Mat
本文从原理、代码实现、函数详解、实战效果全方位讲解 OpenCV 中最核心的 Mat 类,适合 Android NDK + OpenCV 初学者系统学习。
什么是 OpenCV Mat?
Mat(Matrix)是 OpenCV 中用于存储图像/矩阵数据的核心类 ,是所有图像处理操作的基础。
简单理解:Mat = 图像在内存中的容器,由两部分组成:
- 头部信息:图像宽、高、通道数、数据类型、尺寸等描述信息
- 像素数据指针:指向真实图像像素数组的内存地址
在 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 最容易踩坑的地方:
-
浅拷贝
cppcv::Mat image4(image3); image1 = image3;✅ 共用同一块像素内存
✅ 无额外内存开销
❌ 一个对象修改,所有拷贝对象同步变化
-
深拷贝
cppcv::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 对象创建
- 纯色灰度图创建:指定尺寸、类型、像素值
create()函数:重新分配内存,不初始化数据- 纯色彩色图创建:OpenCV 默认使用 BGR 颜色空间
模块2:图像读写
imread():加载本地图片到 Matimwrite():将 Mat 保存为 PNG 图片- 适配 Android 私有目录,无需申请存储权限
模块3:浅拷贝与深拷贝实战
通过代码对比验证:
- 浅拷贝对象会随原图同步变化
- 深拷贝对象完全独立
这是 Mat 最核心的内存机制。
模块4:图像处理操作
flip():实现图像水平翻转convertTo():实现图像数据类型转换(8位 ↔ 32位浮点型)
模块5:Android 端逻辑
- 复制 Assets 图片到私有目录(解决读取权限问题)
- 调用 JNI 函数执行 C++ 图像处理
- 遍历生成的图片,带文件名展示在界面上
总结
Mat是 OpenCV 图像的唯一载体,由头部信息 + 像素数据组成- 核心函数:
create/clone/copyTo/imread/imwrite/flip/convertTo - 浅拷贝共享内存,深拷贝独立内存
- 本文代码是 Android + OpenCV Mat 学习的完整实战模板,可直接编译运行
- 适配 Android 私有目录机制,无权限、无崩溃、无兼容问题
