Repository: pydicom/pydicom

Issue:
Add ability to read only specific tags
_From [njv...@gmail.com](https://code.google.com/u/101862332387277300822/) on December 03, 2010 11:13:32_

On my DICOMs, (2D slices from a GE MRI scanner) I've found that telling PyDICOM to skip the pixel data is actually slower than reading it. This is both true when repeatedly re-reading one file with timeit, or in running time_test.py
1. Point time_test.py at a directory full of dicoms
2. Enable only test_full_read() and test_partial(). Don't set dataset=.
3. Run a bunch of times, compare times.

test_full_read()
601937 function calls (601137 primitive calls) in 1.750 CPU seconds

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   102100    0.906    0.000    0.906    0.000 {method 'read' of 'file' objects}
34200/33600    0.211    0.000    0.656    0.000 filereader.py:150(data_element_generator)
    35100    0.141    0.000    0.171    0.000 tag.py:8(Tag)
  400/200    0.120    0.000    1.032    0.005 filereader.py:259(read_dataset)
    33800    0.086    0.000    0.086    0.000 {built-in method **new** of type object at 0x100126080}

test_partial()
829791 function calls (828991 primitive calls) in 2.355 CPU seconds

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   101900    1.048    0.000    1.048    0.000 {method 'read' of 'file' objects}
    67800    0.275    0.000    0.327    0.000 tag.py:8(Tag)
34100/33500    0.228    0.000    0.958    0.000 filereader.py:150(data_element_generator)
        1    0.162    0.162    2.348    2.348 time_test.py:69(test_partial)
    67200    0.134    0.000    0.502    0.000 tag.py:56(**eq**)

Or, in a python shell:
1. r_all = "d = dicom.read_file('s11_epi.0001')"
2. r_nopix = "d = dicom.read_file('s11_epi.0001', stop_before_pixels=True)"
   3: timeit.timeit(r_all, "import dicom", number=500) # 2.7346301078796387
   4: timeit.timeit(r_nopix, "import dicom", number=500) # 3.6994130611419678

I wouldn't be surprised to not save much time in reading these, but to have it be significantly slower? That's odd.

I'm using the version easy_install grabbed for me on December 2, 2010.

I can't post any dicoms to Google, but I could send you a few.

Cheers,
-Nate

_Original issue: http://code.google.com/p/pydicom/issues/detail?id=94_


Discussion:
vsoch: Even if it's the same speed, I would say this functionality is still useful for applications that may want to expose only a particular set of tags.
darcymason: LGTM for me... can we have a second?

Patch:
diff --git a/pydicom/filereader.py b/pydicom/filereader.py
index b9874fc41..692cc43d9 100644
--- a/pydicom/filereader.py
+++ b/pydicom/filereader.py
@@ -14,7 +14,7 @@ import zlib
 from io import BytesIO
 
 from pydicom.misc import size_in_bytes
-from pydicom.tag import TupleTag
+from pydicom.tag import TupleTag, Tag, BaseTag
 from pydicom.dataelem import RawDataElement
 from pydicom.util.hexutil import bytes2hex
 from pydicom.valuerep import extra_length_VRs
@@ -41,7 +41,7 @@ from pydicom.dataset import (
 )
 
 from pydicom.dicomdir import DicomDir
-from pydicom.datadict import dictionary_VR
+from pydicom.datadict import dictionary_VR, tag_for_keyword
 from pydicom.dataelem import DataElement
 from pydicom.tag import (
     ItemTag,
@@ -159,7 +159,8 @@ def data_element_generator(fp,
                            is_little_endian,
                            stop_when=None,
                            defer_size=None,
-                           encoding=default_encoding):
+                           encoding=default_encoding,
+                           specific_tags=None):
 
     """Create a generator to efficiently return the raw data elements.
 
@@ -177,6 +178,8 @@ def data_element_generator(fp,
         See ``read_file`` for parameter info.
     encoding :
         Encoding scheme
+    specific_tags : list or None
+        See ``read_file`` for parameter info.
 
     Returns
     -------
@@ -230,6 +233,18 @@ def data_element_generator(fp,
     element_struct_unpack = element_struct.unpack
     defer_size = size_in_bytes(defer_size)
 
+    tag_set = set()
+    has_specific_char_set = True
+    if specific_tags is not None:
+        for tag in specific_tags:
+            if isinstance(tag, (str, compat.text_type)):
+                tag = Tag(tag_for_keyword(tag))
+            if isinstance(tag, BaseTag):
+                tag_set.add(tag)
+        has_specific_char_set = Tag(0x08, 0x05) in tag_set
+        tag_set.add(Tag(0x08, 0x05))
+    has_tag_set = len(tag_set) > 0
+
     while True:
         # Read tag, VR, length, get ready to read value
         bytes_read = fp_read(8)
@@ -282,6 +297,11 @@ def data_element_generator(fp,
         if length != 0xFFFFFFFF:
             # don't defer loading of Specific Character Set value as it is
             # needed immediately to get the character encoding for other tags
+            if has_tag_set and tag not in tag_set:
+                # skip the tag if not in specific tags
+                fp.seek(fp_tell() + length)
+                continue
+
             if defer_size is not None and length > defer_size and tag != (
                     0x08, 0x05):
                 # Flag as deferred by setting value to None, and skip bytes
@@ -309,6 +329,8 @@ def data_element_generator(fp,
                 # Store the encoding value in the generator
                 # for use with future elements (SQs)
                 encoding = convert_encodings(encoding)
+                if not has_specific_char_set:
+                    continue
 
             yield RawDataElement(tag, VR, length, value, value_tell,
                                  is_implicit_VR, is_little_endian)
@@ -340,6 +362,8 @@ def data_element_generator(fp,
                     logger_debug(msg.format(fp_tell()))
                 seq = read_sequence(fp, is_implicit_VR,
                                     is_little_endian, length, encoding)
+                if has_tag_set and tag not in tag_set:
+                    continue
                 yield DataElement(tag, VR, seq, value_tell,
                                   is_undefined_length=True)
             else:
@@ -358,14 +382,19 @@ def data_element_generator(fp,
                     # Store the encoding value in the generator for use
                     # with future elements (SQs)
                     encoding = convert_encodings(encoding)
+                    if not has_specific_char_set:
+                        continue
 
+                # tags with undefined length are skipped after read
+                if has_tag_set and tag not in tag_set:
+                    continue
                 yield RawDataElement(tag, VR, length, value, value_tell,
                                      is_implicit_VR, is_little_endian)
 
 
 def read_dataset(fp, is_implicit_VR, is_little_endian, bytelength=None,
                  stop_when=None, defer_size=None,
-                 parent_encoding=default_encoding):
+                 parent_encoding=default_encoding, specific_tags=None):
     """Return a Dataset instance containing the next dataset in the file.
 
     Parameters
@@ -387,6 +416,8 @@ def read_dataset(fp, is_implicit_VR, is_little_endian, bytelength=None,
     parent_encoding :
         optional encoding to use as a default in case
         a Specific Character Set (0008,0005) isn't specified
+    specific_tags : list or None
+        See ``read_file`` for parameter info.
 
     Returns
     -------
@@ -400,7 +431,8 @@ def read_dataset(fp, is_implicit_VR, is_little_endian, bytelength=None,
     raw_data_elements = dict()
     fpStart = fp.tell()
     de_gen = data_element_generator(fp, is_implicit_VR, is_little_endian,
-                                    stop_when, defer_size, parent_encoding)
+                                    stop_when, defer_size, parent_encoding,
+                                    specific_tags)
     try:
         while (bytelength is None) or (fp.tell() - fpStart < bytelength):
             raw_data_element = next(de_gen)
@@ -635,7 +667,8 @@ def _at_pixel_data(tag, VR, length):
     return tag == (0x7fe0, 0x0010)
 
 
-def read_partial(fileobj, stop_when=None, defer_size=None, force=False):
+def read_partial(fileobj, stop_when=None, defer_size=None,
+                 force=False, specific_tags=None):
     """Parse a DICOM file until a condition is met.
 
     Parameters
@@ -648,6 +681,8 @@ def read_partial(fileobj, stop_when=None, defer_size=None, force=False):
         See ``read_file`` for parameter info.
     force : boolean
         See ``read_file`` for parameter info.
+    specific_tags : list or None
+        See ``read_file`` for parameter info.
 
     Notes
     -----
@@ -741,7 +776,8 @@ def read_partial(fileobj, stop_when=None, defer_size=None, force=False):
     #   the transfer syntax (whether read from the file meta or guessed at)
     try:
         dataset = read_dataset(fileobj, is_implicit_VR, is_little_endian,
-                               stop_when=stop_when, defer_size=defer_size)
+                               stop_when=stop_when, defer_size=defer_size,
+                               specific_tags=specific_tags)
     except EOFError:
         pass  # error already logged in read_dataset
 
@@ -757,7 +793,8 @@ def read_partial(fileobj, stop_when=None, defer_size=None, force=False):
                            is_implicit_VR, is_little_endian)
 
 
-def read_file(fp, defer_size=None, stop_before_pixels=False, force=False):
+def read_file(fp, defer_size=None, stop_before_pixels=False,
+              force=False, specific_tags=None):
     """Read and parse a DICOM dataset stored in the DICOM File Format.
 
     Read a DICOM dataset stored in accordance with the DICOM File Format
@@ -785,6 +822,9 @@ def read_file(fp, defer_size=None, stop_before_pixels=False, force=False):
         If False (default), raises an InvalidDicomError if the file is missing
         the File Meta Information header. Set to True to force reading even if
         no File Meta Information header is found.
+    specific_tags : list or None
+        If not None, only the tags in the list are returned. The list
+        elements can be tags or tag names.
 
     Returns
     -------
@@ -832,8 +872,9 @@ def read_file(fp, defer_size=None, stop_before_pixels=False, force=False):
         logger.debug("\n" + "-" * 80)
         logger.debug("Call to read_file()")
         msg = ("filename:'%s', defer_size='%s', "
-               "stop_before_pixels=%s, force=%s")
-        logger.debug(msg % (fp.name, defer_size, stop_before_pixels, force))
+               "stop_before_pixels=%s, force=%s, specific_tags=%s")
+        logger.debug(msg % (fp.name, defer_size, stop_before_pixels,
+                            force, specific_tags))
         if caller_owns_file:
             logger.debug("Caller passed file object")
         else:
@@ -849,7 +890,7 @@ def read_file(fp, defer_size=None, stop_before_pixels=False, force=False):
         stop_when = _at_pixel_data
     try:
         dataset = read_partial(fp, stop_when, defer_size=defer_size,
-                               force=force)
+                               force=force, specific_tags=specific_tags)
     finally:
         if not caller_owns_file:
             fp.close()


