diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index acba0afe..8e143c8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,145 +1,96 @@ # ANTsPy Contributors Guide -So, you want to contribute to ANTsPy - great! This guide tells you everything -you need to know to set up an efficient workflow and get started writing code. +This guide tells you everything you need to know to set up the development workflow that will allow you to contribute code to antspy. ## Installing ANTsPy Development Environment -To start developing, you need to install a development copy of ANTsPy. -This is straight-forward, just run the following commands: +To start developing, you need to install a development copy of ANTsPy. This follows the same steps as developing any python package. ```bash git clone https://github.com/ANTsX/ANTsPy.git cd ANTsPy -python setup.py develop +python -m pip install -v -e . ``` -The above commands will take ~1 hour, so go ride your bike around the block or -something until it finishes. With the development environment, you get the following: - -- if you change PURE PYTHON code only, just restart the python kernel and import - as normal - the code will be automatically updated. -- if you change any C++ code, just go back to the main directory and run - `python setup.py develop` - only the changed code will be re-compiled, so it will - be quick. +Notice the `-v` flag to have a verbose output so you can follow the build process that can take ~45 minutes. Then there is also the `-e` flag that will build the antspy package in such a way that any changes to the python code will be automatically detected when you restart your python terminal without having to build the package again. +Any changes to C++ code will require you to run that last line (`python -m pip install -v -e .`) again to rebuild the compiled libraries. ## What happens when I install ANTsPy? -When you run `python setup.py install` or `python setup.py develop` to install -ANTsPy from source, the following steps happen. Refer here if you want to change -any part of the install process. - -1. The `setup.py` file is run. The entire build runs from here. -2. We check for a local VTK copy by seeing if the environment variable $VTK_DIR is set. -3. If there is a local VTK build found, move on. Otherwise, clone the VTK repo -and make it by running the script `configure_VTK.sh`. This does NOT run `make install`. -4. We check for a local ITK copy by seeing if the environment variable $ITK_DIR is set. -5. If there is a local ITK build found, move on. Otherwise, clone the ITK repo -and make it by running the script `configure_ITK.sh`. This does NOT run `make install`. -6. After VTK and ITK are built, the `configure_ANTsPy.sh` script is run. -This clones the core ANTs repo and copies all of the source files -into the `ants/lib/` directory. Note that ANTs core is not actually built on its own directly. -Then, a copy of `pybind11` is cloned and put into the `ants/lib/` directory - -which is how we can actually wrap C++ code in Python. Finally, the example data/images -which ship with ANTsPy (in the `data/` directory) are moved to a folder in the user's -home directory (`~/.antspy/`). -4. Then, the ANTsPy library is built by running the cmake command in the `ants/lib/` -directory and referring to the `ants/lib/CMakeLists.txt` file. This builds all of the -shared object libraries. -5. Finally, the library is installed as any other python package. To see what happens -there (depends on whether `develop` or `install` was run), refer to the official -python documentation. - - -## Software Architecture - -This section describes the general architecture of the package. - -- The python code has the following modules: - - `contrib` : contains any experimental code or code which significantly differs from ANTsR. - - `core` : contains all core object class definitions and all IO related to these objects. - - `learn` : contains any code related to Statistical Learning, such as `sparse_decom2`. - - `lib` : contains any C++ wrapping code. Any file which has code which does not - directly wrap ANTs functions are prefaced with "LOCAL_". - Any code which directly wraps ANTs functions are prefaced with "WRAP_". - - `registration` : contains all code related to image registration. - - `segmentation` : contains all code related to image segmentation. - - `utils` : contains all code which perform utility functions or functions which are - not easily categorized elsewhere. - - `viz` : contains any visualization code. - - -## How do I wrap an ANTs core function? - -Wrapping an ANTs function is ridiculously easy since pybind11 implicitly casts between -python and C++ standard types, allowing you to directly interface with C++ code. -Here's an example: - -Say we want to wrap the `Atropos` function. We create the following file called -`WRAP_Atropos.cxx` in the `ants/lib/` directory: +When you run `python -m pip install .` or `python -m pip install -e .` to install antspy from source, the CMakeLists.txt file is run. Refer there if you want to change any part of the install process. Briefly, it performs the following steps: + +1. ITK is cloned and built from the `scripts/configure_ITK.sh` file. +2. ANTs is cloned from `scripts/configure_ANTs.sh` +3. The C++ files from the `src` directory are used to build the antspy library files +4. The antspy python package is built as normal + +## Wrapping core ANTs functions + +Wrapping an ANTs function is easy since pybind11 implicitly casts between python and C++ standard types, allowing you to directly interface with C++ code. Here's an example: + +Say we want to wrap the `Atropos` function. We create the following file called +`WRAP_Atropos.cxx` in the `src/` directory: ```cpp -#include -#include +#include +#include +#include #include "antscore/Atropos.h" -namespace py = pybind11; +namespace nb = nanobind; +using namespace nb::literals; int Atropos( std::vector instring ) { return ants::Atropos(instring, NULL); } -PYBIND11_MODULE(Atropos, m) +void wrap_Atropos(nb::module_ &m) { m.def("Atropos", &Atropos); } ``` -The `WRAP_function.cxx` file is the SAME for every ANTs function - simply exchange the -word "Atropos" with whatever the function name is. +The `WRAP_function.cxx` file is the SAME for every ANTs function - simply exchange the word "Atropos" with whatever the function name is. -Next, we add the following two lines in the `ants/lib/CMakeLists.txt` file: +Next, we add the following two lines to the top of the `src/main.cpp` file: -``` -pybind11_add_module(Atropos antscore/Atropos.cxx WRAP_Atropos.cxx) -target_link_libraries(Atropos PRIVATE ${ITK_LIBRARIES} antsUtilities) +```c++ +#include "WRAP_Atropos.cxx" +void wrap_Atropos(nb::module_ &); ``` -Again, these two lines are the same for every function with only the function named changed. +Then, we add the following line inside the `NB_MODULE(lib, m) { ... }` call in the same file: -Finally, we add the following line in the `ants/lib/__init__.py` file: - -``` -from .Atropos import * +```c++ +wrap_Atropos(m); ``` -That's it! Now you have access to the Atropos function by calling `ants.lib.Atropos(...)`. -Of course, we always add some python wrapper code instead of directly calling the library -function. The general workflow for wrapping a library calls involves the following steps: +Rebuilding the package should make the `lib.Atropos` function available for you. However, remember that lib functions should never be called directly by users, so you have to add the python wrapper code to process the arguments and call this underlying lib function. + +The general workflow for wrapping a library calls involves the following steps: - write a wrapper python function (e.g. `def atropos(...)`) - build up a list or dictionary of string argument names as in ANTs - pass those raw arguments through the function `utils._int_antsProcessArguments(args)` - pass those processed arguments into the library call (e.g. `lib.Atropos(processed_args)`). -## How do I go from an ANTsImage to an ITK Image? +## Writing custom code for antspy -First off, the python-based ANTsImage class holds a pointer to the underlying -ITK object in the property `self.pointer`. +You can write any kind of custom code to process antspy images. The underlying image is ITK so the AntsImage class holds a pointer to the underlying ITK object in the in the property `self.pointer`. -To go from a C++ ANTsImage class to an ITK image, use the following code: +To go from a C++ ANTsImage class to an ITK image, pass in an `AntsImage` argument (`image.pointer` in python) and call `.ptr` to access the ITK image. ```cpp -#include "LOCAL_antsImage.h" +#include "antsImage.h" template -ImageType::Pointer getITKImage( py::capsule antsImage ) +ImageType::Pointer getITKImage( AntsImage antsImage ) { typedef typename ImageType::Pointer ImagePointerType; - ImagePointerType itkImage = as( antsImage ); + ImagePointerType itkImage = antsImage.ptr; return itkImage } ``` @@ -150,26 +101,30 @@ contains the ITK image pointer. ### Example 1 - getOrigin -Let's do a full example where we get the origin of a Python ANTsImage from the -underlying ITK image. +Let's do a full example where we get the origin of a Python AntsImage from the underlying ITK image. -We would create the following file `ants/lib/LOCAL_getOrigin.cxx`: +We would create the following file `src/getOrigin.cxx`: ```cpp -#include // needed for wrapping -#include // needed for casting from std::vector to python::list +#include +#include +#include +#include +#include +#include +#include #include "itkImage.h" // any ITK or other includes -#include "LOCAL_antsImage.h" // needed for casting to & from ANTsImage<->ITKImage +#include "antsImage.h" // needed for casting to & from ANTsImage<->ITKImage // all functions accepted ANTsImage types must be templated template -std::vector getOrigin( py::capsule antsImage ) +std::vector getOrigin( AntsImage antsImage ) { // cast to ITK image as shown above typedef typename ImageType::Pointer ImagePointerType; - ImagePointerType itkImage = as( antsImage ); + ImagePointerType itkImage = antsImage.ptr; // do everything else as normal with ITK Imaeg typename ImageType::PointType origin = image->GetOrigin(); @@ -185,89 +140,72 @@ std::vector getOrigin( py::capsule antsImage ) } // wrap function above with all possible types -PYBIND11_MODULE(getOrigin, m) +void getOrigin(nb::module_ &m) { - m.def("getOriginUC2", &getOrigin>); - m.def("getOriginUC3", &getOrigin>); - m.def("getOriginF2", &getOrigin>); - m.def("getOriginF3", &getOrigin>); + m.def("getOrigin", &getOrigin>); + m.def("getOrigin", &getOrigin>); + m.def("getOrigin", &getOrigin>); + m.def("getOrigin", &getOrigin>); } ``` -Now we add the following lines in `ants/lib/CMakeLists.txt` : +Now we add the following lines in `src/main.cpp` : -``` -pybind11_add_module(getOrigin LOCAL_getOrigin.cxx) -target_link_libraries(getOrigin PRIVATE ${ITK_LIBRARIES}) +```c++ +#include "getOrigin.cxx" +void getOrigin(nb::module_ &); ``` -And add the following line in `ants/lib/__init__.py`: +And add the following line to the same file inside the `NB_MODULE(lib, m) { ... }` call: ``` -from .getOrigin import * +getOrigin(m); ``` -Finally, we create a wrapper function in python file `get_origin.py`: +Finally, we create a wrapper function in python file `get_origin.py`. Notice that the `lib.getOrigin` is overloaded so that it can automatically infer the ITK ImageType. ```python from ants import lib # use relative import e.g. "from .. import lib" in package code -# dictionary for storing template-wrapped library function -_get_origin_dict = { - 2: { - 'unsigned char': 'getOriginUC2', - 'float': 'getOriginF2' - }, - 3: { - 'unsigned char': 'getOriginUC3', - 'float': 'getOriginF3' - } -} - def get_origin(img): idim = img.dimension ptype = img.pixeltype - # get the template function corresponding to image dimension and pixeltype - _get_origin_fn = lib.__dict__[_get_origin_dict[idim][ptype]] - # call function - NOTE how we pass in `img.pointer`, not `img` directly - origin = _get_origin_fn(img.pointer) + origin = lib.getOrigin(img.pointer) # return as tuple return tuple(origin) ``` -And that's it! For more other return types, you should refer to the pybind11 docs. +And that's it! For more other return types, you should refer to the nanobind docs. -## How do I go from an ITK Image to an ANTsImage? +## Wrapping an ITK image for antspy -In the previous section, we saw how easy it is to cast from ANTsImage to ITK Image -using the `as( antsImage )` function. The same is true for going the other way. +In the previous section, we saw how easy it is to cast from AntsImage to ITK Image by calling `antsImage.ptr`. It is also easy to go the other way and wrap an ITK image as an AntsImage. -Example: +Here is an example: ```cpp #include "itkImage.h" // include any other ITK imports as normal -#include "LOCAL_antsImage.h" +#include "antsImage.h" template -py::capsule someFunction( py::capsule antsImage ) +AntsImage someFunction( AntsImage antsImage ) { // cast from ANTsImage to ITK Image typedef typename ImageType::Pointer ImagePointerType; - ImagePointerType itkImage = as( antsImage ); - + ImagePointerType itkImage = antsImage.ptr; + // do some stuff on ITK image // ... - // cast from ITK Image to ANTsImage - py::capsule newAntsImage; - newAntsImage = wrap( itkImage ); + // wrap ITK Image in AntsImage struct + AntsImage outImage = { itkImage }; - return newAntsImage; + return outImage; } ``` @@ -275,159 +213,37 @@ If the function doesnt return the same image type, you need two template argumen ```cpp #include "itkImage.h" // include any other ITK imports as normal -#include "LOCAL_antsImage.h" +#include "antsImage.h" template -py::capsule someFunction( py::capsule antsImage ) +AntsImage someFunction( AntsImage antsImage ) { - // cast from ANTsImage to ITK Image of InImageType - typedef typename ImageType::Pointer ImagePointerType; - ImagePointerType itkImage = as( antsImage ); - + // cast from ANTsImage to ITK Image + typedef typename InImageType::Pointer ImagePointerType; + ImagePointerType itkImage = antsImage.ptr; + // do some stuff on ITK image // ... - // cast from ITK Image to ANTsImage of OutImageType - py::capsule newAntsImage; - newAntsImage = wrap( itkImage ); - - return newAntsImage; -} -``` - -So you see we use `as( antsImage )` to go from ANTsImage to ITK Image -and `wrap( itkImagePointer)` to go from ITK Image to ANTsImage - -### Example 2 - Cloning an Image - -In this example, I will show how to clone an ANTsImage directly in ITK. -First, we create the C++ file `ants/lib/LOCAL_antsImageClone.cxx`: - -```cpp -#include -#include - -#include "itkImage.h" -#include "itkImageFileWriter.h" - -#include "LOCAL_antsImage.h" - -namespace py = pybind11; - -template -py::capsule antsImageClone( py::capsule antsImage ) -{ - // --------------------------------------------- - // cast from ANTsImage to ITK Image - typedef typename InImageType::Pointer InImagePointerType; - InImagePointerType in_image = as< InImageType >( antsImage ); - - // --------------------------------------------- - // do stuff on ITK Image ... - typename OutImageType::Pointer out_image = OutImageType::New() ; - out_image->SetRegions( in_image->GetLargestPossibleRegion() ) ; - out_image->SetSpacing( in_image->GetSpacing() ) ; - out_image->SetOrigin( in_image->GetOrigin() ) ; - out_image->SetDirection( in_image->GetDirection() ); - //out_image->CopyInformation( in_image ); - out_image->AllocateInitialized() ; - - itk::ImageRegionConstIterator< InImageType > in_iterator( in_image , in_image->GetLargestPossibleRegion() ) ; - itk::ImageRegionIterator< OutImageType > out_iterator( out_image , out_image->GetLargestPossibleRegion() ) ; - for( in_iterator.GoToBegin() , out_iterator.GoToBegin() ; !in_iterator.IsAtEnd() ; ++in_iterator , ++out_iterator ) - { - out_iterator.Set( static_cast< typename OutImageType::PixelType >( in_iterator.Get() ) ) ; - } - // --------------------------------------------- - // cast from ITK image to ANTsImage and return that image - return wrap< OutImageType >( out_image ); -} - -// wrap this function for possible image types -// this is annoying, but saves on time and eliminates chances for bugs in code -// NOTE: you need the random `m` there. -PYBIND11_MODULE(imageCloneModule, m) -{ - m.def("antsImageCloneUC2UC2", antsImageClone, itk::Image>); - m.def("antsImageCloneUI2UI2", antsImageClone, itk::Image>); - m.def("antsImageCloneF2F2", antsImageClone, itk::Image>); - // ... and so on ... -} -``` - -Note above how we have to explicilty wrap every combination of image input and output types. This -is annoying but ultimately saves on runtime and most importantly reduces possibilities for bugs -by eliminates the need for massive IF-ELSE statements determining the type of the image -in the c++ code. - -Next, we add the build lines to `ants/lib/CMakeLists.txt`: - -``` -pybind11_add_module(imageCloneModule LOCAL_antsImageClone.cxx) -target_link_libraries(imageCloneModule PRIVATE ${ITK_LIBRARIES}) -``` - -And add the following line in `ants/lib/__init__.py`: - -``` -from .imageCloneModule import * -``` + // wrap ITK Image in AntsImage struct + AntsImage outImage = { itkImage }; -Finally, we add the Python wrapping function: - -```python -from ants import lib -from ants.core import ants_image as iio - -_image_clone_dict = { - 2: { - 'unsigned char': { - 'unsigned char': 'antsImageCloneUCUC2' - }, - 'unsigned int': { - 'unsigned int': 'antsImageCloneUI2UI2' - }, - 'float': { - 'float': 'antsImageCloneF2F2' - } - } + return outImage; } - -def image_clone(img1, pixeltype=None): - idim = img1.dimension - ptype1 = img1.pixeltype - - if pixeltype is None: - ptype2 = img1.pixeltype - else: - ptype2 = pixeltype - - _image_clone_fn = lib.__dict__[_image_clone_dict[idim][ptype1][ptype2]] - - # this function returns a C++ ANTsImage object - cloned_img_ptr = _image_clone_fn(img1.pointer) - - # we need to wrap the C++ ANTsImage object into a Python ANTsImage object - cloned_ants_image = iio.ANTsImage(pixeltype=ptype1, dimension=idim, - components=img1.components, pointer=cloned_img_ptr) - return cloned_ants_image ``` -And that's it! - - ## Running Tests All tests can be executed by running the following command from the main directory: ```bash -./tests/run_tests.sh +sh ./tests/run_tests.sh ``` Similarly, code coverage can be calculated by adding a flag to the above command: ```bash -./tests/run_tests.sh -c +sh ./tests/run_tests.sh -c ``` This will create an html folder in the `tests` directory with detailed coverage information. @@ -457,24 +273,20 @@ class TestMyFunction(unittest.TestCase): # add whatever code here to tear down after all the tests pass - def test_function1(self): + def test_function1(self): # add whatever here - # use self.assertTrue(...), self.assertEqual(...), + # use self.assertTrue(...), self.assertEqual(...), # nptest.assert_close(...), etc for tests pass def test_function2(self): # add whatever here - # use self.assertTrue(...), self.assertEqual(...), + # use self.assertTrue(...), self.assertEqual(...), # nptest.assert_close(...), etc for tests pass ... ``` + Tests are actually carried out through assertion statements such as `self.assertTrue(...)` and `self.assertEqual(...)`. - - - - -