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