changeset 21:2a29bcff60a7

tests of image decoding
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 22 Jun 2015 14:14:37 +0200
parents f3d08a75a636
children 8f4b70c89467
files Database/Formats/Brainix.png Database/Formats/Generate.sh Database/Formats/Jpeg.dcm Database/Formats/JpegLossless.dcm Database/Formats/KarstenHilbertRF.png Database/Formats/Multiframe0.png Database/Formats/Multiframe75.png Database/Formats/Rle.dcm Database/Formats/SignedCT.png Database/Lua/TransferSyntaxDisable.lua Database/Lua/TransferSyntaxEnable.lua Database/README.txt Database/SignedCT.dcm GenerateConfigurationForTests.py Tests/Run.py Tests/Tests.py Tests/Toolbox.py
diffstat 17 files changed, 273 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
Binary file Database/Formats/Brainix.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Database/Formats/Generate.sh	Mon Jun 22 14:14:37 2015 +0200
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+set -e
+
+# http://gdcm.sourceforge.net/html/gdcmconv.html
+
+gdcmconv -i ../Brainix/Epi/IM-0001-0001.dcm -o JpegLossless.dcm -L
+gdcmconv -i ../Brainix/Epi/IM-0001-0001.dcm -o Jpeg.dcm -J
+gdcmconv -i ../Brainix/Epi/IM-0001-0001.dcm -o Rle.dcm -R
+
+# Generate study/series/sop instance UID++++
+dcmodify -e '(0008,0005)' -m '(0010,0020)=FromGDCM' -gin -gst -gse JpegLossless.dcm 
+dcmodify -e '(0008,0005)' -m '(0010,0020)=FromGDCM' -gin -gst -gse Jpeg.dcm 
+dcmodify -e '(0008,0005)' -m '(0010,0020)=FromGDCM' -gin -gst -gse Rle.dcm 
+
+rm -f JpegLossless.dcm.bak Jpeg.dcm.bak Rle.dcm.bak
+
+gdcmraw -t PixelData ../Brainix/Epi/IM-0001-0001.dcm PixelData.raw
+convert -define png:include-chunks=none -define png:compression-level=9 -size 256x256 -depth 16 gray:PixelData.raw Brainix.png
+
+gdcmraw -t PixelData ../KarstenHilbertRF.dcm PixelData.raw
+convert -define png:include-chunks=none -define png:compression-level=9 -size 512x464 -depth 8 gray:PixelData.raw KarstenHilbertRF.png
+
+# Decompress the multiframe image
+gdcmconv -w ../Multiframe.dcm tmp.dcm
+gdcmraw -t PixelData ./tmp.dcm PixelData.raw
+SIZE=$((512*512))
+dd if=PixelData.raw of=PixelData2.raw bs=$SIZE count=1 skip=0 &> /dev/null
+convert -define png:include-chunks=none -define png:compression-level=9 -size 512x512 -depth 8 gray:PixelData2.raw Multiframe0.png
+dd if=PixelData.raw of=PixelData2.raw bs=$SIZE count=1 skip=75 &> /dev/null
+convert -define png:include-chunks=none -define png:compression-level=9 -size 512x512 -depth 8 gray:PixelData2.raw Multiframe75.png
+
+# Decompress the signed CT image, ignoring the fact that the data is signed
+gdcmraw -t PixelData ../SignedCT.dcm PixelData.raw
+convert -define png:include-chunks=none -define png:compression-level=9 -size 512x512 -depth 16 gray:PixelData.raw SignedCT.png
+
+rm -f PixelData.raw PixelData2.raw tmp.dcm
Binary file Database/Formats/Jpeg.dcm has changed
Binary file Database/Formats/JpegLossless.dcm has changed
Binary file Database/Formats/KarstenHilbertRF.png has changed
Binary file Database/Formats/Multiframe0.png has changed
Binary file Database/Formats/Multiframe75.png has changed
Binary file Database/Formats/Rle.dcm has changed
Binary file Database/Formats/SignedCT.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Database/Lua/TransferSyntaxDisable.lua	Mon Jun 22 14:14:37 2015 +0200
@@ -0,0 +1,29 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+print('All special transfer syntaxes are now disallowed')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Database/Lua/TransferSyntaxEnable.lua	Mon Jun 22 14:14:37 2015 +0200
@@ -0,0 +1,29 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+print('All special transfer syntaxes are now accepted')
--- a/Database/README.txt	Mon Jun 22 09:47:25 2015 +0200
+++ b/Database/README.txt	Mon Jun 22 14:14:37 2015 +0200
@@ -56,3 +56,4 @@
 - PilatesArgenturGEUltrasoundOsiriX.dcm: From https://groups.google.com/d/msg/orthanc-users/m3zQLyl_jNc/TUrR462UKSMJ
 - PrivateMDNTags.dcm : From University Hospital of Liege
 - PrivateTags.dcm : From GDCM, "images_of_interest/494APG9K.dcm"
