Transcode or decompress based on destination DICOM.

Hi there!

We use Orthanc 1.9.1 to route studies from a fleet of mobile US and XR techs to a central location, and then on to various destinations.

Basically, each modality talks to a local (laptop/tablet) instance of Orthanc. The mobile instance uses a LUA script with five pre-established destinations based on a string in the SCP AE title called by the modality. The mobile Orthanc auto-routes using a peer-to-peer connection over cellular back to the central Orthanc server, which auto-routes the studies to the corresponding path of the five options via DICOM. This process works without issue.

In one particular case, the technicians need to send studies to two different destinations, let’s call them A and B. Originally I was auto-routing those with the central Orthanc server in the LUA script. But destination A will only accept J2K, and destination B cannot handle J2K. Right now we’re using the modality’s settings to send them twice, uncompressed for destination A (which is transcoded to J2K by the mobile Orthanc), and JPEG compressed for destination B (which is passed on as-is.)

What I want to do is be able to receive the J2K-encoded study at the central Orthanc server, and forward it on to destination A as-is, but transcode it to either JPEG or uncompressed for destination B.

I believe from what I’ve read that it would be possible to either adjust the config.json to make the specific DICOM server always send uncompressed, or put a REST API command in the LUA to transcode the study and then send that. I haven’t been able to figure out the exact syntax to do either of these things.

– LUA Auto-routing script for Central Orthanc Server

function OnStoredInstance(instanceId, tags, metadata, origin)
print('*—Routing instanceId: ', instanceId)

if origin[‘Username’] ~= nil then
– Extract the value of the “Username” tag
local UserNameVar = string.upper(origin[‘Username’])
– print(UserNameVar)

