【Camera专题】HAL层- 实现第三方算法并集成到Android系统

系列文章

动手入门第三方算法集成系列:
【Camera专题】HAL层- 实现第三方算法并集成到Android系统
【Camera专题】HAL层- 以SO库的方式集成第三方算法
【Camera专题】HAL1- 多帧降噪算法的集成(实战1)

一、前言

最近自己学了一下Camera数据流的知识,如何运用这些知识呢?
最好的方式就是加入第三方算法。当然,虽然学习都是以HelloWorld的方式;
但自己实现的算法,还是要能看到效果的,也要容易理解。

平台:高通8909
版本:HAL1
需要掌握的一些知识点

  • 1.熟悉Camera数据流,至少知道回调函数在哪里
  • 2.了解YUV的相关知识
  • 3.掌握相关的结构体
  • 4.掌握HAL层的一些API
  • 5.掌握JNI的知识
  • 6.动手实践第三方算法,并植入系统。

第三方算法的集成方式

  • APP集成
    这就需要你会java并且懂JNI
  • Hal层集成
    需要你熟悉Camera数据流,YUV的相关知识,HAL层的一些API。

本 文 选 用 H A L 层 的 集 成 方 式 。 \color{red}{本文选用HAL层的集成方式。} HAL

第三方算法集成推荐博客
传送门
这是我同事的博客,以前专门做第三方算法移植的,现在做framework层了。
很多知识我也是看他的博客学习的,下面的知识点就是参考他的。
然后我加入自己的实践,学以致用!

二、知识点

1.HAL1-Camera数据流的回调函数

参考我同事写的文章
高通(QCOM)平台HAL层获取预览/拍照/录像YUV数据
这里做一些总结:(以下是HAL1的函数)

1.Preview流 回调函数

  • 正常模式下
    根据平台不同,会调用都下面其中一个函数。
    hardware/qcom/camera/QCamera2QCameraHWICallbacks.cpp
1. QCamera2HardwareInterface::preview_stream_cb_routine(...)
2. QCamera2HardwareInterface::synchronous_stream_cb_routine(...)
  • no-display-mode模式下
    no-display-mode是指App打开Camera后, 在startPreview之前设置参数Parameters.set(“no-display-mode”, “1”);,
    然后可以不设置预览surface进行预览拍照(主要用于双摄项目中, 副摄设置no-display-mode, 对用户不可见).
1. QCamera2HardwareInterface::nodisplay_preview_stream_cb_routine(...)

2.Snapshot流 回调函数

  • 非ZSL模式
    hardware/qcom/camera/QCamera2/QCameraHWICallbacks.cpp
    hardware/qcom/camera/QCamera2/QCameraPostProc.cpp
1. QCamera2HardwareInterface::capture_channel_cb_routine(...)
2. QCameraPostProcessor::processPPData(...)
  • ZSL模式
1. QCamera2HardwareInterface::zsl_channel_cb(...)
2. QCameraPostProcessor::processPPData(...)

小 技 巧 : 如 果 希 望 第 三 方 算 法 对 任 意 拍 照 模 式 都 生 效 , 那 就 在 【 p r o c e s s P P D a t a 】 函 数 里 面 改 动 ! \color{red}{小技巧:如果希望第三方算法对任意拍照模式都生效,那就在【processPPData】函数里面改动!} processPPData

3.Video流 回调函数

  • 录像预览流数据
    hardware/qcom/camera/QCamera2/QCameraHWICallbacks.cpp
1.  QCamera2HardwareInterface::video_stream_cb_routine(...)
  • 录像拍照流数据
1.QCamera2HardwareInterface::snapshot_channel_cb_routine(...)

小结:

以上的函数,都可拿到 void* data 类型的YUV数据,本质上就是 unsigned char* 类型的数组
数据范围[0-255]
如果你打印出来,你可以看到 0x00到0xff的数据


2.YUV的一些基础知识

关于YUV的知识,可以自行度娘或者谷歌学习。

以下总结一些我觉得应该知道的:

YUV的含义

YUV也是一种颜色编码方法,主要用于电视系统以及模拟视频领域,
它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,
只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。
并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。

