| /* |
| * 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.io.OutputStreamWriter; |
| import java.nio.ByteBuffer; |
| import java.nio.ShortBuffer; |
| import java.nio.FloatBuffer; |
| 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), |
| DEPTH16(ImageFormat.DEPTH16), |
| DEPTH_POINT_CLOUD(ImageFormat.DEPTH_POINT_CLOUD); |
| |
| 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); |
| TLog.i("Format " + format + " supported"); |
| } else { |
| TLog.i("Format " + format + " not supported"); |
| } |
| } |
| |
| 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; |
| } |
| case ImageFormat.DEPTH16: { |
| ShortBuffer y16Buffer = img.getPlanes()[0].getBuffer().asShortBuffer(); |
| y16Buffer.rewind(); |
| // Very rough nearest-neighbor downsample for display |
| int w = img.getWidth() / SCALE_FACTOR; |
| int h = img.getHeight() / SCALE_FACTOR; |
| int stride = img.getPlanes()[0].getRowStride(); |
| short[] yRow = new short[img.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 |
| y16Buffer.position( |
| y * SCALE_FACTOR * stride); |
| y16Buffer.get(yRow); |
| for (int x = 0, i = 0; x < w; x++, i += SCALE_FACTOR, j++) { |
| int d = (yRow[i] >> 8) & 0xFF; |
| imgArray[j] = Color.rgb(d,d,d); |
| } |
| } |
| imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888); |
| 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; |
| } |
| case ImageFormat.DEPTH16: { |
| writeDepth16Image(img, out); |
| break; |
| } |
| case ImageFormat.DEPTH_POINT_CLOUD: { |
| writeDepthPointImage(img, out); |
| 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); |
| } |
| } |
| } |
| } |
| |
| // This saves a 16-bpp depth image as a PNG, with the low bits in the |
| // red channel and the high bits in the blue |
| private void writeDepth16Image(Image img, OutputStream out) throws IOException { |
| if (img.getFormat() != ImageFormat.DEPTH16) { |
| throw new IOException( |
| String.format("Unexpected Image format: %d, expected ImageFormat.DEPTH16", |
| img.getFormat())); |
| } |
| int w = img.getWidth(); |
| int h = img.getHeight(); |
| int rowStride = img.getPlanes()[0].getRowStride() / 2; // in shorts |
| int[] rgbData = new int[w * h]; |
| short[] yRow = new short[w]; |
| ShortBuffer y16Data = img.getPlanes()[0].getBuffer().asShortBuffer(); |
| int rgbIndex = 0; |
| |
| for (int y = 0; y < h; y++) { |
| y16Data.position(y * rowStride); |
| y16Data.get(yRow, 0, w); |
| for (int x = 0; x < w; x++) { |
| short y16 = yRow[x]; |
| rgbData[rgbIndex++] = |
| Color.rgb(y16 & 0x00FF, (y16 >> 8) & 0x00FF, 0); |
| } |
| } |
| |
| Bitmap rgbImage = Bitmap.createBitmap(rgbData, w, h, Bitmap.Config.ARGB_8888); |
| rgbImage.compress(Bitmap.CompressFormat.PNG, 100, out); |
| } |
| |
| // This saves a text file of float values for a point cloud |
| private void writeDepthPointImage(Image img, OutputStream out) throws IOException { |
| if (img.getFormat() != ImageFormat.DEPTH_POINT_CLOUD) { |
| throw new IOException( |
| String.format("Unexpected Image format: %d, expected ImageFormat.DEPTH16", |
| img.getFormat())); |
| } |
| FloatBuffer pointList = img.getPlanes()[0].getBuffer().asFloatBuffer(); |
| int pointCount = pointList.limit() / 3; |
| OutputStreamWriter writer = new OutputStreamWriter(out); |
| for (int i = 0; i < pointCount; i++) { |
| String pt = String.format("%f, %f, %f\n", |
| pointList.get(), pointList.get(),pointList.get()); |
| writer.write(pt, 0, pt.length()); |
| } |
| } |
| |
| 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; |
| case ImageFormat.DEPTH16: |
| mediaFile = new File(mediaStorageDir.getPath() + File.separator + |
| "IMG_"+ timeStamp + "_depth.png"); |
| case ImageFormat.DEPTH_POINT_CLOUD: |
| mediaFile = new File(mediaStorageDir.getPath() + File.separator + |
| "IMG_"+ timeStamp + "_depth_points.txt"); |
| |
| } |
| |
| return mediaFile; |
| } |
| |
| } |