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 df4d2ab..04c7a79 100644 Binary files a/DummyLoader/DummyLoader.rc and b/DummyLoader/DummyLoader.rc differ diff --git a/DummyLoader/Image3dSource.cpp b/DummyLoader/Image3dSource.cpp index 08cd789..133474f 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() { @@ -39,113 +37,138 @@ 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 0.20f,0, 0, // dir1 (width) 0, 0.10f, 0, // dir2 (depth) 0, 0, 0.15f};// dir3 (elevation) - m_img_geom = geom; + m_bbox = 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, FORMAT_U8, dims, img_buf)); - } - } + // simulate tissue + color-flow data + m_stream_types.push_back(IMAGE_TYPE_TISSUE); + m_stream_types.push_back(IMAGE_TYPE_BLOOD_VEL); } 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 == 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 + 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], 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; } -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; + } 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; } HRESULT Image3dSource::GetECG(/*out*/EcgSeries *ecg) { diff --git a/DummyLoader/Image3dSource.hpp b/DummyLoader/Image3dSource.hpp index 4806d6b..3f63717 100644 --- a/DummyLoader/Image3dSource.hpp +++ b/DummyLoader/Image3dSource.hpp @@ -16,15 +16,14 @@ 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; - 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; @@ -41,9 +40,13 @@ class ATL_NO_VTABLE Image3dSource : private: ProbeInfo m_probe; EcgSeries m_ecg; - std::array m_color_map_tissue; - Cart3dGeom m_img_geom = {}; - std::vector m_frames; + + 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; }; OBJECT_ENTRY_AUTO(__uuidof(Image3dSource), Image3dSource) diff --git a/DummyLoader/Image3dStream.cpp b/DummyLoader/Image3dStream.cpp index 33ccde5..37381fb 100644 --- a/DummyLoader/Image3dStream.cpp +++ b/DummyLoader/Image3dStream.cpp @@ -1 +1,128 @@ #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 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 + } +} + +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 e6d06e1..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 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 e1256ce..6ee0ce1 100644 --- a/Image3dAPI/IImage3d.idl +++ b/Image3dAPI/IImage3d.idl @@ -22,14 +22,34 @@ 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).")] + 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) + 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 @@ -96,7 +116,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;") @@ -274,25 +294,43 @@ 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 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/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 bb4101b..00f0b98 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; } @@ -16,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 5e88c3d..f9f5a7c 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"; } } @@ -60,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; } @@ -82,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; @@ -92,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 e4019f9..f00cbed 100644 --- a/TestPython/ITKExport.py +++ b/TestPython/ITKExport.py @@ -55,12 +55,18 @@ 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() + 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 d48fcff..85e4461 100644 --- a/TestPython/TestPython.py +++ b/TestPython/TestPython.py @@ -38,13 +38,18 @@ 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() + 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 85f0d3e..7b54a3e 100644 --- a/TestViewer/MainWindow.xaml.cs +++ b/TestViewer/MainWindow.xaml.cs @@ -43,9 +43,12 @@ 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_streamXYcf; + IImage3dStream m_streamXZ; + IImage3dStream m_streamXZcf; + IImage3dStream m_streamZY; + IImage3dStream m_streamZYcf; public MainWindow() { @@ -68,9 +71,12 @@ void ClearUI() ImageXZ.Source = null; ImageZY.Source = null; - m_bboxXY = new Cart3dGeom(); - m_bboxXZ = new Cart3dGeom(); - m_bboxZY = new Cart3dGeom(); + m_streamXY = null; + m_streamXYcf = null; + m_streamXZ = null; + m_streamXZcf = null; + m_streamZY = null; + m_streamZYcf = null; ECG.Data = null; @@ -183,18 +189,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 +274,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,71 +296,120 @@ 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 }); + 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") - 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 }); + 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") - 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 }); + 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); - uint[] color_map = m_source.GetColorMap(); + ImageFormat 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"); + + 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; + } - // retrieve image slices - const ushort HORIZONTAL_RES = 256; - const ushort VERTICAL_RES = 256; + cf_frame = (uint)closest_idx; + } // 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 }); - ImageXY.Source = GenerateBitmap(imageXY, color_map); - + Image3d imageXY = m_streamXY.GetFrame(frame); + 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_source.GetFrame(frame, m_bboxXZ, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); - ImageXZ.Source = GenerateBitmap(imageXZ, color_map); + Image3d imageXZ = m_streamXZ.GetFrame(frame); + 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_source.GetFrame(frame, m_bboxZY, new ushort[] { HORIZONTAL_RES, VERTICAL_RES, 1 }); - ImageZY.Source = GenerateBitmap(imageZY, color_map); + Image3d imageZY = m_streamZY.GetFrame(frame); + 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; } - private WriteableBitmap GenerateBitmap(Image3d t_img, uint[] t_map) + private WriteableBitmap GenerateBitmap(Image3d t_img, byte[] 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(); @@ -359,7 +419,46 @@ 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; + 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; + } + + 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;