安卓特效相機(一) Camera2的使用

系列文章:

安卓特效相機(一) Camera2的使用
安卓特效相機(二) EGL基礎(chǔ)
安卓特效相機(三) OpenGL ES 特效渲染
安卓特效相機(四) 視頻錄制

谷歌在安卓5.0的時候廢棄了原來的Camera架構(gòu),推出了全新的Camera2架構(gòu)。api相對之前的版本有很大的區(qū)別。

為了熟悉這個Camera2架構(gòu)的使用,我寫了個簡單的特效相機應(yīng)用,它支持三種簡單的特效:

5.jpg

接下來的幾篇文章我會一步步講下整個程序是如何實現(xiàn)的。

整體架構(gòu)

這篇文章主要講Camera2的使用,包括預覽和拍照。

Camera2的整體架構(gòu)如下:

一臺安卓設(shè)備可能擁有多個攝像設(shè)備,比如一般手機都有前后攝像頭,而每個攝像頭即為一個CameraDevice。應(yīng)用程序可以通過CameraManager獲取到所有的攝像設(shè)備的信息,打開攝像設(shè)備然后創(chuàng)建一個CameraCaptureSession連接應(yīng)用程序與攝像設(shè)備。之后應(yīng)用程序就可以使用這個CameraCaptureSession向攝像設(shè)備發(fā)送CaptureRequest來指揮攝像頭工作。

所以使用Camera2的流程大致為:

  1. 從CameraManager選擇攝像設(shè)備并打開
  2. 創(chuàng)建與CameraDevice的CameraCaptureSession
  3. 使用CameraCaptureSession向CameraDevice發(fā)送CaptureRequest

獲取攝像設(shè)備信息

鏡頭朝向

通常應(yīng)用程序想要使用攝像頭,需要先遍歷設(shè)備所有的攝像頭,然后選出合適的攝像頭去拍攝,例如我們想使用后置攝像頭:

CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);

try {
    for (String id : manager.getCameraIdList()) {
        CameraCharacteristics cc = manager.getCameraCharacteristics(id);
        if (cc.get(CameraCharacteristics.LENS_FACING) == facing) {
            ...
            break;
        }
    }
} catch (Exception e) {
    Log.e(TAG, "can not open camera", e);
}

通過CameraManager.getCameraIdList()方法可以列出所有攝像頭的id,然后通過CameraManager.getCameraCharacteristics可以拿到對應(yīng)攝像頭的CameraCharacteristics(特征集合),通過這個CameraCharacteristics我們可以拿到攝像頭的一些屬性,例如上面的鏡頭朝向。

輸出尺寸

應(yīng)用展示攝像頭畫面的view大小千奇百怪,如果攝像頭只能拍攝一種尺寸的畫面,那屏幕上顯示的時候就勢必需要進行縮放了。如果view長寬比和拍攝的畫面長寬比是一樣的還好,只需要等比縮放就可以了。但是如果長寬比不一樣那就勢必要發(fā)生形變或者裁切像素了。

于是一般攝像頭都會支持多種尺寸的輸出畫面,開發(fā)者可以種里面選取最合適的尺寸去顯示。

Size[] previewSizes = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(SurfaceTexture.class);
Size previewSize = getMostSuitableSize(previewSizes, width, height);
preview.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
...
manager.openCamera(id, mOpenCameraCallback, handler);

可以看到輸出尺寸和想要用于顯示的類相關(guān),例如我們的demo使用SurfaceTexture去顯示,就可以獲取攝像頭支持SurfaceTexture的所有尺寸。

然后指定輸出尺寸并不是將想要的尺寸設(shè)置給攝像機,而是設(shè)置SurfaceTexture的Buffer大小,然后攝像頭在將畫面繪制到SurfaceTexture上的時候就會使用最接近的尺寸去繪制了。

Camera2支持將畫面繪制到下面的幾種目標:

  • ImageReader
  • MediaRecorder
  • MediaCodec
  • Allocation
  • SurfaceHolder
  • SurfaceTexture

