/*
 * Copyright 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.hardware.camera2;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.hardware.camera2.impl.CameraMetadataNative;
import android.location.Location;
import android.media.ExifInterface;
import android.media.Image;
import android.os.SystemClock;
import android.util.Log;
import android.util.Size;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

The DngCreator class provides functions to write raw pixel data as a DNG file.

This class is designed to be used with the ImageFormat.RAW_SENSOR buffers available from CameraDevice, or with Bayer-type raw pixel data that is otherwise generated by an application. The DNG metadata tags will be generated from a CaptureResult object or set directly.

The DNG file format is a cross-platform file format that is used to store pixel data from camera sensors with minimal pre-processing applied. DNG files allow for pixel data to be defined in a user-defined colorspace, and have associated metadata that allow for this pixel data to be converted to the standard CIE XYZ colorspace during post-processing.

For more information on the DNG file format and associated metadata, please refer to the Adobe DNG 1.4.0.0 specification.

/** * The {@link DngCreator} class provides functions to write raw pixel data as a DNG file. * * <p> * This class is designed to be used with the {@link android.graphics.ImageFormat#RAW_SENSOR} * buffers available from {@link android.hardware.camera2.CameraDevice}, or with Bayer-type raw * pixel data that is otherwise generated by an application. The DNG metadata tags will be * generated from a {@link android.hardware.camera2.CaptureResult} object or set directly. * </p> * * <p> * The DNG file format is a cross-platform file format that is used to store pixel data from * camera sensors with minimal pre-processing applied. DNG files allow for pixel data to be * defined in a user-defined colorspace, and have associated metadata that allow for this * pixel data to be converted to the standard CIE XYZ colorspace during post-processing. * </p> * * <p> * For more information on the DNG file format and associated metadata, please refer to the * <a href= * "https://wwwimages2.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf"> * Adobe DNG 1.4.0.0 specification</a>. * </p> */
public final class DngCreator implements AutoCloseable { private static final String TAG = "DngCreator";
Create a new DNG object.

It is not necessary to call any set methods to write a well-formatted DNG file.

DNG metadata tags will be generated from the corresponding parameters in the CaptureResult object.

For best quality DNG files, it is strongly recommended that lens shading map output is enabled if supported. See CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE.

Params:
  • characteristics – an object containing the static CameraCharacteristics.
  • metadata – a metadata object to generate tags from.
/** * Create a new DNG object. * * <p> * It is not necessary to call any set methods to write a well-formatted DNG file. * </p> * <p> * DNG metadata tags will be generated from the corresponding parameters in the * {@link android.hardware.camera2.CaptureResult} object. * </p> * <p> * For best quality DNG files, it is strongly recommended that lens shading map output is * enabled if supported. See {@link CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE}. * </p> * @param characteristics an object containing the static * {@link android.hardware.camera2.CameraCharacteristics}. * @param metadata a metadata object to generate tags from. */
public DngCreator(@NonNull CameraCharacteristics characteristics, @NonNull CaptureResult metadata) { if (characteristics == null || metadata == null) { throw new IllegalArgumentException("Null argument to DngCreator constructor"); } // Find current time in milliseconds since 1970 long currentTime = System.currentTimeMillis(); // Assume that sensor timestamp has that timebase to start long timeOffset = 0; int timestampSource = characteristics.get( CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE); if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME) { // This means the same timebase as SystemClock.elapsedRealtime(), // which is CLOCK_BOOTTIME timeOffset = currentTime - SystemClock.elapsedRealtime(); } else if (timestampSource == CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN) { // This means the same timebase as System.currentTimeMillis(), // which is CLOCK_MONOTONIC timeOffset = currentTime - SystemClock.uptimeMillis(); } else { // Unexpected time source - treat as CLOCK_MONOTONIC Log.w(TAG, "Sensor timestamp source is unexpected: " + timestampSource); timeOffset = currentTime - SystemClock.uptimeMillis(); } // Find capture time (nanos since boot) Long timestamp = metadata.get(CaptureResult.SENSOR_TIMESTAMP); long captureTime = currentTime; if (timestamp != null) { captureTime = timestamp / 1000000 + timeOffset; } // Create this fresh each time since the time zone may change while a long-running application // is active. final DateFormat dateTimeStampFormat = new SimpleDateFormat(TIFF_DATETIME_FORMAT, Locale.US); dateTimeStampFormat.setTimeZone(TimeZone.getDefault()); // Format for metadata String formattedCaptureTime = dateTimeStampFormat.format(captureTime); nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(), formattedCaptureTime); }
Set the orientation value to write.

This will be written as the TIFF "Orientation" tag (0x0112). Calling this will override any prior settings for this tag.

Params:
Returns:this DngCreator object.
/** * Set the orientation value to write. * * <p> * This will be written as the TIFF "Orientation" tag {@code (0x0112)}. * Calling this will override any prior settings for this tag. * </p> * * @param orientation the orientation value to set, one of: * <ul> * <li>{@link android.media.ExifInterface#ORIENTATION_NORMAL}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_HORIZONTAL}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_180}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_VERTICAL}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSPOSE}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_90}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSVERSE}</li> * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li> * </ul> * @return this {@link #DngCreator} object. */
@NonNull public DngCreator setOrientation(int orientation) { if (orientation < ExifInterface.ORIENTATION_UNDEFINED || orientation > ExifInterface.ORIENTATION_ROTATE_270) { throw new IllegalArgumentException("Orientation " + orientation + " is not a valid EXIF orientation value"); } // ExifInterface and TIFF/EP spec differ on definition of // "Unknown" orientation; other values map directly if (orientation == ExifInterface.ORIENTATION_UNDEFINED) { orientation = TAG_ORIENTATION_UNKNOWN; } nativeSetOrientation(orientation); return this; }
Set the thumbnail image.

Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel. The alpha channel will be discarded. Thumbnail images with a dimension larger than MAX_THUMBNAIL_DIMENSION will be rejected.

Params:
  • pixels – a Bitmap of pixel data.
Throws:
Returns:this DngCreator object.
/** * Set the thumbnail image. * * <p> * Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel. * The alpha channel will be discarded. Thumbnail images with a dimension larger than * {@link #MAX_THUMBNAIL_DIMENSION} will be rejected. * </p> * * @param pixels a {@link android.graphics.Bitmap} of pixel data. * @return this {@link #DngCreator} object. * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension * larger than {@link #MAX_THUMBNAIL_DIMENSION}. */
@NonNull public DngCreator setThumbnail(@NonNull Bitmap pixels) { if (pixels == null) { throw new IllegalArgumentException("Null argument to setThumbnail"); } int width = pixels.getWidth(); int height = pixels.getHeight(); if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) { throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width + "," + height + ") too large, dimensions must be smaller than " + MAX_THUMBNAIL_DIMENSION); } ByteBuffer rgbBuffer = convertToRGB(pixels); nativeSetThumbnail(rgbBuffer, width, height); return this; }
Set the thumbnail image.

Pixel data is interpreted as a ImageFormat.YUV_420_888 image. Thumbnail images with a dimension larger than MAX_THUMBNAIL_DIMENSION will be rejected.

Params:
Throws:
Returns:this DngCreator object.
/** * Set the thumbnail image. * * <p> * Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image. * Thumbnail images with a dimension larger than {@link #MAX_THUMBNAIL_DIMENSION} will be * rejected. * </p> * * @param pixels an {@link android.media.Image} object with the format * {@link android.graphics.ImageFormat#YUV_420_888}. * @return this {@link #DngCreator} object. * @throws java.lang.IllegalArgumentException if the given thumbnail image has a dimension * larger than {@link #MAX_THUMBNAIL_DIMENSION}. */
@NonNull public DngCreator setThumbnail(@NonNull Image pixels) { if (pixels == null) { throw new IllegalArgumentException("Null argument to setThumbnail"); } int format = pixels.getFormat(); if (format != ImageFormat.YUV_420_888) { throw new IllegalArgumentException("Unsupported Image format " + format); } int width = pixels.getWidth(); int height = pixels.getHeight(); if (width > MAX_THUMBNAIL_DIMENSION || height > MAX_THUMBNAIL_DIMENSION) { throw new IllegalArgumentException("Thumbnail dimensions width,height (" + width + "," + height + ") too large, dimensions must be smaller than " + MAX_THUMBNAIL_DIMENSION); } ByteBuffer rgbBuffer = convertToRGB(pixels); nativeSetThumbnail(rgbBuffer, width, height); return this; }
Set image location metadata.

The given location object must contain at least a valid time, latitude, and longitude (equivalent to the values returned by Location.getTime(), Location.getLatitude(), and Location.getLongitude() methods).

Params:
Throws:
Returns:this DngCreator object.
/** * Set image location metadata. * * <p> * The given location object must contain at least a valid time, latitude, and longitude * (equivalent to the values returned by {@link android.location.Location#getTime()}, * {@link android.location.Location#getLatitude()}, and * {@link android.location.Location#getLongitude()} methods). * </p> * * @param location an {@link android.location.Location} object to set. * @return this {@link #DngCreator} object. * * @throws java.lang.IllegalArgumentException if the given location object doesn't * contain enough information to set location metadata. */
@NonNull public DngCreator setLocation(@NonNull Location location) { if (location == null) { throw new IllegalArgumentException("Null location passed to setLocation"); } double latitude = location.getLatitude(); double longitude = location.getLongitude(); long time = location.getTime(); int[] latTag = toExifLatLong(latitude); int[] longTag = toExifLatLong(longitude); String latRef = latitude >= 0 ? GPS_LAT_REF_NORTH : GPS_LAT_REF_SOUTH; String longRef = longitude >= 0 ? GPS_LONG_REF_EAST : GPS_LONG_REF_WEST; String dateTag = sExifGPSDateStamp.format(time); mGPSTimeStampCalendar.setTimeInMillis(time); int[] timeTag = new int[] { mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1, mGPSTimeStampCalendar.get(Calendar.MINUTE), 1, mGPSTimeStampCalendar.get(Calendar.SECOND), 1 }; nativeSetGpsTags(latTag, latRef, longTag, longRef, dateTag, timeTag); return this; }
Set the user description string to write.

This is equivalent to setting the TIFF "ImageDescription" tag (0x010E).

Params:
  • description – the user description string.
Returns:this DngCreator object.
/** * Set the user description string to write. * * <p> * This is equivalent to setting the TIFF "ImageDescription" tag {@code (0x010E)}. * </p> * * @param description the user description string. * @return this {@link #DngCreator} object. */
@NonNull public DngCreator setDescription(@NonNull String description) { if (description == null) { throw new IllegalArgumentException("Null description passed to setDescription."); } nativeSetDescription(description); return this; }
Write the ImageFormat.RAW_SENSOR pixel data to a DNG file with the currently configured metadata.

Raw pixel data must have 16 bits per pixel, and the input must contain at least offset + 2 * width * height) bytes. The width and height of the input are taken from the width and height set in the DngCreator metadata tags, and will typically be equal to the width and height of CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE. Prior to API level 23, this was always the same as CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE. The pixel layout in the input is determined from the reported color filter arrangement (CFA) set in CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT. If insufficient metadata is available to write a well-formatted DNG file, an IllegalStateException will be thrown.

