Decode NV21-Encoded Image Data

From CodeCodex

Java on Android[edit]

NV21 is the default format for live previews returned from the camera device on Android. This is a more compact format than a simple RGB or ARGB encoding, taking advantage of the fact that the human eye is less sensitive to detail in colour variation than in luminance variation. The image consists of an array of one-byte monochrome luminance values, followed by a smaller block of corresponding two-byte chrominance (colour) values, downsampled so a single chrominance sample is shared among a 2×2 block of luminance samples.

The following two routines aid in decoding of such image formats into an array of ARGB colour values that can be rendered into an Android Bitmap. NV21DataSize computes the number of bytes necessary to hold an NV21-encoded image of the specified dimensions, while DecodeNV21 implements the actual decoding of such an image, incorporating a rotation by a specified integer multiple of 90° at the same time, which is useful to compensate for variations in device orientation (portrait/landscape).

   public static int NV21DataSize
     (
       int Width,
       int Height
     )
     /* returns the size of a data buffer to hold an NV21-encoded image
       of the specified dimensions. */
     {
       return
               Width * Height
           +
               ((Width + 1) / 2) * ((Height + 1) / 2) * 2;
     } /*NV21DataSize*/
   
   public static void DecodeNV21
     (
       int SrcWidth, /* dimensions of image before rotation */
       int SrcHeight,
       byte[] Data, /* length = NV21DataSize(SrcWidth, SrcHeight) */
       int Rotate, /* [0 .. 3], angle is 90° * Rotate clockwise */
       int Alpha, /* set as alpha for all decoded pixels */
       int[] Pixels /* length = Width * Height */
     )
     /* decodes NV21-encoded image data, which is the default camera preview image format. */
     {
       final int AlphaMask = Alpha << 24;
     /* Rotation involves accessing either the source or destination pixels in a
       non-sequential fashion. Since the source is smaller, I figure it's less
       cache-unfriendly to go jumping around that. */
       final int DstWidth = (Rotate & 1) != 0 ? SrcHeight : SrcWidth;
       final int DstHeight = (Rotate & 1) != 0 ? SrcWidth : SrcHeight;
       final boolean DecrementRow = Rotate > 1;
       final boolean DecrementCol = Rotate == 1 || Rotate == 2;
       final int LumaRowStride = (Rotate & 1) != 0 ? 1 : SrcWidth;
       final int LumaColStride = (Rotate & 1) != 0 ? SrcWidth : 1;
       final int ChromaRowStride = (Rotate & 1) != 0 ? 2 : SrcWidth;
       final int ChromaColStride = (Rotate & 1) != 0 ? SrcWidth : 2;
       int dst = 0;
       for (int row = DecrementRow ? DstHeight : 0;;)
         {
           if (row == (DecrementRow ? 0 : DstHeight))
               break;
           if (DecrementRow)
             {
               --row;
             } /*if*/
           for (int col = DecrementCol ? DstWidth : 0;;)
             {
               if (col == (DecrementCol ? 0 : DstWidth))
                   break;
               if (DecrementCol)
                 {
                   --col;
                 } /*if*/
               final int Y = 0xff & (int)Data[row * LumaRowStride + col * LumaColStride]; /* [0 .. 255] */
             /* U/V data follows entire luminance block, downsampled to half luminance
               resolution both horizontally and vertically */
             /* decoding follows algorithm shown at
               <http://www.mail-archive.com/android-developers@googlegroups.com/msg14558.html>,
               except it gets red and blue the wrong way round (decoding NV12 rather than NV21) */
             /* see also good overview of YUV-family formats at <http://wiki.videolan.org/YUV> */
               final int Cr =
                   (0xff & (int)Data[SrcHeight * SrcWidth + row / 2 * ChromaRowStride + col / 2 * ChromaColStride]) - 128;
                     /* [-128 .. +127] */
               final int Cb =
                   (0xff & (int)Data[SrcHeight * SrcWidth + row / 2 * ChromaRowStride + col / 2 * ChromaColStride + 1]) - 128;
                     /* [-128 .. +127] */
               Pixels[dst++] =
                       AlphaMask
                   |
                           Math.max
                             (
                               Math.min
                                 (
                                   (int)(
                                           Y
                                       +
                                           Cr
                                       +
                                           (Cr >> 1)
                                       +
                                           (Cr >> 2)
                                       +
                                           (Cr >> 6)
                                   ),
                                   255
                                 ),
                                 0
                             )
                       <<
                           16 /* red */
                   |
                           Math.max
                             (
                               Math.min
                                 (
                                   (int)(
                                           Y
                                       -
                                           (Cr >> 2)
                                       +
                                           (Cr >> 4)
                                       +
                                           (Cr >> 5)
                                       -
                                           (Cb >> 1)
                                       +
                                           (Cb >> 3)
                                       +
                                           (Cb >> 4)
                                       +
                                           (Cb >> 5)
                                   ),
                                   255
                                 ),
                               0
                             )
                       <<
                           8 /* green */
                   |
                       Math.max
                         (
                           Math.min
                             (
                               (int)(
                                       Y
                                   +
                                       Cb
                                   +
                                       (Cb >> 2)
                                   +
                                       (Cb >> 3)
                                   +
                                       (Cb >> 5)
                               ),
                               255
                             ),
                           0
                         ); /* blue */
               if (!DecrementCol)
                 {
                   ++col;
                 } /*if*/
             } /*for*/
           if (!DecrementRow)
             {
               ++row;
             } /*if*/
         } /*for*/
     } /*DecodeNV21*/