Java 图像处理传 JNI 到 C++(OpenCV):两种高效实现方式对比

在图像处理时,Java 的图像数据换到 c++中是无法直接使用的,需要转为 BGR 格式,要么在 java 层处理,要么在 jni 层处理,算法工程师的提供的动态库一般不会处理图片格式,直接拿到图像数据就使用了,这里写的是我自己用过的两种实现方式。


背景

算法提供了动态库,需要 web 端上传文件到后台,后台调用算法库对图像进行处理,处理完成后返回结果给到 web 端展示。

调试时遇到的问题:

  • 图像的颜色通道顺序(Java 是 RGB,OpenCV 是 BGR),需要转换
  • 图像类型多样(ARGB、灰度、RGB、BGR 等)
  • 是否需要中间转换?
  • 数据如何传输最优?

两种实现方式

环境:
JDK:8
OPENCV:4.5.3

对比

实现方式 Java 处理通道顺序 JNI 层构造 cv::Mat 性能 灵活性
方式一:Java 转为 BGR Java 层预处理为 BGR 直接生成 Mat
方式二:Java 原始 byte[] 不处理 C++ 层判断格式并转换

没有做过实际的数据对比,但处理速度上方式二是比方式一快的,至于快多少,不估了,啥时候有空再上数据吧。

以下是代码示例:

假设算法头文件其中一个方法是这样的:

java 复制代码
//进行MRZ码识别,只识别,不进行任何MRZ码内容校验。
/**
 * @brief       对输入的图像,进行字符OCR,并选择最长的两行字符输出
 * @param       pData:  图像数据,BGR格式或Gray
 * @param       nw:     图像宽度
 * @param       nh:     图像高度
 * @param       channels:   图像通道数
 * @param       ocr:    字符识别对象句柄,来自InitModel
 * @return      emp_MRZ*:   字符识别结果,需要调用Release_empMRZ接口释放内存。
 */
PassportMRZ_API emp_MRZ* detectMRZ(unsigned char * pData, int nw, int nh,int channels,void * ocr);

方式一:Java 层转为 BGR 格式,再传给 JNI

Java 层使用先将图像转换为 OpenCV 兼容的 BGR 通道顺序,然后传入 JNI 层即可直接构造 `cv::Mat`。

Java 实现

方法映射

java 复制代码
public native static EmpMRZ detectMRZ(byte[] imageData, int width, int height, int channels, long ocrPtr);

图像处理工具类:

java 复制代码
public class CustomImgUtils {
    /**
     * @param image
     * @param bandOffset 用于判断通道顺序
     * @return
     */
    private static boolean equalBandOffsetWith3Byte(BufferedImage image,int[] bandOffset){
        if(image.getType()==BufferedImage.TYPE_3BYTE_BGR){
            if(image.getData().getSampleModel() instanceof ComponentSampleModel){
                ComponentSampleModel sampleModel = (ComponentSampleModel)image.getData().getSampleModel();
                if(Arrays.equals(sampleModel.getBandOffsets(), bandOffset)){
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 判断图像是否为BGR格式
     * @return
     */
    public static boolean isBGR3Byte(BufferedImage image){
        return equalBandOffsetWith3Byte(image,new int[]{0, 1, 2});
    }
    /**
     * 判断图像是否为RGB格式
     * @return
     */
    public static boolean isRGB3Byte(BufferedImage image){
        return equalBandOffsetWith3Byte(image,new int[]{2, 1, 0});
    }


    /**
     * 判断图像是否为ARGB格式
     * @return
     */
    public static boolean isARGB(BufferedImage image){
        return image.getType() == BufferedImage.TYPE_INT_ARGB ||
        image.getType() == BufferedImage.TYPE_INT_ARGB_PRE ||
        image.getType() == BufferedImage.TYPE_4BYTE_ABGR ||
        image.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE;
    }

    /**
     * 判断图像是否为灰度图
     * @return
     */
    public boolean isGray(BufferedImage image){
        return image.getType() == BufferedImage.TYPE_BYTE_GRAY;

    }


    /**
     * 对图像解码返回RGB格式矩阵数据
     * @param image
     * @return
     */
    public static byte[] getMatrixRGB(BufferedImage image) {
        if(null==image)
            throw new NullPointerException();
        byte[] matrixRGB;
        if(isRGB3Byte(image)){
            matrixRGB= (byte[]) image.getData().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
        }else{
            // 转RGB格式
            BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(),
                                                       BufferedImage.TYPE_3BYTE_BGR);
            new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_sRGB), null).filter(image, rgbImage);
            matrixRGB= (byte[]) rgbImage.getData().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
        }
        return matrixRGB;
    }
    /**
     * 对图像解码返回BGR格式矩阵数据
     * @param image
     * @param channels
     * @return
     */
    public static byte[] getMatrixBGR(BufferedImage image){
        if(null==image)
            throw new NullPointerException();
        byte[] matrixBGR;
        if(isBGR3Byte(image)){
            matrixBGR= (byte[]) image.getData().getDataElements(0, 0, image.getWidth(), image.getHeight(), null);
        } else if (isGray(image)) {
            byte[] gray = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
            // 创建BGR矩阵
            matrixBGR = new byte[gray.length * 3];
            for (int i = 0, j = 0; i < gray.length; ++i, j += 3) {
                // Blue
                matrixBGR[j] = gray[i];
                // Green
                matrixBGR[j + 1] = gray[i];
                // Red
                matrixBGR[j + 2] = gray[i];
            }
            return matrixBGR;
        } else {
            // ARGB格式图像数据
            int[] intrgb = image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());
            matrixBGR = new byte[image.getWidth() * image.getHeight() * 3];
            // ARGB转BGR格式
            for (int i = 0, j = 0; i < intrgb.length; ++i, j += 3) {
                // Blue
                matrixBGR[j] = (byte) (intrgb[i] & 0xff);
                // Green
                matrixBGR[j + 1] = (byte) ((intrgb[i] >> 8) & 0xff);
                // Red
                matrixBGR[j + 2] = (byte) ((intrgb[i] >> 16) & 0xff);
            }
        }
        return matrixBGR;
    }

