Skip to main content

Helping PICTools Decode 48-bpp JPEG 2000 from DICOM

developer sitting at computer
The “Using PICTools to Decode DICOM Pixel Data” article shows how to extract and decode the generic images existing in the DICOM files using PICTools. This article will expand on this idea to show how to handle complex 48-bpp planar JPEG 2000 images.

Decoding such images with the PICTools OP_J2KE opcode requires understanding how the decompressed data will be stored in memory and how to convert from the extended 48-bit colorspace to normal 24-bit, so the image can be displayed or stored as common Windows bitmap. Planar image format means color channels will be represented as a separate memory buffers for Red, Green, and Blue data. Every element of the buffer will contain a 16-bit value representing the corresponding color.

developer sitting at computer

To visualize the image with a display device on a Microsoft Windows operating system, the planar red, green, and blue 16-bit color samples must be reduced to 8-bit values and interleaved in blue, green, red (BGR) order.

developer sitting at computer

Taking this into account, we need a function that reduces the size of each color value from 16-bits to 8-bits, and interleaves color values into the BGR order expected in Windows bitmaps. You can refer to the Microsoft Windows Bitmap API for understanding the bitmap structures.

/** brief
* Interleave 3, 16-bpc planar components into Blue, Green, Red order, discarding
* the upper 8-bits of each component sample, to either display or store as a
* Windows bitmap file.
* param red16 Buffer containing 16-bit Red component samples.
* param green16 Buffer containing 16-bit Green component samples.
* param blue16 Buffer containing 16-bit Blue component samples.
* param image_width The number of color samples in each row.
* param image_height The number of rows in each component.
* param bgr24 Buffer sized to hold all 24-bpp interleaved BGR values, with each
*   image row padded to a 32-bit (DWORD) boundary.
void convertPlanar48bppToInterleavedRGB(const uint16_t red16 [],
                                        const uint16_t green16 [],
                                        const uint16_t blue16 [],
                                        size_t image_width,
                                        size_t image_height,
                                        uint8_t bgr24 [])
    const size_t stride = (image_width * 24 + 31) / 32 * 4;
    for (size_t j = 0; j < image_height; ++j)
        for (size_t i = 0; i < image_width; ++i)
            bgr24[j * stride + i * 3 + 2] = static_cast(  red16[j * image_width + i]);
            bgr24[j * stride + i * 3 + 1] = static_cast(green16[j * image_width + i]);
            bgr24[j * stride + i * 3 + 0] = static_cast( blue16[j * image_width + i]);

Now we can set up PICTools to decompress JPEG 2000 stream and convert 16-bit red, green, and blue channels into 8-bit interleaved representation suitable for the Windows bitmap.

/** brief
* Decode 3, 16-bpc (48-bpp) J2K to 24-bpp BGR, discarding the upper
* 8-bits of each 16-bpc color sample decoded.
* param j2k_data Buffer containing 3, 16-bpc (48-bpp) J2K encoded image.
* param j2k_size Size of the J2K input stream.
* param bmi BITMAPINFOHEADER structure for decoded 24-bpp BGR image.
* param pixel_data Output buffer containing 24-bpp BGR color values.
* return size of the output buffer in bytes.
size_t decodePlanarJ2K48bpp(uint8_t j2k_data [],
                            size_t j2k_size,
                            BITMAPINFOHEADER & bmi,
                            std::unique_ptr<uint8_t[]> & pixel_data)
    // PICTools setup.
    PIC_PARM param = {};
    param.ParmSize = sizeof(param);
    param.ParmVer = CURRENT_PARMVER;
    param.Get.QFlags = Q_EOF;

    // PICTools Get queue setup.
    param.Get.Start = param.Get.Front = j2k_data;
    param.Get.End = param.Get.Rear = j2k_data + j2k_size;

    // Opcode setup. In this case we assume the Get queue contains
    // the entire J2K 48-bit planar image.
    // Please refer to the PICTools programmers reference to find more details.
    param.Op = OP_J2KE;
    param.ParmVerMinor = 3;
    param.Flags = F_Raw;

    // Retrieve image partition count and dimensions.
    RESPONSE res = Pegasus(&param, REQ_INIT);

    // Partition count is stored in param.u.J2K.NumPartitions.
    res = Pegasus(&param, REQ_TERM);

    // Allocate memory to hold decoded partitions.
    size_t plane_size = param.u.J2K.Region.Stride * param.u.J2K.Region.Height;
    std::vector plane_data(plane_size * param.u.J2K.NumPartitions);

    // Setup partition pointers.
    uint8_t * red_ptr = &plane_data[0];
    uint8_t * green_ptr = red_ptr + plane_size;
    uint8_t * blue_ptr = green_ptr + plane_size;

    // Allocate partition descriptors.
    param.u.J2K.NumOtherPartitions = param.u.J2K.NumPartitions - 1;
    std::vector other_partitions(param.u.J2K.NumOtherPartitions);
    param.u.J2K.OtherPartitions = &other_partitions[0];

    // Setup first partition. We want to decode Red plane and store the
    // decoded data in the param.Put buffer.
    param.Put.Start = red_ptr;
    param.Put.Front = red_ptr;
    param.Put.Rear = red_ptr;
    param.Put.End = red_ptr + plane_size;

    // Setup second partition. We want to decode Green plane and store the
    // decoded data in the param.u.J2K.OtherPartitions[0].Queue buffer.
    param.u.J2K.OtherPartitions[0].Queue.Start = green_ptr;
    param.u.J2K.OtherPartitions[0].Queue.Front = green_ptr;
    param.u.J2K.OtherPartitions[0].Queue.Rear = green_ptr;
    param.u.J2K.OtherPartitions[0].Queue.End = green_ptr + plane_size;
    param.u.J2K.OtherPartitions[0].Region = param.u.J2K.Region;

    // Setup third partition. we want to decode Blue plane and store the
    // decoded data in the param.u.J2K.OtherPartitions[1].Queue buffer.
    param.u.J2K.OtherPartitions[1].Queue.Start = blue_ptr;
    param.u.J2K.OtherPartitions[1].Queue.Front = blue_ptr;
    param.u.J2K.OtherPartitions[1].Queue.Rear = blue_ptr;
    param.u.J2K.OtherPartitions[1].Queue.End = blue_ptr + plane_size;
    param.u.J2K.OtherPartitions[1].Region = param.u.J2K.Region;

    // Re-initialize PICTools to recover partitions.
    res = Pegasus(&param, REQ_INIT);

    // Decode the J2K stream to reconstruct partitions.
    res = Pegasus(&param, REQ_EXEC);

    // Free PICTools resources.
    res = Pegasus(&param, REQ_TERM);

    // Reduce partition bit depth from 16-bit to 8-bit and interleave,
    // ordering samples Blue, Green, Red, with each row padded to a
    // 32-bit (DWORD) boundary.
    size_t bgr24_stride = (param.u.J2K.Region.Width * 24 + 31) / 32 * 4;
    size_t bgr24_size = bgr24_stride * param.u.J2K.Region.Height;
    std::unique_ptr<uint8_t[]> bgr24_data(new uint8_t[bgr24_size]);

    // Update the bitmap header.
    bmi = param.Head;
    bmi.biSize = sizeof(BITMAPINFOHEADER);
    bmi.biSizeImage = bgr24_size;
    bmi.biCompression = BI_RGB;
    bmi.biHeight = -labs(bmi.biHeight);
    bmi.biBitCount = 24;

    // Return the decoded buffer size.
    pixel_data = bgr24_data;
    return bgr24_size;

The conclusion? Resulted data in bgr24 buffer can be displayed or stored to the file as a bitmap using provided BITMAPINFOHEADER structure.


Ivan Headshot

Ivan Lyapunov, Software Engineer

Ivan Lyapunov joined Accusoft in May of 2016. A graduate of MIREA – Russian Technological University, Ivan received a bachelor’s degree in networking and computer engineering in 2000. As a member of the remote PrizmDoc team, he worked on LibreOffice and Microsoft Office engines for the PrizmDoc server. He currently contributes to the SDK team, where he has been working since 2018. Ivan currently helps the team develop ImagXpress, ScanFix Xpress, FormSuite, SmartZone, and PICTools. In his spare time, Ivan enjoys playing video games with his children, traveling, and experimenting with machine learning algorithms.