将SuperResolution图像前后处理逻辑从 C++ 迁移到 Java 的开发文档

开发文档:将图像前后处理逻辑从 C++ 迁移到 Java
本项目基于 ai-engine-direct-helper (QAI_AppBuilder),相关 GitHub 链接:
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 中增加了两个核心方法:preprocesspostprocess

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 之前对其进行初始化。这通常在 ActivityonCreate 方法的开头完成。

    @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 层,实现了更清晰的架构和更高效的开发流程。