Params:
  • dngOutput – an OutputStream to write the DNG file to.
  • size – the Size of the image to write, in pixels.
  • pixels – an InputStream of pixel data to write.
  • offset – the offset of the raw image in bytes. This indicates how many bytes will be skipped in the input before any pixel data is read.
Throws:
/** * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with * the currently configured metadata. * * <p> * Raw pixel data must have 16 bits per pixel, and the input must contain at least * {@code offset + 2 * width * height)} bytes. The width and height of * the input are taken from the width and height set in the {@link DngCreator} metadata tags, * and will typically be equal to the width and height of * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to * API level 23, this was always the same as * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. * The pixel layout in the input is determined from the reported color filter arrangement (CFA) * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient * metadata is available to write a well-formatted DNG file, an * {@link java.lang.IllegalStateException} will be thrown. * </p> * * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. * @param size the {@link Size} of the image to write, in pixels. * @param pixels an {@link java.io.InputStream} of pixel data to write. * @param offset the offset of the raw image in bytes. This indicates how many bytes will * be skipped in the input before any pixel data is read. * * @throws IOException if an error was encountered in the input or output stream. * @throws java.lang.IllegalStateException if not enough metadata information has been * set to write a well-formatted DNG file. * @throws java.lang.IllegalArgumentException if the size passed in does not match the */
public void writeInputStream(@NonNull OutputStream dngOutput, @NonNull Size size, @NonNull InputStream pixels, @IntRange(from=0) long offset) throws IOException { if (dngOutput == null) { throw new IllegalArgumentException("Null dngOutput passed to writeInputStream"); } else if (size == null) { throw new IllegalArgumentException("Null size passed to writeInputStream"); } else if (pixels == null) { throw new IllegalArgumentException("Null pixels passed to writeInputStream"); } else if (offset < 0) { throw new IllegalArgumentException("Negative offset passed to writeInputStream"); } int width = size.getWidth(); int height = size.getHeight(); if (width <= 0 || height <= 0) { throw new IllegalArgumentException("Size with invalid width, height: (" + width + "," + height + ") passed to writeInputStream"); } nativeWriteInputStream(dngOutput, pixels, width, height, offset); }
Write the ImageFormat.RAW_SENSOR pixel data to a DNG file with the currently configured metadata.