+- SignedCT.dcm : From Sébastien Jodogne.
Binary file Database/SignedCT.dcm has changed
--- a/GenerateConfigurationForTests.py	Mon Jun 22 09:47:25 2015 +0200
+++ b/GenerateConfigurationForTests.py	Mon Jun 22 14:14:37 2015 +0200
@@ -74,6 +74,7 @@
 config = re.sub(r'("DicomAet"\s*:)\s*".*?"', r'\1 "ORTHANC"', config)
 config = re.sub(r'("RemoteAccessAllowed"\s*:)\s*false', r'\1 true', config)
 config = re.sub(r'("AuthenticationEnabled"\s*:)\s*false', r'\1 true', config)
+config = re.sub(r'("DicomAssociationCloseDelay"\s*:)\s*[0-9]*', r'\1 0', config)
 config = re.sub(r'("RegisteredUsers"\s*:)\s*{', r'\1 { "alice" : "orthanctest"', config)
 config = re.sub(r'("DicomModalities"\s*:)\s*{', r'\1 { "orthanctest" : [ "%s", "%s", %d ]' % 
                 ('ORTHANCTEST', ip, 5001), config)
--- a/Tests/Run.py	Mon Jun 22 09:47:25 2015 +0200
+++ b/Tests/Run.py	Mon Jun 22 14:14:37 2015 +0200
@@ -84,7 +84,7 @@
 if args.docker:
     args.server = GetDockerHostAddress()
 
-CONFIG = '/tmp/Configuration.json'
+CONFIG = '/tmp/IntegrationTestsConfiguration.json'
 subprocess.check_call([ 'Orthanc', '--config=%s' % CONFIG ])
 
 with open(CONFIG, 'rt') as f:
@@ -98,6 +98,7 @@
 config = re.sub(r'("RemoteAccessAllowed"\s*:)\s*false', r'\1 true', config)
 config = re.sub(r'("AuthenticationEnabled"\s*:)\s*false', r'\1 true', config)
 config = re.sub(r'("RegisteredUsers"\s*:)\s*{', r'\1 { "alice" : "orthanctest"', config)
+config = re.sub(r'("DicomAssociationCloseDelay"\s*:)\s*[0-9]*', r'\1 0', config)
 config = re.sub(r'("DicomModalities"\s*:)\s*{', r'\1 { "orthanc" : [ "%s", "%s", "%s" ]' % 
                 (args.aet, args.server, args.dicom), config)
 
--- a/Tests/Tests.py	Mon Jun 22 09:47:25 2015 +0200
+++ b/Tests/Tests.py	Mon Jun 22 14:14:37 2015 +0200
@@ -59,11 +59,31 @@
     DoPost(_REMOTE, '/tools/execute-script', 'function OnStoredInstance() end', 'application/lua')
 
 
+def CompareLists(a, b):
+    if len(a) != len(b):
+        return False
+
+    for i in range(len(a)):
+        d = a[i] - b[i]
+        if abs(d) >= 0.51:  # Add some tolerance for rounding errors
+            return False
+
+    return True
+
+
+
+
 
 class Orthanc(unittest.TestCase):
     def setUp(self):
         DropOrthanc(_LOCAL)
         DropOrthanc(_REMOTE)
+        #print "In test", self._testMethodName
+
+    def AssertSameImages(self, truth, url):
+        im = GetImage(_REMOTE, url)
+        self.assertTrue(CompareLists(truth, im.getdata()))
+
 
     def test_system(self):
         self.assertTrue('Version' in DoGet(_REMOTE, '/system'))
@@ -243,7 +263,7 @@
         self.assertEqual('0', DoGet(_REMOTE, '/statistics')['TotalUncompressedSize'])
 
 
-    def test_multi_frame(self):
+    def test_multiframe(self):
         i = UploadInstance(_REMOTE, 'Multiframe.dcm')['ID']
         self.assertEqual(76, len(DoGet(_REMOTE, '/instances/%s/frames' % i)))
 
@@ -1322,14 +1342,11 @@
 
 
     def test_issue_19(self):
-        # This is an image with "YBR_FULL" photometric interpretation
+        # This is an image with "YBR_FULL" photometric interpretation, it is not supported by Orthanc
         # gdcmconv -i /home/jodogne/DICOM/GdcmDatabase/US_DataSet/HDI5000_US/3EAF5E01 -w -o Issue19.dcm
 
         a = UploadInstance(_REMOTE, 'Issue19.dcm')['ID']