getMostSuitableSize里面我們選擇長寬比最接近width*height的尺寸:

 private Size getMostSuitableSize(
        Size[] sizes,
        float width,
        float height) {

    float targetRatio = height / width;
    Size result = null;
    for (Size size : sizes) {
        if (result == null || isMoreSuitable(result, size, targetRatio)) {
            result = size;
        }
    }
    return result;
}

private boolean isMoreSuitable(Size current, Size target, float targetRatio) {
    if (current == null) {
        return true;
    }
    float dRatioTarget = Math.abs(targetRatio - getRatio(target));
    float dRatioCurrent = Math.abs(targetRatio - getRatio(current));
    return dRatioTarget < dRatioCurrent
            || (dRatioTarget == dRatioCurrent && getArea(target) > getArea(current));
}

相機方向

細心的同學可能會看到這里的長寬比我用的是height/width,這是由于攝像機的方向和屏幕方向相差了90度,所以相機的長寬比應(yīng)該是屏幕的寬長比。

這個攝像頭方向的介紹可以看官方文檔:

The orientation of the camera image. The value is the angle that the camera image needs to be rotated clockwise so it shows correctly on the display in its natural orientation. It should be 0, 90, 180, or 270.

For example, suppose a device has a naturally tall screen. The back-facing camera sensor is mounted in landscape. You are looking at the screen. If the top side of the camera sensor is aligned with the right edge of the screen in natural orientation, the value should be 90. If the top side of a front-facing camera sensor is aligned with the right of the screen, the value should be 270.

比方說后置攝像頭的正方向就是豎著拿屏幕的時候的屏幕的右方:

4.png

所以豎著拿手機的時候拍的照片其實是橫的。于是我們在計算長寬比查找最時候的尺寸的時候就需要旋轉(zhuǎn)90度,也就是用height/width。

打開攝像頭

我們可以使用CameraManager.openCamera方法打開指定的攝像頭:

private CameraDevice.StateCallback mOpenCameraCallback =
            new CameraDevice.StateCallback() {
                @Override
                public void onOpened(CameraDevice camera) {
                    openCameraSession(camera);
                }

                @Override
                public void onDisconnected(CameraDevice camera) {
                }

                @Override
                public void onError(CameraDevice camera, int error) {
                }
            };

...

manager.openCamera(id, mOpenCameraCallback, handler);

mOpenCameraCallback是打開結(jié)果的回調(diào),而handler則決定了這個回調(diào)在哪個線程調(diào)用

連接攝像頭

在打開攝像頭的回調(diào)里我們可以拿到CameraDevice,然后但是我們并不能直接指揮攝像設(shè)備去干活,而是要通過CameraCaptureSession。

那怎么創(chuàng)建與CameraDevice的CameraCaptureSession呢?

可以通過CameraDevice.createCaptureSession

private CameraCaptureSession.StateCallback mCreateSessionCallback =
        new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession session) {
                mCameraCaptureSession = session;
                requestPreview(session);
            }

            @Override
            public void onConfigureFailed(CameraCaptureSession session) {

            }
        };
...

private void openCameraSession(CameraDevice camera) {
        mCameraDevice = camera;
        try {
            List<Surface> outputs = Arrays.asList(mPreviewSurface);
            camera.createCaptureSession(outputs, mCreateSessionCallback, mHandler);
        } catch (CameraAccessException e) {
            Log.e(TAG, "createCaptureSession failed", e);
        }
    }

除了同樣需要傳入回調(diào)和指定回調(diào)執(zhí)行線程的handler之外。

然后還需要傳入一個列表告訴攝像設(shè)備它需要繪制到什么地方,Camera2支持同時往多個目標繪制畫面。

但是并不是說我們這里指定mPreviewSurface,攝像頭就會直接開始往里面繪制畫面了,還需要發(fā)送request去請求繪制。

發(fā)送繪制請求

CaptureRequest的配置比較多,如果一個個去配的話比較繁瑣,所以谷歌已經(jīng)給我們創(chuàng)建好了幾個常用的模板,我們可以根據(jù)自己的需求去選擇一個來做修改

實時預覽

我們用TextureView來實時預覽攝像機畫面:

mPreview.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @SuppressLint("NewApi")
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        mSurfaceTexture = surface;
        mPreviewSurface = new Surface(surface);
        ...
        openCamera(mSurfaceTexture,
                CameraCharacteristics.LENS_FACING_BACK,
                mPreview.getWidth(),
                mPreview.getHeight());
        ...
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }
});

CaptureRequest這里依然需要指定將畫面繪制到我們的預覽Surface上:

CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
builder.addTarget(mPreviewSurface);
session.setRepeatingRequest(builder.build(), null, null);

值得注意的是每一次請求只會繪制一次,如果是預覽界面的話需要不停繪制,我們可以使用CameraCaptureSession.setRepeatingRequest讓他不斷發(fā)送Request去不斷的繪制,達到實時預覽的功能。

這個方法的第一個參數(shù)是CaptureRequest,第二和第三個參數(shù)仍然是回調(diào)和handler,這里我們不需要監(jiān)聽回調(diào),都設(shè)成null就好。

拍照

我們可以創(chuàng)建ImageReader來接收畫面:

Size[] photoSizes = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
                           .getOutputSizes(ImageReader.class);
mImageReader = getImageReader(getMostSuitableSize(photoSizes, width, height));

...

private ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            savePhoto(reader);
        }
    };

private ImageReader getImageReader(Size size) {
    ImageReader imageReader = ImageReader.newInstance(
            size.getWidth(),
            size.getHeight(),
            ImageFormat.JPEG,
            5);
    imageReader.setOnImageAvailableListener(mOnImageAvailableListener, mHandler);
    return imageReader;
}

在觸摸屏幕的時候發(fā)送請求繪制畫面到ImageReader上:

private void takePhoto() {
    try {
        CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(
                CameraDevice.TEMPLATE_STILL_CAPTURE);
        builder.addTarget(mPreviewSurface);
        builder.addTarget(mImageReader.getSurface());
        int rotation = getWindowManager().getDefaultDisplay().getRotation();
        builder.set(CaptureRequest.JPEG_ORIENTATION, mSensorOrientation);
        mCameraCaptureSession.capture(builder.build(), null, null);
    } catch (CameraAccessException e) {
        Log.d(TAG, "takePhoto failed", e);
    }
}

圖片方向

我們這里設(shè)置了下CaptureRequest.JPEG_ORIENTATION,原因和上面說的攝像頭設(shè)備的方向有關(guān),如果不設(shè)置的話,預覽的窗口里面是豎著拍的照片實際保存下來會變成橫的。

這個mSensorOrientation也是從CameraCharacteristics里面獲取的:

CameraCharacteristics cc = manager.getCameraCharacteristics(id);
...
mSensorOrientation = cc.get(CameraCharacteristics.SENSOR_ORIENTATION);

請求隊列

細心的同學可能還會注意到,這個請求不僅將mImageReader.getSurface()添加到Target,同時也將mPreviewSurface添加到Target了,這是為什么呢?

不知道大家還記不記得之前我們說過,每一個CaptureRequest會執(zhí)行一次繪制,實時預覽靠的就是setRepeatingRequest不斷重復的發(fā)送CaptureRequest。

其實CameraDevice對CaptureRequest的執(zhí)行是串行的,當沒有拍照的請求的時候,請求隊列是這樣的:

2.png

而當有拍照的請求進去的時候,請求隊列是這樣的:

3.png

預覽請求中間插入了一個拍照請求,如果這個拍照請求里面沒有將畫面繪制到預覽的View上面,預覽畫面就會少了一幀,相當于卡了一下。所以拍照的時候也要將mPreviewSurface放到Target中。

關(guān)閉攝像頭

我們需要在退出應(yīng)用的時候關(guān)閉攝像頭,要不然可能會影響其他應(yīng)用使用攝像頭:

private CameraDevice mCameraDevice;。

...    

if (mImageReader != null) {
    mImageReader.close();
    mImageReader = null;
}

if (mCameraCaptureSession != null) {
    mCameraCaptureSession.close();
    mCameraCaptureSession = null;
}

if (mCameraDevice != null) {
    mCameraDevice.close();
    mCameraDevice = null;
}

本篇文章的完整代碼可以在github上獲取

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內(nèi)容