├── .gitignore ├── DummyLoader ├── .gitignore ├── AppID.rgs ├── DummyLoader.def ├── DummyLoader.idl ├── DummyLoader.rc ├── DummyLoader.vcxproj ├── DummyLoader.vcxproj.filters ├── GenRgsFiles.py ├── Image3dFileLoader.cpp ├── Image3dFileLoader.hpp ├── Image3dSource.cpp ├── Image3dSource.hpp ├── Image3dStream.cpp ├── Image3dStream.hpp ├── LinAlg.hpp ├── Main.cpp ├── Resource.h └── UNREGISTER_DummyLoader.bat ├── Guidelines.md ├── Image3dAPI.sln ├── Image3dAPI ├── .gitignore ├── ComSupport.hpp ├── IImage3d.idl ├── IImage3dTypeLibraryGenerator.idl ├── Image3dAPI.vcxproj ├── Image3dAPI.vcxproj.filters ├── RegistryCheck.hpp └── UNREGISTER_Image3dAPI.bat ├── LICENSE.txt ├── PackagingGE ├── BuildNuGet.bat ├── DetermineNextTag.py ├── DummyLoader.redist.nuspec ├── DummyLoader.redist.targets ├── Image3dAPI.net.targets ├── Image3dAPI.nuspec ├── Image3dAPI.targets ├── PackagePublishNuget.bat └── SetNuspecVersion.py ├── README.md ├── RegFreeTest ├── .gitignore ├── Main.cpp ├── RegFreeTest.vcxproj └── RegFreeTest.vcxproj.filters ├── SandboxTest ├── .gitignore ├── LowIntegrity.hpp ├── Main.cpp ├── SandboxTest.vcxproj └── SandboxTest.vcxproj.filters ├── TestPython ├── .gitignore ├── ITKExport.py ├── TestPython.py ├── TestPython.pyproj └── utils.py └── TestViewer ├── .gitignore ├── App.config ├── App.xaml ├── App.xaml.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings └── TestViewer.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /x64 3 | /Win32 4 | /*.VC.db 5 | /*.VC.VC.opendb 6 | *.nupkg 7 | NEW_TAG.txt 8 | PREV_TAG.txt 9 | changelog.txt 10 | -------------------------------------------------------------------------------- /DummyLoader/.gitignore: -------------------------------------------------------------------------------- 1 | /x64 2 | /Win32 3 | /*.rgs 4 | /DummyLoader.h 5 | /DummyLoader_i.c 6 | -------------------------------------------------------------------------------- /DummyLoader/AppID.rgs: -------------------------------------------------------------------------------- 1 | HKCR 2 | { 3 | NoRemove AppID 4 | { 5 | ForceRemove '%APPID%' = s 'Image3dFileLoader Object' 6 | { 7 | val DllSurrogate = s '' 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DummyLoader/DummyLoader.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | DllCanUnloadNow PRIVATE 3 | DllGetClassObject PRIVATE 4 | DllRegisterServer PRIVATE 5 | DllUnregisterServer PRIVATE 6 | DllInstall PRIVATE 7 | -------------------------------------------------------------------------------- /DummyLoader/DummyLoader.idl: -------------------------------------------------------------------------------- 1 | import "oaidl.idl"; 2 | import "ocidl.idl"; 3 | import "IImage3d.idl"; 4 | 5 | 6 | [ 7 | version(1.2), 8 | uuid(67E59584-3F6A-4852-8051-103A4583CA5E), 9 | helpstring("DummyLoader module") 10 | ] 11 | library DummyLoader 12 | { 13 | importlib("stdole2.tlb"); 14 | 15 | [ 16 | version(1.2), 17 | uuid(6FA82ED5-6332-4344-8417-DEA55E72098C), 18 | helpstring("3D image source") 19 | ] 20 | coclass Image3dSource 21 | { 22 | [default] interface IImage3dSource; 23 | }; 24 | 25 | [ 26 | version(1.2), 27 | uuid(8E754A72-0067-462B-9267-E84AF84828F1), 28 | helpstring("3D image file loader") 29 | ] 30 | coclass Image3dFileLoader 31 | { 32 | [default] interface IImage3dFileLoader; 33 | }; 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /DummyLoader/DummyLoader.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MedicalUltrasound/Image3dAPI/721eaa43179caa9f259168a8c5e6eaac4d84902c/DummyLoader/DummyLoader.rc -------------------------------------------------------------------------------- /DummyLoader/DummyLoader.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {277D8DBA-C8B3-423B-A707-B6435130BF4A} 23 | DummyLoader 24 | 10.0 25 | 26 | 27 | 28 | DynamicLibrary 29 | true 30 | v142 31 | MultiByte 32 | 33 | 34 | DynamicLibrary 35 | false 36 | v142 37 | true 38 | MultiByte 39 | 40 | 41 | DynamicLibrary 42 | true 43 | v142 44 | MultiByte 45 | 46 | 47 | DynamicLibrary 48 | false 49 | v142 50 | true 51 | MultiByte 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | $(SolutionDir)$(Platform)\$(Configuration)\ 73 | $(Platform)\$(Configuration)\ 74 | $(ProjectName)D 75 | false 76 | 77 | 78 | $(SolutionDir)$(Platform)\$(Configuration)\ 79 | $(Platform)\$(Configuration)\ 80 | false 81 | 82 | 83 | $(ProjectName)D 84 | false 85 | 86 | 87 | false 88 | 89 | 90 | 91 | Level3 92 | Disabled 93 | true 94 | MultiThreadedDebug 95 | $(ProjectDir)..\Image3dAPI 96 | 97 | 98 | true 99 | DummyLoader.def 100 | 101 | 102 | IF $(ConfigurationType) == DynamicLibrary ( 103 | echo Registering DLL... 104 | regsvr32 /s "$(TargetPath)" 105 | ) ELSE ( 106 | echo Registering EXE... 107 | "$(TargetPath)" /regserver 108 | ) 109 | 110 | echo Registering component 111 | 112 | 113 | $(TargetName)$(TargetExt) 114 | Image3dFileLoader.rgs 115 | 116 | 117 | $(ProjectDir)..\Image3dAPI 118 | %(Filename).h 119 | 120 | 121 | $(IntDir) 122 | 123 | 124 | GenRgsFiles.py 125 | Generate RGS-files 126 | 127 | 128 | 129 | 130 | Level3 131 | Disabled 132 | true 133 | MultiThreadedDebug 134 | $(ProjectDir)..\Image3dAPI 135 | 136 | 137 | true 138 | DummyLoader.def 139 | 140 | 141 | IF $(ConfigurationType) == DynamicLibrary ( 142 | echo Registering DLL... 143 | regsvr32 /s "$(TargetPath)" 144 | ) ELSE ( 145 | echo Registering EXE... 146 | "$(TargetPath)" /regserver 147 | ) 148 | 149 | echo Registering component 150 | 151 | 152 | $(TargetName)$(TargetExt) 153 | Image3dFileLoader.rgs 154 | 155 | 156 | $(ProjectDir)..\Image3dAPI 157 | %(Filename).h 158 | 159 | 160 | $(IntDir) 161 | 162 | 163 | GenRgsFiles.py 164 | Generate RGS-files 165 | 166 | 167 | 168 | 169 | Level3 170 | MaxSpeed 171 | true 172 | true 173 | true 174 | MultiThreaded 175 | $(ProjectDir)..\Image3dAPI 176 | 177 | 178 | true 179 | true 180 | true 181 | DummyLoader.def 182 | 183 | 184 | IF $(ConfigurationType) == DynamicLibrary ( 185 | echo Registering DLL... 186 | regsvr32 /s "$(TargetPath)" 187 | ) ELSE ( 188 | echo Registering EXE... 189 | "$(TargetPath)" /regserver 190 | ) 191 | 192 | echo Registering component 193 | 194 | 195 | $(TargetName)$(TargetExt) 196 | Image3dFileLoader.rgs 197 | 198 | 199 | $(ProjectDir)..\Image3dAPI 200 | %(Filename).h 201 | 202 | 203 | $(IntDir) 204 | 205 | 206 | GenRgsFiles.py 207 | Generate RGS-files 208 | 209 | 210 | 211 | 212 | Level3 213 | MaxSpeed 214 | true 215 | true 216 | true 217 | MultiThreaded 218 | $(ProjectDir)..\Image3dAPI 219 | 220 | 221 | true 222 | true 223 | true 224 | DummyLoader.def 225 | 226 | 227 | IF $(ConfigurationType) == DynamicLibrary ( 228 | echo Registering DLL... 229 | regsvr32 /s "$(TargetPath)" 230 | ) ELSE ( 231 | echo Registering EXE... 232 | "$(TargetPath)" /regserver 233 | ) 234 | 235 | echo Registering component 236 | 237 | 238 | $(TargetName)$(TargetExt) 239 | Image3dFileLoader.rgs 240 | 241 | 242 | $(ProjectDir)..\Image3dAPI 243 | %(Filename).h 244 | 245 | 246 | $(IntDir) 247 | 248 | 249 | GenRgsFiles.py 250 | Generate RGS-files 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | -------------------------------------------------------------------------------- /DummyLoader/DummyLoader.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DummyLoader/GenRgsFiles.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | progid_template1 = """ 4 | PROG-NAME.CLASS-NAME.1 = s 'CLASS-NAME Object' 5 | { 6 | CLSID = s '{CLASS-GUID}' 7 | } 8 | PROG-NAME.CLASS-NAME = s 'CLASS-NAME Object' 9 | { 10 | CLSID = s '{CLASS-GUID}' 11 | CurVer = s 'PROG-NAME.CLASS-NAME.1' 12 | }""" 13 | 14 | clsid_template1 = """ 15 | NoRemove CLSID 16 | { 17 | ForceRemove {CLASS-GUID} = s 'CLASS-NAME Object' 18 | {""" 19 | 20 | progid_template2 = """ 21 | ProgID = s 'PROG-NAME.CLASS-NAME.1' 22 | VersionIndependentProgID = s 'PROG-NAME.CLASS-NAME'""" 23 | 24 | clsid_template2 = """ 25 | InprocServer32 = s '%MODULE%' 26 | { 27 | val ThreadingModel = s 'THREAD-MODEL' 28 | } 29 | TypeLib = s '{TYPE-LIB}' 30 | Version = s 'VERSION' 31 | ADDITIONAL-REGISTRY-ENTRIES 32 | } 33 | }""" 34 | 35 | class ComClass: 36 | def __init__(self, name, uuid, version, creatable): 37 | self.name = name 38 | self.uuid = uuid 39 | self.version = version 40 | self.creatable = creatable 41 | self.additional_entries = '' # default 42 | 43 | 44 | def GenRgsFiles (progname, typelib, classes, threadmodel, concat_filename=None): 45 | all_rgs_content = '' 46 | for cls in classes: 47 | content = "HKCR\n{" 48 | if cls.creatable: 49 | content += progid_template1 50 | content += clsid_template1 51 | if cls.creatable: 52 | content += progid_template2 53 | content += clsid_template2 54 | content += "\n}\n" 55 | 56 | content = content.replace('PROG-NAME', progname) 57 | content = content.replace('TYPE-LIB', typelib) 58 | content = content.replace('THREAD-MODEL', threadmodel) 59 | content = content.replace('CLASS-NAME', cls.name) 60 | content = content.replace('CLASS-GUID', cls.uuid) 61 | content = content.replace('VERSION', cls.version) 62 | content = content.replace('ADDITIONAL-REGISTRY-ENTRIES', cls.additional_entries) 63 | 64 | #print(content) 65 | filename = cls.name+'.rgs' 66 | with open(filename, 'w') as f: 67 | f.write(content) 68 | all_rgs_content += content 69 | print('Written '+filename) 70 | 71 | # write concatenation of all RGS files into a separate file (needed for reg-free COM) 72 | if concat_filename: 73 | with open(concat_filename, 'w') as f: 74 | f.write(all_rgs_content) 75 | print('Written '+concat_filename) 76 | 77 | 78 | def ParseUuidString (val): 79 | uuid = val[val.find('uuid(')+5:] 80 | return uuid[:uuid.find(')')] 81 | 82 | def ParseVersionString (val): 83 | ver = val[val.find('version(')+8:] 84 | return ver[:ver.find(')')] 85 | 86 | 87 | def ParseIdl (filename): 88 | '''Parse IDL file to determine library name, typelib GUID and classes with associated GUIDs''' 89 | with open(filename, 'r') as f: 90 | source = f.read() 91 | 92 | attribs = '\[[^[]+\]\s*' # detect [...] attribute blocks 93 | name = '\s+[a-zA-Z0-9_]+' # detect coclass/interface name 94 | 95 | # find "library" name and associated typelib uuid 96 | for lib_hit in re.findall(attribs+'library'+name, source): 97 | tokens = lib_hit.split() 98 | for token in tokens: 99 | if 'uuid(' in token: 100 | typelib = ParseUuidString(token) 101 | break 102 | libname = tokens[-1] 103 | 104 | # find "coclass" names with associated uuid and version 105 | classes = [] 106 | for cls_hit in re.findall(attribs+'coclass'+name, source): 107 | tokens = cls_hit.split() 108 | for token in tokens: 109 | if 'uuid(' in token: 110 | uuid = ParseUuidString(token) 111 | elif 'version(' in token: 112 | version = ParseVersionString(token) 113 | classes.append(ComClass(tokens[-1], uuid, version, creatable=True)) 114 | 115 | return libname, typelib, classes 116 | 117 | def ParseImage3dAPIVersion (filename): 118 | with open(filename, 'r') as f: 119 | for line in f: 120 | if "IMAGE3DAPI_VERSION_MAJOR =" in line: 121 | major = int(line.split()[-1][:-1]) 122 | elif "IMAGE3DAPI_VERSION_MINOR =" in line: 123 | minor = int(line.split()[-1][:-1]) 124 | return str(major)+"."+str(minor) 125 | 126 | 127 | if __name__ == "__main__": 128 | libname, typelib, classes = ParseIdl('DummyLoader.idl') 129 | version = ParseImage3dAPIVersion("../Image3dAPI/IImage3d.idl") 130 | 131 | # verify that COM class versions matches API version 132 | for cls in classes: 133 | if cls.version != version: 134 | raise Exception("Version mismatch detected for "+cls.name) 135 | if cls.name == "Image3dFileLoader": 136 | cls.additional_entries += ''' 137 | val AppID = s '%APPID%' 138 | SupportedManufacturerModels 139 | { 140 | val 'Dummy medical systems' = s 'Super scanner *' 141 | val 'Dummy healthcare' = s 'Some scanner 1;Some scanner 2' 142 | }''' 143 | else: 144 | cls.creatable = False 145 | 146 | GenRgsFiles(libname, typelib, classes, 'Both') 147 | -------------------------------------------------------------------------------- /DummyLoader/Image3dFileLoader.cpp: -------------------------------------------------------------------------------- 1 | #include "Image3dFileLoader.hpp" 2 | 3 | 4 | Image3dFileLoader::Image3dFileLoader() { 5 | } 6 | 7 | Image3dFileLoader::~Image3dFileLoader() { 8 | } 9 | 10 | 11 | HRESULT Image3dFileLoader::LoadFile(BSTR file_name, /*out*/Image3dError *err_type, /*out*/BSTR *err_msg) { 12 | if (!err_type || !err_msg) 13 | return E_INVALIDARG; 14 | 15 | *err_type = Image3d_SUCCESS; 16 | *err_msg = CComBSTR().Detach(); 17 | return S_OK; // no operation 18 | } 19 | 20 | HRESULT Image3dFileLoader::GetImageSource(/*out*/IImage3dSource **img_src) { 21 | if (!img_src) 22 | return E_INVALIDARG; 23 | 24 | CComPtr obj = CreateLocalInstance(); 25 | *img_src = obj.Detach(); 26 | return S_OK; 27 | } 28 | -------------------------------------------------------------------------------- /DummyLoader/Image3dFileLoader.hpp: -------------------------------------------------------------------------------- 1 | /* Dummy test loader for the "3D API". 2 | Designed by Fredrik Orderud . 3 | Copyright (c) 2016, GE Healthcare, Ultrasound. */ 4 | #pragma once 5 | 6 | #include "../Image3dAPI/ComSupport.hpp" 7 | #include "../Image3dAPI/IImage3d.h" 8 | #include "Image3dSource.hpp" 9 | 10 | #include "DummyLoader.h" 11 | #include "Resource.h" 12 | 13 | class ATL_NO_VTABLE Image3dFileLoader : 14 | public CComObjectRootEx, 15 | public CComCoClass, 16 | public IImage3dFileLoader { 17 | public: 18 | Image3dFileLoader(); 19 | 20 | /*NOT virtual*/ ~Image3dFileLoader(); 21 | 22 | HRESULT STDMETHODCALLTYPE LoadFile(BSTR file_name, /*out*/Image3dError *err_type, /*out*/BSTR *err_msg) override; 23 | 24 | HRESULT STDMETHODCALLTYPE GetImageSource(/*out*/IImage3dSource **img_src) override; 25 | 26 | DECLARE_REGISTRY_RESOURCEID(IDR_Image3dFileLoader) 27 | 28 | BEGIN_COM_MAP(Image3dFileLoader) 29 | COM_INTERFACE_ENTRY(IImage3dFileLoader) 30 | END_COM_MAP() 31 | }; 32 | 33 | OBJECT_ENTRY_AUTO(__uuidof(Image3dFileLoader), Image3dFileLoader) 34 | -------------------------------------------------------------------------------- /DummyLoader/Image3dSource.cpp: -------------------------------------------------------------------------------- 1 | #include "Image3dSource.hpp" 2 | #include "LinAlg.hpp" 3 | 4 | 5 | static const uint8_t OUTSIDE_VAL = 0; // black outside image volume 6 | static const uint8_t PROBE_PLANE = 127; // gray value for plane closest to probe 7 | 8 | 9 | Image3dSource::Image3dSource() { 10 | m_probe.type = PROBE_External; 11 | m_probe.name = L"4V"; 12 | 13 | // One second loop starting at t = 10 14 | const size_t numFrames = 25; 15 | const double duration = 1.0; // ECG duration in seconds (the sum of the duration of individual ECG samples) 16 | const double startTime = 10.0; 17 | 18 | { 19 | // simulate sine-wave ECG 20 | const int N = 128; 21 | CComSafeArray samples(N); 22 | for (int i = 0; i < N; ++i) 23 | samples[i] = static_cast(sin(4 * i*M_PI / N)); 24 | 25 | CComSafeArray trig_times; 26 | trig_times.Add(startTime); // trig every 1/2 sec 27 | trig_times.Add(startTime + duration / 2); 28 | trig_times.Add(startTime + duration); 29 | 30 | EcgSeries ecg; 31 | ecg.start_time = startTime; 32 | ecg.delta_time = duration / N; 33 | ecg.samples = samples.Detach(); 34 | ecg.trig_times = trig_times.Detach(); 35 | m_ecg = EcgSeries(ecg); 36 | } 37 | { 38 | // flat gray tissue scale 39 | for (size_t i = 0; i < m_color_map_tissue.size(); ++i) 40 | m_color_map_tissue[i] = R8G8B8A8(static_cast(i), static_cast(i), static_cast(i), 0xFF); 41 | } 42 | { 43 | // image geometry X Y Z 44 | Cart3dGeom geom = { -0.1f, 0, -0.075f,// origin 45 | 0.20f,0, 0, // dir1 (width) 46 | 0, 0.10f, 0, // dir2 (depth) 47 | 0, 0, 0.15f};// dir3 (elevation) 48 | m_img_geom = geom; 49 | } 50 | { 51 | // checker board image data 52 | unsigned short dims[] = { 20, 15, 10 }; // matches length of dir1, dir2 & dir3, so that the image squares become quadratic 53 | std::vector img_buf(dims[0] * dims[1] * dims[2]); 54 | for (size_t frameNumber = 0; frameNumber < numFrames; ++frameNumber) { 55 | for (unsigned int z = 0; z < dims[2]; ++z) { 56 | for (unsigned int y = 0; y < dims[1]; ++y) { 57 | for (unsigned int x = 0; x < dims[0]; ++x) { 58 | bool even_f = (frameNumber / 2 % 2) == 0; 59 | bool even_x = (x / 2 % 2) == 0; 60 | bool even_y = (y / 2 % 2) == 0; 61 | bool even_z = (z / 2 % 2) == 0; 62 | byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; 63 | if (even_f ^ even_x ^ even_y ^ even_z) 64 | out_sample = 255; 65 | else 66 | out_sample = 0; 67 | } 68 | } 69 | } 70 | 71 | // special grayscale value for plane closest to probe 72 | for (unsigned int z = 0; z < dims[2]; ++z) { 73 | for (unsigned int x = 0; x < dims[0]; ++x) { 74 | unsigned int y = 0; 75 | byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; 76 | out_sample = PROBE_PLANE; 77 | } 78 | } 79 | 80 | m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, FORMAT_U8, dims, img_buf)); 81 | } 82 | } 83 | } 84 | 85 | Image3dSource::~Image3dSource() { 86 | } 87 | 88 | 89 | HRESULT Image3dSource::GetFrameCount(/*out*/unsigned int *size) { 90 | if (!size) 91 | return E_INVALIDARG; 92 | 93 | *size = static_cast(m_frames.size()); 94 | return S_OK; 95 | } 96 | 97 | HRESULT Image3dSource::GetFrameTimes(/*out*/SAFEARRAY * *frame_times) { 98 | if (!frame_times) 99 | return E_INVALIDARG; 100 | 101 | const unsigned int N = static_cast(m_frames.size()); 102 | CComSafeArray result(N); 103 | if (N > 0) { 104 | double * time_arr = &result.GetAt(0); 105 | for (unsigned int i = 0; i < N; ++i) 106 | time_arr[i] = m_frames[i].time; 107 | } 108 | 109 | *frame_times = result.Detach(); 110 | return S_OK; 111 | } 112 | 113 | 114 | HRESULT Image3dSource::GetFrame(unsigned int index, Cart3dGeom out_geom, unsigned short max_res[3], /*out*/Image3d *data) { 115 | if (!data) 116 | return E_INVALIDARG; 117 | if (index >= m_frames.size()) 118 | return E_BOUNDS; 119 | 120 | ImageFormat format = m_frames[index].format; 121 | if (format == FORMAT_U8) { 122 | Image3d result = SampleFrame(m_frames[index], m_img_geom, out_geom, max_res); 123 | *data = std::move(result); 124 | return S_OK; 125 | } 126 | 127 | return E_NOTIMPL; 128 | } 129 | 130 | HRESULT Image3dSource::GetBoundingBox(/*out*/Cart3dGeom *geom) { 131 | if (!geom) 132 | return E_INVALIDARG; 133 | 134 | *geom = m_img_geom; 135 | return S_OK; 136 | } 137 | 138 | HRESULT Image3dSource::GetColorMap(/*out*/SAFEARRAY ** map) { 139 | if (!map) 140 | return E_INVALIDARG; 141 | if (*map) 142 | return E_INVALIDARG; 143 | 144 | // copy to new buffer 145 | CComSafeArray color_map(static_cast(m_color_map_tissue.size())); 146 | memcpy(&color_map.GetAt(0), m_color_map_tissue.data(), sizeof(m_color_map_tissue)); 147 | *map = color_map.Detach(); // transfer ownership 148 | return S_OK; 149 | } 150 | 151 | HRESULT Image3dSource::GetECG(/*out*/EcgSeries *ecg) { 152 | if (!ecg) 153 | return E_INVALIDARG; 154 | 155 | // return a copy 156 | *ecg = EcgSeries(m_ecg); 157 | return S_OK; 158 | } 159 | 160 | HRESULT Image3dSource::GetProbeInfo(/*out*/ProbeInfo *probe) { 161 | if (!probe) 162 | return E_INVALIDARG; 163 | 164 | // return a copy 165 | *probe = m_probe; 166 | return S_OK; 167 | } 168 | 169 | HRESULT Image3dSource::GetSopInstanceUID(/*out*/BSTR *uid_str) { 170 | if (!uid_str) 171 | return E_INVALIDARG; 172 | if (*uid_str) 173 | return E_INVALIDARG; // input must be pointer to nullptr 174 | 175 | *uid_str = CComBSTR("DUMMY_UID").Detach(); 176 | return S_OK; 177 | } 178 | -------------------------------------------------------------------------------- /DummyLoader/Image3dSource.hpp: -------------------------------------------------------------------------------- 1 | /* Dummy test loader for the "3D API". 2 | Designed by Fredrik Orderud . 3 | Copyright (c) 2016, GE Healthcare, Ultrasound. */ 4 | #pragma once 5 | 6 | #include "Image3dStream.hpp" 7 | 8 | 9 | 10 | class ATL_NO_VTABLE Image3dSource : 11 | public CComObjectRootEx, 12 | public CComCoClass, 13 | public IImage3dSource { 14 | public: 15 | Image3dSource(); 16 | 17 | /*NOT virtual*/ ~Image3dSource(); 18 | 19 | HRESULT STDMETHODCALLTYPE GetFrameCount(/*out*/unsigned int *size) override; 20 | 21 | HRESULT STDMETHODCALLTYPE GetFrameTimes(/*out*/SAFEARRAY * *frame_times) override; 22 | 23 | HRESULT STDMETHODCALLTYPE GetFrame(unsigned int index, Cart3dGeom out_geom, unsigned short max_res[3], /*out*/Image3d *data) override; 24 | 25 | HRESULT STDMETHODCALLTYPE GetBoundingBox(/*out*/Cart3dGeom *geom) override; 26 | 27 | HRESULT STDMETHODCALLTYPE GetColorMap(/*out*/SAFEARRAY ** map) override; 28 | 29 | HRESULT STDMETHODCALLTYPE GetECG(/*out*/EcgSeries *ecg) override; 30 | 31 | HRESULT STDMETHODCALLTYPE GetProbeInfo(/*out*/ProbeInfo *probe) override; 32 | 33 | HRESULT STDMETHODCALLTYPE GetSopInstanceUID(/*out*/BSTR *uid_str) override; 34 | 35 | DECLARE_REGISTRY_RESOURCEID(IDR_Image3dSource) 36 | 37 | BEGIN_COM_MAP(Image3dSource) 38 | COM_INTERFACE_ENTRY(IImage3dSource) 39 | END_COM_MAP() 40 | 41 | private: 42 | ProbeInfo m_probe; 43 | EcgSeries m_ecg; 44 | std::array m_color_map_tissue; 45 | Cart3dGeom m_img_geom = {}; 46 | std::vector m_frames; 47 | }; 48 | 49 | OBJECT_ENTRY_AUTO(__uuidof(Image3dSource), Image3dSource) 50 | -------------------------------------------------------------------------------- /DummyLoader/Image3dStream.cpp: -------------------------------------------------------------------------------- 1 | #include "Image3dStream.hpp" 2 | -------------------------------------------------------------------------------- /DummyLoader/Image3dStream.hpp: -------------------------------------------------------------------------------- 1 | /* Dummy test loader for the "3D API". 2 | Designed by Fredrik Orderud . 3 | Copyright (c) 2020, GE Healthcare, Ultrasound. */ 4 | #pragma once 5 | 6 | #define _USE_MATH_DEFINES // for M_PI 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "../Image3dAPI/ComSupport.hpp" 12 | #include "../Image3dAPI/IImage3d.h" 13 | 14 | #include "DummyLoader.h" 15 | #include "Resource.h" 16 | 17 | 18 | /** RGBA color struct that matches DXGI_FORMAT_R8G8B8A8_UNORM. 19 | Created due to the lack of such a class/struct in the Windows or Direct3D SDKs. 20 | Please remove this class if a more standardized alternative is available. */ 21 | struct R8G8B8A8 { 22 | R8G8B8A8 () { 23 | } 24 | 25 | R8G8B8A8 (unsigned char _r, unsigned char _g, unsigned char _b, unsigned char _a) : r(_r), g(_g), b(_b), a(_a) { 26 | } 27 | 28 | operator unsigned int () const { 29 | return *reinterpret_cast(this); 30 | } 31 | 32 | uint8_t r = 0; ///< color channels 33 | uint8_t g = 0; 34 | uint8_t b = 0; 35 | uint8_t a = 0; 36 | }; 37 | 38 | /** Determine the sample size [bytes] for a given image format. */ 39 | static unsigned int ImageFormatSize(ImageFormat format) { 40 | switch (format) { 41 | case FORMAT_U8: return 1; 42 | } 43 | 44 | abort(); // should never be reached 45 | } 46 | 47 | /** Create a Image3d object from a std::vector buffer. */ 48 | static Image3d CreateImage3d (double time, ImageFormat format, const unsigned short dims[3], const std::vector &img_buf) { 49 | assert(img_buf.size() == ImageFormatSize(format)*dims[0]*dims[1]*dims[2]); 50 | 51 | Image3d img; 52 | img.time = time; 53 | img.format = format; 54 | for (size_t i = 0; i < 3; ++i) 55 | img.dims[i] = dims[i]; 56 | 57 | CComSafeArray data(static_cast(img_buf.size())); 58 | memcpy(data.m_psa->pvData, img_buf.data(), img_buf.size()); 59 | img.data = data.Detach(); 60 | 61 | // assume packed storage 62 | img.stride0 = dims[0] * ImageFormatSize(format); 63 | img.stride1 = dims[1] * img.stride0; 64 | 65 | return img; 66 | } 67 | -------------------------------------------------------------------------------- /DummyLoader/LinAlg.hpp: -------------------------------------------------------------------------------- 1 | /* Basic Ultrasound Image library (UsImage). 2 | Designed by Fredrik Orderud 3 | Copyright (c) 2015, GE Vingmed Ultrasound */ 4 | #pragma once 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | /** 3D vector type. */ 12 | struct vec3f { 13 | vec3f() : x(0), y(0), z(0) { 14 | } 15 | vec3f(float _x, float _y, float _z) : x(_x), y(_y), z(_z) { 16 | } 17 | 18 | vec3f operator + (vec3f other) const { 19 | return vec3f(x + other.x, y + other.y, z + other.z); 20 | } 21 | vec3f operator - (vec3f other) const { 22 | return vec3f(x - other.x, y - other.y, z - other.z); 23 | } 24 | 25 | vec3f operator - () const { 26 | return vec3f(-x, -y, -z); 27 | } 28 | 29 | vec3f & operator *= (float val) { 30 | x *= val; 31 | y *= val; 32 | z *= val; 33 | return *this; 34 | } 35 | vec3f & operator /= (float val) { 36 | x /= val; 37 | y /= val; 38 | z /= val; 39 | return *this; 40 | } 41 | 42 | vec3f & operator += (vec3f val) { 43 | x += val.x; 44 | y += val.y; 45 | z += val.z; 46 | return *this; 47 | } 48 | vec3f & operator -= (vec3f val) { 49 | x -= val.x; 50 | y -= val.y; 51 | z -= val.z; 52 | return *this; 53 | } 54 | 55 | bool operator == (vec3f other) const { 56 | if ((x == other.x) && (y == other.y) && (z == other.z)) 57 | return true; 58 | return false; 59 | } 60 | bool operator != (vec3f other) const { 61 | return !operator==(other); 62 | } 63 | 64 | float x, y, z; 65 | }; 66 | 67 | static vec3f operator * (float val, vec3f vec) { 68 | return vec3f(val*vec.x, val*vec.y, val*vec.z); 69 | } 70 | 71 | /** Calculates the vector cross-product */ 72 | static inline vec3f cross_prod(vec3f a, vec3f b) { 73 | return vec3f(a.y*b.z - a.z*b.y, 74 | a.z*b.x - a.x*b.z, 75 | a.x*b.y - a.y*b.x); 76 | } 77 | 78 | 79 | /** Calculates the vector dot-product */ 80 | static inline float dot_prod(vec3f a, vec3f b) { 81 | return a.x*b.x + a.y*b.y + a.z*b.z; 82 | } 83 | 84 | 85 | /** Calculates the Eucledian length of a vector. */ 86 | static inline float length(vec3f vec) { 87 | return sqrt(vec.x*vec.x + vec.y*vec.y + vec.z*vec.z); 88 | } 89 | 90 | 91 | /** Returns the input vector normalized to unit length. */ 92 | static inline vec3f normalize(vec3f vec) { 93 | vec3f result = vec; 94 | float norm = length(result); 95 | 96 | if (norm == 0.0f) 97 | return result; 98 | 99 | result.x /= norm; 100 | result.y /= norm; 101 | result.z /= norm; 102 | 103 | return result; 104 | } 105 | 106 | 107 | /** 3x3 matrix type. */ 108 | class mat33f { 109 | public: 110 | mat33f() { 111 | clear(); 112 | } 113 | 114 | float & operator () (size_t i, size_t j) { 115 | assert(i < 3 && j < 3); 116 | return m_data[i + 3 * j]; 117 | } 118 | const float & operator () (size_t i, size_t j) const { 119 | assert(i < 3 && j < 3); 120 | return m_data[i + 3 * j]; 121 | } 122 | 123 | void clear() { 124 | m_data.fill(0); 125 | } 126 | 127 | const float * data() const { 128 | return m_data.data(); 129 | } 130 | 131 | void transpose() { 132 | static_assert(3 == 3, "only transpose of square matrices is supported"); 133 | 134 | // make temporary matrix 135 | mat33f t; 136 | for (unsigned int i = 0; i < 3; ++i) 137 | for (unsigned int j = 0; j < 3; ++j) 138 | t(j, i) = (*this)(i, j); 139 | 140 | // copy to "this" 141 | (*this) = t; 142 | } 143 | 144 | private: 145 | std::array m_data; 146 | }; 147 | 148 | static inline void operator *= (mat33f & m, float val) { 149 | for (size_t j = 0; j < 3; j++) 150 | for (size_t i = 0; i < 3; i++) 151 | m(i, j) *= val; 152 | } 153 | 154 | /** Assign a row to a matrix. */ 155 | static inline void row_assign(mat33f & m, size_t i, const vec3f & val) { 156 | m(i, 0) = val.x; 157 | m(i, 1) = val.y; 158 | m(i, 2) = val.z; 159 | } 160 | 161 | /** Assign a column to a matrix. */ 162 | static inline void col_assign(mat33f & m, size_t i, const vec3f & val) { 163 | m(0, i) = val.x; 164 | m(1, i) = val.y; 165 | m(2, i) = val.z; 166 | } 167 | 168 | 169 | /** Determinant of a 3 x 3 matrix. */ 170 | static inline float det(const mat33f & M) { 171 | float a = M(0, 0), b = M(0, 1), c = M(0, 2); 172 | float d = M(1, 0), e = M(1, 1), f = M(1, 2); 173 | float g = M(2, 0), h = M(2, 1), i = M(2, 2); 174 | 175 | float det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g); 176 | return det; 177 | } 178 | 179 | /** Inverts a 3 x 3 matrix analytically. */ 180 | static inline mat33f inv(const mat33f & M, bool normalize = true) { 181 | float a = M(0, 0), b = M(0, 1), c = M(0, 2); 182 | float d = M(1, 0), e = M(1, 1), f = M(1, 2); 183 | float g = M(2, 0), h = M(2, 1), i = M(2, 2); 184 | 185 | mat33f invM; 186 | invM(0, 0) = e*i - f*h; 187 | invM(0, 1) = c*h - b*i; 188 | invM(0, 2) = b*f - c*e; 189 | invM(1, 0) = f*g - d*i; 190 | invM(1, 1) = a*i - c*g; 191 | invM(1, 2) = c*d - a*f; 192 | invM(2, 0) = d*h - e*g; 193 | invM(2, 1) = b*g - a*h; 194 | invM(2, 2) = a*e - b*d; 195 | 196 | if (normalize) { 197 | invM *= 1.0f / det(M); 198 | } 199 | 200 | return invM; 201 | } 202 | 203 | 204 | /** Matrix vector product. */ 205 | static inline vec3f prod(const mat33f & m, const vec3f & p) { 206 | vec3f sum(0, 0, 0); 207 | sum += p.x * vec3f(m(0, 0), m(1, 0), m(2, 0)); 208 | sum += p.y * vec3f(m(0, 1), m(1, 1), m(2, 1)); 209 | sum += p.z * vec3f(m(0, 2), m(1, 2), m(2, 2)); 210 | return sum; 211 | } 212 | 213 | 214 | /** Convert from normalized voxel pos in [0,1) to (x,y,z) coordinate. */ 215 | static vec3f PosToCoord (vec3f origin, vec3f dir1, vec3f dir2, vec3f dir3, const vec3f pos) { 216 | mat33f M; 217 | col_assign(M, 0, dir1); 218 | col_assign(M, 1, dir2); 219 | col_assign(M, 2, dir3); 220 | 221 | return prod(M, pos) + origin; 222 | } 223 | 224 | 225 | static std::tuple FromCart3dGeom (Cart3dGeom geom) { 226 | const vec3f origin(geom.origin_x, geom.origin_y, geom.origin_z); 227 | const vec3f dir1(geom.dir1_x, geom.dir1_y, geom.dir1_z); 228 | const vec3f dir2(geom.dir2_x, geom.dir2_y, geom.dir2_z); 229 | const vec3f dir3(geom.dir3_x, geom.dir3_y, geom.dir3_z); 230 | 231 | return std::make_tuple(origin, dir1, dir2, dir3); 232 | } 233 | 234 | 235 | static Cart3dGeom ToCart3dGeom (vec3f origin, vec3f dir1, vec3f dir2, vec3f dir3) { 236 | Cart3dGeom geom = {}; 237 | { 238 | geom.origin_x = origin.x; 239 | geom.origin_y = origin.y; 240 | geom.origin_z = origin.z; 241 | 242 | geom.dir1_x = dir1.x; 243 | geom.dir1_y = dir1.y; 244 | geom.dir1_z = dir1.z; 245 | 246 | geom.dir2_x = dir2.x; 247 | geom.dir2_y = dir2.y; 248 | geom.dir2_z = dir2.z; 249 | 250 | geom.dir3_x = dir3.x; 251 | geom.dir3_y = dir3.y; 252 | geom.dir3_z = dir3.z; 253 | } 254 | return geom; 255 | } 256 | 257 | 258 | /** Convert from (x,y,z) coordinate to normalized voxel pos in [0,1). */ 259 | static vec3f CoordToPos (Cart3dGeom geom, const vec3f xyz) { 260 | vec3f origin, dir1, dir2, dir3; 261 | std::tie(origin, dir1, dir2, dir3) = FromCart3dGeom(geom); 262 | 263 | mat33f M; 264 | col_assign(M, 0, dir1); 265 | col_assign(M, 1, dir2); 266 | col_assign(M, 2, dir3); 267 | 268 | return prod(inv(M), xyz - origin); 269 | } 270 | 271 | 272 | template 273 | static T SampleVoxel (const Image3d & frame, const vec3f pos) { 274 | assert(ImageFormatSize(frame.format) == sizeof(T)); 275 | 276 | // out-of-bounds checking 277 | if ((pos.x < 0) || (pos.y < 0) || (pos.z < 0)) 278 | return OUTSIDE_VAL; 279 | 280 | auto x = static_cast(frame.dims[0] * pos.x); 281 | auto y = static_cast(frame.dims[1] * pos.y); 282 | auto z = static_cast(frame.dims[2] * pos.z); 283 | 284 | // out-of-bounds checking 285 | if ((x >= frame.dims[0]) || (y >= frame.dims[1]) || (z >= frame.dims[2])) 286 | return OUTSIDE_VAL; 287 | 288 | return static_cast(frame.data->pvData)[x + y*frame.stride0/sizeof(T) + z*frame.stride1/sizeof(T)]; 289 | } 290 | 291 | 292 | template 293 | static Image3d SampleFrame (const Image3d & frame, Cart3dGeom frame_geom, Cart3dGeom out_geom, unsigned short max_res[3]) { 294 | if (max_res[2] == 0) 295 | max_res[2] = 1; // require at least one plane to to retrieved 296 | 297 | vec3f out_origin, out_dir1, out_dir2, out_dir3; 298 | std::tie(out_origin, out_dir1, out_dir2, out_dir3) = FromCart3dGeom(out_geom); 299 | 300 | // allow 3rd axis to be empty if only retrieving a single slice 301 | if ((out_dir3 == vec3f(0, 0, 0)) && (max_res[2] < 2)) 302 | out_dir3 = cross_prod(out_dir1, out_dir2); 303 | 304 | // sample image buffer 305 | std::vector img_buf(sizeof(T) * max_res[0] * max_res[1] * max_res[2], 127); 306 | for (unsigned short z = 0; z < max_res[2]; ++z) { 307 | for (unsigned short y = 0; y < max_res[1]; ++y) { 308 | for (unsigned short x = 0; x < max_res[0]; ++x) { 309 | // convert from input texture coordinate to output texture coordinate 310 | vec3f pos_in(x*1.0f/max_res[0], y*1.0f/max_res[1], z*1.0f/max_res[2]); 311 | vec3f xyz = PosToCoord(out_origin, out_dir1, out_dir2, out_dir3, pos_in); 312 | vec3f pos_out = CoordToPos(frame_geom, xyz); 313 | 314 | T val = SampleVoxel(frame, pos_out); 315 | reinterpret_cast(img_buf.data())[x + y*max_res[0] + z*max_res[0] * max_res[1]] = val; 316 | } 317 | } 318 | } 319 | 320 | return CreateImage3d(frame.time, frame.format, max_res, img_buf); 321 | } 322 | -------------------------------------------------------------------------------- /DummyLoader/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "Image3dSource.hpp" 2 | #include "Image3dFileLoader.hpp" 3 | 4 | #include "resource.h" 5 | #include "DummyLoader.h" 6 | #include "DummyLoader_i.c" 7 | 8 | 9 | class DummyLoaderModule : 10 | public ATL::CAtlDllModuleT 11 | { 12 | public: 13 | DECLARE_LIBID(LIBID_DummyLoader) 14 | DECLARE_REGISTRY_APPID_RESOURCEID(IDR_AppID, "{92280FDD-C149-44E3-BDEE-736F9F9EEA4E}") 15 | }; 16 | 17 | DummyLoaderModule _AtlModule; 18 | 19 | 20 | 21 | // DLL Entry Point 22 | extern "C" BOOL WINAPI DllMain(HINSTANCE /*hInstance*/, DWORD dwReason, LPVOID lpReserved) 23 | { 24 | return _AtlModule.DllMain(dwReason, lpReserved); 25 | } 26 | 27 | // Used to determine whether the DLL can be unloaded by OLE. 28 | STDAPI DllCanUnloadNow() 29 | { 30 | return _AtlModule.DllCanUnloadNow(); 31 | } 32 | 33 | // Returns a class factory to create an object of the requested type. 34 | _Check_return_ 35 | STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv) 36 | { 37 | return _AtlModule.DllGetClassObject(rclsid, riid, ppv); 38 | } 39 | 40 | // DllRegisterServer - Adds entries to the system registry. 41 | STDAPI DllRegisterServer() 42 | { 43 | // registers object, typelib and all interfaces in typelib 44 | return _AtlModule.DllRegisterServer(); 45 | } 46 | 47 | // DllUnregisterServer - Removes entries from the system registry. 48 | STDAPI DllUnregisterServer() 49 | { 50 | return _AtlModule.DllUnregisterServer(); 51 | } 52 | 53 | // DllInstall - Adds/Removes entries to the system registry per user per machine. 54 | STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine) 55 | { 56 | static const wchar_t szUserSwitch[] = L"user"; 57 | 58 | if (pszCmdLine != NULL) { 59 | if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0) 60 | ATL::AtlSetPerUserRegistration(true); 61 | } 62 | 63 | HRESULT hr = E_FAIL; 64 | if (bInstall) { 65 | hr = DllRegisterServer(); 66 | if (FAILED(hr)) 67 | DllUnregisterServer(); 68 | } 69 | else { 70 | hr = DllUnregisterServer(); 71 | } 72 | 73 | return hr; 74 | } 75 | -------------------------------------------------------------------------------- /DummyLoader/Resource.h: -------------------------------------------------------------------------------- 1 | #define IDS_PROJNAME 100 2 | 3 | #define IDR_AppID 105 4 | #define IDR_Image3dSource 106 5 | #define IDR_Image3dFileLoader 107 6 | -------------------------------------------------------------------------------- /DummyLoader/UNREGISTER_DummyLoader.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo NOTICE: Script MUST be run as Administrator. 3 | :: Errors from "reg" tool are muted to avoid flooding the build log with errors from already deleted registry entries. 4 | 5 | :: Fix issue with "Run as Administrator" current dir 6 | setlocal enableextensions 7 | cd /d "%~dp0" 8 | 9 | 10 | :: Remove all traces of DummyLoader from registry 11 | for %%R in (HKEY_LOCAL_MACHINE HKEY_CURRENT_USER) do ( 12 | :: TypeLib 13 | reg delete "%%R\SOFTWARE\Classes\TypeLib\{67E59584-3F6A-4852-8051-103A4583CA5E}" /f 2> NUL 14 | 15 | for %%P in (32 64) do ( 16 | :: Image3dSource class 17 | reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dSource" /f 2> NUL 18 | reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dSource.1" /f 2> NUL 19 | reg delete "%%R\SOFTWARE\Classes\CLSID\{6FA82ED5-6332-4344-8417-DEA55E72098C}" /f /reg:%%P 2> NUL 20 | 21 | :: Image3dFileLoader class 22 | reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dFileLoader" /f 2> NUL 23 | reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dFileLoader.1" /f 2> NUL 24 | reg delete "%%R\SOFTWARE\Classes\CLSID\{8E754A72-0067-462B-9267-E84AF84828F1}" /f /reg:%%P 2> NUL 25 | ) 26 | ) 27 | 28 | ::pause 29 | -------------------------------------------------------------------------------- /Guidelines.md: -------------------------------------------------------------------------------- 1 | ## Guidelines for implementing the interface 2 | 3 | ### Performance 4 | 5 | * Data loading of a typical image should take less than 3 seconds, including fetching all frames (from local HDD). 6 | * IImage3dFileLoader::LoadFile must be fast (1-10 ms). I.e. This is needed to permit scanning many DICOM files to see if they can be loaded. LoadFile should not do scan conversion (i.e., from ultrasound scan-lines to a 3D Cartesian volume) by itself. This conversion should be done only when the volume data is actually requested. 7 | * It is strongly recommended to avoid caching of frames inside the loader. It should be up to the host to perform necessary caching. This allows the host to limit memory usage depending on available resources. Hence, loading image frame data from file and scan conversion/image processing should be delayed until Iimage3dSource::GetFrame is called. 8 | 9 | ### Robustness 10 | 11 | * IImage3dFileLoader::LoadFile should return success only if the file can be read as 3D data. 12 | * If IImage3dFileLoader::LoadFile returns success, the file must be readable with the loader. 13 | * IImage3dFileLoader::LoadFile should be well behaved when and after provided with non-conforming DICOM files or non-DICOM files. 14 | * API methods shall not throw exceptions. If it is possible for exceptions to be thrown in the internal implementation, they should be caught within the component, and translated into a failure return value for the IImage3D API method call. 15 | * IImage3D objects should expect themselves to be launched as out-of-process COM servers, because the client software will want to prevent itself from failing if there is a failure in the IImage3D object. 16 | 17 | ### Environment 18 | 19 | * The loader should be compiled both in 32 and 64 bits. 20 | * A DirectX 11 compliant GPU is required for interactive review of 3D images. The loader should still be well behaved when running without a DirectX 11 compliant GPU, but without any performance guarantees. 21 | * The minimum requirement for CPU is 2GHz Intel Core i-series CPU or better, with 2 GB RAM or more. 22 | * The loader should support all resolutions (as specified by Image3dSource::GetFrame parameter max_resolution). It is up to the workstation integration team to assess memory consumption/performance and determine suitable limits for max_resolution upon integration. The loader should be well behaved even though it cannot support the max_resolution requested by the client. 23 | * IImage3dFileLoader::LoadFile should accept both paths on the form 'C:\path' as well as UNC paths on the form '\\server\path' -------------------------------------------------------------------------------- /Image3dAPI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Image3dAPI", "Image3dAPI\Image3dAPI.vcxproj", "{4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E504F5C4-A5F4-4D7B-B7DE-B74C0D6A220E}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DummyLoader", "DummyLoader\DummyLoader.vcxproj", "{277D8DBA-C8B3-423B-A707-B6435130BF4A}" 14 | ProjectSection(ProjectDependencies) = postProject 15 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} = {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} 16 | EndProjectSection 17 | EndProject 18 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegFreeTest", "RegFreeTest\RegFreeTest.vcxproj", "{8ABF3DC3-6C48-439B-B3A1-DED84351B808}" 19 | ProjectSection(ProjectDependencies) = postProject 20 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} = {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} 21 | {277D8DBA-C8B3-423B-A707-B6435130BF4A} = {277D8DBA-C8B3-423B-A707-B6435130BF4A} 22 | EndProjectSection 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestViewer", "TestViewer\TestViewer.csproj", "{08D5EA11-0B4C-40FD-87EC-23D42C195D59}" 25 | ProjectSection(ProjectDependencies) = postProject 26 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} = {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} 27 | EndProjectSection 28 | EndProject 29 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "TestPython", "TestPython\TestPython.pyproj", "{A594D690-99DB-499B-B3B0-0BF81364EBA2}" 30 | EndProject 31 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SandboxTest", "SandboxTest\SandboxTest.vcxproj", "{DFA5034E-BEA9-4299-A301-38A5DF7FD256}" 32 | ProjectSection(ProjectDependencies) = postProject 33 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} = {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} 34 | {277D8DBA-C8B3-423B-A707-B6435130BF4A} = {277D8DBA-C8B3-423B-A707-B6435130BF4A} 35 | EndProjectSection 36 | EndProject 37 | Global 38 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 39 | Debug|Win32 = Debug|Win32 40 | Debug|x64 = Debug|x64 41 | Release|Win32 = Release|Win32 42 | Release|x64 = Release|x64 43 | EndGlobalSection 44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 45 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Debug|Win32.ActiveCfg = Debug|Win32 46 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Debug|Win32.Build.0 = Debug|Win32 47 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Debug|x64.ActiveCfg = Debug|x64 48 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Debug|x64.Build.0 = Debug|x64 49 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Release|Win32.ActiveCfg = Release|Win32 50 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Release|Win32.Build.0 = Release|Win32 51 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Release|x64.ActiveCfg = Release|x64 52 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3}.Release|x64.Build.0 = Release|x64 53 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Debug|Win32.ActiveCfg = Debug|Win32 54 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Debug|Win32.Build.0 = Debug|Win32 55 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Debug|x64.ActiveCfg = Debug|x64 56 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Debug|x64.Build.0 = Debug|x64 57 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Release|Win32.ActiveCfg = Release|Win32 58 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Release|Win32.Build.0 = Release|Win32 59 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Release|x64.ActiveCfg = Release|x64 60 | {277D8DBA-C8B3-423B-A707-B6435130BF4A}.Release|x64.Build.0 = Release|x64 61 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Debug|Win32.ActiveCfg = Debug|Win32 62 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Debug|Win32.Build.0 = Debug|Win32 63 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Debug|x64.ActiveCfg = Debug|x64 64 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Debug|x64.Build.0 = Debug|x64 65 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Release|Win32.ActiveCfg = Release|Win32 66 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Release|Win32.Build.0 = Release|Win32 67 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Release|x64.ActiveCfg = Release|x64 68 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808}.Release|x64.Build.0 = Release|x64 69 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Debug|Win32.ActiveCfg = Debug|x86 70 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Debug|Win32.Build.0 = Debug|x86 71 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Debug|x64.ActiveCfg = Debug|x64 72 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Debug|x64.Build.0 = Debug|x64 73 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Release|Win32.ActiveCfg = Release|x86 74 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Release|Win32.Build.0 = Release|x86 75 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Release|x64.ActiveCfg = Release|x64 76 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59}.Release|x64.Build.0 = Release|x64 77 | {A594D690-99DB-499B-B3B0-0BF81364EBA2}.Debug|Win32.ActiveCfg = Debug|Any CPU 78 | {A594D690-99DB-499B-B3B0-0BF81364EBA2}.Debug|x64.ActiveCfg = Debug|Any CPU 79 | {A594D690-99DB-499B-B3B0-0BF81364EBA2}.Release|Win32.ActiveCfg = Release|Any CPU 80 | {A594D690-99DB-499B-B3B0-0BF81364EBA2}.Release|x64.ActiveCfg = Release|Any CPU 81 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Debug|Win32.ActiveCfg = Debug|Win32 82 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Debug|Win32.Build.0 = Debug|Win32 83 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Debug|x64.ActiveCfg = Debug|x64 84 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Debug|x64.Build.0 = Debug|x64 85 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Release|Win32.ActiveCfg = Release|Win32 86 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Release|Win32.Build.0 = Release|Win32 87 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Release|x64.ActiveCfg = Release|x64 88 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256}.Release|x64.Build.0 = Release|x64 89 | EndGlobalSection 90 | GlobalSection(SolutionProperties) = preSolution 91 | HideSolutionNode = FALSE 92 | EndGlobalSection 93 | GlobalSection(ExtensibilityGlobals) = postSolution 94 | SolutionGuid = {9C3239D5-451E-429B-95DE-4B251511748E} 95 | EndGlobalSection 96 | EndGlobal 97 | -------------------------------------------------------------------------------- /Image3dAPI/.gitignore: -------------------------------------------------------------------------------- 1 | /x64 2 | /Win32 3 | /IImage3d.h 4 | /IImage3d_i.c 5 | /IImage3d_p.c 6 | /dlldata.c 7 | /IImage3dTypeLibraryGenerator.h 8 | /IImage3dTypeLibraryGenerator_i.c 9 | /Image3dAPI.dll 10 | /Image3dAPI.vcxproj.user 11 | -------------------------------------------------------------------------------- /Image3dAPI/ComSupport.hpp: -------------------------------------------------------------------------------- 1 | /* "Plugin API" convenience functions. 2 | Designed by Fredrik Orderud . 3 | Copyright (c) 2016, GE Healthcare, Ultrasound. */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include // for _com_error 11 | #include 12 | #include // for CComSafeArray 13 | #include // for CComObject 14 | 15 | 16 | /** Converts unicode string to ASCII */ 17 | static inline std::string ToAscii (const std::wstring& w_str) { 18 | #pragma warning(push) 19 | #pragma warning(disable: 4996) // function or variable may be unsafe 20 | size_t N = w_str.size(); 21 | std::string s_str; 22 | s_str.resize(N); 23 | 24 | wcstombs((char*)s_str.c_str(), w_str.c_str(), N); 25 | 26 | return s_str; 27 | #pragma warning(pop) 28 | } 29 | 30 | 31 | /** Translate COM HRESULT failure into exceptions. */ 32 | static void CHECK (HRESULT hr) { 33 | if (FAILED(hr)) { 34 | _com_error err(hr); 35 | #ifdef _UNICODE 36 | const wchar_t * msg = err.ErrorMessage(); // weak ptr. 37 | throw std::runtime_error(ToAscii(msg)); 38 | #else 39 | const char * msg = err.ErrorMessage(); // weak ptr. 40 | throw std::runtime_error(msg); 41 | #endif 42 | } 43 | } 44 | 45 | 46 | /* Disable BSTR caching to ease memory management debugging. 47 | REF: http://msdn.microsoft.com/en-us/library/windows/desktop/ms644360.aspx */ 48 | extern "C" void SetOaNoCache(); 49 | 50 | 51 | 52 | /** RAII class for COM initialization. */ 53 | class ComInitialize { 54 | public: 55 | ComInitialize (COINIT apartment /*= COINIT_MULTITHREADED*/) : m_initialized(false) { 56 | // REF: https://msdn.microsoft.com/en-us/library/windows/desktop/ms695279.aspx 57 | HRESULT hr = CoInitializeEx(NULL, apartment); 58 | if (SUCCEEDED(hr)) 59 | m_initialized = true; 60 | 61 | #ifdef _DEBUG 62 | SetOaNoCache(); 63 | #endif 64 | } 65 | 66 | ~ComInitialize() { 67 | if (m_initialized) 68 | CoUninitialize(); 69 | } 70 | 71 | private: 72 | bool m_initialized; ///< must uninitialize in dtor 73 | }; 74 | 75 | 76 | /** Convenience function to create a locally implemented COM instance without the overhead of CoCreateInstance. 77 | The COM class does not need to be registred for construction to succeed. However, lack of registration can 78 | cause problems if transporting the class out-of-process. */ 79 | template 80 | static CComPtr CreateLocalInstance() { 81 | // create an object (with ref. count zero) 82 | CComObject * tmp = nullptr; 83 | CHECK(CComObject::CreateInstance(&tmp)); 84 | 85 | // move into smart-ptr (will incr. ref. count to one) 86 | return CComPtr(tmp); 87 | } 88 | 89 | 90 | /** Convert SafeArray to a std::vector. */ 91 | template 92 | static std::vector ConvertToVector (const CComSafeArray & input) { 93 | std::vector result(input.GetCount()); 94 | if (result.size() > 0) { 95 | const T * in_ptr = &input.GetAt(0); // will fail if empty 96 | for (size_t i = 0; i < result.size(); ++i) 97 | result[i] = in_ptr[i]; 98 | } 99 | return result; 100 | } 101 | 102 | /** Convert raw array to SafeArray. */ 103 | template 104 | static CComSafeArray ConvertToSafeArray (const T * input, size_t element_count) { 105 | CComSafeArray result(static_cast(element_count)); 106 | if (element_count > 0) { 107 | T * out_ptr = &result.GetAt(0); // will fail if empty 108 | for (size_t i = 0; i < element_count; ++i) 109 | out_ptr[i] = input[i]; 110 | } 111 | return result; 112 | } 113 | -------------------------------------------------------------------------------- /Image3dAPI/IImage3d.idl: -------------------------------------------------------------------------------- 1 | /* "3D API" interface header. 2 | Designed by Fredrik Orderud . 3 | Copyright (c) 2016, GE Healthcare, Ultrasound. 4 | 5 | Design goals: 6 | * Stable compiler-independent ABI (binary API). 7 | * Support transfer of image data ownership across API (to avoid copying). 8 | * Support out-of-process loaders (for sandboxing). 9 | * Support multiple programming languages natively. 10 | */ 11 | 12 | /* Interfaces and data structures for exchanging image data. */ 13 | import "oaidl.idl"; 14 | 15 | 16 | typedef [ 17 | v1_enum, // 32bit enum size 18 | helpstring("Image3dAPI version.")] 19 | enum Image3dAPIVersion { 20 | IMAGE3DAPI_VERSION_MAJOR = 1, 21 | IMAGE3DAPI_VERSION_MINOR = 2, 22 | } Image3dAPIVersion; 23 | 24 | 25 | typedef [ 26 | v1_enum, // 32bit enum size 27 | helpstring("Enum of supported image formats (extended upon demand).")] 28 | enum ImageFormat { 29 | FORMAT_INVALID = 0, ///< make sure that "cleared" state is invalid 30 | FORMAT_U8 = 1, ///< unsigned 8bit grayscale 31 | } ImageFormat; 32 | 33 | 34 | typedef [ 35 | v1_enum, // 32bit enum size 36 | helpstring("Probe type enum." 37 | "Values taken from http://dicom.nema.org/medical/Dicom/2017b/output/chtml/part16/sect_CID_12035.html")] 38 | enum ProbeType { 39 | PROBE_UNKNOWN = 0, 40 | PROBE_External = 125261, 41 | PROBE_Transesophageal = 125262, 42 | PROBE_Endovaginal = 125263, 43 | PROBE_Endorectal = 125264, 44 | PROBE_Intravascular = 125265, 45 | } ProbeType; 46 | 47 | cpp_quote("") 48 | cpp_quote("#ifndef __cplusplus") 49 | 50 | typedef [ 51 | helpstring("Probe information.")] 52 | struct ProbeInfo { 53 | [helpstring("Can be useful for initializing renderings and quantification tools.")] 54 | ProbeType type; 55 | 56 | [helpstring("short name describing probe")] 57 | BSTR name; 58 | } ProbeInfo; 59 | 60 | cpp_quote("") 61 | cpp_quote("#else // __cplusplus") 62 | cpp_quote("} // extern \"C\"") 63 | cpp_quote("") 64 | cpp_quote("struct ProbeInfo {") 65 | cpp_quote(" ProbeType type = PROBE_UNKNOWN;") 66 | cpp_quote(" CComBSTR name; ///< BSTR wrapper") 67 | cpp_quote("};") 68 | cpp_quote("") 69 | cpp_quote("extern \"C\"{") 70 | cpp_quote("#endif") 71 | cpp_quote("") 72 | cpp_quote("#if defined _WIN64 || defined __x86_64__") 73 | cpp_quote("static_assert(sizeof(ProbeInfo) == 4+4+8, \"ProbeInfo size mismatch\");") 74 | cpp_quote("#else") 75 | cpp_quote("static_assert(sizeof(ProbeInfo) == 4+4, \"ProbeInfo size mismatch\");") 76 | cpp_quote("#endif") 77 | 78 | 79 | cpp_quote("") 80 | cpp_quote("#ifndef __cplusplus") 81 | 82 | typedef [ 83 | helpstring("3D image data struct. Stored in row-major order. The data buffer might be padded between rows due to alignment needs.")] 84 | struct Image3d { 85 | [helpstring("time [seconds]")] double time; 86 | [helpstring("")] ImageFormat format; 87 | [helpstring("resolution (width/columns, height/rows, planes)")] unsigned short dims[3]; 88 | [helpstring("distance between each row [bytes] (>= width*element_size)")] unsigned int stride0; 89 | [helpstring("distance between each plane [bytes] (>= height*stride0)")] unsigned int stride1; 90 | [helpstring("underlying 1D image buffer (size >= planes*stride1)")] SAFEARRAY(byte) data; 91 | } Image3d; 92 | 93 | cpp_quote("") 94 | cpp_quote("#else // __cplusplus") 95 | cpp_quote("} // extern \"C\"") 96 | cpp_quote("") 97 | cpp_quote("struct Image3d {") 98 | cpp_quote(" double time = 0;") 99 | cpp_quote(" ImageFormat format = FORMAT_INVALID;") 100 | cpp_quote(" unsigned short dims[3] = {0,0,0};") 101 | cpp_quote(" unsigned int stride0 = 0;") 102 | cpp_quote(" unsigned int stride1 = 0;") 103 | cpp_quote(" SAFEARRAY * data = nullptr; ///< BYTE array") 104 | cpp_quote(" ") 105 | cpp_quote(" /* Primary ctor. */") 106 | cpp_quote(" Image3d() {") 107 | cpp_quote(" }") 108 | cpp_quote(" /** Copy ctor. Performs deep copy. */") 109 | cpp_quote(" Image3d(const Image3d& obj) {") 110 | cpp_quote(" time = obj.time;") 111 | cpp_quote(" format = obj.format;") 112 | cpp_quote(" for (unsigned int i = 0; i < 3; ++i)") 113 | cpp_quote(" dims[i] = obj.dims[i];") 114 | cpp_quote(" stride0 = obj.stride0;") 115 | cpp_quote(" stride1 = obj.stride1;") 116 | cpp_quote(" CComSafeArray tmp;") 117 | cpp_quote(" tmp.Attach(obj.data);") 118 | cpp_quote(" tmp.CopyTo(&data);") 119 | cpp_quote(" tmp.Detach();") 120 | cpp_quote(" }") 121 | cpp_quote(" ") 122 | cpp_quote(" ~Image3d() {") 123 | cpp_quote(" release(); // clear existing state") 124 | cpp_quote(" }") 125 | cpp_quote(" ") 126 | cpp_quote(" /** Move assignment.*/") 127 | cpp_quote(" Image3d& operator = (Image3d&& obj) {") 128 | cpp_quote(" release(); // clear existing state") 129 | cpp_quote(" ") 130 | cpp_quote(" time = obj.time;") 131 | cpp_quote(" format = obj.format;") 132 | cpp_quote(" for (unsigned int i = 0; i < 3; ++i)") 133 | cpp_quote(" dims[i] = obj.dims[i];") 134 | cpp_quote(" stride0 = obj.stride0;") 135 | cpp_quote(" stride1 = obj.stride1;") 136 | cpp_quote(" data = obj.data;") 137 | cpp_quote(" obj.data = nullptr;") 138 | cpp_quote(" return *this;") 139 | cpp_quote(" }") 140 | cpp_quote(" ") 141 | cpp_quote("private:") 142 | cpp_quote(" void release () {") 143 | cpp_quote(" if (data) {") 144 | cpp_quote(" CComSafeArray tmp;") 145 | cpp_quote(" tmp.Attach(data);") 146 | cpp_quote(" data = nullptr;") 147 | cpp_quote(" }") 148 | cpp_quote(" }") 149 | cpp_quote(" ") 150 | cpp_quote(" Image3d & operator = (const Image3d& obj) = delete; ///< disallow assignment operator") 151 | cpp_quote("};") 152 | cpp_quote("") 153 | cpp_quote("extern \"C\"{") 154 | cpp_quote("#endif") 155 | cpp_quote("") 156 | cpp_quote("#if defined _WIN64 || defined __x86_64__") 157 | cpp_quote("static_assert(sizeof(Image3d) == 40, \"Image3d size mismatch\");") 158 | cpp_quote("#else") 159 | cpp_quote("static_assert(sizeof(Image3d) == 32, \"Image3d size mismatch\");") 160 | cpp_quote("#endif") 161 | 162 | 163 | typedef [ 164 | helpstring("3D image geometry description that matches C.8.X.2.1.2 'Transducer Frame of Reference' in DICOM Enhanced Ultrasound (sup 43)\n" 165 | "All units are in meter [m] with orthogonal axes forming a right-handed coordinate system.\n" 166 | "Coordinates are relative to the tip of the transducer, with origin at the center of the aperture. The X- & Z-axes follow the 1st and 2nd aperture axes, and the Y-axis point into the imaged body.")] 167 | struct Cart3dGeom { 168 | // coordinates are not arrays to avoid "warning MIDL2492: structure containing array of float/double might not be marshalled correctly when using type library marshalling" 169 | float origin_x; ///< coordinate of 1st sample 170 | float origin_y; 171 | float origin_z; 172 | 173 | float dir1_x; ///< 1st direction vector (from first to last column) 174 | float dir1_y; 175 | float dir1_z; 176 | 177 | float dir2_x; ///< 2nd direction vector (from first to last row) 178 | float dir2_y; 179 | float dir2_z; 180 | 181 | float dir3_x; ///< 3rd direction vector (from first to last plane) 182 | float dir3_y; 183 | float dir3_z; 184 | } Cart3dGeom; 185 | 186 | cpp_quote("") 187 | cpp_quote("#ifndef __cplusplus") 188 | 189 | typedef [ 190 | helpstring("ECG time series.")] 191 | struct EcgSeries { 192 | [helpstring("time of 1st sample [seconds]")] 193 | double start_time; 194 | 195 | [helpstring("time difference between each sample [seconds]")] 196 | double delta_time; 197 | 198 | [helpstring("ECG sample waveform")] 199 | SAFEARRAY(float) samples; 200 | 201 | [helpstring("QRS/R-wave trig time(s) [seconds] within series [optional]")] 202 | SAFEARRAY(double) trig_times; 203 | } EcgSeries; 204 | 205 | cpp_quote("") 206 | cpp_quote("#else // __cplusplus") 207 | cpp_quote("} // extern \"C\"") 208 | cpp_quote("") 209 | cpp_quote("struct EcgSeries {") 210 | cpp_quote(" double start_time = 0;") 211 | cpp_quote(" double delta_time = 0;") 212 | cpp_quote(" SAFEARRAY * samples = nullptr; ///< float array") 213 | cpp_quote(" SAFEARRAY * trig_times = nullptr; ///< double array") 214 | cpp_quote(" ") 215 | cpp_quote(" /* Primary ctor initializes to empty EcgSeries. */") 216 | cpp_quote(" EcgSeries() {") 217 | cpp_quote(" }") 218 | cpp_quote(" /** Copy ctor. Performs deep copy. */") 219 | cpp_quote(" EcgSeries(const EcgSeries& obj) {") 220 | cpp_quote(" start_time = obj.start_time;") 221 | cpp_quote(" delta_time = obj.delta_time;") 222 | cpp_quote(" CComSafeArray samples_tmp;") 223 | cpp_quote(" samples_tmp.Attach(obj.samples);") 224 | cpp_quote(" samples_tmp.CopyTo(&samples);") 225 | cpp_quote(" samples_tmp.Detach();") 226 | cpp_quote(" CComSafeArray trig_tmp;") 227 | cpp_quote(" trig_tmp.Attach(obj.trig_times);") 228 | cpp_quote(" trig_tmp.CopyTo(&trig_times);") 229 | cpp_quote(" trig_tmp.Detach();") 230 | cpp_quote(" }") 231 | cpp_quote(" ") 232 | cpp_quote(" ~EcgSeries() {") 233 | cpp_quote(" release(); // clear existing state") 234 | cpp_quote(" }") 235 | cpp_quote(" ") 236 | cpp_quote(" /** Move assignment.*/") 237 | cpp_quote(" EcgSeries& operator = (EcgSeries&& obj) {") 238 | cpp_quote(" release(); // clear existing state") 239 | cpp_quote(" ") 240 | cpp_quote(" start_time = obj.start_time;") 241 | cpp_quote(" obj.start_time = 0;") 242 | cpp_quote(" delta_time = obj.delta_time;") 243 | cpp_quote(" obj.delta_time = 0;") 244 | cpp_quote(" samples = obj.samples;") 245 | cpp_quote(" obj.samples = nullptr;") 246 | cpp_quote(" trig_times = obj.trig_times;") 247 | cpp_quote(" obj.trig_times = nullptr;") 248 | cpp_quote(" return *this;") 249 | cpp_quote(" }") 250 | cpp_quote(" ") 251 | cpp_quote("private:") 252 | cpp_quote(" void release () {") 253 | cpp_quote(" if (samples) {") 254 | cpp_quote(" CComSafeArray samples_tmp;") 255 | cpp_quote(" samples_tmp.Attach(samples);") 256 | cpp_quote(" samples = nullptr;") 257 | cpp_quote(" }") 258 | cpp_quote(" if (trig_times) {") 259 | cpp_quote(" CComSafeArray trig_tmp;") 260 | cpp_quote(" trig_tmp.Attach(trig_times);") 261 | cpp_quote(" trig_times = nullptr;") 262 | cpp_quote(" }") 263 | cpp_quote(" }") 264 | cpp_quote(" ") 265 | cpp_quote(" EcgSeries & operator = (const EcgSeries& obj) = delete; ///< disallow assignment operator") 266 | cpp_quote("};") 267 | cpp_quote("") 268 | cpp_quote("extern \"C\"{") 269 | cpp_quote("#endif") 270 | cpp_quote("") 271 | cpp_quote("#if defined _WIN64 || defined __x86_64__") 272 | cpp_quote("static_assert(sizeof(EcgSeries) == 4*8, \"EcgSeries size mismatch\");") 273 | cpp_quote("#else") 274 | cpp_quote("static_assert(sizeof(EcgSeries) == 2*8+2*4, \"EcgSeries size mismatch\");") 275 | cpp_quote("#endif") 276 | 277 | [ object, 278 | oleautomation, // use "automation" marshaler (oleaut32.dll) 279 | uuid(D483D815-52DD-4750-8CA2-5C6C489588B6), 280 | helpstring("Interface for retrieving 3D image data.")] 281 | interface IImage3dSource : IUnknown { 282 | [helpstring("Get the number of frames available")] 283 | HRESULT GetFrameCount ([out,retval] unsigned int * size); 284 | 285 | [helpstring("Get the time of all frames (useful for matching frame indices to ECG before retrieving image data) ")] 286 | HRESULT GetFrameTimes ([out, retval] SAFEARRAY(double) * frame_times); 287 | 288 | [helpstring("Get image data (const) for a given frame within a specified geometry. The returned frame might have lower resolution than requested.")] 289 | HRESULT GetFrame ([in] unsigned int index, [in] Cart3dGeom geom, [in] unsigned short max_resolution[3], [out,retval] Image3d * data); 290 | 291 | [helpstring("Get a bounding box encapsulating all image data. Can be used as intput to GetFrame to avoid cropping.")] 292 | HRESULT GetBoundingBox ([out,retval] Cart3dGeom * geom); 293 | 294 | [helpstring("Retrieve color-map table for mapping image intensities to RGBx values. Length is 256.")] 295 | HRESULT GetColorMap ([out,retval] SAFEARRAY(unsigned int) * map); 296 | 297 | [helpstring("Get ECG data if available [optional]. Shall return S_OK with an empty EcgSeries (all struct members set to 0) if EGC is not available")] 298 | HRESULT GetECG ([out,retval] EcgSeries * ecg); 299 | 300 | [helpstring("")] 301 | HRESULT GetProbeInfo ([out,retval] ProbeInfo * probe); 302 | 303 | [helpstring("Get per-file DICOM UID string (to be matched against corresponding file)")] 304 | HRESULT GetSopInstanceUID ([out,string,retval] BSTR * uid_str); 305 | }; 306 | 307 | 308 | typedef [ 309 | v1_enum, // 32bit enum size 310 | helpstring("Image3dAPI error codes.")] 311 | enum Image3dError { 312 | Image3d_SUCCESS = 0, 313 | Image3d_ACCESS_FAILURE = 1, ///< file missing, inaccessible or locked 314 | Image3d_VALIDATION_FAILURE = 2, ///< file corrupt or does not contain required vendor tags 315 | Image3d_NOT_YET_SUPPORTED = 3, ///< the loader is too old to read the file and need to be updated 316 | Image3d_SUPPORT_DISCONTINUED = 4, ///< the loader no longer support the file version 317 | } Image3dError; 318 | 319 | 320 | [ object, 321 | oleautomation, // use "automation" marshaler (oleaut32.dll) 322 | uuid(CD30759B-EB38-4469-9CA5-4DF75737A31B), 323 | helpstring("Factory for loading 3D image data from a file.\n" 324 | "Implementors are responsible for also providing details on relevant DICOM tags that indicate that the loader might support the file.")] 325 | interface IImage3dFileLoader : IUnknown { 326 | [helpstring("Load proprietary image file.\n" 327 | "The file might already by opened elsewhere, so no exclusive locks can be taken.\n" 328 | "The function shall return quickly with error type and diagnostic message (in english) in case of failure.")] 329 | HRESULT LoadFile ([in,string] BSTR file_name, [out] Image3dError * error_type, [out,string] BSTR * error_msg); 330 | 331 | HRESULT GetImageSource ([out,retval] IImage3dSource ** img_src); 332 | }; 333 | -------------------------------------------------------------------------------- /Image3dAPI/IImage3dTypeLibraryGenerator.idl: -------------------------------------------------------------------------------- 1 | /* IImage3dTypeLibraryGenerator.idl 2 | * Generates a type library file for IImage3d.idl for use in interface implementation 3 | */ 4 | 5 | 6 | import "oaidl.idl"; 7 | import "IImage3d.idl"; 8 | 9 | 10 | [ 11 | uuid(3ff1aab8-f3d8-33d4-825d-00104b3646c0), 12 | helpstring("Interface to generate IImage3dAPI.tlb .") 13 | ] 14 | library Image3dlib 15 | { 16 | importlib("stdole32.tlb"); 17 | importlib("stdole2.tlb"); 18 | 19 | enum Image3dAPIVersion; 20 | interface IImage3dFileLoader; 21 | }; 22 | -------------------------------------------------------------------------------- /Image3dAPI/Image3dAPI.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | Win32 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {4ADC48A2-1A9E-4F7B-A048-67FE02D31CB3} 23 | My3dAPI 24 | 10.0 25 | 26 | 27 | 28 | StaticLibrary 29 | true 30 | v142 31 | MultiByte 32 | 33 | 34 | StaticLibrary 35 | true 36 | v142 37 | MultiByte 38 | 39 | 40 | StaticLibrary 41 | false 42 | v142 43 | true 44 | MultiByte 45 | 46 | 47 | StaticLibrary 48 | false 49 | v142 50 | true 51 | MultiByte 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | $(SolutionDir)$(Platform)\$(Configuration)\ 71 | $(Platform)\$(Configuration)\ 72 | 73 | 74 | $(SolutionDir)$(Platform)\$(Configuration)\ 75 | $(Platform)\$(Configuration)\ 76 | 77 | 78 | 79 | Level3 80 | Disabled 81 | true 82 | 83 | 84 | true 85 | 86 | 87 | $(IntDir) 88 | %(Filename).h 89 | $(OutDir)$(ProjectName).tlb 90 | 91 | 92 | TlbImp.exe /machine:x86 "$(OutDir)$(TargetName).tlb" /out:"$(OutDir)$(TargetName).dll" 93 | :: Copy interface definitions to shared project dir 94 | xcopy /Y /D "$(IntDir)*.h" "$(ProjectDir)" || exit /b 1 95 | xcopy /Y /D "$(IntDir)*.c" "$(ProjectDir)" || exit /b 1 96 | :: Copy type libraries to shared platform folder 97 | xcopy /Y /D "$(OutDir)$(TargetName).tlb" "$(SolutionDir)$(Platform)" || exit /b 1 98 | xcopy /Y /D "$(OutDir)$(TargetName).dll" "$(SolutionDir)$(Platform)" || exit /b 1 99 | 100 | 101 | 102 | 103 | Level3 104 | Disabled 105 | true 106 | 107 | 108 | true 109 | 110 | 111 | $(IntDir) 112 | %(Filename).h 113 | $(OutDir)$(ProjectName).tlb 114 | 115 | 116 | TlbImp.exe /machine:x64 "$(OutDir)$(TargetName).tlb" /out:"$(OutDir)$(TargetName).dll" 117 | :: Copy interface definitions to shared project dir 118 | xcopy /Y /D "$(IntDir)*.h" "$(ProjectDir)" || exit /b 1 119 | xcopy /Y /D "$(IntDir)*.c" "$(ProjectDir)" || exit /b 1 120 | :: Copy type libraries to shared platform folder 121 | xcopy /Y /D "$(OutDir)$(TargetName).tlb" "$(SolutionDir)$(Platform)" || exit /b 1 122 | xcopy /Y /D "$(OutDir)$(TargetName).dll" "$(SolutionDir)$(Platform)" || exit /b 1 123 | 124 | 125 | 126 | 127 | Level3 128 | MaxSpeed 129 | true 130 | true 131 | true 132 | 133 | 134 | true 135 | true 136 | true 137 | 138 | 139 | $(IntDir) 140 | %(Filename).h 141 | $(OutDir)$(ProjectName).tlb 142 | 143 | 144 | TlbImp.exe /machine:x86 "$(OutDir)$(TargetName).tlb" /out:"$(OutDir)$(TargetName).dll" 145 | :: Copy interface definitions to shared project dir 146 | xcopy /Y /D "$(IntDir)*.h" "$(ProjectDir)" || exit /b 1 147 | xcopy /Y /D "$(IntDir)*.c" "$(ProjectDir)" || exit /b 1 148 | :: Copy type libraries to shared platform folder 149 | xcopy /Y /D "$(OutDir)$(TargetName).tlb" "$(SolutionDir)$(Platform)" || exit /b 1 150 | xcopy /Y /D "$(OutDir)$(TargetName).dll" "$(SolutionDir)$(Platform)" || exit /b 1 151 | 152 | 153 | 154 | 155 | Level3 156 | MaxSpeed 157 | true 158 | true 159 | true 160 | 161 | 162 | true 163 | true 164 | true 165 | 166 | 167 | $(IntDir) 168 | %(Filename).h 169 | $(OutDir)$(ProjectName).tlb 170 | 171 | 172 | TlbImp.exe /machine:x64 "$(OutDir)$(TargetName).tlb" /out:"$(OutDir)$(TargetName).dll" 173 | :: Copy interface definitions to shared project dir 174 | xcopy /Y /D "$(IntDir)*.h" "$(ProjectDir)" || exit /b 1 175 | xcopy /Y /D "$(IntDir)*.c" "$(ProjectDir)" || exit /b 1 176 | :: Copy type libraries to shared platform folder 177 | xcopy /Y /D "$(OutDir)$(TargetName).tlb" "$(SolutionDir)$(Platform)" || exit /b 1 178 | xcopy /Y /D "$(OutDir)$(TargetName).dll" "$(SolutionDir)$(Platform)" || exit /b 1 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /Image3dAPI/Image3dAPI.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Image3dAPI/RegistryCheck.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "ComSupport.hpp" 4 | 5 | 6 | static const REGSAM WOW_SAME_AS_CLIENT = 0; 7 | 8 | /** Compare the version of a COM class against the current Image3dAPI version. 9 | NOTE: Not compatible with reg-free COM, since the COM class version is not included in the manifest. 10 | \param clsid - COM class ID (hexadecimal) 11 | \param bitness - Part of the registry to query. Alternatives: 12 | WOW_SAME_AS_CLIENT Query part of regitry that matches client bitnes 13 | KEY_WOW64_32KEY Query 32-bit part of registry, regardless of client bitness 14 | KEY_WOW64_64KEY Query 64-bit part of registry, regardless of client bitness */ 15 | static HRESULT CheckImage3dAPIVersion (CLSID clsid, REGSAM bitness = WOW_SAME_AS_CLIENT) { 16 | // build registry path") 17 | CComBSTR reg_path(L"CLSID\\"); 18 | reg_path.Append(clsid); 19 | reg_path.Append(L"\\Version"); 20 | 21 | // extract COM class version 22 | CRegKey cls_reg; 23 | if (cls_reg.Open(HKEY_CLASSES_ROOT, reg_path, KEY_READ | bitness) != ERROR_SUCCESS) 24 | return E_NOT_SET; 25 | ULONG ver_str_len = 0; 26 | if (cls_reg.QueryStringValue(nullptr, nullptr, &ver_str_len) != ERROR_SUCCESS) 27 | return E_NOT_SET; 28 | std::wstring ver_str(ver_str_len, L'\0'); 29 | if (cls_reg.QueryStringValue(nullptr, const_cast(ver_str.data()), &ver_str_len) != ERROR_SUCCESS) 30 | return E_NOT_SET; 31 | ver_str.resize(ver_str_len-1); // remove extra zero-termination 32 | 33 | // compare against current Image3dAPI version 34 | std::wstring cur_ver = std::to_wstring(Image3dAPIVersion::IMAGE3DAPI_VERSION_MAJOR) 35 | +L"."+std::to_wstring(Image3dAPIVersion::IMAGE3DAPI_VERSION_MINOR); 36 | if (ver_str == cur_ver) 37 | return S_OK; 38 | else 39 | return E_INVALIDARG; // version mismatch 40 | } 41 | 42 | struct SupportedManufacturerModels { 43 | /** Read list of suported (manufacturer,model)-pairs for a given plugin. 44 | \param clsid - COM class ID (hexadecimal) 45 | \param bitness - Part of the registry to query. Alternatives: 46 | WOW_SAME_AS_CLIENT Query part of regitry that matches client bitnes 47 | KEY_WOW64_32KEY Query 32-bit part of registry, regardless of client bitness 48 | KEY_WOW64_64KEY Query 64-bit part of registry, regardless of client bitness */ 49 | static std::vector ReadList (CLSID clsid, REGSAM bitness = WOW_SAME_AS_CLIENT) { 50 | // build registry path") 51 | CComBSTR reg_path(L"CLSID\\"); 52 | reg_path.Append(clsid); 53 | reg_path.Append(L"\\SupportedManufacturerModels"); 54 | 55 | std::vector list; 56 | 57 | // extract COM class 58 | CRegKey cls_reg; 59 | if (cls_reg.Open(HKEY_CLASSES_ROOT, reg_path, KEY_READ | bitness) != ERROR_SUCCESS) 60 | return list; 61 | 62 | 63 | for (DWORD idx = 0;; ++idx) { 64 | // read key name and value length 65 | // registry key names are guaranteed to not exceed 16k characters (https://docs.microsoft.com/nb-no/windows/desktop/SysInfo/registry-element-size-limits) 66 | DWORD name_len = 16384; // key name length (excluding null-termination) 67 | std::wstring name(name_len, L'\0'); 68 | DWORD type = 0; 69 | DWORD value_len = 0; // in bytes 70 | auto res = RegEnumValue(cls_reg, idx, /*out*/const_cast(name.data()), &name_len, NULL, &type, nullptr/*out*/, &value_len); 71 | if (res != ERROR_SUCCESS) 72 | break; 73 | name.resize(name_len); // shrink to fit 74 | 75 | if (type != REG_SZ) 76 | continue; // value not a string 77 | 78 | // read key value 79 | value_len /= 2; // convert lenght from bytes to #chars (including null-termination) 80 | std::wstring value(value_len, L'\0'); 81 | res = cls_reg.QueryStringValue(name.c_str(), const_cast(value.data()), &value_len); 82 | if (res != ERROR_SUCCESS) 83 | break; 84 | value.resize(value_len-1); // remove null-termination 85 | 86 | // split key value on ';' 87 | std::vector models; 88 | for (size_t pos = 0;;) { 89 | auto prev = pos; 90 | pos = value.find(L';', pos); 91 | 92 | if (pos == std::wstring::npos) { 93 | // no more separators found 94 | models.push_back(value.substr(prev)); 95 | break; 96 | } else { 97 | models.push_back(value.substr(prev, pos - prev)); 98 | pos += 1; // skip separator 99 | } 100 | } 101 | 102 | list.push_back({name, models}); 103 | } 104 | 105 | return list; 106 | } 107 | 108 | std::wstring manufacturer; ///< corresponds to Manufacturer tag, DICOM (0008,0070) 109 | std::vector models; ///< associated Manufacturer's Model Name tags, DICOM (0008,1090). The strings might contain '*' as wildcard 110 | }; 111 | -------------------------------------------------------------------------------- /Image3dAPI/UNREGISTER_Image3dAPI.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo NOTICE: Script MUST be run as Administrator. 3 | :: Errors from "reg" tool are muted to avoid flooding the build log with errors from already deleted registry entries. 4 | 5 | :: Fix issue with "Run as Administrator" current dir 6 | setlocal enableextensions 7 | cd /d "%~dp0" 8 | 9 | 10 | :: Remove all traces of Image3dAPI interfaces from registry 11 | 12 | :: IImage3dTypeLibraryGenerator.idl 13 | reg delete "HKCR\TypeLib\{3ff1aab8-f3d8-33d4-825d-00104b3646c0}" /f 2> NUL 14 | 15 | for %%P in (32 64) do ( 16 | :: IImage3d.idl 17 | reg delete "HKCR\Interface\{D483D815-52DD-4750-8CA2-5C6C489588B6}" /f /reg:%%P 2> NUL 18 | reg delete "HKCR\Interface\{CD30759B-EB38-4469-9CA5-4DF75737A31B}" /f /reg:%%P 2> NUL 19 | ) 20 | 21 | ::pause 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | API License Agreement for GE Healthcare’s Image3DAPI 2 | 3 | This API License Agreement (“Agreement”) contains the terms and conditions that govern your access to and use of GE Healthcare’s Image3DAPI (“Image3DAPI”), as further detailed below, and is an agreement between GE Healthcare (“GE”) and you or the entity on whose behalf you accept these terms. By clicking an “I Accept” button or check box presented with these terms, or by using the access credentials provided to download and/or use the Image3DAPI, you agree to the following terms and conditions. 4 | 5 | 1. Definition of Terms 6 | “Image3DAPI” defines a vendor neutral/agnostic Application Programming Interface (“API”) for loading 3D ultrasound image data stored in proprietary data formats. A separate loader library is required for each data format. 7 | 8 | 2. Copyright Notice 9 | Copyright (c) 2017, GE Healthcare 10 | All rights reserved. 11 | 12 | 3. User Terms and Conditions 13 | Redistribution and use of the Image3DAPI in source and/or binary forms, with or without modification, is permitted provided that the following conditions are met: 14 | • Redistributions of source code must retain the above Copyright Notice, this list of conditions and the following disclaimer. 15 | • Redistributions in binary form must reproduce the above Copyright Notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 16 | • Neither the name of the GE Healthcare nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 17 | 18 | 4. Disclaimer and Limitation of Liabilities 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GE HEALTHCARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /PackagingGE/BuildNuGet.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set NUGET_REPO=%1 3 | 4 | set PATH=%PATH%;C:\Python27 5 | set PATH=%PATH%;"C:\Program Files\Git\bin" 6 | 7 | :: Dependencies: 8 | :: * Python installed 9 | :: * Visual Studio command prompt (msbuild & C++ compiler in PATH) 10 | :: * NuGet.exe in PATH (https://www.nuget.org/downloads) 11 | 12 | pushd .. 13 | 14 | if DEFINED NUGET_REPO ( 15 | PackagingGE\DetermineNextTag.py minor > NEW_TAG.txt 16 | ) else ( 17 | :: Local nuget build. Not to be deployed. 18 | PackagingGE\DetermineNextTag.py patch > NEW_TAG.txt 19 | ) 20 | IF %ERRORLEVEL% NEQ 0 exit /B 1 21 | set /p VERSION=< NEW_TAG.txt 22 | 23 | 24 | echo Building projects: 25 | msbuild /nologo /verbosity:minimal /target:Image3dAPI;DummyLoader /property:Configuration="Debug";Platform="Win32" Image3dAPI.sln 26 | IF %ERRORLEVEL% NEQ 0 exit /B 1 27 | msbuild /nologo /verbosity:minimal /target:Image3dAPI;DummyLoader /property:Configuration="Release";Platform="Win32" Image3dAPI.sln 28 | IF %ERRORLEVEL% NEQ 0 exit /B 1 29 | msbuild /nologo /verbosity:minimal /target:Image3dAPI;DummyLoader /property:Configuration="Debug";Platform="x64" Image3dAPI.sln 30 | IF %ERRORLEVEL% NEQ 0 exit /B 1 31 | msbuild /nologo /verbosity:minimal /target:Image3dAPI;DummyLoader /property:Configuration="Release";Platform="x64" Image3dAPI.sln 32 | IF %ERRORLEVEL% NEQ 0 exit /B 1 33 | 34 | echo Determine previous tag: 35 | git describe --abbrev=0 --tags > PREV_TAG.txt 36 | set /p PREV_TAG=< PREV_TAG.txt 37 | 38 | 39 | echo Creating new GIT tag: 40 | git tag %VERSION% 41 | if DEFINED NUGET_REPO ( 42 | git push origin %VERSION% 43 | ) 44 | 45 | echo Generating changelog (with tag decoration, graph and change stats): 46 | git log %PREV_TAG%..%VERSION% --decorate --graph --stat > changelog.txt 47 | 48 | pushd Image3dApi 49 | CALL ..\PackagingGE\PackagePublishNuget.bat ..\PackagingGE\Image3dAPI.nuspec %VERSION% %NUGET_REPO% 50 | IF %ERRORLEVEL% NEQ 0 exit /B 1 51 | popd 52 | 53 | pushd DummyLoader 54 | CALL ..\PackagingGE\PackagePublishNuget.bat ..\PackagingGE\DummyLoader.redist.nuspec %VERSION% %NUGET_REPO% 55 | IF %ERRORLEVEL% NEQ 0 exit /B 1 56 | popd 57 | 58 | popd 59 | echo [done] 60 | -------------------------------------------------------------------------------- /PackagingGE/DetermineNextTag.py: -------------------------------------------------------------------------------- 1 | # Script to determine highest tag number and increment version 2 | from __future__ import print_function 3 | from distutils.version import StrictVersion 4 | import subprocess 5 | import sys 6 | 7 | 8 | def DetermineHighestTagVersion (): 9 | # retrive all tags 10 | cmd = 'git tag' 11 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 12 | tags = [] 13 | for line in p.stdout.readlines(): 14 | line = line.decode() 15 | line = line.strip(' /\r\n') # remove '/' & newline suffix 16 | tags.append(line) 17 | 18 | # identify newest tag 19 | newest_tag = tags[0] 20 | for tag in tags: 21 | try: 22 | if StrictVersion(tag) > StrictVersion(newest_tag): 23 | newest_tag = tag 24 | except: 25 | print('Ignoring unparsable tag '+tag, file=sys.stderr) 26 | 27 | return newest_tag 28 | 29 | 30 | def IncrementVersionNumber (version, incr_idx): 31 | # tokenize version 32 | tokens = version.split('.') 33 | 34 | # append zero tokens to avoid out-of-bounds access 35 | while incr_idx >= len(tokens): 36 | tokens.append('0') 37 | 38 | # increment token 39 | tokens[incr_idx] = str(int(tokens[incr_idx])+1) 40 | 41 | # set lower version indices to zero (eg. go from '0.9' to '1.0') 42 | for i in range(incr_idx+1, len(tokens)): 43 | tokens[i] = '0' 44 | 45 | # joint tokens into new version string 46 | return '.'.join(tokens) 47 | 48 | 49 | if __name__ == "__main__": 50 | incr_type = sys.argv[1] # version increment type (major, minor, patch) 51 | 52 | # determine which version index to increment 53 | if incr_type == 'major': incr_idx = 0 54 | elif incr_type == 'minor': incr_idx = 1 55 | elif incr_type == 'patch': incr_idx = 2 56 | else: raise Exception('unknown incr_type') 57 | 58 | prev_ver = DetermineHighestTagVersion() 59 | new_ver = IncrementVersionNumber(prev_ver, incr_idx) 60 | 61 | # output new version number to stdout, without newline suffix 62 | sys.stdout.write(new_ver) 63 | -------------------------------------------------------------------------------- /PackagingGE/DummyLoader.redist.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DummyLoader.redist 5 | dummyloader, native 6 | GE Vingmed Ultrasound (GE Healthcare) 7 | <> 8 | 9 | http://PROPRIETARY_LICENSE 10 | http://www.ge.com/sites/all/themes/ge_2012/favicon.ico 11 | false 12 | Dummy implementation of COM APIs for cross-vendor exchange of 3D ultrasound image data. 13 | 14 | 15 | Fredrik Orderud 16 | <> 17 | changelog in docs/changelog.txt 18 | Copyright 2015-2018 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /PackagingGE/DummyLoader.redist.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /PackagingGE/Image3dAPI.net.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildThisFileDirectory)..\native\lib\Win32\Image3dAPI.dll 8 | true 9 | 10 | 11 | 12 | 13 | $(MSBuildThisFileDirectory)..\native\lib\x64\Image3dAPI.dll 14 | true 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /PackagingGE/Image3dAPI.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image3dAPI 5 | image3dapi, native 6 | GE Ultrasound (GE Healthcare) 7 | <> 8 | 9 | http://PROPRIETARY_LICENSE 10 | http://www.ge.com/sites/all/themes/ge_2012/favicon.ico 11 | false 12 | COM APIs for cross-vendor exchange of 3D ultrasound image data 13 | 14 | 15 | Fredrik Orderud 16 | <> 17 | 18 | changelog in docs/changelog.txt 19 | Copyright 2015-2018 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /PackagingGE/Image3dAPI.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(MSBuildThisFileDirectory)include\;$(MSBuildThisFileDirectory)include\Image3dAPI;%(AdditionalIncludeDirectories) 6 | 7 | 8 | $(MSBuildThisFileDirectory)include\Image3dAPI;%(AdditionalIncludeDirectories) 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PackagingGE/PackagePublishNuget.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set NUSPEC_FILE=%1 3 | set VERSION_NUMBER=%2 4 | set NUGET_REPO=%3 5 | 6 | :: Change NuGet packaging version and project URL 7 | ..\PackagingGE\SetNuspecVersion.py %NUSPEC_FILE% %VERSION_NUMBER% https://github.com/MedicalUltrasound/Image3dAPI/tree/%VERSION_NUMBER% 8 | IF %ERRORLEVEL% NEQ 0 exit /B 1 9 | 10 | :: Package artifacts 11 | nuget pack %NUSPEC_FILE% 12 | IF %ERRORLEVEL% NEQ 0 exit /B 1 13 | 14 | :: Publish artifact 15 | if NOT DEFINED NUGET_REPO exit /B 0 16 | :: Use timeout not dividable by 60 as work-around for https://nuget.codeplex.com/workitem/3917 17 | NuGet.exe push *.nupkg -Source %NUGET_REPO% -Timeout 1201 18 | IF %ERRORLEVEL% NEQ 0 exit /B 1 19 | -------------------------------------------------------------------------------- /PackagingGE/SetNuspecVersion.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import fileinput 3 | import os 4 | import sys 5 | from xml.dom import minidom 6 | 7 | 8 | def IdentifyDependencies (filename): 9 | '''Parse NuGet dependencies from a 'packages.config'-style file''' 10 | 11 | # parse dependencies into a dict 12 | xml_doc = minidom.parse(filename) 13 | dep_dict = {} 14 | for package in xml_doc.childNodes[0].childNodes: 15 | if package.nodeName == 'package': 16 | id = package.attributes['id'].value 17 | version = package.attributes['version'].value 18 | dep_dict[id] = version 19 | 20 | return dep_dict 21 | 22 | 23 | def UpdateNuspec (filename, version, url, dependencies): 24 | '''Set the minor version in an nuspec file''' 25 | 26 | for line in fileinput.input(filename, inplace=True): 27 | if '<>' in line: 28 | # use current git version 29 | print(line.replace('<>', version), end='') 30 | elif '<>' in line: 31 | print(line.replace('<>', url), end='') 32 | elif '<>' in line: 33 | # line on "" format 34 | tokens = line.split() 35 | for token in tokens: 36 | if token[:4] == 'id="': 37 | package = token[4:-1] 38 | elif token[:6] == 'version="': 39 | pass # assume that tag contains "<>" string 40 | line = ' ' 41 | print(line, end='\n') 42 | elif '<>' in line: 43 | # convert dependency dict to nuspec XML representation 44 | dep_str = '' 45 | for key in dependencies: 46 | dep_str += '\n ' 47 | print(line.replace('<>', dep_str), end='') 48 | else: 49 | print(line, end='') 50 | 51 | 52 | if __name__ == "__main__": 53 | filename = sys.argv[1] # nuspec file 54 | version = sys.argv[2] # version number 55 | url = sys.argv[3] # release URL 56 | 57 | # parse dependencies from NuGet config file arguments 58 | deps = {} 59 | for i in range(4, len(sys.argv)): 60 | print('Parsing dependencies from '+sys.argv[i]) 61 | tmp = IdentifyDependencies(sys.argv[i]) 62 | for key in tmp: 63 | if key in deps: 64 | if deps[key] != tmp[key]: 65 | raise BaseException('Version mismatch in package '+key+': '+deps[key]+' vs. '+tmp[key]) 66 | else: 67 | deps[key] = tmp[key] 68 | 69 | print('Setting version and URL of '+filename+' to '+version+' and '+url) 70 | UpdateNuspec(filename, version, url, deps) 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image3dAPI 2 | Interfaces for inter-vendor exchange of 3D ultrasound data, together with test code. 3 | 4 | ## Description 5 | Application Programming Interface (API) to load any vendor’s proprietary ultrasound data, commonly referred to as “raw” data, using a well-defined data structures that can be used to render, display and manipulate 3D data within any vendor’s review and analysis tools. 6 | 7 | Each ultrasound vendor will need to provide an implementation of this API specific to their proprietary data format. Any software or analysis tool using the API to load data can be agnostic to the specifics of the data loader, as only the common API needs to be considered. 8 | 9 | ### Content 10 | * [DummyLoader](DummyLoader/) - Example loader library 11 | * [Image3dAPI](Image3dAPI/) - API definitions 12 | * [PackagingGE](PackagingGE/) - NuGet packaging configuration 13 | * [RegFreeTest](RegFreeTest/) - Example of how to leverage manifest files to avoid COM registration 14 | * [SandboxTest](SandboxTest/) - Example of how to sandbox a loader in a separate process 15 | * [TestPython](TestPython/) - Python-based sample code 16 | * [TestViewer](TestViewer/) - Simple .NET-based image viewer 17 | 18 | ## Benefits 19 | This will allow analysis tools to support data from multiple vendors, rather than having to use “plugins”, e.g. GE EchoPAC or Philips QLab. This approach also allows the vendors to simplify their commercial offerings to end-users since it will no longer be required to purchase multiple plugins from multiple vendors and coordinate multiple installs. 20 | 21 | Furthermore, companies outside the ultrasound system vendors (such as 3D printing, Virtual Reality, Surgical Planning, researchers, etc.) will be able to load 3D echo data seamlessly from all participating vendors. 22 | 23 | ## Relationship to DICOM 24 | The DICOM 3D standard is an alternative solution to sharing of 3D ultrasound data. However, DICOM 3D does not allow access to studies that are already stored in existing 3D proprietary formats. Furthermore, the limited vendor adoption of DICOM 3D means that it will take years for it to truly impact the market. 25 | 26 | This approach has advantages over the traditional DICOM data standard technique including complete seamless backward compatibility to existing stored studies, smaller files sizes, faster network transfers, and faster loading of studies (since no conversion from vendor raw data to DICOM format is required). This new approach also allows the vendors to continually innovate as well, instead of being locked into a frozen data specification. 27 | 28 | ## Getting started 29 | Install [Visual Studio](https://visualstudio.microsoft.com/), either professional or community edition. Any version >= 2015 is supported. 30 | Enable the following workloads during installation: 31 | * .Net desktop development, 32 | * Desktop development with C++, and 33 | * Python development. 34 | 35 | In addition, Python.exe need to be registered as default app for running .py scripts. This can be done by right-clicking on a .py file, then selecting "Open with", "Choose another app", locate Python.exe and enable "Always use this app to open .py files". 36 | 37 | 38 | Build instructions: 39 | * Open Visual Studio with administrative privileges. 40 | * Open `Image3dAPI.sln`. 41 | * Build the solution. 42 | 43 | ## Documentation 44 | * [API license](LICENSE.txt) 45 | * [Guidelines](Guidelines.md) for implementing the interface 46 | -------------------------------------------------------------------------------- /RegFreeTest/.gitignore: -------------------------------------------------------------------------------- 1 | /Win32 2 | /x64 3 | /RegFreeTest.vcxproj.user 4 | -------------------------------------------------------------------------------- /RegFreeTest/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "../Image3dAPI/ComSupport.hpp" 2 | #include "../Image3dAPI/IImage3d.h" 3 | #include "../Image3dAPI/RegistryCheck.hpp" 4 | #include 5 | 6 | 7 | void ParseSource (IImage3dSource & source) { 8 | CComSafeArray color_map; 9 | { 10 | SAFEARRAY * tmp = nullptr; 11 | CHECK(source.GetColorMap(&tmp)); 12 | color_map.Attach(tmp); 13 | tmp = nullptr; 14 | } 15 | 16 | Cart3dGeom bbox = {}; 17 | CHECK(source.GetBoundingBox(&bbox)); 18 | 19 | unsigned int frame_count = 0; 20 | CHECK(source.GetFrameCount(&frame_count)); 21 | std::wcout << L"Frame count: " << frame_count << L"\n"; 22 | 23 | for (unsigned int frame = 0; frame < frame_count; ++frame) { 24 | unsigned short max_res[] = {64, 64, 64}; 25 | 26 | // retrieve frame data 27 | Image3d data; 28 | CHECK(source.GetFrame(frame, bbox, max_res, &data)); 29 | } 30 | } 31 | 32 | 33 | int wmain (int argc, wchar_t *argv[]) { 34 | if (argc < 3) { 35 | std::wcout << L"Usage:\n"; 36 | std::wcout << L"RegFreeTest.exe " << std::endl; 37 | return -1; 38 | } 39 | 40 | CComBSTR progid = argv[1]; // e.g. "DummyLoader.Image3dFileLoader" 41 | CComBSTR filename = argv[2]; 42 | 43 | ComInitialize com(COINIT_MULTITHREADED); 44 | 45 | CLSID clsid = {}; 46 | if (FAILED(CLSIDFromProgID(progid, &clsid))) { 47 | std::wcerr << L"ERRORR: Unknown progid " << progid.m_str << L"\n"; 48 | return -1; 49 | } 50 | 51 | // verify that loader library is compatible 52 | //CHECK(CheckImage3dAPIVersion(clsid)); // disabled, since it's not compatible with reg-free COM 53 | 54 | // create loader without accessing the registry (possible since the DummyLoader manifest is linked in) 55 | std::wcout << L"Creating loader " << progid.m_str << L"...\n"; 56 | CComPtr loader; 57 | CHECK(loader.CoCreateInstance(clsid)); 58 | 59 | { 60 | // load file 61 | Image3dError err_type = {}; 62 | CComBSTR err_msg; 63 | HRESULT hr = loader->LoadFile(filename, &err_type, &err_msg); 64 | if (FAILED(hr)) { 65 | std::wcerr << L"LoadFile failed: code=" << err_type << L", message=" << err_msg.m_str << std::endl; 66 | return -1; 67 | } 68 | } 69 | 70 | CComPtr source; 71 | CHECK(loader->GetImageSource(&source)); 72 | 73 | ProbeInfo probe; 74 | CHECK(source->GetProbeInfo(&probe)); 75 | 76 | EcgSeries ecg; 77 | CHECK(source->GetECG(&ecg)); 78 | 79 | ParseSource(*source); 80 | 81 | return 0; 82 | } 83 | -------------------------------------------------------------------------------- /RegFreeTest/RegFreeTest.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | Win32 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {8ABF3DC3-6C48-439B-B3A1-DED84351B808} 23 | Win32Proj 24 | RegFreeTest 25 | 10.0 26 | 27 | 28 | 29 | Application 30 | true 31 | v142 32 | Unicode 33 | 34 | 35 | Application 36 | true 37 | v142 38 | Unicode 39 | 40 | 41 | Application 42 | false 43 | v142 44 | true 45 | Unicode 46 | 47 | 48 | Application 49 | false 50 | v142 51 | true 52 | Unicode 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | true 72 | $(SolutionDir)$(Platform)\$(Configuration)\ 73 | $(Platform)\$(Configuration)\ 74 | 75 | 76 | true 77 | $(SolutionDir)$(Platform)\$(Configuration)\ 78 | $(Platform)\$(Configuration)\ 79 | 80 | 81 | false 82 | $(SolutionDir)$(Platform)\$(Configuration)\ 83 | $(Platform)\$(Configuration)\ 84 | 85 | 86 | false 87 | $(SolutionDir)$(Platform)\$(Configuration)\ 88 | $(Platform)\$(Configuration)\ 89 | 90 | 91 | 92 | Level3 93 | Disabled 94 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 95 | true 96 | 97 | 98 | Console 99 | true 100 | 101 | 102 | $(OutDir)DummyLoaderD.dll.manifest 103 | 104 | 105 | 106 | 107 | Level3 108 | Disabled 109 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 110 | true 111 | 112 | 113 | Console 114 | true 115 | 116 | 117 | $(OutDir)DummyLoaderD.dll.manifest 118 | 119 | 120 | 121 | 122 | Level3 123 | MaxSpeed 124 | true 125 | true 126 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 127 | true 128 | 129 | 130 | Console 131 | true 132 | true 133 | true 134 | 135 | 136 | $(OutDir)DummyLoader.dll.manifest 137 | 138 | 139 | 140 | 141 | Level3 142 | MaxSpeed 143 | true 144 | true 145 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 146 | true 147 | 148 | 149 | Console 150 | true 151 | true 152 | true 153 | 154 | 155 | $(OutDir)DummyLoader.dll.manifest 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /RegFreeTest/RegFreeTest.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SandboxTest/.gitignore: -------------------------------------------------------------------------------- 1 | /Win32 2 | /x64 3 | /SandboxTest.vcxproj.user 4 | -------------------------------------------------------------------------------- /SandboxTest/LowIntegrity.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | 6 | /** RAII class for temporarily impersonating low-integrity level for the current thread. 7 | Intended to be used together with CLSCTX_ENABLE_CLOAKING when creating COM objects. 8 | Based on "Designing Applications to Run at a Low Integrity Level" https://docs.microsoft.com/en-us/previous-versions/dotnet/articles/bb625960(v=msdn.10) */ 9 | struct LowIntegrity { 10 | LowIntegrity() { 11 | HANDLE cur_token = nullptr; 12 | if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, &cur_token)) 13 | abort(); 14 | 15 | if (!DuplicateTokenEx(cur_token, 0, NULL, SecurityImpersonation, TokenPrimary, &m_token)) 16 | abort(); 17 | 18 | CloseHandle(cur_token); 19 | 20 | PSID li_sid = nullptr; 21 | if (!ConvertStringSidToSid(L"S-1-16-4096", &li_sid)) // low integrity SID 22 | abort(); 23 | 24 | // reduce process integrity level 25 | TOKEN_MANDATORY_LABEL TIL = {}; 26 | TIL.Label.Attributes = SE_GROUP_INTEGRITY; 27 | TIL.Label.Sid = li_sid; 28 | if (!SetTokenInformation(m_token, TokenIntegrityLevel, &TIL, sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(li_sid))) 29 | abort(); 30 | 31 | if (!ImpersonateLoggedOnUser(m_token)) // change current thread integrity 32 | abort(); 33 | 34 | LocalFree(li_sid); 35 | li_sid = nullptr; 36 | } 37 | 38 | ~LowIntegrity() { 39 | if (!RevertToSelf()) 40 | abort(); 41 | 42 | CloseHandle(m_token); 43 | m_token = nullptr; 44 | } 45 | 46 | private: 47 | HANDLE m_token = nullptr; 48 | }; 49 | -------------------------------------------------------------------------------- /SandboxTest/Main.cpp: -------------------------------------------------------------------------------- 1 | #include "../Image3dAPI/ComSupport.hpp" 2 | #include "../Image3dAPI/IImage3d.h" 3 | #include "../Image3dAPI/RegistryCheck.hpp" 4 | #include "LowIntegrity.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | class PerfTimer { 13 | using clock = std::chrono::high_resolution_clock; 14 | public: 15 | PerfTimer(const char * prefix, bool enable) : m_prefix(prefix) { 16 | if (enable) 17 | m_start = clock::now(); 18 | 19 | } 20 | ~PerfTimer() { 21 | if (m_start == clock::time_point()) 22 | return; // profiling disabled 23 | 24 | auto stop = clock::now(); 25 | auto duration = std::chrono::duration_cast(stop - m_start).count(); 26 | std::cout << m_prefix << " took " << duration << "ms\n"; 27 | } 28 | private: 29 | const char * m_prefix; 30 | clock::time_point m_start; 31 | }; 32 | 33 | 34 | void ParseSource (IImage3dSource & source, bool verbose, bool profile) { 35 | CComSafeArray color_map; 36 | { 37 | SAFEARRAY * tmp = nullptr; 38 | CHECK(source.GetColorMap(&tmp)); 39 | color_map.Attach(tmp); 40 | tmp = nullptr; 41 | } 42 | 43 | if (verbose) { 44 | std::cout << "Color-map:\n"; 45 | for (unsigned int i = 0; i < color_map.GetCount(); i++) { 46 | unsigned int color = color_map[(int)i]; 47 | uint8_t *rgbx = reinterpret_cast(&color); 48 | std::cout << " [" << (int)rgbx[0] << "," << (int)rgbx[1] << "," << (int)rgbx[2] << "," << (int)rgbx[3] << "]\n"; 49 | } 50 | } 51 | 52 | Cart3dGeom bbox = {}; 53 | CHECK(source.GetBoundingBox(&bbox)); 54 | 55 | if (verbose) { 56 | std::cout << "Bounding box:\n"; 57 | std::cout << " Origin: " << bbox.origin_x << ", " << bbox.origin_y << ", " << bbox.origin_z << "\n"; 58 | std::cout << " Dir1: " << bbox.dir1_x << ", " << bbox.dir1_y << ", " << bbox.dir1_z << "\n"; 59 | std::cout << " Dir2: " << bbox.dir2_x << ", " << bbox.dir2_y << ", " << bbox.dir2_z << "\n"; 60 | std::cout << " Dir3: " << bbox.dir3_x << ", " << bbox.dir3_y << ", " << bbox.dir3_z << "\n"; 61 | } 62 | 63 | unsigned int frame_count = 0; 64 | CHECK(source.GetFrameCount(&frame_count)); 65 | std::wcout << L"Frame count: " << frame_count << L"\n"; 66 | 67 | CComSafeArray frame_times; 68 | { 69 | SAFEARRAY * data = nullptr; 70 | CHECK(source.GetFrameTimes(&data)); 71 | frame_times.Attach(data); 72 | data = nullptr; 73 | } 74 | 75 | if (verbose) 76 | { 77 | std::cout << "Frame times:\n"; 78 | for (unsigned int i = 0; i < frame_times.GetCount(); i++) { 79 | double time = frame_times[(int)i]; 80 | std::cout << " " << time << "\n"; 81 | } 82 | } 83 | 84 | for (unsigned int frame = 0; frame < frame_count; ++frame) { 85 | unsigned short max_res[] = { 64, 64, 64 }; 86 | if (profile) { 87 | max_res[0] = 128; 88 | max_res[1] = 128; 89 | max_res[2] = 128; 90 | } 91 | 92 | // retrieve frame data 93 | Image3d data; 94 | PerfTimer timer("GetFrame", profile); 95 | CHECK(source.GetFrame(frame, bbox, max_res, &data)); 96 | 97 | if (frame == 0) 98 | std::cout << "First frame time: " << data.time << "\n"; 99 | if (frame == frame_count-1) 100 | std::cout << "Last frame time: " << data.time << "\n"; 101 | } 102 | } 103 | 104 | CComPtr CreateLoader(const CComBSTR &progid, const CLSID clsid, const bool profile) 105 | { 106 | CComPtr loader; 107 | std::wcout << L"Creating loader " << progid.m_str << L" in low-integrity mode...\n"; 108 | LowIntegrity low_integrity; 109 | PerfTimer timer("CoCreateInstance", profile); 110 | HRESULT hr = loader.CoCreateInstance(clsid, nullptr, CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING); 111 | if (FAILED(hr)) { 112 | _com_error err(hr); 113 | std::wcerr << L"CoCreateInstance failed: code=" << hr << L", message=" << err.ErrorMessage() << std::endl; 114 | exit(-1); 115 | } 116 | 117 | return loader; 118 | } 119 | 120 | CComPtr LoadFileAndGetImageSource(const CComPtr& loader, const CComBSTR& filename, const bool profile) 121 | { 122 | { 123 | // load file 124 | Image3dError err_type = {}; 125 | CComBSTR err_msg; 126 | PerfTimer timer("LoadFile", profile); 127 | HRESULT hr = loader->LoadFile(filename, &err_type, &err_msg); 128 | if (FAILED(hr)) { 129 | std::wcerr << L"LoadFile failed: code=" << err_type << L", message=" << err_msg.m_str << std::endl; 130 | exit(-1); 131 | } 132 | } 133 | 134 | CComPtr source; 135 | { 136 | PerfTimer timer("GetImageSource", profile); 137 | HRESULT hr = loader->GetImageSource(&source); 138 | if (FAILED(hr)) { 139 | _com_error err(hr); 140 | std::wcerr << L"GetImageSource failed: code=" << hr << L", message=" << err.ErrorMessage() << std::endl; 141 | exit(-1); 142 | } 143 | } 144 | 145 | return source; 146 | } 147 | 148 | int wmain(int argc, wchar_t *argv[]) { 149 | if (argc < 3) { 150 | std::wcout << L"Usage:\n"; 151 | std::wcout << L"SandboxTest.exe [-verbose|-profile] [-threading]" << std::endl; 152 | return -1; 153 | } 154 | 155 | CComBSTR progid = argv[1]; // e.g. "DummyLoader.Image3dFileLoader" 156 | CComBSTR filename = argv[2]; 157 | 158 | std::set options; 159 | for (int i = 3; i < argc; ++i) { 160 | options.insert(argv[i]); 161 | } 162 | 163 | bool verbose = options.find(L"-verbose") != options.end(); // more extensive logging 164 | bool profile = options.find(L"-profile") != options.end(); // profile loader performance 165 | bool test_threading = options.find(L"-threading") != options.end(); // instantiate loader, load file and get image source in a separate thread 166 | 167 | bool test_locked_input = true; 168 | 169 | std::ifstream locked_file; 170 | if (test_locked_input) { 171 | std::wcout << L"Open file: " << argv[2] << " to test read-locked input file in loader." << std::endl; 172 | locked_file.open(filename); 173 | } 174 | 175 | ComInitialize com(COINIT_APARTMENTTHREADED); 176 | 177 | CLSID clsid = {}; 178 | if (FAILED(CLSIDFromProgID(progid, &clsid))) { 179 | std::wcerr << L"ERRORR: Unknown progid " << progid.m_str << L"\n"; 180 | return -1; 181 | } 182 | 183 | auto list = SupportedManufacturerModels::ReadList(clsid); 184 | 185 | // verify that loader library is compatible 186 | // first check loader with matching bitness 187 | REGSAM bitness = WOW_SAME_AS_CLIENT; 188 | if (FAILED(CheckImage3dAPIVersion(clsid, bitness))) { 189 | // then check loader with non-matching bitness 190 | #ifdef _WIN64 191 | bitness = KEY_WOW64_32KEY; 192 | #else 193 | bitness = KEY_WOW64_64KEY; 194 | #endif 195 | if (FAILED(CheckImage3dAPIVersion(clsid, bitness))) { 196 | std::wcerr << L"ERRORR: Loader " << progid.m_str << L" not compatible with current API version.\n"; 197 | return -1; 198 | } 199 | } 200 | 201 | // create loader in a separate "low integrity" dllhost.exe process 202 | CComPtr loader; 203 | CComPtr source; 204 | 205 | if (!test_threading) { 206 | loader = CreateLoader(progid, clsid, profile); 207 | source = LoadFileAndGetImageSource(loader, filename, profile); 208 | } else { 209 | std::wcout << L"Instantiate loader, load file and get image source in a separate thread" << std::endl; 210 | CComPtr stream; 211 | 212 | std::thread loader_thread([&]() { 213 | ComInitialize com_thread(COINIT_APARTMENTTHREADED); 214 | loader = CreateLoader(progid, clsid, profile); 215 | 216 | CComPtr internal_source = LoadFileAndGetImageSource(loader, filename, profile); 217 | 218 | CHECK(CoMarshalInterThreadInterfaceInStream(__uuidof(IImage3dSource), internal_source, &stream)); 219 | }); 220 | 221 | loader_thread.join(); 222 | 223 | CHECK(CoGetInterfaceAndReleaseStream(stream.Detach(), __uuidof(IImage3dSource), (void**)&source)); 224 | } 225 | 226 | { 227 | CComBSTR sopInstanceUID; 228 | CHECK(source->GetSopInstanceUID(&sopInstanceUID)); 229 | std::wcout << L"SOP Instance UID: " << sopInstanceUID.m_str << L"\n"; 230 | } 231 | 232 | ProbeInfo probe; 233 | CHECK(source->GetProbeInfo(&probe)); 234 | std::wcout << L"Probe name: " << probe.name.m_str << L"\n"; 235 | 236 | EcgSeries ecg; 237 | CHECK(source->GetECG(&ecg)); 238 | 239 | if (verbose) { 240 | CComSafeArray samples; 241 | samples.Attach(ecg.samples); // transfer ownership 242 | ecg.samples = nullptr; 243 | 244 | std::wcout << L"ECG sample count: " << samples.GetCount() << L"\n"; 245 | std::wcout << L"First ECG sample time: " << ecg.start_time << L"\n"; 246 | std::wcout << L"Last ECG sample time: " << ecg.start_time + (samples.GetCount()-1)*ecg.delta_time << L"\n"; 247 | 248 | CComSafeArray trig_times; 249 | trig_times.Attach(ecg.trig_times); // transfer ownership 250 | ecg.trig_times = nullptr; 251 | 252 | std::wcout << L"ECG QRS trig times:\n"; 253 | for (unsigned int i = 0; i < trig_times.GetCount(); ++i) 254 | std::wcout << L" " << trig_times[(int)i] << "\n"; 255 | } 256 | 257 | { 258 | PerfTimer timer("ParseSource", profile); 259 | ParseSource(*source, verbose, profile); 260 | } 261 | 262 | return 0; 263 | } 264 | -------------------------------------------------------------------------------- /SandboxTest/SandboxTest.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {DFA5034E-BEA9-4299-A301-38A5DF7FD256} 29 | Win32Proj 30 | SandboxTest 31 | 10.0 32 | 33 | 34 | 35 | Application 36 | true 37 | v142 38 | Unicode 39 | 40 | 41 | Application 42 | false 43 | v142 44 | true 45 | Unicode 46 | 47 | 48 | Application 49 | true 50 | v142 51 | Unicode 52 | 53 | 54 | Application 55 | false 56 | v142 57 | true 58 | Unicode 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | true 78 | $(SolutionDir)$(Platform)\$(Configuration)\ 79 | $(Platform)\$(Configuration)\ 80 | 81 | 82 | true 83 | $(SolutionDir)$(Platform)\$(Configuration)\ 84 | $(Platform)\$(Configuration)\ 85 | 86 | 87 | false 88 | $(SolutionDir)$(Platform)\$(Configuration)\ 89 | $(Platform)\$(Configuration)\ 90 | 91 | 92 | false 93 | $(SolutionDir)$(Platform)\$(Configuration)\ 94 | $(Platform)\$(Configuration)\ 95 | 96 | 97 | 98 | Level3 99 | Disabled 100 | true 101 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 102 | true 103 | 104 | 105 | Console 106 | true 107 | 108 | 109 | 110 | 111 | Level3 112 | Disabled 113 | true 114 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 115 | true 116 | 117 | 118 | Console 119 | true 120 | 121 | 122 | 123 | 124 | Level3 125 | MaxSpeed 126 | true 127 | true 128 | true 129 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 130 | true 131 | 132 | 133 | Console 134 | true 135 | true 136 | true 137 | 138 | 139 | 140 | 141 | Level3 142 | MaxSpeed 143 | true 144 | true 145 | true 146 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 147 | true 148 | 149 | 150 | Console 151 | true 152 | true 153 | true 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /SandboxTest/SandboxTest.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TestPython/.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | -------------------------------------------------------------------------------- /TestPython/ITKExport.py: -------------------------------------------------------------------------------- 1 | ## Sample code to demonstrate how to access Image3dAPI from a python script 2 | import platform 3 | import comtypes 4 | import comtypes.client 5 | import numpy as np 6 | import SimpleITK as sitk 7 | from utils import SafeArrayToNumpy 8 | from utils import FrameTo3dArray 9 | 10 | def SaveITKImage(imgFrame, bbox, outputFilename): 11 | array = FrameTo3dArray(imgFrame) 12 | itk_img = sitk.GetImageFromArray(array.astype("float64")) 13 | 14 | m2mm = 1000 15 | 16 | dir1 = [bbox.dir1_x, bbox.dir1_y, bbox.dir1_z] 17 | dir2 = [bbox.dir2_x, bbox.dir2_y, bbox.dir2_z] 18 | dir3 = [bbox.dir3_x, bbox.dir3_y, bbox.dir3_z] 19 | 20 | # all units from Image3dAPI are in meters according to https://github.com/MedicalUltrasound/Image3dAPI/wiki#image-geometry 21 | spacingX = np.linalg.norm(dir1) / imgFrame.dims[0] * m2mm # convert from meters to millimeters 22 | spacingY = np.linalg.norm(dir2) / imgFrame.dims[1] * m2mm 23 | spacingZ = np.linalg.norm(dir3) / imgFrame.dims[2] * m2mm 24 | 25 | 26 | dir1 = dir1 / np.linalg.norm(dir1) 27 | dir2 = dir2 / np.linalg.norm(dir2) 28 | dir3 = dir3 / np.linalg.norm(dir3) 29 | 30 | itk_img.SetSpacing((spacingX,spacingY,spacingZ)) 31 | itk_img.SetOrigin((bbox.origin_x*m2mm, bbox.origin_y*m2mm, bbox.origin_z*m2mm)) 32 | itk_img.SetDirection((dir1[0], dir2[0], dir3[0], dir1[1], dir2[1], dir3[1], dir1[2], dir2[2], dir3[2])) 33 | 34 | sitk.WriteImage(itk_img, outputFilename) 35 | 36 | if __name__=="__main__": 37 | # load type library 38 | if "32" in platform.architecture()[0]: 39 | Image3dAPI = comtypes.client.GetModule("../Win32/Image3dAPI.tlb") 40 | else: 41 | Image3dAPI = comtypes.client.GetModule("../x64/Image3dAPI.tlb") 42 | 43 | # create loader object 44 | loader = comtypes.client.CreateObject("DummyLoader.Image3dFileLoader") 45 | loader = loader.QueryInterface(Image3dAPI.IImage3dFileLoader) 46 | 47 | # load file 48 | err_type, err_msg = loader.LoadFile("dummy.dcm") 49 | source = loader.GetImageSource() 50 | 51 | # get bounding box 52 | bbox = source.GetBoundingBox() 53 | origin = [bbox.origin_x, bbox.origin_y, bbox.origin_z] 54 | dir1 = [bbox.dir1_x, bbox.dir1_y, bbox.dir1_z] 55 | dir2 = [bbox.dir2_x, bbox.dir2_y, bbox.dir2_z] 56 | dir3 = [bbox.dir3_x, bbox.dir3_y, bbox.dir3_z] 57 | 58 | color_map = source.GetColorMap() 59 | print("Color-map length: "+str(len(color_map))) 60 | 61 | frame_count = source.GetFrameCount() 62 | for i in range(frame_count): 63 | max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) 64 | frame = source.GetFrame(i, bbox, max_res) 65 | data = FrameTo3dArray(frame) 66 | SaveITKImage(frame, bbox, "image3DAPIDummOutput" + str(i) + ".mhd") 67 | -------------------------------------------------------------------------------- /TestPython/TestPython.py: -------------------------------------------------------------------------------- 1 | ## Sample code to demonstrate how to access Image3dAPI from a python script 2 | import platform 3 | import comtypes 4 | import comtypes.client 5 | import numpy as np 6 | from utils import SafeArrayToNumpy 7 | from utils import FrameTo3dArray 8 | from utils import TypeLibFromObject 9 | 10 | 11 | if __name__=="__main__": 12 | # create loader object 13 | loader = comtypes.client.CreateObject("DummyLoader.Image3dFileLoader") 14 | # cast to IImage3dFileLoader interface 15 | Image3dAPI = TypeLibFromObject(loader) 16 | loader = loader.QueryInterface(Image3dAPI.IImage3dFileLoader) 17 | 18 | # load file 19 | err_type, err_msg = loader.LoadFile("dummy.dcm") 20 | source = loader.GetImageSource() 21 | 22 | # retrieve probe info 23 | probe = source.GetProbeInfo() 24 | print("Probe name: "+probe.name) 25 | print("Probe type: "+str(probe.type)) 26 | 27 | # retrieve ECG info 28 | ecg = source.GetECG() 29 | print("ECG start: "+str(ecg.start_time)) 30 | print("ECG delta: "+str(ecg.delta_time)) 31 | samples = SafeArrayToNumpy(ecg.samples) 32 | trig_times = SafeArrayToNumpy(ecg.trig_times) 33 | 34 | # get bounding box 35 | bbox = source.GetBoundingBox() 36 | origin = [bbox.origin_x, bbox.origin_y, bbox.origin_z] 37 | dir1 = [bbox.dir1_x, bbox.dir1_y, bbox.dir1_z] 38 | dir2 = [bbox.dir2_x, bbox.dir2_y, bbox.dir2_z] 39 | dir3 = [bbox.dir3_x, bbox.dir3_y, bbox.dir3_z] 40 | 41 | color_map = source.GetColorMap() 42 | print("Color-map length: "+str(len(color_map))) 43 | 44 | frame_count = source.GetFrameCount() 45 | for i in range(frame_count): 46 | max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) 47 | frame = source.GetFrame(i, bbox, max_res) 48 | 49 | print("Frame metadata:") 50 | print(" time: "+str(frame.time)) 51 | print(" format: "+str(frame.format)) 52 | print(" dims: ("+str(frame.dims[0])+", "+str(frame.dims[1])+", "+str(frame.dims[2])+")") 53 | 54 | data = FrameTo3dArray(frame) 55 | print(" shape: "+str(data.shape)) 56 | 57 | -------------------------------------------------------------------------------- /TestPython/TestPython.pyproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | 2.0 6 | a594d690-99db-499b-b3b0-0bf81364eba2 7 | . 8 | TestPython.py 9 | 10 | 11 | . 12 | . 13 | TestPython 14 | TestPython 15 | Standard Python launcher 16 | False 17 | 18 | 19 | true 20 | false 21 | 22 | 23 | true 24 | false 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 10.0 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /TestPython/utils.py: -------------------------------------------------------------------------------- 1 | ## Sample code to demonstrate how to access Image3dAPI from a python script 2 | import platform 3 | import comtypes 4 | import comtypes.client 5 | import numpy as np 6 | 7 | 8 | def TypeLibFromObject (object): 9 | """Loads the type library associated with a COM class instance""" 10 | import winreg 11 | 12 | with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, "CLSID\\"+object.__clsid+"\\TypeLib", 0, winreg.KEY_READ) as key: 13 | typelib = winreg.EnumValue(key, 0)[1] 14 | with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, "CLSID\\"+object.__clsid+"\\Version", 0, winreg.KEY_READ) as key: 15 | version = winreg.EnumValue(key, 0)[1] 16 | 17 | try: 18 | major_ver, minor_ver = version.split(".") 19 | return comtypes.client.GetModule([typelib, int(major_ver), int(minor_ver)]) 20 | except OSError as err: 21 | # API 1.2-only compatibility fallback to avoid breaking existing loaders 22 | if (version != "1.2") or (err.winerror != -2147319779): # Library not registered 23 | raise # rethrow 24 | # Fallback to TypeLib version 1.0 25 | return comtypes.client.GetModule([typelib, 1, 0]) 26 | 27 | 28 | def SafeArrayToNumpy (safearr_ptr, copy=True): 29 | """Convert a SAFEARRAY buffer to its numpy equivalent""" 30 | import ctypes 31 | 32 | # only support 1D data for now 33 | assert(comtypes._safearray.SafeArrayGetDim(safearr_ptr) == 1) 34 | 35 | # access underlying pointer 36 | data_ptr = ctypes.POINTER(safearr_ptr._itemtype_)() 37 | comtypes._safearray.SafeArrayAccessData(safearr_ptr, ctypes.byref(data_ptr)) 38 | 39 | upper_bound = comtypes._safearray.SafeArrayGetUBound(safearr_ptr, 1) + 1 # +1 to go from inclusive to exclusive bound 40 | lower_bound = comtypes._safearray.SafeArrayGetLBound(safearr_ptr, 1) 41 | array_size = upper_bound - lower_bound 42 | 43 | # wrap pointer in numpy array 44 | arr = np.ctypeslib.as_array(data_ptr,shape=(array_size,)) 45 | return np.copy(arr) if copy else arr 46 | 47 | 48 | def FrameTo3dArray (frame): 49 | """Convert Image3d data into a numpy 3D array""" 50 | arr_1d = SafeArrayToNumpy(frame.data, copy=False) 51 | assert(arr_1d.dtype == np.uint8) # only tested with 1byte/elm 52 | 53 | arr_3d = np.lib.stride_tricks.as_strided(arr_1d, shape=frame.dims, strides=(1, frame.stride0, frame.stride1)) 54 | return np.copy(arr_3d) 55 | 56 | -------------------------------------------------------------------------------- /TestViewer/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /obj 3 | -------------------------------------------------------------------------------- /TestViewer/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /TestViewer/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /TestViewer/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace TestViewer 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TestViewer/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | DummyLoader.Image3dFileLoader 20 | GEHC_CARD_US.Image3dFileLoader 21 | CanonLoader.Image3dFileLoader 22 | HitCV3DLoader.Image3dFileLoader 23 | SiemensLoader.Image3dFileLoader 24 | PhilipsLoader.Image3dFileLoader 25 | KretzLoader.KretzImage3dFileLoader 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /TestViewer/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | using System.Windows.Media.Imaging; 6 | using System.Diagnostics; 7 | using System.Runtime.InteropServices; 8 | using Microsoft.Win32; 9 | using Image3dAPI; 10 | 11 | 12 | /** Alternative to System.Activator.CreateInstance that allows explicit control over the activation context. 13 | * Based on https://stackoverflow.com/questions/22901224/hosting-managed-code-and-garbage-collection */ 14 | public static class ComExt { 15 | [DllImport("ole32.dll", ExactSpelling = true, PreserveSig = false)] 16 | static extern void CoCreateInstance( 17 | [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, 18 | [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, 19 | uint dwClsContext, 20 | [MarshalAs(UnmanagedType.LPStruct)] Guid riid, 21 | [MarshalAs(UnmanagedType.Interface)] out object rReturnedComObject); 22 | 23 | public static object CreateInstance(Guid clsid, bool force_out_of_process) { 24 | Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046"); 25 | const uint CLSCTX_INPROC_SERVER = 0x1; 26 | const uint CLSCTX_LOCAL_SERVER = 0x4; 27 | 28 | uint class_context = CLSCTX_LOCAL_SERVER; // always allow out-of-process activation 29 | if (!force_out_of_process) 30 | class_context |= CLSCTX_INPROC_SERVER; // allow in-process activation 31 | 32 | object unk; 33 | CoCreateInstance(clsid, null, class_context, IID_IUnknown, out unk); 34 | return unk; 35 | } 36 | } 37 | 38 | 39 | namespace TestViewer 40 | { 41 | public partial class MainWindow : Window 42 | { 43 | IImage3dFileLoader m_loader; 44 | IImage3dSource m_source; 45 | 46 | Cart3dGeom m_bboxXY; 47 | Cart3dGeom m_bboxXZ; 48 | Cart3dGeom m_bboxZY; 49 | 50 | public MainWindow() 51 | { 52 | InitializeComponent(); 53 | } 54 | 55 | void ClearUI() 56 | { 57 | FrameSelector.Minimum = 0; 58 | FrameSelector.Maximum = 0; 59 | FrameSelector.IsEnabled = false; 60 | FrameSelector.Value = 0; 61 | 62 | FrameCount.Text = ""; 63 | ProbeInfo.Text = ""; 64 | InstanceUID.Text = ""; 65 | 66 | 67 | ImageXY.Source = null; 68 | ImageXZ.Source = null; 69 | ImageZY.Source = null; 70 | 71 | m_bboxXY = new Cart3dGeom(); 72 | m_bboxXZ = new Cart3dGeom(); 73 | m_bboxZY = new Cart3dGeom(); 74 | 75 | ECG.Data = null; 76 | 77 | if (m_source != null) { 78 | Marshal.ReleaseComObject(m_source); 79 | m_source = null; 80 | } 81 | } 82 | 83 | private void LoadDefaultBtn_Click(object sender, RoutedEventArgs e) 84 | { 85 | LoadImpl(false); 86 | } 87 | 88 | private void LoadOutOfProcBtn_Click(object sender, RoutedEventArgs e) 89 | { 90 | LoadImpl(true); 91 | } 92 | 93 | private void LoadImpl(bool force_out_of_proc) 94 | { 95 | // try to parse string as ProgId first 96 | Type comType = Type.GetTypeFromProgID(LoaderName.Text); 97 | if (comType == null) { 98 | try { 99 | // fallback to parse string as CLSID hex value 100 | Guid guid = Guid.Parse(LoaderName.Text); 101 | comType = Type.GetTypeFromCLSID(guid); 102 | } catch (FormatException) { 103 | MessageBox.Show("Unknown ProgId of CLSID."); 104 | return; 105 | } 106 | } 107 | 108 | // API version compatibility check 109 | try { 110 | RegistryKey ver_key = Registry.ClassesRoot.OpenSubKey("CLSID\\{" + comType.GUID + "}\\Version"); 111 | string ver_str = (string)ver_key.GetValue(""); 112 | string cur_ver = string.Format("{0}.{1}", (int)Image3dAPIVersion.IMAGE3DAPI_VERSION_MAJOR, (int)Image3dAPIVersion.IMAGE3DAPI_VERSION_MINOR); 113 | if (ver_str != cur_ver) { 114 | MessageBox.Show(string.Format("Loader uses version {0}, while the current version is {1}.", ver_str, cur_ver), "Incompatible loader version"); 115 | return; 116 | } 117 | } catch (Exception err) { 118 | MessageBox.Show(err.Message, "Version check error"); 119 | // continue, since this error will also appear if the loader has non-matching bitness 120 | } 121 | 122 | // clear UI when switching to a new loader 123 | ClearUI(); 124 | 125 | if (m_loader != null) 126 | Marshal.ReleaseComObject(m_loader); 127 | m_loader = (IImage3dFileLoader)ComExt.CreateInstance(comType.GUID, force_out_of_proc); 128 | 129 | this.FileOpenBtn.IsEnabled = true; 130 | } 131 | 132 | private void FileSelectBtn_Click(object sender, RoutedEventArgs e) 133 | { 134 | OpenFileDialog dialog = new OpenFileDialog(); 135 | if (dialog.ShowDialog() != true) 136 | return; // user hit cancel 137 | 138 | // clear UI when opening a new file 139 | ClearUI(); 140 | 141 | FileName.Text = dialog.FileName; 142 | } 143 | 144 | private void FileOpenBtn_Click(object sender, RoutedEventArgs e) 145 | { 146 | Debug.Assert(m_loader != null); 147 | 148 | Image3dError err_type = Image3dError.Image3d_SUCCESS; 149 | string err_msg = ""; 150 | try { 151 | m_loader.LoadFile(FileName.Text, out err_type, out err_msg); 152 | } catch (Exception) { 153 | // NOTE: err_msg does not seem to be marshaled back on LoadFile failure in .Net. 154 | // NOTE: This problem is limited to .Net, and does not occur in C++ 155 | 156 | string message = "Unknown error"; 157 | if ((err_type != Image3dError.Image3d_SUCCESS)) { 158 | switch (err_type) { 159 | case Image3dError.Image3d_ACCESS_FAILURE: 160 | message = "Unable to open the file. The file might be missing or locked."; 161 | break; 162 | case Image3dError.Image3d_VALIDATION_FAILURE: 163 | message = "Unsupported file. Probably due to unsupported vendor or modality."; 164 | break; 165 | case Image3dError.Image3d_NOT_YET_SUPPORTED: 166 | message = "The loader is too old to parse the file."; 167 | break; 168 | case Image3dError.Image3d_SUPPORT_DISCONTINUED: 169 | message = "The the file version is no longer supported (pre-DICOM format?)."; 170 | break; 171 | } 172 | } 173 | MessageBox.Show("LoadFile error: " + message + " (" + err_msg+")"); 174 | return; 175 | } 176 | 177 | try { 178 | if (m_source != null) 179 | Marshal.ReleaseComObject(m_source); 180 | m_source = m_loader.GetImageSource(); 181 | } catch (Exception err) { 182 | MessageBox.Show("ERROR: " + err.Message, "GetImageSource error"); 183 | return; 184 | } 185 | 186 | FrameSelector.Minimum = 0; 187 | FrameSelector.Maximum = m_source.GetFrameCount()-1; 188 | FrameSelector.IsEnabled = true; 189 | FrameSelector.Value = 0; 190 | 191 | FrameCount.Text = "Frame count: " + m_source.GetFrameCount(); 192 | ProbeInfo.Text = "Probe name: "+ m_source.GetProbeInfo().name; 193 | InstanceUID.Text = "UID: " + m_source.GetSopInstanceUID(); 194 | 195 | InitializeSlices(); 196 | DrawSlices(0); 197 | DrawEcg(m_source.GetFrameTimes()[0]); 198 | } 199 | 200 | private void DrawEcg (double cur_time) 201 | { 202 | EcgSeries ecg; 203 | try { 204 | ecg = m_source.GetECG(); 205 | 206 | if (ecg.samples == null) { 207 | ECG.Data = null; // ECG not available 208 | return; 209 | } 210 | } catch (Exception) { 211 | ECG.Data = null; // ECG not available 212 | return; 213 | } 214 | 215 | // shrink width & height slightly, so that the "actual" width/height remain unchanged 216 | double W = (int)(ECG.ActualWidth - 1); 217 | double H = (int)(ECG.ActualHeight - 1); 218 | 219 | // horizontal scaling (index to X coord) 220 | double ecg_pitch = W/ecg.samples.Length; 221 | 222 | // vertical scaling (sample val to Y coord) 223 | double ecg_offset = H*ecg.samples.Max()/(ecg.samples.Max()-ecg.samples.Min()); 224 | double ecg_scale = -H/(ecg.samples.Max()-ecg.samples.Min()); 225 | 226 | // vertical scaling (time to Y coord conv) 227 | double time_offset = -W*ecg.start_time/(ecg.delta_time*ecg.samples.Length); 228 | double time_scale = W/(ecg.delta_time*ecg.samples.Length); 229 | 230 | PathGeometry pathGeom = new PathGeometry(); 231 | { 232 | // draw ECG trace 233 | PathSegmentCollection pathSegmentCollection = new PathSegmentCollection(); 234 | for (int i = 0; i < ecg.samples.Length; ++i) { 235 | LineSegment lineSegment = new LineSegment(); 236 | lineSegment.Point = new Point(ecg_pitch*i, ecg_offset+ecg_scale*ecg.samples[i]); 237 | pathSegmentCollection.Add(lineSegment); 238 | } 239 | 240 | PathFigure pathFig = new PathFigure(); 241 | pathFig.StartPoint = new Point(0, ecg_offset+ecg_scale*ecg.samples[0]); 242 | pathFig.Segments = pathSegmentCollection; 243 | 244 | PathFigureCollection pathFigCol = new PathFigureCollection(); 245 | pathFigCol.Add(pathFig); 246 | 247 | pathGeom.Figures = pathFigCol; 248 | } 249 | 250 | { 251 | // draw current frame line 252 | double x_pos = time_offset + time_scale * cur_time; 253 | 254 | LineGeometry line = new LineGeometry(); 255 | line.StartPoint = new Point(x_pos, 0); 256 | line.EndPoint = new Point(x_pos, H); 257 | 258 | pathGeom.AddGeometry(line); 259 | } 260 | 261 | ECG.Stroke = Brushes.Blue; 262 | ECG.StrokeThickness = 1.0; 263 | ECG.Data = pathGeom; 264 | } 265 | 266 | private void FrameSelector_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) 267 | { 268 | var idx = (uint)FrameSelector.Value; 269 | DrawSlices(idx); 270 | DrawEcg(m_source.GetFrameTimes()[idx]); 271 | } 272 | 273 | private void InitializeSlices() 274 | { 275 | Debug.Assert(m_source != null); 276 | 277 | Cart3dGeom bbox = m_source.GetBoundingBox(); 278 | if (Math.Abs(bbox.dir3_y) > Math.Abs(bbox.dir2_y)) { 279 | // swap 2nd & 3rd axis, so that the 2nd becomes predominately "Y" 280 | SwapVals(ref bbox.dir2_x, ref bbox.dir3_x); 281 | SwapVals(ref bbox.dir2_y, ref bbox.dir3_y); 282 | SwapVals(ref bbox.dir2_z, ref bbox.dir3_z); 283 | } 284 | 285 | // extend bounding-box axes, so that dir1, dir2 & dir3 have equal length 286 | ExtendBoundingBox(ref bbox); 287 | 288 | // get XY plane (assumes 1st axis is "X" and 2nd is "Y") 289 | m_bboxXY = bbox; 290 | m_bboxXY.origin_x = m_bboxXY.origin_x + m_bboxXY.dir3_x / 2; 291 | m_bboxXY.origin_y = m_bboxXY.origin_y + m_bboxXY.dir3_y / 2; 292 | m_bboxXY.origin_z = m_bboxXY.origin_z + m_bboxXY.dir3_z / 2; 293 | m_bboxXY.dir3_x = 0; 294 | m_bboxXY.dir3_y = 0; 295 | m_bboxXY.dir3_z = 0; 296 | 297 | // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") 298 | m_bboxXZ = bbox; 299 | m_bboxXZ.origin_x = m_bboxXZ.origin_x + m_bboxXZ.dir2_x / 2; 300 | m_bboxXZ.origin_y = m_bboxXZ.origin_y + m_bboxXZ.dir2_y / 2; 301 | m_bboxXZ.origin_z = m_bboxXZ.origin_z + m_bboxXZ.dir2_z / 2; 302 | m_bboxXZ.dir2_x = m_bboxXZ.dir3_x; 303 | m_bboxXZ.dir2_y = m_bboxXZ.dir3_y; 304 | m_bboxXZ.dir2_z = m_bboxXZ.dir3_z; 305 | m_bboxXZ.dir3_x = 0; 306 | m_bboxXZ.dir3_y = 0; 307 | m_bboxXZ.dir3_z = 0; 308 | 309 | // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") 310 | m_bboxZY = bbox; 311 | m_bboxZY.origin_x = bbox.origin_x + bbox.dir1_x / 2; 312 | m_bboxZY.origin_y = bbox.origin_y + bbox.dir1_y / 2; 313 | m_bboxZY.origin_z = bbox.origin_z + bbox.dir1_z / 2; 314 | m_bboxZY.dir1_x = bbox.dir3_x; 315 | m_bboxZY.dir1_y = bbox.dir3_y; 316 | m_bboxZY.dir1_z = bbox.dir3_z; 317 | m_bboxZY.dir2_x = bbox.dir2_x; 318 | m_bboxZY.dir2_y = bbox.dir2_y; 319 | m_bboxZY.dir2_z = bbox.dir2_z; 320 | m_bboxZY.dir3_x = 0; 321 | m_bboxZY.dir3_y = 0; 322 | m_bboxZY.dir3_z = 0; 323 | } 324 | 325 | private void DrawSlices (uint frame) 326 | { 327 | Debug.Assert(m_source != null); 328 | 329 | uint[] color_map = m_source.GetColorMap(); 330 | 331 | // retrieve image slices 332 | const ushort HORIZONTAL_RES = 256; 333 | const ushort VERTICAL_RES = 256; 334 | 335 | // get XY plane (assumes 1st axis is "X" and 2nd is "Y") 336 | Image3d imageXY = m_source.GetFrame(frame, m_bboxXY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); 337 | ImageXY.Source = GenerateBitmap(imageXY, color_map); 338 | 339 | // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") 340 | Image3d imageXZ = m_source.GetFrame(frame, m_bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); 341 | ImageXZ.Source = GenerateBitmap(imageXZ, color_map); 342 | 343 | // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") 344 | Image3d imageZY = m_source.GetFrame(frame, m_bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); 345 | ImageZY.Source = GenerateBitmap(imageZY, color_map); 346 | 347 | FrameTime.Text = "Frame time: " + imageXY.time; 348 | } 349 | 350 | private WriteableBitmap GenerateBitmap(Image3d t_img, uint[] t_map) 351 | { 352 | Debug.Assert(t_img.format == ImageFormat.FORMAT_U8); 353 | 354 | WriteableBitmap bitmap = new WriteableBitmap(t_img.dims[0], t_img.dims[1], 96.0, 96.0, PixelFormats.Rgb24, null); 355 | bitmap.Lock(); 356 | unsafe { 357 | for (int y = 0; y < bitmap.Height; ++y) { 358 | for (int x = 0; x < bitmap.Width; ++x) { 359 | byte t_val = t_img.data[x + y * t_img.stride0]; 360 | 361 | // lookup tissue color 362 | byte[] channels = BitConverter.GetBytes(t_map[t_val]); 363 | 364 | // assign red, green & blue 365 | byte* pixel = (byte*)bitmap.BackBuffer + x * (bitmap.Format.BitsPerPixel / 8) + y * bitmap.BackBufferStride; 366 | pixel[0] = channels[0]; // red 367 | pixel[1] = channels[1]; // green 368 | pixel[2] = channels[2]; // blue 369 | // discard alpha channel 370 | } 371 | } 372 | } 373 | bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight)); 374 | bitmap.Unlock(); 375 | return bitmap; 376 | } 377 | 378 | static void SwapVals(ref float v1, ref float v2) 379 | { 380 | float tmp = v1; 381 | v1 = v2; 382 | v2 = tmp; 383 | } 384 | 385 | static float VecLen(float x, float y, float z) 386 | { 387 | return (float)Math.Sqrt(x * x + y * y + z * z); 388 | } 389 | 390 | static float VecLen(Cart3dGeom g, int idx) 391 | { 392 | if (idx == 1) 393 | return VecLen(g.dir1_x, g.dir1_y, g.dir1_z); 394 | else if (idx == 2) 395 | return VecLen(g.dir2_x, g.dir2_y, g.dir2_z); 396 | else if (idx == 3) 397 | return VecLen(g.dir3_x, g.dir3_y, g.dir3_z); 398 | 399 | throw new Exception("unsupported direction index"); 400 | } 401 | 402 | /** Scale bounding-box, so that all axes share the same length. 403 | * Also update the origin to keep the bounding-box centered. */ 404 | static void ExtendBoundingBox(ref Cart3dGeom g) 405 | { 406 | float dir1_len = VecLen(g, 1); 407 | float dir2_len = VecLen(g, 2); 408 | float dir3_len = VecLen(g, 3); 409 | 410 | float max_len = Math.Max(dir1_len, Math.Max(dir2_len, dir3_len)); 411 | 412 | if (dir1_len < max_len) 413 | { 414 | float delta = max_len - dir1_len; 415 | float dx, dy, dz; 416 | ScaleVector(g.dir1_x, g.dir1_y, g.dir1_z, delta, out dx, out dy, out dz); 417 | // scale up dir1 so that it becomes the same length as the other axes 418 | g.dir1_x += dx; 419 | g.dir1_y += dy; 420 | g.dir1_z += dz; 421 | // move origin to keep the bounding-box centered 422 | g.origin_x -= dx/2; 423 | g.origin_y -= dy/2; 424 | g.origin_z -= dz/2; 425 | } 426 | 427 | if (dir2_len < max_len) 428 | { 429 | float delta = max_len - dir2_len; 430 | float dx, dy, dz; 431 | ScaleVector(g.dir2_x, g.dir2_y, g.dir2_z, delta, out dx, out dy, out dz); 432 | // scale up dir2 so that it becomes the same length as the other axes 433 | g.dir2_x += dx; 434 | g.dir2_y += dy; 435 | g.dir2_z += dz; 436 | // move origin to keep the bounding-box centered 437 | g.origin_x -= dx / 2; 438 | g.origin_y -= dy / 2; 439 | g.origin_z -= dz / 2; 440 | } 441 | 442 | if (dir3_len < max_len) 443 | { 444 | float delta = max_len - dir3_len; 445 | float dx, dy, dz; 446 | ScaleVector(g.dir3_x, g.dir3_y, g.dir3_z, delta, out dx, out dy, out dz); 447 | // scale up dir3 so that it becomes the same length as the other axes 448 | float factor = max_len / dir3_len; 449 | g.dir3_x += dx; 450 | g.dir3_y += dy; 451 | g.dir3_z += dz; 452 | // move origin to keep the bounding-box centered 453 | g.origin_x -= dx / 2; 454 | g.origin_y -= dy / 2; 455 | g.origin_z -= dz / 2; 456 | } 457 | } 458 | 459 | static void ScaleVector(float in_x, float in_y, float in_z, float length, out float out_x, out float out_y, out float out_z) 460 | { 461 | float cur_len = VecLen(in_x, in_y, in_z); 462 | out_x = in_x * length / cur_len; 463 | out_y = in_y * length / cur_len; 464 | out_z = in_z * length / cur_len; 465 | } 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /TestViewer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("TestViewer")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("GE Healthcare")] 14 | [assembly: AssemblyProduct("TestViewer")] 15 | [assembly: AssemblyCopyright("Copyright © GE Healthcare 2018")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | //In order to begin building localizable applications, set 25 | //CultureYouAreCodingWith in your .csproj file 26 | //inside a . For example, if you are using US english 27 | //in your source files, set the to en-US. Then uncomment 28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 29 | //the line below to match the UICulture setting in the project file. 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 36 | //(used if a resource is not found in the page, 37 | // or application resource dictionaries) 38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 39 | //(used if a resource is not found in the page, 40 | // app, or any theme specific resource dictionaries) 41 | )] 42 | 43 | 44 | // Version information for an assembly consists of the following four values: 45 | // 46 | // Major Version 47 | // Minor Version 48 | // Build Number 49 | // Revision 50 | // 51 | // You can specify all the values or you can default the Build and Revision Numbers 52 | // by using the '*' as shown below: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("1.0.0.0")] 55 | [assembly: AssemblyFileVersion("1.0.0.0")] 56 | -------------------------------------------------------------------------------- /TestViewer/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestViewer.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TestViewer.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TestViewer/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /TestViewer/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TestViewer.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TestViewer/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /TestViewer/TestViewer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {08D5EA11-0B4C-40FD-87EC-23D42C195D59} 8 | WinExe 9 | Properties 10 | TestViewer 11 | TestViewer 12 | v4.8 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | true 17 | 18 | 19 | 20 | true 21 | bin\x64\Debug\ 22 | DEBUG;TRACE 23 | true 24 | full 25 | x64 26 | prompt 27 | MinimumRecommendedRules.ruleset 28 | 29 | 30 | bin\x64\Release\ 31 | TRACE 32 | true 33 | true 34 | pdbonly 35 | x64 36 | prompt 37 | MinimumRecommendedRules.ruleset 38 | 39 | 40 | true 41 | bin\x86\Debug\ 42 | DEBUG;TRACE 43 | true 44 | full 45 | x86 46 | prompt 47 | MinimumRecommendedRules.ruleset 48 | 49 | 50 | bin\x86\Release\ 51 | TRACE 52 | true 53 | true 54 | pdbonly 55 | x86 56 | prompt 57 | MinimumRecommendedRules.ruleset 58 | 59 | 60 | 61 | ..\Win32\Image3dAPI.dll 62 | True 63 | 64 | 65 | 66 | 67 | ..\x64\Image3dAPI.dll 68 | True 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 4.0 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | MSBuild:Compile 90 | Designer 91 | 92 | 93 | MSBuild:Compile 94 | Designer 95 | 96 | 97 | App.xaml 98 | Code 99 | 100 | 101 | MainWindow.xaml 102 | Code 103 | 104 | 105 | 106 | 107 | Code 108 | 109 | 110 | True 111 | True 112 | Resources.resx 113 | 114 | 115 | True 116 | Settings.settings 117 | True 118 | 119 | 120 | ResXFileCodeGenerator 121 | Resources.Designer.cs 122 | 123 | 124 | SettingsSingleFileGenerator 125 | Settings.Designer.cs 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 140 | --------------------------------------------------------------------------------