-        i = DoGet(_REMOTE, '/instances/941ad3c8-05d05b88-560459f9-0eae0e20-6cddd533/preview')
-
-        # eb5d156c3594497f589158a6c6f3ca51 <=> Unsupported.png
-        self.assertEqual('eb5d156c3594497f589158a6c6f3ca51', ComputeMD5(i))
+        self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/941ad3c8-05d05b88-560459f9-0eae0e20-6cddd533/preview'))
 
 
     def test_issue_37(self):
@@ -1800,3 +1817,151 @@
         self.assertEqual('hello', DoGet(_REMOTE, '/instances/%s/content/StudyDescription' % b).strip())
         self.assertEqual('world', DoGet(_REMOTE, '/instances/%s/content/Modality' % b).strip())
 
+
+    def test_incoming_jpeg(self):
+        def storescu():
+            with open(os.devnull, 'w') as FNULL:
+                subprocess.check_call([ 'storescu', '-xs',
+                                        _REMOTE['Server'], str(_REMOTE['DicomPort']),
+                                        GetDatabasePath('Knix/Loc/IM-0001-0001.dcm') ],
+                                      stderr = FNULL)
+
+        self.assertEqual(0, len(DoGet(_REMOTE, '/patients')))
+        InstallLuaScript('Lua/TransferSyntaxDisable.lua')
+        self.assertRaises(Exception, storescu)
+        self.assertEqual(0, len(DoGet(_REMOTE, '/patients')))
+        InstallLuaScript('Lua/TransferSyntaxEnable.lua')
+        storescu()
+        self.assertEqual(1, len(DoGet(_REMOTE, '/patients')))
+
+
+    def test_storescu_jpeg(self):
+        self.assertEqual(0, len(DoGet(_REMOTE, '/exports')['Exports']))
+
+        knixStudy = 'b9c08539-26f93bde-c81ab0d7-bffaf2cb-a4d0bdd0'
+        UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0001.dcm')
+        UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0002.dcm')
+        UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0003.dcm')
+
+        a = UploadInstance(_REMOTE, 'Brainix/Flair/IM-0001-0001.dcm')['ID']
+        b = UploadInstance(_REMOTE, 'ColorTestImageJ.dcm')['ID']
+        self.assertEqual(0, len(DoGet(_LOCAL, '/instances')))
+        DoPost(_REMOTE, '/modalities/orthanctest/store', [ knixStudy, a, b ])
+        self.assertEqual(5, len(DoGet(_LOCAL, '/instances')))
+
+        self.assertEqual(3, len(DoGet(_REMOTE, '/exports')['Exports']))
+
+        DropOrthanc(_REMOTE)
+        self.assertEqual(0, len(DoGet(_REMOTE, '/exports')['Exports']))
+
+
+    def test_pixel_data(self):
+        jpeg = UploadInstance(_REMOTE, 'Knix/Loc/IM-0001-0001.dcm')['ID']
+        color = UploadInstance(_REMOTE, 'ColorTestImageJ.dcm')['ID']
+        phenix = UploadInstance(_REMOTE, 'Phenix/IM-0001-0001.dcm')['ID']
+        
+        phenixSize = 358 * 512 * 2
+        colorSize = 1000 * 1000 * 3
+        jpegSize = 51918
+
+        self.assertEqual(1, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010' % phenix)))
+        self.assertEqual(1, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010' % color)))
+        self.assertEqual(2, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010' % jpeg)))
+        
+        self.assertEqual(0, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010/0' % jpeg)))
+        self.assertEqual(jpegSize, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010/1' % jpeg)))       
+
+        self.assertEqual(phenixSize, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010/0' % phenix)))
+        self.assertEqual(colorSize, len(DoGet(_REMOTE, '/instances/%s/content/7fe0-0010/0' % color)))
+
+
+    def test_decode_brainix(self):
+        brainix = [
+            UploadInstance(_REMOTE, 'Brainix/Epi/IM-0001-0001.dcm')['ID'], # (*)
+            UploadInstance(_REMOTE, 'Formats/JpegLossless.dcm')['ID'],  # JPEG-LS, same as (*) (since Orthanc 0.7.6)
+            UploadInstance(_REMOTE, 'Formats/Jpeg.dcm')['ID'],  # JPEG, same as (*) (since Orthanc 0.7.6)
+            ]
+        h = '6fb11b932d535c2be04beabd99793ff8'
+        maxValue = 426.0
+
+        truth = Image.open(GetDatabasePath('Formats/Brainix.png'))
+        for i in brainix:
+            self.AssertSameImages(truth.getdata(), '/instances/%s/image-int16' % i)
+            self.AssertSameImages(truth.getdata(), '/instances/%s/image-uint16' % i)
+
+        truth2 = map(lambda x: min(255, x), truth.getdata())
+        for i in brainix:
+            self.AssertSameImages(truth2, '/instances/%s/image-uint8' % i)
+
+        truth2 = map(lambda x: x * 255.0 / maxValue, truth.getdata())
+        for i in brainix:
+            self.AssertSameImages(truth2, '/instances/%s/preview' % i)
+
+        for i in brainix:
+            self.assertEqual(h, ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % i)))
+
+
+    def test_decode_color(self):
+        imagej = UploadInstance(_REMOTE, 'ColorTestImageJ.dcm')['ID']
+        color = UploadInstance(_REMOTE, 'ColorTestMalaterre.dcm')['ID']
+
+        for i in [ imagej, color ]:
+            for j in [ 'image-uint8', 'image-uint16', 'image-int16' ]:
+                self.assertRaises(Exception, lambda: DoGet(_REMOTE, '/instances/%s/%s' % (i, j)))
+
+        self.assertEqual('c14c687f7a1ea9fe022479fc87c67274', ComputeMD5(DoGet(_REMOTE, '/instances/%s/preview' % imagej)))
+        self.assertEqual('a87d122918a56f803bcfe9d2586b9125', ComputeMD5(DoGet(_REMOTE, '/instances/%s/preview' % color)))
+
+        self.assertEqual('30cc46bfa7aba77a40e4178f6184c25a', ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % imagej)))
+        self.assertEqual('ff195005cef06b59666fd220a9b4cd9a', ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % color)))
+
+
+    def test_decode_rf(self):
+        rf = UploadInstance(_REMOTE, 'KarstenHilbertRF.dcm')['ID']
+        truth = Image.open(GetDatabasePath('Formats/KarstenHilbertRF.png'))
+
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-uint8' % rf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-uint16' % rf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-int16' % rf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/preview' % rf)
+
+        self.assertEqual('42254d70efd2f4a1b8f3455909689f0e', ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % rf)))
+
+
+    def test_decode_multiframe(self):
+        mf = UploadInstance(_REMOTE, 'Multiframe.dcm')['ID']
+
+        # Test the first frame
+        truth = Image.open(GetDatabasePath('Formats/Multiframe0.png'))
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-uint8' % mf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-uint16' % mf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-int16' % mf)
+        self.AssertSameImages(truth.getdata(), '/instances/%s/preview' % mf)
+        self.assertEqual('9812b99d93bbcd4e7684ded089b5dfb3', ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % mf)))
+
+        self.AssertSameImages(truth.getdata(), '/instances/%s/frames/0/image-uint16' % mf)
+
+        # Test the last frame
+        truth = Image.open(GetDatabasePath('Formats/Multiframe75.png'))
+        self.AssertSameImages(truth.getdata(), '/instances/%s/frames/75/image-uint16' % mf)
+
+
+    def test_decode_signed(self):
+        signed = UploadInstance(_REMOTE, 'SignedCT.dcm')['ID']
+        minValue = -2000
+        maxValue = 4042
+
+        truth = Image.open(GetDatabasePath('Formats/SignedCT.png'))
+        self.AssertSameImages(truth.getdata(), '/instances/%s/image-int16' % signed)
+
+        truth2 = map(lambda x: 0 if x >= 32768 else x, truth.getdata())
+        self.AssertSameImages(truth2, '/instances/%s/image-uint16' % signed)
+
+        truth3 = map(lambda x: 255 if x >= 256 else x, truth2)
+        self.AssertSameImages(truth3, '/instances/%s/image-uint8' % signed)
+
+        tmp = map(lambda x: x - 65536 if x >= 32768 else x, truth.getdata())
+        tmp = map(lambda x: (255.0 * (x - minValue)) / (maxValue - minValue), tmp)
+        self.AssertSameImages(tmp, '/instances/%s/preview' % signed)
+
+        self.assertEqual('b57e6c872a3da50877c7da689b03a444', ComputeMD5(DoGet(_REMOTE, '/instances/%s/matlab' % signed)))
--- a/Tests/Toolbox.py	Mon Jun 22 09:47:25 2015 +0200
+++ b/Tests/Toolbox.py	Mon Jun 22 14:14:37 2015 +0200
@@ -77,6 +77,7 @@
         d = '?' + urlencode(data)
 
     http = httplib2.Http()
+    http.follow_redirects = False
     _SetupCredentials(orthanc, http)
 
     resp, content = http.request(orthanc['Url'] + uri + d, 'GET', body = body,
@@ -91,6 +92,7 @@
 
 def _DoPutOrPost(orthanc, uri, method, data, contentType, headers):
     http = httplib2.Http()
+    http.follow_redirects = False
     _SetupCredentials(orthanc, http)
 
     if isinstance(data, str):
@@ -116,6 +118,7 @@
 
 def DoDelete(orthanc, uri):
     http = httplib2.Http()
+    http.follow_redirects = False
     _SetupCredentials(orthanc, http)
 
     resp, content = http.request(orthanc['Url'] + uri, 'DELETE')