YUV 采样(其中一种)
YUV 4:2:0采样,每四个Y共用一组UV分量一个YUV占8+2+2 = 12bits 1.5个字节。
需要占用的内存:w * h * 3 / 2=w * h * 1.5
内存则是:yyyyyyyyuuvv

存放方式是一个一维数组,但是你可以根据宽x高把他看作二维数组来理解,如下图。

YUV420sp格式如下图
分辨率为8X4的YUV图像,它们的格式如下图:

我们经常看到的Android或者IOS摄像头采集到的NV12或者NV21数据,其实本质上是YUV,即YUV420sp。
数据存放的格式【YYYYUV】,先放完所有的Y数据,然后在放UV数据。

3.掌握相关的结构体

1. mm_camera_buf_def_t:数据流 帧缓冲区 结构体
hardware/qcom/camera/QCamera2/stack/common/mm_camera_interface.h

typedef struct {
    
    
    uint32_t stream_id;//标识流对象的id,跟人的身份证类似
    cam_stream_type_t stream_type;//数据流类型
    uint32_t buf_idx;//放在内存中的 buf索引
    uint8_t is_uv_subsampled;
    struct timespec ts; //时间戳,在调用DQBUF时填充
    uint32_t frame_idx;//帧序列编号,待DQBUF填充
    int8_t num_planes;//帧缓冲区的平面数,在mem分配期间填充
    struct v4l2_plane planes[VIDEO_MAX_PLANES];//帧缓冲区的平面信息,将在mem分配期间填充
    int fd; //帧缓冲区的文件描述符
    void *buffer;//指向帧缓冲区的指针
    size_t frame_len;//帧长度
    void *mem_info;//指向附加mem信息的用户特定指针
} mm_camera_buf_def_t;

这个结构体里,最重要的信息:

1. v o i d ∗ b u f f e r \color{red}{1. void *buffer} 1.voidbuffer 指向帧缓冲区的指针
void* data 类型的YUV数据,本质上就是 unsigned char* 类型的数组
2. s i z e _ t f r a m e _ l e n \color{red}{2. size\_t f rame\_len} 2.size_tframe_len 帧长度

2. mm_camera_super_buf_t

hardware/qcom/camera/QCamera2/stack/common/mm_camera_interface.h

typedef struct {
    
    
    uint32_t camera_handle;//唯一标识相机对象
    uint32_t ch_id;//通道id
    uint32_t num_bufs;//super buf 中的缓冲区数,不能超过4
    mm_camera_buf_def_t* bufs[MAX_STREAM_NUM_IN_BUNDLE];
    //bundle中的缓冲区数组
} mm_camera_super_buf_t;

这里的super_buf"继承"了父类mm_camera_buf_def_t,
(C语言的继承就是结构体里包含其他结构体)。
加入了通道信息ch_id、缓冲区数。

3. cam_frame_len_offset_t 和cam_mp_len_offset_t

hardware/qcom/camera/QCamera2/stack/common/cam_types.h

typedef struct{
    
    
    uint32_t len;
    uint32_t offset;
    int32_t offset_x;
    int32_t offset_y;
    int32_t stride;
    int32_t scanline;
    int32_t width;    /* width without padding */
    int32_t height;   /* height without padding */
} cam_mp_len_offset_t;

typedef struct {
    
    
    uint32_t num_planes;
    union {
    
    
        cam_sp_len_offset_t sp;
        cam_mp_len_offset_t mp[VIDEO_MAX_PLANES];
    };
    uint32_t frame_len;
} cam_frame_len_offset_t;

非对齐(非填充)的宽高

  • width
  • height

对齐(填充)后的宽高

  • int32_t stride;
  • nt32_t scanline;

4. cam_dimension_t

hardware/qcom/camera/QCamera2/stack/common/cam_types.h

typedef struct {
    
    
    int32_t width;
    int32_t height;
} cam_dimension_t;

图片的实际宽高

对齐(填充)概念

在高通平台, YUV数据一般会有对齐, 对齐是指为了处理效率更高, 图片宽高必须是某些数的整数倍(如 32或者64),
当然为什么对齐后处理效率更高, 这个好像是由于硬件设计的一些特性, 详细就不太清楚了. 如果图片宽高不是64位倍数, 
对齐过后会在原图片右侧和下方留下无效像素, 当然经过JPEG硬件编码过后会被裁剪,
所以App层看到的是正常的, 只不过我们在HAL层获取的YUV数据是有无效像素的, 