Raw pixel data must have 16 bits per pixel, and the input must contain at least offset + 2 * width * height) bytes. The width and height of the input are taken from the width and height set in the DngCreator metadata tags, and will typically be equal to the width and height of CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE. Prior to API level 23, this was always the same as SENSOR_INFO_ACTIVE_ARRAY_SIZE.SENSOR_INFO_ACTIVE_ARRAY_SIZE. The pixel layout in the input is determined from the reported color filter arrangement (CFA) set in CameraCharacteristics.SENSOR_INFO_COLOR_FILTER_ARRANGEMENT. If insufficient metadata is available to write a well-formatted DNG file, an IllegalStateException will be thrown.

Any mark or limit set on this ByteBuffer is ignored, and will be cleared by this method.

Params:
  • dngOutput – an OutputStream to write the DNG file to.
  • size – the Size of the image to write, in pixels.
  • pixels – an ByteBuffer of pixel data to write.
  • offset – the offset of the raw image in bytes. This indicates how many bytes will be skipped in the input before any pixel data is read.
Throws:
  • IOException – if an error was encountered in the input or output stream.
  • IllegalStateException – if not enough metadata information has been set to write a well-formatted DNG file.
/** * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with * the currently configured metadata. * * <p> * Raw pixel data must have 16 bits per pixel, and the input must contain at least * {@code offset + 2 * width * height)} bytes. The width and height of * the input are taken from the width and height set in the {@link DngCreator} metadata tags, * and will typically be equal to the width and height of * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE}. Prior to * API level 23, this was always the same as * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. * The pixel layout in the input is determined from the reported color filter arrangement (CFA) * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient * metadata is available to write a well-formatted DNG file, an * {@link java.lang.IllegalStateException} will be thrown. * </p> * * <p> * Any mark or limit set on this {@link ByteBuffer} is ignored, and will be cleared by this * method. * </p> * * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. * @param size the {@link Size} of the image to write, in pixels. * @param pixels an {@link java.nio.ByteBuffer} of pixel data to write. * @param offset the offset of the raw image in bytes. This indicates how many bytes will * be skipped in the input before any pixel data is read. * * @throws IOException if an error was encountered in the input or output stream. * @throws java.lang.IllegalStateException if not enough metadata information has been * set to write a well-formatted DNG file. */
public void writeByteBuffer(@NonNull OutputStream dngOutput, @NonNull Size size, @NonNull ByteBuffer pixels, @IntRange(from=0) long offset) throws IOException { if (dngOutput == null) { throw new IllegalArgumentException("Null dngOutput passed to writeByteBuffer"); } else if (size == null) { throw new IllegalArgumentException("Null size passed to writeByteBuffer"); } else if (pixels == null) { throw new IllegalArgumentException("Null pixels passed to writeByteBuffer"); } else if (offset < 0) { throw new IllegalArgumentException("Negative offset passed to writeByteBuffer"); } int width = size.getWidth(); int height = size.getHeight(); writeByteBuffer(width, height, pixels, dngOutput, DEFAULT_PIXEL_STRIDE, width * DEFAULT_PIXEL_STRIDE, offset); }
Write the pixel data to a DNG file with the currently configured metadata.

