Repository: pydicom/pydicom

Issue:
Converting Dicom image to Png
**Describe the issue**
hi, i am trying to convert Dicom image to png but in case of some particular file i am getting this "list out of range error".

**Expected behavior**
dicom image converted to png pne

**Steps To Reproduce**
How to reproduce the issue. Please include:
1. A minimum working code sample
```
from pydicom import dcmread
def read_xray(path, voi_lut = True, fix_monochrome = True):
    dicom = dcmread(path, force=True)
    
    # VOI LUT (if available by DICOM device) is used to transform raw DICOM data to "human-friendly" view
    if voi_lut:
        data = apply_voi_lut(dicom.pixel_array, dicom)
    else:
        data = dicom.pixel_array
               
    # depending on this value, X-ray may look inverted - fix that:
    if fix_monochrome and dicom.PhotometricInterpretation == "MONOCHROME1":
        data = np.amax(data) - data
        
    data = data - np.min(data)
    data = data / np.max(data)
    data = (data * 255).astype(np.uint8)
        
    return data

img = read_xray('/content/a.5545da1153f57ff8425be6f4bc712c090e7e22efff194da525210c84aba2a947.dcm')
plt.figure(figsize = (12,12))
plt.imshow(img)
```
2. The traceback (if one occurred)
```
IndexError                                Traceback (most recent call last)
<ipython-input-13-6e53d7d16b90> in <module>()
     19     return data
     20 
---> 21 img = read_xray('/content/a.5545da1153f57ff8425be6f4bc712c090e7e22efff194da525210c84aba2a947.dcm')
     22 plt.figure(figsize = (12,12))
     23 plt.imshow(img)

2 frames
/usr/local/lib/python3.7/dist-packages/pydicom/multival.py in __getitem__(self, index)
     93         self, index: Union[slice, int]
     94     ) -> Union[MutableSequence[_ItemType], _ItemType]:
---> 95         return self._list[index]
     96 
     97     def insert(self, position: int, val: _T) -> None:

IndexError: list index out of range
```

3. Which of the following packages are available and their versions:
  * Numpy : latest as of 29th dec
  * Pillow : latest as of 29th dec
  * JPEG-LS : latest as of 29th dec
  * GDCM : latest as of 29th dec
4. The anonymized DICOM dataset (if possible).
image link : https://drive.google.com/file/d/1j13XTTPCLX-8e7FE--1n5Staxz7GGNWm/view?usp=sharing

**Your environment**
If you're using **pydicom 2 or later**, please use the `pydicom.env_info`
module to gather information about your environment and paste it in the issue:

```bash
$ python -m pydicom.env_info
```

For **pydicom 1.x**, please run the following code snippet and paste the
output.

```python
import platform, sys, pydicom
print(platform.platform(),
      "\nPython", sys.version,
      "\npydicom", pydicom.__version__)
```



Discussion:
```
Traceback (most recent call last):
  File "pyd1554.py", line 29, in <module>
    img = read_xray('datasets/pyd1554.dcm')
  File "...pyd1554.py", line 14, in read_xray
    data = apply_voi_lut(dicom.pixel_array, dicom)
  File ".../pydicom/pixel_data_handlers/util.py", line 348, in apply_voi_lut
    ds.VOILUTSequence[0].get('LUTDescriptor', None),
  File ".../pydicom/multival.py", line 95, in __getitem__
    return self._list[index]
IndexError: list index out of range
```
The *VOI LUT Sequence* is empty, which is probably non-conformant but I can't actually tell what the *SOP Class UID* is (your anonymiser is weird, but probably mammo).

Here's the whole (also weird) *VOI LUT Sequence* (in hex):
```
       Tag | SQ  |     | Length    | Seq end delimiter     |
28 00 10 30 53 51 00 00 FF FF FF FF FE FF DD E0 00 00 00 00
```

I might add a check for an empty sequence in `apply_voi_lut` (and the other visualisation functions).


Patch:
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -1488,7 +1488,7 @@ def _convert_pixel_data_without_handler(self) -> None:
         possible_handlers = [
             hh for hh in pydicom.config.pixel_data_handlers
             if hh is not None
-            and hh.supports_transfer_syntax(ts)  # type: ignore[attr-defined]
+            and hh.supports_transfer_syntax(ts)
         ]
 
         # No handlers support the transfer syntax
