pydicom / pynetdicom

A Python implementation of the DICOM networking protocol
https://pydicom.github.io/pynetdicom
MIT License
500 stars 176 forks source link

StoreSCP: how to ensure saved image is identical to that received #946

Closed jeremygray2 closed 2 months ago

jeremygray2 commented 2 months ago

First of all, pynetdicom is bringing major joy to my summer. Its awesome to build my own StoreSCP (in a university research group). Thanks to everyone who makes this possible!

I did not see the answer to my question in previous open or closed issues, or in the docs, or when consulting a LLM.

Ideally, I'd like to have a test where I compare sha1sums of an instance (saved as a file) before and after sending it through my StoreSCP, and have them be identical. (I had this working in Orthanc sending an image using the REST API. Using dcmsend, it would add OFFIS_DCMTK_364 into the header.)

I'm seeing various diffs pre-post, eg in the headers (using dcmdump):

< (0002,0010) UI =LittleEndianImplicit                    #  18, 1 TransferSyntaxUID
< (0002,0012) UI [1.2.826.0.1.3680043.9.3811.2.1.0]       #  32, 1 ImplementationClassUID
< (0002,0013) SH [PYNETDICOM_210]                         #  14, 1 ImplementationVersionName
---
> (0002,0010) UI =LittleEndianExplicit                    #  20, 1 TransferSyntaxUID
> (0002,0012) UI [1.2.276.0.7230010.3.0.3.6.4]            #  28, 1 ImplementationClassUID
> (0002,0013) SH [OFFIS_DCMTK_364]                        #  16, 1 ImplementationVersionName
< (0019,1008) ?? 49\4d\41\47\45\20\4e\55\4d\20\34\20      #  12, 1 Unknown Tag & Data
< (0019,1009) ?? 31\2e\30\20                              #   4, 1 Unknown Tag & Data
---
> (0019,1008) CS [IMAGE NUM 4]                            #  12, 1 Unknown Tag & Data
> (0019,1009) LO [1.0]                                    #   4, 1 Unknown Tag & Data

Questions

  1. is it possible to get hash-level identical image files out the other end of a storescp, including headers? how?
  2. if not, how close can I get--can I edit a header value or two, and have things otherwise be identical?
  3. if no go on 2, can I compare the pixel_arrays -- or is there a better way to accomplish making a test of the integrity of my code?

Here's the supported contexts--trying to support ~everything, but I only need MRI-related

ae = AE()
ae.supported_contexts = StoragePresentationContexts
storage_sop_classes = [cx.abstract_syntax for cx in AllStoragePresentationContexts]
for uid in storage_sop_classes:
    ae.add_supported_context(uid, ALL_TRANSFER_SYNTAXES)

here's the part of my store-event callback where I (try to) get the file_meta info

def _handle_store(self, event):
    dataset = event.dataset
    dataset.file_meta = event.file_meta

and here's how I am storing each received image:

with open(temp_file, 'wb') as fh:
    fh.write(event.encoded_dataset())

With pynetdicom v2.0.x I was doing this, same behavior

event.dataset.save_as(temp_file, write_like_original=False)
scaramallion commented 2 months ago

On the other hand, if there's no transfer syntax change and you use pynetdicom to write the raw dataset codestream directory to file/memory without decoding it first, the codestream should be identical to that sent by the SCU. Then you only have to account for any differences between the file as-read and the file as-sent that may be introduced by the SCU.

def handle_store(event):
    """Handle a C-STORE request event."""
    with open(f"{uuid.uuid4()}", 'wb') as f:
        # Write the raw encoded dataset to `f`
        # No file meta information header, not conformant to DICOM Standard!
        f.write(event.request.DataSet.getvalue())

    # Return a 'Success' status
    return 0x0000

And as long as the transfer syntax hasn't changed from uncompressed to/from compressed then the actual element values of things like Pixel Data should remain the same, where "should" means the same as it always does in relation to DICOM, which means it will probably work about 98% of the time.

Personally, I'd compare the output you care about though.

from pydicom import dcmread
import numpy as np

ds_original = dcmread("file_sent.dcm")
ds_sent = dcmread("file_received.dcm")

assert np.array_equal(ds_sent.pixel_array, ds_original.pixel_array)

Just remember to keep the plugin used for pixel data conversion to ndarray consistent.