For this method to succeed, the Image input must contain ImageFormat.RAW_SENSOR pixel data, otherwise an IllegalArgumentException will be thrown.

Params:
Throws:
/** * Write the pixel data to a DNG file with the currently configured metadata. * * <p> * For this method to succeed, the {@link android.media.Image} input must contain * {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data, otherwise an * {@link java.lang.IllegalArgumentException} will be thrown. * </p> * * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. * @param pixels an {@link android.media.Image} to write. * * @throws java.io.IOException if an error was encountered in the output stream. * @throws java.lang.IllegalArgumentException if an image with an unsupported format was used. * @throws java.lang.IllegalStateException if not enough metadata information has been * set to write a well-formatted DNG file. */
public void writeImage(@NonNull OutputStream dngOutput, @NonNull Image pixels) throws IOException { if (dngOutput == null) { throw new IllegalArgumentException("Null dngOutput to writeImage"); } else if (pixels == null) { throw new IllegalArgumentException("Null pixels to writeImage"); } int format = pixels.getFormat(); if (format != ImageFormat.RAW_SENSOR) { throw new IllegalArgumentException("Unsupported image format " + format); } Image.Plane[] planes = pixels.getPlanes(); if (planes == null || planes.length <= 0) { throw new IllegalArgumentException("Image with no planes passed to writeImage"); } ByteBuffer buf = planes[0].getBuffer(); writeByteBuffer(pixels.getWidth(), pixels.getHeight(), buf, dngOutput, planes[0].getPixelStride(), planes[0].getRowStride(), 0); } @Override public void close() { nativeDestroy(); }
Max width or height dimension for thumbnails.
/** * Max width or height dimension for thumbnails. */
public static final int MAX_THUMBNAIL_DIMENSION = 256; // max pixel dimension for TIFF/EP @Override protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } private static final String GPS_LAT_REF_NORTH = "N"; private static final String GPS_LAT_REF_SOUTH = "S"; private static final String GPS_LONG_REF_EAST = "E"; private static final String GPS_LONG_REF_WEST = "W"; private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd HH:mm:ss"; private static final DateFormat sExifGPSDateStamp = new SimpleDateFormat(GPS_DATE_FORMAT_STR, Locale.US); private final Calendar mGPSTimeStampCalendar = Calendar .getInstance(TimeZone.getTimeZone("UTC")); static { sExifGPSDateStamp.setTimeZone(TimeZone.getTimeZone("UTC")); } private static final int DEFAULT_PIXEL_STRIDE = 2; // bytes per sample private static final int BYTES_PER_RGB_PIX = 3; // byts per pixel // TIFF tag values needed to map between public API and TIFF spec private static final int TAG_ORIENTATION_UNKNOWN = 9;
Offset, rowStride, and pixelStride are given in bytes. Height and width are given in pixels.
/** * Offset, rowStride, and pixelStride are given in bytes. Height and width are given in pixels. */
private void writeByteBuffer(int width, int height, ByteBuffer pixels, OutputStream dngOutput, int pixelStride, int rowStride, long offset) throws IOException { if (width <= 0 || height <= 0) { throw new IllegalArgumentException("Image with invalid width, height: (" + width + "," + height + ") passed to write"); } long capacity = pixels.capacity(); long totalSize = ((long) rowStride) * height + offset; if (capacity < totalSize) { throw new IllegalArgumentException("Image size " + capacity + " is too small (must be larger than " + totalSize + ")"); } int minRowStride = pixelStride * width; if (minRowStride > rowStride) { throw new IllegalArgumentException("Invalid image pixel stride, row byte width " + minRowStride + " is too large, expecting " + rowStride); } pixels.clear(); // Reset mark and limit nativeWriteImage(dngOutput, width, height, pixels, rowStride, pixelStride, offset, pixels.isDirect()); pixels.clear(); }
Convert a single YUV pixel to RGB.
/** * Convert a single YUV pixel to RGB. */
private static void yuvToRgb(byte[] yuvData, int outOffset, /*out*/byte[] rgbOut) { final int COLOR_MAX = 255; float y = yuvData[0] & 0xFF; // Y channel float cb = yuvData[1] & 0xFF; // U channel float cr = yuvData[2] & 0xFF; // V channel // convert YUV -> RGB (from JFIF's "Conversion to and from RGB" section) float r = y + 1.402f * (cr - 128); float g = y - 0.34414f * (cb - 128) - 0.71414f * (cr - 128); float b = y + 1.772f * (cb - 128); // clamp to [0,255] rgbOut[outOffset] = (byte) Math.max(0, Math.min(COLOR_MAX, r)); rgbOut[outOffset + 1] = (byte) Math.max(0, Math.min(COLOR_MAX, g)); rgbOut[outOffset + 2] = (byte) Math.max(0, Math.min(COLOR_MAX, b)); }
Convert a single Color pixel to RGB.
/** * Convert a single {@link Color} pixel to RGB. */
private static void colorToRgb(int color, int outOffset, /*out*/byte[] rgbOut) { rgbOut[outOffset] = (byte) Color.red(color); rgbOut[outOffset + 1] = (byte) Color.green(color); rgbOut[outOffset + 2] = (byte) Color.blue(color); // Discards Alpha }
Generate a direct RGB ByteBuffer from a YUV420_888 Image.
/** * Generate a direct RGB {@link ByteBuffer} from a YUV420_888 {@link Image}. */
private static ByteBuffer convertToRGB(Image yuvImage) { // TODO: Optimize this with renderscript intrinsic. int width = yuvImage.getWidth(); int height = yuvImage.getHeight(); ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); Image.Plane yPlane = yuvImage.getPlanes()[0]; Image.Plane uPlane = yuvImage.getPlanes()[1]; Image.Plane vPlane = yuvImage.getPlanes()[2]; ByteBuffer yBuf = yPlane.getBuffer(); ByteBuffer uBuf = uPlane.getBuffer(); ByteBuffer vBuf = vPlane.getBuffer(); yBuf.rewind(); uBuf.rewind(); vBuf.rewind(); int yRowStride = yPlane.getRowStride(); int vRowStride = vPlane.getRowStride(); int uRowStride = uPlane.getRowStride(); int yPixStride = yPlane.getPixelStride(); int vPixStride = vPlane.getPixelStride(); int uPixStride = uPlane.getPixelStride(); byte[] yuvPixel = { 0, 0, 0 }; byte[] yFullRow = new byte[yPixStride * (width - 1) + 1]; byte[] uFullRow = new byte[uPixStride * (width / 2 - 1) + 1]; byte[] vFullRow = new byte[vPixStride * (width / 2 - 1) + 1]; byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; for (int i = 0; i < height; i++) { int halfH = i / 2; yBuf.position(yRowStride * i); yBuf.get(yFullRow); uBuf.position(uRowStride * halfH); uBuf.get(uFullRow); vBuf.position(vRowStride * halfH); vBuf.get(vFullRow); for (int j = 0; j < width; j++) { int halfW = j / 2; yuvPixel[0] = yFullRow[yPixStride * j]; yuvPixel[1] = uFullRow[uPixStride * halfW]; yuvPixel[2] = vFullRow[vPixStride * halfW]; yuvToRgb(yuvPixel, j * BYTES_PER_RGB_PIX, /*out*/finalRow); } buf.put(finalRow); } yBuf.rewind(); uBuf.rewind(); vBuf.rewind(); buf.rewind(); return buf; }
Generate a direct RGB ByteBuffer from a Bitmap.
/** * Generate a direct RGB {@link ByteBuffer} from a {@link Bitmap}. */
private static ByteBuffer convertToRGB(Bitmap argbBitmap) { // TODO: Optimize this. int width = argbBitmap.getWidth(); int height = argbBitmap.getHeight(); ByteBuffer buf = ByteBuffer.allocateDirect(BYTES_PER_RGB_PIX * width * height); int[] pixelRow = new int[width]; byte[] finalRow = new byte[BYTES_PER_RGB_PIX * width]; for (int i = 0; i < height; i++) { argbBitmap.getPixels(pixelRow, /*offset*/0, /*stride*/width, /*x*/0, /*y*/i, /*width*/width, /*height*/1); for (int j = 0; j < width; j++) { colorToRgb(pixelRow[j], j * BYTES_PER_RGB_PIX, /*out*/finalRow); } buf.put(finalRow); } buf.rewind(); return buf; }
Convert coordinate to EXIF GPS tag format.
/** * Convert coordinate to EXIF GPS tag format. */
private static int[] toExifLatLong(double value) { // convert to the format dd/1 mm/1 ssss/100 value = Math.abs(value); int degrees = (int) value; value = (value - degrees) * 60; int minutes = (int) value; value = (value - minutes) * 6000; int seconds = (int) value; return new int[] { degrees, 1, minutes, 1, seconds, 100 }; }
This field is used by native code, do not access or modify.
/** * This field is used by native code, do not access or modify. */
private long mNativeContext; private static native void nativeClassInit(); private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics, CameraMetadataNative nativeResult, String captureTime); private synchronized native void nativeDestroy(); private synchronized native void nativeSetOrientation(int orientation); private synchronized native void nativeSetDescription(String description); private synchronized native void nativeSetGpsTags(int[] latTag, String latRef, int[] longTag, String longRef, String dateTag, int[] timeTag); private synchronized native void nativeSetThumbnail(ByteBuffer buffer, int width, int height); private synchronized native void nativeWriteImage(OutputStream out, int width, int height, ByteBuffer rawBuffer, int rowStride, int pixStride, long offset, boolean isDirect) throws IOException; private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream, int width, int height, long offset) throws IOException; static { nativeClassInit(); } }