详述Android照相功能的实现(基于飞凌S3C6410开发板+单独编译安卓模块)

本文主要是介绍详述Android照相功能的实现(基于飞凌S3C6410开发板+单独编译安卓模块),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

http://bbs.gkong.com/archive.aspx?id=301714


前些日子买了块飞凌OK6410的开发板+OV9650摄像头模块准备做Android应用开发。自己手里虽有现成的Android手机,但考虑到日后裁减硬件,不得不从最原始的开发板着手。之前没有写过Android的驱动,这算是一次尝试。本文涉及到以下几个方面的内容: 

1. Android 模块编译 
2. Android 模块的板上加载及调试 
3. Android Camera 模块的改写 
Android 模块编译 
每次为了一个模块而编译整个Android系统是一个灾难(4个小时一次),这里会展示如何仅仅编译一个模块而节省大量的宝贵时间。网上多数的方法是通过执行envsetup.sh,接着运行mmm <directory>命令来编译一个文件夹下的模块,但在编译libcamera这个模块时一直没能成功,显示编译依赖于其他几个模块。这里介绍另一种方法,每个模块的文件夹下都必须有一个Android.mk文件,在其中有一项LOCAL_MODULE用于定义模块名称,以照相模块为例,即被定义为LOCAL_MODULE:=libcamera,记下这个模块名称,跳转到Android源码的根目录下,执行以下操作: 
Step 1.  进入宿主机linux终端,输入以下命令: 
 
<name>@<machine>:<folder>#source ./build/envsetup.sh  
<name>@<machine>:<folder>#choosecombo 
 
 


Step 2.  选择Device->Release->键入OK6410->eng 
Step 3.  输入make <libname>编译特定模块,如摄像头模块:  
 
<name>@<machine>:<folder>#make libcamera  
 



Step 4. 经过以上几个步骤后,摄像头模块就开始编译了,生成后的动态连接库文件(*.so)会存放在out/target/product/OK6410/system/lib/下,本文我们仅需要libcamera.so 
我把上述步骤做成了一个shell脚本,每次修改照相模块的HAL后会自动编译,并将更新后的libcamera.so拷贝到Android源码根目录下,如果愿意,也可以自行修改脚本将libcamera.so拷贝到SD中。  
附件下载: 
makelibcamera.zip 点击此处下载 ourdev_684187ZGUGB5.zip(文件大小:373字节) (原文件名:makelibcamera.zip)  


Step 4. 经过以上几个步骤后,摄像头模块就开始编译了,生成后的动态连接库文件(*.so)会存放在out/target/product/OK6410/system/lib/下,本文我们仅需要libcamera.so 
我把上述步骤做成了一个shell脚本,每次修改照相模块的HAL后会自动编译,并将更新后的libcamera.so拷贝到Android源码根目录下,如果愿意,也可以自行修改脚本将libcamera.so拷贝到SD中。  


Android 模块的板上加载及调试  
libcamera.so已经生成了,那怎么调试呢?一种办法是加载到模拟的Android系统中,但这种方法对于硬件调试往往行不通,那剩下的方法就是板上调试了。如果板子已经能够和PC进行adb连接,那就用adb push把libcamera.so推到目标机/system/lib/中。但可能是OK6410 USB接口设计的问题,与MacOSX总是无法建立起连接,于是每次我只能通过SD卡进行中转...手动从SD卡上把照相模块cp到lib目录下,然后reboot。 
嵌入式开发比起应用开发,其开发环境往往要恶劣许多。就拿调试而言,往往要通过代码中插入类printf的语句来查看运行状态。android中提供了一个很好的工具logcat,在用户空间中,通过LOGV(Verbose),LOGE(Error),LOGD(Debug)等提供类似printf的功能。假定在程序中#define LOG_TAG "CameraHardware",那通过如LOGE("%s, Hello World!", LOG_TAG)就可以记录在系统日志中。系统日志杂乱繁多,要查看特定的日志就要限定范围,在目标机上定义ANDROID_LOG_TAGS环境变量就可以通过logcat -d来查看CameraHardware的“错误”日志了: 
 
export ANDROID_LOG_TAGS="CameraHardware:E *:S" 
logcat -d  
 

目标机和宿主机相连后,通过超级终端来执行以上命令



