Android平台第三方图像处理算法集成问题总结

之前工作大部分时间是在集成第三方图像处理算法, 其中主要是双摄虚化(Bokeh)相关, 在此总结一下其中遇到的一些问题和解决方法.

集成方式

第三方算法公司提供的SDK都是C/C++动态库(.so) + 头文件的方式, 集成到手机中通常就两种方式:

  1. 集成到单独的App中
  2. 集成到Android系统源码中

两种方式优缺点如下:

  1. 优点: 通用性较好, 针对不同平台(QCOM/MTK)或者厂商不需要额外修改代码, 缺点:性能表现一般, 无法用到系统的一些独特的硬件(GPU, DSP等)
  2. 优点: 性能较好, 最大限度利用硬件资源, 缺点: 通用性很差, 每个平台集成代码都有些差异.

使用哪种集成方式一般取决于具体算法使用场景: 比如对应Camera360这样的通用应用, 就必须集成到App中, 而对于一些作为系统亮点或者差异化功能的卖点的厂商, 自然是集成到Android系统中, 像我做过的双摄手机项目, 由于特殊性, 就必须集成到系统中.

两种集成方式代码方面稍有差异, 详细内容请看: Android调用第三方C++算法库

extern C 问题

虽然C++号称完全兼容C, 也想取代C, 但最终并没有完全实现, 不同算法公司或者Android系统中不同模块, 使用的语言也不完全相同, 有的使用C, 有的用C++, 因此集成过程中会有一些兼容问题, 主要表现如下:

  • 算法接口是C++, 调用的模块是C代码

    此类问题要一般都是要求算法提供商提供C接口(只改接口部分, 修改并不多), C代码在Android系统中还是比较多的, 比较成熟的算法提供商一般默认都是提供C接口的, 这样就能兼容C和C++了

  • 算法是C接口, 调用用的C++

    因为C++兼容C, 看起来没问题, 但是由于头文件中没有加extern C, 会导致以C++方式编译后, 产生的函数名和算法中C接口的函数名不同, 所以会出现找不到调用的函数问题, 这种情况在引入的头文件中加入extern C即可.

undefined reference to xxx 快速定位

此类错误一般有两种情况

  • 忘记在调用算法库的模块的Android.mk中加入LOCAL_SHARED_LIBRARIES := libxxx 如果 LOCAL_SHARED_LIBRARIES变量在当前mk中定义过, 则需使用LOCAL_SHARED_LIBRARIES += libxxx
  • 算法库和头文件不对应,缺少函数
    这种情况一般是算法提供商的问题, 确定是否是这个问题可以使用linux中提供的nm命令,如:
    nm -D xxx路径/libxxx.so, 看看输出信息中是否有你要找的那个个函数名称, 没有就说明是算法库和头文件不对应.

注: nm -D输出信息中, 如果是C算法库, 函数名和头文件中是一样的, 如果是C++算法库, 则稍有差异, C++由于有重载特性, 函数名编译后会带有参数类型和返回值信息以及命名空间信息.
比如: 对于函数void startPreview(int flag), 如果是C方式编译, 通过nm -D输出的函数名字为startPreview , C++则是_Z12startPreviewi, 比如我们看下libandroid_runtime.so里面和Camera预览相关的函数

$ nm -D system/lib/libandroid_runtime.so |grep -i preview
         U _ZN7android6Camera11stopPreviewEv
         U _ZN7android6Camera12startPreviewEv
         U _ZN7android6Camera14previewEnabledEv
         U _ZN7android6Camera16setPreviewTargetERKNS_2spINS_22IGraphicBufferProducerEEE
         U _ZN7android6Camera23setPreviewCallbackFlagsEi
         U _ZN7android6Camera24setPreviewCallbackTargetERKNS_2spINS_22IGraphicBufferProducerEEE

可以看到相关接口都是C++的.

can not load library

