# JPEG-LS Encoding

The requirements for JPEG-LS encoding are defined in Section 8.2.3 and Annex A.4.3 of Part 5 of the DICOM Standard. The JPEG-LS compression scheme is defined by ISO/IEC 14495-1/ITU T.87 (the second link has free access).

## Valid Image Pixel Parameters

The table below lists the valid Image Pixel module parameters for Pixel Data encoded using the JPEG-LS Lossless or JPEG-LS Near-lossless transfer syntaxes. For an explanation of each parameter and its relationship with the Pixel Data see the glossary of Image Pixel elements.

Samples per Pixel | Photometric Interpretation | Pixel Representation | Planar Configuration | Bits Allocated | Bits Stored
--- | --- | --- | --- | --- | ---
1 | MONOCHROME1 | 0 or 1 | (absent) | 8 or 16 | 2 to 16
MONOCHROME2
PALETTE COLOR 1 | 0
3 | RGB | 0 | 0 | 8 or 16 | 2 to 16
YBR_FULL | 8 | 2 to 8

1 JPEG-LS Lossless only

### Pixel Representation

The DICOM Standard allows the use of the JPEG-LS Near Lossless transfer syntax with signed pixel data as long as the Photometric Interpretation is `MONOCHROME1` or `MONOCHROME2`. In practice, however, this is complicated by the way lossy JPEG-LS encoding works:

- JPEG-LS does not track the signedness of the pixel data, so all data is assumed to be unsigned during compression

- JPEG-LS uses the specified absolute pixel value error as the constraint when performing lossy encoding (the NEAR parameter - in pydicom this is the jls_error parameter passed to the `encoding functions`)

Because of this, even though a NEAR value of `1` should limit the absolute pixel value error to 1 intensity unit, it’s possible to have pixels with an absolute error up to the sample bit-depth of the data:

    Raw 8-bit value:
        -128: 0b10000000 (as signed integer)
         128: 0b10000000 (as unsigned integer)

    Possible value after lossy encoding with a NEAR value of 1:
         127: 0b01111111 (as signed/unsigned integer)

    Total error as unsigned: 1 - OK
    Total error as signed: 255 - Very much not OK

Signed pixel data values should therefore be limited to the range (MIN + NEAR, MAX - NEAR), where MIN and MAX are the minimum and maximum values allowed for the given Bits Stored value: `-2**(Bits Stored - 1)` and `2**(Bits Stored - 1) - 1`. For example, when performing lossy encoding of 8-bit signed data and a NEAR value of 3 you should limit the pixel data values to the range (-128 + 3, 127 - 3).

Lossless JPEG-LS encoding has no such restriction and the full value range for the given Bits Stored can be used with both signed and unsigned pixel data.

### Photometric Interpretation

To ensure you have the correct Photometric Interpretation the uncompressed pixel data should already be in the corresponding color space:

- If your uncompressed pixel data is grayscale (intensity) based:

  - Use `MONOCHROME1` if the minimum intensity value should be displayed as white.

  - Use `MONOCHROME2` if the minimum intensity value should be displayed as black.

- If your uncompressed pixel data uses a single sample per pixel and is an index to the Red, Green and Blue Palette Color Lookup Tables:

  - Use `PALETTE COLOR`.

- If your uncompressed pixel data is in RGB color space:

  - For Photometric Interpretation `RGB` nothing else is required.

  - For Photometric Interpretation `YBR_FULL`

    - For Bits Allocated and Bits Stored less than or equal to 8: pixel data must be `converted into YCbCr color space`. However you should keep in mind that the conversion operation is lossy.

    - For Bits Allocated and Bits Stored between 9 and 16 (inclusive): pixel data should be downscaled to 8-bit (with Bits Stored, Bits Allocated and High Bit updated accordingly) and converted to YCbCr color space. Both of these operations are lossy.

- If your uncompressed pixel data is in YCbCr color space:

  - For Photometric Interpretation `RGB` the pixel data must first be `converted into RGB color space`. However the conversion operation is lossy.

  - For Photometric Interpretation `YBR_FULL` nothing else is required.

### Planar Configuration

If your uncompressed pixel data is in `RGB` or `YBR_FULL` color space then you may use a Planar Configuration of either `0` or `1` as JPEG-LS allows the use of different interleave modes. While a Planar Configuration of `1` (interleave mode 0) may result in better compression ratios, its also more likely to result in downstream issues with decoders that expect the more common Planar Configuration `0` (interleave mode 2) pixel ordering.