Android Camera 模块的改写 
这是本文的重点,展示如何在驱动层实现拍照功能。通过查看飞凌的Android源代码会发现,其OV9650和USB摄像头HAL的实现就是Android Fake Camera的改写,前者位于<android root folder>/hardware/forlinx/libcamera,后者位于<android root folder>/frameworks/base/services/camera/libcameraservice中。在Android中,OV9650已经有了基本的预览功能,这证明至少Preview函数已经完善,我们就从preview功能切入,来分析它的实现。打开libcamera下的S3C6410CameraHardware.cpp,在initDefaultParapeters方法中,可以看到preview的格式是RGB565,是一种常用于TFT显示的格式。原FakeCamera中是YUV420SP: 
void CameraHardware::initDefaultParameters() 

   CameraParameters p; 

   p.set(CameraParameters::KEY_SUPPORTED_PREVIEW_SIZES, "320x240"); 
   p.setPreviewSize(320, 240); 
   p.setPreviewFrameRate(15); 
//    p.setPreviewFormat(CameraParameters::PIXEL_FORMAT_YUV420SP); 
   p.setPreviewFormat(CameraParameters::PIXEL_FORMAT_RGB565); 
   p.set(CameraParameters::KEY_ROTATION, 0);//90 

// 其余代码省略 
   } 
    
}  
现在让我们看看Preview功能的实现,也许可以给我们启发,我们不难注意到previewThread方法,其中mPreviewHeap存储着n个帧的缓冲,这块区域被分割为n个mBuffers。buffer为当前帧的引用,通过mDataCb(CAMERA_MSG_PREVIEW_FRAME, buffer, mCallbackCookie)就可以将buffer输出到屏幕。那每一个帧是怎么存到mPreviewHeap上的呢?关键的一句就是Ov965xCamera->getNextFrameAsRgb565((uint16_t *)frame) ,通过看它的实现可以知道(在Ov96xCamera.app中),一个帧的数据以16位的格式写入frame中,这里的frame即是对mPreviewHeap上某个mBuffer的引用: 
int CameraHardware::previewThread()   

   mLock.lock(); 
       // the attributes below can change under our feet... 

       int previewFrameRate = mParameters.getPreviewFrameRate(); 

       // Find the offset within the heap of the current buffer. 
       ssize_t offset = mCurrentPreviewFrame * mPreviewFrameSize; 

       sp<MemoryHeapBase> heap = mPreviewHeap; 

       // this assumes the internal state of fake camera doesn't change 
       // (or is thread safe) 
       Ov965xCamera* Ov965xCamera = mOv965xCamera; 
       USBCamera* USBCamera = mUSBCamera; 
       sp<MemoryBase> buffer = mBuffers[mCurrentPreviewFrame]; 

   mLock.unlock(); 

   // TODO: here check all the conditions that could go wrong 
   if (buffer != 0) { 
       // Calculate how long to wait between frames. 
       int delay = (int)(1000000.0f / float(previewFrameRate)); 

       // This is always valid, even if the client died -- the memory 
       // is still mapped in our process. 
       void *base = heap->base(); 

       // Fill the current frame with the fake camera. 
       uint8_t *frame = ((uint8_t *)base) + offset; 
       //Ov965xCamera->getNextFrameAsYuv420(frame); 

   if(mCamType == CAMTYPE_CMOS)  
       Ov965xCamera->getNextFrameAsRgb565((uint16_t *)frame);//获取一个帧的数据,放入frame 
   else if (mCamType == CAMTYPE_USB) 
       USBCamera->getNextFrameAsRgb565((uint16_t *)frame); 

       //LOGV("previewThread: generated frame to buffer %d", mCurrentPreviewFrame); 

       // Notify the client of a new frame. 
       if (mMsgEnabled & CAMERA_MSG_PREVIEW_FRAME) 
           mDataCb(CAMERA_MSG_PREVIEW_FRAME, buffer, mCallbackCookie); 

       // Advance the buffer pointer. 
       mCurrentPreviewFrame = (mCurrentPreviewFrame + 1) % kBufferCount; 

       // Wait for it... 
       usleep(delay); 
   } 

   return NO_ERROR; 