@@ -1504,7 +1504,7 @@ def _convert_pixel_data_without_handler(self) -> None:
         #   dependencies met
         available_handlers = [
             hh for hh in possible_handlers
-            if hh.is_available()  # type: ignore[attr-defined]
+            if hh.is_available()
         ]
 
         # There are handlers that support the transfer syntax but none of them
@@ -1518,13 +1518,13 @@ def _convert_pixel_data_without_handler(self) -> None:
             )
             pkg_msg = []
             for hh in possible_handlers:
-                hh_deps = hh.DEPENDENCIES  # type: ignore[attr-defined]
+                hh_deps = hh.DEPENDENCIES
                 # Missing packages
                 missing = [dd for dd in hh_deps if have_package(dd) is None]
                 # Package names
                 names = [hh_deps[name][1] for name in missing]
                 pkg_msg.append(
-                    f"{hh.HANDLER_NAME} "  # type: ignore[attr-defined]
+                    f"{hh.HANDLER_NAME} "
                     f"(req. {', '.join(names)})"
                 )
 
@@ -1819,7 +1819,7 @@ def overlay_array(self, group: int) -> "numpy.ndarray":
 
         available_handlers = [
             hh for hh in overlay_data_handlers
-            if hh.is_available()  # type: ignore[attr-defined]
+            if hh.is_available()
         ]
         if not available_handlers:
             # For each of the handlers we want to find which
@@ -1830,13 +1830,13 @@ def overlay_array(self, group: int) -> "numpy.ndarray":
             )
             pkg_msg = []
             for hh in overlay_data_handlers:
-                hh_deps = hh.DEPENDENCIES  # type: ignore[attr-defined]
+                hh_deps = hh.DEPENDENCIES
                 # Missing packages
                 missing = [dd for dd in hh_deps if have_package(dd) is None]
                 # Package names
                 names = [hh_deps[name][1] for name in missing]
                 pkg_msg.append(
-                    f"{hh.HANDLER_NAME} "  # type: ignore[attr-defined]
+                    f"{hh.HANDLER_NAME} "
                     f"(req. {', '.join(names)})"
                 )
 
@@ -1846,7 +1846,7 @@ def overlay_array(self, group: int) -> "numpy.ndarray":
         for handler in available_handlers:
             try:
                 # Use the handler to get an ndarray of the pixel data
