From 17f31d2d52ffa92f206d1dd55c32cc297767ba19 Mon Sep 17 00:00:00 2001 From: scaramallion Date: Thu, 29 Apr 2021 20:10:52 +1000 Subject: [PATCH] Match with pylibjpeg encoding interface (#6) --- rle/__init__.py | 4 ++- rle/tests/test_utils.py | 48 ++++++++++++++++----------- rle/utils.py | 72 ++++++++++++++++++++--------------------- 3 files changed, 69 insertions(+), 55 deletions(-) diff --git a/rle/__init__.py b/rle/__init__.py index 5845a32..2d8c99f 100644 --- a/rle/__init__.py +++ b/rle/__init__.py @@ -1,4 +1,6 @@ """Set package shortcuts.""" from rle._version import __version__ -from rle.utils import pixel_array, generate_frames, decode_pixel_data +from rle.utils import ( + pixel_array, generate_frames, decode_pixel_data, encode_pixel_data +) diff --git a/rle/tests/test_utils.py b/rle/tests/test_utils.py index d123f7b..1d9e6a5 100644 --- a/rle/tests/test_utils.py +++ b/rle/tests/test_utils.py @@ -59,9 +59,9 @@ def test_u8_1s_1f_by_kwargs(self): kwargs = {} kwargs['rows'] = ds.Rows kwargs['columns'] = ds.Columns - kwargs['samples_per_px'] = ds.SamplesPerPixel - kwargs['bits_per_px'] = ds.BitsAllocated - kwargs['nr_frames'] = int(getattr(ds, "NumberOfFrames", 1)) + kwargs['samples_per_pixel'] = ds.SamplesPerPixel + kwargs['bits_allocated'] = ds.BitsAllocated + kwargs['number_of_frames'] = int(getattr(ds, "NumberOfFrames", 1)) ref = ds.pixel_array gen = encode_array(ref, **kwargs) @@ -105,9 +105,9 @@ def test_u32_3s_2f_by_kwargs(self): kwargs = {} kwargs['rows'] = ds.Rows kwargs['columns'] = ds.Columns - kwargs['samples_per_px'] = ds.SamplesPerPixel - kwargs['bits_per_px'] = ds.BitsAllocated - kwargs['nr_frames'] = int(getattr(ds, "NumberOfFrames", 1)) + kwargs['samples_per_pixel'] = ds.SamplesPerPixel + kwargs['bits_allocated'] = ds.BitsAllocated + kwargs['number_of_frames'] = int(getattr(ds, "NumberOfFrames", 1)) ref = ds.pixel_array gen = encode_array(ref, **kwargs) @@ -137,8 +137,8 @@ def test_bad_byteorder_raises(self): kwargs = { 'rows': 0, 'columns': 0, - 'samples_per_px': 1, - 'bits_per_px': 16, + 'samples_per_pixel': 1, + 'bits_allocated': 16, 'byteorder': '=', } @@ -153,17 +153,29 @@ def test_bad_byteorder_raises(self): with pytest.raises(ValueError, match=msg): encode_pixel_data(b'', **kwargs) + def test_no_byteorder_u8(self): + """Test exception raised if invalid byteorder.""" + kwargs = { + 'rows': 1, + 'columns': 1, + 'samples_per_pixel': 1, + 'bits_allocated': 8, + 'byteorder': None, + } + + assert b'\x00\x01' == encode_pixel_data(b'\x01', **kwargs)[64:] + def test_bad_samples_raises(self): """Test exception raised if invalid samples per pixel.""" kwargs = { 'rows': 0, 'columns': 0, - 'samples_per_px': 0, - 'bits_per_px': 0, + 'samples_per_pixel': 0, + 'bits_allocated': 0, 'byteorder': '<', } - msg = r"'samples_per_px' must be 1 or 3" + msg = r"'samples_per_pixel' must be 1 or 3" with pytest.raises(ValueError, match=msg): encode_pixel_data(b'', **kwargs) @@ -172,12 +184,12 @@ def test_bad_bits_allocated_raises(self): kwargs = { 'rows': 0, 'columns': 0, - 'samples_per_px': 1, - 'bits_per_px': 2, + 'samples_per_pixel': 1, + 'bits_allocated': 2, 'byteorder': '<', } - msg = r"'bits_per_px' must be 8, 16, 32 or 64" + msg = r"'bits_allocated' must be 8, 16, 32 or 64" with pytest.raises(ValueError, match=msg): encode_pixel_data(b'', **kwargs) @@ -186,8 +198,8 @@ def test_bad_length_raises(self): kwargs = { 'rows': 1, 'columns': 1, - 'samples_per_px': 1, - 'bits_per_px': 8, + 'samples_per_pixel': 1, + 'bits_allocated': 8, 'byteorder': '<', } @@ -200,8 +212,8 @@ def test_too_many_segments_raises(self): kwargs = { 'rows': 1, 'columns': 1, - 'samples_per_px': 3, - 'bits_per_px': 64, + 'samples_per_pixel': 3, + 'bits_allocated': 64, 'byteorder': '<', } diff --git a/rle/utils.py b/rle/utils.py index 2ceafb3..bed019a 100644 --- a/rle/utils.py +++ b/rle/utils.py @@ -20,8 +20,8 @@ def decode_pixel_data(src: bytes, ds: "Dataset", **kwargs) -> "np.ndarray": ds : pydicom.dataset.Dataset A :class:`~pydicom.dataset.Dataset` containing the group ``0x0028`` elements corresponding to the image frame. - kwargs : dict, optional - A dictionary containing options for the decoder. Current options are: + **kwargs + Current decoding options are: * ``{'byteorder': str}`` specify the byte ordering for the decoded data when more than 8 bits per pixel are used, should be '<' for little @@ -64,17 +64,16 @@ def encode_array( The dataset corresponding to `arr` with matching values for *Rows*, *Columns*, *Samples per Pixel* and *Bits Allocated*. Required if the array properties aren't specified using `kwargs`. - kwargs : dict, optional - A dictionary containing keyword arguments. Required if `ds` isn't used, - keys are: + **kwargs + Required keyword parameters if `ds` isn't used are: - * ``{'rows': int, 'columns': int}`` the number of rows and columns - contained in `arr`. - * ``{samples_per_px': int}`` the number of samples per pixel, either + * ``'rows': int`` the number of rows contained in `src` + * ``'columns': int`` the number of columns contained in `src` + * ``samples_per_px': int`` the number of samples per pixel, either 1 for monochrome or 3 for RGB or similar data. - * ``{'bits_per_px': int}`` the number of bits needed to contain each + * ``'bits_per_px': int`` the number of bits needed to contain each pixel, either 8, 16, 32 or 64. - * ``{'nr_frames': int}`` the number of frames in `arr`, required if + * ``'nr_frames': int`` the number of frames in `arr`, required if more than one frame is present. Yields @@ -91,11 +90,11 @@ def encode_array( if ds: kwargs['rows'] = ds.Rows kwargs['columns'] = ds.Columns - kwargs['samples_per_px'] = ds.SamplesPerPixel - kwargs['bits_per_px'] = ds.BitsAllocated - kwargs['nr_frames'] = int(getattr(ds, "NumberOfFrames", 1)) + kwargs['samples_per_pixel'] = ds.SamplesPerPixel + kwargs['bits_allocated'] = ds.BitsAllocated + kwargs['number_of_frames'] = int(getattr(ds, "NumberOfFrames", 1) or 1) - if kwargs['nr_frames'] > 1: + if kwargs['number_of_frames'] > 1: for frame in arr: yield encode_pixel_data(frame.tobytes(), **kwargs) else: @@ -127,21 +126,18 @@ def encode_pixel_data( *Columns*, *Samples per Pixel* and *Bits Allocated*. Required if the frame properties aren't specified using `kwargs`. byteorder : str, optional - Required if the samples per pixel is greater than 1 and the value is - not passed using `kwargs`. If `src` is in little-endian byte order - then ``'<'``, otherwise ``'>'`` for big-endian. - kwargs : dict - A dictionary containing keyword arguments. Required keys are: - - * ``{'rows': int, 'columns': int}`` the number of rows and columns - contained in `src` - * ``{samples_per_px': int}`` the number of samples per pixel, either + Required if the samples per pixel is greater than 1. If `src` is in + little-endian byte order then ``'<'``, otherwise ``'>'`` for + big-endian. + **kwargs + If `ds` is not used then the following are required: + + * ``'rows': int`` the number of rows contained in `src` + * ``'columns': int`` the number of columns contained in `src` + * ``samples_per_pixel': int`` the number of samples per pixel, either 1 for monochrome or 3 for RGB or similar data. - * ``{'bits_per_px': int}`` the number of bits needed to contain each + * ``'bits_allocated': int`` the number of bits needed to contain each pixel, either 8, 16, 32 or 64. - * ``{'byteorder': str}``, required if the samples per pixel is greater - than 1. If `src` is in little-endian byte order then ``'<'``, - otherwise ``'>'`` for big-endian. Returns ------- @@ -149,21 +145,23 @@ def encode_pixel_data( The RLE encoded frame. """ if ds: - r, c = ds.Rows, ds.Columns + r = ds.Rows + c = ds.Columns bpp = ds.BitsAllocated spp = ds.SamplesPerPixel else: - r, c = kwargs['rows'], kwargs['columns'] - bpp = kwargs['bits_per_px'] - spp = kwargs['samples_per_px'] + r = kwargs['rows'] + c = kwargs['columns'] + bpp = kwargs['bits_allocated'] + spp = kwargs['samples_per_pixel'] # Validate input if spp not in [1, 3]: - src = "(0028,0002) 'Samples per Pixel'" if ds else "'samples_per_px'" + src = "(0028,0002) 'Samples per Pixel'" if ds else "'samples_per_pixel'" raise ValueError(src + " must be 1 or 3") if bpp not in [8, 16, 32, 64]: - src = "(0028,0100) 'Bits Allocated'" if ds else "'bits_per_px'" + src = "(0028,0100) 'Bits Allocated'" if ds else "'bits_allocated'" raise ValueError(src + " must be 8, 16, 32 or 64") if bpp / 8 * spp > 15: @@ -172,7 +170,8 @@ def encode_pixel_data( "Standard only allows a maximum of 15 segments" ) - if bpp > 8 and byteorder not in ('<', '>'): + byteorder = '<' if bpp == 8 else byteorder + if byteorder not in ('<', '>'): raise ValueError( "A valid 'byteorder' is required when the number of bits per " "pixel is greater than 8" @@ -236,8 +235,9 @@ def generate_frames(ds: "Dataset", reshape: bool = True) -> "np.ndarray": "elements are missing from the dataset: " + ", ".join(missing) ) - nr_frames = getattr(ds, "NumberOfFrames", 1) - r, c = ds.Rows, ds.Columns + nr_frames = int(getattr(ds, "NumberOfFrames", 1) or 1) + r = ds.Rows + c = ds.Columns bpp = ds.BitsAllocated dtype = pixel_dtype(ds)