aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorEino-Ville Talvala <etalvala@google.com>2014-06-17 09:45:39 -0700
committerEino-Ville Talvala <etalvala@google.com>2014-07-14 17:32:18 +0000
commitfdfd60aee36376910bacac00426d1b0f22bcfa2b (patch)
tree101e0c2bfc98d1e3dd6ead3993879586fc120e6f /apps
parent8cac9f1633776c08cd895946ef806c0ecf3ae774 (diff)
downloadpdk-fdfd60aee36376910bacac00426d1b0f22bcfa2b.tar.gz
Add ImageReader target
- Capture JPEG, YUV_420_888 (as raw planar .yuv dump), and RAW_SENSOR (as DNG) - View all image types with rough rendering - Full-color JPEG - Monochrome YUV - RAW_SENSOR with no color balancing and downsample-as-demosaic - RAW10 not yet supported Change-Id: I04908a8ad2516a50f6b6aef19f0fa8b30635f759
Diffstat (limited to 'apps')
-rw-r--r--apps/TestingCamera2/res/layout/imagereader_target_subpane.xml62
-rw-r--r--apps/TestingCamera2/res/values/strings.xml6
-rw-r--r--apps/TestingCamera2/src/com/android/testingcamera2/CameraControlPane.java41
-rw-r--r--apps/TestingCamera2/src/com/android/testingcamera2/ImageReaderSubPane.java648
-rw-r--r--apps/TestingCamera2/src/com/android/testingcamera2/TargetControlPane.java2
5 files changed, 754 insertions, 5 deletions
diff --git a/apps/TestingCamera2/res/layout/imagereader_target_subpane.xml b/apps/TestingCamera2/res/layout/imagereader_target_subpane.xml
new file mode 100644
index 0000000..390fc30
--- /dev/null
+++ b/apps/TestingCamera2/res/layout/imagereader_target_subpane.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res/com.android.testingcamera2"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <Spinner
+ android:id="@+id/target_subpane_image_reader_format_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/target_subpane_image_reader_format_prompt" />
+ <Spinner
+ android:id="@+id/target_subpane_image_reader_size_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/target_subpane_image_reader_size_prompt" />
+ <Spinner
+ android:id="@+id/target_subpane_image_reader_count_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/target_subpane_image_reader_count_prompt" />
+ <ImageView
+ android:id="@+id/target_subpane_image_reader_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/target_subpane_image_reader_prev_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/target_subpane_image_reader_prev_button" />
+ <Button
+ android:id="@+id/target_subpane_image_reader_save_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/target_subpane_image_reader_save_button" />
+ <Button
+ android:id="@+id/target_subpane_image_reader_next_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/target_subpane_image_reader_next_button" />
+ </LinearLayout>
+</merge>
diff --git a/apps/TestingCamera2/res/values/strings.xml b/apps/TestingCamera2/res/values/strings.xml
index 7c9b2a0..c4047e3 100644
--- a/apps/TestingCamera2/res/values/strings.xml
+++ b/apps/TestingCamera2/res/values/strings.xml
@@ -62,6 +62,12 @@
<string name="target_subpane_texture_view_size_prompt">Size</string>
<string name="target_subpane_surface_view_size_prompt">Size</string>
+ <string name="target_subpane_image_reader_format_prompt">Format</string>
+ <string name="target_subpane_image_reader_size_prompt">Size</string>
+ <string name="target_subpane_image_reader_count_prompt">Max buffers</string>
+ <string name="target_subpane_image_reader_prev_button">&lt;&lt;&lt;</string>
+ <string name="target_subpane_image_reader_next_button">&gt;&gt;&gt;</string>
+ <string name="target_subpane_image_reader_save_button">Save</string>
<string name="request_pane_title">Request</string>
<string name="request_pane_capture_button">Capture</string>
diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/CameraControlPane.java b/apps/TestingCamera2/src/com/android/testingcamera2/CameraControlPane.java
index 19c9b11..58c782f 100644
--- a/apps/TestingCamera2/src/com/android/testingcamera2/CameraControlPane.java
+++ b/apps/TestingCamera2/src/com/android/testingcamera2/CameraControlPane.java
@@ -18,6 +18,7 @@ package com.android.testingcamera2;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -37,10 +38,13 @@ import android.widget.TextView;
import android.widget.ToggleButton;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCaptureSession.CaptureListener;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -68,6 +72,8 @@ public class CameraControlPane extends ControlPane {
// End XML attributes
+ private static final int MAX_CACHED_RESULTS = 100;
+
private static int mCameraPaneIdCounter = 0;
/**
@@ -146,6 +152,7 @@ public class CameraControlPane extends ControlPane {
private CameraCaptureSession mCurrentCaptureSession;
private SessionState mSessionState = SessionState.NONE;
private CameraCall mActiveCameraCall;
+ private LinkedList<TotalCaptureResult> mRecentResults = new LinkedList<>();
private List<Surface> mConfiguredSurfaces;
private List<TargetControlPane> mConfiguredTargetPanes;
@@ -243,15 +250,19 @@ public class CameraControlPane extends ControlPane {
}
public CaptureRequest.Builder getRequestBuilder(int template) {
+ CaptureRequest.Builder request = null;
if (mCurrentCamera != null) {
try {
- return mCurrentCamera.createCaptureRequest(template);
+ request = mCurrentCamera.createCaptureRequest(template);
+ // Workaround for b/15748139
+ request.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE,
+ CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON);
} catch (CameraAccessException e) {
TLog.e("Unable to build request for camera %s with template %d.", e,
mCurrentCameraId, template);
}
}
- return null;
+ return request;
}
/**
@@ -263,7 +274,7 @@ public class CameraControlPane extends ControlPane {
public boolean capture(CaptureRequest request) {
if (mCurrentCaptureSession != null) {
try {
- mCurrentCaptureSession.capture(request, null, null);
+ mCurrentCaptureSession.capture(request, mResultListener, null);
return true;
} catch (CameraAccessException e) {
TLog.e("Unable to capture for camera %s.", e, mCurrentCameraId);
@@ -275,7 +286,7 @@ public class CameraControlPane extends ControlPane {
public boolean repeat(CaptureRequest request) {
if (mCurrentCaptureSession != null) {
try {
- mCurrentCaptureSession.setRepeatingRequest(request, null, null);
+ mCurrentCaptureSession.setRepeatingRequest(request, mResultListener, null);
return true;
} catch (CameraAccessException e) {
TLog.e("Unable to set repeating request for camera %s.", e, mCurrentCameraId);
@@ -284,6 +295,27 @@ public class CameraControlPane extends ControlPane {
return false;
}
+ public TotalCaptureResult getResultAt(long timestamp) {
+ for (TotalCaptureResult result : mRecentResults) {
+ long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
+ if (resultTimestamp == timestamp) return result;
+ if (resultTimestamp > timestamp) return null;
+ }
+ return null;
+ }
+
+ private CaptureListener mResultListener = new CaptureListener() {
+ public void onCaptureCompleted(
+ CameraCaptureSession session,
+ CaptureRequest request,
+ TotalCaptureResult result) {
+ mRecentResults.add(result);
+ if (mRecentResults.size() > MAX_CACHED_RESULTS) {
+ mRecentResults.remove();
+ }
+ }
+ };
+
private void setUpUI(Context context) {
String paneName =
String.format(Locale.US, "%s %c",
@@ -676,5 +708,4 @@ public class CameraControlPane extends ControlPane {
switchToCamera(null);
}
};
-
}
diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/ImageReaderSubPane.java b/apps/TestingCamera2/src/com/android/testingcamera2/ImageReaderSubPane.java
new file mode 100644
index 0000000..d8b41c5
--- /dev/null
+++ b/apps/TestingCamera2/src/com/android/testingcamera2/ImageReaderSubPane.java
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 2014 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.testingcamera2;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.WritableByteChannel;
+import java.text.SimpleDateFormat;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.DngCreator;
+import android.hardware.camera2.TotalCaptureResult;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.Size;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+public class ImageReaderSubPane extends TargetSubPane {
+
+ private static final int NO_FORMAT = -1;
+ private static final int NO_SIZE = -1;
+ private static final int NO_IMAGE = -1;
+ private static final int MAX_BUFFER_COUNT = 25;
+ private static final int DEFAULT_BUFFER_COUNT = 3;
+
+ enum OutputFormat {
+ JPEG(ImageFormat.JPEG),
+ RAW16(ImageFormat.RAW_SENSOR),
+ RAW10(ImageFormat.RAW10),
+ YUV_420_888(ImageFormat.YUV_420_888);
+
+ public final int imageFormat;
+
+ OutputFormat(int imageFormat) {
+ this.imageFormat = imageFormat;
+ }
+ };
+
+ private Surface mSurface;
+
+ private final Spinner mFormatSpinner;
+ private final List<OutputFormat> mFormats = new ArrayList<>();
+
+ private final Spinner mSizeSpinner;
+ private Size[] mSizes;
+ private final Spinner mCountSpinner;
+ private Integer[] mCounts;
+
+ private final ImageView mImageView;
+
+ private int mCurrentCameraOrientation = 0;
+ private int mCurrentUiOrientation = 0;
+
+ private int mCurrentFormatId = NO_FORMAT;
+ private int mCurrentSizeId = NO_SIZE;
+ private CameraControlPane mCurrentCamera;
+
+ private OutputFormat mConfiguredFormat = null;
+ private Size mConfiguredSize = null;
+ private int mConfiguredCount = 0;
+
+ private ImageReader mReader = null;
+ private final LinkedList<Image> mCurrentImages = new LinkedList<>();
+ private int mCurrentImageIdx = NO_IMAGE;
+
+ private int mRawShiftFactor = 0;
+ private int mRawShiftRow = 0;
+ private int mRawShiftCol = 0;
+
+ public ImageReaderSubPane(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ inflater.inflate(R.layout.imagereader_target_subpane, this);
+ this.setOrientation(VERTICAL);
+
+ mFormatSpinner =
+ (Spinner) this.findViewById(R.id.target_subpane_image_reader_format_spinner);
+ mFormatSpinner.setOnItemSelectedListener(mFormatSpinnerListener);
+
+ mSizeSpinner = (Spinner) this.findViewById(R.id.target_subpane_image_reader_size_spinner);
+ mSizeSpinner.setOnItemSelectedListener(mSizeSpinnerListener);
+
+ mCountSpinner =
+ (Spinner) this.findViewById(R.id.target_subpane_image_reader_count_spinner);
+ mCounts = new Integer[MAX_BUFFER_COUNT];
+ for (int i = 0; i < mCounts.length; i++) {
+ mCounts[i] = i + 1;
+ }
+ mCountSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
+ mCounts));
+ mCountSpinner.setSelection(DEFAULT_BUFFER_COUNT - 1);
+
+ mImageView = (ImageView) this.findViewById(R.id.target_subpane_image_reader_view);
+
+ Button b = (Button) this.findViewById(R.id.target_subpane_image_reader_prev_button);
+ b.setOnClickListener(mPrevButtonListener);
+
+ b = (Button) this.findViewById(R.id.target_subpane_image_reader_next_button);
+ b.setOnClickListener(mNextButtonListener);
+
+ b = (Button) this.findViewById(R.id.target_subpane_image_reader_save_button);
+ b.setOnClickListener(mSaveButtonListener);
+ }
+
+ @Override
+ public void setTargetCameraPane(CameraControlPane target) {
+ mCurrentCamera = target;
+ if (target != null) {
+ updateFormats();
+ } else {
+ mSizeSpinner.setAdapter(null);
+ mCurrentSizeId = NO_SIZE;
+ }
+ }
+
+ @Override
+ public void setUiOrientation(int orientation) {
+ mCurrentUiOrientation = orientation;
+ }
+
+ private void updateFormats() {
+ if (mCurrentCamera == null) {
+ mFormatSpinner.setAdapter(null);
+ mCurrentFormatId = NO_FORMAT;
+ updateSizes();
+ return;
+ }
+
+ OutputFormat oldFormat = null;
+ if (mCurrentFormatId != NO_FORMAT) {
+ oldFormat = mFormats.get(mCurrentFormatId);
+ }
+
+ CameraCharacteristics info = mCurrentCamera.getCharacteristics();
+ StreamConfigurationMap streamConfigMap =
+ info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+ mFormats.clear();
+ for (OutputFormat format : OutputFormat.values()) {
+ if (streamConfigMap.isOutputSupportedFor(format.imageFormat)) {
+ mFormats.add(format);
+ }
+ }
+
+ int newSelectionId = 0;
+ for (int i = 0; i < mFormats.size(); i++) {
+ if (mFormats.get(i).equals(oldFormat)) {
+ newSelectionId = i;
+ break;
+ }
+ }
+
+ String[] outputFormatItems = new String[mFormats.size()];
+ for (int i = 0; i < outputFormatItems.length; i++) {
+ outputFormatItems[i] = mFormats.get(i).toString();
+ }
+
+ mFormatSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
+ outputFormatItems));
+ mFormatSpinner.setSelection(newSelectionId);
+ mCurrentFormatId = newSelectionId;
+
+ // Map sensor orientation to Surface.ROTATE_* constants
+ final int SENSOR_ORIENTATION_TO_SURFACE_ROTATE = 90;
+ mCurrentCameraOrientation = info.get(CameraCharacteristics.SENSOR_ORIENTATION) /
+ SENSOR_ORIENTATION_TO_SURFACE_ROTATE;
+
+ // Get the max white level for raw data if any
+ Integer maxLevel = info.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL);
+ if (maxLevel != null) {
+ int l = maxLevel;
+ // Find number of bits to shift to map from 0..WHITE_LEVEL to 0..255
+ for (mRawShiftFactor = 0; l > 255; mRawShiftFactor++) l >>= 1;
+ } else {
+ mRawShiftFactor = 0;
+ }
+
+ Integer cfa = info.get(CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT);
+ if (cfa != null) {
+ switch (cfa) {
+ case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGGB:
+ mRawShiftRow = 0;
+ mRawShiftCol = 0;
+ break;
+ case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GRBG:
+ mRawShiftRow = 0;
+ mRawShiftCol = 1;
+ break;
+ case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GBRG:
+ mRawShiftRow = 1;
+ mRawShiftCol = 0;
+ break;
+ case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR:
+ mRawShiftRow = 1;
+ mRawShiftCol = 1;
+ break;
+ case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGB:
+ mRawShiftRow = 0;
+ mRawShiftCol = 0;
+
+ break;
+ }
+ }
+ updateSizes();
+ }
+
+ private void updateSizes() {
+
+ if (mCurrentCamera == null) {
+ mSizeSpinner.setAdapter(null);
+ mCurrentSizeId = NO_SIZE;
+ return;
+ }
+
+ Size oldSize = null;
+ if (mCurrentSizeId != NO_SIZE) {
+ oldSize = mSizes[mCurrentSizeId];
+ }
+
+ CameraCharacteristics info = mCurrentCamera.getCharacteristics();
+ StreamConfigurationMap streamConfigMap =
+ info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+ mSizes = streamConfigMap.getOutputSizes(mFormats.get(mCurrentFormatId).imageFormat);
+
+ int newSelectionId = 0;
+ for (int i = 0; i < mSizes.length; i++) {
+ if (mSizes[i].equals(oldSize)) {
+ newSelectionId = i;
+ break;
+ }
+ }
+ String[] outputSizeItems = new String[mSizes.length];
+ for (int i = 0; i < outputSizeItems.length; i++) {
+ outputSizeItems[i] = mSizes[i].toString();
+ }
+
+ mSizeSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
+ outputSizeItems));
+ mSizeSpinner.setSelection(newSelectionId);
+ mCurrentSizeId = newSelectionId;
+ }
+
+ private void updateImage() {
+ if (mCurrentImageIdx == NO_IMAGE) return;
+ Image img = mCurrentImages.get(mCurrentImageIdx);
+
+ // Find rough scale factor to fit image into imageview to minimize processing overhead
+ // Want to be one factor too large
+ int SCALE_FACTOR = 2;
+ while (mConfiguredSize.getWidth() > (mImageView.getWidth() * SCALE_FACTOR << 1) ) {
+ SCALE_FACTOR <<= 1;
+ }
+
+ Bitmap imgBitmap = null;
+ switch (img.getFormat()) {
+ case ImageFormat.JPEG: {
+ ByteBuffer jpegBuffer = img.getPlanes()[0].getBuffer();
+ jpegBuffer.rewind();
+ byte[] jpegData = new byte[jpegBuffer.limit()];
+ jpegBuffer.get(jpegData);
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inSampleSize = SCALE_FACTOR;
+ imgBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, opts);
+ break;
+ }
+ case ImageFormat.YUV_420_888: {
+ ByteBuffer yBuffer = img.getPlanes()[0].getBuffer();
+ yBuffer.rewind();
+ int w = mConfiguredSize.getWidth() / SCALE_FACTOR;
+ int h = mConfiguredSize.getHeight() / SCALE_FACTOR;
+ byte[] row = new byte[mConfiguredSize.getWidth()];
+ int[] imgArray = new int[w * h];
+ for (int y = 0, j = 0; y < h; y++) {
+ yBuffer.position(y * SCALE_FACTOR * mConfiguredSize.getWidth());
+ yBuffer.get(row);
+ for (int x = 0, i = 0; x < w; x++) {
+ int yval = row[i] & 0xFF;
+ imgArray[j] = Color.rgb(yval, yval, yval);
+ i += SCALE_FACTOR;
+ j++;
+ }
+ }
+ imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
+ break;
+ }
+ case ImageFormat.RAW_SENSOR: {
+ ShortBuffer rawBuffer = img.getPlanes()[0].getBuffer().asShortBuffer();
+ rawBuffer.rewind();
+ // Very rough nearest-neighbor downsample for display
+ int w = mConfiguredSize.getWidth() / SCALE_FACTOR;
+ int h = mConfiguredSize.getHeight() / SCALE_FACTOR;
+ short[] redRow = new short[mConfiguredSize.getWidth()];
+ short[] blueRow = new short[mConfiguredSize.getWidth()];
+ int[] imgArray = new int[w * h];
+ for (int y = 0, j = 0; y < h; y++) {
+ // Align to start of red row in the pair to sample from
+ rawBuffer.position(
+ (y * SCALE_FACTOR + mRawShiftRow) * mConfiguredSize.getWidth());
+ rawBuffer.get(redRow);
+ // Align to start of blue row in the pair to sample from
+ rawBuffer.position(
+ (y * SCALE_FACTOR + 1 - mRawShiftRow) * mConfiguredSize.getWidth());
+ rawBuffer.get(blueRow);
+ for (int x = 0, i = 0; x < w; x++, i += SCALE_FACTOR, j++) {
+ int r = redRow[i + mRawShiftCol] >> mRawShiftFactor;
+ int g = redRow[i + 1 - mRawShiftCol] >> mRawShiftFactor;
+ int b = blueRow[i + 1 - mRawShiftCol] >> mRawShiftFactor;
+ imgArray[j] = Color.rgb(r,g,b);
+ }
+ }
+ imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
+ break;
+ }
+ case ImageFormat.RAW10: {
+ TLog.e("RAW10 viewing not implemented");
+ break;
+ }
+ }
+ if (imgBitmap != null) {
+ mImageView.setImageBitmap(imgBitmap);
+ }
+ }
+
+ @Override
+ public Surface getOutputSurface() {
+ if (mCurrentSizeId == NO_SIZE ||
+ mCurrentFormatId == NO_FORMAT) {
+ return null;
+ }
+ Size s = mSizes[mCurrentSizeId];
+ OutputFormat f = mFormats.get(mCurrentFormatId);
+ int c = (Integer) mCountSpinner.getSelectedItem();
+ if (mReader == null ||
+ !Objects.equals(mConfiguredSize, s) ||
+ !Objects.equals(mConfiguredFormat, f) ||
+ mConfiguredCount != c) {
+
+ if (mReader != null) {
+ mReader.close();
+ mCurrentImages.clear();
+ mCurrentImageIdx = NO_IMAGE;
+ }
+ mReader = ImageReader.newInstance(s.getWidth(), s.getHeight(), f.imageFormat, c);
+ mReader.setOnImageAvailableListener(mImageListener, null);
+ mConfiguredSize = s;
+ mConfiguredFormat = f;
+ mConfiguredCount = c;
+ }
+ return mReader.getSurface();
+ }
+
+ private final OnItemSelectedListener mFormatSpinnerListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ mCurrentFormatId = pos;
+ updateSizes();
+ };
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ mCurrentFormatId = NO_FORMAT;
+ };
+ };
+
+ private final OnItemSelectedListener mSizeSpinnerListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ mCurrentSizeId = pos;
+ };
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ mCurrentSizeId = NO_SIZE;
+ };
+ };
+
+ private final OnClickListener mPrevButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCurrentImageIdx != NO_IMAGE) {
+ int prevIdx = mCurrentImageIdx;
+ mCurrentImageIdx = (mCurrentImageIdx == 0) ?
+ (mCurrentImages.size() - 1) : (mCurrentImageIdx - 1);
+ if (prevIdx != mCurrentImageIdx) {
+ updateImage();
+ }
+ }
+ }
+ };
+
+ private final OnClickListener mNextButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCurrentImageIdx != NO_IMAGE) {
+ int prevIdx = mCurrentImageIdx;
+ mCurrentImageIdx = (mCurrentImageIdx == mCurrentImages.size() - 1) ?
+ 0 : (mCurrentImageIdx + 1);
+ if (prevIdx != mCurrentImageIdx) {
+ updateImage();
+ }
+ }
+ }
+ };
+
+ private final OnClickListener mSaveButtonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: Make async and coordinate with onImageAvailable
+ if (mCurrentImageIdx != NO_IMAGE) {
+ Image img = mCurrentImages.get(mCurrentImageIdx);
+ try {
+ String name = saveImage(img);
+ TLog.i("Saved image as %s", name);
+ } catch (IOException e) {
+ TLog.e("Can't save file:", e);
+ }
+ }
+ }
+ };
+
+ private final ImageReader.OnImageAvailableListener mImageListener =
+ new ImageReader.OnImageAvailableListener() {
+ @Override
+ public void onImageAvailable(ImageReader reader) {
+ while (mCurrentImages.size() >= reader.getMaxImages()) {
+ Image oldest = mCurrentImages.remove();
+ oldest.close();
+ mCurrentImageIdx = Math.min(mCurrentImageIdx - 1, 0);
+ }
+ mCurrentImages.add(reader.acquireNextImage());
+ if (mCurrentImageIdx == NO_IMAGE) {
+ mCurrentImageIdx = 0;
+ }
+ updateImage();
+ }
+ };
+
+ private String saveImage(Image img) throws IOException {
+ long timestamp = img.getTimestamp();
+ File output = getOutputImageFile(img.getFormat(), timestamp);
+ try (FileOutputStream out = new FileOutputStream(output)) {
+ switch(img.getFormat()) {
+ case ImageFormat.JPEG: {
+ writeJpegImage(img, out);
+ break;
+ }
+ case ImageFormat.YUV_420_888: {
+ writeYuvImage(img, out);
+ break;
+ }
+ case ImageFormat.RAW_SENSOR: {
+ writeDngImage(img, out);
+ break;
+ }
+ case ImageFormat.RAW10: {
+ TLog.e("RAW10 saving not implemented");
+ break;
+ }
+ }
+ }
+ return output.getName();
+ }
+
+ private void writeDngImage(Image img, OutputStream out) throws IOException {
+ if (img.getFormat() != ImageFormat.RAW_SENSOR) {
+ throw new IOException(
+ String.format("Unexpected Image format: %d, expected ImageFormat.RAW_SENSOR",
+ img.getFormat()));
+ }
+ long timestamp = img.getTimestamp();
+ if (mCurrentCamera == null) {
+ TLog.e("No camera availble for camera info, not saving DNG (timestamp %d)",
+ timestamp);
+ throw new IOException("No camera info available");
+ }
+ TotalCaptureResult result = mCurrentCamera.getResultAt(timestamp);
+ if (result == null) {
+ TLog.e("No result matching raw image found, not saving DNG (timestamp %d)",
+ timestamp);
+ throw new IOException("No matching result found");
+ }
+ CameraCharacteristics info = mCurrentCamera.getCharacteristics();
+ try (DngCreator writer = new DngCreator(info, result)) {
+ writer.writeImage(out, img);
+ }
+ }
+
+ private void writeJpegImage(Image img, OutputStream out) throws IOException {
+ if (img.getFormat() != ImageFormat.JPEG) {
+ throw new IOException(
+ String.format("Unexpected Image format: %d, expected ImageFormat.JPEG",
+ img.getFormat()));
+ }
+ WritableByteChannel outChannel = Channels.newChannel(out);
+ ByteBuffer jpegData = img.getPlanes()[0].getBuffer();
+ jpegData.rewind();
+ outChannel.write(jpegData);
+ }
+
+ private void writeYuvImage(Image img, OutputStream out)
+ throws IOException {
+ if (img.getFormat() != ImageFormat.YUV_420_888) {
+ throw new IOException(
+ String.format("Unexpected Image format: %d, expected ImageFormat.YUV_420_888",
+ img.getFormat()));
+ }
+ WritableByteChannel outChannel = Channels.newChannel(out);
+ for (int plane = 0; plane < 3; plane++) {
+ Image.Plane colorPlane = img.getPlanes()[plane];
+ ByteBuffer colorData = colorPlane.getBuffer();
+ int subsampleFactor = (plane == 0) ? 1 : 2;
+ int colorW = img.getWidth() / subsampleFactor;
+ int colorH = img.getHeight() / subsampleFactor;
+ colorData.rewind();
+ colorData.limit(colorData.capacity());
+ if (colorPlane.getPixelStride() == 1) {
+ // Can write contiguous rows
+ for (int y = 0, rowStart = 0; y < colorH;
+ y++, rowStart += colorPlane.getRowStride()) {
+ colorData.limit(rowStart + colorW);
+ colorData.position(rowStart);
+ outChannel.write(colorData);
+ }
+ } else {
+ // Need to pack rows
+ byte[] row = new byte[colorW * colorPlane.getPixelStride()];
+ byte[] packedRow = new byte[colorW];
+ ByteBuffer packedRowBuffer = ByteBuffer.wrap(packedRow);
+ for (int y = 0, rowStart = 0; y < colorH;
+ y++, rowStart += colorPlane.getRowStride()) {
+ colorData.position(rowStart);
+ colorData.get(row);
+ for (int x = 0, i = 0; x < colorW;
+ x++, i += colorPlane.getPixelStride()) {
+ packedRow[x] = row[i];
+ }
+ packedRowBuffer.rewind();
+ outChannel.write(packedRowBuffer);
+ }
+ }
+ }
+ }
+
+ File getOutputImageFile(int type, long timestamp){
+ // To be safe, you should check that the SDCard is mounted
+ // using Environment.getExternalStorageState() before doing this.
+
+ String state = Environment.getExternalStorageState();
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return null;
+ }
+
+ File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DCIM), "TestingCamera2");
+ // This location works best if you want the created images to be shared
+ // between applications and persist after your app has been uninstalled.
+
+ // Create the storage directory if it does not exist
+ if (!mediaStorageDir.exists()){
+ if (!mediaStorageDir.mkdirs()){
+ TLog.e("Failed to create directory for pictures/video");
+ return null;
+ }
+ }
+
+ // Create a media file name
+
+ // Find out time now in the Date and boottime time bases.
+ long nowMs = new Date().getTime();
+ long nowBootTimeNs = SystemClock.elapsedRealtimeNanos();
+
+ // Convert timestamp from boottime time base to the Date timebase
+ // Slightly approximate, but close enough
+ final long NS_PER_MS = 1000000l;
+ long timestampMs = (nowMs * NS_PER_MS - nowBootTimeNs + timestamp) / NS_PER_MS;
+
+ String timeStamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS").
+ format(new Date(timestampMs));
+ File mediaFile = null;
+ switch(type) {
+ case ImageFormat.JPEG:
+ mediaFile = new File(mediaStorageDir.getPath() + File.separator +
+ "IMG_"+ timeStamp + ".jpg");
+ break;
+ case ImageFormat.YUV_420_888:
+ mediaFile = new File(mediaStorageDir.getPath() + File.separator +
+ "IMG_"+ timeStamp + ".yuv");
+ break;
+ case ImageFormat.RAW_SENSOR:
+ mediaFile = new File(mediaStorageDir.getPath() + File.separator +
+ "IMG_"+ timeStamp + ".dng");
+ break;
+ case ImageFormat.RAW10:
+ mediaFile = new File(mediaStorageDir.getPath() + File.separator +
+ "IMG_"+ timeStamp + ".raw10");
+ break;
+ }
+
+ return mediaFile;
+ }
+
+}
diff --git a/apps/TestingCamera2/src/com/android/testingcamera2/TargetControlPane.java b/apps/TestingCamera2/src/com/android/testingcamera2/TargetControlPane.java
index 03bcf17..a8fe4f8 100644
--- a/apps/TestingCamera2/src/com/android/testingcamera2/TargetControlPane.java
+++ b/apps/TestingCamera2/src/com/android/testingcamera2/TargetControlPane.java
@@ -277,6 +277,8 @@ public class TargetControlPane extends ControlPane {
TargetSubPane newPane = null;
switch (type) {
case IMAGE_READER:
+ newPane = new ImageReaderSubPane(getContext(), null);
+ break;
case MEDIA_CODEC:
case MEDIA_RECORDER:
case RENDERSCRIPT: