From 1bc17395f2e383552eacb9169ff486b5b80476bb Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Fri, 12 Jul 2019 19:09:32 +0200 Subject: [PATCH 1/5] API: Extend ImageFormat enum with a new FREQ8POW8 value Also, prefix enum values with "IMAGE_" to make room for a separate enum for color-map types. --- DummyLoader/Image3dSource.cpp | 4 ++-- DummyLoader/Image3dStream.hpp | 2 +- Image3dAPI/IImage3d.idl | 9 +++++---- TestViewer/MainWindow.xaml.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/DummyLoader/Image3dSource.cpp b/DummyLoader/Image3dSource.cpp index 08cd789..1050173 100644 --- a/DummyLoader/Image3dSource.cpp +++ b/DummyLoader/Image3dSource.cpp @@ -77,7 +77,7 @@ Image3dSource::Image3dSource() { } } - m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, FORMAT_U8, dims, img_buf)); + m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, IMAGE_FORMAT_U8, dims, img_buf)); } } } @@ -118,7 +118,7 @@ HRESULT Image3dSource::GetFrame(unsigned int index, Cart3dGeom out_geom, unsigne return E_BOUNDS; ImageFormat format = m_frames[index].format; - if (format == FORMAT_U8) { + if (format == IMAGE_FORMAT_U8) { Image3d result = SampleFrame(m_frames[index], m_img_geom, out_geom, max_res); *data = std::move(result); return S_OK; diff --git a/DummyLoader/Image3dStream.hpp b/DummyLoader/Image3dStream.hpp index e6d06e1..31a9028 100644 --- a/DummyLoader/Image3dStream.hpp +++ b/DummyLoader/Image3dStream.hpp @@ -38,7 +38,7 @@ struct R8G8B8A8 { /** Determine the sample size [bytes] for a given image format. */ static unsigned int ImageFormatSize(ImageFormat format) { switch (format) { - case FORMAT_U8: return 1; + case IMAGE_FORMAT_U8: return 1; } abort(); // should never be reached diff --git a/Image3dAPI/IImage3d.idl b/Image3dAPI/IImage3d.idl index e1256ce..1a694c0 100644 --- a/Image3dAPI/IImage3d.idl +++ b/Image3dAPI/IImage3d.idl @@ -24,10 +24,11 @@ enum Image3dAPIVersion { typedef [ v1_enum, // 32bit enum size - helpstring("Enum of supported image formats (extended upon demand).")] + helpstring("Enum of supported image formats (extended upon demand). Multi-channel formats are documented in byte order.")] enum ImageFormat { - FORMAT_INVALID = 0, ///< make sure that "cleared" state is invalid - FORMAT_U8 = 1, ///< unsigned 8bit grayscale + IMAGE_FORMAT_INVALID = 0, ///< make sure that "cleared" state is invalid + IMAGE_FORMAT_U8 = 1, ///< unsigned 8bit grayscale + IMAGE_FORMAT_FREQ8POW8 = 2, ///< 16bit color-flow format (8bit signed frequency value, 8bit unsigned power/bandwidth value) } ImageFormat; @@ -96,7 +97,7 @@ cpp_quote("} // extern \"C\"") cpp_quote("") cpp_quote("struct Image3d {") cpp_quote(" double time = 0;") -cpp_quote(" ImageFormat format = FORMAT_INVALID;") +cpp_quote(" ImageFormat format = IMAGE_FORMAT_INVALID;") cpp_quote(" unsigned short dims[3] = {0,0,0};") cpp_quote(" unsigned int stride0 = 0;") cpp_quote(" unsigned int stride1 = 0;") diff --git a/TestViewer/MainWindow.xaml.cs b/TestViewer/MainWindow.xaml.cs index 85f0d3e..a78df08 100644 --- a/TestViewer/MainWindow.xaml.cs +++ b/TestViewer/MainWindow.xaml.cs @@ -349,7 +349,7 @@ private void DrawSlices (uint frame) private WriteableBitmap GenerateBitmap(Image3d t_img, uint[] t_map) { - Debug.Assert(t_img.format == ImageFormat.FORMAT_U8); + Debug.Assert(t_img.format == ImageFormat.IMAGE_FORMAT_U8); WriteableBitmap bitmap = new WriteableBitmap(t_img.dims[0], t_img.dims[1], 96.0, 96.0, PixelFormats.Rgb24, null); bitmap.Lock(); From 0900784b2ab59e28a88da7633f0cb2f1bd12ef80 Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Fri, 12 Jul 2019 19:14:03 +0200 Subject: [PATCH 2/5] API: Extend ImageFormat enum with a new R8G8B8A8 value --- Image3dAPI/IImage3d.idl | 1 + 1 file changed, 1 insertion(+) diff --git a/Image3dAPI/IImage3d.idl b/Image3dAPI/IImage3d.idl index 1a694c0..4c6d2e0 100644 --- a/Image3dAPI/IImage3d.idl +++ b/Image3dAPI/IImage3d.idl @@ -29,6 +29,7 @@ enum ImageFormat { IMAGE_FORMAT_INVALID = 0, ///< make sure that "cleared" state is invalid IMAGE_FORMAT_U8 = 1, ///< unsigned 8bit grayscale IMAGE_FORMAT_FREQ8POW8 = 2, ///< 16bit color-flow format (8bit signed frequency value, 8bit unsigned power/bandwidth value) + IMAGE_FORMAT_R8G8B8A8 = 3, ///< 32bit RGBA (alpha channel ignored) } ImageFormat; From dc5e4e503c3d522520ea51356dc7899212ffcbaa Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Fri, 12 Jul 2019 19:16:29 +0200 Subject: [PATCH 3/5] API Add a new ColorMapType enum Specify type when calling GetColorMap. --- DummyLoader/Image3dSource.cpp | 17 +++++++++++------ DummyLoader/Image3dSource.hpp | 2 +- Image3dAPI/IImage3d.idl | 13 +++++++++++-- RegFreeTest/Main.cpp | 9 +++++++-- SandboxTest/Main.cpp | 14 +++++++++----- TestPython/ITKExport.py | 4 ++-- TestPython/TestPython.py | 4 ++-- TestViewer/MainWindow.xaml.cs | 9 ++++++--- 8 files changed, 49 insertions(+), 23 deletions(-) diff --git a/DummyLoader/Image3dSource.cpp b/DummyLoader/Image3dSource.cpp index 1050173..d0cd288 100644 --- a/DummyLoader/Image3dSource.cpp +++ b/DummyLoader/Image3dSource.cpp @@ -135,17 +135,22 @@ HRESULT Image3dSource::GetBoundingBox(/*out*/Cart3dGeom *geom) { return S_OK; } -HRESULT Image3dSource::GetColorMap(/*out*/SAFEARRAY ** map) { +HRESULT Image3dSource::GetColorMap(ColorMapType type, /*out*/ImageFormat * format, /*out*/SAFEARRAY ** map) { if (!map) return E_INVALIDARG; if (*map) return E_INVALIDARG; - // copy to new buffer - CComSafeArray color_map(static_cast(m_color_map_tissue.size())); - memcpy(&color_map.GetAt(0), m_color_map_tissue.data(), sizeof(m_color_map_tissue)); - *map = color_map.Detach(); // transfer ownership - return S_OK; + if (type == TYPE_TISSUE_COLOR) { + *format = IMAGE_FORMAT_R8G8B8A8; + // copy to new buffer + CComSafeArray color_map(4*static_cast(m_color_map_tissue.size())); + memcpy(&color_map.GetAt(0), m_color_map_tissue.data(), sizeof(m_color_map_tissue)); + *map = color_map.Detach(); // transfer ownership + return S_OK; + } + + return E_NOTIMPL; } HRESULT Image3dSource::GetECG(/*out*/EcgSeries *ecg) { diff --git a/DummyLoader/Image3dSource.hpp b/DummyLoader/Image3dSource.hpp index 4806d6b..a294286 100644 --- a/DummyLoader/Image3dSource.hpp +++ b/DummyLoader/Image3dSource.hpp @@ -24,7 +24,7 @@ class ATL_NO_VTABLE Image3dSource : HRESULT STDMETHODCALLTYPE GetBoundingBox(/*out*/Cart3dGeom *geom) override; - HRESULT STDMETHODCALLTYPE GetColorMap(/*out*/SAFEARRAY ** map) override; + HRESULT STDMETHODCALLTYPE GetColorMap(ColorMapType type, /*out*/ImageFormat * format, /*out*/SAFEARRAY ** map) override; HRESULT STDMETHODCALLTYPE GetECG(/*out*/EcgSeries *ecg) override; diff --git a/Image3dAPI/IImage3d.idl b/Image3dAPI/IImage3d.idl index 4c6d2e0..cfcf012 100644 --- a/Image3dAPI/IImage3d.idl +++ b/Image3dAPI/IImage3d.idl @@ -32,6 +32,15 @@ enum ImageFormat { IMAGE_FORMAT_R8G8B8A8 = 3, ///< 32bit RGBA (alpha channel ignored) } ImageFormat; +typedef [ + v1_enum, // 32bit enum size + helpstring("Enum of supported color-map formats (extended upon demand).")] +enum ColorMapType { + TYPE_TISSUE_COLOR, ///< tisse color-map; type=R8G8B8A8, size=256 (for slicing) + TYPE_FLOW_COLOR, ///< flow color-map; type=R8G8B8A8, size=256*256 (for slicing, in freq&bw space) + TYPE_FLOW_ARB, ///< flow arbitration; type=uint8_t, size=256*256 (for deciding if tissue or flow shall be shown) +} ColorMapType; + typedef [ v1_enum, // 32bit enum size @@ -293,8 +302,8 @@ interface IImage3dSource : IUnknown { [helpstring("Get a bounding box encapsulating all image data. Can be used as intput to GetFrame to avoid cropping.")] HRESULT GetBoundingBox ([out,retval] Cart3dGeom * geom); - [helpstring("Retrieve color-map table for mapping image intensities to RGBx values. Length is 256.")] - HRESULT GetColorMap ([out,retval] SAFEARRAY(unsigned int) * map); + [helpstring("Retrieve color-map table for mapping image samples to RGBx values. Length and format depend on type.")] + HRESULT GetColorMap ([in] ColorMapType type, [out] ImageFormat * format, [out,retval] SAFEARRAY(BYTE) * table); [helpstring("Get ECG data if available [optional]. Shall return S_OK with an empty EcgSeries if EGC is not available.")] HRESULT GetECG ([out,retval] EcgSeries * ecg); diff --git a/RegFreeTest/Main.cpp b/RegFreeTest/Main.cpp index bb4101b..23710e7 100644 --- a/RegFreeTest/Main.cpp +++ b/RegFreeTest/Main.cpp @@ -5,10 +5,15 @@ void ParseSource (IImage3dSource & source) { - CComSafeArray color_map; + CComSafeArray color_map; { + ImageFormat img_format = IMAGE_FORMAT_INVALID; SAFEARRAY * tmp = nullptr; - CHECK(source.GetColorMap(&tmp)); + CHECK(source.GetColorMap(TYPE_TISSUE_COLOR, &img_format, &tmp)); + if (img_format != IMAGE_FORMAT_R8G8B8A8) { + std::wcerr << "ERROR: Unexpected color-map format.\n"; + std::exit(-1); + } color_map.Attach(tmp); tmp = nullptr; } diff --git a/SandboxTest/Main.cpp b/SandboxTest/Main.cpp index 5e88c3d..f277f85 100644 --- a/SandboxTest/Main.cpp +++ b/SandboxTest/Main.cpp @@ -32,19 +32,23 @@ class PerfTimer { void ParseSource (IImage3dSource & source, bool verbose, bool profile) { - CComSafeArray color_map; + CComSafeArray color_map; { + ImageFormat img_format = IMAGE_FORMAT_INVALID; SAFEARRAY * tmp = nullptr; - CHECK(source.GetColorMap(&tmp)); + CHECK(source.GetColorMap(TYPE_TISSUE_COLOR, &img_format, &tmp)); + if (img_format != IMAGE_FORMAT_R8G8B8A8) { + std::wcerr << "ERROR: Unexpected color-map format.\n"; + std::exit(-1); + } color_map.Attach(tmp); tmp = nullptr; } if (verbose) { std::cout << "Color-map:\n"; - for (unsigned int i = 0; i < color_map.GetCount(); i++) { - unsigned int color = color_map[(int)i]; - uint8_t *rgbx = reinterpret_cast(&color); + for (unsigned int i = 0; i < color_map.GetCount()/4; i++) { + uint8_t *rgbx = &color_map[(int)(4*i)]; std::cout << " [" << (int)rgbx[0] << "," << (int)rgbx[1] << "," << (int)rgbx[2] << "," << (int)rgbx[3] << "]\n"; } } diff --git a/TestPython/ITKExport.py b/TestPython/ITKExport.py index e4019f9..ab98725 100644 --- a/TestPython/ITKExport.py +++ b/TestPython/ITKExport.py @@ -55,8 +55,8 @@ def SaveITKImage(imgFrame, bbox, outputFilename): dir2 = [bbox.dir2_x, bbox.dir2_y, bbox.dir2_z] dir3 = [bbox.dir3_x, bbox.dir3_y, bbox.dir3_z] - color_map = source.GetColorMap() - print("Color-map length: "+str(len(color_map))) + color_format, color_map = source.GetColorMap(Image3dAPI.TYPE_TISSUE_COLOR) + print("Color-map length: "+str(len(color_map)/4)) frame_count = source.GetFrameCount() for i in range(frame_count): diff --git a/TestPython/TestPython.py b/TestPython/TestPython.py index d48fcff..f71de4b 100644 --- a/TestPython/TestPython.py +++ b/TestPython/TestPython.py @@ -38,8 +38,8 @@ dir2 = [bbox.dir2_x, bbox.dir2_y, bbox.dir2_z] dir3 = [bbox.dir3_x, bbox.dir3_y, bbox.dir3_z] - color_map = source.GetColorMap() - print("Color-map length: "+str(len(color_map))) + color_format, color_map = source.GetColorMap(Image3dAPI.TYPE_TISSUE_COLOR) + print("Color-map length: "+str(len(color_map)/4)) frame_count = source.GetFrameCount() for i in range(frame_count): diff --git a/TestViewer/MainWindow.xaml.cs b/TestViewer/MainWindow.xaml.cs index a78df08..492fc66 100644 --- a/TestViewer/MainWindow.xaml.cs +++ b/TestViewer/MainWindow.xaml.cs @@ -326,7 +326,10 @@ private void DrawSlices (uint frame) { Debug.Assert(m_source != null); - uint[] color_map = m_source.GetColorMap(); + ImageFormat image_format; + byte[] color_map = m_source.GetColorMap(ColorMapType.TYPE_TISSUE_COLOR, out image_format); + if (image_format != ImageFormat.IMAGE_FORMAT_R8G8B8A8) + throw new Exception("Unexpected color-map format"); // retrieve image slices const ushort HORIZONTAL_RES = 256; @@ -347,7 +350,7 @@ private void DrawSlices (uint frame) FrameTime.Text = "Frame time: " + imageXY.time; } - private WriteableBitmap GenerateBitmap(Image3d t_img, uint[] t_map) + private WriteableBitmap GenerateBitmap(Image3d t_img, byte[] t_map) { Debug.Assert(t_img.format == ImageFormat.IMAGE_FORMAT_U8); @@ -359,7 +362,7 @@ private WriteableBitmap GenerateBitmap(Image3d t_img, uint[] t_map) byte t_val = t_img.data[x + y * t_img.stride0]; // lookup tissue color - byte[] channels = BitConverter.GetBytes(t_map[t_val]); + byte[] channels = BitConverter.GetBytes(BitConverter.ToUInt32(t_map, 4*t_val)); // assign red, green & blue byte* pixel = (byte*)bitmap.BackBuffer + x * (bitmap.Format.BitsPerPixel / 8) + y * bitmap.BackBufferStride; From a2b6e249dd0870940625cbcd62a0ec31de9e184a Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Fri, 12 Jul 2019 19:28:56 +0200 Subject: [PATCH 4/5] API: Introduce a new IImage3dStream interface to also support color-flow data Extend the API to also support display of color-flow data, in addition to tissue. This is intended to be a simple representation that is compatible with the way color-flow data is processed by any vendor. Please note that the color-flow appearance is unlikely to exactly match the appearance on the original system, since the "internal" vendor encoding and algorithms are likely to be more advanced. The goal is therefore to get visual appearance that is fairly close to the original. Integration of this extension will probably require bidirectional code for converting between the "internal" vendor encoding and the Image3dAPI encoding of flow data. Only an API update so far. Actual color-flow support will be added in a later PR. --- DummyLoader/DummyLoader.idl | 10 +++ DummyLoader/DummyLoader.rc | Bin 4142 -> 4296 bytes DummyLoader/Image3dSource.cpp | 81 ++++-------------- DummyLoader/Image3dSource.hpp | 10 +-- DummyLoader/Image3dStream.cpp | 109 +++++++++++++++++++++++++ DummyLoader/Image3dStream.hpp | 47 ++++++++++- DummyLoader/Resource.h | 5 +- DummyLoader/UNREGISTER_DummyLoader.bat | 5 ++ Image3dAPI/IImage3d.idl | 43 ++++++++-- Image3dAPI/UNREGISTER_Image3dAPI.bat | 1 + RegFreeTest/Main.cpp | 20 ++++- SandboxTest/Main.cpp | 21 ++++- TestPython/ITKExport.py | 12 ++- TestPython/TestPython.py | 11 ++- TestViewer/MainWindow.xaml.cs | 101 ++++++++++++----------- 15 files changed, 337 insertions(+), 139 deletions(-) diff --git a/DummyLoader/DummyLoader.idl b/DummyLoader/DummyLoader.idl index f4aa7bf..78d129d 100644 --- a/DummyLoader/DummyLoader.idl +++ b/DummyLoader/DummyLoader.idl @@ -12,6 +12,16 @@ library DummyLoader { importlib("stdole2.tlb"); + [ + version(1.2), + uuid(78317A0E-56BF-4735-AB5B-FE0751219FE8), + helpstring("3D image stream") + ] + coclass Image3dStream + { + [default] interface IImage3dStream; + }; + [ version(1.2), uuid(6FA82ED5-6332-4344-8417-DEA55E72098C), diff --git a/DummyLoader/DummyLoader.rc b/DummyLoader/DummyLoader.rc index df4d2ab45091d5cabb1d3de35bae860aa48d71e3..04c7a79d3cc4342a26dcc3e3f9d1b11e596721f4 100644 GIT binary patch delta 46 tcmZ3da6)mzJZ{bsh9ZVkhD3(k$%5SSlM{FxxPuvx#CTLU&*7fL0su9>4153p delta 12 TcmX@1xK3fiJnqdFJQG*|B7Ov% diff --git a/DummyLoader/Image3dSource.cpp b/DummyLoader/Image3dSource.cpp index d0cd288..d254c0d 100644 --- a/DummyLoader/Image3dSource.cpp +++ b/DummyLoader/Image3dSource.cpp @@ -2,8 +2,6 @@ #include "LinAlg.hpp" -static const uint8_t OUTSIDE_VAL = 0; // black outside image volume -static const uint8_t PROBE_PLANE = 127; // gray value for plane closest to probe Image3dSource::Image3dSource() { @@ -45,93 +43,50 @@ Image3dSource::Image3dSource() { 0.20f,0, 0, // dir1 (width) 0, 0.10f, 0, // dir2 (depth) 0, 0, 0.15f};// dir3 (elevation) - m_img_geom = geom; - } - { - // checker board image data - unsigned short dims[] = { 20, 15, 10 }; // matches length of dir1, dir2 & dir3, so that the image squares become quadratic - std::vector img_buf(dims[0] * dims[1] * dims[2]); - for (size_t frameNumber = 0; frameNumber < numFrames; ++frameNumber) { - for (unsigned int z = 0; z < dims[2]; ++z) { - for (unsigned int y = 0; y < dims[1]; ++y) { - for (unsigned int x = 0; x < dims[0]; ++x) { - bool even_f = (frameNumber / 2 % 2) == 0; - bool even_x = (x / 2 % 2) == 0; - bool even_y = (y / 2 % 2) == 0; - bool even_z = (z / 2 % 2) == 0; - byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; - if (even_f ^ even_x ^ even_y ^ even_z) - out_sample = 255; - else - out_sample = 0; - } - } - } - - // special grayscale value for plane closest to probe - for (unsigned int z = 0; z < dims[2]; ++z) { - for (unsigned int x = 0; x < dims[0]; ++x) { - unsigned int y = 0; - byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; - out_sample = PROBE_PLANE; - } - } - - m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, IMAGE_FORMAT_U8, dims, img_buf)); - } + m_bbox = geom; } + + // a single tissue stream + m_stream_types.push_back(IMAGE_TYPE_TISSUE); } Image3dSource::~Image3dSource() { } -HRESULT Image3dSource::GetFrameCount(/*out*/unsigned int *size) { +HRESULT Image3dSource::GetStreamCount(/*out*/unsigned int *size) { if (!size) return E_INVALIDARG; - *size = static_cast(m_frames.size()); + *size = static_cast(m_stream_types.size()); return S_OK; } -HRESULT Image3dSource::GetFrameTimes(/*out*/SAFEARRAY * *frame_times) { - if (!frame_times) - return E_INVALIDARG; - const unsigned int N = static_cast(m_frames.size()); - CComSafeArray result(N); - if (N > 0) { - double * time_arr = &result.GetAt(0); - for (unsigned int i = 0; i < N; ++i) - time_arr[i] = m_frames[i].time; - } - *frame_times = result.Detach(); - return S_OK; -} - - -HRESULT Image3dSource::GetFrame(unsigned int index, Cart3dGeom out_geom, unsigned short max_res[3], /*out*/Image3d *data) { - if (!data) +HRESULT Image3dSource::GetStream(unsigned int index, Cart3dGeom out_geom, unsigned short max_resolution[3], /*out*/IImage3dStream ** stream) { + if (!stream) return E_INVALIDARG; - if (index >= m_frames.size()) + if (index >= m_stream_types.size()) return E_BOUNDS; - ImageFormat format = m_frames[index].format; - if (format == IMAGE_FORMAT_U8) { - Image3d result = SampleFrame(m_frames[index], m_img_geom, out_geom, max_res); - *data = std::move(result); - return S_OK; + CComPtr stream_obj; + { + // on-demand stream creation + auto stream_cls = CreateLocalInstance(); + stream_cls->Initialize(m_stream_types[index], m_bbox, out_geom, max_resolution); + stream_obj = stream_cls; // cast class pointer to interface } - return E_NOTIMPL; + *stream = stream_obj.Detach(); + return S_OK; } HRESULT Image3dSource::GetBoundingBox(/*out*/Cart3dGeom *geom) { if (!geom) return E_INVALIDARG; - *geom = m_img_geom; + *geom = m_bbox; return S_OK; } diff --git a/DummyLoader/Image3dSource.hpp b/DummyLoader/Image3dSource.hpp index a294286..ba292e3 100644 --- a/DummyLoader/Image3dSource.hpp +++ b/DummyLoader/Image3dSource.hpp @@ -16,11 +16,10 @@ class ATL_NO_VTABLE Image3dSource : /*NOT virtual*/ ~Image3dSource(); - HRESULT STDMETHODCALLTYPE GetFrameCount(/*out*/unsigned int *size) override; - HRESULT STDMETHODCALLTYPE GetFrameTimes(/*out*/SAFEARRAY * *frame_times) override; + HRESULT STDMETHODCALLTYPE GetStreamCount (/*out*/unsigned int * size) override; - HRESULT STDMETHODCALLTYPE GetFrame(unsigned int index, Cart3dGeom out_geom, unsigned short max_res[3], /*out*/Image3d *data) override; + HRESULT STDMETHODCALLTYPE GetStream (unsigned int index, Cart3dGeom out_geom, unsigned short max_resolution[3], /*out*/IImage3dStream ** stream) override; HRESULT STDMETHODCALLTYPE GetBoundingBox(/*out*/Cart3dGeom *geom) override; @@ -42,8 +41,9 @@ class ATL_NO_VTABLE Image3dSource : ProbeInfo m_probe; EcgSeries m_ecg; std::array m_color_map_tissue; - Cart3dGeom m_img_geom = {}; - std::vector m_frames; + + Cart3dGeom m_bbox = {}; + std::vector m_stream_types; }; OBJECT_ENTRY_AUTO(__uuidof(Image3dSource), Image3dSource) diff --git a/DummyLoader/Image3dStream.cpp b/DummyLoader/Image3dStream.cpp index 33ccde5..8256786 100644 --- a/DummyLoader/Image3dStream.cpp +++ b/DummyLoader/Image3dStream.cpp @@ -1 +1,110 @@ #include "Image3dStream.hpp" + +#include "LinAlg.hpp" + + +static const uint8_t OUTSIDE_VAL = 0; // black outside image volume +static const uint8_t PROBE_PLANE = 127; // gray value for plane closest to probe + + +Image3dStream::Image3dStream() { +} + +void Image3dStream::Initialize (ImageType type, Cart3dGeom img_geom, Cart3dGeom out_geom, unsigned short max_resolution[3]) { + m_type = type; + m_img_geom = img_geom; + m_out_geom = out_geom; + + for (size_t i = 0; i < 3; ++i) + m_max_res[i] = max_resolution[i]; + + // One second loop starting at t = 10 + const size_t numFrames = 25; + const double duration = 1.0; // Seconds + const double startTime = 10.0; + + if (type == IMAGE_TYPE_TISSUE) { + // checker board image data + unsigned short dims[] = { 20, 15, 10 }; // matches length of dir1, dir2 & dir3, so that the image squares become quadratic + std::vector img_buf(dims[0] * dims[1] * dims[2]); + for (size_t frameNumber = 0; frameNumber < numFrames; ++frameNumber) { + for (unsigned int z = 0; z < dims[2]; ++z) { + for (unsigned int y = 0; y < dims[1]; ++y) { + for (unsigned int x = 0; x < dims[0]; ++x) { + bool even_f = (frameNumber / 2 % 2) == 0; + bool even_x = (x / 2 % 2) == 0; + bool even_y = (y / 2 % 2) == 0; + bool even_z = (z / 2 % 2) == 0; + byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; + if (even_f ^ even_x ^ even_y ^ even_z) + out_sample = 255; + else + out_sample = 0; + } + } + } + + // special grayscale value for plane closest to probe + for (unsigned int z = 0; z < dims[2]; ++z) { + for (unsigned int x = 0; x < dims[0]; ++x) { + unsigned int y = 0; + byte & out_sample = img_buf[x + y*dims[0] + z*dims[0] * dims[1]]; + out_sample = PROBE_PLANE; + } + } + + m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, IMAGE_FORMAT_U8, dims, img_buf)); + } + } else { + abort(); // should never be reached + } +} + +Image3dStream::~Image3dStream() { +} + + +HRESULT Image3dStream::GetFrameCount(/*out*/unsigned int *size) { + if (!size) + return E_INVALIDARG; + + *size = static_cast(m_frames.size()); + return S_OK; +} + +HRESULT Image3dStream::GetFrameTimes(/*out*/SAFEARRAY * *frame_times) { + if (!frame_times) + return E_INVALIDARG; + + const unsigned int N = static_cast(m_frames.size()); + CComSafeArray result(N); + if (N > 0) { + double * time_arr = &result.GetAt(0); + for (unsigned int i = 0; i < N; ++i) + time_arr[i] = m_frames[i].time; + } + + *frame_times = result.Detach(); + return S_OK; +} + + +HRESULT Image3dStream::GetFrame(unsigned int index, /*out*/Image3d *data) { + if (!data) + return E_INVALIDARG; + if (index >= m_frames.size()) + return E_BOUNDS; + + ImageFormat format = m_frames[index].format; + if (format == IMAGE_FORMAT_U8) { + Image3d result = SampleFrame(m_frames[index], m_img_geom, m_out_geom, m_max_res); + *data = std::move(result); + return S_OK; + } else if (format == IMAGE_FORMAT_FREQ8POW8) { + Image3d result = SampleFrame(m_frames[index], m_img_geom, m_out_geom, m_max_res); + *data = std::move(result); + return S_OK; + } + + return E_NOTIMPL; +} diff --git a/DummyLoader/Image3dStream.hpp b/DummyLoader/Image3dStream.hpp index 31a9028..96bee67 100644 --- a/DummyLoader/Image3dStream.hpp +++ b/DummyLoader/Image3dStream.hpp @@ -38,7 +38,9 @@ struct R8G8B8A8 { /** Determine the sample size [bytes] for a given image format. */ static unsigned int ImageFormatSize(ImageFormat format) { switch (format) { - case IMAGE_FORMAT_U8: return 1; + case IMAGE_FORMAT_U8: return 1; + case IMAGE_FORMAT_FREQ8POW8: return 2; + case IMAGE_FORMAT_R8G8B8A8: return 4; } abort(); // should never be reached @@ -64,3 +66,46 @@ static Image3d CreateImage3d (double time, ImageFormat format, const unsigned sh return img; } + + +class ATL_NO_VTABLE Image3dStream : + public CComObjectRootEx, + public CComCoClass, + public IImage3dStream { +public: + Image3dStream(); + + /*NOT virtual*/ ~Image3dStream(); + + void Initialize (ImageType type, Cart3dGeom img_geom, Cart3dGeom out_geom, unsigned short max_resolution[3]); + + HRESULT STDMETHODCALLTYPE GetType (/*out*/ImageType * type) override { + if (!type) + return E_INVALIDARG; + + *type = m_type; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetFrameCount(/*out*/unsigned int *size) override; + + HRESULT STDMETHODCALLTYPE GetFrameTimes(/*out*/SAFEARRAY * *frame_times) override; + + HRESULT STDMETHODCALLTYPE GetFrame(unsigned int index, /*out*/Image3d *data) override; + + DECLARE_REGISTRY_RESOURCEID(IDR_Image3dStream) + + BEGIN_COM_MAP(Image3dStream) + COM_INTERFACE_ENTRY(IImage3dStream) + END_COM_MAP() + +private: + ImageType m_type = IMAGE_TYPE_INVALID; + Cart3dGeom m_img_geom = {}; + std::vector m_frames; + + Cart3dGeom m_out_geom = {}; + unsigned short m_max_res[3]; +}; + +OBJECT_ENTRY_AUTO(__uuidof(Image3dStream), Image3dStream) diff --git a/DummyLoader/Resource.h b/DummyLoader/Resource.h index d0d987c..816d32b 100644 --- a/DummyLoader/Resource.h +++ b/DummyLoader/Resource.h @@ -1,5 +1,6 @@ #define IDS_PROJNAME 100 #define IDR_AppID 105 -#define IDR_Image3dSource 106 -#define IDR_Image3dFileLoader 107 +#define IDR_Image3dStream 106 +#define IDR_Image3dSource 107 +#define IDR_Image3dFileLoader 108 diff --git a/DummyLoader/UNREGISTER_DummyLoader.bat b/DummyLoader/UNREGISTER_DummyLoader.bat index 9585016..80b4202 100644 --- a/DummyLoader/UNREGISTER_DummyLoader.bat +++ b/DummyLoader/UNREGISTER_DummyLoader.bat @@ -13,6 +13,11 @@ for %%R in (HKEY_LOCAL_MACHINE HKEY_CURRENT_USER) do ( reg delete "%%R\SOFTWARE\Classes\TypeLib\{67E59584-3F6A-4852-8051-103A4583CA5E}" /f 2> NUL for %%P in (32 64) do ( + :: Image3dStream class + reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dStream" /f 2> NUL + reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dStream.1" /f 2> NUL + reg delete "%%R\SOFTWARE\Classes\CLSID\{78317A0E-56BF-4735-AB5B-FE0751219FE8}" /f /reg:%%P 2> NUL + :: Image3dSource class reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dSource" /f 2> NUL reg delete "%%R\SOFTWARE\Classes\DummyLoader.Image3dSource.1" /f 2> NUL diff --git a/Image3dAPI/IImage3d.idl b/Image3dAPI/IImage3d.idl index cfcf012..6ee0ce1 100644 --- a/Image3dAPI/IImage3d.idl +++ b/Image3dAPI/IImage3d.idl @@ -22,6 +22,15 @@ enum Image3dAPIVersion { } Image3dAPIVersion; +typedef [ + v1_enum, // 32bit enum size + helpstring("Enum of supported image types (extended upon demand).")] +enum ImageType { + IMAGE_TYPE_INVALID = 0, ///< make sure that "cleared" state is invalid + IMAGE_TYPE_TISSUE = 1, ///< grayscale B-mode image + IMAGE_TYPE_BLOOD_VEL = 2, ///< blood velocities (color-flow) (rel. to probe) +} ImageType; + typedef [ v1_enum, // 32bit enum size helpstring("Enum of supported image formats (extended upon demand). Multi-channel formats are documented in byte order.")] @@ -285,21 +294,39 @@ cpp_quote("#else") cpp_quote("static_assert(sizeof(EcgSeries) == 2*8+2*4, \"EcgSeries size mismatch\");") cpp_quote("#endif") + +[object, + oleautomation, // use "automation" marshaler (oleaut32.dll) + uuid(D7D2A41A-F738-41EB-8133-1B06DDA1FF40), + helpstring("3D image stream interface. Used to group frames sharing a common type (e.g. tissue or color-flow). All frames within a stream share the same geometry and resolution.")] +interface IImage3dStream : IUnknown { + [helpstring("Get stream type.")] + HRESULT GetType ([out, retval] ImageType * type); + + [helpstring("Get the number of frames available")] + HRESULT GetFrameCount ([out, retval] unsigned int * size); + + [helpstring("Get the time of all frames (useful for matching frame indices to ECG before retrieving image data) ")] + HRESULT GetFrameTimes ([out, retval] SAFEARRAY(double) * frame_times); + + [helpstring("Get image data (const) for a given frame within a specified geometry")] + HRESULT GetFrame ([in] unsigned int index, [out, retval] Image3d * data); +}; + + [ object, oleautomation, // use "automation" marshaler (oleaut32.dll) uuid(D483D815-52DD-4750-8CA2-5C6C489588B6), helpstring("Interface for retrieving 3D image data.")] interface IImage3dSource : IUnknown { - [helpstring("Get the number of frames available")] - HRESULT GetFrameCount ([out,retval] unsigned int * size); - - [helpstring("Get the time of all frames (useful for matching frame indices to ECG before retrieving image data) ")] - HRESULT GetFrameTimes ([out, retval] SAFEARRAY(double) * frame_times); + [helpstring("Get the number of streams available.")] + HRESULT GetStreamCount ([out, retval] unsigned int * size); - [helpstring("Get image data (const) for a given frame within a specified geometry. The returned frame might have lower resolution than requested.")] - HRESULT GetFrame ([in] unsigned int index, [in] Cart3dGeom geom, [in] unsigned short max_resolution[3], [out,retval] Image3d * data); + [helpstring("Get cartesian image stream. The returned frames might have lower resolution than requested.\n" + "Clients should therefore check the actual resolution of the retrieved images afterwards.")] + HRESULT GetStream ([in] unsigned int index, [in] Cart3dGeom geom, [in] unsigned short max_resolution[3], [out, retval] IImage3dStream ** stream); - [helpstring("Get a bounding box encapsulating all image data. Can be used as intput to GetFrame to avoid cropping.")] + [helpstring("Get a bounding box encapsulating all image data. Can be used as intput to GetStream to avoid cropping.")] HRESULT GetBoundingBox ([out,retval] Cart3dGeom * geom); [helpstring("Retrieve color-map table for mapping image samples to RGBx values. Length and format depend on type.")] diff --git a/Image3dAPI/UNREGISTER_Image3dAPI.bat b/Image3dAPI/UNREGISTER_Image3dAPI.bat index 6f6a849..98ce305 100644 --- a/Image3dAPI/UNREGISTER_Image3dAPI.bat +++ b/Image3dAPI/UNREGISTER_Image3dAPI.bat @@ -14,6 +14,7 @@ reg delete "HKCR\TypeLib\{3ff1aab8-f3d8-33d4-825d-00104b3646c0}" /f 2> NUL for %%P in (32 64) do ( :: IImage3d.idl + reg delete "HKCR\Interface\{D7D2A41A-F738-41EB-8133-1B06DDA1FF40}" /f /reg:%%P 2> NUL reg delete "HKCR\Interface\{D483D815-52DD-4750-8CA2-5C6C489588B6}" /f /reg:%%P 2> NUL reg delete "HKCR\Interface\{CD30759B-EB38-4469-9CA5-4DF75737A31B}" /f /reg:%%P 2> NUL ) diff --git a/RegFreeTest/Main.cpp b/RegFreeTest/Main.cpp index 23710e7..00f0b98 100644 --- a/RegFreeTest/Main.cpp +++ b/RegFreeTest/Main.cpp @@ -21,16 +21,28 @@ void ParseSource (IImage3dSource & source) { Cart3dGeom bbox = {}; CHECK(source.GetBoundingBox(&bbox)); + unsigned int stream_count = 0; + CHECK(source.GetStreamCount(&stream_count)); + if (stream_count == 0) { + std::wcerr << "ERROR: No image streams found.\n"; + std::exit(-1); + } + + unsigned short max_res[] = {64, 64, 64}; + CComPtr stream; + CHECK(source.GetStream(0, bbox, max_res, &stream)); + + ImageType stream_type = IMAGE_TYPE_INVALID; + CHECK(stream->GetType(&stream_type)); + unsigned int frame_count = 0; - CHECK(source.GetFrameCount(&frame_count)); + CHECK(stream->GetFrameCount(&frame_count)); std::wcout << L"Frame count: " << frame_count << L"\n"; for (unsigned int frame = 0; frame < frame_count; ++frame) { - unsigned short max_res[] = {64, 64, 64}; - // retrieve frame data Image3d data; - CHECK(source.GetFrame(frame, bbox, max_res, &data)); + CHECK(stream->GetFrame(frame, &data)); } } diff --git a/SandboxTest/Main.cpp b/SandboxTest/Main.cpp index f277f85..f9f5a7c 100644 --- a/SandboxTest/Main.cpp +++ b/SandboxTest/Main.cpp @@ -64,14 +64,28 @@ void ParseSource (IImage3dSource & source, bool verbose, bool profile) { std::cout << " Dir3: " << bbox.dir3_x << ", " << bbox.dir3_y << ", " << bbox.dir3_z << "\n"; } + unsigned int stream_count = 0; + CHECK(source.GetStreamCount(&stream_count)); + if (stream_count == 0) { + std::wcerr << "ERROR: No image streams found.\n"; + std::exit(-1); + } + + unsigned short max_res[] = {64, 64, 64}; + CComPtr stream; + CHECK(source.GetStream(0, bbox, max_res, &stream)); + + ImageType stream_type = IMAGE_TYPE_INVALID; + CHECK(stream->GetType(&stream_type)); + unsigned int frame_count = 0; - CHECK(source.GetFrameCount(&frame_count)); + CHECK(stream->GetFrameCount(&frame_count)); std::wcout << L"Frame count: " << frame_count << L"\n"; CComSafeArray frame_times; { SAFEARRAY * data = nullptr; - CHECK(source.GetFrameTimes(&data)); + CHECK(stream->GetFrameTimes(&data)); frame_times.Attach(data); data = nullptr; } @@ -86,7 +100,6 @@ void ParseSource (IImage3dSource & source, bool verbose, bool profile) { } for (unsigned int frame = 0; frame < frame_count; ++frame) { - unsigned short max_res[] = { 64, 64, 64 }; if (profile) { max_res[0] = 128; max_res[1] = 128; @@ -96,7 +109,7 @@ void ParseSource (IImage3dSource & source, bool verbose, bool profile) { // retrieve frame data Image3d data; PerfTimer timer("GetFrame", profile); - CHECK(source.GetFrame(frame, bbox, max_res, &data)); + CHECK(stream->GetFrame(frame, &data)); if (frame == 0) std::cout << "First frame time: " << data.time << "\n"; diff --git a/TestPython/ITKExport.py b/TestPython/ITKExport.py index ab98725..f00cbed 100644 --- a/TestPython/ITKExport.py +++ b/TestPython/ITKExport.py @@ -58,9 +58,15 @@ def SaveITKImage(imgFrame, bbox, outputFilename): color_format, color_map = source.GetColorMap(Image3dAPI.TYPE_TISSUE_COLOR) print("Color-map length: "+str(len(color_map)/4)) - frame_count = source.GetFrameCount() + max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) + stream = source.GetStream(0, bbox, max_res) + + stream_type = stream.GetType() + print("Stream type "+str(stream_type)) + + frame_count = stream.GetFrameCount() for i in range(frame_count): - max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) - frame = source.GetFrame(i, bbox, max_res) + frame = stream.GetFrame(i) + data = FrameTo3dArray(frame) SaveITKImage(frame, bbox, "image3DAPIDummOutput" + str(i) + ".mhd") diff --git a/TestPython/TestPython.py b/TestPython/TestPython.py index f71de4b..85e4461 100644 --- a/TestPython/TestPython.py +++ b/TestPython/TestPython.py @@ -41,10 +41,15 @@ color_format, color_map = source.GetColorMap(Image3dAPI.TYPE_TISSUE_COLOR) print("Color-map length: "+str(len(color_map)/4)) - frame_count = source.GetFrameCount() + max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) + stream = source.GetStream(0, bbox, max_res) + + stream_type = stream.GetType() + print("Stream type "+str(stream_type)) + + frame_count = stream.GetFrameCount() for i in range(frame_count): - max_res = np.ctypeslib.as_ctypes(np.array([64, 64, 64], dtype=np.ushort)) - frame = source.GetFrame(i, bbox, max_res) + frame = stream.GetFrame(i) print("Frame metadata:") print(" time: "+str(frame.time)) diff --git a/TestViewer/MainWindow.xaml.cs b/TestViewer/MainWindow.xaml.cs index 492fc66..9dba342 100644 --- a/TestViewer/MainWindow.xaml.cs +++ b/TestViewer/MainWindow.xaml.cs @@ -43,9 +43,9 @@ public partial class MainWindow : Window IImage3dFileLoader m_loader; IImage3dSource m_source; - Cart3dGeom m_bboxXY; - Cart3dGeom m_bboxXZ; - Cart3dGeom m_bboxZY; + IImage3dStream m_streamXY; + IImage3dStream m_streamXZ; + IImage3dStream m_streamZY; public MainWindow() { @@ -68,9 +68,9 @@ void ClearUI() ImageXZ.Source = null; ImageZY.Source = null; - m_bboxXY = new Cart3dGeom(); - m_bboxXZ = new Cart3dGeom(); - m_bboxZY = new Cart3dGeom(); + m_streamXY = null; + m_streamXZ = null; + m_streamZY = null; ECG.Data = null; @@ -183,18 +183,19 @@ private void FileOpenBtn_Click(object sender, RoutedEventArgs e) return; } + InitializeSlices(); + FrameSelector.Minimum = 0; - FrameSelector.Maximum = m_source.GetFrameCount()-1; + FrameSelector.Maximum = m_streamXY.GetFrameCount()-1; FrameSelector.IsEnabled = true; FrameSelector.Value = 0; - FrameCount.Text = "Frame count: " + m_source.GetFrameCount(); + FrameCount.Text = "Frame count: " + m_streamXY.GetFrameCount(); ProbeInfo.Text = "Probe name: "+ m_source.GetProbeInfo().name; InstanceUID.Text = "UID: " + m_source.GetSopInstanceUID(); - InitializeSlices(); DrawSlices(0); - DrawEcg(m_source.GetFrameTimes()[0]); + DrawEcg(m_streamXY.GetFrameTimes()[0]); } private void DrawEcg (double cur_time) @@ -267,13 +268,17 @@ private void FrameSelector_ValueChanged(object sender, RoutedPropertyChangedEven { var idx = (uint)FrameSelector.Value; DrawSlices(idx); - DrawEcg(m_source.GetFrameTimes()[idx]); + DrawEcg(m_streamXY.GetFrameTimes()[idx]); } private void InitializeSlices() { Debug.Assert(m_source != null); + uint stream_count = m_source.GetStreamCount(); + if (stream_count < 1) + throw new Exception("No image streams found"); + Cart3dGeom bbox = m_source.GetBoundingBox(); if (Math.Abs(bbox.dir3_y) > Math.Abs(bbox.dir2_y)) { // swap 2nd & 3rd axis, so that the 2nd becomes predominately "Y" @@ -285,41 +290,47 @@ private void InitializeSlices() // extend bounding-box axes, so that dir1, dir2 & dir3 have equal length ExtendBoundingBox(ref bbox); + const ushort HORIZONTAL_RES = 256; + const ushort VERTICAL_RES = 256; + // get XY plane (assumes 1st axis is "X" and 2nd is "Y") - m_bboxXY = bbox; - m_bboxXY.origin_x = m_bboxXY.origin_x + m_bboxXY.dir3_x / 2; - m_bboxXY.origin_y = m_bboxXY.origin_y + m_bboxXY.dir3_y / 2; - m_bboxXY.origin_z = m_bboxXY.origin_z + m_bboxXY.dir3_z / 2; - m_bboxXY.dir3_x = 0; - m_bboxXY.dir3_y = 0; - m_bboxXY.dir3_z = 0; + Cart3dGeom bboxXY = bbox; + bboxXY.origin_x = bboxXY.origin_x + bboxXY.dir3_x / 2; + bboxXY.origin_y = bboxXY.origin_y + bboxXY.dir3_y / 2; + bboxXY.origin_z = bboxXY.origin_z + bboxXY.dir3_z / 2; + bboxXY.dir3_x = 0; + bboxXY.dir3_y = 0; + bboxXY.dir3_z = 0; + m_streamXY = m_source.GetStream(0, bboxXY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") - m_bboxXZ = bbox; - m_bboxXZ.origin_x = m_bboxXZ.origin_x + m_bboxXZ.dir2_x / 2; - m_bboxXZ.origin_y = m_bboxXZ.origin_y + m_bboxXZ.dir2_y / 2; - m_bboxXZ.origin_z = m_bboxXZ.origin_z + m_bboxXZ.dir2_z / 2; - m_bboxXZ.dir2_x = m_bboxXZ.dir3_x; - m_bboxXZ.dir2_y = m_bboxXZ.dir3_y; - m_bboxXZ.dir2_z = m_bboxXZ.dir3_z; - m_bboxXZ.dir3_x = 0; - m_bboxXZ.dir3_y = 0; - m_bboxXZ.dir3_z = 0; + Cart3dGeom bboxXZ = bbox; + bboxXZ.origin_x = bboxXZ.origin_x + bboxXZ.dir2_x / 2; + bboxXZ.origin_y = bboxXZ.origin_y + bboxXZ.dir2_y / 2; + bboxXZ.origin_z = bboxXZ.origin_z + bboxXZ.dir2_z / 2; + bboxXZ.dir2_x = bboxXZ.dir3_x; + bboxXZ.dir2_y = bboxXZ.dir3_y; + bboxXZ.dir2_z = bboxXZ.dir3_z; + bboxXZ.dir3_x = 0; + bboxXZ.dir3_y = 0; + bboxXZ.dir3_z = 0; + m_streamXZ = m_source.GetStream(0, bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") - m_bboxZY = bbox; - m_bboxZY.origin_x = bbox.origin_x + bbox.dir1_x / 2; - m_bboxZY.origin_y = bbox.origin_y + bbox.dir1_y / 2; - m_bboxZY.origin_z = bbox.origin_z + bbox.dir1_z / 2; - m_bboxZY.dir1_x = bbox.dir3_x; - m_bboxZY.dir1_y = bbox.dir3_y; - m_bboxZY.dir1_z = bbox.dir3_z; - m_bboxZY.dir2_x = bbox.dir2_x; - m_bboxZY.dir2_y = bbox.dir2_y; - m_bboxZY.dir2_z = bbox.dir2_z; - m_bboxZY.dir3_x = 0; - m_bboxZY.dir3_y = 0; - m_bboxZY.dir3_z = 0; + Cart3dGeom bboxZY = bbox; + bboxZY.origin_x = bbox.origin_x + bbox.dir1_x / 2; + bboxZY.origin_y = bbox.origin_y + bbox.dir1_y / 2; + bboxZY.origin_z = bbox.origin_z + bbox.dir1_z / 2; + bboxZY.dir1_x = bbox.dir3_x; + bboxZY.dir1_y = bbox.dir3_y; + bboxZY.dir1_z = bbox.dir3_z; + bboxZY.dir2_x = bbox.dir2_x; + bboxZY.dir2_y = bbox.dir2_y; + bboxZY.dir2_z = bbox.dir2_z; + bboxZY.dir3_x = 0; + bboxZY.dir3_y = 0; + bboxZY.dir3_z = 0; + m_streamZY = m_source.GetStream(0, bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); } private void DrawSlices (uint frame) @@ -332,19 +343,17 @@ private void DrawSlices (uint frame) throw new Exception("Unexpected color-map format"); // retrieve image slices - const ushort HORIZONTAL_RES = 256; - const ushort VERTICAL_RES = 256; // get XY plane (assumes 1st axis is "X" and 2nd is "Y") - Image3d imageXY = m_source.GetFrame(frame, m_bboxXY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + Image3d imageXY = m_streamXY.GetFrame(frame); ImageXY.Source = GenerateBitmap(imageXY, color_map); // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") - Image3d imageXZ = m_source.GetFrame(frame, m_bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + Image3d imageXZ = m_streamXZ.GetFrame(frame); ImageXZ.Source = GenerateBitmap(imageXZ, color_map); // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") - Image3d imageZY = m_source.GetFrame(frame, m_bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + Image3d imageZY = m_streamZY.GetFrame(frame); ImageZY.Source = GenerateBitmap(imageZY, color_map); FrameTime.Text = "Frame time: " + imageXY.time; From 57e4c7b28f814db3599fca422bc36866d80f6000 Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Thu, 20 Feb 2020 09:56:01 +0100 Subject: [PATCH 5/5] Implement rudimentary CF simulation and display. The important part here is the TestViewer GenerateBitmap function, which serves as a reference implementation of color-flow display. --- DummyLoader/Image3dSource.cpp | 67 +++++++++++++++++++++- DummyLoader/Image3dSource.hpp | 5 +- DummyLoader/Image3dStream.cpp | 18 ++++++ TestViewer/MainWindow.xaml.cs | 101 +++++++++++++++++++++++++++++++--- 4 files changed, 181 insertions(+), 10 deletions(-) diff --git a/DummyLoader/Image3dSource.cpp b/DummyLoader/Image3dSource.cpp index d254c0d..133474f 100644 --- a/DummyLoader/Image3dSource.cpp +++ b/DummyLoader/Image3dSource.cpp @@ -37,6 +37,39 @@ Image3dSource::Image3dSource() { for (size_t i = 0; i < m_color_map_tissue.size(); ++i) m_color_map_tissue[i] = R8G8B8A8(static_cast(i), static_cast(i), static_cast(i), 0xFF); } + { + // red to blue flow scale with green at high BW + assert(m_color_map_flow.size() == 256*256); + for (size_t bw = 0; bw < 256; ++bw) { + // increasing green for high bandwidth + uint8_t green = (bw >= 192) ? static_cast(bw) : 0; + + for (size_t freq = 0; freq < 256; ++freq) { // unsigned counter, so freq>127 corresponds to negative velocities + // increasing red for positive velocities + uint8_t red = (freq < 128) ? 128+static_cast(freq) : 0; + if (green) + red = 0; + + // increasing blue for negative velocities + uint8_t blue = (freq >= 128) ? 128+static_cast(255-freq) : 0; + if (green) + blue = 0; + + m_color_map_flow[freq + bw*256] = R8G8B8A8(red, green, blue, 0xFF); + } + } + } + { + // flow arbitration scale + assert(m_flow_arb.size() == 256*256); + for (size_t bw = 0; bw < 256; ++bw) { + for (size_t freq = 0; freq < 256; ++freq) { // unsigned counter, so freq>127 corresponds to negative velocities + // show flow when |freq| >= 16 + bool show_flow = std::abs(static_cast(freq)) >= 16; + m_flow_arb[freq + bw*256] = show_flow ? 0xFF : 0x00; + } + } + } { // image geometry X Y Z Cart3dGeom geom = { -0.1f, 0, -0.075f,// origin @@ -46,8 +79,9 @@ Image3dSource::Image3dSource() { m_bbox = geom; } - // a single tissue stream + // simulate tissue + color-flow data m_stream_types.push_back(IMAGE_TYPE_TISSUE); + m_stream_types.push_back(IMAGE_TYPE_BLOOD_VEL); } Image3dSource::~Image3dSource() { @@ -73,8 +107,23 @@ HRESULT Image3dSource::GetStream(unsigned int index, Cart3dGeom out_geom, unsign CComPtr stream_obj; { // on-demand stream creation + Cart3dGeom bbox = m_bbox; + if (m_stream_types[index] == IMAGE_TYPE_BLOOD_VEL) { + // shrink color-flow sector to make it more realistic + vec3f origin, dir1, dir2, dir3; + std::tie(origin,dir1,dir2,dir3) = FromCart3dGeom(bbox); + + float SCALE_FACTOR = 0.8f; + origin += 0.5f*(1-SCALE_FACTOR)*(dir1 + dir2 + dir3); + dir1 *= SCALE_FACTOR; + dir2 *= SCALE_FACTOR; + dir3 *= SCALE_FACTOR; + + bbox = ToCart3dGeom(origin, dir1, dir2, dir3); + } + auto stream_cls = CreateLocalInstance(); - stream_cls->Initialize(m_stream_types[index], m_bbox, out_geom, max_resolution); + stream_cls->Initialize(m_stream_types[index], bbox, out_geom, max_resolution); stream_obj = stream_cls; // cast class pointer to interface } @@ -103,6 +152,20 @@ HRESULT Image3dSource::GetColorMap(ColorMapType type, /*out*/ImageFormat * forma memcpy(&color_map.GetAt(0), m_color_map_tissue.data(), sizeof(m_color_map_tissue)); *map = color_map.Detach(); // transfer ownership return S_OK; + } else if (type == TYPE_FLOW_COLOR) { + *format = IMAGE_FORMAT_R8G8B8A8; + // copy to new buffer + CComSafeArray color_map(4*static_cast(m_color_map_flow.size())); + memcpy(&color_map.GetAt(0), m_color_map_flow.data(), sizeof(m_color_map_flow)); + *map = color_map.Detach(); // transfer ownership + return S_OK; + } else if (type = TYPE_FLOW_ARB) { + *format = IMAGE_FORMAT_U8; + // copy to new buffer + CComSafeArray color_map(static_cast(m_flow_arb.size())); + memcpy(&color_map.GetAt(0), m_flow_arb.data(), sizeof(m_flow_arb)); + *map = color_map.Detach(); // transfer ownership + return S_OK; } return E_NOTIMPL; diff --git a/DummyLoader/Image3dSource.hpp b/DummyLoader/Image3dSource.hpp index ba292e3..3f63717 100644 --- a/DummyLoader/Image3dSource.hpp +++ b/DummyLoader/Image3dSource.hpp @@ -40,7 +40,10 @@ class ATL_NO_VTABLE Image3dSource : private: ProbeInfo m_probe; EcgSeries m_ecg; - std::array m_color_map_tissue; + + std::array m_color_map_tissue; + std::array m_color_map_flow; + std::array m_flow_arb; Cart3dGeom m_bbox = {}; std::vector m_stream_types; diff --git a/DummyLoader/Image3dStream.cpp b/DummyLoader/Image3dStream.cpp index 8256786..37381fb 100644 --- a/DummyLoader/Image3dStream.cpp +++ b/DummyLoader/Image3dStream.cpp @@ -55,6 +55,24 @@ void Image3dStream::Initialize (ImageType type, Cart3dGeom img_geom, Cart3dGeom m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, IMAGE_FORMAT_U8, dims, img_buf)); } + } else if (type == IMAGE_TYPE_BLOOD_VEL) { + // velocity & bandwidth scale color-flow data + unsigned short dims[] = { 20, 15, 10 }; // matches length of dir1, dir2 & dir3, so that the image squares become quadratic + std::vector img_buf(2 * dims[0] * dims[1] * dims[2]); + for (size_t frameNumber = 0; frameNumber < numFrames; ++frameNumber) { + for (unsigned int z = 0; z < dims[2]; ++z) { + for (unsigned int y = 0; y < dims[1]; ++y) { + for (unsigned int x = 0; x < dims[0]; ++x) { + int8_t & freq = reinterpret_cast(img_buf[0 + 2*(x + y*dims[0] + z*dims[0] * dims[1])]); + byte & bw = img_buf[1 + 2*(x + y*dims[0] + z*dims[0] * dims[1])]; + + freq = static_cast(255*(0.5f - y*1.0f/dims[1])); // [+127, -128] along Y axis + bw = static_cast(256*(x*1.0f/dims[0])); // [0,255] along X axis + } + } + } + m_frames.push_back(CreateImage3d(frameNumber*(duration/numFrames) + startTime, IMAGE_FORMAT_FREQ8POW8, dims, img_buf)); + } } else { abort(); // should never be reached } diff --git a/TestViewer/MainWindow.xaml.cs b/TestViewer/MainWindow.xaml.cs index 9dba342..7b54a3e 100644 --- a/TestViewer/MainWindow.xaml.cs +++ b/TestViewer/MainWindow.xaml.cs @@ -44,8 +44,11 @@ public partial class MainWindow : Window IImage3dSource m_source; IImage3dStream m_streamXY; + IImage3dStream m_streamXYcf; IImage3dStream m_streamXZ; + IImage3dStream m_streamXZcf; IImage3dStream m_streamZY; + IImage3dStream m_streamZYcf; public MainWindow() { @@ -69,8 +72,11 @@ void ClearUI() ImageZY.Source = null; m_streamXY = null; + m_streamXYcf = null; m_streamXZ = null; + m_streamXZcf = null; m_streamZY = null; + m_streamZYcf = null; ECG.Data = null; @@ -302,6 +308,8 @@ private void InitializeSlices() bboxXY.dir3_y = 0; bboxXY.dir3_z = 0; m_streamXY = m_source.GetStream(0, bboxXY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + if (stream_count >= 2) + m_streamXYcf = m_source.GetStream(1, bboxXY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); // assume 2nd stream is color-flow // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") Cart3dGeom bboxXZ = bbox; @@ -315,6 +323,8 @@ private void InitializeSlices() bboxXZ.dir3_y = 0; bboxXZ.dir3_z = 0; m_streamXZ = m_source.GetStream(0, bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + if (stream_count >= 2) + m_streamXZcf = m_source.GetStream(1, bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); // assume 2nd stream is color-flow // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") Cart3dGeom bboxZY = bbox; @@ -331,30 +341,68 @@ private void InitializeSlices() bboxZY.dir3_y = 0; bboxZY.dir3_z = 0; m_streamZY = m_source.GetStream(0, bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); + if (stream_count >= 2) + m_streamZYcf = m_source.GetStream(1, bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); // assume 2nd stream is color-flow } - private void DrawSlices (uint frame) + private void DrawSlices(uint frame) { Debug.Assert(m_source != null); ImageFormat image_format; - byte[] color_map = m_source.GetColorMap(ColorMapType.TYPE_TISSUE_COLOR, out image_format); + byte[] tissue_map = m_source.GetColorMap(ColorMapType.TYPE_TISSUE_COLOR, out image_format); if (image_format != ImageFormat.IMAGE_FORMAT_R8G8B8A8) throw new Exception("Unexpected color-map format"); - // retrieve image slices + byte[] cf_map = m_source.GetColorMap(ColorMapType.TYPE_FLOW_COLOR, out image_format); + if (image_format != ImageFormat.IMAGE_FORMAT_R8G8B8A8) + throw new Exception("Unexpected color-map format"); + + byte[] arb_table = m_source.GetColorMap(ColorMapType.TYPE_FLOW_ARB, out image_format); + if (image_format != ImageFormat.IMAGE_FORMAT_U8) + throw new Exception("Unexpected color-map format"); + + uint cf_frame = 0; + if (m_streamXYcf != null) { + // find closest corresponding CF frame + double t_time = m_streamXY.GetFrame(frame).time; + double[] cf_times = m_streamXYcf.GetFrameTimes(); + + int closest_idx = 0; + for (int i = 1; i < cf_times.Length; ++i) { + if (Math.Abs(cf_times[i] - t_time) < Math.Abs(cf_times[closest_idx] - t_time)) + closest_idx = i; + } + + cf_frame = (uint)closest_idx; + } // get XY plane (assumes 1st axis is "X" and 2nd is "Y") Image3d imageXY = m_streamXY.GetFrame(frame); - ImageXY.Source = GenerateBitmap(imageXY, color_map); - + if (m_streamXYcf != null) { + Image3d imageXYcf = m_streamXYcf.GetFrame(cf_frame); + ImageXY.Source = GenerateBitmap(imageXY, imageXYcf, tissue_map, cf_map, arb_table); + } else { + ImageXY.Source = GenerateBitmap(imageXY, tissue_map); + } + // get XZ plane (assumes 1st axis is "X" and 3rd is "Z") Image3d imageXZ = m_streamXZ.GetFrame(frame); - ImageXZ.Source = GenerateBitmap(imageXZ, color_map); + if (m_streamXZcf != null) { + Image3d imageXZcf = m_streamXZcf.GetFrame(cf_frame); + ImageXZ.Source = GenerateBitmap(imageXZ, imageXZcf, tissue_map, cf_map, arb_table); + } else { + ImageXZ.Source = GenerateBitmap(imageXZ, tissue_map); + } // get ZY plane (assumes 2nd axis is "Y" and 3rd is "Z") Image3d imageZY = m_streamZY.GetFrame(frame); - ImageZY.Source = GenerateBitmap(imageZY, color_map); + if (m_streamZYcf != null) { + Image3d imageZYcf = m_streamZYcf.GetFrame(cf_frame); + ImageZY.Source = GenerateBitmap(imageZY, imageZYcf, tissue_map, cf_map, arb_table); + } else { + ImageZY.Source = GenerateBitmap(imageZY, tissue_map); + } FrameTime.Text = "Frame time: " + imageXY.time; } @@ -387,6 +435,45 @@ private WriteableBitmap GenerateBitmap(Image3d t_img, byte[] t_map) return bitmap; } + private WriteableBitmap GenerateBitmap(Image3d t_img, Image3d cf_img, byte[] t_map, byte[] cf_map, byte[] arb_table) + { + Debug.Assert(t_img.dims.SequenceEqual(cf_img.dims)); + Debug.Assert(t_img.format == ImageFormat.IMAGE_FORMAT_U8); + Debug.Assert(cf_img.format == ImageFormat.IMAGE_FORMAT_FREQ8POW8); + + WriteableBitmap bitmap = new WriteableBitmap(t_img.dims[0], t_img.dims[1], 96.0, 96.0, PixelFormats.Rgb24, null); + bitmap.Lock(); + unsafe { + for (int y = 0; y < bitmap.Height; ++y) { + for (int x = 0; x < bitmap.Width; ++x) { + byte t_val = t_img.data[x + y * t_img.stride0]; + ushort cf_val = BitConverter.ToUInt16(cf_img.data, 2*x + y*(int)cf_img.stride0); + + uint rgba = 0; + if (arb_table[cf_val] > t_val) { + // display color-flow overlay + rgba = BitConverter.ToUInt32(cf_map, 4*cf_val); + } else { + // display tissue underlay + rgba = BitConverter.ToUInt32(t_map, 4*t_val); + } + + byte[] channels = BitConverter.GetBytes(rgba); + + // assign red, green & blue + byte* pixel = (byte*)bitmap.BackBuffer + x * (bitmap.Format.BitsPerPixel / 8) + y * bitmap.BackBufferStride; + pixel[0] = channels[0]; // red + pixel[1] = channels[1]; // green + pixel[2] = channels[2]; // blue + // discard alpha channel + } + } + } + bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight)); + bitmap.Unlock(); + return bitmap; + } + static void SwapVals(ref float v1, ref float v2) { float tmp = v1;