动手入门第三方算法集成系列:
【Camera专题】HAL层- 实现第三方算法并集成到Android系统
最近自己学了一下Camera数据流的知识,如何运用这些知识呢?
最好的方式就是加入第三方算法。当然,虽然学习都是以HelloWorld的方式;
但自己实现的算法,还是要能看到效果的,也要容易理解。
平台:高通8909
版本:HAL1
需要掌握的一些知识点
第三方算法的集成方式
本 文 选 用 H A L 层 的 集 成 方 式 。 \color{red}{本文选用HAL层的集成方式。} 本文选用HAL层的集成方式。
第三方算法集成推荐博客
传送门
这是我同事的博客,以前专门做第三方算法移植的,现在做framework层了。
很多知识我也是看他的博客学习的,下面的知识点就是参考他的。
然后我加入自己的实践,学以致用!
参考我同事写的文章
高通(QCOM)平台HAL层获取预览/拍照/录像YUV数据
这里做一些总结:(以下是HAL1的函数)
1.Preview流 回调函数
1. QCamera2HardwareInterface::preview_stream_cb_routine(...)
2. QCamera2HardwareInterface::synchronous_stream_cb_routine(...)
1. QCamera2HardwareInterface::nodisplay_preview_stream_cb_routine(...)
2.Snapshot流 回调函数
1. QCamera2HardwareInterface::capture_channel_cb_routine(...)
2. QCameraPostProcessor::processPPData(...)
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流 回调函数
1. QCamera2HardwareInterface::video_stream_cb_routine(...)
1.QCamera2HardwareInterface::snapshot_channel_cb_routine(...)
以上的函数,都可拿到 void* data 类型的YUV数据,本质上就是 unsigned char* 类型的数组
数据范围[0-255]
如果你打印出来,你可以看到 0x00到0xff的数据
关于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数据。
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.void∗buffer 指向帧缓冲区的指针
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;
非对齐(非填充)的宽高
对齐(填充)后的宽高
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数据是有无效像素的,
如果你看过我之前写的博客,应该知道,数据流都是存放在通道里的!
一个通道里面可以有多个数据流。
1.获取 通道[QCameraChannel]
通道类型:
相关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.获取 数据流里面的相关信息
mm_camera_buf_def_t *frame;
unsigned char * yuvDta = (unsigned char *)frame->buffer;
mm_camera_buf_def_t *frame;
int len = frame->len;
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
推荐2个视频学习
Android-JNI入门
Android-JNI进阶
当然网上也有许多关于JNI的博客可以学习。
本文是基于HAL层集成第三方算法,JNI这部分用不到,你可以先不用学习。
还是那句话,纸上得来终觉浅,绝知此事要躬行!
因为目前手上没有第三方算法,所以我们就自己动手实现一个。
为了看到效果,我们把数据流的一半变成灰色,另一半正常。
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开始计算的)。
算法很简单,麻雀虽小五脏俱全。
我们要对预览流进行处理,那么在相应的回调函数修改即可。
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
然后重启生效
可以看到,屏幕的一半变成了灰色!
学到这,你应该懂得如何集成第三方算法了!
虽然自己写的这个算法不牛逼,甚至有点丑陋,
不过集成的基本思路就是这样!
集成算法的步骤:
那么集成算法有哪些技术含量呢?
下篇文章,我们以so库的形式集成第三方算法!