学习 Android (十六) 学习 OpenCV (一)
学习 Android (十六) 学习 OpenCV (一)
在前几个章节中,我们对 NDK 相关的开发有了一定的了解,所谓磨刀不误砍柴工,有了这些基础的知识储备之后,我们可以来简单上手一下 OpenCV 相关的知识,接下来跟随作者一起来学习吧:
-
什么是 OpenCV ?
-
搭建 OpenCV Android SDK 环境
-
OpenCV 的基本使用
1. 什么是 OpenCV?
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和机器学习软件库,由英特尔(Intel)于 1999 年首次发布,现由开源社区维护。它提供了丰富的工具和算法,用于处理图像和视频数据,广泛应用于图像处理、物体检测、人脸识别、增强现实(AR)、自动驾驶、医学影像分析等领域。
1.1 OpenCV 的核心特点
-
跨平台支持:
-
支持 Windows、Linux、macOS、Android 和 ios。
-
提供 C++、Python、Java 等语言的接口。
-
-
丰富的功能模块:
-
图像处理(滤波、边缘检测、色彩空间转换等)
-
特做检测(SIFT、SURF、ORB、角色检测)
-
目标检测(Haar 级联、YOLD、SSD)
-
机器学习(SVM、KNN、神经网络)
-
摄像头标定与 3D 重建
-
视频分析(光流、背景减除)
-
-
高性能优化:
-
底层使用 C/C++ 编写、支持多线程和 GPU 加速(如 CUDA、OpenCL)。
-
提供预训练模型(如 DNN 模块支持 TensorFlow、PyTorch 模型)。
-
-
开源免费:
- 基于 BSD 许可证,可自由用于商业和研究用途。
1.2 OpenCV 的典型应用场景
领域 | 应用示例 |
---|---|
人脸识别 | 人脸检测、表情分析、活体检测(如手机解锁) |
自动驾驶 | 车道检测、交通标志识别、行人检测 |
医学影像 | 肿瘤检测、X 光分析、显微镜图像处理 |
工业检测 | 缺陷检测、二维码识别、物体测量 |
增强现实(AR) | 虚拟贴纸、3D 物体叠加(如 Snapchat 滤镜) |
机器人 | SLAM(同步定位与地图构建)、避障 |
1.3 OpenCV 的架构
-
核心模块(Core)
-
基础数据结构(如
Mat
存储图像矩阵)。 -
数学运算、文件 I/O。
-
-
Imgproc(图像处理)
- 滤波(高斯模糊、中值滤波)、边缘检测(Canny)、几何变换(旋转、缩放)。
-
HighGUI(图形界面)
- 显示图像、视频捕获、滑动条交互。
-
DNN(深度学习)
- 支持加载 TensorFlow、PyTorch 模型进行推理。
-
Calib3d(相机标定)
- 相机畸变校正、3D 重建。
2. 搭建 OpenCV Android SDK
在对 OpenCV 有了初步的了解之后,我们开始进行 Android 相关的 SDK 搭建,我们可以从官方下载 OpenCV Android SDK 包 Releases - OpenCV,这里作者选择了最新的 OpenCV - 4.12.0 Android 的包
将下载好的压缩包解压至自己的目录下,然后我们在 Android Studio 中进行操作,File -> New Project -> Native C++
至此我们创建一个了 Andorid Native 项目,因为从官方下载的 Android SDK 包中,提供给我的是 .a
静态库,而不是 .so
动态库,不过这都无所谓,对目前我们来说,只是去了解一下 OpenCV 而已,之后作者进行处理交叉编译获取到 .so
文件的,所以我现在就直接导入我们下载的文件中的 SDK 模块至项目中
选择下载后解压的目录至 sdk 目录下,这里作者的目录是 F:\OpenCV_SDK\opencv-4.12.0-android-sdk\OpenCV-android-sdk\sdk,读者根据自己的来替换
点击 FINISH,会卡顿一会,等待模块的导入即可,接下来我们将添加的 OpenCV 模块添加到我们的 app 模块中
点击ok即可,完成添加,在 MainActivity 代码中尝试是否有 opencv 相关的库
有就说明我们添加成功,至此,OpenCV 的 Android SDK 环境就已经搭建好了,接下来我们将了解并学习 OpenCV 的一下基本使用
3. OpenCV 的基本使用
接下来,我们将进行 OpenCV 的基本使用,能够读写图像、处理图像、使用滤镜、做简单变换
目标 | 示例函数 / 说明 |
---|---|
OpenCV 基本结构 | cv::Mat 图像数据结构、图像类型(灰度、彩色) |
图像读写 | cv::imread , cv::imwrite |
图像显示(桌面平台) | cv::imshow , cv::waitKey (Android 无需) |
基本变换 | 灰度转换 cv::cvtColor ,缩放 cv::resize ,旋转 cv::rotate |
图像滤波 | cv::GaussianBlur , cv::medianBlur |
边缘检测 | cv::Canny ,cv::Sobel |
绘图函数 | cv::line , cv::rectangle , cv::putText |
图像 ROI | 图像裁剪、区域选择 |
3.1 OpenCV 基本结构
OpenCV(Open Source Computer Vision Library)是一个模块化设计的跨平台计算机视觉库,其代码结构清晰,核心功能按模块划分。以下是 OpenCV 的基本架构和关键模块分析:
1. 核心模块(Core)
-
功能:基础数据结构和算法。
-
关键内容:
-
cv::Mat
:多维数组类,存储图像/矩阵数据。 -
基本数学运算(矩阵操作、SVD、FFT等)。
-
文件 I/O(XML/YAML/JSON 读写)。
-
-
文件路径:
-
头文件:
opencv2/core.hpp
-
源码:
modules/core/src/
-
2. 图像处理模块(Imgproc)
-
功能:传统图像处理算法。
-
关键内容:
-
滤波(高斯模糊、中值滤波)。
-
几何变换(旋转、缩放、仿射变换)。
-
边缘检测(Canny、Sobel)。
-
颜色空间转换(
cvtColor
)。
-
-
文件路径:
-
头文件:
opencv2/imgproc.hpp
-
源码:
modules/imgproc/src/
-
3. 高层GUI模块(HighGUI)
-
功能:图像/视频的显示与交互。
-
关键内容:
-
窗口管理(
imshow
、namedWindow
)。 -
视频捕获(
VideoCapture
)。 -
滑动条(
createTrackbar
)。
-
-
文件路径:
-
头文件:
opencv2/highgui.hpp
-
源码:
modules/highgui/src/
-
4. 视频分析模块(Video)
-
功能:视频流处理与分析。
-
关键内容:
-
光流(Lucas-Kanade、Farneback)。
-
背景减除(MOG2、KNN)。
-
目标跟踪(
TrackerKCF
、TrackerMOSSE
)。
-
-
文件路径:
-
头文件:
opencv2/video.hpp
-
源码:
modules/video/src/
-
5. 相机标定与3D重建(Calib3D)
-
功能:几何视觉算法。
-
关键内容:
-
相机标定(
calibrateCamera
)。 -
立体匹配(
StereoBM
、StereoSGBM
)。 -
3D重建(
solvePnP
、recoverPose
)。
-
-
文件路径:
-
头文件:
opencv2/calib3d.hpp
-
源码:
modules/calib3d/src/
-
6. 机器学习模块(ML)
-
功能:经典机器学习算法。
-
关键内容:
-
SVM、KNN、决策树。
-
数据归一化(
Normalizer
)。
-
-
文件路径:
-
头文件:
opencv2/ml.hpp
-
源码:
modules/ml/src/
-
7. 深度学习模块(DNN)
-
功能:神经网络模型推理。
-
关键内容:
-
支持 TensorFlow、PyTorch、ONNX 模型。
-
预训练模型(YOLO、SSD、ResNet)。
-
-
文件路径:
-
头文件:
opencv2/dnn.hpp
-
源码:
modules/dnn/src/
-
8. 加速模块(OpenCL/CUDA)
-
功能:硬件加速实现。
-
关键内容:
-
OpenCL 内核(
.cl
文件)。 -
CUDA 加速(需 NVIDIA GPU)。
-
-
文件路径:
-
OpenCL:
modules/core/src/ocl/
-
CUDA:
modules/cudaarithm/src/
-
9. 贡献模块(Contrib)
-
功能:扩展功能(非核心模块)。
-
关键内容:
-
人脸识别(
FaceRecognizer
)。 -
文本检测(
text
模块)。
-
-
GitHub仓库:
- opencv_contrib
10. 语言绑定与工具
-
Python:通过
cv2.so
提供接口。 -
Java:
opencv-java.jar
+ JNI。 -
编译工具:
-
CMake 构建系统(
CMakeLists.txt
)。 -
预编译脚本(
platforms/
目录)。
-
3.2 图像读写
在 OpenCV Android SDK 中 图像读写的函数为 cv::imread
、cv::imwrite
,当然,在学习使用之前,我们需要知道,在 Android 开发中,cv::imread
/cv::imwrite
(OpenCV 的读写接口)与 Android 原生图片读写(如 Bitmap
+ FileOutputStream
)各有优劣
3.2.1 功能对比
特性 | OpenCV (cv::imread /cv::imwrite ) | Android 原生方式 |
---|---|---|
支持格式 | 广泛:JPEG、PNG、TIFF、WebP、BMP 等 | 受限:JPEG、PNG、WebP(依赖 Bitmap.CompressFormat ) |
颜色空间 | 保留原始通道(如 BGR、RGBA),可指定读取模式(灰度/彩色) | 默认转换为 ARGB_8888 格式,可能丢失原始通道顺序 |
元数据处理 | 可读取/写入 EXIF 等元数据 | 需额外使用 ExifInterface 处理 |
性能优化 | 多线程、SIMD 加速,适合大图像 | 依赖系统 API,优化程度因设备而异 |
存储位置 | 需自行处理路径权限 | 可直接使用 MediaStore 或应用私有目录 |
3.2.2 性能对比
读取速度
-
OpenCV:
-
优势:直接解析文件为
cv::Mat
,避免格式转换开销。 -
适合场景:需保留 BGR 通道或处理非标准格式(如 TIFF)。
-
-
Android 原生:
-
BitmapFactory.decodeFile()
会转换为 ARGB_8888,可能更慢。 -
适合场景:需直接显示在
ImageView
中。
写入速度
-
-
OpenCV:
cv::imwrite
对 JPEG/PNG 编码优化较好,支持调整压缩参数。
-
Android 原生:
Bitmap.compress()
的压缩效率取决于系统实现,可控性较低。
3.2.3 存储权限与路径处理
方面 | OpenCV | Android 原生 |
---|---|---|
权限需求 | 需手动处理路径权限(如 READ_EXTERNAL_STORAGE ) | 同左,但可通过 MediaStore 免权限访问公共目录 |
路径兼容性 | 需处理 Android 10+ 的 Scoped Storage | 推荐使用 Context.getExternalFilesDir() 或 MediaStore |
文件管理 | 无内置媒体扫描通知 | 可通过 MediaScannerConnection 通知系统更新相册 |
3.2.4 典型场景推荐
优先使用 OpenCV 的情况
-
需要保留 BGR 通道顺序(如后续 OpenCV 处理)。
-
处理非标准格式(如 TIFF、PNG 16位)。
-
对读写性能要求高(如视频帧处理)。
优先使用 Android 原生的情况
-
图像直接显示在 UI 上(避免 BGR → ARGB 转换)。
-
需要与系统相册交互(如保存后立即显示在 Gallery 中)。
-
避免引入 OpenCV 库的额外体积。
3.2.5 混合代码示例
在项目中创建一个 OpenCVUItils
工具类
在 native-ib.cpp
中添加读写操作
#include <jni.h>
#include <string>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/opencv.hpp>
#include <android/asset_manager.h>
#include <android/log.h>#define LOG_TAG "OpenCV_Native"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_opencv_1sdk_1example_OpenCVUtils_imageReadAndWrite(JNIEnv *env, jclass,jstring inputPath,jstring outputPath) {const char *inPath = env->GetStringUTFChars(inputPath, nullptr);const char *outPath = env->GetStringUTFChars(outputPath, nullptr);LOGD("输入路径: %s", inPath);LOGD("输出路径: %s", outPath);// 1. 读取图像cv::Mat img = cv::imread(inPath, cv::IMREAD_COLOR);if (img.empty()) {LOGE("无法读取图像: %s", inPath);env->ReleaseStringUTFChars(inputPath, inPath);env->ReleaseStringUTFChars(outputPath, outPath);return false;}LOGD("图像尺寸: %d x %d", img.cols, img.rows);// 2. 处理图像(例如转灰度)cv::cvtColor(img, img, cv::COLOR_BGR2GRAY);// 3. 保存图像bool success = cv::imwrite(outPath, img);if (!success) {LOGE("无法保存图像到: %s", outPath);} else {LOGD("图像保存成功: %s", outPath);}env->ReleaseStringUTFChars(inputPath, inPath);env->ReleaseStringUTFChars(outputPath, outPath);return success;
}
在 OpenCVUtils
中实现
public class OpenCVUtils {static {System.loadLibrary("opencv_java4");System.loadLibrary("opencv_sdk_example");}public static native boolean imageReadAndWrite(String inputPath, String outputPath);/*** 保存图像至本地,并进行相关图片处理(置灰、旋转、缩放)** @param inputPath 原图图片路径* @param outputPath 处理后图片保存路径* */public static boolean processAndSaveImage(String inputPath, String outputPath) {return imageReadAndWrite(inputPath, outputPath);}}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:columnCount="5"android:rowCount="5"tools:context=".MainActivity"><ImageViewandroid:id="@+id/iv_original"android:src="@drawable/lxh" /><Buttonandroid:id="@+id/btn_save"android:text="保存图片" /></GridLayout>
MainActivity
public class MainActivity extends AppCompatActivity {private static final int REQUEST_PERMISSION = 1;private ActivityMainBinding binding;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());binding.btnSave.setOnClickListener(view -> {// 确保有权限if (!checkPermissions()) {requestPermissions();return;}// 创建应用私有目录File privateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);if (privateDir == null || !privateDir.exists()) {Toast.makeText(this, "无法访问存储目录", Toast.LENGTH_SHORT).show();return;}try {// 1. 保存原始图像到临时文件Bitmap original = BitmapFactory.decodeResource(getResources(), R.drawable.lxh);File inputFile = new File(privateDir, "input.jpg");saveBitmapToFile(original, inputFile);// 2. 创建输出文件路径File outputFile = new File(privateDir, "output_gray.jpg");// 3. 调用 Native 方法处理图像boolean success = OpenCVUtils.processAndSaveImage(inputFile.getAbsolutePath(),outputFile.getAbsolutePath());if (success) {// 4. 加载处理后的图像并显示Bitmap processed = BitmapFactory.decodeFile(outputFile.getAbsolutePath());binding.ivOriginal.setImageBitmap(processed);Toast.makeText(this, "图像处理成功!", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "图像处理失败", Toast.LENGTH_SHORT).show();}} catch (Exception e) {Log.e("MainActivity", "处理图像出错: " + e.getMessage());Toast.makeText(this, "处理图像出错: " + e.getMessage(), Toast.LENGTH_LONG).show();}});}private void saveBitmapToFile(Bitmap original, File inputFile) throws IOException {try (FileOutputStream out = new FileOutputStream(inputFile)) {original.compress(Bitmap.CompressFormat.JPEG, 90, out);}}private boolean checkPermissions() {return ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;}private void requestPermissions() {ActivityCompat.requestPermissions(this,new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},REQUEST_PERMISSION);}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQUEST_PERMISSION) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {Toast.makeText(this, "权限已授予", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "需要存储权限才能处理图像", Toast.LENGTH_SHORT).show();}}}}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><!-- 存储权限 --><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!-- Android 10 以上存储相关 --><applicationandroid:requestLegacyExternalStorage="true"android:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.Opencv_sdk_example"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
运行后可以看到
点击按钮,弹出权限申请,给予权限后
我们图片就替换成我们经过处理后的图片了,并且我们在本地存储目录下可以看到读写后的图片
3.3 基本变换
基本转换包含:灰度转换 cv::cvtColor
,缩放 cv::resize
,旋转 cv::rotate
修改 native-lib.cpp
extern "C" JNIEXPORT void JNICALL
Java_com_example_opencv_1sdk_1example_OpenCVUtils_convertToGray(JNIEnv *env, jclass,jlong inputAddr, jlong outputAddr) {cv::Mat &inputMat = *(cv::Mat *) inputAddr;cv::Mat &outputMat = *(cv::Mat *) outputAddr;/*** src:输入图像(如 cv::Mat)。* dst:输出图像,大小和深度与 src 相同(但通道数可能变化)。* code:颜色空间转换标识符(如 cv::COLOR_BGR2GRAY、cv::COLOR_BGR2HSV)。* dstCn:可选参数,指定输出图像的通道数(默认 0 表示自动根据 code 决定)。* */cv::cvtColor(inputMat, outputMat, cv::COLOR_RGBA2GRAY);
}extern "C" JNIEXPORT void JNICALL
Java_com_example_opencv_1sdk_1example_OpenCVUtils_resize(JNIEnv *env, jclass, jlong inputAddr,jlong outputAddr, jint width, jint height,jfloat scaleX, jfloat scaleY) {cv::Mat &inputMat = *(cv::Mat *) inputAddr;cv::Mat &outputMat = *(cv::Mat *) outputAddr;/*** src:输入图像。* dst:输出图像,尺寸为 dsize 或由 fx/fy 计算。* dsize:目标尺寸(Size(width, height)),若为 (0,0) 则根据 fx/fy 计算。fx, fy:沿 x/y 轴的缩放因子(若 dsize=(0,0) 则生效)。* interpolation:插值方法(如 INTER_NEAREST、INTER_LINEAR、INTER_CUBIC)。* */if (width == 0 && height == 0) {cv::resize(inputMat, outputMat, cv::Size(), scaleX, scaleY); // 缩放一半} else {cv::resize(inputMat, outputMat, cv::Size(width, height), 0, 0, cv::INTER_LINEAR);}}extern "C" JNIEXPORT void JNICALL
Java_com_example_opencv_1sdk_1example_OpenCVUtils_rotate(JNIEnv *env, jclass, jlong inputAddr,jlong outputAddr, jint rotateCode) {cv::Mat &inputMat = *(cv::Mat *) inputAddr;cv::Mat &outputMat = *(cv::Mat *) outputAddr;/*** src:输入图像。* dst:输出图像,尺寸根据旋转角度调整。* rotateCode:旋转模式:* ROTATE_90_CLOCKWISE:顺时针 90°* ROTATE_180:180°* ROTATE_90_COUNTERCLOCKWISE:逆时针 90°* */cv::rotate(inputMat, outputMat, rotateCode);
}
修改 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:columnCount="5"android:rowCount="5"tools:context=".MainActivity"><ImageViewandroid:id="@+id/iv_original"android:src="@drawable/lxh" /><Buttonandroid:id="@+id/btn_save"android:text="保存图片" /><ImageView android:id="@+id/image_convert_to_gray" /><ImageView android:id="@+id/image_resize" /><ImageView android:id="@+id/image_rotate" /></GridLayout>
修改 MainActivity
public class MainActivity extends AppCompatActivity {private static final int REQUEST_PERMISSION = 1;private ActivityMainBinding binding;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());Bitmap convertToGrayBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lxh);Bitmap resizeBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lxh);Bitmap rotateBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lxh);if (convertToGrayBitmap != null) {binding.imageConvertToGray.setImageBitmap(OpenCVUtils.convertToGray(convertToGrayBitmap));}if (resizeBitmap != null) {binding.imageResize.setImageBitmap(OpenCVUtils.resize(resizeBitmap, 0, 0, 1.2f, 1.2f));}if (rotateBitmap != null) {binding.imageRotate.setImageBitmap(OpenCVUtils.rotate(rotateBitmap, Core.ROTATE_90_CLOCKWISE));}binding.btnSave.setOnClickListener(view -> {// 确保有权限if (!checkPermissions()) {requestPermissions();return;}// 创建应用私有目录File privateDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);if (privateDir == null || !privateDir.exists()) {Toast.makeText(this, "无法访问存储目录", Toast.LENGTH_SHORT).show();return;}try {// 1. 保存原始图像到临时文件Bitmap original = BitmapFactory.decodeResource(getResources(), R.drawable.lxh);File inputFile = new File(privateDir, "input.jpg");saveBitmapToFile(original, inputFile);// 2. 创建输出文件路径File outputFile = new File(privateDir, "output_gray.jpg");// 3. 调用 Native 方法处理图像boolean success = OpenCVUtils.processAndSaveImage(inputFile.getAbsolutePath(),outputFile.getAbsolutePath());if (success) {// 4. 加载处理后的图像并显示Bitmap processed = BitmapFactory.decodeFile(outputFile.getAbsolutePath());binding.ivOriginal.setImageBitmap(processed);Toast.makeText(this, "图像处理成功!", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "图像处理失败", Toast.LENGTH_SHORT).show();}} catch (Exception e) {Log.e("MainActivity", "处理图像出错: " + e.getMessage());Toast.makeText(this, "处理图像出错: " + e.getMessage(), Toast.LENGTH_LONG).show();}});}private void saveBitmapToFile(Bitmap original, File inputFile) throws IOException {try (FileOutputStream out = new FileOutputStream(inputFile)) {original.compress(Bitmap.CompressFormat.JPEG, 90, out);}}private boolean checkPermissions() {return ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)== PackageManager.PERMISSION_GRANTED;}private void requestPermissions() {ActivityCompat.requestPermissions(this,new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE},REQUEST_PERMISSION);}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQUEST_PERMISSION) {if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {Toast.makeText(this, "权限已授予", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "需要存储权限才能处理图像", Toast.LENGTH_SHORT).show();}}}}
修改 OpenCVUtils
public class OpenCVUtils {static {System.loadLibrary("opencv_java4");System.loadLibrary("opencv_sdk_example");}public static native boolean imageReadAndWrite(String inputPath, String outputPath);public static native void convertToGray(long inputMatAddr, long outputMatAddr);public static native void resize(long inputMatAddr, long outputMatAddr, int width, int height, float scaleX, float scaleY);public static native void rotate(long inputMatAddr, long outputMatAddr, int rotateCode);/*** 灰度处理** @param input 缩放原图*/public static Bitmap convertToGray(Bitmap input) {Mat inputMat = new Mat();Mat outputMat = new Mat();// Bitmap -> MatUtils.bitmapToMat(input, inputMat);// 调用 native 方法处理图像convertToGray(inputMat.getNativeObjAddr(), outputMat.getNativeObjAddr());// Mat -> BitmapBitmap resultBitmap = Bitmap.createBitmap(input.getWidth(), input.getHeight(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(outputMat, resultBitmap);inputMat.release();outputMat.release();return resultBitmap;}/*** 缩放** @param input 缩放原图* @param resizeWidth 缩放至目标宽度* @param resizeHeight 缩放至目标高度* @param scaleX x 轴缩放因子。 当 width、height 都为 0 时, 根据缩缩放因子来进行缩放。* @param scaleY y 抽缩放因子。 当 width、height 都为 0 时, 根据缩缩放因子来进行缩放。*/public static Bitmap resize(Bitmap input, int resizeWidth, int resizeHeight, float scaleX, float scaleY) {Mat inputMat = new Mat();Mat outputMat = new Mat();// Bitmap -> MatUtils.bitmapToMat(input, inputMat);// 调用 native 方法处理图像resize(inputMat.getNativeObjAddr(), outputMat.getNativeObjAddr(), resizeWidth, resizeHeight, scaleX, scaleY);// Mat -> BitmapBitmap resultBitmap = Bitmap.createBitmap(outputMat.cols(), outputMat.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(outputMat, resultBitmap);inputMat.release();outputMat.release();return resultBitmap;}/*** 旋转** @param input 缩放原图* @param rotateCode 旋转方向*/public static Bitmap rotate(Bitmap input, int rotateCode) {Mat inputMat = new Mat();Mat outputMat = new Mat();// Bitmap -> MatUtils.bitmapToMat(input, inputMat);// 调用 native 方法处理图像rotate(inputMat.getNativeObjAddr(), outputMat.getNativeObjAddr(), rotateCode);// Mat -> BitmapBitmap resultBitmap = Bitmap.createBitmap(outputMat.cols(), outputMat.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(outputMat, resultBitmap);inputMat.release();outputMat.release();return resultBitmap;}/*** 保存图像至本地,并进行相关图片处理(置灰、旋转、缩放)** @param inputPath 原图图片路径* @param outputPath 处理后图片保存路径* */public static boolean processAndSaveImage(String inputPath, String outputPath) {return imageReadAndWrite(inputPath, outputPath);}}
最后运行查看结果
3.4 图像滤波
在 Android 中使用 OpenCV 进行图像滤波是图像处理中最基础、最核心的操作之一,它的作用极其广泛,几乎贯穿于所有涉及图像处理的应用场景。简单来说,滤波的核心目的是修改图像的像素值,以达到增强信息、抑制噪声、提取特征或改变图像视觉效果等目标
3.4.1 什么是图像滤波?
图像滤波是一种通过数学运算修改像素值的技术,它使用了一个称为 滤波器(核) 的小矩阵在图像上滑动,对每个像素及其领域进行加权计算,从而改变图像特性。
3.4.2 滤波的核心目的
-
去噪: 消除图像中的随机噪声,图像在采集(手机拍照)、传输或压缩过程中,不可避免地会引入噪声(椒盐噪声、高斯噪声等),表现为图像上的随机斑点、颗粒或色彩失真。
应用场景: 手机拍照后处理、扫描文档去除杂点、医学影像去噪、视频通话实时降噪等。
-
增强: 突出特定特征(如边缘)
应用场景: 纹理分析、指纹识别增强脊线、特定方向边缘提取(如检测水平线)。
-
模糊: 降低图像细节(如隐私保护)
应用场景: 美颜磨皮(去除细小皱纹、斑点)、背景虚化模拟、图像金字塔构建(用于缩放或特征检测)、预处理以减少计算量。
-
锐化: 增强边缘和细节
应用场景: 照片后期处理增强清晰度、医学影像增强细节、模糊图像的修复预处理。
-
边缘检测: 滤波是边缘检测算法(如 Canny)的关键预处理步骤。目的是为了识别图像中物体或区域的边界(亮度或颜色剧烈变化的地方)。
应用场景: 物体识别与分割、手势识别、文档扫描(边缘检测找文档轮廓)、特征提取(如角点检测的第一步)、车道线检测、机器视觉中的定位。
3.4.3 滤波的数学原理
滤波操作本质上是 卷积计算:
I'(x,y) = ΣΣ w(i,j) * I(x+i, y+j)
其中:
-
I
是输入图像 -
I'
是输出图像 -
w
是滤波器核(如 3×3 矩阵) -
(i,j)
是核内相对位置
3.4.4 OpenCV 中的图像滤波类型
-
线性滤波
-
均值滤波
blur()
函数定义
void cv::blur(InputArray src, // 输入图像OutputArray dst, // 输出图像Size ksize, // 卷积核大小,例如 Size(3, 3)Point anchor = Point(-1,-1), // 锚点,默认中心int borderType = BORDER_DEFAULT // 边界填充方式 );
参数名 类型 含义 src
Mat
原始图像 dst
Mat
处理后的图像 ksize
Size
卷积核的大小,如 Size(5,5)
anchor
Point
卷积核中心点(默认居中) borderType
int 边缘处理方式,例如 BORDER_DEFAULT
,BORDER_CONSTANT
等核大小(ksize) 模糊程度 Size(3, 3)
轻微模糊 Size(5, 5)
中等模糊 Size(15, 15)
非常模糊(失焦) 卷积核该如何选择?
OpenCV 中的
Size(3,3)
、Size(5,5)
、Size(15,15)
是 卷积核的大小,也就是参与“平均计算”的邻域范围,越大模糊越强。没有一个固定值是“最优”的,它依赖于你的应用场景。应用目标 推荐核大小 去掉轻微噪声但保持清晰 Size(3,3)
或Size(5,5)
需要模糊背景或特效 Size(15,15)
、Size(25,25)
强烈降噪但不在意清晰度 Size(9,9)
以上准备做边缘检测前的预处理 Size(3,3)
或Size(5,5)
必须是奇数
OpenCV 要求卷积核大小是奇数(如 3、5、7),以便有“中心点”。
与图像分辨率有关
-
对于小图(如 320×240),
5x5
就已经挺模糊了; -
对于大图(如 1920×1080),
5x5
可能看不出明显效果,可能要15x15
起步。
核越大,处理越慢
Size(25,25)
是非常大的卷积,计算量大,会明显拖慢滤波速度,尤其是在 Android 手机上。修改
native-lib.cpp
添加均值滤波处理图片extern "C" JNIEXPORT void JNICALL Java_com_example_opencv_1sdk_1example_MainActivity_filterImage(JNIEnv *env, jobject thiz,jlong inputMatAddr,jlong outputMatAddr,jint filteType,jint kernel_width,jint kernel_height) {// 获取输入/输出Mat的引用cv::Mat &input = *(cv::Mat *) inputMatAddr;cv::Mat &output = *(cv::Mat *) outputMatAddr;// 输入验证if (input.empty()) {__android_log_print(ANDROID_LOG_ERROR, "OpenCV", "Input image is empty!");return;}// 保存原始类型和通道数,用于后续恢复int origType = input.type();int origChannels = input.channels();try {switch (filterType) {case 0: // 均值滤波cv::blur(input, output, cv::Size(kernel_width, kernel_height));break;}// 恢复原始类型(如果需要)if (output.type() != origType) {cv::Mat converted;output.convertTo(converted, origType);converted.copyTo(output);}} catch (const cv::Exception &e) {// 异常处理__android_log_print(ANDROID_LOG_ERROR, "OpenCV", "Filter error: %s", e.what());input.copyTo(output);} }
假如 kernel_width、kernel_height 分别为 5
-
实现原理:使用 OpenCV 的
blur()
函数进行均值滤波,核大小为5×5
。 -
计算过程:
-
对图像每个像素点,取其周围
5×5
邻域内所有像素值的 算术平均值。 -
替换原像素值,实现平滑去噪。
-
-
效果:有效抑制 高斯噪声 和 随机小颗粒噪声,但会导致图像边缘模糊。
-
计算开销:
5×5
核需对每个像素计算 25 次加法,复杂度为`O(width×height×k²)
修改
MainActivity
private void applyFilter(int type) {filterImage(src.getNativeObjAddr(), dst.getNativeObjAddr(), type, 5, 5);Bitmap result = Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_8888);Utils.matToBitmap(dst, result);binding.imageFilter.setImageBitmap(result);}
添加处理滤波的方法,根据按钮点击事件传递不同的 type 在 native 中再根据 type 去处理对应的滤波,图片资源读者自己准备,两个 ImageView 和 按钮,一个保持原图,一个是进行滤波处理后的图
我们可以发现,点击按钮进行均值滤波操作后,我们可以发现,图片没有变好,反而变模糊了,这是为什么?作者经过这个操作的时候,也是懵了,经过作者的了解,我们要知道一个概念,什么叫做 - 空间滤波
其实这正是滤波的正常效果。需要从原理上解释清楚两个层面:数学层面和视觉层面。
数学上,5×5 的均值滤波相当于用一个大窗口对每个像素周围的 25 个点取平均。这个操作本质是低通滤波——削弱高频信号(细节和噪声),保留低频信号(平缓区域)。就像把一杯混着沙子的水静置,沙子沉淀后水变清澈但水里的微小悬浮物也沉淀了。
视觉上,用户注意到的“变糊”其实是边缘被柔化 的结果。因为边缘是像素值突变的地方(高频信息),平均后突变被削弱了。比如眼睛轮廓原本黑白分明,经过滤波后边缘出现灰色过渡带,整体就显得模糊了。
用户可能没意识到的是,这种“糊”在某些场景恰恰是需要的。比如当照片有很多椒盐噪声时,模糊反而能让图像更干净。可以建议用户下次试试有噪点的图片,观察去噪效果。
为此,作者将原图进行噪点强调渐进处理成 10 作为原图去处理,发现结果虽然是模糊了,但是对应噪点确实是被柔滑了,如图所示
我们可以肉眼发现,确实如此,作者也尝试了均值调高点,发现很糊,估摸着是要配合其他操作,才能达成去噪,并且显示完美的效果
-
-
-
高斯滤波
GaussianBlur()
GaussianBlur 是 高斯滤波 的实现,是一种 基于高斯函数的平滑操作,其作用是:“在保留图像边缘特征的同时,柔和模糊图像,降低噪声或细节。
与普通均值滤波的区别
特性 均值滤波 blur()
高斯滤波 GaussianBlur()
权重分布 所有像素平等 中心像素权重大,边缘像素权重小 边缘模糊 边缘模糊严重,结构不保留 边缘模糊较轻,结构自然 视觉效果 偏“块状模糊”,容易糊一团 更加自然、柔和的模糊效果 抗噪能力 一般 更强 运算速度 快 相对慢一些 函数定义:
cv::GaussianBlur(src, dst, cv::Size(ksize, ksize), sigmaX);
参数名 含义 src
原图像( Mat
)dst
输出图像 Size(w,h)
卷积核大小,必须是奇数且正数(如 Size(5,5)
)sigmaX
X 方向的高斯标准差(决定模糊强度) sigmaY
Y 方向的高斯标准差(可省略,默认为与 X 相同) 卷积核 & σ (sigma) 选择指南
使用目的 推荐卷积核大小 推荐 sigmaX 去噪轻微模糊 3x3、5x5 0~1.0 柔和模糊背景 9x9、11x11 2.0~3.0 强烈模糊 15x15 以上 5.0+ 注意:当
sigmaX = 0
时,OpenCV 会根据核大小自动计算最合适的值。修改
native-lib.cpp
添加均值滤波处理图片case 1: // 高斯滤波{// 确保核为奇数int kw = kernel_width | 1; // 位操作确保奇数int kh = kernel_height | 1;// 动态计算 σ:经验公式 σ = 0.3 * ((ksize - 1) * 0.5 - 1) + 0.8double sigma = 0.3 * ((kw - 1) * 0.5 - 1) + 0.8;// 设置 σ = 0 让 OpenCV 自动计算最佳值cv::GaussianBlur(input, output, cv::Size(kw, kh), 0);break;}
添加一个按钮,传递 type 为 2即可查看高斯滤波的处理效果,通过发现,好像和均值滤波处理效果没啥区别,通过上述的定义我们可以知道,高斯滤波的视觉效果比均值效果更好,那么我们加大卷积核的范围,让其将噪点处理的更加模糊一点,来看看均值和高斯的区别,我们将卷积核改完
7 x 7
,效果如图所示我们肉眼可以明显的感觉到,相对于均值滤波的处理,高斯滤波处理的效果更加自然柔和
由于篇幅问题,我们在下一文章在继续学习了解