文章摘要: 在camera1中支援預覽返回格式有ImageFormat.NV21和ImageFormat.YV12camera2中預覽和拍照都沒有直接的影象資料回撥
1.Android sdk中的Camera
- 2.Camera開發中遇到的一些疑問
- 3.Camera例項
- 4.camera2例項
- 5.總結
一、Android sdk中的Camera
在Android Studio中敲下Camera,會給出兩個提示類:
1.android.graphics.Camera ,一個照相機例項可以被用於計算3D變換,生成一個可以被使用的Matrix矩陣,一個例項,用在畫布上。
2.android.hardware.Camera,用於設定影象捕獲設定、開始/停止預覽、快照圖片以及檢索用於視訊編碼的幀。這個類是相機服務的客戶端,它管理實際的相機硬體。
今天要講的是相機管理類android.hardware.Camera,
但在API 21+之後該類已被廢棄,新的相機管理類在 android.hardware.camera2 包下,所以本文會覆蓋camera和camera2兩種API。
二、Camera開發中遇到的一些疑問
- 拍攝的照片為什麼會旋轉90度
- 預覽的影象被拉伸
- 預覽時返回的影象幀資料為什麼不能正常解析成bitmap
針對以上的問題我們從camera成像說起,camera其實應該叫image sensor。
image sensor有兩大類CMOS和CCD,手機中常常使用的是價格低廉和整合性高的CMOS,sensor的成像原理是景物通過鏡頭(LENS)生成的光學 影象投射到影象感測器(Sensor)表面上,然後轉為模擬的電訊號,經過A/D(模數轉換)轉換後變為數字影象訊號,再送到數字訊號處理晶片(DSP) 中加工處理,再通過IO介面傳輸到CPU中處理,通過LCD 就可以看到影象了。
sensor一般的輸出格式:
1)YUV sensor
YUV sensor輸出的Data格式為YUV,也是使用android.hardware.Camera進行預覽返回預覽幀的常見格式。
2)Raw sensor
Raw Sensor輸出的Data格式為原始未經處理的Raw,影象感應器將捕捉到的光源訊號轉化為數字訊號的原始資料。RAW檔案是一種記錄了數碼相機感測器 的原始資訊,同時記錄了由相機拍攝所產生的一些原資料(如ISO的設定、快門速度、光圈值、白平衡等)的檔案,在camera2中可檢視相機是否支援 raw格式並獲取raw資料。
從sensor輸出格式可以知道預覽時的幀資料為什麼不能簡單生成bitmap,因為bitmap建立需要的是rgb格式,而預覽幀資料一般是YUV格式。
image sensor的成像掃描方向是固定的,但image sensor安裝在手機的成像方向不一定和手機自然方向(一般為豎屏)一致。
上圖是常見的後置攝像頭與手機自然方向的對比,image sensor 順時針旋轉90度就和手機自然方向一致,這就是為什麼我們要給camera設定90度才能豎屏預覽。
但90度不是定勢,所以實際編碼中可以獲取image sensor的方向和phone display的方向進行計算得出正確的旋轉值。
來看看Android官方給出的建議計算方案
setCameraDisplayOrientation
/**
* 設定camera sensor 展示方向和螢幕自然方向一致
*/
public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
//通過cameraId獲取當前camera資訊
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
//獲取當前螢幕方向,計算螢幕旋轉角度
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result;
//根據前後攝像頭的不同得出當前需要矯正的角度
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 – result) % 360; // compensate the mirror
} else { // back-facing
result = (info.orientation – degrees + 360) % 360;
}
//設定camera顯示影象角度
camera.setDisplayOrientation(result);
}
值得注意的是,以上操作只是讓預覽中顯示的影象正常,對於預覽返回幀資料和拍照取得的資料並不會真正改變其方向,如果要得到和預覽影象一致的照片還需要對影象資料進行額外的旋轉操作,但如果只需要拍照影象,則不需要對資料進行旋轉,使用camera.getParameters().setRotation(rotation)可以直接得到正確方向的jpeg影象,對於rotation值的計算官方給出的演算法在setRotation方法註釋中,要特別注意的是這個值只改變jpeg方向,並不能改變預覽幀YUV資料方向,在camera2中的使用會更直觀一點,後面再講。
預覽時影象顯示為什麼被拉伸? 這和image sensor返回的預覽影象大小有關,只有預覽圖的大小和顯示預覽檢視大小相近相等時才最自然。
一臺手機image sensor支援的屬性設定都是有固定值的,絕大多數時候拍攝的圖片好不好看取決於image sensor的硬體成本,成像不好,再如何p圖也枉然。
3.Camera例項
camera1 中CameraService執行在mediaserver程序中,雖然camera2在5.0就存在,但CameraServide依然存在於 mediaserver程序中,在Android7.0+系統中CameraService才作為一個獨立的系統服務程序存在。
看下應用層的使用流程。
使用camera2進行預覽拍照最好的例子可檢視官方demo。
先看兩個最關鍵的問題
1、怎麼設定預覽/拍照影象引數?
2、從哪裏拿預覽/拍照的影象資料?
第一點,camera1的引數設定都在camera.getParameters()中,camera2在哪裏呢?在CaptureRequest.Builder中,比如設定對焦和曝光模式:
set
//設定對焦模式為快速連續對焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
前面講過camera的3A模式,這裏看引數命名就直接明瞭了,更多引數詳解可以看3A 模式和狀態轉換。
另 外值得注意的是camera2相比camera1支援的資料格式、預覽大小等都有很大的差異,比如在榮耀8上使用camera1進行預覽獲取的最大預覽大 小是1920×1080, 拍照支援最大size是3968×2240, 使用camera2的最大預覽尺寸為3968×2240,拍照最大支援3968×2240,camera2可支援的預覽size更加豐富,使用 camera2獲取的Size根據格式的不同返回不同, 如果不支援輸出的格式則返回null,使用方式如下;
獲取輸出size
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
for (Size size : sizes) {
System.out.println(“>>>>>>>camera2 preview size : >>>>> ” + size.toString());
}
Size[] jpegSizes = map.getOutputSizes(ImageFormat.JPEG);
for (Size size : jpegSizes) {
System.out.println(“>>>>>> jpeg : size ” + size.toString());
}
Size[] rawSizes = map.getOutputSizes(ImageFormat.RAW_SENSOR);
for (Size size : rawSizes) {
System.out.println(“>>>>>> raw : size ” + size.toString());
第二點,camera2中預覽和拍照都沒有直接的影象資料回撥,它引入了 ImageReader類,在createCaptureSession時可傳入surface陣列入參,通過 CaptureRequest.Builder.addTarget新增多個surface接收影象資料:
SurfaceView、TextureView等呈現的surface資料不能被直接訪問,而ImageReader類允許應用程式直接訪問呈現到surface的影象資料,使用方式如下:
createCameraPreviewSession()
private void createCameraPreviewSession() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
//設定預覽大小
texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
Surface surface = new Surface(texture);
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(surface);
imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.JPEG, 1);
// 暫不加入預覽目標,在需要獲取影象資料時新增即可
// previewRequestBuilder.addTarget(imageReader.getSurface());
imageReader.setOnImageAvailableListener(onImageAvailableListener, handler);
cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
captureSession = session;
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
try {
previewRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation());
// Finally, we start displaying the camera preview.
captureRequest = previewRequestBuilder.build();
captureSession.setRepeatingRequest(captureRequest, captureCallback, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 請求下一個有效的影象
Image img = reader.acquireNextImage();
ByteBuffer buffer = img.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
bms.add(data);
// 請求之後必須關閉
img.close();
}
};
在camera1中支援預覽返回格式有ImageFormat.NV21和ImageFormat.YV12,但是camera2中通常僅開放ImageFormat.JPEG、ImageFormat.RAW_SENSOR和ImageFormat.YUV_420_888,其他格式通常是私有的,不可讀取,尤其不支援ImageFormat.NV21。
本 例的實現就是通過鎖定對焦,預覽計時3秒,從ImageReader的OnImageAvailableListener中獲取可用Image放入 list,解析list生成bitmap,生成原始GIF,自定義view建立畫布,獲得畫布上使用者選取繪製畫素點,根據畫素點列表,獲取第一幀之後的每 一幀相關畫素,迴圈替換第一幀選取區域畫素點,生成微動gif,核心程式碼如下:
生成微動gif
/**
* 生成gif圖片
*/
void makeGif(final ArrayList
new Thread() {
@Override
public void run() {
AnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();
if (!gifFile.exists())
try {
gifFile.createNewFile();
} catch (IOException e1) {
e1.printStackTrace();
}
OutputStream os;
try {
os = new FileOutputStream(gifFile);
gifEncoder.start(os); //注意順序
gifEncoder.setRepeat(0);
Bitmap one = null;
for (int i = 0; i < list.size(); i++) {
byte[] bytes = list.get(i);
if (i == 0) {
//取第一幀為背景
one = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
gifEncoder.addFrame(one);
} else {
//獲取後續幀影象
Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//第一幀作為後續所有幀的原圖
Bitmap other = Bitmap.createBitmap(one);
for (Point point : points) {
//第一幀中的選取畫素點替換成後續幀的畫素點,形成第一幀的區域性動態
other.setPixel(point.x, point.y, bm.getPixel(point.x, point.y));
}
gifEncoder.addFrame(other);
}
}
gifEncoder.finish();
System.out.println(“儲存地址:” + gifFile.getAbsolutePath());
runOnUiThread(new Runnable() {
@Override
public void run() {
callback.complete();
}
});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}.start();
}
選取畫素點操作的自定義view核心程式碼如下:
DrawView
private void init() {
path = new Path();
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0x88cccccc);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20);
p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(20);
p.setColor(0xffff0000);
}
@Override
protected void onDraw(Canvas canvas) {
if (null != bitmap) {
// canvas.drawBitmap(bitmap, 0, 0, new Paint());
canvas.drawPath(path, paint);
canvasb.drawPath(path, p);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
path.lineTo(event.getX(), event.getY());
postInvalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* 畫素點分析
*/
private void test() {
list.clear();
int w = bm.getWidth();
int h = bm.getHeight();
for (int i = 0; i < w; i++) {
for (int j = 0; j < h; j++) {
if (bm.getPixel(i, j) == 0xffff0000) {
list.add(new Point(i, j));
}
}
}
}
5.總結
從camera2和camera1的使用體驗來說,新的api帶來了更多的可能性,但廠商定製手機五花八門,同樣版本的手機也不一定都支援同樣的功能,所以做相機開發需要考慮到相當多的 相容特性。
從Android API出發,需要考慮camera使用版本、預覽view使用類,從裝置出發,需要考慮相機支援特性、支援資料格式等,谷歌官方給出的一份相容方案如下:
https://github.com/google/cameraview
參考資料:
http://lib.csdn.net/article/embeddeddevelopment/67528
http://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.htm
http://www.fourcc.org/yuv.php
https://source.android.google.cn/devices/camera/
[ShareSDK] 輕鬆實現社會化功能 強大的社交分享
[SMSSDK] 快速整合簡訊驗證 聯結通訊錄社交圈
[MobLink] 打破App孤島 實現Web與App無縫連結
[MobPush] 快速整合推送服務 應對多樣化推送場景
[AnalySDK] 精準化行為分析 + 多維資料模型 + 匹配全網標籤 + 垂直行業分析顧問
BBSSDK | ShareREC | MobAPI | MobPay | ShopSDK | MobIM | App工廠
截止2018 年4 月,Mob 開發者服務平臺全球裝置覆蓋超過84 億,SDK下載量超過3,300,000+次,服務超過380,000+款移動應用