Cross-linked DICOM files

48 views
Skip to first unread message

Dan Cutright

unread,
Apr 1, 2020, 10:44:33 AM4/1/20
to pydicom
Hi everyone,

I'm curious if pydicom has a method or dictionary of "Referenced" tag connections?  Or is it safe to assume a cross-file tag link is always pre-pended with "Referenced" in the keyword?

I'm trying to ensure I can maintain cross-file connections when editing DICOM tag values (e.g., RT-Structures keeps its connection to RT-Plan). Currently I check for 'Referenced'+TagKeyword, then iterate though all sequences with names that start with `Referenced`.  Perhaps I should use a walk to search sub-sequences too?

Here is some code for context (note that it's not plug-and-play as shown here), hopefully it's not too hard to follow.

Thanks!

Dan

import pydicom
from pydicom.datadict import keyword_dict


# Lots of code skipped here, following function is actually part of a class
# self.dcm is output from pydicom.read_file()

def sync_referenced_tag(self, keyword, old_value, new_value):
"""
Check if there is a Referenced tag with matching value, then set to new_value if so
:param keyword: DICOM tag keyword
:type keyword: str
:param old_value: if Referenced+keyword tag value is this value, update to new_value
:param new_value: new value of tag if connected
"""
tag = keyword_dict.get("Referenced%s" % keyword)
if tag is not None:
# Edit top-level tag value of dataset's original value is provided old_value
# self.init_tag_values only contain edited tag values
if self.init_tag_values.get(tag) == old_value or \
(tag in list(self.dcm) and self.get_tag_value(tag) == old_value):
self.edit_tag(tag, new_value)

# Find all top-level sequences with names that begin with 'Referenced'
# iterate through all keywords that start with 'Referenced', of each sequence
# if value matches old_value and its tag matches 'Referenced'+keyword, set to new_value
for key in self.dcm.trait_names():
if key.startswith('Referenced'):
dcm_item = getattr(self.dcm, key)
if isinstance(dcm_item, pydicom.sequence.Sequence):
for seq_item in dcm_item:
seq_keys = [sk for sk in seq_item.trait_names() if sk.startswith('Referenced')]
for sk in seq_keys:
if getattr(seq_item, sk) == old_value and sk == 'Referenced' + keyword:
setattr(seq_item, sk, new_value)

Darcy Mason

unread,
Apr 1, 2020, 8:55:03 PM4/1/20
to pydicom
Hi Dan,

I'm not sure if all cross-connections like this are guaranteed by the standard to have "Referenced" in the name, but I have yet to come across one that does not.

However, the thing that connects them is the SOPInstanceUID.  I'm going to assume that's what you really want to change. Things like the Referenced class should not change (an RTPlan will still be an RTPlan).  So I think what you might be looking for is to keep a dict with old SOPInstanceUIDs as keys, and the new ones as corresponding values. Then my approach would be to dataset.walk() recursively, and if VR is "UI", then replace old with new from the dictionary, or leave alone if not in the dictionary.  That will ensure consistent references across all files.  If there are other referenced values that you need to change, then it will be a little more complex, but a similar approach could be used.

Dan Cutright

unread,
Apr 1, 2020, 9:55:39 PM4/1/20
to pydicom
Thanks Darcy.  I've since simplified the code quite a bit with help of the walk and DICOM tree examples in the pydicom docs.

I was hoping for something a little more efficient, but I think your suggested approach is the way to the go.

Darcy Mason

unread,
Apr 2, 2020, 12:35:18 PM4/2/20
to pydicom
If it helps, here are my notes on how DICOM-RT classes are Referenced to each other ("iUID" = Instance UID, SS=structure set) based on my experience with several vendors files (may be differences from this that I haven't seen):

Study iUID (shared by all the below)

  • CTs

    • Series iUID (same for all slices)

  • ->SS -> Refd Frame of Reference Seq -> RT Refd Study Seq -> RT Refd Series Seq  -> refs the Series iUID and then -> Contours each ref individual CT slice SOPiUID

-> RTPlan -> Refd Structure Set Seq (n=1 only) -> Refd SOPiUID

-> RTDose-> Refd RT Plan Seq (n=**) -> SOP iUID 

** n=1 unless DoseSummationType = “MULTI_PLAN”

-> RTImage -> Refd RT Plan Seq -> Refd SOPiUID



Also helpful is the figure showing the reference relationships:

Darcy

Dan Cutright

unread,
Apr 2, 2020, 1:35:24 PM4/2/20
to pydicom
Awesome, thank you.  Looks like SS is bit more complicated than I thought.

By the way, checking for every data element with VR='UI' is a few orders of magnitude slower than the method I originally posted.  I'd definitely need to thread the process so users don't think the app has crashed.

I think I'll modify my original method to use recursion, then use that by default.  And then make the brute force method an option if the user needs it.

Dan Cutright

unread,
Apr 5, 2020, 2:27:19 PM4/5/20
to pydicom
Thanks again for your help Darcy.  I've been active on the message board because I've been writing a DICOM tag editor.  I know there are a few other solutions out there, but none of them quite satisfied my needs.  I think it has a few unique features too, but I didn't do a thorough search.

It's free, I provide executables, and no installation is required.  Built with wxPython and PyInstaller.

Darcy Mason

unread,
Apr 6, 2020, 12:31:24 PM4/6/20
to pydicom
Hi Dan, looks interesting.  The code is a good example for people to follow too, for navigating datasets and changing values.

Just a little glitch for me, after pip install and then running on command line, it come up as more than full screen height, so that file choosing was off-screen - I had to resize the window to find the menus and browse button.  Not sure if that is something you can control...

Thanks
Darcy

Dan Cutright

unread,
Apr 6, 2020, 12:42:12 PM4/6/20
to pydicom
Thanks for letting me know Darcy, I haven't experienced that with any of my wxPython applications (nor have my colleagues).  I set a minimum window size which should be 35% of of screen width and 80% of screen height.  I am curious how this happened... I'll do some research.

Which OS and version are you using? Python version?  I'll try to reproduce it.

Darcy Mason

unread,
Apr 6, 2020, 12:54:27 PM4/6/20
to pydicom
Hmm... looks like "my fault".  I checked my screen resolution, and it has scaling as "150% (recommended)".  I am using a relatively recent setup with a docked laptop and two other screens.  But not sure why that scaling would be the recommended.  In any case, if I change it to 100%, everything is fine.

Dan Cutright

unread,
Apr 6, 2020, 1:04:55 PM4/6/20
to pydicom
Oh, cool.  You got me thinking though.  I should do something smarter than just relative sizes for min dimensions.  This app really shouldn't take up 80% height on a 30" monitor for example.
Reply all
Reply to author
Forward
0 new messages