LAUREL BRIDGE

LaurelBridge.DCFExamples.OpenJpeg Namespace

DICOM Connectivity Framework V3.4
The OpenJpeg example demonstrates how to use the DCF to select and configure the Open Jpeg codec. The example uses the progressive encoding features of OpenJpeg to create lossy datasets from lossless ones.
Classes

  ClassDescription
Public classOptions
Command line options class for parsing user options for OpenJpeg.
Public classProgram
OpenJpeg has created a high performance library that may be utilized in DCF as a replacement for the Jasper codecs for the J2k90 and J2k91 transfer syntax. The following example demonstrates how to select and configure the OpjCodecOptions class to perform various functions using the OpenJpeg codec.
Remarks

Supported OS Platforms:

  • Windows - .Net Framework 4.7.2 64-bit and 32-bit
  • Windows - .Net Core 2.1 64-bit and 32-bit
  • Linux - .Net Core 2.1 64-bit

Examples

OpenJpeg Sample Code
public class Program
{
    /// <summary>
    /// This is the list of resolutions we are using to encode.
    /// </summary>
    private static readonly IList<double> _layerResolutions = new List<double>(new double[] { 50, 30, 10, 5, 1 }).AsReadOnly();

    /// <summary>
    /// Main entry point for OpenJpeg.
    /// </summary>
    /// <param name="args">Program arguments.</param>
    [STAThread]
    public static void Main(string[] args)
    {
        try
        {
            Options options;
            if (!Options.TryParse(args, out options))
            {
                throw new ArgumentException("bad command line parameters");
            }

            string directory = Path.GetDirectoryName(options.FileName) ?? ".";
            string baseName = Path.GetFileNameWithoutExtension(options.FileName);

            string opj90Path = Path.Combine(directory, baseName + "-opj90.dcm");
            DoOpenJpegTranscode(options.FileName, opj90Path);

            // TileCount should vary between 1 and # of layers.

            // create png file for each layer
            for (int layerCount = 1; layerCount <= _layerResolutions.Count; layerCount++)
            {
                string outputPath = Path.Combine(directory, baseName + "-" + layerCount + "layers.png");
                DoLayeredDicomToPng(opj90Path, outputPath, layerCount);
            }

            // create j2k code streams for each layer
            for (int layerCount = 1; layerCount <= _layerResolutions.Count; layerCount++)
            {
                string outputPath = Path.Combine(directory, baseName + "-" + layerCount + "layers.jpc");
                DoLayeredDicomToJpc(opj90Path, outputPath, layerCount);
            }

            // create dcm file for each layer
            for (int layerCount = 1; layerCount <= _layerResolutions.Count; layerCount++)
            {
                string outputPath = Path.Combine(directory, baseName + "-" + layerCount + "layers.dcm");
                double lossyRatio = _layerResolutions[layerCount - 1];
                DoLayeredDicomToDicom(opj90Path, outputPath, layerCount, lossyRatio, "JPEG LOSSY LAYERS(" + layerCount + ")");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("Error during execution: {0}", e);
            Environment.ExitCode = 1;
        }
        finally
        {
            if (Debugger.IsAttached)
            {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey();
            }
        }
    }

    /// <summary>
    /// Perform an OpenJpeg transcode of inputPath to outputPath.
    /// </summary>
    /// <param name="inputPath">input path</param>
    /// <param name="outputPath">output path</param>
    private static void DoOpenJpegTranscode(string inputPath, string outputPath)
    {
        DicomSessionSettings ss = new DicomSessionSettings();
        string resolutions = String.Join(",", _layerResolutions);
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k90) { J2kEncodeOverrides = "-r " + resolutions + " -p LRCP -TP L" });
        ss.EnableCompressionPassThroughMode = false;
        using (DicomFileInput dfi = new DicomFileInput(inputPath))
        {
            DicomDataSet dds = dfi.ReadDataSet();
            if (!dds.ContainsElement(Tags.PixelData))
                throw new NotSupportedException("no pixel data");
            int bitsAllocated = dds.GetElementIntValue(Tags.BitsAllocated, -1);
            if (bitsAllocated != 8 && bitsAllocated != 16)
                throw new NotSupportedException("unsupported bits allocated");
            string photometric = dds.GetElementStringValue(Tags.PhotometricInterpretation, null);
            if (String.IsNullOrEmpty(photometric) || photometric.Equals("PALETTE COLOR"))
                throw new NotSupportedException("unsupported photometric interpretation");
            using (DicomFileOutput dfo = new DicomFileOutput(outputPath, Uids.J2k90, ss))
            {
                dfo.WriteDataSet(dds);
            }
        }
        Console.WriteLine("Successfully transcoded {0} => {1} using OpenJpeg", inputPath, outputPath);
    }

    /// <summary>
    /// Read a DICOM file with layered J2k data and write a PNG file for the specified layers for frame 0.
    /// </summary>
    /// <param name="inputPath">input path</param>
    /// <param name="outputPath">output path</param>
    /// <param name="layers">number of layers to include</param>
    private static void DoLayeredDicomToPng(string inputPath, string outputPath, int layers)
    {
        DicomSessionSettings ss = new DicomSessionSettings();
        string decodeOverrides = layers == 0 ? String.Empty : String.Format("-l {0}", layers);
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k90) { J2kDecodeOverrides = decodeOverrides });

        using (DicomImage img = DicomImage.CreateFromFile(inputPath, ss))
        {
            using (Bitmap bmp = img.GetFrame(0))
            {
                bmp.Save(outputPath, ImageFormat.Png);
            }
        }
        Console.WriteLine("Successfully saved png image of {0} => {1} with layers={2}", inputPath, outputPath, layers);
    }

    /// <summary>
    /// Read a DICOM file with layered J2k and write the J2k code stream for the specified layers for frame 0.
    /// </summary>
    /// <param name="inputPath">input path</param>
    /// <param name="outputPath">output path</param>
    /// <param name="layers">number of layers to include</param>
    private static void DoLayeredDicomToJpc(string inputPath, string outputPath, int layers)
    {
        DicomSessionSettings ss = new DicomSessionSettings();
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k90));
        using (FrameProducer fp = FrameProducer.CreateFromFile(inputPath, null, ss, 0))
        {
            using (Stream compressedStream = fp.GetCompressedDataStream(0))
            {
                using (Stream layerStream = LayerPruner.PruneStream(compressedStream, layers))
                {
                    using (FileStream outputStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
                    {
                        layerStream.CopyTo(outputStream);
                    }
                }
            }
        }
        Console.WriteLine("Successfully saved jpeg data stream from {0} => {1} with layers={2}", inputPath, outputPath, layers);
    }

    /// <summary>
    /// Read a DICOM file with layered J2k and write the J2k code stream for the specified layers for all frames.
    /// </summary>
    /// <param name="inputPath">input path</param>
    /// <param name="outputPath">output path</param>
    /// <param name="layers">number of layers to include</param>
    /// <param name="lossyRatio">the value of the lossy compression ratio to be noted within the dataset</param>
    /// <param name="derivationKind">the value of the derivationKind string to be noted within the dataset</param>
    private static void DoLayeredDicomToDicom(string inputPath, string outputPath, int layers, double lossyRatio, string derivationKind)
    {
        DicomSessionSettings ss = new DicomSessionSettings();
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k90));
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k91));
        using (DicomFileInput dfi = new DicomFileInput(inputPath, ss))
        {
            DicomDataSet dds = dfi.ReadDataSet();
            DicomDataSet ddsClone = dds.Clone() as DicomDataSet; // for safety, but probably unneeded
            using (var lfe = new LayeredFrameEnumerator(ddsClone, layers))
            {
                DoPruneDicom(dds, lfe, lossyRatio, derivationKind);
            }

            using (DicomFileOutput dfo = new DicomFileOutput(outputPath, Uids.J2k91, ss))
            {
                dfo.WriteDataSet(dds);
            }
        }
        Console.WriteLine("Successfully saved pruned dicom stream of {0} => {1} with layers={2}", inputPath, outputPath, layers);
    }

    /// <summary>
    /// Modify the specified lossless compressed layered dataset to create a 'pruned' lossy dataset.
    /// </summary>
    /// <remarks>
    /// For this to work with large and/or multiframe images, a BoundedMemoryStream is used to constrain memory usage.
    /// </remarks>
    /// <param name="dataSet">the original dataset, with the correct number of frames and image header information</param>
    /// <param name="frameStreams">an enumeration of the frames streams in the original dataset</param>
    /// <param name="lossyRatio">the lossy ratio</param>
    /// <param name="derivationKind">the derivation kind</param>
    /// <param name="writeBot">write the basic offset table</param>
    private static void DoPruneDicom(DicomDataSet dataSet, IEnumerable<Stream> frameStreams, double lossyRatio, string derivationKind, bool writeBot = true)
    {
        Debug.Assert(dataSet != null && frameStreams != null);
        DicomSessionSettings ss = new DicomSessionSettings();
        DicomStreamableDataElement pixelElement = dataSet.GetElement(Tags.PixelData) as DicomStreamableDataElement;
        if (pixelElement == null)
            throw new ArgumentException("no pixel element", "dataSet");
        ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k91)
        {
            LossyCompressionRatio = lossyRatio,
            DerivationKind = derivationKind ?? "JPEG2000 LOSSY CHIPPED"
        });
        int frameCount = dataSet.GetElementIntValue(Tags.NumberOfFrames, 1);
        // Create a codec to encapsulate the jpeg streams we receive from frameStreams
        BoundedMemoryStream bms = new BoundedMemoryStream();
        DicomTSCWCodec codec = new DicomTSCWCodec(Uids.J2k91, null, new DicomStreamWriter(bms), ss);

        // first comes basic offset table - write one with all zeros to reserve space - we'll update it after writing the frames
        long botOffset = codec.Writer.CurrentOffset;
        uint[] botData = new uint[writeBot ? frameCount : 0];
        WriteBasicOffsetTable(codec, botData, botOffset);

        // each frame offset in the basic offset table is relative to the first frame
        // currently these are 32bit unsigned values; DICOM is starting to work out how to have 64bit offsets
        long frame0Offset = codec.Writer.CurrentOffset;
        int frameNo = 0;
        foreach (Stream frameStream in frameStreams)
        {
            if (frameNo > frameCount)
                throw new ArgumentException("too many frames, expected exactly frameCount(" + frameCount + ")", "frameStreams");
            Debug.Assert(frameStream.CanRead);
            Debug.Assert(frameStream.CanSeek);
            Debug.Assert(frameStream.Position == 0L);
            long currentOffset = codec.Writer.CurrentOffset;
            long frameOffset = currentOffset - frame0Offset;
            if (writeBot)
            {
                Debug.Assert(frameOffset < UInt32.MaxValue);
                // fill in basic offset table
                botData[frameNo] = (uint)frameOffset;
            }

            long length = frameStream.Length;
            // may need to pad frame
            bool needPad = 0 != ((currentOffset + length) & 0x1);
            long paddedLength = length + (needPad ? 1 : 0);
            Debug.Assert(paddedLength < UInt32.MaxValue);
            codec.WriteElementHeader(new DicomSQDelimElement(Tags.Item, (int)paddedLength));
            Debug.Assert(codec.Writer.CurrentOffset == bms.Position);
            frameStream.CopyTo(bms);
            Debug.Assert(codec.Writer.CurrentOffset == bms.Position);
            if (needPad)
                codec.WriteByte(0x00);
            frameNo++;
        }

        if (frameNo < frameCount)
            throw new ArgumentException("too few frames, expected exactly frameCount(" + frameCount + ")", "frameStreams");

        // add sequence delimitation item to mark end of data
        codec.WriteElementHeader(new DicomSQDelimElement(Tags.SequenceDelimitationItem, 0));

        if (writeBot)
        {
            // now go back and rewrite the basic offset table
            WriteBasicOffsetTable(codec, botData, botOffset);
        }

        // the bounded memory stream now holds all the encapsulated data
        bms.Position = 0L;
        DicomStreamableDataElement newPixelElement = pixelElement is DicomOBElement
            ? new DicomOBElement(Tags.PixelData, -1, pixelElement.GetImageInfo(), null) as DicomStreamableDataElement
            : new DicomOWElement(Tags.PixelData, -1, pixelElement.GetImageInfo(), null);
        DicomCompressedDataInput dcdi = new DicomCompressedDataInput(bms, Uids.J2k91, ss, newPixelElement);
        newPixelElement.SetProducer(dcdi);
        dataSet.Insert(newPixelElement);

        codec.AddDerivedImageElements(dataSet);
    }

    /// <summary>
    /// Write the basic offset table using the given codec, an array of basic offset table values, and a seek offset for the start of the basic offset table.
    /// </summary>
    /// <param name="codec">The codec which must have a seekable writer.</param>
    /// <param name="botData">The data for the basic offset table.</param>
    /// <param name="seekOffset">The seek offset where the basic offset table starts.</param>
    private static void WriteBasicOffsetTable(DicomEncapsulatedCodec codec, uint[] botData, long seekOffset)
    {
        codec.Writer.SetCurrentOffset(seekOffset);
        int frameCount = botData.Length;
        DicomSQDelimElement tmpDelim = new DicomSQDelimElement(Tags.Item, frameCount * sizeof(uint));
        codec.WriteElementHeader(tmpDelim);
        ByteBuffer botBuf = ByteBuffer.Allocate(botData.Length * sizeof(uint));
        foreach (uint botOffset in botData)
        {
            botBuf.PutInt((int)botOffset);
        }

        botBuf.Position = 0;
        codec.Write(botBuf);
    }

    /// <summary>
    /// A class to enumerate the layered frames of a DICOM dataset.  The FrameProducer class is used
    /// to get each frame of the JPEG data from the pixel data element and converted with the PruneStream method.
    /// </summary>
    class LayeredFrameEnumerator : IEnumerable<Stream>, IDisposable
    {
        private readonly DicomSessionSettings _ss;
        private FrameProducer _fp;
        private readonly int _layers;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="dds">The dataset.</param>
        /// <param name="layers">The layer setting which should be between 1 and the number of layers.</param>
        /// <param name="sessionSettings">The session settings.</param>
        public LayeredFrameEnumerator(DicomDataSet dds, int layers, DicomSessionSettings sessionSettings = null)
        {
            _ss = sessionSettings ?? new DicomSessionSettings();
            _ss.SetCodecOptions(new OpjCodecOptions(Uids.J2k90)); // could have used Jasper for this
            _fp = FrameProducer.CreateFromDataSet(dds, null, _ss);
            _layers = layers;
        }

        /// <summary>
        /// Enumerate the jpeg data streams of each frame corresponding to the specified number of layers.
        /// </summary>
        /// <returns>The pruned streams as an enumeration.</returns>
        public IEnumerator<Stream> GetEnumerator()
        {
            for (int i = 0; i < _fp.FrameCount; i++)
            {
                yield return LayerPruner.PruneStream(_fp.GetCompressedDataStream(i), _layers);
            }
        }

        /// <summary>
        /// Enumerate the jpeg data streams of each frame corresponding to the specified number of layers.
        /// </summary>
        /// <returns>The pruned streams as an enumeration.</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Dispose this.
        /// </summary>
        public void Dispose()
        {
            if (_fp != null)
            {
                _fp.Dispose();
                _fp = null;
            }
        }
    }

    /// <summary>
    /// A class to search a J2k stream and prune it to a j2k stream with the specified number of layers.
    /// </summary>
    private static class LayerPruner
    {
        /// <summary>
        /// Get the specified layer from the J2k inputStream.
        /// </summary>
        /// <param name="inputStream">A J2k input stream with layer markers.</param>
        /// <param name="layers">The number of layers to get.</param>
        /// <returns>A new jpeg 2000 stream with the specified number of layers.</returns>
        public static Stream PruneStream(Stream inputStream, int layers)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                inputStream.CopyTo(ms);
                byte[] layerBytes = ms.ToArray();
                bool isCodeStream = layerBytes[0] == 0xFF;
                return isCodeStream
                    ? LayerPruner.PruneCodeStream(layerBytes, layers)
                    : LayerPruner.PruneJp2Format(layerBytes, layers);
            }
        }

        /// <summary>
        /// Prune a j2k codestream into one containing the specified layerCount.
        /// </summary>
        /// <param name="codeStreamBytes">the j2k codestream bytes</param>
        /// <param name="layerCount">the number of layers to use</param>
        /// <returns>The portion of the codestream corresponding to the layer count, properly terminated with an EOI marker and possibly padded.</returns>
        /// <exception cref="ArgumentException">For unexpected layerCount issues.</exception>
        private static Stream PruneCodeStream(byte[] codeStreamBytes, int layerCount)
        {
            Debug.Assert(codeStreamBytes.Length < Int32.MaxValue);
            if (layerCount < 1 || layerCount > 255)
            {
                // invalid tile count
                throw new ArgumentException("invalid layer count(" + layerCount + "): 0 <= layerCount <= 255", "layerCount");
            }
            Debug.Assert(layerCount > 0 && layerCount < 256);
            int curOffset = 0;
            int curLayer;
            for (curLayer = 0; curLayer < layerCount; curLayer++)
            {
                bool foundEOI = false;
                // find sot offset - start of tile part
                int sotOffset = curOffset;
                do
                {
                    sotOffset = Array.FindIndex(codeStreamBytes, (int)sotOffset, codeStreamBytes.Length - sotOffset, v => v == 0xFF);
                    if (sotOffset == -1)
                        throw new InvalidDataException("layer(" + (curLayer + 1) + "): could not find start of tile(FF90) after offset(" + curOffset + ")");
                    byte curTag = codeStreamBytes[sotOffset + 1];
                    if (curTag == 0x90 || curTag == 0xD9)
                    {
                        foundEOI = curTag == 0xD9;
                        break;
                    }
                    sotOffset++;
                } while (true);

                if (foundEOI)
                    break;

                unsafe
                {
                    int sotSize = sizeof(StartOfTile);
                    Debug.Assert(sotOffset > 0 && (sotOffset < codeStreamBytes.Length - sotSize) && codeStreamBytes[sotOffset + 1] == 0x90);
                    fixed (void* p = &codeStreamBytes[sotOffset])
                    {
                        StartOfTile* startOfTile = (StartOfTile*)p;
                        // update the number of tile parts in the codestream
                        byte tnsot = startOfTile->Tnsot;
                        if (tnsot > layerCount)
                        {
                            // only change it if we are reducing number of tiles
                            // j2k spec says we may be able to set TnSot to 0 always to indicate an unspecified number of tiles
                            startOfTile->Tnsot = (byte)layerCount;
                        }
                        // look past the sot tag marker, the start of data marker, and the data for the tile
                        long nxtOffset = sotOffset + startOfTile->Psot;
                        Debug.Assert(nxtOffset < Int32.MaxValue);
                        curOffset = (int)nxtOffset;
                    }
                }
            }

            if (curLayer != layerCount)
                throw new ArgumentException("Expected layerCount(" + layerCount + "): actual layerCount(" + curLayer + ")", "layerCount");

            int outputLength = curOffset + 2;
            MemoryStream result = new MemoryStream(outputLength);
            result.Write(codeStreamBytes, 0, curOffset);
            byte[] eoiBytes = { 0xFF, 0xD9 };
            result.Write(eoiBytes, 0, 2);
            result.Position = 0L;
            return result;
        }

        /// <summary>
        /// Not implemented.
        /// </summary>
        /// <param name="layerBytes">The bytes for the stream.</param>
        /// <param name="layers">The layer count.</param>
        /// <returns>A new jpeg 2000 stream with the specified number of layers.</returns>
        private static Stream PruneJp2Format(byte[] layerBytes, int layers)
        {
            throw new NotSupportedException("layer pruning not implemented for JP2 format");
        }

        /// <summary>
        /// Some unsafe magic to let us read the J2k Start Of Tile marker.
        /// </summary>
        [StructLayout(LayoutKind.Explicit)]
        private struct StartOfTile
        {
            [FieldOffset(0)] private ushort _sot;
            [FieldOffset(2)] private ushort _lsot;
            [FieldOffset(4)] private ushort _isot;
            [FieldOffset(6)] private uint _psot;
            [FieldOffset(10)] private byte _tpsot;
            [FieldOffset(11)] private byte _tnsot;

            public ushort Sot
            {
                get { return Swap16(_sot); }
                set { _sot = Swap16(value); }
            }
            public ushort Lsot
            {
                get { return Swap16(_lsot); }
                set { _lsot = Swap16(value); }
            }
            public ushort Isot
            {
                get { return Swap16(_isot); }
                set { _isot = Swap16(value); }
            }
            public uint Psot
            {
                get { return Swap32(_psot); }
                set { _psot = Swap32(value); }
            }
            public byte Tpsot
            {
                get { return _tpsot; }
                set { _tpsot = value; }
            }
            public byte Tnsot
            {
                get { return _tnsot; }
                set { _tnsot = value; }
            }
            private ushort Swap16(ushort value)
            {
                ushort swapped = (ushort)
                ((0x00FF & (value >> 8))
                    | (0xFF00 & (value << 8)));
                return swapped;
            }
            private uint Swap32(uint value)
            {
                uint swapped = (uint)
                ((0x000000FF & (value >> 24))
                    | (0x0000FF00 & (value >> 8))
                    | (0x00FF0000 & (value << 8))
                    | (0xFF000000 & (value << 24)));
                return swapped;
            }
        }
    }
}