Repository: open-dicom/dicom_parser

Issue:
Enable instantiating a Series using an iterable of Image instances
Instead of only allowing a path, enable passing an iterable of `Image` instances directly. Images should share the *SeriesInstanceUID* and be sorted by *InstanceNumber*.

Discussion:

Patch:
diff --git a/src/dicom_parser/messages.py b/src/dicom_parser/messages.py
index 48bd241..604293b 100644
--- a/src/dicom_parser/messages.py
+++ b/src/dicom_parser/messages.py
@@ -16,5 +16,6 @@ UNREGISTERED_MODALITY: str = (
     "No sequence identifiers registered for {modality}!"
 )
 MISSING_HEADER_INFO: str = "No header information found for {modality} sequence detection using {keys}!"
+MISSING_SERIES_SOURCE: str = "Series instances must be initialized with a path or an iterable of image instances!"
 
 # flake8: noqa: E501
diff --git a/src/dicom_parser/series.py b/src/dicom_parser/series.py
index e2f3692..7915b69 100644
--- a/src/dicom_parser/series.py
+++ b/src/dicom_parser/series.py
@@ -2,10 +2,11 @@
 Definition of the :class:`Series` class.
 """
 from pathlib import Path
-from typing import Any, Generator, Tuple
+from typing import Any, Generator, Iterable, Optional, Tuple
 
 import numpy as np
 
+from dicom_parser import messages
 from dicom_parser.image import Image
 from dicom_parser.messages import (
     EMPTY_SERIES_DIRECTORY,
@@ -22,25 +23,43 @@ class Series:
     from a single directory and ordered by InstanceNumber.
     """
 
-    def __init__(self, path: Path, mime: bool = False):
+    def __init__(
+        self,
+        path: Optional[Path] = None,
+        images: Optional[Iterable[Image]] = None,
+        mime: bool = False,
+    ):
         """
         The Series class should be initialized with a string or a
         :class:`~pathlib.Path` instance representing the path of single
-        `DICOM series`_.
+        `DICOM series`_, or an iterable or :class:`dicom_parser.image.Image`
+        instances (one of *path* or *images* must be provided).
 
         .. _DICOM series:
            https://dcm4che.atlassian.net/wiki/spaces/d2/pages/1835038/A+Very+Basic+DICOM+Introduction
 
         Parameters
         ----------
-        path : :class:`~pathlib.Path` or str
-            Directory containing .dcm files.
+        path : :class:`~pathlib.Path` or str, optional
+            Directory containing .dcm files
+        images : Iterable[Image], optional
+            Image instances that make up the series
         mime : bool, optional
             Whether to find DICOM images by file mime type instead of
             extension, defaults to False
         """
-        self.path = self.check_path(path)
-        self.images = self.get_images(mime)
+        # Find images in series directory path, if provided.
+        if isinstance(path, (Path, str)):
+            self.path = self.check_path(path)
+            self.images = self.get_images(mime)
+        # Tupelize any iterable of images.
+        elif images is not None:
+            self.images = tuple(images)
+        # Otherwise, raise an exception.
+        else:
+            raise ValueError(messages.MISSING_SERIES_SOURCE)
+
+        # Pixel array cache.
         self._data = None
 
     def __len__(self) -> int:


Test patch:
diff --git a/tests/test_series.py b/tests/test_series.py
index 8e6e840..25419c9 100644
--- a/tests/test_series.py
+++ b/tests/test_series.py
@@ -168,3 +168,13 @@ class SeriesTestCase(TestCase):
         series = Series(TEST_SERIES_PATH)
         value = series.spatial_resolution
         self.assertTupleEqual(value, SERIES_SPATIAL_RESOLUTION)
+
+    def test_series_from_images(self):
+        images = [Image(dcm) for dcm in Path(TEST_SERIES_PATH).rglob("*.dcm")]
+        from_images = Series(images=images)
+        from_path = Series(TEST_SERIES_PATH)
+        self.assertEqual(len(from_images), len(from_path))
+
+    def test_series_init_without_path_or_images_raises_valueerror(self):
+        with self.assertRaises(ValueError):
+            Series()