For either case, if the pixel data being encoded is in an `ndarray` then each frame should be shaped as (rows, columns, samples). If the pixel data being encoded is `bytes` then with Planar Configuration `0` the data is ordered as color-by-pixel:

    # Three 8-bit RGB pixels: (255, 255, 0), (0, 255, 0), (0, 255, 255)
    # Each pixel is encoded separately the concatenated
    #       first pixel | second px | third px  |
    src = b"\xFF\xFF\x00\x00\xFF\x00\x00\xFF\xFF"

With Planar Configuration `1` the data is ordered as color-by-plane:

    # Three 8-bit RGB pixels: (255, 255, 0), (0, 255, 0), (0, 255, 255)
    # Each color channel is encoded separately then concatenated
    #       red channel | green ch. | blue ch.  |
    src = b"\xFF\x00\x00\xFF\xFF\xFF\x00\x00\xFF"

## Examples

### JPEG-LS Lossless

Losslessly compress unsigned RGB pixel data in-place:

    from pydicom import examples
    from pydicom.uid import JPEGLSLossless

    ds = examples.rgb_color
    assert ds.SamplesPerPixel == 1
    assert ds.PhotometricInterpretation == 'RGB'
    assert ds.BitsAllocated == 8
    assert ds.BitsStored == 8
    assert ds.PixelRepresentation == 0
    assert len(ds.PixelData) == 921600

    ds.compress(JPEGLSLossless)

    print(len(ds.PixelData))  # ~261792

Losslessly compress signed greyscale pixel data in-place:

    from pydicom import examples
    from pydicom.uid import JPEGLSLossless

    ds = examples.ct
    assert ds.SamplesPerPixel == 1
    assert ds.PhotometricInterpretation == 'MONOCHROME2'
    assert ds.BitsAllocated == 16
    assert ds.BitsStored == 16
    assert ds.PixelRepresentation == 1
    assert len(ds.PixelData) == 32768

    ds.compress(JPEGLSLossless)

    print(len(ds.PixelData))  # ~14180

### JPEG-LS Near-lossless

Warning

pydicom makes no recommendations for specifying image quality for lossy encoding methods. Any examples of lossy encoding are for illustration purposes only.

When using the JPEG-LS Near-lossless transfer syntax, image quality is controlled by passing the jls_error parameter to the `encoding function`. jls_error is directly related to the JPEG-LS NEAR parameter, which is the allowed absolute error in pixel intensity units from the compression process and should be in the range `(0, 2**BitsStored - 1)`.

Lossy compression of unsigned pixel data with a maximum error of 2 pixel intensity units:

    from pydicom import examples
    from pydicom.uid import JPEGLSNearLossless

    ds = examples.rgb_color
    assert ds.SamplesPerPixel == 1
    assert ds.PhotometricInterpretation == 'RGB'
    assert ds.BitsAllocated == 8
    assert ds.BitsStored == 8
    assert ds.PixelRepresentation == 0
    assert len(ds.PixelData) == 921600

    ds.compress(JPEGLSNearLossless, jls_error=2)

    print(len(ds.PixelData))  # ~149188

Lossy compression of signed pixel data with a maximum error of 3 pixel intensity units:

    from pydicom import examples
    from pydicom.uid import JPEGLSNearLossless

    ds = examples.ct
    assert ds.SamplesPerPixel == 1
    assert ds.PhotometricInterpretation == 'MONOCHROME2'
    assert ds.BitsAllocated == 16
    assert ds.BitsStored == 16
    assert ds.PixelRepresentation == 1
    assert len(ds.PixelData) == 32768

    # Our pixel data therefore uses signed 16-bit integers with a single channel
    # We need to make sure the maximum and minimum values are within the allowed
    #   range (see the section on Pixel Representation near the start of this page)
    jls_error = 3

    # The minimum and maximum sample values for the given *Bits Stored*
    minimum = -2**(ds.BitsStored - 1)
    maximum = 2**(ds.BitsStored - 1) - 1

    arr = ds.pixel_array

    # Clip the array so all values are within the limits, you may want to
    # rescale instead of clipping. For this dataset this isn't actually
    # necessary as the pixel data is already within the limits
    arr = np.clip(minimum + jls_error, maximum - jls_error)

    ds.compress(JPEGLSNearLossless, arr, jls_error=jls_error)

    print(ds.PixelData)  # ~8508

## Available Plugins

Encoder | Plugins
--- | ---
Name | Requires | Added
`JPEGLSLosslessEncoder` | pyjpegls | numpy, pyjpegls | v3.0
`JPEGLSNearLosslessEncoder`