我们是最后一代婴儿时期没有被人用手机拍过照片的人。
——轶话
照相机诞生至今,已有近200年的历史。
1826年,法国的尼普斯(Nicéphore Niépce,1765—1833)用沥青加上薰衣草油涂在铅锡合金板上,曝光约8小时,拍摄了自家二楼阳台的窗外,这是人类保留下来的最早的照片,如图2-8所示。
图2-8 人类最早的照片
1861年,第一张彩色图像在电磁学之父麦克斯韦的演讲中诞生。
1878年,柯达(Kodak)创始人乔治·伊士曼(George Eastman,美国,1854—1932)成功研制了胶片,开启了传统相机时代。
1975年,柯达的史蒂文·赛尚(Steven J.Sasson,美国,1950—)研制出第一台数码相机,光电逐步取代光化学。
2007年,随着苹果智能手机的发布,拍摄成为触手可及的日常。
本节将介绍获取视频采集设备信息的方法,包括采集设备枚举、参数选择、插拔通知。
视频采集的第一步,是枚举出系统上有哪些采集设备。
在Windows上,我们使用DirectShow技术来实现设备枚举,这基于C++的组件对象模型技术。
在macOS和iOS上,我们使用AVFoundation的AVCaptureDevice来实现枚举,这基于Objective-C的OC类。
在Android上,我们使用android.hardware.Camera2来实现枚举,这基于Java的对象包。
不论在哪个平台上,我们最终都需要将数据转换到C++层,以便后续统一处理。这涉及COM编程、OC混编、JNI编程等基础知识,有关内容可以查阅附录B和F。
Windows采集设备枚举
使用DirectShow需要如下头文件和库文件。
我们创建CLSID_SystemDeviceEnum对象,并获取ICreateDevEnum接口。
ICreateDevEnum接口只有一个方法,就是获取一个IEnumMoniker的接口指针。
通过IEnumMoniker,我们可以循环枚举到IMoniker接口,其中Moniker是名字的意思。
通过IMoniker的BindToStorage方法,获取设备属性接口IPropertyBag,从而获得设备属性。
通过IMoniker的BindToObject方法,获取一个IBaseFilter的指针,后续可用它操纵设备。
对于每个设备,我们都定义一个C++类,用来保存这些信息。
m_device_name用作设备名称的展示;m_device_path用作设备ID的获取;m_device_filter用来查询设备的能力,显示设备的设置对话框,或者后续采集时使用。
在作者的Windows系统上,有一个内置摄像头和一个USB外接摄像头,获取的信息如下。
macOS和iOS采集设备枚举
相比Windows,在macOS下的采集设备枚举要容易得多,Apple公司已经封装好了AV CaptureDevice类。
对于返回的AVCaptureDevice数组,使用count可访问其总数,使用objectAtIndex可访问其元素。这个设备对象有30多个属性,我们感兴趣的是下面这些。
作者的macOS笔记本上有一个内置的摄像头,其信息如下。
OC文件的后缀名通常是.m。当OC文件需要包含C++头文件时,需要将其后缀名改为.mm。
对于iOS,这里的枚举方法是完全一样的,作者的iPhone手机有前后两个摄像头。
Android设备枚举
Android系统提供了android.hardware.Camera包,可以轻松枚举相机。
上述代码在作者的Android手机上的运行结果如下。
Camera1在API level 21时被废弃了。推荐使用Camera2来完成相机的枚举。
上述代码在作者的Android手机上显示如下。
这里后置摄像头为编号0,旋转角为90 ° 。前置摄像头为编号1,旋转角为270 ° 。
俗话说,摄影是用光的艺术。对于相机而言,如何捕获与控制光的留存是核心问题。将拍照过程想象成给黑暗的密室开一扇窗,透一缕光,则关键的控制部件有以下4个。
· 镜头 (Lens)调整焦距控制进光的范围,即窗的取景范围。
· 光圈 (Aperture)控制窗户的大小。
· 快门 (Shutter)控制开窗的时长。
· 感光度 (ISO)调整底片/感应器对光的敏感程度。
(1)镜头焦距决定了我们要拍的物体范围。焦距越小,则视角越广。拍风景时,焦距通常在24~35mm,手机上的广角主摄为25~28mm。拍人像时,焦距通常在35~70mm,这属于标准镜头,其与肉眼看到的内容大致相符,50mm拍人像时效果最佳。
特殊镜头,如长焦镜头、微焦镜头、超广角镜头等,提供了更宽泛的焦距和视角。通过镜头的物理变焦也称为 光学变焦 。
(2)光圈用来控制进光量,常用 f 值表示。 f 值越小,光圈越大,如图2-9所示。
图2-9 不同光圈大小
光圈越大,进的光线越多,图像越亮,但同时会使景深(Depth of Field,DOF)变浅,带来背景虚化模糊。反之,光圈小,进光少,则景深大,背景清晰。
(3)快门用来控制曝光时间,常见值为1秒、1/2秒、1/4秒……至1/2500秒。该值应与光圈大小配合,光圈越大,快门时间应越短,使得进入的光线总量保持恒定。通常快门时间越短,能捕捉到的运动细节越多。
(4)感光度的概念来自传统相机,常见值为100、200和400,数值越高,表明对光线越敏感。感光度过高会产生噪点,应尽量使用低感光度进行拍摄。只有在小光圈高速快门时,为了提高拍摄成功率,才稍微增大感光度。
上述4个物理层面的拍照参数,会影响最终的成像效果。
对RTC而言,我们关心的采集参数为分辨率、帧率、像素格式。
·分辨率,就是图像大小,用长和宽表示,单位是像素。如640×480、1280×720等,常限制于4096之内。普通屏幕使用4∶3,宽屏使用16∶9,常见的视频分辨率规格如表2-3所示。
表2-3 常见的视频分辨率规格
·帧率,就是每秒采样的图像数,如15 fps、30 fps等,常限制在120 fps以内。帧率会受曝光时间长短的影响。在开启自动曝光(Automatic Exposure)模式后,设光圈的 f 值为 A ,曝光时间为 T ,环境亮度为 B ,感光度为 S ,则它们之间的关系如式(2.7)所示。
因此环境的亮度通常会影响摄像头的输出帧率,越亮的环境,曝光时间越短,帧率往往会越高。
·像素格式,为硬件输出的帧格式,有BGRA、I420、NV12等。
下面我们来看各个平台是如何枚举这些能力的。
Windows平台
在上一节,我们获得了每个设备的IBaseFilter指针,它有一个EnumPins()方法,可以用来枚举Filter上的端口(Pin)。每个Filter都会有若干输入Pin和输出Pin。通过枚举,可以找到Filter上所有的输出端口(Output Pin)。
在输出端口上,可以查询IAMStreamConfig接口,使用它可以遍历其支持的分辨率、像素格式及平均帧率。
对于给定的像素格式和分辨率,支持的帧率可能有多种。可以使用Filter上的IAMVideo Control来获取帧率列表。
当要选择某种格式和分辨率进行采集时,使用IAMStreamConfig的SetFormat函数。
CaptureDevice
CaptureDevice是一个简单的设备枚举示例程序,用来在Windows上枚举视频采集设备,打印其所有能力。其界面如图2-10所示。
这里对IAMStreamConfig获取的结构体的每一个值都进行了dump输出。
在VIDEOINFOHEADER结构中,有一个BITMAPINFO结构,它与位图文件头中的定义是一致的。
“Show Window”按钮显示了一个基于IBaseFilter的设备属性框。
具体的实现细节,读者可以直接查看示例工程的代码。
图2-10 CaptureDevice界面
macOS和iOS平台
macOS和iOS平台的格式遍历仍使用AVCaptureDevice,它有一个formats方法,返回该设备支持的格式。
这里420v表示kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange。
对应420f表示kCVPixelFormatType_420YpCbCr8BiPlanarFullRange。
yuvs表示kCVPixelFormatType_422YpCbCr8_yuvs,以Y0 Cb Y1 Cr排列。
若要设置此格式进行采集,需要先锁住设备,然后修改。
若要修改输出像素格式,需给AVCaptureSession添加AVCaptureVideoDataOutput,设置其videoSettings属性的kCVPixelBufferPixelFormatTypeKey字段,参看下一节的示例工程。
Android平台
Camera1的Camera.Parameters类提供了大量的相机参数信息。
Camera2可从设备ID获取的CameraCharacteristics继续获得更多属性。
给定分辨率下的最大帧率,还可以通过如下方法获得。
Android平台支持NV21和YV12格式,默认会使用NV21格式的视频帧。
现代PC都支持外接设备的热插拔。如果在采集过程中,设备发生了断开,或者有新的采集设备连接,我们希望得到相应的通知处理。
本节将介绍各个平台上的设备插拔与程序之间是如何交互的。
Windows平台
Windows平台上的设备插拔通过WM_DEVICECHANGE事件发送到对应的窗口上。一个窗口想要系统通知这个事件,需要调用下列API进行注册。
当事件跟随WM_DEVICECHANGE到来时,WPARAM是事件的类型,我们关注的两类事件是DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE。
LPARAM是一个DEV_BROADCAST_DEVICEINTERFACE*的指针。
当我们不再需要监听变更设备时,注销之前的通知句柄即可。
macOS平台
macOS平台使用NSNotificationCenter来注册设备的添加与删除事件。
当不需要再关注通知时,可以移除观察者。
移动端采集中断
在手机上,摄像头不会轻易被插拔,但是由于手机场景的特殊性,采集过程经常会被以下事件打断。
·App退到后台:VideoDeviceNotAvailableInBackground。
·相机被其他App抢占:VideoDeviceInUseByAnotherClient。
·多个前台App:VideoDeviceNotAvailableWithMultipleForegroundApps。
·系统压力:VideoDeviceNotAvailableDueToSystemPressure。
在iOS平台上,相关的通知通过AVCaptureSessionWasInterruptedNotification的AVCaptureSessionInterruptionReason获得。
在Android平台上,相关的错误通知可以通过RuntimeException等异常抛出来,也可以通过android.hardware.Camera的setErrorCallback接口来设置回调通知。
本节我们介绍了各个平台上的视频采集设备枚举方法,对设备支持的采样参数做了枚举,并学习了如何得到设备插拔的通知。下一节我们将介绍如何获取视频帧数据。
1.照相机的发展史,可以通过网络在线查阅 。
2.微软音视频技术有MSDN在线文档,其中有关于视频采集的专题。
3.微软为DirectShow提供了一些基类,更好地封装了使用步骤。
4.《DirectShow开发指南》(2003年),陆其明著,是DirectShow非常好的入门图书。
5.《AV Foundation开发秘籍》(2015年)介绍了AVFoundation的用法。
6.《Android应用开发攻略》(2013年)介绍了Android应用的开发调试过程。
1.[1人天](设备的详细信息)尝试打印iOS视频采集设备的全部属性。
2.[2人天](Linux设备枚举)在Linux上,我们对文件设备/dev/videoX( X =0,1,···,63)进行open()操作,获得对应的文件描述符(File Descriptor,FD),来实现设备的枚举。试着写一个枚举程序。
3.[1人天](高拍仪)高拍仪是一种全新的采集设备,主要用来拍摄文本纸张,用作在线教学使用。试枚举出高拍仪的各种采集能力。
4.[1人天](闪光与变焦)手机上的采集往往可以进行闪光灯与变焦的调整。试使用相关API。
5.[1人天](iOS高级参数)iOS有许多高级采集参数,包括缩放因子(zoom factor)、曝光模式(exposure mode)、曝光点(set_exposure_point)、曝光补偿(set_exposure_compensation)、人脸检测(face detection)、对焦模式(focus Mode)、白平衡模式(whiteBalanceMode)、防抖配置(anti-shake),试了解其含义,并使用相关API。
6.[30分钟](Android相机检测)写一个函数,检查Android手机上是否有相机。