这样分析过后,问题就变得很明了了,要将图片存储下来,只要获取其一帧数据(getNextFrameAsRGB565),在takePicture函数中将其存储下来即可。好,让我们看看takePicture的实现:它启动了一个线程来调用pictureThread方法,这里就是我们大显身手的地方了! 
int CameraHardware::pictureThread() 

   if (mMsgEnabled & CAMERA_MSG_SHUTTER) 
       mNotifyCb(CAMERA_MSG_SHUTTER, 0, 0, mCallbackCookie); // 对应ShutterCallback 

   if (mMsgEnabled & CAMERA_MSG_RAW_IMAGE) { 
       mDataCb(CAMERA_MSG_RAW_IMAGE, mem, mCallbackCookie); // 对应原始图片(RAW)的PictureCallback 
   } 

   if (mMsgEnabled & CAMERA_MSG_COMPRESSED_IMAGE) { 
       mDataCb(CAMERA_MSG_COMPRESSED_IMAGE, mem, mCallbackCookie); // 对应JPEG图片的PictureCallback 
   } 
   return NO_ERROR; 
}  
每个if都对应了一个takePicture函数的callback,第二第三个就是图片要输出的地方!Android上的照相应用程序并不管RAW图片的输出,我们直接聚焦到第三个if。这里我们就不难理解为什么老是输出“小机器人”了,原来在第三个if中,飞凌并没有改原FakeCamera的代码,FakeCamera在这里直接读入一个CannedJpeg.h中的数据,而这里面存的就是那个“可爱”的机器人....好,那我们就改这里!首先先不管RAW到JPEG的转换,我们把RAW的数据直接写成BMP格式输出,看看是否工作。BMP格式文件头有54个字节,16位的数据格式为RGB555,所以完成的流程就三步: 
Step1. 在MemoryHeap上申请一块BMP数据暂存区,并写文件头 
Step2. 将原始数据从RGB565转换到RGB555,并存储到BMP数据暂存区 
Step3. 将BMP暂存区数据传递给mDataCb输出 
 
具体代码如下:  
Step1. 首先申请BMP暂存区: 
   int w, h; 
   unsigned int DATA_OFFSET = 54; 
   uint16_t WIDTH = w; 
   uint16_t HEIGHT = h; 

   mParameters.getPictureSize(&w, &h); 
   Ov965xCamera* Ov965xCamera = mOv965xCamera;     
   sp<MemoryHeapBase> heap = new MemoryHeapBase(DATA_OFFSET+w * h* 2); 
   sp<MemoryBase> mem = new MemoryBase(heap, 0, DATA_OFFSET+w * h* 2); // 2个字节构成一个像素  
 
写BMP文件头: 
       uint8_t header[54] = { 0x42, // identity : B 
       0x4d, // identity : M 
       0, 0, 0, 0, // file size 
       0, 0, // reserved1 
       0, 0, // reserved2 
       54, 0, 0, 0, // RGB data offset 
       40, 0, 0, 0, // struct BITMAPINFOHEADER size 
       0, 0, 0, 0, // bmp height 
       0, 0, 0, 0, // bmp width 
       1, 0, // planes 
       16, 0, // bit per pixel 
       0, 0, 0, 0, // compression 
       0, 0, 0, 0, // data size 
       0, 0, 0, 0, // h resolution 
       0, 0, 0, 0, // v resolution 
       0, 0, 0, 0, // used colors 
       0, 0, 0, 0 // important colors 
       }; 

       // file size offset 54 
   uint16_t file_size = WIDTH * HEIGHT * 2 + DATA_OFFSET; 
   header[2] = (uint8_t)(file_size & 0x000000ff); 
   header[3] = (file_size >> 8) & 0x000000ff; 
   header[4] = (file_size >> 16) & 0x000000ff; 
   header[5] = (file_size >> 24) & 0x000000ff; 

   // height 
   header[18] = HEIGHT & 0x000000ff; 
   header[19] = (HEIGHT >> 8) & 0x000000ff; 
   header[20] = (HEIGHT >> 16) & 0x000000ff; 
   header[21] = (HEIGHT >> 24) & 0x000000ff; 

   // width 
   header[22] = WIDTH & 0x000000ff; 
   header[23] = (WIDTH >> 8) & 0x000000ff; 
   header[24] = (WIDTH >> 16) & 0x000000ff; 
   header[25] = (WIDTH >> 24) & 0x000000ff;   
 