if string.find(UserNameVar, ‘_DCM1’) ~= nil then
– Send *_DCM1 studies to DestinationA.
– print(‘UserNameVar includes _DCM1’)
SendToModality(instanceId, ‘Destination_A’)
print(‘P2P-’, UserNameVar, ’ - Fwd>DestinationA.')
end

if string.find(UserNameVar, ‘_DCM2’) ~= nil then
– Send *_DCM2 studies to both DestinationB and DestinationA.
– print(‘UserNameVar includes _DCM2’)
SendToModality(instanceId, ‘DestinationB’)
SendToModality(instanceId, ‘Destination_A’)
print(‘P2P-’, UserNameVar, ’ - Fwd>DestinationA, Fwd>DestinationB.')
end

if string.find(UserNameVar, ‘_DCM3’) ~= nil then
– Send *_DCM3 studies to Destination_C.
– print(‘UserNameVar includes _DCM3’)
SendToModality(instanceId, ‘Destination_C’)
print(‘P2P-’, UserNameVar, ’ - Fwd>DestinationC.')
end

– if string.find(UserNameVar, ‘_DCM4’) ~= nil then
– – Send *_DCM4 studies to XXXDestination4.
– print(‘UserNameVar includes _DCM4’)
– SendToModality(instanceId, ‘XXXDestination4’)
– print(‘P2P-’, UserNameVar, ’ - Fwd>??.')
– end

if string.find(UserNameVar, ‘_DCM5’) ~= nil then
– Send *_DCM5 studies to Destination_E.
– print(‘UserNameVar includes _DCM5’)
SendToModality(instanceId, ‘Destination_E’)
print(‘P2P-’, UserNameVar, ’ - Fwd>DestinationE.')
end
– print(‘Username DICOM Routing End’)
end

if origin[‘CalledAet’] ~= nil then
– Skip processing rules if CalledAet is nil.
local CalledAETVar = string.upper(origin[‘CalledAet’])
– Extract the value of the “CalledAet” DICOM tag
– print(CalledAETVar)

if string.find(CalledAETVar, ‘_DCM1’) ~= nil then
– Send *_DCM1 studies to DestinationA.
– print(‘CalledAETVar includes _DCM1’)
SendToModality(instanceId, ‘Destination_A’)
print(‘DICOM-’, CalledAETVar, ’ - Fwd>DestinationA.')
end

if string.find(CalledAETVar, ‘_DCM2’) ~= nil then
– Send *_DCM2 studies to both DestinationB and DestinationA.
– print(‘CalledAETVar includes _DCM2’)
SendToModality(instanceId, ‘DestinationB’)
SendToModality(instanceId, ‘Destination_A’)
print(‘DICOM-’, CalledAETVar, ’ - Fwd>DestinationA, Fwd>DestinationB.')
end

if string.find(CalledAETVar, ‘_DCM3’) ~= nil then
– Send *_DCM3 studies to Destination_C.
– print(‘CalledAETVar includes _DCM3’)
SendToModality(instanceId, ‘Destination_C’)
print(‘DICOM-’, CalledAETVar, ’ - Fwd>DestinationC.')
end

– if string.find(CalledAETVar, ‘_DCM4’) ~= nil then
– – Send *_DCM4 studies to XXXDestination4.
– print(‘CalledAETVar includes _DCM4’)
– SendToModality(instanceId, ‘XXXDestination4’)
– print(‘DICOM-’, CalledAETVar, ’ - Fwd>??.')
– end

if string.find(CalledAETVar, ‘_DCM5’) ~= nil then
– Send *_DCM5 studies to Destination_E.
– print(‘CalledAETVar includes _DCM5’)
SendToModality(instanceId, ‘Destination_E’)
print(‘DICOM-’, CalledAETVar, ’ - Fwd>DestinationE.')
end
– print(‘AETitle DICOM Routing End’)
end

– This Delete is to keep Orthanc clean. Uncomment when not testing.
Delete(instanceId)
print(‘Done —*’)

end

Hi Dustin,

Unfortunately, the /modalites/{id}/store API route does not support the “Transcode” field (the /peers/{id}/store route does !) so you can not force the transfer syntax for a specific C-Store

What are your configurations wrt ?

Another option is to download a modified version of the instance (there, you can use the “Transocode” field: https://book.orthanc-server.com/users/anonymization.html#id3) and then use the “/modalities/{id}/store-straight” route to push it. You can get some inspiration from this script: https://bitbucket.org/osimis/orthanc-setup-samples/src/master/lua-samples/sanitize-and-forward.lua

HTH,

Alain.

Thank you for responding!

Based on that, since we’re using Orthanc Peers on the mobile Orthanc instances, we could theoretically transcode them there differently for each peer? That would still be sending the studies over the mobile network twice, but what would the syntax be for that?

Right now the peers list in the orthanc.json on a mobile unit looks like this:

“Central Server_DCM1” : [ “https://dcm.domain.com”, “USER_DCM1”, “” ],
“Central Server_DCM2” : [ “https://dcm.domain.com”, “USER_DCM2”, “” ],
“Central Server_DCM3” : [ “https://dcm.domain.com”, “USER_DCM3”, “” ],
“Central Server_DCM4” : [ “https://dcm.domain.com”, “USER_DCM4”, “” ],
“Central Server_DCM5” : [ “https://dcm.domain.com”, “USER_DCM5”, “” ]

And the LUA routing script on each unit looks like this:

function OnStoredInstance(instanceId, tags, metadata, origin)
print((origin[‘CalledAet’]), (instanceId), (‘OnStoredInstance’))
– PrintRecursive(origin)

local CalledAETVar = string.upper(origin[‘CalledAet’])
– Extract the value of the “CalledAet” DICOM tag

if origin[‘CalledAet’] ~= nil then
– Skip processing rules if CalledAet is nil.
print(CalledAETVar)

if string.find(CalledAETVar, ‘_DCM1’) ~= nil then
– Send _DCM1 studies to Destination 1.
print(‘Routing to DCM.DOMAIN.COM Destination 1’)
SendToPeer(instanceId, ‘CentralServer_DCM1’)
print(‘SendToPeer - Destination 1 - done…’)
end

if string.find(CalledAETVar, ‘_DCM2’) ~= nil then
– Send _DCM2 studies to Destination 2.
print(‘Routing to DCM.DOMAIN.COM Destination 2’)
SendToPeer(instanceId, ‘CentralServer_DCM2’)
print(‘SendToPeer - Destination 2 - done…’)
end

if string.find(CalledAETVar, ‘_DCM3’) ~= nil then
– Send _DCM3 studies to Destination 3.
print(‘Routing to DCM.DOMAIN.COM Destination 3’)
SendToPeer(instanceId, ‘CentralServer_DCM3’)
print(‘SendToPeer - Destination 3 - done…’)
end

if string.find(CalledAETVar, ‘_DCM4’) ~= nil then
– Send _DCM4 studies to Destination 4.
print(‘Routing to DCM.DOMAIN.COM Destination 4’)
SendToPeer(instanceId, ‘CentralServer_DCM4’)
print(‘SendToPeer - Destination 4 - done…’)
end

if string.find(CalledAETVar, ‘_DCM5’) ~= nil then
– Send _DCM5 studies to Destination 5.
print(‘Routing to DCM.DOMAIN.COM Destination 5’)
SendToPeer(instanceId, ‘CentralServer_DCM5’)
print(‘SendToPeer - Destination 5 - done…’)
end

end
print(‘DICOM Routing Complete’)
end

On the central server, the destinations are just using this format in the Orthanc.json file:

“Destination A” : [ “DESTA_SCP”, “XXX.XXX.XXX.XXX”, 4004 ],
“Destination B” : [ “DESTB_SCP”, “XXX.XXX.XXX.XXX”, 104 ],

I did try adding a block like this to the file, but it did not change the result previously. (Which was likely because the preferred syntax was still J2K… lol) I also wasn’t sure if this could be mixed in with the simpler version of the list, too.

“Destination B” : {
“AET” : “DESTB_SCP”,
“Port” : 104,
“Host” : “XXX.XXX.XXX.XXX”,
“Manufacturer” : “Generic”,
“AllowTranscoding” : true,
}

On the central server, due to confusion trying to make sure studies for Destination A were J2K since that destination does long-term storage, but will not transcode even uncompressed studies, it was previously set to this:
“DicomScuPreferredTransferSyntax” : “1.2.840.10008.1.2.4.90”,

I changed it to this while I was writing the first post up, and tested it before posting:

“DicomScuPreferredTransferSyntax” : “1.2.840.10008.1.2”,

It didn’t resolve the errors from Destination B… However, I missed this setting, which was likely also changed for Dest A, and likely prevents the fall-back functionality from sending the images to Dest B in an uncompressed format:

“TranscodeDicomProtocol” : false,

I’ve just changed that one to true, to see if it will at least allow the J2K studies bound for Dest B to get there in the meantime.

I do still have a second instance of Orthanc on the central server, from earlier efforts before I figured out how to route the peer-to-peer; I was hoping there was something I could do in the config or the LUA to handle it, though.

Sorry, I’m a bit lost in your setup so only answering the question about the format of the /peers/{…}/store request.

The doc is here: https://api.orthanc-server.com/#tag/Networking/paths/~1peers~1{id}~1store/post

In lua, that should look like:

local query = {}
query[“Resources”] = {}
table.insert(query[“Resources”], instanceId)
query[“Transcode”] = “1.2.840.10008.1.2”

local response = RestApiPost(‘/peers/DCMX/store’, DumpJson(query))

Disclaimer: not tested

HTH,

Alain

Thank you! That helps a lot.

As for the setup, the modality connects to the Mobile Orthanc, sending the study while calling ANYTHING_DCM#.

The mobile Orthanc routes to the corresponding peer based on the CalledAETitle. All five peers are the same, but with five different sets of credentials set up for each mobile Orthanc device. LAPTOP##_DCM#

The central server receives the study from the peer, and searched the username for the _DCM#, routing based on that.

It was the only way I could find to allow the central Orthanc instance to route based only on the AE Title at the modality, when using peer-to-peer.