4.HAL层相关的API

如果你看过我之前写的博客,应该知道,数据流都是存放在通道里的!
一个通道里面可以有多个数据流。

1.获取 通道[QCameraChannel]

通道类型:

  • QCAMERA_CH_TYPE_VIDEO(video)
    录像预览通道
  • QCAMERA_CH_TYPE_SNAPSHOT(video snap)
    录像拍照通道
  • QCAMERA_CH_TYPE_ZSL(zsl)
    ZSL通道
  • QCAMERA_CH_TYPE_CAPTURE(capture)
    拍照通道

相关API:
示例(zsl_channel_cb()中获取channel):

QCamera2HardwareInterface *pme = (QCamera2HardwareInterface *)userdata;
QCameraChannel *pChannel = pme->m_channels[QCAMERA_CH_TYPE_ZSL];

其他通道类似。

2.获取 数据流

  • 方式一
    mm_camera_buf_def_t* yuvFrame = NULL;//定义数据流缓冲区结构体
    QCameraStream* stream = NULL;//数据流
    // frame 为 mm_camera_super_buf_t 类型
    for (uint32_t i = 0; i < frame->num_bufs; i++) {
    
    
    //通过getStreamByHandle找到数据流
        stream = pChannel->getStreamByHandle(frame->bufs[i]->stream_id);
        if (stream != NULL) {
    
    
            // 通过isOrignalTypeOf 找到拍照数据等其他数据
            if (stream->isOrignalTypeOf(CAM_STREAM_TYPE_SNAPSHOT)) {
    
    
                yuvFrame = frame->bufs[i];
                break;
            }
        }
    }
  • 方式二
preview_stream_cb_routine(mm_camera_super_buf_t *super_frame,
                            QCameraStream * stream,
                            void *userdata) {
    
    
···
 mm_camera_buf_def_t *frame = super_frame->bufs[0];

···                            

}

在我们的回调函数中,通过 mm_camera_buf_def_t *frame = super_frame->bufs[0];
就可以拿到mm_camera_buf_def_t 的YUV数据了。

3.获取 数据流里面的相关信息

  • 1.数据流的地址
mm_camera_buf_def_t *frame;
unsigned char * yuvDta = (unsigned char *)frame->buffer;
  • 2.数据流长度
mm_camera_buf_def_t *frame;
int len = frame->len;
  • 3.数据流的宽高(对齐和非对齐)
cam_frame_len_offset_t offset;
memset(&offset, 0, sizeof(cam_frame_len_offset_t));
cam_dimension_t dim;
memset(&dim, 0, sizeof(dim));
//stream为 QCameraStream*
stream->getFrameOffset(offset);
stream->getFrameDimension(dim);

图片实际宽为:dim.width, 高为:dim.height,
对齐后的宽为:offset.mp[0].stride, 高为:offset.mp[0].scanline

4.JNI的知识

推荐2个视频学习
Android-JNI入门
Android-JNI进阶
当然网上也有许多关于JNI的博客可以学习。

本文是基于HAL层集成第三方算法,JNI这部分用不到,你可以先不用学习。

还是那句话,纸上得来终觉浅,绝知此事要躬行!

三、实践

1、自己实现一个“”牛逼“”的算法

因为目前手上没有第三方算法,所以我们就自己动手实现一个。
为了看到效果,我们把数据流的一半变成灰色,另一半正常。

  • 0-黑色
  • 128-灰色
  • 255-白色

hardware/qcom/camera/QCamera2/HAL/QCamera2HWICallbacks.cpp
加入以下代码

void YUV2Gray(unsigned char* srcYuv,int w,int h) {
    
    
      int mid = w*h/2;//找到图像中间位置
      unsigned char* startP = srcYuv + mid;//指针移到中间
      //处理Y数据,uv数据是色彩信息 我们不管
      for(int i=0;i<mid;i++) {
    
    
      		  //把数据赋值128 即灰色
              *startP = 0x80;//128
              startP++;
      }
}