Test patch:
diff --git a/pydicom/tests/test_charset.py b/pydicom/tests/test_charset.py
index a8b41de2c..e54f516c8 100644
--- a/pydicom/tests/test_charset.py
+++ b/pydicom/tests/test_charset.py
@@ -79,6 +79,17 @@ class charsetTests(unittest.TestCase):
         ds = dicomio.read_file(multiPN_file)
         ds.decode()
 
+    def testEncodingWithSpecificTags(self):
+        """Encoding is correctly applied even if  Specific Character Set
+        is not in specific tags..."""
+        ds = dicomio.read_file(jp_file, specific_tags=['PatientName'])
+        ds.decode()
+        self.assertEqual(1, len(ds))
+        expected = ('Yamada^Tarou='
+                    '\033$B;3ED\033(B^\033$BB@O:\033(B='
+                    '\033$B$d$^$@\033(B^\033$B$?$m$&\033(B')
+        self.assertEqual(expected, ds.PatientName)
+
 
 if __name__ == "__main__":
     # This is called if run alone, but not if loaded through run_tests.py
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
index c89d4b15b..5a81c80ae 100644
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -310,6 +310,43 @@ class ReaderTests(unittest.TestCase):
         missing = [Tag(0x7fe0, 0x10), Tag(0xfffc, 0xfffc)]
         self.assertEqual(ctfull_tags, ctpartial_tags + missing, msg)
 