代码运行时找不到so库的问题一般也分为两种情况:

  • 算法库没有打包到ROM包中

    这种情况一般对于初学者经常发生, 通常会发生在将so库预置到Android系统中, 比如我们通过如下代码预置一个so库

    include $(CLEAR_VARS)
    LOCAL_MODULE := libxxx
    LOCAL_MODULE_TAGS := optional
    LOCAL_SRC_FILES := libs/armeabi-v7a/$(LOCAL_MODULE).so
    LOCAL_MODULE_STEM := $(LOCAL_MODULE)
    LOCAL_MODULE_SUFFIX := $(suffix $(LOCAL_SRC_FILES))
    LOCAL_MULTILIB := 32
    LOCAL_MODULE_CLASS := SHARED_LIBRARIES
    include $(BUILD_PREBUILT)
    

    如果只有这些代码, 模块编译(mmm)也能正常编译, so库也会被编译到/system/lib/下, 但make clean全编译的时候, 这个libxxx.so是不会被编译到out目录中的, 因为你只是预置到系统中, 确没有告诉系统那个地方要用到这个so, 所以make的时候就会被默认剔除, 可通过在要调用的模块的Android.mk中加入LOCAL_SHARED_LIBRARIES += libxxx或者LOCAL_REQUIRED_MODULES := libxxx来解决此问题, 表明有模块在使用这个so. 另外使用LOCAL_SHARED_LIBRARIES会使当前编译模块依赖于libxxx, 即其他地方加载当前so文件中时会自动去加载libxxx.so, 如果这个库是JNI的so库, 也可通过LOCAL_JNI_SHARED_LIBRARIES := libxxx来表明依赖关系

  • 缺少依赖库文件

    缺少依赖库是指算法商提供的so库可能依赖系统或者其他第三方库, 但你没有直接看出来, 如果错误信息出现了can not load library libxxx,而且这个libxxx在系统或者算法商提供的列表里并没有, 这种情况一般是算法商漏提供了一些so库或者不同平台公共库不一样(比如高通平台OpenCL接口库名称为libOpenCL.so, MTK平台则是libGLES_mali.so), 这种情况可以使用linux命令 readelf来查看依赖的库, 方法如下:

     $ readelf -d out/target/product/msm8909w/system/lib/hw/bluetooth.default.so
    Dynamic section at offset 0x19085c contains 40 entries:
    Tag        Type                         Name/Value
     0x00000001 (NEEDED)                     Shared library: [libcutils.so]
     0x00000001 (NEEDED)                     Shared library: [libdl.so]
     0x00000001 (NEEDED)                     Shared library: [liblog.so]
     0x00000001 (NEEDED)                     Shared library: [libz.so]
     0x00000001 (NEEDED)                     Shared library: [libpower.so]
     0x00000001 (NEEDED)                     Shared library: [libprotobuf-cpp-full.so]
     0x00000001 (NEEDED)                     Shared library: [libmedia.so]
     0x00000001 (NEEDED)                     Shared library: [libutils.so]
     0x0000000e (SONAME)                     Library soname: [bluetooth.default.so]
    

    上面是我截取输出中的一部分, 可以看到有很多依赖的so, 比如你要打印LOG, 就要依赖Android系统中liblog.so这个库,通过此命令就很容易知道有没有漏掉一些算法库.

YUV数据对齐(stride/scanline)

由于芯片硬件特性, 做硬件JPEG编码时, 图片宽高如果都是处理器位数(32或者64, 也有可能是其他数值, 取决于芯片平台)的整数倍, 能得到更好的性能, 所以如果预览或者拍照的图片宽高相不是关数值的整数倍, 比如msm8953平台是64位对齐, 拍照设置的图片尺寸为2592x1944,在HAL层获取的yuv数据实际宽高为2624x1984,如果用相关工具看这个yuv图片, 图片右边和下边有无效像素(绿边或者黑边或者对应像素的延伸). 对齐后的宽在QCOM平台宽称为stride(步长),即相邻两行图像数据之间的间隔, 高称之为scanline,即有多少行数据可以进行读取.

一个简单计算对齐后宽高的函数(只针对2幂次方对齐, 2,4,8,16...)

int align(int target, int align)
{
    return (target + align -1) & (~(align - 1));
}
//示例:
align(2592, 64);// 得到 2624

当然一般平台返回的yuv数据都有相关信息记录stride和scanline, 高通平台获取方式请看:高通(QCOM)平台HAL层获取预览/拍照/录像YUV数据 ,MTK平台相关信息都在IImageBuffer这个类中, 详细信息可以看下这个类的相关定义, 由于我这里没有源码(博客要及时写, 不然后面再写想验证东西或者看代码, 发现没有相关环境了......), 就不说明了.

YUV数据格式

由于平台的差异QCOM/MTK, 使用的yuv数据格式也有差异, QCOM平台预览格式为yuv420sp,即NV21, 拍照HAL层格式也是NV21.

MTK平台预览默认为yuv420p, 即YV21, 拍照HAL层默认情况为yuv422的一种格式, 但MTK平台提供了接口用于申请不同格式的YUV数据, 我自己尝试过在MT6750T平台申请拍照的YV12数据, 能正常得到相关数据.

预览格式可以通过dumpsys media.camera来查看, 一般支持多种格式, 可以通过设置参数的方式控制预览格式, 拍照也一样:

//查看支持的预览格式, adb方式, 当然也可以通过Camera的API
adb shell dumpsys media.camera |grep preview-format-values // linux
adb shell dumpsys media.camera |findstr preview-format-values //windows

注意:QCOM平台更改预览格式会导致exif中的缩略图出现异常色块, 这个问题很多QCOM平台都有, 原因是缩略图是根据预览的数据进行JPEG编码生成的, 但编码时默认用的NV21格式, 我们的预览不是NV21格式就会有问题, 是个bug.

加载系统so库失败

从Android 7.0开始, 非系统App, 无法通过System.loadLibrary("libname");加载系统库,会出现如下错误:
java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/lib/libxxx.so" needed or dlopened by "/system/lib/libnativeloader.so" is not accessible for the namespace "classloader-namespace", Google之所以这样做是为了加强系统安全性, 这种情况一般只有将App预置到系统中, 第三方安装的App就没法调用系统库了, 有些人可能说可以把系统库pull出来放到App中, 但这不可行的, 原因有两点:

  1. 系统库本身会依赖其他库, pull一个库是无法运行的, 都pull出来显然不太现实, 严重增加apk体积
  2. 不同系统库在不同平台会有差异, 导致表现会有差异

所以非系统App还是调用通用API吧, 当然如果你的App只是针对一个平台或者一个机型, 就当我没说......

你可能感兴趣的:(Android平台第三方图像处理算法集成问题总结)