Step2.  获取当前帧,进行RGB565到RGB555的转换,将转换后的数据放入MemoryHeap中 
   unsigned int i; 
   for(i=0;i<DATA_OFFSET;i++){ 
     *((uint8_t*)heap->base()+i)=header[i]; 
   } 
    
   Ov965xCamera->getNextFrameAsRgb565((uint16_t*)heap->base()+DATA_OFFSET/2); 
    
   uint16_t *heap_base = (uint16_t*)heap->base(); 
   uint16_t pixel_data; 
   uint8_t tail_data; 
   for(i=DATA_OFFSET/2;i<DATA_OFFSET/2+WIDTH*HEIGHT;i++){ 
     pixel_data = *(heap_base+i); 
     tail_data = (uint8_t)(pixel_data & 0x001f); 
     pixel_data = (pixel_data & 0xffc0)>>1 | tail_data; 
     *(heap_base+i)=pixel_data; 
   }    
Step3. 调用callback,将数据存储到设备,并释放MemoryHeap 
   mDataCb(CAMERA_MSG_COMPRESSED_IMAGE, mem, mCallbackCookie); 
   heap=NULL;   
 
到这里,OK6410的Android系统就真正可以拍照了,通过之前介绍编译的方法,将编译好的libcamera.so放入目标机的/system/lib中,重启就能看到效果了,这是我用OV9650拍的照片,在PC上查看建议把jpg后缀名改为bmp,在Android上查看没任何问题: 


待解决的问题: 
1. 目前输出的文件虽然是jpg后缀,但实际是BMP格式的,想法是使用external/jpeg库中的函数来解决。 
2. 目前输出为320*240,若要使用更大分辨率,势必要更大的MemoryHeap。比如需要1024*768*2字节的空间,虽然可以申请,但暂时无法访问到后面的空间,目前还没想到解决方案。 
希望大家能一起把以上这两个问题解决,这样我们的开发板就能在图像应用上有更多用武之地了! 

文章转引自 飞凌嵌入式Android技术讨论区:www.witech.com.cn 
原文地址: www.th7.cn/Program/Android.shtml 感谢FlexChen大神整理。

这篇关于详述Android照相功能的实现(基于飞凌S3C6410开发板+单独编译安卓模块)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/675127

相关文章

基于 HTML5 Canvas 实现图片旋转与下载功能(完整代码展示)

《基于HTML5Canvas实现图片旋转与下载功能(完整代码展示)》本文将深入剖析一段基于HTML5Canvas的代码,该代码实现了图片的旋转(90度和180度)以及旋转后图片的下载... 目录一、引言二、html 结构分析三、css 样式分析四、JavaScript 功能实现一、引言在 Web 开发中,

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

Conda虚拟环境的复制和迁移的四种方法实现

《Conda虚拟环境的复制和迁移的四种方法实现》本文主要介绍了Conda虚拟环境的复制和迁移的四种方法实现,包括requirements.txt,environment.yml,conda-pack,... 目录在本机复制Conda虚拟环境相同操作系统之间复制环境方法一:requirements.txt方法

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Nginx 配置跨域的实现及常见问题解决

《Nginx配置跨域的实现及常见问题解决》本文主要介绍了Nginx配置跨域的实现及常见问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来... 目录1. 跨域1.1 同源策略1.2 跨域资源共享(CORS)2. Nginx 配置跨域的场景2.1

Python中提取文件名扩展名的多种方法实现

《Python中提取文件名扩展名的多种方法实现》在Python编程中,经常会遇到需要从文件名中提取扩展名的场景,Python提供了多种方法来实现这一功能,不同方法适用于不同的场景和需求,包括os.pa... 目录技术背景实现步骤方法一:使用os.path.splitext方法二:使用pathlib模块方法三

CSS实现元素撑满剩余空间的五种方法

《CSS实现元素撑满剩余空间的五种方法》在日常开发中,我们经常需要让某个元素占据容器的剩余空间,本文将介绍5种不同的方法来实现这个需求,并分析各种方法的优缺点,感兴趣的朋友一起看看吧... css实现元素撑满剩余空间的5种方法 在日常开发中,我们经常需要让某个元素占据容器的剩余空间。这是一个常见的布局需求

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja