Home | History | Annotate | Download | only in testingcamera2
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.testingcamera2;
     18 
     19 import java.util.ArrayList;
     20 import java.util.Date;
     21 import java.util.LinkedList;
     22 import java.util.List;
     23 import java.util.Objects;
     24 import java.io.File;
     25 import java.io.FileOutputStream;
     26 import java.io.IOException;
     27 import java.io.OutputStream;
     28 import java.io.OutputStreamWriter;
     29 import java.nio.ByteBuffer;
     30 import java.nio.ShortBuffer;
     31 import java.nio.FloatBuffer;
     32 import java.nio.channels.Channels;
     33 import java.nio.channels.WritableByteChannel;
     34 import java.text.SimpleDateFormat;
     35 
     36 import android.content.Context;
     37 import android.graphics.ImageFormat;
     38 import android.graphics.Bitmap;
     39 import android.graphics.BitmapFactory;
     40 import android.graphics.Color;
     41 import android.graphics.ColorMatrixColorFilter;
     42 import android.hardware.camera2.CameraCharacteristics;
     43 import android.hardware.camera2.DngCreator;
     44 import android.hardware.camera2.TotalCaptureResult;
     45 import android.hardware.camera2.params.StreamConfigurationMap;
     46 import android.media.Image;
     47 import android.media.ImageReader;
     48 import android.os.Environment;
     49 import android.os.SystemClock;
     50 import android.util.Size;
     51 import android.util.AttributeSet;
     52 import android.view.LayoutInflater;
     53 import android.view.Surface;
     54 import android.view.View;
     55 import android.widget.AdapterView;
     56 import android.widget.ArrayAdapter;
     57 import android.widget.Button;
     58 import android.widget.ImageView;
     59 import android.widget.LinearLayout;
     60 import android.widget.Spinner;
     61 import android.widget.AdapterView.OnItemSelectedListener;
     62 
     63 public class ImageReaderSubPane extends TargetSubPane {
     64 
     65     private static final int NO_FORMAT = -1;
     66     private static final int NO_SIZE = -1;
     67     private static final int NO_IMAGE = -1;
     68     private static final int MAX_BUFFER_COUNT = 25;
     69     private static final int DEFAULT_BUFFER_COUNT = 3;
     70 
     71     enum OutputFormat {
     72         JPEG(ImageFormat.JPEG),
     73         RAW16(ImageFormat.RAW_SENSOR),
     74         RAW10(ImageFormat.RAW10),
     75         YUV_420_888(ImageFormat.YUV_420_888),
     76         DEPTH16(ImageFormat.DEPTH16),
     77         DEPTH_POINT_CLOUD(ImageFormat.DEPTH_POINT_CLOUD);
     78 
     79         public final int imageFormat;
     80 
     81         OutputFormat(int imageFormat) {
     82             this.imageFormat = imageFormat;
     83         }
     84     };
     85 
     86     private Surface mSurface;
     87 
     88     private final Spinner mFormatSpinner;
     89     private final List<OutputFormat> mFormats = new ArrayList<>();
     90 
     91     private final Spinner mSizeSpinner;
     92     private Size[] mSizes;
     93     private final Spinner mCountSpinner;
     94     private Integer[] mCounts;
     95 
     96     private final ImageView mImageView;
     97 
     98     private int mCurrentCameraOrientation = 0;
     99     private int mCurrentUiOrientation = 0;
    100 
    101     private int mCurrentFormatId = NO_FORMAT;
    102     private int mCurrentSizeId = NO_SIZE;
    103     private CameraControlPane mCurrentCamera;
    104 
    105     private OutputFormat mConfiguredFormat = null;
    106     private Size mConfiguredSize = null;
    107     private int mConfiguredCount = 0;
    108 
    109     private ImageReader mReader = null;
    110     private final LinkedList<Image> mCurrentImages = new LinkedList<>();
    111     private int mCurrentImageIdx = NO_IMAGE;
    112 
    113     private int mRawShiftFactor = 0;
    114     private int mRawShiftRow = 0;
    115     private int mRawShiftCol = 0;
    116 
    117     // 5x4 color matrix for YUV->RGB conversion
    118     private static final ColorMatrixColorFilter sJFIF_YUVToRGB_Filter =
    119             new ColorMatrixColorFilter(new float[] {
    120                         1f,        0f,    1.402f, 0f, -179.456f,
    121                         1f, -0.34414f, -0.71414f, 0f,   135.46f,
    122                         1f,    1.772f,        0f, 0f, -226.816f,
    123                         0f,        0f,        0f, 1f,        0f
    124                     });
    125 
    126     public ImageReaderSubPane(Context context, AttributeSet attrs) {
    127         super(context, attrs);
    128 
    129         LayoutInflater inflater =
    130                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    131 
    132         inflater.inflate(R.layout.imagereader_target_subpane, this);
    133         this.setOrientation(VERTICAL);
    134 
    135         mFormatSpinner =
    136                 (Spinner) this.findViewById(R.id.target_subpane_image_reader_format_spinner);
    137         mFormatSpinner.setOnItemSelectedListener(mFormatSpinnerListener);
    138 
    139         mSizeSpinner = (Spinner) this.findViewById(R.id.target_subpane_image_reader_size_spinner);
    140         mSizeSpinner.setOnItemSelectedListener(mSizeSpinnerListener);
    141 
    142         mCountSpinner =
    143                 (Spinner) this.findViewById(R.id.target_subpane_image_reader_count_spinner);
    144         mCounts = new Integer[MAX_BUFFER_COUNT];
    145         for (int i = 0; i < mCounts.length; i++) {
    146             mCounts[i] = i + 1;
    147         }
    148         mCountSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
    149                         mCounts));
    150         mCountSpinner.setSelection(DEFAULT_BUFFER_COUNT - 1);
    151 
    152         mImageView = (ImageView) this.findViewById(R.id.target_subpane_image_reader_view);
    153 
    154         Button b = (Button) this.findViewById(R.id.target_subpane_image_reader_prev_button);
    155         b.setOnClickListener(mPrevButtonListener);
    156 
    157         b = (Button) this.findViewById(R.id.target_subpane_image_reader_next_button);
    158         b.setOnClickListener(mNextButtonListener);
    159 
    160         b = (Button) this.findViewById(R.id.target_subpane_image_reader_save_button);
    161         b.setOnClickListener(mSaveButtonListener);
    162     }
    163 
    164     @Override
    165     public void setTargetCameraPane(CameraControlPane target) {
    166         mCurrentCamera = target;
    167         if (target != null) {
    168             updateFormats();
    169         } else {
    170             mSizeSpinner.setAdapter(null);
    171             mCurrentSizeId = NO_SIZE;
    172         }
    173     }
    174 
    175     @Override
    176     public void setUiOrientation(int orientation) {
    177         mCurrentUiOrientation = orientation;
    178     }
    179 
    180     private void updateFormats() {
    181         if (mCurrentCamera == null) {
    182             mFormatSpinner.setAdapter(null);
    183             mCurrentFormatId = NO_FORMAT;
    184             updateSizes();
    185             return;
    186         }
    187 
    188         OutputFormat oldFormat = null;
    189         if (mCurrentFormatId != NO_FORMAT) {
    190             oldFormat = mFormats.get(mCurrentFormatId);
    191         }
    192 
    193         CameraCharacteristics info = mCurrentCamera.getCharacteristics();
    194         StreamConfigurationMap streamConfigMap =
    195                 info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    196 
    197         mFormats.clear();
    198         for (OutputFormat format : OutputFormat.values()) {
    199             try {
    200                 if (streamConfigMap.isOutputSupportedFor(format.imageFormat)) {
    201                     mFormats.add(format);
    202                     TLog.i("Format " + format + " supported");
    203                 } else {
    204                     TLog.i("Format " + format + " not supported");
    205                 }
    206             } catch(IllegalArgumentException e) {
    207                 TLog.i("Format " + format + " unknown to framework");
    208             }
    209         }
    210 
    211         int newSelectionId = 0;
    212         for (int i = 0; i < mFormats.size(); i++) {
    213             if (mFormats.get(i).equals(oldFormat)) {
    214                 newSelectionId = i;
    215                 break;
    216             }
    217         }
    218 
    219         String[] outputFormatItems = new String[mFormats.size()];
    220         for (int i = 0; i < outputFormatItems.length; i++) {
    221             outputFormatItems[i] = mFormats.get(i).toString();
    222         }
    223 
    224         mFormatSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
    225                         outputFormatItems));
    226         mFormatSpinner.setSelection(newSelectionId);
    227         mCurrentFormatId = newSelectionId;
    228 
    229         // Map sensor orientation to Surface.ROTATE_* constants
    230         final int SENSOR_ORIENTATION_TO_SURFACE_ROTATE = 90;
    231         mCurrentCameraOrientation = info.get(CameraCharacteristics.SENSOR_ORIENTATION) /
    232                 SENSOR_ORIENTATION_TO_SURFACE_ROTATE;
    233 
    234         // Get the max white level for raw data if any
    235         Integer maxLevel = info.get(CameraCharacteristics.SENSOR_INFO_WHITE_LEVEL);
    236         if (maxLevel != null) {
    237             int l = maxLevel;
    238             // Find number of bits to shift to map from 0..WHITE_LEVEL to 0..255
    239             for (mRawShiftFactor = 0; l > 255; mRawShiftFactor++) l >>= 1;
    240         } else {
    241             mRawShiftFactor = 0;
    242         }
    243 
    244         Integer cfa = info.get(CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT);
    245         if (cfa != null) {
    246             switch (cfa) {
    247                 case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGGB:
    248                     mRawShiftRow = 0;
    249                     mRawShiftCol = 0;
    250                     break;
    251                 case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GRBG:
    252                     mRawShiftRow = 0;
    253                     mRawShiftCol = 1;
    254                     break;
    255                 case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_GBRG:
    256                     mRawShiftRow = 1;
    257                     mRawShiftCol = 0;
    258                     break;
    259                 case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_BGGR:
    260                     mRawShiftRow = 1;
    261                     mRawShiftCol = 1;
    262                     break;
    263                 case CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT_RGB:
    264                     mRawShiftRow = 0;
    265                     mRawShiftCol = 0;
    266 
    267                     break;
    268             }
    269         }
    270         updateSizes();
    271     }
    272 
    273     private void updateSizes() {
    274 
    275         if (mCurrentCamera == null) {
    276             mSizeSpinner.setAdapter(null);
    277             mCurrentSizeId = NO_SIZE;
    278             return;
    279         }
    280 
    281         Size oldSize = null;
    282         if (mCurrentSizeId != NO_SIZE) {
    283             oldSize = mSizes[mCurrentSizeId];
    284         }
    285 
    286         CameraCharacteristics info = mCurrentCamera.getCharacteristics();
    287         StreamConfigurationMap streamConfigMap =
    288                 info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    289 
    290         mSizes = streamConfigMap.getOutputSizes(mFormats.get(mCurrentFormatId).imageFormat);
    291 
    292         int newSelectionId = 0;
    293         for (int i = 0; i < mSizes.length; i++) {
    294             if (mSizes[i].equals(oldSize)) {
    295                 newSelectionId = i;
    296                 break;
    297             }
    298         }
    299         String[] outputSizeItems = new String[mSizes.length];
    300         for (int i = 0; i < outputSizeItems.length; i++) {
    301             outputSizeItems[i] = mSizes[i].toString();
    302         }
    303 
    304         mSizeSpinner.setAdapter(new ArrayAdapter<>(getContext(), R.layout.spinner_item,
    305                         outputSizeItems));
    306         mSizeSpinner.setSelection(newSelectionId);
    307         mCurrentSizeId = newSelectionId;
    308     }
    309 
    310     private void updateImage() {
    311         if (mCurrentImageIdx == NO_IMAGE) return;
    312         Image img = mCurrentImages.get(mCurrentImageIdx);
    313 
    314         // Find rough scale factor to fit image into imageview to minimize processing overhead
    315         // Want to be one factor too large
    316         int SCALE_FACTOR = 2;
    317         while (mConfiguredSize.getWidth() > (mImageView.getWidth() * SCALE_FACTOR << 1) ) {
    318             SCALE_FACTOR <<= 1;
    319         }
    320 
    321         Bitmap imgBitmap = null;
    322         switch (img.getFormat()) {
    323             case ImageFormat.JPEG: {
    324                 ByteBuffer jpegBuffer = img.getPlanes()[0].getBuffer();
    325                 jpegBuffer.rewind();
    326                 byte[] jpegData = new byte[jpegBuffer.limit()];
    327                 jpegBuffer.get(jpegData);
    328                 BitmapFactory.Options opts = new BitmapFactory.Options();
    329                 opts.inSampleSize = SCALE_FACTOR;
    330                 imgBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length, opts);
    331                 break;
    332             }
    333             case ImageFormat.YUV_420_888: {
    334                 ByteBuffer yBuffer = img.getPlanes()[0].getBuffer();
    335                 ByteBuffer uBuffer = img.getPlanes()[1].getBuffer();
    336                 ByteBuffer vBuffer = img.getPlanes()[2].getBuffer();
    337                 yBuffer.rewind();
    338                 uBuffer.rewind();
    339                 vBuffer.rewind();
    340                 int w = mConfiguredSize.getWidth() / SCALE_FACTOR;
    341                 int h = mConfiguredSize.getHeight() / SCALE_FACTOR;
    342                 int stride = img.getPlanes()[0].getRowStride();
    343                 int uStride = img.getPlanes()[1].getRowStride();
    344                 int vStride = img.getPlanes()[2].getRowStride();
    345                 int uPStride = img.getPlanes()[1].getPixelStride();
    346                 int vPStride = img.getPlanes()[2].getPixelStride();
    347                 byte[] row = new byte[mConfiguredSize.getWidth()];
    348                 byte[] uRow = new byte[(mConfiguredSize.getWidth()/2-1)*uPStride + 1];
    349                 byte[] vRow = new byte[(mConfiguredSize.getWidth()/2-1)*vPStride + 1];
    350                 int[] imgArray = new int[w * h];
    351                 for (int y = 0, j = 0, rowStart = 0, uRowStart = 0, vRowStart = 0; y < h;
    352                      y++, rowStart += stride*SCALE_FACTOR) {
    353                     yBuffer.position(rowStart);
    354                     yBuffer.get(row);
    355                     if (y * SCALE_FACTOR % 2 == 0) {
    356                         uBuffer.position(uRowStart);
    357                         uBuffer.get(uRow);
    358                         vBuffer.position(vRowStart);
    359                         vBuffer.get(vRow);
    360                         uRowStart += uStride*SCALE_FACTOR/2;
    361                         vRowStart += vStride*SCALE_FACTOR/2;
    362                     }
    363                     for (int x = 0, i = 0; x < w; x++) {
    364                         int yval = row[i] & 0xFF;
    365                         int uval = uRow[i/2 * uPStride] & 0xFF;
    366                         int vval = vRow[i/2 * vPStride] & 0xFF;
    367                         // Write YUV directly; the ImageView color filter will convert to RGB for us.
    368                         imgArray[j] = Color.rgb(yval, uval, vval);
    369                         i += SCALE_FACTOR;
    370                         j++;
    371                     }
    372                 }
    373                 imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
    374                 break;
    375             }
    376             case ImageFormat.RAW_SENSOR: {
    377                 ShortBuffer rawBuffer = img.getPlanes()[0].getBuffer().asShortBuffer();
    378                 rawBuffer.rewind();
    379                 // Very rough nearest-neighbor downsample for display
    380                 int w = mConfiguredSize.getWidth() / SCALE_FACTOR;
    381                 int h = mConfiguredSize.getHeight() / SCALE_FACTOR;
    382                 short[] redRow = new short[mConfiguredSize.getWidth()];
    383                 short[] blueRow = new short[mConfiguredSize.getWidth()];
    384                 int[] imgArray = new int[w * h];
    385                 for (int y = 0, j = 0; y < h; y++) {
    386                     // Align to start of red row in the pair to sample from
    387                     rawBuffer.position(
    388                         (y * SCALE_FACTOR + mRawShiftRow) * mConfiguredSize.getWidth());
    389                     rawBuffer.get(redRow);
    390                     // Align to start of blue row in the pair to sample from
    391                     rawBuffer.position(
    392                         (y * SCALE_FACTOR + 1 - mRawShiftRow) * mConfiguredSize.getWidth());
    393                     rawBuffer.get(blueRow);
    394                     for (int x = 0, i = 0; x < w; x++, i += SCALE_FACTOR, j++) {
    395                         int r = redRow[i + mRawShiftCol] >> mRawShiftFactor;
    396                         int g = redRow[i + 1 - mRawShiftCol] >> mRawShiftFactor;
    397                         int b = blueRow[i + 1 - mRawShiftCol] >> mRawShiftFactor;
    398                         imgArray[j] = Color.rgb(r,g,b);
    399                     }
    400                 }
    401                 imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
    402                 break;
    403             }
    404             case ImageFormat.RAW10: {
    405                 TLog.e("RAW10 viewing not implemented");
    406                 break;
    407             }
    408             case ImageFormat.DEPTH16: {
    409                 ShortBuffer y16Buffer = img.getPlanes()[0].getBuffer().asShortBuffer();
    410                 y16Buffer.rewind();
    411                 // Very rough nearest-neighbor downsample for display
    412                 int w = img.getWidth();
    413                 int h = img.getHeight();
    414                 // rowStride is in bytes, accessing array as shorts
    415                 int stride = img.getPlanes()[0].getRowStride() / 2;
    416 
    417                 imgBitmap = convertDepthToFalseColor(y16Buffer, w, h, stride, SCALE_FACTOR);
    418 
    419                 break;
    420 
    421             }
    422         }
    423         if (imgBitmap != null) {
    424             mImageView.setImageBitmap(imgBitmap);
    425         }
    426     }
    427 
    428     /**
    429      * Convert depth16 buffer into a false-color RGBA Bitmap, scaling down
    430      * by factor of scale
    431      */
    432     private Bitmap convertDepthToFalseColor(ShortBuffer depthBuffer, int w, int h,
    433             int stride, int scale) {
    434         short[] yRow = new short[w];
    435         int[] imgArray = new int[w * h];
    436         w = w / scale;
    437         h = h / scale;
    438         stride = stride * scale;
    439         for (int y = 0, j = 0, rowStart = 0; y < h; y++, rowStart += stride) {
    440             // Align to start of nearest-neighbor row
    441             depthBuffer.position(rowStart);
    442             depthBuffer.get(yRow);
    443             for (int x = 0, i = 0; x < w; x++, i += scale, j++) {
    444                 short y16 = yRow[i];
    445                 int r = y16 & 0x00FF;
    446                 int g = (y16 >> 8) & 0x00FF;
    447                 imgArray[j] = Color.rgb(r, g, 0);
    448             }
    449         }
    450         return Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
    451     }
    452 
    453     @Override
    454     public Surface getOutputSurface() {
    455         if (mCurrentSizeId == NO_SIZE ||
    456                 mCurrentFormatId == NO_FORMAT) {
    457             return null;
    458         }
    459         Size s = mSizes[mCurrentSizeId];
    460         OutputFormat f = mFormats.get(mCurrentFormatId);
    461         int c = (Integer) mCountSpinner.getSelectedItem();
    462         if (mReader == null ||
    463                 !Objects.equals(mConfiguredSize, s) ||
    464                 !Objects.equals(mConfiguredFormat, f) ||
    465                 mConfiguredCount != c) {
    466 
    467             if (mReader != null) {
    468                 mReader.close();
    469                 mCurrentImages.clear();
    470                 mCurrentImageIdx = NO_IMAGE;
    471             }
    472             mReader = ImageReader.newInstance(s.getWidth(), s.getHeight(), f.imageFormat, c);
    473             mReader.setOnImageAvailableListener(mImageListener, null);
    474             mConfiguredSize = s;
    475             mConfiguredFormat = f;
    476             mConfiguredCount = c;
    477 
    478             // We use ImageView's color filter to do YUV->RGB conversion for us for YUV outputs
    479             if (mConfiguredFormat == OutputFormat.YUV_420_888) {
    480                 mImageView.setColorFilter(sJFIF_YUVToRGB_Filter);
    481             } else {
    482                 mImageView.setColorFilter(null);
    483             }
    484             // Clear output now that we're actually changing to a new target
    485             mImageView.setImageBitmap(null);
    486         }
    487         return mReader.getSurface();
    488     }
    489 
    490     private final OnItemSelectedListener mFormatSpinnerListener = new OnItemSelectedListener() {
    491         @Override
    492         public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
    493             mCurrentFormatId = pos;
    494             updateSizes();
    495         };
    496 
    497         @Override
    498         public void onNothingSelected(AdapterView<?> parent) {
    499             mCurrentFormatId = NO_FORMAT;
    500         };
    501     };
    502 
    503     private final OnItemSelectedListener mSizeSpinnerListener = new OnItemSelectedListener() {
    504         @Override
    505         public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
    506             mCurrentSizeId = pos;
    507         };
    508 
    509         @Override
    510         public void onNothingSelected(AdapterView<?> parent) {
    511             mCurrentSizeId = NO_SIZE;
    512         };
    513     };
    514 
    515     private final OnClickListener mPrevButtonListener = new OnClickListener() {
    516         @Override
    517         public void onClick(View v) {
    518             if (mCurrentImageIdx != NO_IMAGE) {
    519                 int prevIdx = mCurrentImageIdx;
    520                 mCurrentImageIdx = (mCurrentImageIdx == 0) ?
    521                         (mCurrentImages.size() - 1) : (mCurrentImageIdx - 1);
    522                 if (prevIdx != mCurrentImageIdx) {
    523                     updateImage();
    524                 }
    525             }
    526         }
    527     };
    528 
    529     private final OnClickListener mNextButtonListener = new OnClickListener() {
    530         @Override
    531         public void onClick(View v) {
    532             if (mCurrentImageIdx != NO_IMAGE) {
    533                 int prevIdx = mCurrentImageIdx;
    534                 mCurrentImageIdx = (mCurrentImageIdx == mCurrentImages.size() - 1) ?
    535                         0 : (mCurrentImageIdx + 1);
    536                 if (prevIdx != mCurrentImageIdx) {
    537                     updateImage();
    538                 }
    539             }
    540         }
    541     };
    542 
    543     private final OnClickListener mSaveButtonListener = new OnClickListener() {
    544         @Override
    545         public void onClick(View v) {
    546             // TODO: Make async and coordinate with onImageAvailable
    547             if (mCurrentImageIdx != NO_IMAGE) {
    548                 Image img = mCurrentImages.get(mCurrentImageIdx);
    549                 try {
    550                     String name = saveImage(img);
    551                     TLog.i("Saved image as %s", name);
    552                 } catch (IOException e) {
    553                     TLog.e("Can't save file:", e);
    554                 }
    555             }
    556         }
    557     };
    558 
    559     private final ImageReader.OnImageAvailableListener mImageListener =
    560             new ImageReader.OnImageAvailableListener() {
    561         @Override
    562         public void onImageAvailable(ImageReader reader) {
    563             while (mCurrentImages.size() >= reader.getMaxImages()) {
    564                 Image oldest = mCurrentImages.remove();
    565                 oldest.close();
    566                 mCurrentImageIdx = Math.min(mCurrentImageIdx - 1, 0);
    567             }
    568             mCurrentImages.add(reader.acquireNextImage());
    569             if (mCurrentImageIdx == NO_IMAGE) {
    570                 mCurrentImageIdx = 0;
    571             }
    572             updateImage();
    573         }
    574     };
    575 
    576     private String saveImage(Image img) throws IOException {
    577         long timestamp = img.getTimestamp();
    578         File output = getOutputImageFile(img.getFormat(), timestamp);
    579         try (FileOutputStream out = new FileOutputStream(output)) {
    580             switch(img.getFormat()) {
    581                 case ImageFormat.JPEG: {
    582                     writeJpegImage(img, out);
    583                     break;
    584                 }
    585                 case ImageFormat.YUV_420_888: {
    586                     writeYuvImage(img, out);
    587                     break;
    588                 }
    589                 case ImageFormat.RAW_SENSOR: {
    590                     writeDngImage(img, out);
    591                     break;
    592                 }
    593                 case ImageFormat.RAW10: {
    594                     TLog.e("RAW10 saving not implemented");
    595                     break;
    596                 }
    597                 case ImageFormat.DEPTH16: {
    598                     writeDepth16Image(img, out);
    599                     break;
    600                 }
    601                 case ImageFormat.DEPTH_POINT_CLOUD: {
    602                     writeDepthPointImage(img, out);
    603                     break;
    604                 }
    605             }
    606         }
    607         return output.getName();
    608     }
    609 
    610     private void writeDngImage(Image img, OutputStream out) throws IOException {
    611         if (img.getFormat() != ImageFormat.RAW_SENSOR) {
    612             throw new IOException(
    613                     String.format("Unexpected Image format: %d, expected ImageFormat.RAW_SENSOR",
    614                             img.getFormat()));
    615         }
    616         long timestamp = img.getTimestamp();
    617         if (mCurrentCamera == null) {
    618             TLog.e("No camera availble for camera info, not saving DNG (timestamp %d)",
    619                     timestamp);
    620             throw new IOException("No camera info available");
    621         }
    622         TotalCaptureResult result = mCurrentCamera.getResultAt(timestamp);
    623         if (result == null) {
    624             TLog.e("No result matching raw image found, not saving DNG (timestamp %d)",
    625                     timestamp);
    626             throw new IOException("No matching result found");
    627         }
    628         CameraCharacteristics info = mCurrentCamera.getCharacteristics();
    629         try (DngCreator writer = new DngCreator(info, result)) {
    630             writer.writeImage(out, img);
    631         }
    632     }
    633 
    634     private void writeJpegImage(Image img, OutputStream out) throws IOException {
    635         if (img.getFormat() != ImageFormat.JPEG) {
    636             throw new IOException(
    637                     String.format("Unexpected Image format: %d, expected ImageFormat.JPEG",
    638                             img.getFormat()));
    639         }
    640         WritableByteChannel outChannel = Channels.newChannel(out);
    641         ByteBuffer jpegData = img.getPlanes()[0].getBuffer();
    642         jpegData.rewind();
    643         outChannel.write(jpegData);
    644     }
    645 
    646     private void writeYuvImage(Image img, OutputStream out)
    647             throws IOException {
    648         if (img.getFormat() != ImageFormat.YUV_420_888) {
    649             throw new IOException(
    650                     String.format("Unexpected Image format: %d, expected ImageFormat.YUV_420_888",
    651                             img.getFormat()));
    652         }
    653         WritableByteChannel outChannel = Channels.newChannel(out);
    654         for (int plane = 0; plane < 3; plane++) {
    655             Image.Plane colorPlane = img.getPlanes()[plane];
    656             ByteBuffer colorData = colorPlane.getBuffer();
    657             int subsampleFactor = (plane == 0) ? 1 : 2;
    658             int colorW = img.getWidth() / subsampleFactor;
    659             int colorH = img.getHeight() / subsampleFactor;
    660             colorData.rewind();
    661             colorData.limit(colorData.capacity());
    662             if (colorPlane.getPixelStride() == 1) {
    663                 // Can write contiguous rows
    664                 for (int y = 0, rowStart = 0; y < colorH;
    665                         y++, rowStart += colorPlane.getRowStride()) {
    666                     colorData.limit(rowStart + colorW);
    667                     colorData.position(rowStart);
    668                     outChannel.write(colorData);
    669                 }
    670             } else {
    671                 // Need to pack rows
    672                 byte[] row = new byte[(colorW - 1) * colorPlane.getPixelStride() + 1];
    673                 byte[] packedRow = new byte[colorW];
    674                 ByteBuffer packedRowBuffer = ByteBuffer.wrap(packedRow);
    675                 for (int y = 0, rowStart = 0; y < colorH;
    676                         y++, rowStart += colorPlane.getRowStride()) {
    677                     colorData.position(rowStart);
    678                     colorData.get(row);
    679                     for (int x = 0, i = 0; x < colorW;
    680                             x++, i += colorPlane.getPixelStride()) {
    681                         packedRow[x] = row[i];
    682                     }
    683                     packedRowBuffer.rewind();
    684                     outChannel.write(packedRowBuffer);
    685                 }
    686             }
    687         }
    688     }
    689 
    690     /**
    691      * Save a 16-bpp depth image as a false-color PNG
    692      */
    693     private void writeDepth16Image(Image img, OutputStream out) throws IOException {
    694         if (img.getFormat() != ImageFormat.DEPTH16) {
    695             throw new IOException(
    696                     String.format("Unexpected Image format: %d, expected ImageFormat.DEPTH16",
    697                             img.getFormat()));
    698         }
    699         int w = img.getWidth();
    700         int h = img.getHeight();
    701         int rowStride = img.getPlanes()[0].getRowStride() / 2; // in shorts
    702         ShortBuffer y16Data = img.getPlanes()[0].getBuffer().asShortBuffer();
    703 
    704         Bitmap rgbImage = convertDepthToFalseColor(y16Data, w, h, rowStride, /*scale*/ 1);
    705         rgbImage.compress(Bitmap.CompressFormat.PNG, 100, out);
    706         rgbImage.recycle();
    707     }
    708 
    709     // This saves a text file of float values for a point cloud
    710     private void writeDepthPointImage(Image img, OutputStream out) throws IOException {
    711         if (img.getFormat() != ImageFormat.DEPTH_POINT_CLOUD) {
    712             throw new IOException(
    713                     String.format("Unexpected Image format: %d, expected ImageFormat.DEPTH16",
    714                             img.getFormat()));
    715         }
    716         FloatBuffer pointList = img.getPlanes()[0].getBuffer().asFloatBuffer();
    717         int pointCount = pointList.limit() / 3;
    718         OutputStreamWriter writer = new OutputStreamWriter(out);
    719         for (int i = 0; i < pointCount; i++) {
    720             String pt = String.format("%f, %f, %f\n",
    721                     pointList.get(), pointList.get(),pointList.get());
    722             writer.write(pt, 0, pt.length());
    723         }
    724     }
    725 
    726     File getOutputImageFile(int type, long timestamp){
    727         // To be safe, you should check that the SDCard is mounted
    728         // using Environment.getExternalStorageState() before doing this.
    729 
    730         String state = Environment.getExternalStorageState();
    731         if (!Environment.MEDIA_MOUNTED.equals(state)) {
    732             return null;
    733         }
    734 
    735         File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
    736                   Environment.DIRECTORY_DCIM), "TestingCamera2");
    737         // This location works best if you want the created images to be shared
    738         // between applications and persist after your app has been uninstalled.
    739 
    740         // Create the storage directory if it does not exist
    741         if (!mediaStorageDir.exists()){
    742             if (!mediaStorageDir.mkdirs()){
    743                 TLog.e("Failed to create directory for pictures/video");
    744                 return null;
    745             }
    746         }
    747 
    748         // Create a media file name
    749 
    750         // Find out time now in the Date and boottime time bases.
    751         long nowMs = new Date().getTime();
    752         long nowBootTimeNs = SystemClock.elapsedRealtimeNanos();
    753 
    754         // Convert timestamp from boottime time base to the Date timebase
    755         // Slightly approximate, but close enough
    756         final long NS_PER_MS = 1000000l;
    757         long timestampMs = (nowMs * NS_PER_MS - nowBootTimeNs + timestamp) / NS_PER_MS;
    758 
    759         String timeStamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS").
    760                 format(new Date(timestampMs));
    761         File mediaFile = null;
    762         switch(type) {
    763             case ImageFormat.JPEG:
    764                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    765                         "IMG_"+ timeStamp + ".jpg");
    766                 break;
    767             case ImageFormat.YUV_420_888:
    768                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    769                         "IMG_"+ timeStamp + ".yuv");
    770                 break;
    771             case ImageFormat.RAW_SENSOR:
    772                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    773                         "IMG_"+ timeStamp + ".dng");
    774                 break;
    775             case ImageFormat.RAW10:
    776                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    777                         "IMG_"+ timeStamp + ".raw10");
    778                 break;
    779             case ImageFormat.DEPTH16:
    780                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    781                         "IMG_"+ timeStamp + "_depth.png");
    782                 break;
    783             case ImageFormat.DEPTH_POINT_CLOUD:
    784                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    785                         "IMG_"+ timeStamp + "_depth_points.txt");
    786                 break;
    787             default:
    788                 mediaFile = new File(mediaStorageDir.getPath() + File.separator +
    789                         "IMG_"+ timeStamp + ".unknown");
    790                 TLog.e("Unknown image format for saving, using .unknown extension: " + type);
    791                 break;
    792         }
    793 
    794         return mediaFile;
    795     }
    796 
    797 }
    798