pydicom / pynetdicom

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

Help - Return Dataset as Dicom file to client #388

Closed Neuroforge closed 5 years ago

Neuroforge commented 5 years ago

Hello,

I am using Pynetdicom to communicate with a PACS. It works well and i can return the dataset objects. Currently, pynetdicom can save the dataset using ds.save_as() but this requires disk reads/writes.

These dataset objects need to be returned to a javascript client as a file object.

Write the bytes to a temp file.

    def queryImage(self, ds):
        try:
            dataBytes = encode(ds, ds.is_implicit_VR, ds.is_little_endian)
            tempfile = MemoryTempfile()
            with tempfile.TemporaryFile("w", buffering=-1) as tempFile:
                tempFile.write(dataBytes.decode("utf-8") )
                tempSeek(0)
                return tempfile
        except Exception as e:
            print(e)
        return None

Errors with - 'utf-8' codec can't decode byte 0x80 in position 358: invalid start byte

Is there a way to put the data into a file without saving it to disk? The client side uses Cornerstonejs or Dicomparser.

Options. 1) Write dataset to temporary file, however, not sure how to format dataset correctly as encode/decode creates a byte array that cannot be decoded to a string. 2) Have a method similar to to_json like in pydicom, but not available on pynetdicom for some reason. 3) Encode byte array and return to dicomparser which appears to accept a byte array. (https://github.com/cornerstonejs/dicomParser) var dataSet = dicomParser.parseDicom(byteArray/*, options */);

There is another thread asking a similar question (https://github.com/pydicom/pynetdicom/issues/300) where it is suggested to json-ify the dataset. However, this won't be opened by Cornerstonejs or DicomParser.

scaramallion commented 5 years ago

pynetdicom.dsutils.encode() returns pydicom.filebase.DicomBytesIO which is just a subclass of io.BytesIO and keeps it in memory and should be file-like. You can use io.BytesIO.getvalue() to get bytes which I assume should be convertible to the byteArray required by parseDicom.

Though if you're using pynetdicom you can use event.request.DataSet which is already a io.BytesIO and skip the unecessary decode/re-encode step (unless you specifically need it for something else).

Neuroforge commented 5 years ago

Awesome. So the event.request. Where is that during the callback on_c_store?

def on_c_store(ds, context, info):
    meta = Dataset()
    meta.MediaStorageSOPClassUID = ds.SOPClassUID
    meta.MediaStorageSOPInstanceUID = ds.SOPInstanceUID
    meta.ImplementationClassUID = PYNETDICOM_IMPLEMENTATION_UID
    meta.TransferSyntaxUID = context.transfer_syntax

    # Add the file meta to the dataset
    ds.file_meta = meta
    ds.is_little_endian = context.transfer_syntax.is_little_endian
    ds.is_implicit_VR = context.transfer_syntax.is_implicit_VR

Similar code. https://github.com/pydicom/pynetdicom/issues/45#issuecomment-408580948

scaramallion commented 5 years ago

Oh, sorry, that's only available for v1.3 and higher with the event-handler system.

scaramallion commented 5 years ago

The other thing is that I don't think you should be decoding the raw data using utf-8, as utf-8 is a character encoding scheme which is why it throws an exception when it hits a value that doesn't match up with a character code. You should just write the raw binary data to the temp file or whatever.

data = encode(ds, ds.is_implicit_VR, ds.is_little_endian)
with tempfile.TemporaryFile('wb') as tfile:
    tfile.write(data)

And if you're just using plain python and javascript I think you're going to have to save the data to disk somewhere.

Neuroforge commented 5 years ago

Thank you for your input.

This is what i am trying now, but it appears that reading it back isn't suitable for cornerstonejs.

        fullFilePath = ""
        try:
            dataJson = self.db.get(fileId)

            ds = pickle.loads(dataJson)
            transferSyntax = ds.file_meta.TransferSyntaxUID
            if transferSyntax == JPEG2000Lossless:
                ds.decompress()

            fileName = self.getFileNameFromDataSet(ds)
            fullFilePath = os.path.join("static/temp", fileName)
            ds.save_as(fullFilePath)

            with open(fullFilePath, "rb") as tempFile:
                byteData = tempFile.read()
                return byteData
        except Exception as e:
            print(e)
            pass
        finally:
            if os.path.exists(fullFilePath):
                os.remove(fullFilePath)

Cornerstone error. throw 'dicomParser.readPart10Header: DICM prefix not found at location 132 - this is not a valid DICOM P10 file.';

scaramallion commented 5 years ago

Have you added the file meta information before trying to encode the dataset?

I was thinking about it a bit more and django should let you mix python and javascript on the server side without having to write to disk. Not sure if django is suitable for your use case though.

Neuroforge commented 5 years ago

Setting the write like original flag to false allows the file to be read to a HttpResponse

def QueryImage(fileId):
            .....
            fullFilePath = os.path.join("static/temp", fileName)
            ds.save_as(fullFilePath, write_like_original=False)

            with open(fullFilePath, "rb") as tempFile:
                byteData = tempFile.read()
                return byteData

Package response with content_type and Content-Disposition.

                     dataBytes = QueryImage(fileId)
                    if dataBytes:
                        response = HttpResponse(
                            dataBytes, content_type="application/dicom"
                        )
                        response["Content-Disposition"] = (
                            "attachment; filename=" + fileId
                        )
                        return response

Javascript can recieve (Using Angular7) as follows

    getImages(fileId: string): Observable<any> {
        return this.http
            .get(`${this.serverURL}/pacs/patient`, {
                params: new HttpParams().set('fileId', fileId),
                responseType: 'blob',
            })
            .pipe();
    }