+    def testSpecificTags(self):
+        """Returns only tags specified by user."""
+        ctspecific = read_file(ct_name, specific_tags=[
+            Tag(0x0010, 0x0010), 'PatientID', 'ImageType', 'ViewName'])
+        ctspecific_tags = sorted(ctspecific.keys())
+        expected = [
+            # ViewName does not exist in the data set
+            Tag(0x0008, 0x0008), Tag(0x0010, 0x0010), Tag(0x0010, 0x0020)
+        ]
+        self.assertEqual(expected, ctspecific_tags)
+
+    def testSpecificTagsWithUnknownLengthSQ(self):
+        """Returns only tags specified by user."""
+        unknown_len_sq_tag = Tag(0x3f03, 0x1001)
+        tags = read_file(priv_SQ_name, specific_tags=[
+            unknown_len_sq_tag])
+        tags = sorted(tags.keys())
+        self.assertEqual([unknown_len_sq_tag], tags)
+
+        tags = read_file(priv_SQ_name, specific_tags=[
+            'PatientName'])
+        tags = sorted(tags.keys())
+        self.assertEqual([], tags)
+
+    def testSpecificTagsWithUnknownLengthTag(self):
+        """Returns only tags specified by user."""
+        unknown_len_tag = Tag(0x7fe0, 0x0010)  # Pixel Data
+        tags = read_file(emri_jpeg_2k_lossless, specific_tags=[
+            unknown_len_tag])
+        tags = sorted(tags.keys())
+        self.assertEqual([unknown_len_tag], tags)
+
+        tags = read_file(emri_jpeg_2k_lossless, specific_tags=[
+            'SpecificCharacterSet'])
+        tags = sorted(tags.keys())
+        self.assertEqual([Tag(0x08, 0x05)], tags)
+
     def testPrivateSQ(self):
         """Can read private undefined length SQ without error."""
         # From issues 91, 97, 98. Bug introduced by fast reading, due to