| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.cts.verifier.camera.formats; |
| |
| import com.android.cts.verifier.PassFailButtons; |
| import com.android.cts.verifier.R; |
| |
| import android.app.AlertDialog; |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.graphics.ColorMatrix; |
| import android.graphics.ColorMatrixColorFilter; |
| import android.graphics.ImageFormat; |
| import android.graphics.Matrix; |
| import android.graphics.SurfaceTexture; |
| import android.hardware.Camera; |
| import android.hardware.Camera.CameraInfo; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.Menu; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.view.Surface; |
| import android.view.TextureView; |
| import android.widget.AdapterView; |
| import android.widget.ArrayAdapter; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.Spinner; |
| import android.widget.Toast; |
| |
| import java.io.IOException; |
| import java.lang.Math; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.TreeSet; |
| |
| /** |
| * Tests for manual verification of the CDD-required camera output formats |
| * for preview callbacks |
| */ |
| public class CameraFormatsActivity extends PassFailButtons.Activity |
| implements TextureView.SurfaceTextureListener, Camera.PreviewCallback { |
| |
| private static final String TAG = "CameraFormats"; |
| |
| private TextureView mPreviewView; |
| private SurfaceTexture mPreviewTexture; |
| private int mPreviewTexWidth; |
| private int mPreviewTexHeight; |
| private int mPreviewRotation; |
| |
| private ImageView mFormatView; |
| |
| private Spinner mCameraSpinner; |
| private Spinner mFormatSpinner; |
| private Spinner mResolutionSpinner; |
| |
| private int mCurrentCameraId = -1; |
| private Camera mCamera; |
| |
| private List<Camera.Size> mPreviewSizes; |
| private Camera.Size mNextPreviewSize; |
| private Camera.Size mPreviewSize; |
| private List<Integer> mPreviewFormats; |
| private int mNextPreviewFormat; |
| private int mPreviewFormat; |
| private SparseArray<String> mPreviewFormatNames; |
| |
| private ColorMatrixColorFilter mYuv2RgbFilter; |
| |
| private Bitmap mCallbackBitmap; |
| private int[] mRgbData; |
| private int mRgbWidth; |
| private int mRgbHeight; |
| |
| private static final int STATE_OFF = 0; |
| private static final int STATE_PREVIEW = 1; |
| private static final int STATE_NO_CALLBACKS = 2; |
| private int mState = STATE_OFF; |
| private boolean mProcessInProgress = false; |
| private boolean mProcessingFirstFrame = false; |
| |
| private TreeSet<String> mTestedCombinations = new TreeSet<String>(); |
| private TreeSet<String> mUntestedCombinations = new TreeSet<String>(); |
| |
| private int mAllCombinationsSize = 0; |
| |
| // Menu to show the test progress |
| private static final int MENU_ID_PROGRESS = Menu.FIRST + 1; |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| setContentView(R.layout.cf_main); |
| |
| mAllCombinationsSize = calcAllCombinationsSize(); |
| |
| // disable "Pass" button until all combinations are tested |
| setPassButtonEnabled(false); |
| |
| setPassFailButtonClickListeners(); |
| setInfoResources(R.string.camera_format, R.string.cf_info, -1); |
| |
| mPreviewView = (TextureView) findViewById(R.id.preview_view); |
| mFormatView = (ImageView) findViewById(R.id.format_view); |
| |
| mPreviewView.setSurfaceTextureListener(this); |
| |
| int numCameras = Camera.getNumberOfCameras(); |
| String[] cameraNames = new String[numCameras]; |
| for (int i = 0; i < numCameras; i++) { |
| cameraNames[i] = "Camera " + i; |
| mUntestedCombinations.add("All combinations for Camera " + i + "\n"); |
| } |
| mCameraSpinner = (Spinner) findViewById(R.id.cameras_selection); |
| mCameraSpinner.setAdapter( |
| new ArrayAdapter<String>( |
| this, R.layout.cf_format_list_item, cameraNames)); |
| mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener); |
| |
| mFormatSpinner = (Spinner) findViewById(R.id.format_selection); |
| mFormatSpinner.setOnItemSelectedListener(mFormatSelectedListener); |
| |
| mResolutionSpinner = (Spinner) findViewById(R.id.resolution_selection); |
| mResolutionSpinner.setOnItemSelectedListener(mResolutionSelectedListener); |
| |
| // Must be kept in sync with android.graphics.ImageFormat manually |
| mPreviewFormatNames = new SparseArray(7); |
| mPreviewFormatNames.append(ImageFormat.JPEG, "JPEG"); |
| mPreviewFormatNames.append(ImageFormat.NV16, "NV16"); |
| mPreviewFormatNames.append(ImageFormat.NV21, "NV21"); |
| mPreviewFormatNames.append(ImageFormat.RGB_565, "RGB_565"); |
| mPreviewFormatNames.append(ImageFormat.UNKNOWN, "UNKNOWN"); |
| mPreviewFormatNames.append(ImageFormat.YUY2, "YUY2"); |
| mPreviewFormatNames.append(ImageFormat.YV12, "YV12"); |
| |
| // Need YUV->RGB conversion in many cases |
| |
| ColorMatrix y2r = new ColorMatrix(); |
| y2r.setYUV2RGB(); |
| float[] yuvOffset = new float[] { |
| 1.f, 0.f, 0.f, 0.f, 0.f, |
| 0.f, 1.f, 0.f, 0.f, -128.f, |
| 0.f, 0.f, 1.f, 0.f, -128.f, |
| 0.f, 0.f, 0.f, 1.f, 0.f |
| }; |
| |
| ColorMatrix yOffset = new ColorMatrix(yuvOffset); |
| |
| ColorMatrix yTotal = new ColorMatrix(); |
| yTotal.setConcat(y2r, yOffset); |
| |
| mYuv2RgbFilter = new ColorMatrixColorFilter(yTotal); |
| } |
| |
| @Override |
| public boolean onCreateOptionsMenu(Menu menu) { |
| menu.add(Menu.NONE, MENU_ID_PROGRESS, Menu.NONE, "Current Progress"); |
| return super.onCreateOptionsMenu(menu); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| boolean ret = true; |
| switch (item.getItemId()) { |
| case MENU_ID_PROGRESS: |
| showCombinationsDialog(); |
| ret = true; |
| break; |
| default: |
| ret = super.onOptionsItemSelected(item); |
| break; |
| } |
| return ret; |
| } |
| |
| private void showCombinationsDialog() { |
| AlertDialog.Builder builder = |
| new AlertDialog.Builder(CameraFormatsActivity.this); |
| builder.setMessage(getTestDetails()) |
| .setTitle("Current Progress") |
| .setPositiveButton("OK", null); |
| builder.show(); |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| |
| setUpCamera(mCameraSpinner.getSelectedItemPosition()); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| |
| shutdownCamera(); |
| mPreviewTexture = null; |
| } |
| |
| @Override |
| public String getTestDetails() { |
| StringBuilder reportBuilder = new StringBuilder(); |
| reportBuilder.append("Tested combinations:\n"); |
| for (String combination: mTestedCombinations) { |
| reportBuilder.append(combination); |
| } |
| reportBuilder.append("Untested combinations:\n"); |
| for (String combination: mUntestedCombinations) { |
| reportBuilder.append(combination); |
| } |
| return reportBuilder.toString(); |
| } |
| |
| |
| public void onSurfaceTextureAvailable(SurfaceTexture surface, |
| int width, int height) { |
| mPreviewTexture = surface; |
| mPreviewTexWidth = width; |
| mPreviewTexHeight = height; |
| if (mCamera != null) { |
| startPreview(); |
| } |
| } |
| |
| public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { |
| // Ignored, Camera does all the work for us |
| } |
| |
| public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { |
| return true; |
| } |
| |
| public void onSurfaceTextureUpdated(SurfaceTexture surface) { |
| // Invoked every time there's a new Camera preview frame |
| } |
| |
| private AdapterView.OnItemSelectedListener mCameraSpinnerListener = |
| new AdapterView.OnItemSelectedListener() { |
| public void onItemSelected(AdapterView<?> parent, |
| View view, int pos, long id) { |
| if (mCurrentCameraId != pos) { |
| setUpCamera(pos); |
| } |
| } |
| |
| public void onNothingSelected(AdapterView parent) { |
| |
| } |
| |
| }; |
| |
| private AdapterView.OnItemSelectedListener mResolutionSelectedListener = |
| new AdapterView.OnItemSelectedListener() { |
| public void onItemSelected(AdapterView<?> parent, |
| View view, int position, long id) { |
| if (mPreviewSizes.get(position) != mPreviewSize) { |
| mNextPreviewSize = mPreviewSizes.get(position); |
| startPreview(); |
| } |
| } |
| |
| public void onNothingSelected(AdapterView parent) { |
| |
| } |
| |
| }; |
| |
| |
| private AdapterView.OnItemSelectedListener mFormatSelectedListener = |
| new AdapterView.OnItemSelectedListener() { |
| public void onItemSelected(AdapterView<?> parent, |
| View view, int position, long id) { |
| if (mPreviewFormats.get(position) != mNextPreviewFormat) { |
| mNextPreviewFormat = mPreviewFormats.get(position); |
| startPreview(); |
| } |
| } |
| |
| public void onNothingSelected(AdapterView parent) { |
| |
| } |
| |
| }; |
| |
| |
| |
| private void setUpCamera(int id) { |
| shutdownCamera(); |
| |
| mCurrentCameraId = id; |
| mCamera = Camera.open(id); |
| Camera.Parameters p = mCamera.getParameters(); |
| |
| // Get preview resolutions |
| |
| List<Camera.Size> unsortedSizes = p.getSupportedPreviewSizes(); |
| |
| class SizeCompare implements Comparator<Camera.Size> { |
| public int compare(Camera.Size lhs, Camera.Size rhs) { |
| if (lhs.width < rhs.width) return -1; |
| if (lhs.width > rhs.width) return 1; |
| if (lhs.height < rhs.height) return -1; |
| if (lhs.height > rhs.height) return 1; |
| return 0; |
| } |
| }; |
| |
| SizeCompare s = new SizeCompare(); |
| TreeSet<Camera.Size> sortedResolutions = new TreeSet<Camera.Size>(s); |
| sortedResolutions.addAll(unsortedSizes); |
| |
| mPreviewSizes = new ArrayList<Camera.Size>(sortedResolutions); |
| |
| String[] availableSizeNames = new String[mPreviewSizes.size()]; |
| for (int i = 0; i < mPreviewSizes.size(); i++) { |
| availableSizeNames[i] = |
| Integer.toString(mPreviewSizes.get(i).width) + " x " + |
| Integer.toString(mPreviewSizes.get(i).height); |
| } |
| mResolutionSpinner.setAdapter( |
| new ArrayAdapter<String>( |
| this, R.layout.cf_format_list_item, availableSizeNames)); |
| |
| // Get preview formats, removing duplicates |
| |
| HashSet<Integer> formatSet = new HashSet<>(p.getSupportedPreviewFormats()); |
| mPreviewFormats = new ArrayList<Integer>(formatSet); |
| |
| String[] availableFormatNames = new String[mPreviewFormats.size()]; |
| for (int i = 0; i < mPreviewFormats.size(); i++) { |
| availableFormatNames[i] = |
| mPreviewFormatNames.get(mPreviewFormats.get(i)); |
| } |
| mFormatSpinner.setAdapter( |
| new ArrayAdapter<String>( |
| this, R.layout.cf_format_list_item, availableFormatNames)); |
| |
| // Update untested entries |
| |
| mUntestedCombinations.remove("All combinations for Camera " + id + "\n"); |
| for (Camera.Size previewSize: mPreviewSizes) { |
| for (int previewFormat: mPreviewFormats) { |
| String combination = "Camera " + id + ", " |
| + previewSize.width + "x" + previewSize.height |
| + ", " + mPreviewFormatNames.get(previewFormat) |
| + "\n"; |
| if (!mTestedCombinations.contains(combination)) { |
| mUntestedCombinations.add(combination); |
| } |
| } |
| } |
| |
| // Set initial values |
| |
| mNextPreviewSize = mPreviewSizes.get(0); |
| mResolutionSpinner.setSelection(0); |
| |
| mNextPreviewFormat = mPreviewFormats.get(0); |
| mFormatSpinner.setSelection(0); |
| |
| |
| // Set up correct display orientation |
| |
| CameraInfo info = |
| new CameraInfo(); |
| Camera.getCameraInfo(id, info); |
| int rotation = 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; |
| } |
| |
| if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { |
| mPreviewRotation = (info.orientation + degrees) % 360; |
| mPreviewRotation = (360 - mPreviewRotation) % 360; // compensate the mirror |
| } else { // back-facing |
| mPreviewRotation = (info.orientation - degrees + 360) % 360; |
| } |
| if (mPreviewRotation != 0 && mPreviewRotation != 180) { |
| Log.w(TAG, |
| "Display orientation correction is not 0 or 180, as expected!"); |
| } |
| |
| mCamera.setDisplayOrientation(mPreviewRotation); |
| |
| // Start up preview if display is ready |
| |
| if (mPreviewTexture != null) { |
| startPreview(); |
| } |
| |
| } |
| |
| private void shutdownCamera() { |
| if (mCamera != null) { |
| mCamera.setPreviewCallback(null); |
| mCamera.stopPreview(); |
| mCamera.release(); |
| mCamera = null; |
| mState = STATE_OFF; |
| } |
| } |
| |
| private void startPreview() { |
| if (mState != STATE_OFF) { |
| // Stop for a while to drain callbacks |
| mCamera.setPreviewCallback(null); |
| mCamera.stopPreview(); |
| mState = STATE_OFF; |
| Handler h = new Handler(); |
| Runnable mDelayedPreview = new Runnable() { |
| public void run() { |
| startPreview(); |
| } |
| }; |
| h.postDelayed(mDelayedPreview, 300); |
| return; |
| } |
| mState = STATE_PREVIEW; |
| |
| Matrix transform = new Matrix(); |
| float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth; |
| float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight; |
| |
| if (heightRatio < widthRatio) { |
| transform.setScale(1, heightRatio/widthRatio); |
| transform.postTranslate(0, |
| mPreviewTexHeight * (1 - heightRatio/widthRatio)/2); |
| } else { |
| transform.setScale(widthRatio/heightRatio, 1); |
| transform.postTranslate(mPreviewTexWidth * (1 - widthRatio/heightRatio)/2, |
| 0); |
| } |
| |
| mPreviewView.setTransform(transform); |
| |
| mPreviewFormat = mNextPreviewFormat; |
| mPreviewSize = mNextPreviewSize; |
| |
| Camera.Parameters p = mCamera.getParameters(); |
| p.setPreviewFormat(mPreviewFormat); |
| p.setPreviewSize(mPreviewSize.width, mPreviewSize.height); |
| mCamera.setParameters(p); |
| |
| mCamera.setPreviewCallback(this); |
| switch (mPreviewFormat) { |
| case ImageFormat.NV16: |
| case ImageFormat.NV21: |
| case ImageFormat.YUY2: |
| case ImageFormat.YV12: |
| mFormatView.setColorFilter(mYuv2RgbFilter); |
| break; |
| default: |
| mFormatView.setColorFilter(null); |
| break; |
| } |
| |
| // Filter out currently untestable formats |
| switch (mPreviewFormat) { |
| case ImageFormat.NV16: |
| case ImageFormat.RGB_565: |
| case ImageFormat.UNKNOWN: |
| case ImageFormat.JPEG: |
| AlertDialog.Builder builder = |
| new AlertDialog.Builder(CameraFormatsActivity.this); |
| builder.setMessage("Unsupported format " + |
| mPreviewFormatNames.get(mPreviewFormat) + |
| "; consider this combination as pass. ") |
| .setTitle("Missing test" ) |
| .setNeutralButton("Back", null); |
| builder.show(); |
| mState = STATE_NO_CALLBACKS; |
| mCamera.setPreviewCallback(null); |
| break; |
| default: |
| // supported |
| break; |
| } |
| |
| mProcessingFirstFrame = true; |
| try { |
| mCamera.setPreviewTexture(mPreviewTexture); |
| mCamera.startPreview(); |
| } catch (IOException ioe) { |
| // Something bad happened |
| Log.e(TAG, "Unable to start up preview"); |
| } |
| } |
| |
| private class ProcessPreviewDataTask extends AsyncTask<byte[], Void, Boolean> { |
| protected Boolean doInBackground(byte[]... datas) { |
| byte[] data = datas[0]; |
| try { |
| if (mRgbData == null || |
| mPreviewSize.width != mRgbWidth || |
| mPreviewSize.height != mRgbHeight) { |
| |
| mRgbData = new int[mPreviewSize.width * mPreviewSize.height * 4]; |
| mRgbWidth = mPreviewSize.width; |
| mRgbHeight = mPreviewSize.height; |
| } |
| switch(mPreviewFormat) { |
| case ImageFormat.NV21: |
| convertFromNV21(data, mRgbData); |
| break; |
| case ImageFormat.YV12: |
| convertFromYV12(data, mRgbData); |
| break; |
| case ImageFormat.YUY2: |
| convertFromYUY2(data, mRgbData); |
| break; |
| case ImageFormat.NV16: |
| case ImageFormat.RGB_565: |
| case ImageFormat.UNKNOWN: |
| case ImageFormat.JPEG: |
| default: |
| convertFromUnknown(data, mRgbData); |
| break; |
| } |
| |
| if (mCallbackBitmap == null || |
| mRgbWidth != mCallbackBitmap.getWidth() || |
| mRgbHeight != mCallbackBitmap.getHeight() ) { |
| mCallbackBitmap = |
| Bitmap.createBitmap( |
| mRgbWidth, mRgbHeight, |
| Bitmap.Config.ARGB_8888); |
| } |
| mCallbackBitmap.setPixels(mRgbData, 0, mRgbWidth, |
| 0, 0, mRgbWidth, mRgbHeight); |
| } catch (OutOfMemoryError o) { |
| Log.e(TAG, "Out of memory trying to process preview data"); |
| return false; |
| } |
| return true; |
| } |
| |
| protected void onPostExecute(Boolean result) { |
| if (result) { |
| mFormatView.setImageBitmap(mCallbackBitmap); |
| if (mProcessingFirstFrame) { |
| mProcessingFirstFrame = false; |
| String combination = "Camera " + mCurrentCameraId + ", " |
| + mPreviewSize.width + "x" + mPreviewSize.height |
| + ", " + mPreviewFormatNames.get(mPreviewFormat) |
| + "\n"; |
| mUntestedCombinations.remove(combination); |
| mTestedCombinations.add(combination); |
| |
| displayToast(combination.replace("\n", "")); |
| |
| if (mTestedCombinations.size() == mAllCombinationsSize) { |
| setPassButtonEnabled(true); |
| } |
| } |
| } |
| mProcessInProgress = false; |
| } |
| |
| } |
| |
| private void setPassButtonEnabled(boolean enabled) { |
| ImageButton pass_button = (ImageButton) findViewById(R.id.pass_button); |
| pass_button.setEnabled(enabled); |
| } |
| |
| private int calcAllCombinationsSize() { |
| int allCombinationsSize = 0; |
| int numCameras = Camera.getNumberOfCameras(); |
| |
| for (int i = 0; i<numCameras; i++) { |
| // must release a Camera object before a new Camera object is created |
| shutdownCamera(); |
| |
| mCamera = Camera.open(i); |
| Camera.Parameters p = mCamera.getParameters(); |
| |
| HashSet<Integer> formatSet = new HashSet<>(p.getSupportedPreviewFormats()); |
| |
| allCombinationsSize += |
| p.getSupportedPreviewSizes().size() * // resolutions |
| formatSet.size(); // unique formats |
| } |
| |
| return allCombinationsSize; |
| } |
| |
| private void displayToast(String combination) { |
| Toast.makeText(this, "\"" + combination + "\"\n" + " has been tested.", Toast.LENGTH_LONG).show(); |
| } |
| |
| public void onPreviewFrame(byte[] data, Camera camera) { |
| if (mProcessInProgress || mState != STATE_PREVIEW) return; |
| |
| int expectedBytes; |
| switch (mPreviewFormat) { |
| case ImageFormat.YV12: |
| // YV12 may have stride != width. |
| int w = mPreviewSize.width; |
| int h = mPreviewSize.height; |
| int yStride = (int)Math.ceil(w / 16.0) * 16; |
| int uvStride = (int)Math.ceil(yStride / 2 / 16.0) * 16; |
| int ySize = yStride * h; |
| int uvSize = uvStride * h / 2; |
| expectedBytes = ySize + uvSize * 2; |
| break; |
| case ImageFormat.NV21: |
| case ImageFormat.YUY2: |
| default: |
| expectedBytes = mPreviewSize.width * mPreviewSize.height * |
| ImageFormat.getBitsPerPixel(mPreviewFormat) / 8; |
| break; |
| } |
| if (expectedBytes != data.length) { |
| AlertDialog.Builder builder = |
| new AlertDialog.Builder(CameraFormatsActivity.this); |
| builder.setMessage("Mismatched size of buffer! Expected " + |
| expectedBytes + ", but got " + |
| data.length + " bytes instead!") |
| .setTitle("Error trying to use format " |
| + mPreviewFormatNames.get(mPreviewFormat)) |
| .setNeutralButton("Back", null); |
| |
| builder.show(); |
| |
| mState = STATE_NO_CALLBACKS; |
| mCamera.setPreviewCallback(null); |
| return; |
| } |
| |
| mProcessInProgress = true; |
| new ProcessPreviewDataTask().execute(data); |
| } |
| |
| private void convertFromUnknown(byte[] data, int[] rgbData) { |
| int w = mPreviewSize.width; |
| int h = mPreviewSize.height; |
| // RGBA output |
| int rgbInc = 1; |
| if (mPreviewRotation == 180) { |
| rgbInc = -1; |
| } |
| int index = 0; |
| for (int y = 0; y < h; y++) { |
| int rgbIndex = y * w; |
| if (mPreviewRotation == 180) { |
| rgbIndex = w * (h - y) - 1; |
| } |
| for (int x = 0; x < mPreviewSize.width/3; x++) { |
| int r = data[index + 0] & 0xFF; |
| int g = data[index + 1] & 0xFF; |
| int b = data[index + 2] & 0xFF; |
| rgbData[rgbIndex] = Color.rgb(r,g,b); |
| rgbIndex += rgbInc; |
| index += 3; |
| } |
| } |
| } |
| |
| // NV21 is a semi-planar 4:2:0 format, in the order YVU, which means we have: |
| // a W x H-size 1-byte-per-pixel Y plane, then |
| // a W/2 x H/2-size 2-byte-per-pixel plane, where each pixel has V then U. |
| private void convertFromNV21(byte[] data, int rgbData[]) { |
| int w = mPreviewSize.width; |
| int h = mPreviewSize.height; |
| // RGBA output |
| int rgbIndex = 0; |
| int rgbInc = 1; |
| if (mPreviewRotation == 180) { |
| rgbIndex = h * w - 1; |
| rgbInc = -1; |
| } |
| int yIndex = 0; |
| int uvRowIndex = w*h; |
| int uvRowInc = 0; |
| for (int y = 0; y < h; y++) { |
| int uvInc = 0; |
| int vIndex = uvRowIndex; |
| int uIndex = uvRowIndex + 1; |
| |
| uvRowIndex += uvRowInc * w; |
| uvRowInc = (uvRowInc + 1) & 0x1; |
| |
| for (int x = 0; x < w; x++) { |
| int yv = data[yIndex] & 0xFF; |
| int uv = data[uIndex] & 0xFF; |
| int vv = data[vIndex] & 0xFF; |
| rgbData[rgbIndex] = |
| Color.rgb(yv, uv, vv); |
| |
| rgbIndex += rgbInc; |
| yIndex += 1; |
| uIndex += uvInc; |
| vIndex += uvInc; |
| uvInc = (uvInc + 2) & 0x2; |
| } |
| } |
| } |
| |
| // YV12 is a planar 4:2:0 format, in the order YVU, which means we have: |
| // a W x H-size 1-byte-per-pixel Y plane, then |
| // a W/2 x H/2-size 1-byte-per-pixel V plane, then |
| // a W/2 x H/2-size 1-byte-per-pixel U plane |
| // The stride may not be equal to width, since it has to be a multiple of |
| // 16 pixels for both the Y and UV planes. |
| private void convertFromYV12(byte[] data, int rgbData[]) { |
| int w = mPreviewSize.width; |
| int h = mPreviewSize.height; |
| // RGBA output |
| int rgbIndex = 0; |
| int rgbInc = 1; |
| if (mPreviewRotation == 180) { |
| rgbIndex = h * w - 1; |
| rgbInc = -1; |
| } |
| |
| int yStride = (int)Math.ceil(w / 16.0) * 16; |
| int uvStride = (int)Math.ceil(yStride/2/16.0) * 16; |
| int ySize = yStride * h; |
| int uvSize = uvStride * h / 2; |
| |
| int uRowIndex = ySize + uvSize; |
| int vRowIndex = ySize; |
| |
| int uv_w = w/2; |
| for (int y = 0; y < h; y++) { |
| int yIndex = yStride * y; |
| int uIndex = uRowIndex; |
| int vIndex = vRowIndex; |
| |
| if ( (y & 0x1) == 1) { |
| uRowIndex += uvStride; |
| vRowIndex += uvStride; |
| } |
| |
| int uv = 0, vv = 0; |
| for (int x = 0; x < w; x++) { |
| if ( (x & 0x1) == 0) { |
| uv = data[uIndex] & 0xFF; |
| vv = data[vIndex] & 0xFF; |
| uIndex++; |
| vIndex++; |
| } |
| int yv = data[yIndex] & 0xFF; |
| rgbData[rgbIndex] = |
| Color.rgb(yv, uv, vv); |
| |
| rgbIndex += rgbInc; |
| yIndex += 1; |
| } |
| } |
| } |
| |
| // YUY2 is an interleaved 4:2:2 format: YU,YV,YU,YV |
| private void convertFromYUY2(byte[] data, int[] rgbData) { |
| int w = mPreviewSize.width; |
| int h = mPreviewSize.height; |
| // RGBA output |
| int yIndex = 0; |
| int uIndex = 1; |
| int vIndex = 3; |
| int rgbIndex = 0; |
| int rgbInc = 1; |
| if (mPreviewRotation == 180) { |
| rgbIndex = h * w - 1; |
| rgbInc = -1; |
| } |
| |
| for (int y = 0; y < h; y++) { |
| for (int x = 0; x < w; x++) { |
| int yv = data[yIndex] & 0xFF; |
| int uv = data[uIndex] & 0xFF; |
| int vv = data[vIndex] & 0xFF; |
| rgbData[rgbIndex] = Color.rgb(yv,uv,vv); |
| rgbIndex += rgbInc; |
| yIndex += 2; |
| if ( (x & 0x1) == 1 ) { |
| uIndex += 4; |
| vIndex += 4; |
| } |
| } |
| } |
| } |
| |
| } |