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 
OutputFormat(int imageFormat)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 
ImageReaderSubPane(Context context, AttributeSet attrs)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
setTargetCameraPane(CameraControlPane target)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
setUiOrientation(int orientation)176     public void setUiOrientation(int orientation) {
177         mCurrentUiOrientation = orientation;
178     }
179 
updateFormats()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 
updateSizes()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 
updateImage()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      */
convertDepthToFalseColor(ShortBuffer depthBuffer, int w, int h, int stride, int scale)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
getOutputSurface()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 
saveImage(Image img)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 
writeDngImage(Image img, OutputStream out)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 
writeJpegImage(Image img, OutputStream out)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 
writeYuvImage(Image img, OutputStream out)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      */
writeDepth16Image(Image img, OutputStream out)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
writeDepthPointImage(Image img, OutputStream out)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 
getOutputImageFile(int type, long timestamp)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