将SuperResolution图像前后处理逻辑从 C++ 迁移到 Java 的开发文档
https://github.com/quic/ai-engine-direct-helper.git
本文档旨在阐述在 SuperResolution Android 示例项目中,将图像的预处理(Pre-processing)和后处理(Post-processing)逻辑从 C++ Native 层迁移到 Java 应用层的原因、实现方式以及相关的配置变更。
1. 迁移原因分析
将图像处理逻辑从 C++ 层迁移至 Java 层主要基于以下几点考虑,旨在优化项目的开发效率、可维护性及整体架构的清晰度。
-
简化 Native 层职责:让 C++ Native 层(JNI)的职责更单一、更纯粹。迁移后,C++ 代码只负责核心的、计算密集型的推理任务(
buffer-to-buffer),不再关心图像文件的读取、格式转换等 I/O 和图像处理操作。这使得 Native 层的代码更简洁,易于维护和复用。 -
提升开发和调试效率:在 Android Studio 中,使用 Java/Kotlin 调试图像处理流程远比调试 C++ 代码方便。开发者可以轻松地设置断点、查看
Bitmap对象、检查中间处理结果(例如,查看Mat对象的像素值),从而快速定位问题。 -
更好地与 Android UI 框架集成:图像数据源于 Android UI(如图库选择),最终也要显示在 UI 上(如
ImageView)。将处理逻辑放在 Java 层,可以直接操作 Android 的Bitmap对象,避免了在 C++ 和 Java 之间频繁传递文件路径或进行复杂的对象转换,代码逻辑更顺畅。 -
减少 Native 编译依赖:将 OpenCV 的使用完全移至 Java 层后,C++ 的构建系统(CMake)不再需要链接庞大的 OpenCV 库。这简化了
CMakeLists.txt的配置,减少了 Native 库的体积和编译复杂度。
2. 实现方法
实现这一重构的核心思想是改变 JNI 接口的数据传递方式,从传递文件路径(String)改为传递直接内存缓冲区(java.nio.ByteBuffer)。
2.1 修改 C++ JNI 接口
我们将原有的 JNI 函数签名从接收文件路径改为接收 ByteBuffer。
2.1.1 修改前 (Before)
Java_com_example_superresolution_MainActivity_SuperResolution(
JNIEnv* env,
jobject,
jstring j_libsDir,
jstring j_model_path,
jstring j_input_img,
jstring j_output_img)
2.1.2 修改后 (After)
Java_com_example_superresolution_MainActivity_SuperResolution(
JNIEnv* env,
jobject,
jstring j_libsDir,
jstring j_model_path,
jobject j_inputBuffer,
jobject j_outputBuffer)
在 C++ 实现中,我们使用 env->GetDirectBufferAddress() 来获取 ByteBuffer 的内存地址,从而直接读写由 Java 分配的内存。所有与 OpenCV 相关的代码(如 cv::imread, cv::resize, cv::imwrite 等)都已从 native-lib.cpp 中移除。
2.2 在 Java 中实现前后处理
我们在 MainActivity.java 中增加了两个核心方法:preprocess 和 postprocess。
2.2.1 预处理 (preprocess)
此方法负责将用户选择的 Bitmap 图像转换为模型推理所需的 ByteBuffer。
private ByteBuffer preprocess(Bitmap bitmap) {
// 1. 将 Bitmap 转换为 OpenCV Mat
Mat mat = new Mat();
Utils.bitmapToMat(bitmap, mat);
// 2. 颜色空间转换 (Android Bitmap 是 RGBA, 模型可能需要 RGB)
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2RGB);
// 3. 归一化 (将像素值从 0-255 映射到 0.0-1.0) 并转为 32 位浮点型
mat.convertTo(mat, CvType.CV_32F, 1.0 / 255.0);
// 4. 分配直接 ByteBuffer
int bufferSize = IMAGE_WIDTH * IMAGE_HEIGHT * 3 * 4; // 3通道, float32
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bufferSize);
byteBuffer.order(ByteOrder.nativeOrder());
// 5. 将 Mat 数据写入 ByteBuffer
float[] data = new float[IMAGE_WIDTH * IMAGE_HEIGHT * 3];
mat.get(0, 0, data);
byteBuffer.asFloatBuffer().put(data);
return byteBuffer;
}
2.2.2 后处理 (postprocess)
此方法负责将 Native 层返回的 ByteBuffer 结果转换回可供显示的 Bitmap 图像。
private Bitmap postprocess(ByteBuffer buffer, int width, int height) {
// 1. 重置 buffer 指针,并读取 float 数据
buffer.rewind();
FloatBuffer floatBuffer = buffer.asFloatBuffer();
float[] data = new float[width * height * 3];
floatBuffer.get(data);
// 2. 反归一化 (将 0.0-1.0 的值映射回 0-255) 并进行 clip
byte[] byteData = new byte[data.length];
for (int i = 0; i < data.length; i++) {
byteData[i] = (byte) Math.max(0, Math.min(255, data[i] * 255.0f));
}
// 3. 将数据写入 Mat
Mat mat = new Mat(height, width, CvType.CV_8UC3);
mat.put(0, 0, byteData);
// 4. 颜色空间转换 (从 RGB 转回 RGBA 以正确生成 Bitmap)
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGB2RGBA);
// 5. 将 Mat 转换回 Bitmap
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Utils.matToBitmap(mat, bitmap);
return bitmap;
}
2.3 更新 Java 层的调用逻辑
在 covertImageButton 的点击事件中,我们更新了调用流程,串联起预处理、Native 推理和后处理。
// ... 在 covertImageButton.setOnClickListener 中 ...
// 1. 加载和缩放原始图片
Bitmap bitmap = BitmapFactory.decodeFile(input_img);
Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, IMAGE_WIDTH, IMAGE_HEIGHT, true);
// 2. 预处理
ByteBuffer inputBuffer = preprocess(resizedBitmap);
// 3. 准备输出 Buffer
int srWidth = SCALE * IMAGE_WIDTH;
int srHeight = SCALE * IMAGE_HEIGHT;
int outputBufferSize = srWidth * srHeight * 3 * 4; // float32
ByteBuffer outputBuffer = ByteBuffer.allocateDirect(outputBufferSize);
outputBuffer.order(ByteOrder.nativeOrder());
// 4. 调用 Native 方法进行推理
SuperResolution(nativeLibPath, model_name, inputBuffer, outputBuffer);
// 5. 后处理并显示结果
Bitmap outputBitmap = postprocess(outputBuffer, srWidth, srHeight);
outputImagePreview.setImageBitmap(outputBitmap);
3. 依赖与配置变更
为了支持 Java 层的 OpenCV 功能,需要进行以下配置变更。
-
app/build.gradle.kts:在
dependencies代码块中,添加 OpenCV 的官方 Android 依赖。dependencies { // ... 其他依赖 implementation("org.opencv:opencv:4.9.0") } -
MainActivity.java:为了让 App 在运行时能找到并加载 OpenCV 的 Native 库 (
.so文件),必须在调用任何 OpenCV API 之前对其进行初始化。这通常在Activity的onCreate方法的开头完成。@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 初始化 OpenCV if (!OpenCVLoader.initDebug()) { Log.e(TAG, "OpenCV initialization failed!"); } else { Log.d(TAG, "OpenCV initialization successful!"); } // ... 后续代码 } -
app/src/main/cpp/CMakeLists.txt:由于 C++ 代码不再使用 OpenCV,我们移除了相关的头文件包含路径和库链接配置,以简化 Native 构建。
- 移除
include_directories中的 OpenCV 路径。 - 移除
add_library(opencv ...)的定义。 - 移除
target_link_libraries中的opencv链接。
- 移除
通过以上步骤,我们成功地将前后处理逻辑迁移到了 Java 层,实现了更清晰的架构和更高效的开发流程。