YUV数据中,前面W * H是Y数据,后面W * H/2是UV数据。
那我们要定位到图像的中间位置,就是W * H/2.
举个例子:
宽:8 高:4的YUV 数据如下图

那么图像的中间就是Y17对的数据,即4x8/2=16(数组是从0开始计算的)。

算法很简单,麻雀虽小五脏俱全。

2、把算法集成到HAL层

我们要对预览流进行处理,那么在相应的回调函数修改即可。
hardware/qcom/camera/QCamera2/HAL/QCamera2HWICallbacks.cpp

//自定义第三方算法
void YUV2Gray(unsigned char* srcYuv,int w,int h) {
    
    
    int mid = w*h/2;
    unsigned char* startP = srcYuv + mid;
    for(int i=0;i<mid;i++) {
    
    
        *startP = 0x80;//128
        startP++;
    }
}
void QCamera2HardwareInterface::preview_stream_cb_routine(mm_camera_super_buf_t *super_frame,
                                                          QCameraStream * stream,
                                                          void *userdata)
{
    
    
    ATRACE_CALL();
    CDBG("[KPI Perf] %s : BEGIN", __func__);
    int err = NO_ERROR;
    QCamera2HardwareInterface *pme = (QCamera2HardwareInterface *)userdata;
    QCameraGrallocMemory *memory = (QCameraGrallocMemory *)super_frame->bufs[0]->mem_info;

    if (pme == NULL) {
    
    
        ALOGE("%s: Invalid hardware object", __func__);
        free(super_frame);
        return;
    }    
    if (memory == NULL) {
    
    
        ALOGE("%s: Invalid memory object", __func__);
        free(super_frame);
        return;
    }    

    mm_camera_buf_def_t *frame = super_frame->bufs[0];
    if (NULL == frame) {
    
    
        ALOGE("%s: preview frame is NLUL", __func__);
        free(super_frame);
        return;
    }    
      //获取YUV数据地址
++    unsigned char* yuv = (unsigned char *)frame->buffer;
++    cam_dimension_t dim; 
      //获取实际宽高
++    stream->getFrameDimension(dim);
++    cam_frame_len_offset_t offset;
	  //获取对齐后宽高
++    stream->getFrameOffset(offset);
++    ALOGE("%s: zcf frame_len=%d dim.w=%d,diw.h=%d,对齐后w=%d,高=%d",
		__func__,
		frame->frame_len,dim.width,dim.height,
		offset.mp[0].stride,offset.mp[0].scanline);

      //获取调用第三方算法
++    YUV2Gray(yuv,offset.mp[0].offset.mp[0].scanline);
···
    }

我们把宽高打印出来看看:

实际宽高:360x320
对齐宽高:384x320

实际上数据要做对齐:360/32=11.25 不能整除,系统会自动对齐,填充数据整除。
因此buf_len= 384x320x1.5=184320.
为啥log是188416呢,因为还有别的一些无效信息被填充,因此会大一些。

编译
你也可以编译整个系统:
make -j32 2>&1 | tee mlog
然后刷机验证。
或者
mmm hardware/qcom/camera/QCamera2/HAL/
编译完成后会在以下目录:
out/target/product/【项目名】/system/lib/hw
生成camera.msm8909.so库
adb push camera.msm8909.so system/lib/hw
然后重启生效

3.结果


可以看到,屏幕的一半变成了灰色!

学到这,你应该懂得如何集成第三方算法了!
虽然自己写的这个算法不牛逼,甚至有点丑陋,
不过集成的基本思路就是这样!

4.小结

集成算法的步骤:

  • 1.弄明白第三方算法的接口
  • 2.熟悉整个数据流,知道在哪里改

那么集成算法有哪些技术含量呢?

  • 1.熟悉数据流
  • 2.懂得评估算法,从内存,耗时,效果三个方面
  • 3.流程和策略上的优化
  • 4.多帧处理,双摄甚至多摄的集成等

下篇文章,我们以so库的形式集成第三方算法!

Stay Hungry,Stay Foolish!

猜你喜欢

转载自blog.csdn.net/justXiaoSha/article/details/100730520