-                func = handler.get_overlay_array  # type: ignore[attr-defined]
+                func = handler.get_overlay_array
                 return cast("numpy.ndarray", func(self, group))
             except Exception as exc:
                 logger.debug(
diff --git a/pydicom/encoders/base.py b/pydicom/encoders/base.py
--- a/pydicom/encoders/base.py
+++ b/pydicom/encoders/base.py
@@ -84,11 +84,11 @@ def add_plugin(self, label: str, import_path: Tuple[str, str]) -> None:
         module = import_module(import_path[0])
 
         # `is_available(UID)` is required for plugins
-        if module.is_available(self.UID):  # type: ignore[attr-defined]
+        if module.is_available(self.UID):
             self._available[label] = getattr(module, import_path[1])
         else:
             # `ENCODER_DEPENDENCIES[UID]` is required for plugins
-            deps = module.ENCODER_DEPENDENCIES  # type: ignore[attr-defined]
+            deps = module.ENCODER_DEPENDENCIES
             self._unavailable[label] = deps[self.UID]
 
     @staticmethod
diff --git a/pydicom/filebase.py b/pydicom/filebase.py
--- a/pydicom/filebase.py
+++ b/pydicom/filebase.py
@@ -182,15 +182,15 @@ def __init__(
         self.close = file_like_obj.close
         self.name: str = getattr(file_like_obj, 'name', '<no filename>')
 
-    def no_write(self, bytes_read: bytes) -> None:
+    def no_write(self, bytes_read: bytes) -> int:
         """Used for file-like objects where no write is available"""
         raise IOError("This DicomFileLike object has no write() method")
 
-    def no_read(self, bytes_read: Optional[int] = None) -> None:
+    def no_read(self, size: int = -1) -> bytes:
         """Used for file-like objects where no read is available"""
         raise IOError("This DicomFileLike object has no read() method")
 
-    def no_seek(self, offset: int, from_what: int = 0) -> None:
+    def no_seek(self, offset: int, whence: int = 0) -> int:
         """Used for file-like objects where no seek is available"""
         raise IOError("This DicomFileLike object has no seek() method")
 
diff --git a/pydicom/fileset.py b/pydicom/fileset.py
--- a/pydicom/fileset.py
+++ b/pydicom/fileset.py
@@ -2865,10 +2865,10 @@ def my_recorder(ds: Dataset) -> Dataset:
 
 def _single_level_record_type(ds: Dataset) -> str:
     """Return a single-level *Directory Record Type* for `ds`."""
-    sop_class = getattr(ds, "SOPClassUID", None)
+    sop_class = cast(Optional[UID], getattr(ds, "SOPClassUID", None))
 
     try:
-        return _SINGLE_LEVEL_SOP_CLASSES[sop_class]
+        return _SINGLE_LEVEL_SOP_CLASSES[sop_class]  # type: ignore[index]
     except KeyError:
         return "PATIENT"
 
@@ -2888,9 +2888,9 @@ def _four_level_record_type(ds: Dataset) -> str:
     if "RTPlanLabel" in ds:
         return "RT PLAN"
 
-    sop_class = getattr(ds, "SOPClassUID", None)
+    sop_class = cast(Optional[UID], getattr(ds, "SOPClassUID", None))
 
     try:
-        return _FOUR_LEVEL_SOP_CLASSES[sop_class]
+        return _FOUR_LEVEL_SOP_CLASSES[sop_class]  # type: ignore[index]
     except KeyError:
         return "IMAGE"
diff --git a/pydicom/filewriter.py b/pydicom/filewriter.py
--- a/pydicom/filewriter.py
+++ b/pydicom/filewriter.py
@@ -1000,6 +1000,7 @@ def dcmwrite(
         Write a DICOM file from a dataset that was read in with ``dcmread()``.
         ``save_as()`` wraps ``dcmwrite()``.
     """
+    tsyntax: Optional[UID]
 
     # Ensure is_little_endian and is_implicit_VR are set
     if None in (dataset.is_little_endian, dataset.is_implicit_VR):
@@ -1090,7 +1091,7 @@ def dcmwrite(
             fp.write(preamble)
             fp.write(b'DICM')
 
-        tsyntax: Optional[UID] = None  # type: ignore[no-redef]
+        tsyntax = None
         if dataset.file_meta:  # May be an empty Dataset
             # If we want to `write_like_original`, don't enforce_standard
             write_file_meta_info(
diff --git a/pydicom/pixel_data_handlers/util.py b/pydicom/pixel_data_handlers/util.py
--- a/pydicom/pixel_data_handlers/util.py
+++ b/pydicom/pixel_data_handlers/util.py
@@ -244,7 +244,7 @@ def apply_modality_lut(arr: "np.ndarray", ds: "Dataset") -> "np.ndarray":
     * DICOM Standard, Part 4, :dcm:`Annex N.2.1.1
       <part04/sect_N.2.html#sect_N.2.1.1>`
     """
-    if 'ModalityLUTSequence' in ds:
+    if ds.get("ModalityLUTSequence"):
         item = cast(List["Dataset"], ds.ModalityLUTSequence)[0]
         nr_entries = cast(List[int], item.LUTDescriptor)[0] or 2**16
         first_map = cast(List[int], item.LUTDescriptor)[1]
@@ -342,7 +342,7 @@ def apply_voi_lut(
       <part04/sect_N.2.html#sect_N.2.1.1>`
     """
     valid_voi = False
-    if 'VOILUTSequence' in ds:
+    if ds.get('VOILUTSequence'):
         ds.VOILUTSequence = cast(List["Dataset"], ds.VOILUTSequence)
         valid_voi = None not in [
             ds.VOILUTSequence[0].get('LUTDescriptor', None),
@@ -408,7 +408,7 @@ def apply_voi(
     * DICOM Standard, Part 4, :dcm:`Annex N.2.1.1
       <part04/sect_N.2.html#sect_N.2.1.1>`
     """
-    if "VOILUTSequence" not in ds:
+    if not ds.get('VOILUTSequence'):
         return arr
 
     if not np.issubdtype(arr.dtype, np.integer):
@@ -529,7 +529,7 @@ def apply_windowing(
     ds.BitsStored = cast(int, ds.BitsStored)
     y_min: float
     y_max: float
-    if 'ModalityLUTSequence' in ds:
+    if ds.get('ModalityLUTSequence'):
         # Unsigned - see PS3.3 C.11.1.1.1
         y_min = 0
         item = cast(List["Dataset"], ds.ModalityLUTSequence)[0]
diff --git a/pydicom/valuerep.py b/pydicom/valuerep.py
--- a/pydicom/valuerep.py
+++ b/pydicom/valuerep.py
@@ -55,7 +55,10 @@ def __getstate__(self) -> Dict[str, Any]:
     def __setstate__(self, state: Dict[str, Any]) -> None:
         self.__dict__.update(state)
 
-    def __reduce_ex__(self, protocol: int) -> Tuple[Any, ...]:
+    def __reduce_ex__(  # type: ignore[override]
+        self, protocol: int
+    ) -> Tuple[Any, ...]:
+        # Python 3.8 - protocol: SupportsIndex (added in 3.8)
         # datetime.time, and datetime.datetime return Tuple[Any, ...]
         # datetime.date doesn't define __reduce_ex__
         reduce_ex = cast(Tuple[Any, ...], super().__reduce_ex__(protocol))
@@ -324,17 +327,17 @@ def __new__(  # type: ignore[misc]
             if match.group('ms'):
                 microsecond = int(match.group('ms').rstrip().ljust(6, '0'))
 
-            return super().__new__(  # type: ignore[call-arg, no-any-return]
+            return super().__new__(
                 cls, hour, minute, second, microsecond
             )
 
         if isinstance(val, datetime.time):
-            return super().__new__(  # type: ignore[call-arg, no-any-return]
+            return super().__new__(
                 cls, val.hour, val.minute, val.second, val.microsecond
             )
 
         try:
-            return super().__new__(  # type: ignore[call-arg, no-any-return]
+            return super().__new__(
                 cls, *args, **kwargs
             )
         except Exception as exc:
@@ -925,7 +928,7 @@ def __new__(  # type: ignore[misc]
         if len(args) and args[0] is None:
             return None
 
-        return cast("PersonName", super().__new__(cls))
+        return super().__new__(cls)
 
     def __init__(
         self,


Test patch:
diff --git a/pydicom/tests/test_handler_util.py b/pydicom/tests/test_handler_util.py
--- a/pydicom/tests/test_handler_util.py
+++ b/pydicom/tests/test_handler_util.py
@@ -890,6 +890,10 @@ def test_unchanged(self):
         out = apply_modality_lut(arr, ds)
         assert arr is out
 
+        ds.ModalityLUTSequence = []
+        out = apply_modality_lut(arr, ds)
+        assert arr is out
+
     def test_lutdata_ow(self):
         """Test LUT Data with VR OW."""
         ds = dcmread(MOD_16_SEQ)
@@ -1839,6 +1843,10 @@ def test_unchanged(self):
         out = apply_windowing(arr, ds)
         assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
 
+        ds.ModalityLUTSequence = []
+        out = apply_windowing(arr, ds)
+        assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
+
     def test_rescale_empty(self):
         """Test RescaleSlope and RescaleIntercept being empty."""
         ds = dcmread(WIN_12_1F)
@@ -2051,6 +2059,11 @@ def test_unchanged(self):
         out = apply_voi(arr, ds)
         assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
 
+        ds.VOILUTSequence = []
+        out = apply_voi(arr, ds)
+        assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
+
+
     def test_voi_lutdata_ow(self):
         """Test LUT Data with VR OW."""
         ds = Dataset()
@@ -2072,6 +2085,7 @@ def test_voi_lutdata_ow(self):
 
 @pytest.mark.skipif(not HAVE_NP, reason="Numpy is not available")
 class TestNumpy_ApplyVOILUT:
+    """Tests for util.apply_voi_lut()"""
     def test_unchanged(self):
         """Test input array is unchanged if no VOI LUT"""
         ds = Dataset()
@@ -2082,6 +2096,10 @@ def test_unchanged(self):
         out = apply_voi_lut(arr, ds)
         assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
 
+        ds.VOILUTSequence = []
+        out = apply_voi_lut(arr, ds)
+        assert [-128, -127, -1, 0, 1, 126, 127] == out.tolist()
+
     def test_only_windowing(self):
         """Test only windowing operation elements present."""
         ds = Dataset()