    // 判断是否为单通道图像(灰度图或单通道类型)
    public static boolean isSingleChannel(BufferedImage image) {
        // 通过颜色模型判断:颜色分量数量为1时表示单通道
        return image.getColorModel().getNumColorComponents() == 1;
    }

    // 单通道转三通道的具体实现
    public static BufferedImage convertSingleToThreeChannels(BufferedImage srcImage) {
        int width = srcImage.getWidth();
        int height = srcImage.getHeight();
        BufferedImage destImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int grayValue = srcImage.getRaster().getSample(x, y, 0);
                int rgb = (grayValue << 16) | (grayValue << 8) | grayValue;
                destImage.setRGB(x, y, rgb);
            }
        }
        return destImage;
    }

}

逻辑层:

java 复制代码
public R<MrzInfo> delectJni(MultipartFile file) throws IOException {
       log.info("begin detect");
       try {
           long start = System.currentTimeMillis();
           if (null ==file || file.isEmpty()) {
               return R.failed("传入文件为空");
           }
           BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
//        int channels = bufferedImage.getColorModel().getNumComponents();
           int channels = 3;
           if (CustomImgUtils.isSingleChannel(bufferedImage)) {
               // 通道转换
               bufferedImage = CustomImgUtils.convertSingleToThreeChannels(bufferedImage);
           }
           int width = bufferedImage.getWidth();
           int height = bufferedImage.getHeight();
           byte[] matrixBGR = CustomImgUtils.getMatrixBGR(bufferedImage);
           MrzInfo mrzInfo = new MrzInfo();
           long mrzStart = System.currentTimeMillis();
           EmpMRZ result = PassportMRZ.detectMrz(matrixBGR, width, height, channels);
           // ......
       } catch (Exception e) {
           log.error("error:",e);
           return R.failed(new MrzInfo(),"detect exception");
       }

   }
JNI实现
cpp 复制代码
jbyte* imageDataBytes = env->GetByteArrayElements(imageData, nullptr);
// 获取后直接传到对应方法里即可,有时可能需要转换:reinterpret_cast<unsigned char*>(imageDataBytes)

这样做的好处是:

  • 图像格式一致,C++ 层无需关心图像类型
  • 支持灰度、ARGB、RGB 等复杂格式

方式二:Java 层不处理,传原始 byte[] 到 JNI

直接从 Java 获取图像的 `byte[]` 数据(不转换颜色通道),连同图像类型、宽高传到 JNI,在 C++ 中根据图像类型手动构造 `cv::Mat` 并转换颜色。

Java 实现

方法映射:

cpp 复制代码
public native static EmpMRZ detectMRZ(byte[] imageData, long ocrPtr);

图像处理工具类:

cpp 复制代码
public class ImageProcessor {
    // 公共方法:将 InputStream 转换为 byte[]
    public static byte[] inputStreamToByteArray(InputStream inputStream) throws IOException {
        try (ByteArrayOutputStream buffer = new ByteArrayOutputStream();
             InputStream is = inputStream) {
            byte[] data = new byte[8192];
            int nRead;
            while ((nRead = is.read(data, 0, data.length)) != -1) {
                buffer.write(data, 0, nRead);
            }
            return buffer.toByteArray();
        }
    }

逻辑调用层:

java 复制代码
byte[] imageBytes = ImageProcessor.inputStreamToByteArray(file.getInputStream());
detectMRZ(imageBytes, ocrPtr);
JNI实现
cpp 复制代码
JNIEXPORT jobject JNICALL Java_com_emp_empxmrz_util_MrzDetect_detectMRZ
(JNIEnv* env, jclass, jbyteArray imageData, jlong modelPtr) {
    // ......

    // 获取传入的图片字节数组
    jsize len = env->GetArrayLength(imageData);
    if (len <= 0) {
        fprintf(stderr, "[JNI] Invalid image data length: %d\n", len);
        jclass exceptionCls = env->FindClass("java/lang/RuntimeException");
        env->ThrowNew(exceptionCls, "Invalid image data length");
        return nullptr;
    }

    jbyte* dataPtr = env->GetByteArrayElements(imageData, nullptr);
    if (dataPtr == nullptr) {
        fprintf(stderr, "[JNI] Failed to get image data elements\n");
        return nullptr;
    }

    // 复制数据到 vector
    std::vector<uchar> buffer(dataPtr, dataPtr + len);
    env->ReleaseByteArrayElements(imageData, dataPtr, JNI_ABORT);

    // 通过 OpenCV imdecode 解码图片数据
    cv::Mat image = cv::imdecode(buffer, cv::IMREAD_UNCHANGED);
    if (image.empty()) {
        fprintf(stderr, "[JNI] Failed to decode image\n");
        jclass exceptionCls = env->FindClass("java/lang/RuntimeException");
        env->ThrowNew(exceptionCls, "Failed to decode image");
        return nullptr;
    }

    int width = image.cols;
    int height = image.rows;
    int channels = image.channels();

    emp_MRZ* pRes = nullptr;
    try {
        // 调用 detectMRZ 接口进行识别
        pRes = detectMRZ(image.data, width, height, channels, ocr);
        if (pRes == nullptr) {
            fprintf(stderr, "[JNI] detectMRZ returned NULL\n");
            return nullptr;
        }
        // ......
    }
    catch (const std::exception& e) {
        fprintf(stderr, "[JNI] Exception in detectMRZ: %s\n", e.what());
        jclass exceptionCls = env->FindClass("java/lang/RuntimeException");
        env->ThrowNew(exceptionCls, e.what());
        if (pRes) {
            Release_empMRZ(pRes);
        }
        return nullptr;
    }
}

这样做的好处是:

  • Java 层代码干净、简单(只读流,不处理图像)
  • 支持所有格式:JPEG、PNG、BMP、WebP 等
  • C++ 自动识别图像通道:灰度、BGR、BGRA 等
  • 适合网络图像、文件流、Base64 解码后图像等各种来源
  • 易于跨平台、跨语言传输(例如 HTTP 上传)

Java 和 C++ 的图像数据交互不算复杂,关键是理解图像格式的要求。

相关推荐
乌萨奇也要立志学C++4 分钟前
【C++详解】哈希表概念与实现 开放定址法和链地址法、处理哈希冲突、哈希函数介绍
c++·哈希算法·散列表
十八旬17 分钟前
苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
java·开发语言·spring boot·mysql·idea·苍穹外卖
鞋尖的灰尘32 分钟前
springboot-事务
java·后端
银迢迢38 分钟前
SpringCloud微服务技术自用笔记
java·spring cloud·微服务·gateway·sentinel
用户0332126663671 小时前
Java 将 CSV 转换为 Excel:告别繁琐,拥抱高效数据处理
java·excel
这周也會开心1 小时前
Java-多态
java·开发语言
Forward♞1 小时前
Qt——网络通信(UDP/TCP/HTTP)
开发语言·c++·qt
渣哥1 小时前
揭秘!Java反射机制到底是什么?原来应用场景这么广!
java
叫我阿柒啊1 小时前
Java全栈开发实战:从Spring Boot到Vue3的项目实践
java·spring boot·微服务·性能优化·vue3·全栈开发
青草地溪水旁1 小时前
`lock()` 和 `unlock()` 线程同步函数
linux·c++·c