当前位置: 首页 > news >正文

学习 Android (十六) 学习 OpenCV (一)

学习 Android (十六) 学习 OpenCV (一)

在前几个章节中,我们对 NDK 相关的开发有了一定的了解,所谓磨刀不误砍柴工,有了这些基础的知识储备之后,我们可以来简单上手一下 OpenCV 相关的知识,接下来跟随作者一起来学习吧:

  • 什么是 OpenCV ?

  • 搭建 OpenCV Android SDK 环境

  • OpenCV 的基本使用


1. 什么是 OpenCV?

OpenCVOpen 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::Cannycv::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)

  • 功能:图像/视频的显示与交互。

  • 关键内容

    • 窗口管理(imshownamedWindow)。

    • 视频捕获(VideoCapture)。

    • 滑动条(createTrackbar)。

  • 文件路径

    • 头文件:opencv2/highgui.hpp

    • 源码:modules/highgui/src/


4. 视频分析模块(Video)

  • 功能:视频流处理与分析。

  • 关键内容

    • 光流(Lucas-Kanade、Farneback)。

    • 背景减除(MOG2、KNN)。

    • 目标跟踪(TrackerKCFTrackerMOSSE)。

  • 文件路径

    • 头文件:opencv2/video.hpp

    • 源码:modules/video/src/


5. 相机标定与3D重建(Calib3D)

  • 功能:几何视觉算法。

  • 关键内容

    • 相机标定(calibrateCamera)。

    • 立体匹配(StereoBMStereoSGBM)。

    • 3D重建(solvePnPrecoverPose)。

  • 文件路径

    • 头文件: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 提供接口。

  • Javaopencv-java.jar + JNI。

  • 编译工具

    • CMake 构建系统(CMakeLists.txt)。

    • 预编译脚本(platforms/ 目录)。


3.2 图像读写

在 OpenCV Android SDK 中 图像读写的函数为 cv::imreadcv::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 存储权限与路径处理
方面OpenCVAndroid 原生
权限需求需手动处理路径权限(如 READ_EXTERNAL_STORAGE同左,但可通过 MediaStore 免权限访问公共目录
路径兼容性需处理 Android 10+ 的 Scoped Storage推荐使用 Context.getExternalFilesDir()MediaStore
文件管理无内置媒体扫描通知可通过 MediaScannerConnection 通知系统更新相册

3.2.4 典型场景推荐

优先使用 OpenCV 的情况

  1. 需要保留 BGR 通道顺序(如后续 OpenCV 处理)。

  2. 处理非标准格式(如 TIFF、PNG 16位)。

  3. 对读写性能要求高(如视频帧处理)。

优先使用 Android 原生的情况

  1. 图像直接显示在 UI 上(避免 BGR → ARGB 转换)。

  2. 需要与系统相册交互(如保存后立即显示在 Gallery 中)。

  3. 避免引入 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 中的图像滤波类型
  • 线性滤波

    1. 均值滤波 blur()

      函数定义

      void cv::blur(InputArray src,      // 输入图像OutputArray dst,     // 输出图像Size ksize,          // 卷积核大小,例如 Size(3, 3)Point anchor = Point(-1,-1), // 锚点,默认中心int borderType = BORDER_DEFAULT // 边界填充方式
      );
      
      参数名类型含义
      srcMat原始图像
      dstMat处理后的图像
      ksizeSize卷积核的大小,如 Size(5,5)
      anchorPoint卷积核中心点(默认居中)
      borderTypeint边缘处理方式,例如 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 作为原图去处理,发现结果虽然是模糊了,但是对应噪点确实是被柔滑了,如图所示

      在这里插入图片描述

      我们可以肉眼发现,确实如此,作者也尝试了均值调高点,发现很糊,估摸着是要配合其他操作,才能达成去噪,并且显示完美的效果

  1. 高斯滤波 GaussianBlur()

    GaussianBlur 是 高斯滤波 的实现,是一种 基于高斯函数的平滑操作,其作用是:“在保留图像边缘特征的同时,柔和模糊图像,降低噪声或细节。

    与普通均值滤波的区别

    特性均值滤波 blur()高斯滤波 GaussianBlur()
    权重分布所有像素平等中心像素权重大,边缘像素权重小
    边缘模糊边缘模糊严重,结构不保留边缘模糊较轻,结构自然
    视觉效果偏“块状模糊”,容易糊一团更加自然、柔和的模糊效果
    抗噪能力一般更强
    运算速度相对慢一些

    函数定义:

    cv::GaussianBlur(src, dst, cv::Size(ksize, ksize), sigmaX);
    
    参数名含义
    src原图像(Mat
    dst输出图像
    Size(w,h)卷积核大小,必须是奇数且正数(如 Size(5,5)
    sigmaXX 方向的高斯标准差(决定模糊强度)
    sigmaYY 方向的高斯标准差(可省略,默认为与 X 相同)

    卷积核 & σ (sigma) 选择指南

    使用目的推荐卷积核大小推荐 sigmaX
    去噪轻微模糊3x3、5x50~1.0
    柔和模糊背景9x9、11x112.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 ,效果如图所示

    在这里插入图片描述

    我们肉眼可以明显的感觉到,相对于均值滤波的处理,高斯滤波处理的效果更加自然柔和

由于篇幅问题,我们在下一文章在继续学习了解

http://www.dtcms.com/a/317157.html

相关文章:

  • Cglib的Enhancer实现动态代理?
  • 新能源汽车热管理系统核心零部件及工作原理详解
  • AI巨模型对决2025:五强争霸,谁能称王?
  • ORACLE 19C建库时卡在46%、36%
  • 【网络运维】Linux:简单DHCP服务器的部署
  • PyTorch入门引导
  • 识别 Base64 编码的 JSON、凭证和私钥
  • 接口自动化测试用例详解
  • 使用python与streamlit构建的空间微生物分析
  • RabbitMQ 全面指南:从基础概念到高级特性实现
  • 控制服务和守护进程-systemctl
  • python学智能算法(三十四)|SVM-KKT条件回顾
  • 系统的缓存(buff/cache)是如何影响系统性能的?
  • 【学习笔记之redis】删除缓存
  • 【Redis】hash哈希,List列表
  • app-3
  • Python day36
  • Java Stream API 详解(Java 8+)
  • IP与MAC地址的区别解析
  • 数据仓库命名规范
  • AS32S601 芯片 ADC 模块交流耦合测试:技术要点与实践
  • 使用 gptqmodel 量化 Qwen3-Coder-30B-A3B-Instruct
  • 大型音频语言模型论文总结
  • 【前端开发】三. JS运算符
  • MCU程序段的分类
  • 异世界历险之数据结构世界(非递归快排,归并排序(递归,非递归))
  • 搭建私有 Linux 镜像仓库
  • 算法训练营DAY55 第十一章:图论part05
  • 图论(邻接表)DFS
  • 藏文识别技术:为藏文化的保护、传播、研究与发展注入核心动力