diff --git a/src/Registration/NiftyMoMo/BSplineTransformation.cpp b/src/Registration/NiftyMoMo/BSplineTransformation.cpp index ee8df7dab..afcb6264c 100644 --- a/src/Registration/NiftyMoMo/BSplineTransformation.cpp +++ b/src/Registration/NiftyMoMo/BSplineTransformation.cpp @@ -1337,7 +1337,7 @@ double BSplineTransformation::GetConstraintValue() //------------------------------------------------------------------ // BSplineTransformation::GetDVFGradientWRTTransformationParameters //------------------------------------------------------------------ -BSplineTransformation::PrecisionType* BSplineTransformation::GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn, nifti_image* sourceImage ) +BSplineTransformation::PrecisionType* BSplineTransformation::GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn ) { #ifndef NDEBUG std::cout << "Called BSplineTransformation::GetDVFGradientWRTTransformationParameters()" << std::endl; @@ -1369,12 +1369,6 @@ BSplineTransformation::PrecisionType* BSplineTransformation::GetDVFGradientWRTTr nmm_exit( 1, __FILE__, __LINE__ ); } - // Need to reorientate from index gradient to real-world gradient - // Note: Performing the reorientation here is way more efficient, since - // only the transformation parameters need to be touched (and not) - // the complete DVF - this->ReorientateVectorImage( outDVFGradWRTTrafoParams, sourceImage->sto_ijk ); - // Copy over the data pointer from the the image and detach it. Then delete the image. PrecisionType* outDVFGradWRTTrafoParamData = (PrecisionType*) outDVFGradWRTTrafoParams->data; outDVFGradWRTTrafoParams->data = nullptr; diff --git a/src/Registration/NiftyMoMo/Transformation.cpp b/src/Registration/NiftyMoMo/Transformation.cpp index d6304d728..1ccbe14f9 100644 --- a/src/Registration/NiftyMoMo/Transformation.cpp +++ b/src/Registration/NiftyMoMo/Transformation.cpp @@ -134,6 +134,12 @@ void Transformation::GetImageGradientWRTDVF( nifti_image* sourceImage, nifti_ima this->interpolation, this->warpedPaddingValue, 0 ); + // Need to reorientate from index gradient to real-world gradient + // Note: Performing the reorientation here is way more efficient, since + // only the transformation parameters need to be touched (and not) + // the complete DVF + this->ReorientateVectorImage( outWarpedGradientImage, sourceImage->sto_ijk ); + return; } diff --git a/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/BSplineTransformation.h b/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/BSplineTransformation.h index 677715175..cc5f2b7d9 100644 --- a/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/BSplineTransformation.h +++ b/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/BSplineTransformation.h @@ -87,7 +87,7 @@ class BSplineTransformation :public Transformation * \param denseDVFIn Deformation vector field * \param sourceImage The image for which the gradient is calculated */ - virtual BSplineTransformation::PrecisionType* GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn, nifti_image* sourceImage ); + virtual BSplineTransformation::PrecisionType* GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn ); /** Calculate the gradient of the constraint term (regularisation) for the transformation with the current parameters. */ diff --git a/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/Transformation.h b/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/Transformation.h index fdb8785b1..188c29fc4 100644 --- a/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/Transformation.h +++ b/src/Registration/NiftyMoMo/include/sirf/NiftyMoMo/Transformation.h @@ -87,7 +87,7 @@ class Transformation * \return pointer to the internally allocated output data of length numberOfParameters. Needs to be freed outside * of this class since no internal references will be kept. */ - virtual PrecisionType* GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn, nifti_image* sourceImage ) = 0; + virtual PrecisionType* GetDVFGradientWRTTransformationParameters( nifti_image* denseDVFIn ) = 0; /** Get the gradient of the regularisation/constraint with respect to the transformation parameters * \f$ \frac{\partial \mathcal{R}_t}{\partial\mathbf{M}_t} \f$ diff --git a/src/Registration/cReg/CMakeLists.txt b/src/Registration/cReg/CMakeLists.txt index ba4d7e787..a560b5af7 100644 --- a/src/Registration/cReg/CMakeLists.txt +++ b/src/Registration/cReg/CMakeLists.txt @@ -32,7 +32,10 @@ SET(SOURCES "NiftiImageData3D.cpp" "NiftiImageData3DTensor.cpp" "NiftiImageData3DDeformation.cpp" - "NiftiImageData3DDisplacement.cpp") + "NiftiImageData3DDisplacement.cpp" + "NiftiImageData3DBSpline.cpp" + "ControlPointGridToDeformationConverter.cpp" + ) # If we're also wrapping to python or matlab, include the c-files IF(BUILD_PYTHON OR BUILD_MATLAB) diff --git a/src/Registration/cReg/ControlPointGridToDeformationConverter.cpp b/src/Registration/cReg/ControlPointGridToDeformationConverter.cpp new file mode 100644 index 000000000..387b06353 --- /dev/null +++ b/src/Registration/cReg/ControlPointGridToDeformationConverter.cpp @@ -0,0 +1,111 @@ +/* +SyneRBI Synergistic Image Reconstruction Framework (SIRF) +Copyright 2020 University College London + +This is software developed for the Collaborative Computational +Project in Synergistic Reconstruction for Biomedical Imaging (formerly CCP PETMR) +(http://www.ccpsynerbi.ac.uk/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +/*! +\file +\ingroup Registration +\brief Class for converting control point grids to deformation field transformations. + +\author Richard Brown +\author SyneRBI +*/ + +#include "sirf/Reg/ControlPointGridToDeformationConverter.h" +#include "sirf/Reg/NiftiImageData3DDeformation.h" +#include "sirf/Reg/NiftiImageData3DBSpline.h" +#include "sirf/NiftyMoMo/BSplineTransformation.h" + +using namespace sirf; + +template +ControlPointGridToDeformationConverter:: +ControlPointGridToDeformationConverter() +{ + for (unsigned i=0; i<3; ++i) + _spacing[i] = std::numeric_limits::quiet_NaN(); +} + +template +void +ControlPointGridToDeformationConverter:: +set_cpg_spacing(const float spacing[3]) +{ + for (unsigned i=0; i<3; ++i) + _spacing[i] = spacing[i]; +} + +template +void +ControlPointGridToDeformationConverter:: +set_reference_image(const NiftiImageData &ref) +{ + _template_ref_sptr = ref.clone(); +} + +template +NiftiImageData3DDeformation +ControlPointGridToDeformationConverter:: +forward(const NiftiImageData3DBSpline &cpg) const +{ + check_is_set_up(); +// NiftiImageData3DDeformation dvf; +// dvf.create_from_cpp(cpg, *_template_ref_sptr); +// return dvf; + return cpg.get_as_deformation_field(*_template_ref_sptr); +} + +template +NiftiImageData3DBSpline +ControlPointGridToDeformationConverter:: +backward(const NiftiImageData3DDeformation &dvf) const +{ + check_is_set_up(); + // not marked const, so copy + float spacing_nonconst[3] = {_spacing[0], _spacing[1], _spacing[2]}; + // Get raw nifti_image from reference image + nifti_image *ref_ptr = _template_ref_sptr->get_raw_nifti_sptr().get(); + // Create the NiftyMoMo bspline transformation class + NiftyMoMo::BSplineTransformation bspline(ref_ptr, 1, spacing_nonconst); + // Get cpg_ptr + nifti_image *cpg_ptr = bspline.GetTransformationAsImage(); + // Convert DVF to CPG + std::shared_ptr > dvf_sptr = dvf.clone(); + cpg_ptr->data = bspline.GetDVFGradientWRTTransformationParameters(dvf_sptr->get_raw_nifti_sptr().get()); + cpg_ptr->intent_p1 = SPLINE_VEL_GRID; + return NiftiImageData3DBSpline(*cpg_ptr); +} + +template +void ControlPointGridToDeformationConverter:: +check_is_set_up() const +{ + // Has spacing been set? + for (unsigned i=0; i<3; ++i) + if (std::isnan(_spacing[i])) + throw std::runtime_error("ControlPointGridToDeformationConverter: Set CPG spacing."); + + // Has template deformation been set? + if (!_template_ref_sptr) + throw std::runtime_error("ControlPointGridToDeformationConverter: Set template DVF."); +} + +namespace sirf { +template class ControlPointGridToDeformationConverter; +} diff --git a/src/Registration/cReg/NiftiImageData.cpp b/src/Registration/cReg/NiftiImageData.cpp index c6adbbc9a..3cb4ab7c5 100644 --- a/src/Registration/cReg/NiftiImageData.cpp +++ b/src/Registration/cReg/NiftiImageData.cpp @@ -36,6 +36,7 @@ limitations under the License. #include "sirf/Reg/NiftiImageData3DTensor.h" #include "sirf/Reg/NiftiImageData3DDeformation.h" #include "sirf/Reg/NiftiImageData3DDisplacement.h" +#include "sirf/Reg/NiftiImageData3DBSpline.h" #include "sirf/Reg/AffineTransformation.h" #include "sirf/Reg/NiftyResample.h" #include @@ -549,7 +550,10 @@ void NiftiImageData::check_dimensions(const NiftiImageDataType image_t if (image_type == _3D) { ndim= 3; nt= 1; nu= 1; intent_code = NIFTI_INTENT_NONE; intent_p1=-1; } else if (image_type == _3DTensor) { ndim= 5; nt= 1; nu= 3; intent_code = NIFTI_INTENT_VECTOR; intent_p1=-1; } else if (image_type == _3DDisp) { ndim= 5; nt= 1; nu= 3; intent_code = NIFTI_INTENT_VECTOR; intent_p1=DISP_FIELD; } - else /*if (image_type == _3DDef)*/ { ndim= 5; nt= 1; nu= 3; intent_code = NIFTI_INTENT_VECTOR; intent_p1=DEF_FIELD; } + else if (image_type == _3DDef) { ndim= 5; nt= 1; nu= 3; intent_code = NIFTI_INTENT_VECTOR; intent_p1=DEF_FIELD; } + else if (image_type == _3DBSpl) { ndim= 5; nt= 1; nu= 3; intent_code = NIFTI_INTENT_VECTOR; intent_p1=SPLINE_VEL_GRID; } + else + throw std::runtime_error("NiftiImageData::check_dimensions: Unknown image type"); // Check everthing is as it should be. -1 means we don't care about it // (e.g., NiftiImageData3D doesn't care about intent_p1, which is used by NiftyReg for Disp/Def fields) @@ -571,16 +575,19 @@ void NiftiImageData::check_dimensions(const NiftiImageDataType image_t else if (typeid(*this) == typeid(NiftiImageData3DTensor)) ss << "NiftiImageData3DTensor"; else if (typeid(*this) == typeid(NiftiImageData3DDisplacement)) ss << "NiftiImageData3DDisplacement"; else if (typeid(*this) == typeid(NiftiImageData3DDeformation)) ss << "NiftiImageData3DDeformation"; + else if (typeid(*this) == typeid(NiftiImageData3DBSpline)) ss << "NiftiImageData3DBSpline"; ss << ".\n\t\tExpected params: ndim = " << ndim << ", nu = " << nu << ", nt = " << nt; if (intent_code == NIFTI_INTENT_NONE) ss << ", intent_code = None"; else if (intent_code == NIFTI_INTENT_VECTOR) ss << ", intent_code = Vector"; if (intent_p1 == 0) ss << ", intent_p1 = Deformation"; else if (intent_p1 == 1) ss << ", intent_p1 = Displacement"; + else if (intent_p1 == SPLINE_VEL_GRID) ss << ", intent_p1 = Control point grid"; ss << "\n\t\tActual params: ndim = " << _nifti_image->ndim << ", nu = " << _nifti_image->nu << ", nt = " << _nifti_image->nt; if (_nifti_image->intent_code == NIFTI_INTENT_NONE) ss << ", intent_code = None"; else if (_nifti_image->intent_code == NIFTI_INTENT_VECTOR) ss << ", intent_code = Vector"; if (intent_p1 != -1 && _nifti_image->intent_p1 == 0) ss << ", intent_p1 = Deformation"; else if (intent_p1 != -1 && _nifti_image->intent_p1 == 1) ss << ", intent_p1 = Displacement"; + else if (intent_p1 != -1 && _nifti_image->intent_p1 == SPLINE_VEL_GRID) ss << ", intent_p1 = Control point grid"; //std::cout << ss.str() << "\n"; throw std::runtime_error(ss.str()); } diff --git a/src/Registration/cReg/NiftiImageData3DBSpline.cpp b/src/Registration/cReg/NiftiImageData3DBSpline.cpp new file mode 100644 index 000000000..7091149f3 --- /dev/null +++ b/src/Registration/cReg/NiftiImageData3DBSpline.cpp @@ -0,0 +1,78 @@ +/* +SyneRBI Synergistic Image Reconstruction Framework (SIRF) +Copyright 2017 - 2020 University College London + +This is software developed for the Collaborative Computational +Project in Synergistic Reconstruction for Biomedical Imaging (formerly CCP PETMR) +(http://www.ccpsynerbi.ac.uk/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +/*! +\file +\ingroup Registration +\brief Class for deformation field transformations. + +\author Richard Brown +\author SyneRBI +*/ + +#include "sirf/Reg/NiftiImageData3DBSpline.h" +#include "sirf/Reg/NiftiImageData3DDeformation.h" +#include "sirf/NiftyMoMo/BSplineTransformation.h" + +using namespace sirf; + +template +void NiftiImageData3DBSpline::create_from_3D_image(const NiftiImageData &image) +{ + NiftiImageData3DTensor::create_from_3D_image(image); + this->_nifti_image->intent_p1 = SPLINE_VEL_GRID; +} + +template +NiftiImageData3DDeformation NiftiImageData3DBSpline::get_as_deformation_field(const NiftiImageData &ref) const +{ + // Get spacing of reference image + float spacing[3]; + for (unsigned i=0; i<3; ++i) + spacing[i] = this->_nifti_image->pixdim[i+1]; + // Create the NiftyMoMo bspline transformation class + NiftyMoMo::BSplineTransformation bspline(ref.clone()->get_raw_nifti_sptr().get(), 1, spacing); + // Set the CPG + bspline.SetParameters(static_cast(this->_nifti_image->data), false); + // Get the DVF + nifti_image *output_def_ptr = bspline.GetDeformationVectorField(ref.get_raw_nifti_sptr().get()); + return NiftiImageData3DDeformation(*output_def_ptr); +} + +template +NiftiImageData3DBSpline* +NiftiImageData3DBSpline::get_inverse_impl_nr(const std::shared_ptr >) const +{ + throw std::runtime_error("NiftiImageData3DBSpline::get_inverse_impl_nr not yet implemented."); +} + +template +NiftiImageData3DBSpline* +NiftiImageData3DBSpline::get_inverse_impl_vtk(const std::shared_ptr >) const +{ + throw std::runtime_error("NiftiImageData3DBSpline::get_inverse_impl_vtk not yet implemented."); +#ifndef SIRF_VTK + throw std::runtime_error("Build SIRF with VTK support for this functionality"); +#endif +} + +namespace sirf { +template class NiftiImageData3DBSpline; +} diff --git a/src/Registration/cReg/NiftiImageData3DDeformation.cpp b/src/Registration/cReg/NiftiImageData3DDeformation.cpp index 1dbe9eec9..68dd41fa9 100644 --- a/src/Registration/cReg/NiftiImageData3DDeformation.cpp +++ b/src/Registration/cReg/NiftiImageData3DDeformation.cpp @@ -54,11 +54,13 @@ void NiftiImageData3DDeformation::create_from_3D_image(const NiftiImag } template -void NiftiImageData3DDeformation::create_from_cpp(NiftiImageData3DTensor &cpp, const NiftiImageData &ref) +void NiftiImageData3DDeformation::create_from_cpp(const NiftiImageData3DTensor &cpp, const NiftiImageData &ref) { this->create_from_3D_image(ref); - reg_spline_getDeformationField(cpp.get_raw_nifti_sptr().get(), + auto cpp_clone = cpp.clone(); + + reg_spline_getDeformationField(cpp_clone->get_raw_nifti_sptr().get(), this->_nifti_image.get(), NULL, false, //composition diff --git a/src/Registration/cReg/cReg.cpp b/src/Registration/cReg/cReg.cpp index 9a52feeb0..29a23213c 100644 --- a/src/Registration/cReg/cReg.cpp +++ b/src/Registration/cReg/cReg.cpp @@ -26,6 +26,8 @@ limitations under the License. #include "sirf/Reg/NiftiImageData3DTensor.h" #include "sirf/Reg/NiftiImageData3DDisplacement.h" #include "sirf/Reg/NiftiImageData3DDeformation.h" +#include "sirf/Reg/NiftiImageData3DBSpline.h" +#include "sirf/Reg/ControlPointGridToDeformationConverter.h" #include "sirf/Reg/NiftyAladinSym.h" #include "sirf/Reg/NiftyF3dSym.h" #include "sirf/Reg/NiftyResample.h" @@ -68,6 +70,10 @@ void* cReg_newObject(const char* name) return newObjectHandle(std::shared_ptr >(new NiftiImageData3DDisplacement)); if (strcmp(name, "NiftiImageData3DDeformation") == 0) return newObjectHandle(std::shared_ptr >(new NiftiImageData3DDeformation)); + if (strcmp(name, "NiftiImageData3DBSpline") == 0) + return newObjectHandle(std::shared_ptr >(new NiftiImageData3DBSpline)); + if (strcmp(name, "ControlPointGridToDeformationConverter") == 0) + return newObjectHandle(std::shared_ptr >(new ControlPointGridToDeformationConverter)); if (strcmp(name, "NiftyAladinSym") == 0) return newObjectHandle(std::shared_ptr >(new NiftyAladinSym)); if (strcmp(name, "NiftyF3dSym") == 0) @@ -159,6 +165,11 @@ void* cReg_objectFromFile(const char* name, const char* filename) sptr(new NiftiImageData3DDeformation(filename)); return newObjectHandle(sptr); } + if (strcmp(name, "NiftiImageData3DBSpline") == 0) { + std::shared_ptr > + sptr(new NiftiImageData3DBSpline(filename)); + return newObjectHandle(sptr); + } if (strcmp(name, "AffineTransformation") == 0) { std::shared_ptr > sptr(new AffineTransformation(filename)); @@ -533,6 +544,12 @@ void* cReg_NiftiImageData3DTensor_construct_from_3_components(const char* obj, c sptr.reset(new NiftiImageData3DDisplacement(x,y,z)); else if (strcmp(obj,"NiftiImageData3DDeformation") == 0) sptr.reset(new NiftiImageData3DDeformation(x,y,z)); + else if (strcmp(obj,"NiftiImageData3DBSpline") == 0) + sptr.reset(new NiftiImageData3DBSpline(x,y,z)); + else + throw std::runtime_error( + "cReg_NiftiImageData3DTensor_construct_from_3_components, unknown type:" + + std::string(obj)); return newObjectHandle(sptr); } CATCH; @@ -547,6 +564,17 @@ void* cReg_NiftiImageData3DTensor_flip_component(const void *ptr, const int dim) } CATCH; } +extern "C" +void* cReg_NiftiImageData3DTensor_get_tensor_component(const void *ptr, const int dim) +{ + try { + NiftiImageData3DTensor& im = objectFromHandle >(ptr); + std::shared_ptr > im_sptr = im.get_tensor_component(dim); + std::shared_ptr > im3D_sptr = std::make_shared >(*im_sptr); + return newObjectHandle(im3D_sptr); + } + CATCH; +} // -------------------------------------------------------------------------------- // // NiftiImageData3DDeformation // -------------------------------------------------------------------------------- // @@ -610,6 +638,61 @@ void* cReg_NiftiImageData3DDisplacement_create_from_def(const void* def_ptr) CATCH; } +// -------------------------------------------------------------------------------- // +// ControlPointGridToDeformationConverter +// -------------------------------------------------------------------------------- // +extern "C" +void* cReg_CPG2DVF_set_cpg_spacing(const void* converter_ptr, const float spacing_x, const float spacing_y, const float spacing_z) +{ + try { + ControlPointGridToDeformationConverter& cpg_2_dvf_converter = + objectFromHandle >(converter_ptr); + const float spacing[3] = {spacing_x, spacing_y, spacing_z}; + cpg_2_dvf_converter.set_cpg_spacing(spacing); + return new DataHandle; + } + CATCH; +} +extern "C" +void* cReg_CPG2DVF_set_ref_im(const void* converter_ptr, const void* ref_im_ptr) +{ + try { + ControlPointGridToDeformationConverter& cpg_2_dvf_converter = + objectFromHandle >(converter_ptr); + NiftiImageData& ref_im = + objectFromHandle >(ref_im_ptr); + cpg_2_dvf_converter.set_reference_image(ref_im); + return new DataHandle; + } + CATCH; +} +extern "C" +void* cReg_CPG2DVF_forward(const void* converter_ptr, const void* cpg_ptr) +{ + try { + ControlPointGridToDeformationConverter& cpg_2_dvf_converter = + objectFromHandle >(converter_ptr); + NiftiImageData3DBSpline& cpg = + objectFromHandle >(cpg_ptr); + NiftiImageData3DDeformation def = cpg_2_dvf_converter.forward(cpg); + return newObjectHandle(std::make_shared >(def)); + } + CATCH; +} +extern "C" +void* cReg_CPG2DVF_backward(const void* converter_ptr, const void* dvf_ptr) +{ + try { + ControlPointGridToDeformationConverter& cpg_2_dvf_converter = + objectFromHandle >(converter_ptr); + NiftiImageData3DDeformation& dvf = + objectFromHandle >(dvf_ptr); + NiftiImageData3DBSpline cpg = cpg_2_dvf_converter.backward(dvf); + return newObjectHandle(std::make_shared >(cpg)); + } + CATCH; +} + // -------------------------------------------------------------------------------- // // Registration // -------------------------------------------------------------------------------- // @@ -903,6 +986,8 @@ void* cReg_Transformation_get_as_deformation_field(const void* ptr, const char* trans = &objectFromHandle >(ptr); else if (strcmp(name,"NiftiImageData3DDeformation") == 0) trans = &objectFromHandle >(ptr); + else if (strcmp(name,"NiftiImageData3DBSpline") == 0) + trans = &objectFromHandle >(ptr); else throw std::runtime_error("cReg_Transformation_get_as_deformation_field: type should be affine, disp or def."); diff --git a/src/Registration/cReg/include/sirf/Reg/ControlPointGridToDeformationConverter.h b/src/Registration/cReg/include/sirf/Reg/ControlPointGridToDeformationConverter.h new file mode 100644 index 000000000..464655a85 --- /dev/null +++ b/src/Registration/cReg/include/sirf/Reg/ControlPointGridToDeformationConverter.h @@ -0,0 +1,76 @@ +/* +SyneRBI Synergistic Image Reconstruction Framework (SIRF) +Copyright 2020 University College London + +This is software developed for the Collaborative Computational +Project in Synergistic Reconstruction for Biomedical Imaging (formerly CCP PETMR) +(http://www.ccpsynerbi.ac.uk/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +/*! +\file +\ingroup Registration +\brief Class for converting control point grids to deformation field transformations. + +\author Richard Brown +\author SyneRBI +*/ + +#pragma once + +#include + +namespace sirf { + +// Forward declarations +template class NiftiImageData; +template class NiftiImageData3DDeformation; +template class NiftiImageData3DBSpline; + +/*! +\ingroup Registration +\brief Class for converting control point grids to deformation field transformations. + +\author Richard Brown +\author SyneRBI +*/ +template +class ControlPointGridToDeformationConverter +{ +public: + + /// Constructor + ControlPointGridToDeformationConverter(); + + /// Set CPG spacing + void set_cpg_spacing(const float spacing[3]); + + /// Set reference image for generating dvfs + void set_reference_image(const NiftiImageData &ref); + + /// CPG to DVF + NiftiImageData3DDeformation forward(const NiftiImageData3DBSpline &cpg) const; + + /// DVF to CPG + NiftiImageData3DBSpline backward(const NiftiImageData3DDeformation &dvf) const; + +private: + + /// Check is set up + void check_is_set_up() const; + + float _spacing[3]; + std::shared_ptr > _template_ref_sptr; +}; +} diff --git a/src/Registration/cReg/include/sirf/Reg/NiftiImageData.h b/src/Registration/cReg/include/sirf/Reg/NiftiImageData.h index 406c4a498..e6eb0fcbd 100644 --- a/src/Registration/cReg/include/sirf/Reg/NiftiImageData.h +++ b/src/Registration/cReg/include/sirf/Reg/NiftiImageData.h @@ -429,7 +429,7 @@ class NiftiImageData : public ImageData protected: - enum NiftiImageDataType { _general, _3D, _3DTensor, _3DDisp, _3DDef}; + enum NiftiImageDataType { _general, _3D, _3DTensor, _3DDisp, _3DDef, _3DBSpl}; enum MathsType { add, sub, mul }; diff --git a/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DBSpline.h b/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DBSpline.h new file mode 100644 index 000000000..46f71a128 --- /dev/null +++ b/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DBSpline.h @@ -0,0 +1,118 @@ +/* +SyneRBI Synergistic Image Reconstruction Framework (SIRF) +Copyright 2020 University College London + +This is software developed for the Collaborative Computational +Project in Synergistic Reconstruction for Biomedical Imaging (formerly CCP PETMR) +(http://www.ccpsynerbi.ac.uk/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +/*! +\file +\ingroup Registration +\brief Class for b-spline control point grid SIRF image data. + +\author Richard Brown +\author SyneRBI +*/ + +#pragma once + +#include "sirf/Reg/NiftiImageData3DTensor.h" +#include "sirf/Reg/NonRigidTransformation.h" + +namespace sirf { + +/*! +\ingroup Registration +\brief Class for b-spline control point grid SIRF image data. + +\author Richard Brown +\author SyneRBI +*/ +template +class NiftiImageData3DBSpline : public NiftiImageData3DTensor, public NonRigidTransformation +{ +public: + /// Constructor + NiftiImageData3DBSpline() {} + + /// Filename constructor + NiftiImageData3DBSpline(const std::string &filename) + : NiftiImageData3DTensor(filename) { this->check_dimensions(this->_3DBSpl); } + + /// Nifti constructor + NiftiImageData3DBSpline(const nifti_image &image_nifti) + : NiftiImageData3DTensor(image_nifti) { this->check_dimensions(this->_3DBSpl); } + + /// Construct from general tensor + NiftiImageData3DBSpline(const NiftiImageData& tensor) + : NiftiImageData3DTensor(tensor) { this->check_dimensions(this->_3DBSpl); } + + /// Construct from array + template + NiftiImageData3DBSpline(const inputType * const data, const VoxelisedGeometricalInfo3D &geom) + : NiftiImageData3DTensor(data, geom) { this->_nifti_image->intent_code = NIFTI_INTENT_VECTOR; this->_nifti_image->intent_p1=SPLINE_VEL_GRID; } + + /// Create from 3 individual components + NiftiImageData3DBSpline(const NiftiImageData3D &x, const NiftiImageData3D &y, const NiftiImageData3D &z) + : NiftiImageData3DTensor(x,y,z) { this->check_dimensions(this->_3DBSpl); } + + /// Create from 3D image + void create_from_3D_image(const NiftiImageData &image); + + /// Get as deformation field + virtual NiftiImageData3DDeformation get_as_deformation_field(const NiftiImageData &ref) const; + + /// New data handle + virtual ObjectHandle* new_data_container_handle() const + { + return new ObjectHandle + (std::shared_ptr(new NiftiImageData3DBSpline)); + } + /// Write + virtual void write(const std::string &filename) const { this->NiftiImageData::write(filename); } + /// Clone and return as unique pointer. + std::unique_ptr clone() const + { + return std::unique_ptr(this->clone_impl()); + } + + /*! \brief Get inverse as unique pointer (potentially based on another image). + * + * Why would you want to base it on another image? Well, we might have a deformation + * that takes us from image A to B. We'll probably want the inverse to take us from + * image B back to A. In this case, use get_inverse(A). This is because the the deformation + * field is defined for the reference image. In the second case, A is the reference, + * and B is the floating image.*/ + std::unique_ptr get_inverse(const std::shared_ptr > image_sptr = nullptr, const bool use_vtk=false) const + { + throw std::runtime_error("NiftiImageData3DBSpline::get_inverse: not yet implemented"); + } + + +protected: + /// Clone helper function. Don't use. + virtual NiftiImageData3DBSpline* clone_impl() const + { + return new NiftiImageData3DBSpline(*this); + } + + /// Helper function for get_inverse (NiftyReg). Don't use. + virtual NiftiImageData3DBSpline* get_inverse_impl_nr(const std::shared_ptr > image_sptr = nullptr) const; + + /// Helper function for get_inverse (VTK). Don't use. + virtual NiftiImageData3DBSpline* get_inverse_impl_vtk(const std::shared_ptr > image_sptr = nullptr) const; +}; +} diff --git a/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DDeformation.h b/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DDeformation.h index 33fe4e6ac..999a07605 100644 --- a/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DDeformation.h +++ b/src/Registration/cReg/include/sirf/Reg/NiftiImageData3DDeformation.h @@ -85,7 +85,7 @@ class NiftiImageData3DDeformation : public NiftiImageData3DTensor, pub void create_from_3D_image(const NiftiImageData &image); /// Create from control point grid image - void create_from_cpp(NiftiImageData3DTensor &cpp, const NiftiImageData &ref); + void create_from_cpp(const NiftiImageData3DTensor &cpp, const NiftiImageData &ref); /// Get as deformation field virtual NiftiImageData3DDeformation get_as_deformation_field(const NiftiImageData &ref) const; diff --git a/src/Registration/cReg/include/sirf/Reg/cReg.h b/src/Registration/cReg/include/sirf/Reg/cReg.h index 2172ff5d0..010468f5b 100644 --- a/src/Registration/cReg/include/sirf/Reg/cReg.h +++ b/src/Registration/cReg/include/sirf/Reg/cReg.h @@ -71,6 +71,7 @@ extern "C" { void* cReg_NiftiImageData3DTensor_create_from_3D_image(const void *ptr, const void* obj); void* cReg_NiftiImageData3DTensor_construct_from_3_components(const char* obj, const void *x_ptr, const void *y_ptr, const void *z_ptr); void* cReg_NiftiImageData3DTensor_flip_component(const void *ptr, const int dim); + void* cReg_NiftiImageData3DTensor_get_tensor_component(const void *ptr, const int dim); // NiftiImageData3DDeformation void* cReg_NiftiImageData3DDeformation_compose_single_deformation(const void* im, const char* types, const void* trans_vector_ptr); @@ -80,6 +81,12 @@ extern "C" { // NiftiImageData3DDisplacement void* cReg_NiftiImageData3DDisplacement_create_from_def(const void* def_ptr); + // ControlPointGridToDeformationConverter + void* cReg_CPG2DVF_set_cpg_spacing(const void* converter_ptr, const float spacing_x, const float spacing_y, const float spacing_z); + void* cReg_CPG2DVF_set_ref_im(const void* converter_ptr, const void* ref_im_ptr); + void* cReg_CPG2DVF_forward(const void* converter_ptr, const void* cpg_ptr); + void* cReg_CPG2DVF_backward(const void* converter_ptr, const void* dvf_ptr); + // Registration void* cReg_Registration_process(void* ptr); void* cReg_Registration_get_deformation_displacement_image(const void* ptr, const char *transform_type, const int idx); diff --git a/src/Registration/cReg/tests/test_cReg.cpp b/src/Registration/cReg/tests/test_cReg.cpp index f151a05be..dbda0ff3d 100644 --- a/src/Registration/cReg/tests/test_cReg.cpp +++ b/src/Registration/cReg/tests/test_cReg.cpp @@ -37,6 +37,8 @@ limitations under the License. #include "sirf/Reg/NiftiImageData3DDisplacement.h" #include "sirf/Reg/AffineTransformation.h" #include "sirf/Reg/Quaternion.h" +#include "sirf/Reg/NiftiImageData3DBSpline.h" +#include "sirf/Reg/ControlPointGridToDeformationConverter.h" #include #include #ifdef SIRF_SPM @@ -45,6 +47,54 @@ limitations under the License. using namespace sirf; + +void check_non_zero(const NiftiImageData &im, + const std::string &explanation) +{ + if (std::abs(im.get_min()) < 1e-4f && std::abs(im.get_max()) < 1e-4f) + throw std::runtime_error(explanation + ": contains no non-zeroes"); +} +NiftiImageData3DDeformation +CPG2DVF(const ControlPointGridToDeformationConverter &converter, + const NiftiImageData3DBSpline &cpg) +{ + check_non_zero(cpg, "converter::forward (input)"); + auto dvf = converter.forward(cpg); + check_non_zero(dvf, "converter::forward (output)"); + return dvf; +} +NiftiImageData3DBSpline +DVF2CPG(const ControlPointGridToDeformationConverter &converter, + const NiftiImageData3DDeformation &dvf) +{ + check_non_zero(dvf, "converter::backward (input)"); + auto cpg = converter.backward(dvf); + check_non_zero(cpg, "converter::backward (output)"); + return cpg; +} +NiftiImageData3DDeformation +rand_dvf( + NiftiImageData3DDisplacement &disp, + const float min_disp = -10.f, const float max_disp = 10.f) +{ + for (unsigned i=0; i(rand()) /(static_cast(RAND_MAX/(max_disp-min_disp))); + auto dvf = NiftiImageData3DDeformation(disp); + check_non_zero(dvf, "Rand DVF"); + return dvf; +} +NiftiImageData3DBSpline +rand_cpg( + const ControlPointGridToDeformationConverter &converter, + NiftiImageData3DDisplacement &disp, + const float min_disp = -10.f, const float max_disp = 10.f) +{ + auto dvf = rand_dvf(disp, min_disp, max_disp); + auto cpg = DVF2CPG(converter,dvf); + check_non_zero(cpg, "Rand CPG"); + return cpg; +} + int main(int argc, char* argv[]) { @@ -1183,6 +1233,68 @@ int main(int argc, char* argv[]) std::cout << "// Finished weighted mean test.\n"; std::cout << "//------------------------------------------------------------------------ //\n"; } + { + + std::cout << "// ----------------------------------------------------------------------- //\n"; + std::cout << "// Starting CGP<->DVF test...\n"; + std::cout << "//------------------------------------------------------------------------ //\n"; + + // Test both 2D and 3D cases + for (unsigned is_3d=0; is_3d<2; ++is_3d) { + unsigned int z_size = is_3d ? 32 : 1; + // Generate image + VoxelisedGeometricalInfo3D::Size size({150,125,z_size}); + VoxelisedGeometricalInfo3D::Spacing spacing_dvf({2.f,3.f,5.f}); + VoxelisedGeometricalInfo3D::Offset offset({0.f,0.f,0.f}); + std::array dm_row_1({1.f,0.f,0.f}); + std::array dm_row_2({0.f,1.f,0.f}); + std::array dm_row_3({0.f,0.f,1.f}); + VoxelisedGeometricalInfo3D::DirectionMatrix dm({dm_row_1,dm_row_2, dm_row_3}); + VoxelisedGeometricalInfo3D geom_info(offset, spacing_dvf, size, dm); + // Create displacement, convert to deformation and reference image + NiftiImageData3DDisplacement disp( + *NiftiImageData::create_from_geom_info( + geom_info,true, NREG_TRANS_TYPE::DISP_FIELD)); + NiftiImageData3DDeformation dvf(disp); + NiftiImageData ref = *dvf.get_tensor_component(0); + + // CPG spacing double the dvf spacing + float cpg_spacing[3] = {4.f * spacing_dvf[0], 4.f * spacing_dvf[1], 4.f * spacing_dvf[2]}; + + // set up DVF<->CPG converter + ControlPointGridToDeformationConverter cpg_2_dvf_converter; + cpg_2_dvf_converter.set_cpg_spacing(cpg_spacing); + cpg_2_dvf_converter.set_reference_image(ref); + + // ok, now ready to do adjoint test using: + // | - | / 0.5*(||+||) < epsilon + + for (unsigned i=0; i<10; ++i) { + // Get random CPG and DVF + auto x = rand_cpg(cpg_2_dvf_converter, disp); + auto y = rand_dvf(disp); + + // Convert random CPG to DVF and random DVF to CPG + auto Tx = CPG2DVF(cpg_2_dvf_converter,x); + auto Tsy = DVF2CPG(cpg_2_dvf_converter,y); + + // Get inner products + float x_dot, y_dot; + dynamic_cast(x).dot(Tsy, &x_dot); + dynamic_cast(y).dot(Tx, &y_dot); + + float adjoint_test = std::abs(x_dot - y_dot) / (0.5f * (std::abs(x_dot) + std::abs(y_dot))); + std::cout << "\t| - | / 0.5*(||+||) = " << adjoint_test << "\n"; + float epsilon = 1e-4f; + if (adjoint_test > epsilon) + throw std::runtime_error("adjoint test > " + std::to_string(epsilon)); + } + } + + std::cout << "// ----------------------------------------------------------------------- //\n"; + std::cout << "// Finished CGP<->DVF test.\n"; + std::cout << "//------------------------------------------------------------------------ //\n"; + } { std::cout << "// ----------------------------------------------------------------------- //\n"; diff --git a/src/Registration/pReg/Reg.py b/src/Registration/pReg/Reg.py index 745e4942f..cbf0ab846 100644 --- a/src/Registration/pReg/Reg.py +++ b/src/Registration/pReg/Reg.py @@ -22,7 +22,8 @@ import sys import inspect -from sirf.Utilities import error, check_status, try_calling +from sirf.Utilities import error, check_status, try_calling, \ + assert_validity from sirf import SIRF import pyiutilities as pyiutil import pyreg @@ -327,8 +328,11 @@ def deep_copy(self): image = NiftiImageData3DDeformation() elif self.name == 'NiftiImageData3DDisplacement': image = NiftiImageData3DDisplacement() - try_calling(pyreg.cReg_NiftiImageData_deep_copy( - image.handle, self.handle)) + elif self.name == 'NiftiImageData3DBSpline': + image = NiftiImageData3DBSpline() + else: + raise error("unknown object name: " + self.name) + try_calling(pyreg.cReg_NiftiImageData_deep_copy(image.handle, self.handle)) return image def allocate(self, value=0, **kwargs): @@ -598,6 +602,17 @@ def flip_component(self, dim): self.handle, dim)) check_status(self.handle) + def get_tensor_component(self, dim): + """Get tensor component (i.e., nu=3 -> nu=1).""" + if 0 < dim or dim > 2: + raise AssertionError( + "Tensor component to extract should be between 0 and 2.") + output = NiftiImageData3D() + output.handle = pyreg.cReg_NiftiImageData3DTensor_get_tensor_component( + self.handle, dim) + check_status(output.handle) + return output + class NiftiImageData3DDisplacement(NiftiImageData3DTensor, _Transformation): """Class for 3D displacement nifti image data. @@ -727,6 +742,108 @@ def compose_single_deformation(trans, ref): return z +class NiftiImageData3DBSpline(NiftiImageData3DTensor, _Transformation): + """ + Class for 3D b-spline nifti image data. + """ + + def __init__(self, src1=None, src2=None, src3=None): + self.handle = None + self.name = 'NiftiImageData3DBSpline' + if src1 is None: + self.handle = pyreg.cReg_newObject(self.name) + # filename + elif isinstance(src1, str): + self.handle = pyreg.cReg_objectFromFile(self.name, src1) + # 3 x scalar images + elif isinstance(src1, NiftiImageData3D) and \ + isinstance(src2, NiftiImageData3D) and \ + isinstance(src3, NiftiImageData3D): + self.handle = pyreg.\ + cReg_NiftiImageData3DTensor_construct_from_3_components( + self.name, src1.handle, src2.handle, src3.handle) + else: + raise error('Wrong source in NiftiImageData3DBSpline constructor') + check_status(self.handle) + + def __del__(self): + if self.handle is not None: + pyiutil.deleteDataHandle(self.handle) + + +class ControlPointGridToDeformationConverter(object): + """ + Class for converting from control points grids to deformations and vice + versa. + """ + def __init__(self): + self.handle = None + self.dvf_template = None # only used for testing + self.cpg_template = None # only used for testing + self.name = 'ControlPointGridToDeformationConverter' + self.handle = pyreg.cReg_newObject(self.name) + check_status(self.handle) + + def __del__(self): + if self.handle is not None: + pyiutil.deleteDataHandle(self.handle) + + def set_cpg_spacing(self, spacing): + """Set CPG spacing.""" + if len(spacing) != 3: + raise AssertionError("Spacing should be array of 3 numbers.") + try_calling(pyreg.cReg_CPG2DVF_set_cpg_spacing(self.handle, + float(spacing[0]), float(spacing[1]), float(spacing[2]))) + + def set_reference_image(self, ref_im): + """Set reference image for generating dvfs.""" + assert_validity(ref_im, NiftiImageData3D) + try_calling(pyreg.cReg_CPG2DVF_set_ref_im(self.handle, ref_im.handle)) + + def forward(self, cpg): + """CPG to DVF.""" + assert_validity(cpg, NiftiImageData3DBSpline) + output = NiftiImageData3DDeformation() + output.handle = pyreg.cReg_CPG2DVF_forward(self.handle, cpg.handle) + check_status(output.handle) + return output + + def backward(self, dvf): + """DVF to CPG""" + assert_validity(dvf, NiftiImageData3DDeformation) + output = NiftiImageData3DBSpline() + output.handle = pyreg.cReg_CPG2DVF_backward(self.handle, dvf.handle) + check_status(output.handle) + return output + + def _set_up_for_adjoint_test(self, dvf_template, cpg_template): + """Set template dvf and cpg to be used for testing.""" + assert_validity(dvf_template, NiftiImageData3DDeformation) + assert_validity(cpg_template, NiftiImageData3DBSpline) + self.dvf_template = dvf_template + self.cpg_template = cpg_template + + def direct(self, cpg): + """Alias of forward.""" + return self.forward(cpg) + + def adjoint(self, dvf): + """Alias of backward.""" + return self.backward(dvf) + + def is_linear(self): + """Returns whether the transformation is linear""" + return True + + def domain_geometry(self): + """Get domain geometry (only used for testing).""" + return self.cpg_template + + def range_geometry(self): + """Get range geometry (only used for testing).""" + return self.dvf_template + + class _Registration(ABC): """Abstract base class for registration.""" diff --git a/src/Registration/pReg/tests/test_pReg.py b/src/Registration/pReg/tests/test_pReg.py index 8c954fa03..fc877d7c8 100644 --- a/src/Registration/pReg/tests/test_pReg.py +++ b/src/Registration/pReg/tests/test_pReg.py @@ -23,7 +23,7 @@ import numpy as np import nibabel as nib import sirf.Reg -from pUtilities import * +from sirf.Utilities import is_operator_adjoint # Paths SIRF_PATH = os.environ.get('SIRF_PATH') @@ -1063,6 +1063,38 @@ def try_weighted_mean(na): time.sleep(0.5) +# CGP<->DVF conversion +def try_cgp_dvf_conversion(na): + time.sleep(0.5) + sys.stderr.write('\n# --------------------------------------------------------------------------------- #\n') + sys.stderr.write('# Starting CGP<->DVF test...\n') + sys.stderr.write('# --------------------------------------------------------------------------------- #\n') + time.sleep(0.5) + + dvf = na.get_deformation_field_forward() + spacing = dvf.get_voxel_sizes()[1:4] * 2.0 + + # DVF->CPG with converter + cpg_2_dvf_converter = sirf.Reg.ControlPointGridToDeformationConverter() + cpg_2_dvf_converter.set_cpg_spacing(spacing) + cpg_2_dvf_converter.set_reference_image(dvf.get_tensor_component(0)) + # DVF->CPG + dvf_to_cpg = cpg_2_dvf_converter.backward(dvf) + # DVF->CPG->DVF + _ = cpg_2_dvf_converter.forward(dvf_to_cpg) + + # Check the adjoint is truly the adjoint with: | - | / 0.5*(||+||) < epsilon + cpg_2_dvf_converter._set_up_for_adjoint_test(dvf, dvf_to_cpg) + if not is_operator_adjoint(cpg_2_dvf_converter): + raise AssertionError("ControlPointGridToDeformationConverter::adjoint() failed") + + time.sleep(0.5) + sys.stderr.write('\n# --------------------------------------------------------------------------------- #\n') + sys.stderr.write('# Finished CGP<->DVF test.\n') + sys.stderr.write('# --------------------------------------------------------------------------------- #\n') + time.sleep(0.5) + + # AffineTransformation def try_affinetransformation(na): time.sleep(0.5) @@ -1210,23 +1242,21 @@ def try_quaternion(): def test(): - try_niftiimage() - try_niftiimage3d() - try_niftiimage3dtensor() - try_niftiimage3ddisplacement() - try_niftiimage3ddeformation() + # try_niftiimage() + # try_niftiimage3d() + # try_niftiimage3dtensor() + # try_niftiimage3ddisplacement() + # try_niftiimage3ddeformation() na = try_niftyaladin() - try_niftyf3d() - try_transformations(na) - try_resample(na) - try_niftymomo(na) - try_weighted_mean(na) - try_affinetransformation(na) - try_quaternion() + # try_niftyf3d() + # try_transformations(na) + # try_resample(na) + # try_niftymomo(na) + # try_weighted_mean(na) + try_cgp_dvf_conversion(na) + # try_affinetransformation(na) + # try_quaternion() if __name__ == "__main__": - try: - test() - except: - raise error("Error encountered.") + test()