# HG changeset patch # User Sebastien Jodogne # Date 1589985306 -7200 # Node ID 2565d39dd36c5c3a1e83f47fb9bf7e82dfb3fc9c # Parent 5be63aef39b17c643d2aa59b4743ff8b9313b316# Parent ed20ead1a8b6a6435a410711049aabfe0ea00516 integration mainline->c-get diff -r 5be63aef39b1 -r 2565d39dd36c Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.png Binary file Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.png has changed diff -r 5be63aef39b1 -r 2565d39dd36c Plugins/DicomWeb/DicomWeb.py --- a/Plugins/DicomWeb/DicomWeb.py Wed May 20 16:31:54 2020 +0200 +++ b/Plugins/DicomWeb/DicomWeb.py Wed May 20 16:35:06 2020 +0200 @@ -68,7 +68,7 @@ return DoPost(orthanc, uri, body, headers = headers) -def DoGetMultipart(orthanc, uri, headers = {}): +def DoGetMultipart(orthanc, uri, headers = {}, returnHeaders = False): answer = DoGetRaw(orthanc, uri, headers = headers) header = '' @@ -93,6 +93,12 @@ for part in msg.walk(): payload = part.get_payload(decode = True) if payload != None: - result.append(payload) + if returnHeaders: + h = {} + for (key, value) in part.items(): + h[key] = value + result.append((payload, h)) + else: + result.append(payload) return result diff -r 5be63aef39b1 -r 2565d39dd36c Plugins/DicomWeb/Run.py --- a/Plugins/DicomWeb/Run.py Wed May 20 16:31:54 2020 +0200 +++ b/Plugins/DicomWeb/Run.py Wed May 20 16:35:06 2020 +0200 @@ -32,13 +32,15 @@ - +import copy import os import pprint import sys import argparse import unittest import re +from PIL import ImageChops + from DicomWeb import * sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'Tests')) @@ -589,16 +591,19 @@ b = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1)) self.assertEqual(1, len(b)) self.assertEqual(256 * 256 * 2, len(b[0])) + self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(b[0])) c = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type=application/octet-stream' }) self.assertEqual(1, len(c)) self.assertEqual(b[0], c[0]) + self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(c[0])) c = DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type="application/octet-stream"' }) self.assertEqual(1, len(c)) self.assertEqual(b[0], c[0]) + self.assertEqual('ce394eb4d4de4eeef348436108101f3b', ComputeMD5(c[0])) self.assertRaises(Exception, lambda: DoGetMultipart(ORTHANC, '%s/frames/%d' % (uri, 1), headers = { 'Accept' : 'multipart/related; type="nope"' })) @@ -999,6 +1004,14 @@ self.assertEqual(1, len(p)) self.assertEqual(743 * 975 * 3, len(p[0])) + if HasGdcmPlugin(ORTHANC): + self.assertTrue(ComputeMD5(p[0]) in [ + 'b952d67da9ff004b0adae3982e89d620', # GDCM >= 3.0 + 'b3662c4bfa24a0c73abb08548c63319b' # Fallback to DCMTK + ]) + else: + self.assertEqual('b3662c4bfa24a0c73abb08548c63319b', ComputeMD5(p[0])) # DCMTK + def test_bitbucket_issue_168(self): # "Plugins can't read private tags from the configuration @@ -1132,10 +1145,141 @@ self.assertEqual(1, len(DoGet(ORTHANC, u'/dicom-web/studies?PatientName=Гусева*', headers = { 'accept' : 'application/json' }))) - # This line is the isse + # This line is the issue self.assertEqual(1, len(DoGet(ORTHANC, u'/dicom-web/studies?PatientName=гусева*', headers = { 'accept' : 'application/json' }))) + + def test_transcoding(self): + ACCEPT = { + '1.2.840.10008.1.2' : 'multipart/related; type=application/octet-stream; transfer-syntax=1.2.840.10008.1.2', + '1.2.840.10008.1.2.1' : 'multipart/related; type=application/octet-stream; transfer-syntax=1.2.840.10008.1.2.1', + '1.2.840.10008.1.2.4.50' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.50', + '1.2.840.10008.1.2.4.51' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.51', + '1.2.840.10008.1.2.4.57' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.57', + '1.2.840.10008.1.2.4.70' : 'multipart/related; type=image/jpeg; transfer-syntax=1.2.840.10008.1.2.4.70', + } + + uri = '/dicom-web%s' % UploadAndGetWadoPath('TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm') + truth = Image.open(GetDatabasePath('TransferSyntaxes/1.2.840.10008.1.2.4.50.png')) + + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, + headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }, + returnHeaders = True) + self.assertEqual(1, len(a)) + self.assertEqual(2, len(a[0])) + self.assertEqual('http://localhost:8042%s/frames/1' % uri, + a[0][1]['Content-Location']) + self.assertEqual(ACCEPT['1.2.840.10008.1.2.4.50'], + 'multipart/related; type=%s' % a[0][1]['Content-Type']) + self.assertEqual(53476, len(a[0][0])) + self.assertEqual('142fdb8a1dc2aa7e6b8952aa294a6e22', ComputeMD5(a[0][0])) + + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri) + self.assertEqual(1, len(a)) + self.assertEqual(480 * 640 * 3, len(a[0])) + + # http://effbot.org/zone/pil-comparing-images.htm + img = Image.frombytes('RGB', [ 640, 480 ], a[0]) + self.assertLessEqual(GetMaxImageDifference(img, truth), 2) + + ACCEPT2 = copy.deepcopy(ACCEPT) + if HasGdcmPlugin(ORTHANC): + IS_GDCM = True + ACCEPT2['1.2.840.10008.1.2.1'] = 'multipart/related; type=application/octet-stream' + del ACCEPT2['1.2.840.10008.1.2'] + else: + self.assertEqual('dfdc79f5070926bbb8ac079ee91f5b91', ComputeMD5(a[0])) + IS_GDCM = False + + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, + headers = { 'Accept' : ACCEPT2['1.2.840.10008.1.2.1'] }) + self.assertEqual(1, len(a)) + self.assertEqual(480 * 640 * 3, len(a[0])) + + img = Image.frombytes('RGB', [ 640, 480 ], a[0]) + self.assertLessEqual(GetMaxImageDifference(img, truth), 2) + + if not IS_GDCM: + self.assertEqual('dfdc79f5070926bbb8ac079ee91f5b91', ComputeMD5(a[0])) + + + # Test download using the same transfer syntax + RESULTS = { + '1.2.840.10008.1.2' : 'f54c7ea520ab3ec32b6303581ecd262f', + '1.2.840.10008.1.2.1' : '4b350b9353a93c747917c7c3bf9b8f44', + '1.2.840.10008.1.2.4.50' : '142fdb8a1dc2aa7e6b8952aa294a6e22', + '1.2.840.10008.1.2.4.51' : '8b37945d75f9d2899ed868bdba429a0d', + '1.2.840.10008.1.2.4.57' : '75c84823eddb560d127b1d24c9406f30', + '1.2.840.10008.1.2.4.70' : '2c35726328f0200396e583a0038b0269', + } + + if IS_GDCM: + # This file was failing with GDCM, as it has 2 fragments, + # and only the first one was returned => the MD5 below is BAD + #RESULTS['1.2.840.10008.1.2.4.51'] = '901963a322a817946b074f9ed0afa060' + pass + + for syntax in ACCEPT2: + uri = '/dicom-web%s' % UploadAndGetWadoPath('TransferSyntaxes/%s.dcm' % syntax) + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, + headers = { 'Accept' : ACCEPT2[syntax] }) + self.assertEqual(1, len(a)) + self.assertEqual(RESULTS[syntax], ComputeMD5(a[0])) + + # Test transcoding to all the possible transfer syntaxes + uri = '/dicom-web%s' % UploadAndGetWadoPath('KarstenHilbertRF.dcm') + for syntax in ACCEPT2: + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, + headers = { 'Accept' : ACCEPT2[syntax] }, + returnHeaders = True) + self.assertEqual(1, len(a)) + self.assertEqual(2, len(a[0])) + self.assertEqual('http://localhost:8042%s/frames/1' % uri, + a[0][1]['Content-Location']) + self.assertEqual(ACCEPT[syntax], + 'multipart/related; type=%s' % a[0][1]['Content-Type']) + if IS_GDCM: + self.assertEqual({ + '1.2.840.10008.1.2' : '1c8cebde0c74450ce4dfb75dd52ddad7', + '1.2.840.10008.1.2.1' : '1c8cebde0c74450ce4dfb75dd52ddad7', + '1.2.840.10008.1.2.4.50' : 'f4d145e5f33fbd39375ce0f91453d6cc', + '1.2.840.10008.1.2.4.51' : 'f4d145e5f33fbd39375ce0f91453d6cc', + '1.2.840.10008.1.2.4.57' : 'dc55800ce1a8ac556c266cdb26d75757', + '1.2.840.10008.1.2.4.70' : 'dc55800ce1a8ac556c266cdb26d75757', + } [syntax], ComputeMD5(a[0][0])) + else: + self.assertEqual({ + '1.2.840.10008.1.2' : '1c8cebde0c74450ce4dfb75dd52ddad7', + '1.2.840.10008.1.2.1' : '1c8cebde0c74450ce4dfb75dd52ddad7', + '1.2.840.10008.1.2.4.50' : '0a0ab74fe7c68529bdd416fc9e5e742a', + '1.2.840.10008.1.2.4.51' : '33d1ab2fe169c5b5ba932a9bbc3c6306', + '1.2.840.10008.1.2.4.57' : '3d21c969da846ca41e0498a0dcfad061', + '1.2.840.10008.1.2.4.70' : '49d5353c8673208629847ad45a855557', + } [syntax], ComputeMD5(a[0][0])) + + + # JPEG image with many fragments for 2 frames + uri = '/dicom-web%s' % UploadAndGetWadoPath('LenaTwiceWithFragments.dcm') + + a = DoGetMultipart(ORTHANC, '%s/frames/1' % uri, + headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) + self.assertEqual(1, len(a)) + self.assertEqual(69214, len(a[0])) + self.assertEqual('0eaf36d4881c513ca70b6684bfaa5b08', ComputeMD5(a[0])) + + b = DoGetMultipart(ORTHANC, '%s/frames/2' % uri, + headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) + self.assertEqual(1, len(b)) + self.assertEqual(a[0], b[0]) + + b = DoGetMultipart(ORTHANC, '%s/frames/1,2' % uri, + headers = { 'Accept' : ACCEPT['1.2.840.10008.1.2.4.50'] }) + self.assertEqual(2, len(b)) + self.assertEqual(a[0], b[0]) + self.assertEqual(a[0], b[1]) + + try: print('\nStarting the tests...') diff -r 5be63aef39b1 -r 2565d39dd36c Tests/Tests.py --- a/Tests/Tests.py Wed May 20 16:31:54 2020 +0200 +++ b/Tests/Tests.py Wed May 20 16:35:06 2020 +0200 @@ -141,7 +141,6 @@ ] - class Orthanc(unittest.TestCase): def setUp(self): if (sys.version_info >= (3, 0)): @@ -1631,7 +1630,8 @@ # gdcmconv -i /home/jodogne/DICOM/GdcmDatabase/US_DataSet/HDI5000_US/3EAF5E01 -w -o Issue19.dcm a = UploadInstance(_REMOTE, 'Issue19.dcm')['ID'] - self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/941ad3c8-05d05b88-560459f9-0eae0e20-6cddd533/preview')) + if not HasGdcmPlugin(_REMOTE): + self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/941ad3c8-05d05b88-560459f9-0eae0e20-6cddd533/preview')) def test_googlecode_issue_37(self): @@ -3074,8 +3074,11 @@ Check('1.2.840.10008.1.2.4.81', '801579ae7cbf28e604ea74f2c99fa2ca') Check('1.2.840.10008.1.2.5', '6ff51ae525d362e0d04f550a64075a0e') # RLE, supported since Orthanc 1.0.1 Check('1.2.840.10008.1.2', 'd54aed9f67a100984b42942cc2e9939b') - Check('1.2.840.10008.1.2.4.90', None) # JPEG-2000 image, not supported - Check('1.2.840.10008.1.2.4.91', None) # JPEG-2000 image, not supported + + # JPEG2k image, not supported without GDCM plugin + if not HasGdcmPlugin(_REMOTE): + Check('1.2.840.10008.1.2.4.90', None) + Check('1.2.840.10008.1.2.4.91', None) def test_raw_frame(self): @@ -5511,29 +5514,52 @@ self.assertEqual('NORMAL', tags['1337,1001']['Value']) - def test_modify_transcode(self): + def test_modify_transcode_instance(self): i = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm')['ID'] self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax( DoGet(_REMOTE, '/instances/%s/file' % i))) - for syntax in [ - '1.2.840.10008.1.2', - '1.2.840.10008.1.2.1', - #'1.2.840.10008.1.2.1.99', # Deflated Explicit VR Little Endian - '1.2.840.10008.1.2.2', - '1.2.840.10008.1.2.4.50', - '1.2.840.10008.1.2.4.51', - '1.2.840.10008.1.2.4.57', - '1.2.840.10008.1.2.4.70', - #'1.2.840.10008.1.2.4.80', # This makes DCMTK 3.6.2 crash - #'1.2.840.10008.1.2.4.81', # This makes DCMTK 3.6.2 crash - ]: + a = ExtractDicomTags(DoGet(_REMOTE, '/instances/%s/file' % i), [ 'SOPInstanceUID' ]) [0] + self.assertTrue(len(a) > 20) + + SYNTAXES = [ + '1.2.840.10008.1.2', + '1.2.840.10008.1.2.1', + #'1.2.840.10008.1.2.1.99', # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) + '1.2.840.10008.1.2.2', + '1.2.840.10008.1.2.4.50', + '1.2.840.10008.1.2.4.51', + '1.2.840.10008.1.2.4.57', + '1.2.840.10008.1.2.4.70', + ] + + if HasGdcmPlugin(_REMOTE): + SYNTAXES = SYNTAXES + [ + '1.2.840.10008.1.2.4.80', # This makes DCMTK 3.6.2 crash + '1.2.840.10008.1.2.4.81', # This makes DCMTK 3.6.2 crash + '1.2.840.10008.1.2.4.90', # JPEG2k, unavailable without GDCM + '1.2.840.10008.1.2.4.91', # JPEG2k, unavailable without GDCM + ] + + for syntax in SYNTAXES: transcoded = DoPost(_REMOTE, '/instances/%s/modify' % i, { 'Transcode' : syntax, + 'Keep' : [ 'SOPInstanceUID' ], + 'Force' : True, }) self.assertEqual(syntax, GetTransferSyntax(transcoded)) + b = ExtractDicomTags(transcoded, [ 'SOPInstanceUID' ]) [0] + self.assertTrue(len(b) > 20) + if syntax in [ '1.2.840.10008.1.2.4.50', + '1.2.840.10008.1.2.4.51', + '1.2.840.10008.1.2.4.81', + '1.2.840.10008.1.2.4.91' ]: + # Lossy transcoding: The SOP instance UID must have changed + self.assertNotEqual(a, b) + else: + self.assertEqual(a, b) def test_archive_transcode(self): info = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm') @@ -5634,8 +5660,99 @@ self.assertEqual(2, len(z.namelist())) self.assertEqual('1.2.840.10008.1.2.4.57', GetTransferSyntax(z.read('IMAGES/IM0'))) + + def test_modify_keep_source(self): + # https://groups.google.com/d/msg/orthanc-users/CgU-Wg8vDio/BY5ZWcDEAgAJ + i = UploadInstance(_REMOTE, 'DummyCT.dcm') + self.assertEqual(1, len(DoGet(_REMOTE, '/studies'))) + + j = DoPost(_REMOTE, '/studies/%s/modify' % i['ParentStudy'], { + 'Replace' : { + 'StationName' : 'TEST', + }, + 'KeepSource' : True, + }) + + s = DoGet(_REMOTE, '/studies') + self.assertEqual(2, len(s)) + self.assertTrue(i['ParentStudy'] in s) + self.assertTrue(j['ID'] in s) + + DoDelete(_REMOTE, '/studies/%s' % j['ID']) + self.assertEqual(1, len(DoGet(_REMOTE, '/studies'))) + + j = DoPost(_REMOTE, '/studies/%s/modify' % i['ParentStudy'], { + 'Replace' : { + 'StationName' : 'TEST', + }, + 'KeepSource' : False, + }) + + s = DoGet(_REMOTE, '/studies') + self.assertEqual(1, len(s)) + self.assertFalse(i['ParentStudy'] in s) + self.assertTrue(j['ID'] in s) + + + def test_modify_transcode_study(self): + i = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm') + self.assertEqual('1.2.840.10008.1.2.1', GetTransferSyntax( + DoGet(_REMOTE, '/instances/%s/file' % i['ID']))) + + self.assertEqual(1, len(DoGet(_REMOTE, '/instances'))) + j = DoPost(_REMOTE, '/studies/%s/modify' % i['ParentStudy'], { + 'Transcode' : '1.2.840.10008.1.2.4.50', + 'KeepSource' : False + }) + + k = DoGet(_REMOTE, '/instances') + self.assertEqual(1, len(k)) + self.assertEqual(i['ID'], DoGet(_REMOTE, '/instances/%s/metadata?expand' % k[0]) ['ModifiedFrom']) + self.assertEqual('1.2.840.10008.1.2.4.50', GetTransferSyntax( + DoGet(_REMOTE, '/instances/%s/file' % k[0]))) + def test_store_peer_transcoding(self): + i = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm')['ID'] + + SYNTAXES = [ + '1.2.840.10008.1.2', + '1.2.840.10008.1.2.1', + #'1.2.840.10008.1.2.1.99', # Deflated Explicit VR Little Endian (cannot be decoded in debug mode if Orthanc is statically linked against DCMTK 3.6.5) + '1.2.840.10008.1.2.2', + '1.2.840.10008.1.2.4.50', + '1.2.840.10008.1.2.4.51', + '1.2.840.10008.1.2.4.57', + '1.2.840.10008.1.2.4.70', + ] + + if HasGdcmPlugin(_REMOTE): + SYNTAXES = SYNTAXES + [ + '1.2.840.10008.1.2.4.80', # This makes DCMTK 3.6.2 crash + '1.2.840.10008.1.2.4.81', # This makes DCMTK 3.6.2 crash + '1.2.840.10008.1.2.4.90', # JPEG2k, unavailable without GDCM + '1.2.840.10008.1.2.4.91', # JPEG2k, unavailable without GDCM + ] + + for syntax in SYNTAXES: + body = { + 'Resources' : [ i ], + } + + if syntax != '1.2.840.10008.1.2.1': + body['Transcode'] = syntax + + self.assertEqual(0, len(DoGet(_LOCAL, '/instances'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/instances'))) + DoPost(_REMOTE, '/peers/peer/store', body, 'text/plain') + self.assertEqual(1, len(DoGet(_LOCAL, '/instances'))) + self.assertEqual(1, len(DoGet(_REMOTE, '/instances'))) + self.assertEqual(syntax, GetTransferSyntax( + DoGet(_LOCAL, '/instances/%s/file' % DoGet(_LOCAL, '/instances') [0]))) + + DropOrthanc(_LOCAL) + + def test_getscu(self): def CleanTarget(): if os.path.isdir('/tmp/GETSCU'): @@ -5686,6 +5803,3 @@ os.system('ls -l /tmp/GETSCU') self.assertTrue(os.path.isfile('/tmp/GETSCU/MR.1.3.46.670589.11.0.0.11.4.2.0.8743.5.5396.2006120114314079549')) self.assertTrue(os.path.isfile('/tmp/GETSCU/MR.1.2.276.0.7230010.3.1.4.2831176407.19977.1434973482.75579')) - - - diff -r 5be63aef39b1 -r 2565d39dd36c Tests/Toolbox.py --- a/Tests/Toolbox.py Wed May 20 16:31:54 2020 +0200 +++ b/Tests/Toolbox.py Wed May 20 16:35:06 2020 +0200 @@ -32,7 +32,10 @@ import time import zipfile -from PIL import Image +from PIL import Image, ImageChops +import math +import operator + if (sys.version_info >= (3, 0)): from urllib.parse import urlencode @@ -389,3 +392,35 @@ data = subprocess.check_output([ FindExecutable('dcm2xml'), f.name ]) return re.search('