├── .github └── workflows │ ├── cmake.yml │ └── cpplint.yml ├── .gitignore ├── CMakeLists.txt ├── CPPLINT.cfg ├── README.md ├── bindings └── python │ ├── CMakeLists.txt │ └── py-fast-marching-method.cpp ├── examples ├── cpp │ ├── CMakeLists.txt │ └── minimal_example.cpp └── python │ └── py-fast-marching-minimal-example.py ├── img ├── fmm_readme.pdf ├── fmm_readme_concept.png ├── fmm_readme_dilation_bands.png ├── fmm_readme_eikonal_solvers.png ├── fmm_readme_input_values.png ├── fmm_readme_inside_outside.png └── fmm_readme_point_source_error.png ├── include └── thinks │ └── fast_marching_method │ └── fast_marching_method.hpp └── tests ├── CMakeLists.txt ├── eikonal_solvers_test.cpp ├── main.cpp ├── py-bindings-test.py ├── signed_arrival_time_test.cpp └── util.hpp /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 7 | BUILD_TYPE: RelWithDebInfo 8 | 9 | jobs: 10 | build: 11 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 12 | # You can convert this to a matrix build if you need cross-platform coverage. 13 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Install optional tools (clang-tidy) 20 | run: sudo apt-get install -y clang-tidy 21 | 22 | - name: Setup Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: '3.10' 26 | 27 | - name: Install Python dependencies 28 | run: pip install numpy scipy 29 | 30 | - name: Configure CMake 31 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 32 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 33 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} . 34 | 35 | - name: Build 36 | # Build your program with the given configuration 37 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 38 | 39 | - name: Test 40 | working-directory: ${{github.workspace}}/build 41 | # Execute tests defined by the CMake configuration. 42 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 43 | run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/cpplint.yml: -------------------------------------------------------------------------------- 1 | # GitHub Action to run cpplint recursively on all pushes and pull requests 2 | # https://github.com/cpplint/GitHub-Action-for-cpplint 3 | 4 | name: cpplint 5 | on: [push, pull_request] 6 | jobs: 7 | cpplint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.x 14 | - run: pip install cpplint 15 | - run: cpplint --recursive . 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Compiled Dynamic libraries 12 | *.so 13 | *.dylib 14 | *.dll 15 | 16 | # Fortran module files 17 | *.mod 18 | 19 | # Compiled Static libraries 20 | *.lai 21 | *.la 22 | *.a 23 | *.lib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | *.user 30 | test/CMakeLists.txt.user 31 | *.suo 32 | *.sqlite 33 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | project(fast-marching-method) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED True) 6 | 7 | find_program(CLANG_TIDY_EXE NAMES clang-tidy PATHS /opt/homebrew/opt/llvm/bin/) 8 | if(NOT CLANG_TIDY_EXE) 9 | message(STATUS "clang-tidy not found. Skipping corresponding checks.") 10 | else() 11 | set(CMAKE_CXX_CLANG_TIDY 12 | ${CLANG_TIDY_EXE}; 13 | -header-filter=.*fast_marching_method*; 14 | -checks=-*,portability-*,bugprone-*,readability-,clang-analyzer-*,perforance-*;) 15 | message(STATUS "Found clang-tidy: ${CLANG_TIDY_EXE}.") 16 | endif() 17 | 18 | # Default to release build 19 | if(NOT CMAKE_BUILD_TYPE) 20 | #set(CMAKE_BUILD_TYPE Release) 21 | set(CMAKE_BUILD_TYPE RelWithDebInfo) 22 | #set(CMAKE_BUILD_TYPE Debug) 23 | endif() 24 | message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") 25 | 26 | if(MSVC) 27 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 28 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") 29 | set(CompilerFlags 30 | CMAKE_CXX_FLAGS 31 | CMAKE_CXX_FLAGS_DEBUG 32 | CMAKE_CXX_FLAGS_RELEASE 33 | CMAKE_C_FLAGS 34 | CMAKE_C_FLAGS_DEBUG 35 | CMAKE_C_FLAGS_RELEASE) 36 | foreach(CompilerFlag ${CompilerFlags}) 37 | string(REPLACE "/MD" "/MT" ${CompilerFlag} "${${CompilerFlag}}") 38 | endforeach() 39 | message(STATUS "CXX flags (release): ${CMAKE_CXX_FLAGS_RELEASE}") 40 | message(STATUS "CXX flags (debug): ${CMAKE_CXX_FLAGS_DEBUG}") 41 | endif() 42 | 43 | # Warnings and other compiler flags 44 | if(MSVC) 45 | add_compile_options(/W4) 46 | string( REPLACE "/DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") 47 | else() 48 | add_compile_options(-Wall -Wextra -Wpedantic -Wno-gnu-zero-variadic-macro-arguments) 49 | string( REPLACE "-DNDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") 50 | endif() 51 | 52 | enable_testing() 53 | add_subdirectory(tests) 54 | add_subdirectory(examples/cpp) 55 | add_subdirectory(bindings/python) -------------------------------------------------------------------------------- /CPPLINT.cfg: -------------------------------------------------------------------------------- 1 | # Disable the unapproved c++11 header checks 2 | filter=-build/c++11 3 | 4 | # Allow for non-const reference parameters 5 | filter=-runtime/references 6 | 7 | # Disable "Missing username in TODO" warning 8 | filter=-readability/todo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Fast Marching Method 2 | The *Fast Marching Method* (FMM), in its simplest form, can be used to compute the arrival times at grid cells for a monotonously expanding interface. One application of this method is to set the speed of the interface to one which enables computation of distance fields, where the closest distance to the interface is assigned to every cell in a grid. This repository contains an implementation of the FMM in arbitrary dimensions (actually two or more), although typical usage is most likely going to be 2D or 3D. The code is designed to be simple to incorporate into existing projects, and robustness has been prioritized over speed optimizations. All code in this repository is released under the [MIT license](https://en.wikipedia.org/wiki/MIT_License). If you have any comments or suggestions please feel free to make a pull request. 3 | 4 | This note is divided into two major sections. First, we provide examples on how to use the code along with other practical details such as running the accompanying tests. Thereafter, we describe the technical choices that were made in the implementation with references to relevant literature. 5 | 6 | ## Usage 7 | This section describes how to use the FMM implementation presented in this repository. First, we provide some examples on how to call these methods, together with some discussion related to valid inputs. Thereafter, we give instructions on how to run the accompanying tests. Note that there are also a lot of examples within the test code itself, which can be found in the [test folder](https://github.com/thinks/fast-marching-method/tree/master/test). 8 | 9 | The FMM implementation in this repository is contained in a single [header file](https://github.com/thinks/fast-marching-method/blob/master/include/thinks/fast_marching_method/fast_marching_method.hpp). This makes it very easy to add as a dependency to existing projects. Further, the code has no external dependencies other than the standard C++ libraries. All interfaces use standard types, such as `std::array` and `std::vector`. The code contains a fairly large number of `assert` statements, making it easier to debug when the `NDEBUG` preprocessor variable is not defined. However, the code runs very slowly in debug mode so input data set sizes may need to be adjusted accordingly. 10 | 11 | ### Methods 12 | Most of the functions in the single [header file](https://github.com/thinks/fast-marching-method/blob/master/include/thinks/fast_marching_method/fast_marching_method.hpp) (which is all that needs to be included) are in `namespace detail` and should not be called directly. Instead, there is a single entry point provided by the `ArrivalTime` function. As the name suggests this function computes arrival times at grid cells. A conceptual example illustrates what is meant by this. 13 | 14 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_concept.png "Conceptual example") 15 | 16 | In the figure above, the green circle (_left_) was used as input to compute arrival times on a grid (_right_). Locations outside the circle have positive arrival times (or distances depending on interpretation), here shown in red. Similarly, locations inside the circle have negative arrival times, here shown in blue. The intensity gives the distance to the interface (i.e. circle boundary). This is why cells close to the interface appear black, since the red or blue component is small there. Next, we give an example demonstrating how to write code to generate an image similar to the one shown above. 17 | 18 | The input to the `ArrivalTime` function is given as grid cells with known distances (or arrival times depending on interpretation). The following code snippet (full working code [here](https://github.com/thinks/fast-marching-method/blob/master/test/minimal_example.cpp) ) computes a low resolution version of the image shown above. 19 | 20 | ```cpp 21 | namespace fmm = thinks::fast_marching_method; 22 | 23 | // Select points close to a true circle 24 | auto circle_boundary_indices = vector>{ 25 | {{5, 3}}, {{6, 3}}, {{7, 3}}, {{8, 3}}, {{9, 3}}, {{10, 3}}, {{4, 4}}, 26 | {{5, 4}}, {{10, 4}}, {{11, 4}}, {{3, 5}}, {{4, 5}}, {{11, 5}}, {{12, 5}}, 27 | {{3, 6}}, {{12, 6}}, {{3, 7}}, {{12, 7}}, {{3, 8}}, {{12, 8}}, {{3, 9}}, 28 | {{12, 9}}, {{3, 10}}, {{4, 10}}, {{11, 10}}, {{12, 10}}, {{4, 11}}, 29 | {{5, 11}}, {{10, 11}}, {{11, 11}}, {{5, 12}}, {{6, 12}}, {{7, 12}}, 30 | {{8, 12}}, {{9, 12}}, {{10, 12}}, 31 | }; 32 | 33 | // Specify distances of such points to the true circle 34 | auto circle_boundary_distances = vector{ 35 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 0.0417385f, 36 | 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 0.0417385f, -0.0111773f, 37 | -0.0111773f, 0.0417385f, 0.0164635f, 0.0164635f, 0.0029808f, 0.0029808f, 38 | 0.0029808f, 0.0029808f, 0.0164635f, 0.0164635f, 0.0417385f, -0.0111773f, 39 | -0.0111773f, 0.0417385f, 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 40 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 0.0417385f 41 | }; 42 | 43 | auto grid_size = array{{16, 16}}; 44 | auto grid_spacing = array{{1.f/16, 1.f/16}}; 45 | auto uniform_speed = 1.f; 46 | 47 | auto arrival_times = fmm::SignedArrivalTime( 48 | grid_size, 49 | circle_boundary_indices, 50 | circle_boundary_distances, 51 | fmm::UniformSpeedEikonalSolver(grid_spacing, uniform_speed)); 52 | ``` 53 | 54 | First, we define our input, the cell coordinates for which we have known distances. These are stored in two separate lists, one for the coordinates of the cells (`circle_boundary_indices`) and one for the corresponding distances (`circle_boundary_distances`). Normally, these values would of course not be hard-coded, but rather generated by some function. Thereafter, we specify the size (`grid_size`) and dimensions (`grid_spacing`) of the grid. Here the grid dimensions are set so that the domain is [0, 1] in each dimension. In order to be able to interpret the arrival times as euclidean distance, a uniform speed of one is set for the entire grid (`uniform_speed`). The speed is passed to an Eikonal solver that determines the method used to propagate distances. Eikonal solvers will be discussed in further detail in the following section. The resulting image is shown below. 55 | 56 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_input_values.png "Code example") 57 | 58 | Boundary condition cells with known distances are shaded darker grey in the left image. Known input values may be interpreted as radii that intersect the input shape. We note that negative distances are used when the cell center is inside the circle. In the next section we discuss the use of Eikonal solvers, which allow easy customization of the algorithm while re-using the basic ideas. 59 | 60 | ### Eikonal Solvers 61 | The basic idea of the FMM algorithm is to propagate given information at known locations to other locations in a numerically reasonable way. Now, the way that the FMM algorithm handles this is by assuming that information close to known locations is more reliable than information further away, which is why it is said to be a propagating method. Even though the basic propagation scheme remains the same there is still flexibility when it comes to the details of how to compute information at new locations. This is what Eikonal solvers are used for in this implementation. 62 | 63 | Up to this point we have assumed the speed of the propagating interface to be uniformly one, which is convenient since it allows us to intuitively interpret arrival times as distance. However, the FMM allows arbitrary positive speeds and does not require the speed to be the same for each cell. Also, since propagating information requires partial derivates it is possible to achieve better accuracy using higher order discretization schemes. This leads to the following types of basic Eikonal solvers: 64 | * `UniformSpeedEikonalSolver` 65 | * `HighAccuracyUniformSpeedEikonalSolver` 66 | * `VaryingSpeedEikonalSolver` 67 | * `HighAccuracyVaryingSpeedEikonalSolver` 68 | * `DistanceEikonalSolver` (from Bridson book, REF!!!) 69 | 70 | These types are provided in the same [header file](https://github.com/thinks/fast-marching-method/blob/master/include/thinks/fast_marching_method/fast_marching_method.hpp) as the rest of the code. It is of course possible to extend these further with user-defined solvers. Example usages of the different solver types are shown in the image below. 71 | 72 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_eikonal_solvers.png "Eikonal solvers") 73 | 74 | 75 | ### Input Validation 76 | 77 | 78 | ### Tests 79 | In order to run the tests you need to have [CMake](https://cmake.org/) installed. The tests are implemented in the [Google Test](https://github.com/google/googletest) framework, which is included as part of this repository. 80 | 81 | Running the tests is simple. In a terminal do the following: 82 | 83 | ```bash 84 | $ cd d: 85 | $ git clone git@github.com:/thinks/fast-marching-method.git D:/fmm 86 | $ mkdir fmm-build 87 | $ cd fmm-build 88 | $ cmake ../fmm/test -DCMAKE_BUILD_TYPE=Release 89 | $ cmake --build . 90 | $ ctest 91 | ``` 92 | 93 | In order, the following is being done: 94 | * Clone the source code to a directory `D:/fmm`. 95 | * Create an out-of-source build directory `fmm-build`. 96 | * Create the default project files for your machine in the build directory (change `Release` to `Debug` for a debug build). 97 | * Builds the tests. 98 | * Runs the tests. 99 | 100 | If the tests pass you should see something like: 101 | 102 | ``` 103 | Test project D:/fmm-build 104 | Start 1: fast-marching-method-test 105 | 1/1 Test #1: fast-marching-method-test ........ Passed 0.33 sec 106 | 107 | 100% tests passed, 0 tests failed out of 1 108 | 109 | Total test time (real) = 0.35 sec 110 | 111 | ``` 112 | 113 | For more detailed test output you can run the test executable directly: 114 | 115 | ``` 116 | $ D:/fmm-build/fast-marching-method-test.exe 117 | ``` 118 | 119 | ## Technical Details 120 | This section describes our FMM implementation from a more technical perspective. Relevant implementation details are discussed and references are provided when applicable. A high degree of familiarity with the FMM is assumed here. For those who wish learn more about the basics of the FMM we recommend **[3]** as a good starting point. Finally, possible directions for future work together with references for additional material on the FMM are given. 121 | 122 | ### Simplified Fast Marching Method 123 | A key part of the FMM algorithm is the specific order in which cells are visited during arrival time propagation. Achieving this specific order requires maintaining a priority queue of tentative arrival times for cells during propagation. Since the tentative arrival time at a cell can be re-evaluated several times a cell may change position in the priority queue. The operation of finding and moving a cell in the priority queue is relatively expensive and, moreover, requires a specialized data structure. In **[4]**, Jones et al. observe that increasing the tentative arrival time at a cell during recomputation is detrimental to the final result. They therefore propose a somewhat simpler propagation scheme that allows cells to appear multiple times in the priority queue. This alleviates the need for finding and moving cells in the queue at the small cost of having to check if a cell has already been finalized when it appears at the front of the queue. Pseudo-code for the simplified FMM is as follows **[4]**: 124 | 125 | ``` 126 | Extract cell with smallest tentative arrival time from queue 127 | If cell is not finalized 128 | Finalize the cell 129 | Recompute arrival times for unfinalized neighbor cells 130 | Insert neighbor cells in queue 131 | ``` 132 | 133 | Note that this scheme does not require updating existing elements in the priority queue. Instead multiple tentative arrival times may be added for the same cell. Only the smallest tentative arrival time determines when a cell is finalized, effectively ignoring larger tentative arrival times for that cell. This means that a standard priority queue may be used instead of some specialized structure that needs to accommodate updates of existing elements. 134 | 135 | ### High Accuracy Fast Marching Method 136 | At the core of the FMM is the discrete approximation of derivatives used when solving the [eikonal equation](https://en.wikipedia.org/wiki/Eikonal_equation). Commonly, first order approximations are used, but it is sometimes possible to achieve better results using higher order discretization schemes. In **[6]** Sethian describes a second order discretization scheme referred to as *High Accuracy FMM*. Using higher order discretization has the potential to make the FMM significanly more accurate than its first order counterpart. A simple example illustrates this. 137 | 138 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_point_source_error.png "Point source error") 139 | 140 | Starting from a point source at the middle of the image (where arrival time is zero), arrival times were computed using both regular (first order) FMM and high accuracy FMM. A uniform speed of one over the entire domain was used here. Visually it is quite difficult to tell the difference other than that the dark area around the point source appears slightly less axis-aligned in the high accuracy version. However, the differences become apparent when we visualize errors. We note that errors for regular FMM are overall relatively high except along the grid axes. The pattern is similar for high accuracy FMM, but the areas of lower error extend at a wider angle from the grid axes. Since the high accuracy discretization scheme uses a larger cell neighborhood it is important that the initial boundary cells are able to provide such information. Otherwise first order derivatives will be used for the initial neighbors and those inaccuracies will propagate to the rest of the domain. In the example above boundary values were given for the 8-neighborhood of the cell containing the point source when computing the high accuracy arrival times. While such dilated boundaries are trivial to provide in some scenarios it may not be in others. This is a problem that needs to be solved in the steps that generate input to the FMM procedure and is not directly related to the work herein. 141 | 142 | In **[3]** Baerentzen provides numbers for the max and mean error of his implementation in a scenario similar to the one we use above. His numbers are given for a point source at the center of a 3D grid where max and mean error where computed within a radius of 20 cells. A radial cutoff is used to avoid directional bias. The reported max and mean errors for regular FMM are 1.48 and 0.89 voxel units (i.e. the distance between two cell centers, assuming square cells), respectively. For high accuracy FMM these numbers fall to 0.27 and 0.07 voxel units, respectively. We repeated this experiment for the implementation in this repository and found that our result were extremely close to those reported in **[3]**. In fact, there are unit tests in place to assure that the accuracy does not fall below these levels. 143 | 144 | Finally, we use the elegant formulations provided by Rickett and Fomel in **[2]** for accumulating quadratic coefficients when solving the [eikonal equation](https://en.wikipedia.org/wiki/Eikonal_equation). Their framework enables a simple way of using second order derivatives when the cell neighborhood allows it and otherwise falling back to first order derivatives. Interestingly, the propagation scheme, i.e. the order in which cells are updated, is independent of how we choose to solve the [eikonal equation](https://en.wikipedia.org/wiki/Eikonal_equation). 145 | 146 | ### Morphological Sign Computation 147 | In some applications of the FMM boundary cells represent point sources and only positive travel times are of interest. The field of seismic study (e.g. **[7]**) is a good example of this. In these cases boundary cells form filled shapes and there is no concept of inside. In other words, arrival times are only propagated outward from solid shapes. However, in the level set community it is often desirable to convert shapes from explicit representations, such as triangle meshes, to implicit representations, i.e. distance fields. Computing distance fields using the FMM is easily achieved by settings a uniform speed equal to one on the entire domain, which allows arrival time to be interpreted as distance. It is common to use signed distance fields, where the inside of a shape has negative distance values. As discussed below, a simple trick is required to achieve correct inside distances. 148 | 149 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_inside_outside.png "Sign computation") 150 | 151 | We wish to create a signed distance field from the green circle. Cell values are shown as circles since we can interpret them as radii representing the minimum distance to the interface (points on the green circle). From the definition of distance fields we know that cell circles are tangential to the green circle at one or more locations. The concept of negative distance is only used to distinguish between inside-outside, geometrically the tangential property applies to absolute values. 152 | 153 | For simplicity we consider only a few cells in a single grid row. In the top illustration we note the issue of dual contouring, where inside (dashed circles) and outside cells consider the interface to be at different locations. Dual contouring happens because the FMM algorithm is symmetric in the sense that propagated values move away from the interface. 154 | 155 | In the bottom version we show the correct distance values. As mentioned briefly in **[8]**, the dual contouring issue can be resolved by sign-flipping the boundary values while propagating inward. Note that inside values are positive while propagating and these cells need to be manually sign-flipped after inward propagation finishes. Now, given that we need to distinguish between inward and outward propagation, how do we know if a boundary cell neighbor is on the inside or outside? 156 | 157 | A morphological approach is used to determine the inside and outside areas of the shape described by the given boundary cells. First, we perform a morphological dilation of the boundary cells using the vertex neighborhood, i.e. all cells that share a vertex with a boundary cell are tagged as being part of a dilation band. Next, we perform connected component labelling of the dilation band cells. This results in one or more separate dilation bands. Every shape has an outer dilation band and in the presence of multiple dilation bands, the outer dilation band always has the largest bounding box. Other dilation bands are referred to as inner dilation bands and we know that these cells are on the inside of the shape. The image below illustrates these ideas. 158 | 159 | ![alt text](https://github.com/thinks/fast-marching-method/blob/master/img/fmm_readme_dilation_bands.png "Inside/outside") 160 | 161 | The simplest case is when the given boundary cells form a single connected component. There may be an arbitrary number of dilation bands, but inside and outside can always be determined without ambiguities. Note that the input grid is padded by one cell in each direction while computing the dilation bands. The reasoning behind this is that non-closed shapes should not have insides, which would otherwise happen. In the case of two or more connected boundary components there is the possibility that one of the components is contained by one of the other components. If this is the case there is an ambiguity regarding inside and outside, since the contained component's outside will be considered to be inside by the containing component. Interestingly, the outsides of separate components will at some point interact with each other, while insides will never interact with dilation band cells from any other dilation band. In order to determine if a component is contained within another we flood fill all inside regions and check for the existence of boundary cells. 162 | 163 | 164 | Does not work well in 1D! 165 | 166 | ### Code Design 167 | 168 | [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) 169 | 170 | references for further reading 171 | 172 | ### Future Work 173 | * Termination criteria for narrow band marching. 174 | * Comparison with fast sweeping method and vector distance transforms [Ref: VCVDT]. 175 | 176 | 177 | ### References 178 | **[1]** J.A. Sethian. A fast marching level set method for monotonically advancing fronts. *Proceeding of the National Academy of Sciences of the USA - Paper Edition*, 93(4):1591-1595, 1996. 179 | 180 | **[2]** J. Rickett and S. Fomel. Short note: A second-order fast marching eikonal solver. *Technical Report, Stanford Exploration Project*, 2000. 181 | 182 | **[3]** J.A. Baerentzen. On the implementation of fast marching methods for 3D lattices. *Technical Report. IMM-TR-2001-13*, 2001. 183 | 184 | **[4]** M.W. Jones, J.A. Baerentzen, and M. Sramek. 3D Distance Fields: A Survey of Techniques and Applications. *IEEE Transactions on Visualization and Computer Graphics*, 12(4):581-599, July/August 2006. 185 | 186 | **[5]** R. Bridson. Fluid Simulation for Computer Graphics. *CRC Press*, 2015. 187 | 188 | **[6]** J.A. Sethian. Level set methods and fast marching methods. *Cambridge Monographs on Applied and Computational Mathematics*, Cambridge University Press, second edition, 1999. 189 | 190 | **[7]** N. Rawlinson, M. de Kool, and M. Sambridge. Seismic wavefront tracking in 3D heterogeneous media: applications with multiple data classes. *Exploration Geophysics*, 37(4):322–330, 2006. 191 | 192 | **[8]** S. Osher and R. Fedkiw. Level Set Methods and Dynamic Implicit Surfaces. *Springer*, 2003 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /bindings/python/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | FetchContent_Declare( 3 | pybind11 4 | GIT_REPOSITORY https://github.com/pybind/pybind11.git 5 | GIT_TAG v2.10.4 6 | ) 7 | FetchContent_MakeAvailable(pybind11) 8 | 9 | # A linked issue is generated at the moment 10 | # See https://github.com/pybind/pybind11/pull/4301 11 | pybind11_add_module(py_fast_marching_method py-fast-marching-method.cpp) -------------------------------------------------------------------------------- /bindings/python/py-fast-marching-method.cpp: -------------------------------------------------------------------------------- 1 | // Permission is hereby granted, free of charge, to any person obtaining a 2 | // copy of this software and associated documentation files (the "Software"), 3 | // to deal in the Software without restriction, including without limitation 4 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | // and/or sell copies of the Software, and to permit persons to whom the 6 | // Software is furnished to do so, subject to the following conditions: 7 | // 8 | // The above copyright notice and this permission notice shall be included in 9 | // all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include "../../include/thinks/fast_marching_method/fast_marching_method.hpp" 24 | 25 | namespace py = pybind11; 26 | namespace fmm = thinks::fast_marching_method; 27 | 28 | template 29 | py::array_t UniformSpeedSignedArrivalTime( 30 | std::array const& py_grid_size, 31 | std::vector> const& py_boundary_indices, 32 | std::vector const& boundary_times, 33 | std::array const& py_grid_spacing, T const uniform_speed) { 34 | std::array grid_size; 35 | std::reverse_copy(py_grid_size.begin(), py_grid_size.end(), 36 | grid_size.begin()); 37 | 38 | std::array grid_spacing; 39 | std::reverse_copy(py_grid_spacing.begin(), py_grid_spacing.end(), 40 | grid_spacing.begin()); 41 | 42 | std::vector> boundary_indices; 43 | boundary_indices.reserve(py_boundary_indices.size()); 44 | for (auto& py_boundary_index : py_boundary_indices) { 45 | std::array boundary_index; 46 | std::reverse_copy(py_boundary_index.begin(), py_boundary_index.end(), 47 | boundary_index.begin()); 48 | boundary_indices.push_back(boundary_index); 49 | } 50 | 51 | // auto eikonal_solver = fmm::UniformSpeedEikonalSolver 52 | // (grid_spacing, uniform_speed); 53 | 54 | auto eikonal_solver = fmm::HighAccuracyUniformSpeedEikonalSolver( 55 | grid_spacing, uniform_speed); 56 | 57 | // auto eikonal_solver = fmm::DistanceSolver(grid_spacing[0]); 58 | 59 | std::vector arrival_times = fmm::SignedArrivalTime( 60 | grid_size, boundary_indices, boundary_times, eikonal_solver); 61 | 62 | return py::array_t(py_grid_size, &arrival_times[0]); 63 | } 64 | 65 | template 66 | py::array_t VaryingSpeedSignedArrivalTime( 67 | std::array const& py_grid_size, 68 | std::vector> const& py_boundary_indices, 69 | std::vector const& boundary_times, 70 | std::array const& py_grid_spacing, 71 | py::array_t py_speed_buffer) { 72 | auto py_speed_buffer_flat = py_speed_buffer.reshape({py_speed_buffer.size()}); 73 | auto speed_buffer = py_speed_buffer_flat.template cast>(); 74 | 75 | std::array grid_size; 76 | std::reverse_copy(py_grid_size.begin(), py_grid_size.end(), 77 | grid_size.begin()); 78 | 79 | std::array grid_spacing; 80 | std::reverse_copy(py_grid_spacing.begin(), py_grid_spacing.end(), 81 | grid_spacing.begin()); 82 | 83 | std::vector> boundary_indices; 84 | boundary_indices.reserve(py_boundary_indices.size()); 85 | for (auto& py_boundary_index : py_boundary_indices) { 86 | std::array boundary_index; 87 | std::reverse_copy(py_boundary_index.begin(), py_boundary_index.end(), 88 | boundary_index.begin()); 89 | boundary_indices.push_back(boundary_index); 90 | } 91 | 92 | // auto eikonal_solver = fmm::VaryingSpeedEikonalSolver 93 | // (grid_spacing, grid_size, speed_buffer); 94 | 95 | auto eikonal_solver = fmm::HighAccuracyVaryingSpeedEikonalSolver( 96 | grid_spacing, grid_size, speed_buffer); 97 | 98 | std::vector arrival_times = fmm::SignedArrivalTime( 99 | grid_size, boundary_indices, boundary_times, eikonal_solver); 100 | 101 | return py::array_t(py_grid_size, &arrival_times[0]); 102 | } 103 | 104 | PYBIND11_MODULE(py_fast_marching_method, m) { 105 | m.doc() = R"pbdoc( 106 | Python bindings for the fast marching method 107 | ----------------------- 108 | .. currentmodule:: py_fast_marching_method 109 | .. autosummary:: 110 | :toctree: _generate 111 | uniform_speed_signed_arrival_time 112 | varying_speed_signed_arrival_time 113 | )pbdoc"; 114 | 115 | m.def("uniform_speed_signed_arrival_time", 116 | &UniformSpeedSignedArrivalTime, R"pbdoc( 117 | Signed arrival time under uniform speed 118 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 119 | )pbdoc"); 120 | 121 | m.def("uniform_speed_signed_arrival_time", 122 | &UniformSpeedSignedArrivalTime, R"pbdoc( 123 | Signed arrival time under uniform speed 124 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 125 | )pbdoc"); 126 | 127 | m.def("varying_speed_signed_arrival_time", 128 | &VaryingSpeedSignedArrivalTime, R"pbdoc( 129 | Signed arrival time under varying speed 130 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 131 | )pbdoc"); 132 | 133 | m.def("varying_speed_signed_arrival_time", 134 | &VaryingSpeedSignedArrivalTime, R"pbdoc( 135 | Signed arrival time under varying speed 136 | https://github.com/thinks/fast-marching-method#high-accuracy-fast-marching-method 137 | )pbdoc"); 138 | } 139 | -------------------------------------------------------------------------------- /examples/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(fast-marching-minimal-example 2 | minimal_example.cpp) -------------------------------------------------------------------------------- /examples/cpp/minimal_example.cpp: -------------------------------------------------------------------------------- 1 | // Permission is hereby granted, free of charge, to any person obtaining a 2 | // copy of this software and associated documentation files (the "Software"), 3 | // to deal in the Software without restriction, including without limitation 4 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | // and/or sell copies of the Software, and to permit persons to whom the 6 | // Software is furnished to do so, subject to the following conditions: 7 | // 8 | // The above copyright notice and this permission notice shall be included in 9 | // all copies or substantial portions of the Software. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 16 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "../../include/thinks/fast_marching_method/fast_marching_method.hpp" 25 | 26 | int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { 27 | try { 28 | namespace fmm = thinks::fast_marching_method; 29 | 30 | auto grid_size = std::array{{16, 16}}; 31 | auto grid_spacing = std::array{{1.f / 16, 1.f / 16}}; 32 | auto uniform_speed = 1.f; 33 | 34 | std::cout << "Grid size: " << grid_size[0] << ", " << grid_size[1] 35 | << std::endl; 36 | std::cout << "Grid spacing: " << grid_spacing[0] << ", " << grid_spacing[1] 37 | << std::endl; 38 | std::cout << "Uniform speed: " << uniform_speed << std::endl; 39 | 40 | // Select points close to a true circle 41 | auto circle_boundary_indices = std::vector>{ 42 | {{5, 3}}, {{6, 3}}, {{7, 3}}, {{8, 3}}, {{9, 3}}, {{10, 3}}, 43 | {{4, 4}}, {{5, 4}}, {{10, 4}}, {{11, 4}}, {{3, 5}}, {{4, 5}}, 44 | {{11, 5}}, {{12, 5}}, {{3, 6}}, {{12, 6}}, {{3, 7}}, {{12, 7}}, 45 | {{3, 8}}, {{12, 8}}, {{3, 9}}, {{12, 9}}, {{3, 10}}, {{4, 10}}, 46 | {{11, 10}}, {{12, 10}}, {{4, 11}}, {{5, 11}}, {{10, 11}}, {{11, 11}}, 47 | {{5, 12}}, {{6, 12}}, {{7, 12}}, {{8, 12}}, {{9, 12}}, {{10, 12}}, 48 | }; 49 | 50 | auto num_seeds = circle_boundary_indices.size(); 51 | 52 | // Specify distances of such points to the true circle 53 | auto circle_boundary_distances = std::vector{ 54 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 55 | 0.0417385f, 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 56 | 0.0417385f, -0.0111773f, -0.0111773f, 0.0417385f, 0.0164635f, 57 | 0.0164635f, 0.0029808f, 0.0029808f, 0.0029808f, 0.0029808f, 58 | 0.0164635f, 0.0164635f, 0.0417385f, -0.0111773f, -0.0111773f, 59 | 0.0417385f, 0.0293592f, -0.0111773f, -0.0111773f, 0.0293592f, 60 | 0.0417385f, 0.0164635f, 0.0029808f, 0.0029808f, 0.0164635f, 61 | 0.0417385f}; 62 | 63 | // auto circle_boundary_distances = std::vector(num_seeds, 0.f); 64 | 65 | auto nan = std::numeric_limits::quiet_NaN(); 66 | auto initial_distances = 67 | std::vector(grid_size[0] * grid_size[1], nan); 68 | for (std::size_t i = 0; i < num_seeds; ++i) { 69 | auto idx = circle_boundary_indices[i]; 70 | initial_distances[idx[0] + idx[1] * grid_size[0]] = 71 | circle_boundary_distances[i]; 72 | } 73 | 74 | { 75 | std::cout << "Initial distance map (seeds):" << std::endl; 76 | std::size_t idx = 0; 77 | for (std::size_t j = 0; j < grid_size[1]; ++j) { 78 | for (std::size_t i = 0; i < grid_size[0]; ++i) { 79 | std::cout << std::setw(6) << std::fixed << std::setprecision(4) 80 | << initial_distances[idx++] << '\t'; 81 | } 82 | std::cout << std::endl; 83 | } 84 | } 85 | 86 | // Compute distance map 87 | auto arrival_times = fmm::SignedArrivalTime( 88 | grid_size, circle_boundary_indices, circle_boundary_distances, 89 | fmm::UniformSpeedEikonalSolver(grid_spacing, uniform_speed)); 90 | 91 | { 92 | std::cout << "Distance map:" << std::endl; 93 | std::size_t idx = 0; 94 | for (std::size_t j = 0; j < grid_size[1]; ++j) { 95 | for (std::size_t i = 0; i < grid_size[0]; ++i) { 96 | std::cout << std::setw(6) << std::fixed << std::setprecision(4) 97 | << arrival_times[idx++] << '\t'; 98 | } 99 | std::cout << std::endl; 100 | } 101 | } 102 | } catch (const std::exception& e) { 103 | // standard exceptions 104 | std::cout << "Caught std::exception in main: " << e.what() << std::endl; 105 | return EXIT_FAILURE; 106 | } catch (...) { 107 | // everything else 108 | std::cout << "Caught unknown exception in main" << std::endl; 109 | return EXIT_FAILURE; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/python/py-fast-marching-minimal-example.py: -------------------------------------------------------------------------------- 1 | import py_fast_marching_method as fmm 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def signed_arrival_time_example(): 7 | grid_size = np.array([50, 100]) 8 | # grid_spacing = 1.0 / grid_size 9 | grid_spacing = np.ones(grid_size.shape) 10 | boundary_indices = np.array([[31, 75]]) 11 | boundary_times = np.array([0.0]) 12 | uniform_speed = 1.0 13 | 14 | arrival_times = fmm.uniform_speed_signed_arrival_time( 15 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 16 | ) 17 | 18 | print("Max dist:", np.max(arrival_times[:])) 19 | 20 | plt.imshow(arrival_times) 21 | plt.plot(boundary_indices[0,1], boundary_indices[0,0], "mo") 22 | plt.colorbar() 23 | plt.show() 24 | 25 | 26 | if __name__ == "__main__": 27 | signed_arrival_time_example() 28 | -------------------------------------------------------------------------------- /img/fmm_readme.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme.pdf -------------------------------------------------------------------------------- /img/fmm_readme_concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_concept.png -------------------------------------------------------------------------------- /img/fmm_readme_dilation_bands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_dilation_bands.png -------------------------------------------------------------------------------- /img/fmm_readme_eikonal_solvers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_eikonal_solvers.png -------------------------------------------------------------------------------- /img/fmm_readme_input_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_input_values.png -------------------------------------------------------------------------------- /img/fmm_readme_inside_outside.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_inside_outside.png -------------------------------------------------------------------------------- /img/fmm_readme_point_source_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinks/fast-marching-method/b20f0b224b97935e07bd0ba888c2611ba2786f76/img/fmm_readme_point_source_error.png -------------------------------------------------------------------------------- /include/thinks/fast_marching_method/fast_marching_method.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #ifndef INCLUDE_THINKS_FAST_MARCHING_METHOD_FAST_MARCHING_METHOD_HPP_ 22 | #define INCLUDE_THINKS_FAST_MARCHING_METHOD_FAST_MARCHING_METHOD_HPP_ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | 46 | namespace thinks { 47 | namespace fast_marching_method { 48 | namespace detail { 49 | 50 | // forward decls 51 | template 52 | void ThrowIfZeroElementInSize(std::array const& size); 53 | 54 | //! Returns the product of the elements in array @a size. 55 | //! Note: Not checking for integer overflow here! 56 | template 57 | std::size_t LinearSize(std::array const& size) { 58 | ThrowIfZeroElementInSize(size); 59 | return std::accumulate(std::begin(size), std::end(size), size_t{1}, 60 | std::multiplies()); 61 | } 62 | 63 | //! Returns x * x. 64 | //! Note: Not checking for overflow! 65 | template 66 | constexpr T Squared(T const x) { 67 | return x * x; 68 | } 69 | 70 | //! Returns 1 / (x * x). 71 | //! Type T must be floating point. 72 | template 73 | constexpr T InverseSquared(T const x) { 74 | static_assert(std::is_floating_point::value, 75 | "scalar type must be floating point"); 76 | 77 | return T{1} / Squared(x); 78 | } 79 | 80 | //! Returns element-wise 1 / (a * a), i.e. 81 | //! { 1 / a[0] * a[0], 1 / a[1] * a[1], ... } 82 | template 83 | std::array InverseSquared(std::array const& a) { 84 | static_assert(std::is_floating_point::value, 85 | "scalar type must be floating point"); 86 | 87 | auto r = std::array(); 88 | std::transform(std::begin(a), std::end(a), std::begin(r), 89 | [](T const x) { return InverseSquared(x); }); 90 | return r; 91 | } 92 | 93 | //! Returns true if @a index is inside @a size, otherwise false. 94 | template 95 | bool Inside(std::array const& index, 96 | std::array const& size) { 97 | static_assert(N > 0, "invalid dimensionality"); 98 | 99 | for (auto i = std::size_t{0}; i < N; ++i) { 100 | // Cast is safe since we check that index[i] is greater than or 101 | // equal to zero first. 102 | if (!(int32_t{0} <= index[i] && 103 | static_cast(index[i]) < size[i])) { 104 | return false; 105 | } 106 | } 107 | return true; 108 | } 109 | 110 | //! Returns a string representation of the array @a a. 111 | template 112 | std::string ToString(std::array const& a) { 113 | auto ss = std::stringstream(); 114 | ss << "["; 115 | for (auto i = std::size_t{0}; i < N; ++i) { 116 | ss << a[i]; 117 | if (i != (N - 1)) { 118 | ss << ", "; 119 | } 120 | } 121 | ss << "]"; 122 | return ss.str(); 123 | } 124 | 125 | //! Returns an array where every element is set to @a value. 126 | template 127 | std::array FilledArray(T const value) { 128 | auto a = std::array(); 129 | std::fill(std::begin(a), std::end(a), value); 130 | return a; 131 | } 132 | 133 | //! Returns true if @a dx is valid, otherwise false. 134 | template 135 | bool ValidGridSpacingElement(T const dx) { 136 | static_assert(std::is_floating_point::value, 137 | "grid spacing element type must be floating point"); 138 | 139 | // Fail when dx is NaN. 140 | return dx >= T(1e-6); 141 | } 142 | 143 | //! Returns true if @a grid_spacing is valid, otherwise false. 144 | template 145 | bool ValidGridSpacing(std::array const& grid_spacing) { 146 | // All elements must be larger than or equal to a minimum value. 147 | // Fails if any element is NaN. 148 | return std::all_of(std::begin(grid_spacing), std::end(grid_spacing), 149 | [](auto const dx) { return ValidGridSpacingElement(dx); }); 150 | } 151 | 152 | //! Returns true if @a speed is valid, otherwise false. 153 | template 154 | bool ValidSpeed(T const speed) { 155 | static_assert(std::is_floating_point::value, 156 | "speed type must be floating point"); 157 | 158 | return speed >= T(1e-6); 159 | } 160 | 161 | //! Throws an std::invalid_argument exception if one or more of the 162 | //! elements in @a size is zero. 163 | template 164 | void ThrowIfZeroElementInSize(std::array const& size) { 165 | if (std::find_if(std::begin(size), std::end(size), [](auto const x) { 166 | return x == std::size_t{0}; 167 | }) != std::end(size)) { 168 | auto ss = std::stringstream(); 169 | ss << "invalid size: " << ToString(size); 170 | throw std::invalid_argument(ss.str()); 171 | } 172 | } 173 | 174 | //! Throws an std::invalid_argument exception if the linear size 175 | //! of @a grid_size is not equal to @a cell_buffer_size. 176 | template 177 | void ThrowIfInvalidCellBufferSize(std::array const& grid_size, 178 | std::size_t const cell_buffer_size) { 179 | if (LinearSize(grid_size) != cell_buffer_size) { 180 | auto ss = std::stringstream(); 181 | ss << "grid size " << ToString(grid_size) 182 | << " does not match cell buffer size " << cell_buffer_size; 183 | throw std::invalid_argument(ss.str()); 184 | } 185 | } 186 | 187 | //! Throws an std::invalid_argument exception if @a grid_spacing is invalid. 188 | template 189 | void ThrowIfInvalidGridSpacing(std::array const& grid_spacing) { 190 | if (!ValidGridSpacing(grid_spacing)) { 191 | auto ss = std::stringstream(); 192 | ss << "invalid grid spacing: " << ToString(grid_spacing); 193 | throw std::invalid_argument(ss.str()); 194 | } 195 | } 196 | 197 | //! Throws an std::invalid_argument exception if @a speed is invalid. 198 | template 199 | void ThrowIfZeroOrNegativeOrNanSpeed(T const speed) { 200 | if (!ValidSpeed(speed)) { 201 | auto ss = std::stringstream(); 202 | ss << "invalid speed: " << speed; 203 | throw std::invalid_argument(ss.str()); 204 | } 205 | } 206 | 207 | //! Throws an std::invalid_argument exception if @a boundary_indices is empty. 208 | template 209 | void ThrowIfEmptyBoundaryIndices( 210 | std::vector> const& boundary_indices) { 211 | if (boundary_indices.empty()) { 212 | throw std::invalid_argument("empty boundary condition"); 213 | } 214 | } 215 | 216 | //! Throws an std::invalid_argument exception if @a boundary_index is not 217 | //! inside @a grid_size. 218 | template 219 | void ThrowIfBoundaryIndexOutsideGrid( 220 | std::array const& boundary_index, 221 | std::array const& grid_size) { 222 | if (!Inside(boundary_index, grid_size)) { 223 | auto ss = std::stringstream(); 224 | ss << "boundary index outside grid - " 225 | << "index: " << ToString(boundary_index) << ", " 226 | << "grid size: " << ToString(grid_size); 227 | throw std::invalid_argument(ss.str()); 228 | } 229 | } 230 | 231 | //! Throws an std::invalid_argument exception if the flag @a valid is false. 232 | //! @a time is used to construct the exception message. 233 | template 234 | void ThrowIfInvalidBoundaryTime(bool const valid, T const time) { 235 | if (!valid) { 236 | auto ss = std::stringstream(); 237 | ss << "invalid boundary time: " << time; 238 | throw std::invalid_argument(ss.str()); 239 | } 240 | } 241 | 242 | //! Throws an std::invalid_argument if the flag @a duplicate is false. 243 | //! @a index is used to construct the exception message. 244 | template 245 | void ThrowIfDuplicateBoundaryIndex(bool const duplicate, 246 | std::array const& index) { 247 | if (duplicate) { 248 | auto ss = std::stringstream(); 249 | ss << "duplicate boundary index: " << ToString(index); 250 | throw std::invalid_argument(ss.str()); 251 | } 252 | } 253 | 254 | //! Throws an std::invalid_argument if the size of @a boundary_indices is 255 | //! not equal to the size of @a boundary_times. 256 | template 257 | void ThrowIfBoundaryIndicesTimesSizeMismatch( 258 | std::vector> const& boundary_indices, 259 | std::vector const& boundary_times) { 260 | if (boundary_indices.size() != boundary_times.size()) { 261 | auto ss = std::stringstream(); 262 | ss << "boundary indices[" << boundary_indices.size() << "] / " 263 | << "boundary times[" << boundary_times.size() << "] size mismatch"; 264 | throw std::invalid_argument(ss.str()); 265 | } 266 | } 267 | 268 | //! Throws an std::runtime_error exception if @a arrival_time is not valid. 269 | template 270 | void ThrowIfInvalidArrivalTime(T const arrival_time, 271 | std::array const& index) { 272 | // Fail when d is NaN. 273 | if (!(arrival_time >= T{0})) { 274 | auto ss = std::stringstream(); 275 | ss << "invalid arrival time (distance) " << arrival_time << " at index " 276 | << ToString(index); 277 | throw std::runtime_error(ss.str()); 278 | } 279 | } 280 | 281 | //! Throws an std::runtime_error exception if @a arrival_time is not valid. 282 | template 283 | void ThrowIfInvalidArrivalTimeWithDetails( 284 | T const arrival_time, std::array const& index, T const speed, 285 | std::array const& q, T const cell_distance, 286 | std::array, N> const& frozen_neighbor_distances, 287 | std::size_t const frozen_neighbor_distances_count) { 288 | // Fail when d is NaN. 289 | if (!(arrival_time >= T{0})) { 290 | auto ss = std::stringstream(); 291 | 292 | ss << "invalid arrival time (distance) " << arrival_time << " at index " 293 | << ToString(index) << std::endl; 294 | 295 | // auto const cell_distance = distance_grid.Cell(index); 296 | for (auto i = std::size_t{0}; i < frozen_neighbor_distances_count; ++i) { 297 | ss << "[" << i << "]:" << cell_distance << " | " 298 | << frozen_neighbor_distances[i].first << ", " 299 | << frozen_neighbor_distances[i].second << std::endl; 300 | } 301 | ss << "speed: " << speed << std::endl; 302 | ss << "q: " << ToString(q) << std::endl; 303 | ss << "discr: " << q[1] * q[1] - T{4} * q[2] * q[0] << std::endl; 304 | ss << "int64: " 305 | << int64_t{-800046} * int64_t{-800046} - 306 | int64_t{4} * int64_t{1179650} * int64_t{135649} 307 | << std::endl; 308 | 309 | throw std::runtime_error(ss.str()); 310 | } 311 | } 312 | 313 | //! Throws an std::runtime_error exception if @a arrival_time is not valid. 314 | template 315 | void ThrowIfInvalidArrivalTimeWithHighAccuracyDetails( 316 | T const arrival_time, std::array const& index, T const speed, 317 | std::array const& q, T const cell_distance, 318 | std::array, std::size_t>, N> const& 319 | frozen_neighbor_distances, 320 | std::size_t const frozen_neighbor_distances_count) { 321 | // Fail when d is NaN. 322 | if (!(arrival_time >= T{0})) { 323 | auto ss = std::stringstream(); 324 | 325 | ss << "invalid arrival time (distance) " << arrival_time << " at index " 326 | << ToString(index) << std::endl; 327 | 328 | // auto const cell_distance = distance_grid.Cell(index); 329 | for (auto i = std::size_t{0}; i < frozen_neighbor_distances_count; ++i) { 330 | ss << "[" << i << "]:" << cell_distance << " | " 331 | << frozen_neighbor_distances[i].first.first << ", " 332 | << frozen_neighbor_distances[i].first.second << std::endl; 333 | } 334 | ss << "speed: " << speed << std::endl; 335 | ss << "q: " << ToString(q) << std::endl; 336 | ss << "discr: " << q[1] * q[1] - T{4} * q[2] * q[0] << std::endl; 337 | ss << "int64: " 338 | << int64_t{-800046} * int64_t{-800046} - 339 | int64_t{4} * int64_t{1179650} * int64_t{135649} 340 | << std::endl; 341 | 342 | throw std::runtime_error(ss.str()); 343 | } 344 | } 345 | 346 | //! Throws an std::invalid_argument exception if @a speed_index is not 347 | //! inside @a grid_size. 348 | template 349 | void ThrowIfSpeedIndexOutsideGrid( 350 | std::array const& speed_index, 351 | std::array const& grid_size) { 352 | if (!Inside(speed_index, grid_size)) { 353 | auto ss = std::stringstream(); 354 | ss << "speed index outside grid - " 355 | << "index: " << ToString(speed_index) << ", " 356 | << "grid size: " << ToString(grid_size); 357 | throw std::invalid_argument(ss.str()); 358 | } 359 | } 360 | 361 | //! Throws an std::invalid_argument exception if the number of 362 | //! @a boundary_indices is equal to the number of grid cells (given 363 | //! by @a grid_size). Note that we are not checking for duplicates in 364 | //! @a boundary_indices here, we are assuming unique indices. However, 365 | //! duplicate indices are checked elsewhere and will give rise to a separate 366 | //! error. 367 | template 368 | void ThrowIfFullGridBoundaryIndices( 369 | std::vector> const& boundary_indices, 370 | std::array const& grid_size) { 371 | if (boundary_indices.size() == LinearSize(grid_size)) { 372 | auto ss = std::stringstream(); 373 | ss << "full grid boundary"; 374 | throw std::invalid_argument(ss.str()); 375 | } 376 | } 377 | 378 | //! Returns an std::array that can be used to transform an N-dimensional index 379 | //! into a linear index. 380 | template 381 | std::array GridStrides( 382 | std::array const& grid_size) { 383 | auto strides = std::array(); 384 | auto stride = std::size_t{1}; 385 | for (auto i = std::size_t{1}; i < N; ++i) { 386 | stride *= grid_size[i - 1]; 387 | strides[i - 1] = stride; 388 | } 389 | return strides; 390 | } 391 | 392 | //! Returns a linear (scalar) index into an std::array representing an 393 | //! N-dimensional grid for integer coordinate @a index. 394 | //! Note: Not checking for integer overflow here! 395 | template 396 | std::size_t GridLinearIndex( 397 | std::array const& index, 398 | std::array const& grid_strides) { 399 | auto k = static_cast(index[0]); 400 | for (auto i = std::size_t{1}; i < N; ++i) { 401 | k += index[i] * grid_strides[i - 1]; 402 | } 403 | return k; 404 | } 405 | 406 | //! Access a linear array as if it were an N-dimensional grid. 407 | //! Allows mutating operations on the underlying array. The grid does 408 | //! not own the underlying array, but is simply an indexing structure. 409 | //! 410 | //! Usage: 411 | //! auto size = std::array(); 412 | //! size[0] = 2; 413 | //! size[1] = 2; 414 | //! auto cells = std::vector(4); 415 | //! cells[0] = 0.f; 416 | //! cells[1] = 1.f; 417 | //! cells[2] = 2.f; 418 | //! cells[3] = 3.f; 419 | //! auto grid = Grid(size, cells.front()); 420 | //! std::cout << "cell (0,0): " << grid.cell({{0, 0}}) << std::endl; 421 | //! std::cout << "cell (1,0): " << grid.cell({{1, 0}}) << std::endl; 422 | //! std::cout << "cell (0,1): " << grid.cell({{0, 1}}) << std::endl; 423 | //! std::cout << "cell (1,1): " << grid.cell({{1, 1}}) << std::endl; 424 | //! std::cout << "-----" << std::endl; 425 | //! grid.Cell({{0, 1}}) = 5.3; 426 | //! std::cout << "cell (0,1): " << grid.cell({{0, 1}}) << std::endl; 427 | //! 428 | //! Output: 429 | //! cell (0,0): 0 430 | //! cell (1,0): 1 431 | //! cell (0,1): 2 432 | //! cell (1,1): 3 433 | //! ----- 434 | //! cell (0,1): 5.3 435 | template 436 | class Grid { 437 | public: 438 | typedef T CellType; 439 | typedef std::array SizeType; 440 | typedef std::array IndexType; 441 | 442 | //! Construct a grid from a given @a size and @a cell_buffer. Does not 443 | //! take ownership of the cell buffer, it is assumed that this buffer exists 444 | //! during the life-time of the grid object. 445 | //! 446 | //! Preconditions: 447 | //! - @a cell_buffer is not empty. 448 | Grid(SizeType const& size, std::vector& cell_buffer) 449 | : size_(size), strides_(GridStrides(size)), cells_(nullptr) { 450 | ThrowIfZeroElementInSize(size); 451 | ThrowIfInvalidCellBufferSize(size, cell_buffer.size()); 452 | 453 | assert(!cell_buffer.empty() && "cell_buffer must not be empty"); 454 | cells_ = &cell_buffer.front(); 455 | } 456 | 457 | //! Construct a grid from a given @a size and @a const cell_buffer. Does not 458 | //! take ownership of the cell buffer, it is assumed that this buffer exists 459 | //! during the life-time of the grid object. 460 | //! 461 | //! Preconditions: 462 | //! - @a cell_buffer is not empty. 463 | Grid(SizeType const& size, std::vector const& cell_buffer) 464 | : size_(size), strides_(GridStrides(size)), cells_(nullptr) { 465 | ThrowIfZeroElementInSize(size); 466 | ThrowIfInvalidCellBufferSize(size, cell_buffer.size()); 467 | 468 | assert(!cell_buffer.empty() && "cell_buffer must not be empty"); 469 | cells_ = &cell_buffer.front(); 470 | } 471 | 472 | //! Returns the size of the grid. 473 | SizeType size() const { return size_; } 474 | 475 | //! Returns a reference to the cell at @a index. No range checking! 476 | //! 477 | //! Preconditions: 478 | //! - @a index is inside the grid. 479 | template 480 | typename std::enable_if::type Cell( 481 | IndexType const& index) { 482 | assert(GridLinearIndex(index, strides_) < LinearSize(size()) && 483 | "Precondition"); 484 | return cells_[GridLinearIndex(index, strides_)]; 485 | } 486 | 487 | //! Returns a const reference to the cell at @a index. No range checking! 488 | //! 489 | //! Preconditions: 490 | //! - @a index is inside the grid. 491 | CellType const& Cell(IndexType const& index) const { 492 | assert(GridLinearIndex(index, strides_) < LinearSize(size()) && 493 | "Precondition"); 494 | return cells_[GridLinearIndex(index, strides_)]; 495 | } 496 | 497 | private: 498 | std::array const size_; 499 | std::array const strides_; 500 | using CellPtrType = 501 | std::conditional_t; 502 | CellPtrType cells_; 503 | }; 504 | 505 | //! Acceleration structure for keeping track of the smallest distance cell 506 | //! in the narrow band. 507 | template 508 | class NarrowBandStore { 509 | public: 510 | typedef T DistanceType; 511 | typedef std::array IndexType; 512 | typedef std::pair ValueType; 513 | 514 | //! Create an empty store. 515 | NarrowBandStore() {} 516 | 517 | //! Returns true if the store is empty, otherwise false. 518 | bool empty() const { return min_heap_.empty(); } 519 | 520 | //! Remove the value with the smallest distance from the store and 521 | //! return it. 522 | //! 523 | //! Preconditions: 524 | //! - The store is not empty (check first with empty()). 525 | ValueType Pop() { 526 | assert(!min_heap_.empty() && "Precondition"); 527 | auto const v = min_heap_.top(); // O(1) 528 | min_heap_.pop(); // O(log N) 529 | return v; 530 | } 531 | 532 | //! Adds @a value to the store. 533 | void Push(ValueType const& value) { 534 | min_heap_.push(value); // O(log N) 535 | } 536 | 537 | private: 538 | // Place smaller values at the top of the heap. 539 | typedef std::priority_queue, 540 | std::greater> 541 | MinHeap_; 542 | 543 | MinHeap_ min_heap_; 544 | }; 545 | 546 | //! Returns an std::array of pairs, where each element is the min/max index 547 | //! coordinates in the corresponding dimension. 548 | //! 549 | //! Preconditions: 550 | //! - @a indices is not empty. 551 | template 552 | std::array, N> BoundingBox( 553 | std::vector> const& indices) { 554 | static_assert(N > 0, "Dimensionality cannot be zero"); 555 | 556 | assert(!indices.empty() && "Precondition"); 557 | 558 | // Initialize bounding box in all dimensions. 559 | auto bbox = std::array, N>(); 560 | for (auto i = std::size_t{0}; i < N; ++i) { 561 | bbox[i].first = std::numeric_limits::max(); 562 | bbox[i].second = std::numeric_limits::min(); 563 | } 564 | 565 | // Update with each index in all dimensions. 566 | for (auto const& index : indices) { 567 | for (auto i = std::size_t{0}; i < N; ++i) { 568 | bbox[i].first = std::min(bbox[i].first, index[i]); 569 | bbox[i].second = std::max(bbox[i].second, index[i]); 570 | } 571 | } 572 | return bbox; 573 | } 574 | 575 | //! Returns the hyper volume of the provided N-dimensional 576 | //! bounding box @a bbox. This function does not take grid spacing into 577 | //! account, but rather returns the volume in an index space. 578 | //! 579 | //! Preconditions: 580 | //! - The pairs representing bounds in each dimension store the lower bound 581 | //! as the first element and the higher bound as the second element. 582 | template 583 | std::size_t HyperVolume( 584 | std::array, N> const& bbox) { 585 | auto hyper_volume = std::size_t{1}; 586 | for (auto i = std::size_t{0}; i < N; ++i) { 587 | assert(bbox[i].first <= bbox[i].second && "Precondition"); 588 | hyper_volume *= (bbox[i].second - bbox[i].first + 1); 589 | } 590 | return hyper_volume; 591 | } 592 | 593 | //! Returns true if @a inner_bbox is contained by @a outer_bbox. 594 | //! 595 | //! Preconditions: 596 | //! - The hyper-volume of @a outer_bbox is larger than the hyper-volume of 597 | //! @a inner_bbox. 598 | //! - The pairs representing bounds in each dimension store the lower bound 599 | //! as the first element and the higher bound as the second element. 600 | template 601 | bool Contains( 602 | std::array, N> const& outer_bbox, 603 | std::array, N> const& inner_bbox) { 604 | assert(HyperVolume(outer_bbox) >= HyperVolume(inner_bbox) && "Precondition"); 605 | 606 | auto contains = true; 607 | for (auto i = std::size_t{0}; i < N; ++i) { 608 | assert(outer_bbox[i].first <= outer_bbox[i].second && "Precondition"); 609 | assert(inner_bbox[i].first <= inner_bbox[i].second && "Precondition"); 610 | if (!(outer_bbox[i].first < inner_bbox[i].first && 611 | inner_bbox[i].second < outer_bbox[i].second)) { 612 | contains = false; 613 | break; 614 | } 615 | } 616 | return contains; 617 | } 618 | 619 | //! Returns @a base ^ @a exponent as a compile-time constant. 620 | //! Note: Not checking for integer overflow here! 621 | constexpr std::size_t static_pow(std::size_t const base, 622 | std::size_t const exponent) { 623 | // Note: Cannot use loops in constexpr functions in C++11, have to use 624 | // recursion here. 625 | return exponent == std::size_t{0} ? std::size_t{1} 626 | : base * static_pow(base, exponent - 1); 627 | } 628 | 629 | //! Returns a list of face neighbor offset for an N-dimensional cell. 630 | //! In 2D this is the 4-neighborhood, in 3D the 6-neighborhood, etc. 631 | template 632 | std::array, 2 * N> FaceNeighborOffsets() { 633 | static_assert(N > 0, "dimensionality cannot be zero"); 634 | 635 | auto offsets = std::array, std::size_t{2} * N>{}; 636 | for (auto i = std::size_t{0}; i < N; ++i) { 637 | for (auto j = std::size_t{0}; j < N; ++j) { 638 | if (j == i) { 639 | offsets[2 * i + 0][j] = int32_t{+1}; 640 | offsets[2 * i + 1][j] = int32_t{-1}; 641 | } else { 642 | offsets[2 * i + 0][j] = int32_t{0}; 643 | offsets[2 * i + 1][j] = int32_t{0}; 644 | } 645 | } 646 | } 647 | return offsets; 648 | } 649 | 650 | //! Returns a list of vertex neighbor offset for an N-dimensional cell. 651 | //! In 2D this is the 8-neighborhood, in 3D the 26-neighborhood, etc. 652 | template 653 | std::array, static_pow(3, N) - 1> 654 | VertexNeighborOffsets() { 655 | typedef std::array IndexType; 656 | 657 | static_assert(N > 0, "dimensionality cannot be zero"); 658 | 659 | auto offsets = std::array(); 660 | auto index = IndexType(); 661 | std::fill(std::begin(index), std::end(index), int32_t{0}); 662 | for (auto i = std::size_t{0}; i < offsets.size();) { 663 | auto offset = index; 664 | std::for_each(std::begin(offset), std::end(offset), 665 | [](auto& d) { d -= int32_t{1}; }); 666 | if (!std::all_of(std::begin(offset), std::end(offset), 667 | [](auto const i) { return i == 0; })) { 668 | offsets[i++] = offset; 669 | } 670 | 671 | // Next index. 672 | auto j = std::size_t{0}; 673 | while (j < N) { 674 | if ((index[j] + 1) < std::int32_t{3}) { 675 | ++index[j]; 676 | break; 677 | } else { 678 | index[j++] = 0; 679 | } 680 | } 681 | } 682 | 683 | return offsets; 684 | } 685 | 686 | //! Returns a list of connected components. Each connected component is 687 | //! represented as a non-empty list of indices. The provided 688 | //! @a foreground_indices are used as foreground. The neighborhood used to 689 | //! determine connectivity is given by the two iterators 690 | //! @a neighbor_offset_begin and @a neighbor_offset_end. If @a indices is 691 | //! non-empty there is at least one connected component. 692 | //! 693 | //! Preconditions: 694 | //! - All elements in @a indices are inside @a grid_size. 695 | template 696 | std::vector>> ConnectedComponents( 697 | std::vector> const& foreground_indices, 698 | std::array const& grid_size, 699 | NeighborOffsetIt const neighbor_offset_begin, 700 | NeighborOffsetIt const neighbor_offset_end) { 701 | enum class LabelCell : uint8_t { 702 | kBackground = uint8_t{0}, 703 | kForeground, 704 | kLabelled 705 | }; 706 | 707 | if (foreground_indices.empty()) { 708 | return std::vector>>(); 709 | } 710 | 711 | auto label_buffer = 712 | std::vector(LinearSize(grid_size), LabelCell::kBackground); 713 | auto label_grid = Grid(grid_size, label_buffer); 714 | 715 | for (auto const& foreground_index : foreground_indices) { 716 | // Note: We don't check for duplicate indices here since it doesn't 717 | // affect the algorithm. 718 | assert(Inside(foreground_index, label_grid.size()) && "Precondition"); 719 | label_grid.Cell(foreground_index) = LabelCell::kForeground; 720 | } 721 | 722 | auto connected_components = 723 | std::vector>>(); 724 | for (auto const& foreground_index : foreground_indices) { 725 | assert(Inside(foreground_index, label_grid.size())); 726 | assert(label_grid.Cell(foreground_index) == LabelCell::kForeground || 727 | label_grid.Cell(foreground_index) == LabelCell::kLabelled); 728 | 729 | if (label_grid.Cell(foreground_index) == LabelCell::kForeground) { 730 | // This index has not already been labelled. 731 | // Start a new component. 732 | auto component = std::vector>(); 733 | component.reserve(foreground_indices.size()); 734 | auto neighbor_indices = std::stack>(); 735 | label_grid.Cell(foreground_index) = LabelCell::kLabelled; 736 | component.push_back(foreground_index); 737 | neighbor_indices.push(foreground_index); 738 | 739 | // Flood-fill current label. 740 | while (!neighbor_indices.empty()) { 741 | auto const top_neighbor_index = neighbor_indices.top(); 742 | neighbor_indices.pop(); 743 | for (auto neighbor_offset_iter = neighbor_offset_begin; 744 | neighbor_offset_iter != neighbor_offset_end; 745 | ++neighbor_offset_iter) { 746 | // Offset neighbor index. 747 | auto neighbor_index = top_neighbor_index; 748 | for (auto i = std::size_t{0}; i < N; ++i) { 749 | neighbor_index[i] += (*neighbor_offset_iter)[i]; 750 | } 751 | 752 | if (Inside(neighbor_index, label_grid.size()) && 753 | label_grid.Cell(neighbor_index) == LabelCell::kForeground) { 754 | // Tag this neighbor as labelled, store in component and add 755 | // to list of indices whose neighbors we should check. 756 | label_grid.Cell(neighbor_index) = LabelCell::kLabelled; 757 | component.push_back(neighbor_index); 758 | neighbor_indices.push(neighbor_index); 759 | } 760 | } 761 | } 762 | 763 | assert(!component.empty()); 764 | connected_components.push_back(component); 765 | } 766 | } 767 | 768 | assert(!connected_components.empty()); 769 | return connected_components; 770 | } 771 | 772 | //! Returns @a dilation_grid_index transformed to the distance grid. 773 | template 774 | std::array DistanceGridIndexFromDilationGridIndex( 775 | std::array const& dilation_grid_index) { 776 | auto distance_grid_index = dilation_grid_index; 777 | std::for_each(std::begin(distance_grid_index), std::end(distance_grid_index), 778 | [](auto& d) { d -= int32_t{1}; }); 779 | return distance_grid_index; 780 | } 781 | 782 | //! Returns @a distance_grid_index transformed to the dilation grid. 783 | template 784 | std::array DilationGridIndexFromDistanceGridIndex( 785 | std::array const& distance_grid_index) { 786 | auto dilation_grid_index = distance_grid_index; 787 | std::for_each(std::begin(dilation_grid_index), std::end(dilation_grid_index), 788 | [](auto& d) { 789 | d += int32_t{1}; 790 | assert(d >= int32_t{0}); 791 | }); 792 | return dilation_grid_index; 793 | } 794 | 795 | //! Returns a list of dilation bands. A dilation band is defined as a set of 796 | //! cells where each cell has at least one neighbor in @a grid_indices. The 797 | //! neighbor definition is computed using the offsets provided by 798 | //! @a dilation_neighbor_offset_begin and @a dilation_neighbor_offset_end. 799 | //! Furthermore, the cells in a dilation band are connected to each other. In 800 | //! this case the neighborhood is defined by the offsets provided by 801 | //! @a band_neighbor_offset_begin and @a band_neighbor_offset_end. 802 | //! 803 | //! Note that the cells in the dilation bands are defined on a dilation grid 804 | //! that is padded by one in each dimension relative to @a grid_size. 805 | //! 806 | //! Preconditions: 807 | //! - All elements in @a grid_indices are inside @a grid_size. 808 | //! - Neighbor offsets are not larger than one cell in any dimension. 809 | template 811 | std::vector>> DilationBands( 812 | std::vector> const& grid_indices, 813 | std::array const& grid_size, 814 | DilationNeighborOffsetIt const dilation_neighbor_offset_begin, 815 | DilationNeighborOffsetIt const dilation_neighbor_offset_end, 816 | BandNeighborOffsetIt const band_neighbor_offset_begin, 817 | BandNeighborOffsetIt const band_neighbor_offset_end) { 818 | enum class DilationCell : uint8_t { 819 | kBackground = uint8_t{0}, 820 | kForeground, 821 | kDilated 822 | }; 823 | 824 | assert(LinearSize(grid_size) > std::size_t{0}); 825 | 826 | if (grid_indices.empty()) { 827 | return std::vector>>(); 828 | } 829 | 830 | // Dilation grid is padded one cell in each dimension. 831 | // Then transform the provided indices to the dilation grid. 832 | auto dilation_grid_size = grid_size; 833 | std::for_each(std::begin(dilation_grid_size), std::end(dilation_grid_size), 834 | [](auto& d) { d += 2; }); 835 | auto dilation_buffer = std::vector( 836 | LinearSize(dilation_grid_size), DilationCell::kBackground); 837 | auto dilation_grid = 838 | Grid(dilation_grid_size, dilation_buffer); 839 | auto dilation_grid_indices = std::vector>(); 840 | dilation_grid_indices.reserve(grid_indices.size()); 841 | transform(std::begin(grid_indices), std::end(grid_indices), 842 | back_inserter(dilation_grid_indices), [=](auto const& grid_index) { 843 | assert(Inside(grid_index, grid_size)); 844 | auto const dilation_grid_index = 845 | DilationGridIndexFromDistanceGridIndex(grid_index); 846 | assert(Inside(dilation_grid_index, dilation_grid_size)); 847 | return dilation_grid_index; 848 | }); 849 | 850 | // Set foreground from the (transformed) provided indices. 851 | for (auto const& dilation_grid_index : dilation_grid_indices) { 852 | // Note: We don't check for duplicate indices here since it doesn't 853 | // affect the algorithm. 854 | assert(Inside(dilation_grid_index, dilation_grid.size()) && "Precondition"); 855 | dilation_grid.Cell(dilation_grid_index) = DilationCell::kForeground; 856 | } 857 | 858 | // Tag background cells connected to foreground as dilated. 859 | // We only overwrite background cells here. 860 | auto dilation_indices = std::vector>(); 861 | dilation_indices.reserve(size_t{2} * dilation_grid_indices.size()); 862 | for (auto const& dilation_grid_index : dilation_grid_indices) { 863 | assert(dilation_grid.Cell(dilation_grid_index) == 864 | DilationCell::kForeground); 865 | for (auto dilation_neighbor_offset_iter = dilation_neighbor_offset_begin; 866 | dilation_neighbor_offset_iter != dilation_neighbor_offset_end; 867 | ++dilation_neighbor_offset_iter) { 868 | auto neighbor_index = dilation_grid_index; 869 | for (auto i = std::size_t{0}; i < N; ++i) { 870 | neighbor_index[i] += (*dilation_neighbor_offset_iter)[i]; 871 | } 872 | 873 | assert(Inside(neighbor_index, dilation_grid.size()) && "Precondition"); 874 | auto& dilation_cell = dilation_grid.Cell(neighbor_index); 875 | if (dilation_cell == DilationCell::kBackground) { 876 | dilation_cell = DilationCell::kDilated; 877 | dilation_indices.push_back(neighbor_index); 878 | } 879 | } 880 | } 881 | assert(!dilation_indices.empty()); 882 | 883 | // Get connected components of dilated cells. 884 | auto const dilation_bands = 885 | ConnectedComponents(dilation_indices, dilation_grid_size, 886 | band_neighbor_offset_begin, band_neighbor_offset_end); 887 | assert(!dilation_bands.empty()); 888 | return dilation_bands; 889 | } 890 | 891 | //! Since dilation bands are constructed using a vertex neighborhood, 892 | //! not all dilation cells are face-connected to a boundary cell. 893 | //! Given a list of @a dilation_band_indices in dilation grid coordinates 894 | //! (padded by one in each direction), returns a list of narrow band 895 | //! indices in distance grid coordinates. The returned indices are 896 | //! guaranteed to be face-connected to at least one boundary cell 897 | //! in @a boundary_mask_grid. 898 | //! 899 | //! Note that the returned list may be empty. This can happen if all 900 | //! @a dilation_band_indices are on the border of the dilation grid, i.e. 901 | //! outside the distance grid. It also happens if the @a dilation_band_indices 902 | //! list is empty, or if @a boundary_mask_grid has values such that the 903 | //! boundary is not face-connected to any of the dilation indices. 904 | //! 905 | //! It is assumed that the value int8_t{1} is used to tag boundary cells in 906 | //! @a boundary_mask_grid. Also, (transformed) dilation band indices are 907 | //! assumed not to be on a boundary. 908 | template 909 | std::vector> NarrowBandDilationBandCells( 910 | std::vector> const& dilation_band_indices, 911 | Grid const& boundary_mask_grid) { 912 | if (dilation_band_indices.empty()) { 913 | return std::vector>(); 914 | } 915 | 916 | auto narrow_band_indices = std::vector>(); 917 | narrow_band_indices.reserve(dilation_band_indices.size()); 918 | for (auto const& dilation_grid_index : dilation_band_indices) { 919 | // Since dilation bands are constructed using a vertex neighborhood, 920 | // not all dilation cells are face-connected to a boundary cell. 921 | // We add only those dilation cells that are face-connected to a 922 | // boundary cell, since this will be required when estimating distance 923 | // (i.e. solving the eikonal equation). 924 | auto const distance_grid_index = 925 | DistanceGridIndexFromDilationGridIndex(dilation_grid_index); 926 | 927 | // If the distance grid index is not inside the boundary mask 928 | // (i.e. distance) grid it cannot belong to a narrow band. 929 | if (Inside(distance_grid_index, boundary_mask_grid.size())) { 930 | assert(boundary_mask_grid.Cell(distance_grid_index) != uint8_t{1}); 931 | 932 | // Check for boundary face-neighbors in each dimension. 933 | // If we find one boundary face-neighbor we are done. 934 | for (auto i = std::size_t{0}; i < N; ++i) { 935 | // +1 936 | auto neighbor_index = distance_grid_index; 937 | neighbor_index[i] += int32_t{1}; 938 | if (Inside(neighbor_index, boundary_mask_grid.size()) && 939 | boundary_mask_grid.Cell(neighbor_index) == uint8_t{1}) { 940 | narrow_band_indices.push_back(distance_grid_index); 941 | break; 942 | } 943 | // +1 - 2 = -1 944 | neighbor_index[i] -= int32_t{2}; 945 | if (Inside(neighbor_index, boundary_mask_grid.size()) && 946 | boundary_mask_grid.Cell(neighbor_index) == uint8_t{1}) { 947 | narrow_band_indices.push_back(distance_grid_index); 948 | break; 949 | } 950 | } 951 | } 952 | } 953 | assert(narrow_band_indices.size() <= dilation_band_indices.size()); 954 | 955 | return narrow_band_indices; 956 | } 957 | 958 | //! Returns a pair of lists: 959 | //! - The first element is the set of cells closest to the boundary that 960 | //! are on the outside. 961 | //! - The second element is the set of cells closest to the boundary that 962 | //! are on the inside. 963 | //! 964 | //! One or both lists may be empty. In the case of the outside indices, the 965 | //! returned list may contain duplicates (this is not the case for the inside 966 | //! indices). 967 | //! 968 | //! All returned indices are guaranteed to be inside @a grid_size. 969 | //! 970 | //! Preconditions: 971 | //! - Every element in @a boundary_indices is inside @a grid_size. 972 | template 973 | std::pair>, 974 | std::vector>> 975 | OutsideInsideNarrowBandIndices( 976 | std::vector> const& boundary_indices, 977 | std::array const& grid_size) { 978 | auto inside_narrow_band_indices = std::vector>(); 979 | auto outside_narrow_band_indices = std::vector>(); 980 | if (boundary_indices.empty()) { 981 | return {outside_narrow_band_indices, inside_narrow_band_indices}; 982 | } 983 | 984 | // Compute connected components of boundary cells. 985 | auto const vtx_neighbor_offsets = VertexNeighborOffsets(); 986 | auto const connected_components = ConnectedComponents( 987 | boundary_indices, grid_size, begin(vtx_neighbor_offsets), 988 | std::end(vtx_neighbor_offsets)); 989 | assert(!connected_components.empty()); 990 | 991 | // Check if any connected component is contained by another. 992 | auto const connected_components_size = connected_components.size(); 993 | if (connected_components_size > 1) { 994 | auto cc_bbox = std::vector< 995 | std::pair, N>, std::size_t>>(); 996 | for (auto const& connected_component : connected_components) { 997 | auto const bbox = BoundingBox(connected_component); 998 | cc_bbox.push_back({bbox, HyperVolume(bbox)}); 999 | } 1000 | // Sort by descending area (hyper volume). 1001 | sort(std::begin(cc_bbox), std::end(cc_bbox), 1002 | [](auto const& lhs, auto const& rhs) { 1003 | return lhs.second > rhs.second; 1004 | }); 1005 | // A smaller bounding box cannot contain a larger one. 1006 | for (auto i = std::size_t{0}; i < connected_components_size; ++i) { 1007 | auto const& outer_bbox = cc_bbox[i].first; 1008 | 1009 | // A bounding box that is "flat" in one or more dimensions cannot 1010 | // contain another bounding box. 1011 | auto has_inside = true; 1012 | for (auto k = std::size_t{0}; k < N; ++k) { 1013 | if (outer_bbox[k].first == outer_bbox[k].second) { 1014 | has_inside = false; 1015 | break; 1016 | } 1017 | } 1018 | 1019 | if (has_inside) { 1020 | for (auto j = i + 1; j < connected_components_size; ++j) { 1021 | auto const& inner_bbox = cc_bbox[j].first; 1022 | if (Contains(outer_bbox, inner_bbox)) { 1023 | throw std::invalid_argument("contained component"); 1024 | } 1025 | } 1026 | } 1027 | } 1028 | } 1029 | 1030 | // Create a mask where: 1031 | // - boundary cells = 1 1032 | // - non-boundary cells = 0 1033 | auto boundary_mask_buffer = 1034 | std::vector(LinearSize(grid_size), uint8_t{0}); 1035 | auto boundary_mask_grid = Grid(grid_size, boundary_mask_buffer); 1036 | for (auto const& boundary_index : boundary_indices) { 1037 | assert(Inside(boundary_index, boundary_mask_grid.size()) && "Precondition"); 1038 | boundary_mask_grid.Cell(boundary_index) = uint8_t{1}; 1039 | } 1040 | 1041 | // Check dilation bands of connected boundary components. 1042 | // Dilation bands must be computed per connected component since each 1043 | // component has a separate outer dilation band. If we were to compute 1044 | // dilation bands for all boundary indices at once we would then need to 1045 | // do extra work to figure out if these were outer or inner dilation bands. 1046 | auto const face_neighbor_offsets = FaceNeighborOffsets(); 1047 | for (auto const& connected_component : connected_components) { 1048 | auto const dilation_bands = DilationBands( 1049 | connected_component, grid_size, begin(vtx_neighbor_offsets), 1050 | std::end(vtx_neighbor_offsets), begin(face_neighbor_offsets), 1051 | std::end(face_neighbor_offsets)); 1052 | assert(!dilation_bands.empty()); 1053 | 1054 | if (dilation_bands.size() == 1) { 1055 | // Only one dilation band means that the connected component has genus 1056 | // zero, i.e. no holes. Thus, the dilation band must define 1057 | // the outside. 1058 | // 1059 | // Note that the outer *dilation band* can never be empty, but the 1060 | // *outer narrow band* can be! The outer narrow band is empty when the 1061 | // whole border of the distance grid is boundary. 1062 | auto const& outer_dilation_band = dilation_bands.front(); 1063 | assert(!outer_dilation_band.empty()); 1064 | auto const outer_narrow_band_indices = 1065 | NarrowBandDilationBandCells(outer_dilation_band, boundary_mask_grid); 1066 | outside_narrow_band_indices.insert( 1067 | std::end(outside_narrow_band_indices), // Position. 1068 | begin(outer_narrow_band_indices), 1069 | std::end(outer_narrow_band_indices)); 1070 | } else { 1071 | // We have more than one dilation band: one outer and one or more 1072 | // inner. The outer dilation band has the largest bounding box. 1073 | // Note that when we have several dilation bands none of them can be 1074 | // empty. The reasoning is that an empty dilation band requires the 1075 | // whole distance grid to be frozen, in which case there cannot exist 1076 | // an inner area. 1077 | // 1078 | // We compute the bounding boxes in dilation grid coordinates. This is 1079 | // necessary since the entire outer dilation band may not be inside the 1080 | // distance grid. 1081 | auto dilation_band_areas = std::vector>(); 1082 | dilation_band_areas.reserve(dilation_bands.size()); 1083 | for (auto i = std::size_t{0}; i < dilation_bands.size(); ++i) { 1084 | [[maybe_unused]] auto const& dilation_band = dilation_bands[i]; 1085 | assert(!dilation_band.empty()); 1086 | dilation_band_areas.push_back( 1087 | {i, HyperVolume(BoundingBox(dilation_bands[i]))}); 1088 | } 1089 | 1090 | // Sort dilation bands by descending volume. The outer dilation band 1091 | // is then the first element. Note that the outer dilation band area 1092 | // should be strictly larger than the largest inner dilation band area 1093 | // (except in 1D). 1094 | sort(std::begin(dilation_band_areas), std::end(dilation_band_areas), 1095 | [](auto const& lhs, auto const& rhs) { 1096 | return lhs.second > rhs.second; 1097 | }); 1098 | assert(N == 1 || 1099 | dilation_band_areas[0].second > dilation_band_areas[1].second); 1100 | 1101 | // Outer dilation bands of several connected components may overlap. 1102 | // We are fine with adding an index multiple times to the outside 1103 | // narrow band. The smallest distance will be used first and the rest 1104 | // will be ignored. Worst-case we estimate distances for cells that 1105 | // are not impactful. 1106 | auto const& outer_dilation_band_indices = 1107 | dilation_bands[dilation_band_areas[0].first]; 1108 | assert(!outer_dilation_band_indices.empty()); 1109 | auto const outer_narrow_band_indices = NarrowBandDilationBandCells( 1110 | outer_dilation_band_indices, boundary_mask_grid); 1111 | assert(none_of(std::begin(outer_narrow_band_indices), 1112 | std::end(outer_narrow_band_indices), 1113 | [=](auto const& distance_grid_index) { 1114 | return !Inside(distance_grid_index, grid_size); 1115 | })); 1116 | // Note that the outer narrow band is empty when the whole border of the 1117 | // grid is frozen. 1118 | outside_narrow_band_indices.insert(end(outside_narrow_band_indices), 1119 | begin(outer_narrow_band_indices), 1120 | std::end(outer_narrow_band_indices)); 1121 | 1122 | // Inner dilation bands cannot overlap. 1123 | for (auto k = std::size_t{1}; k < dilation_band_areas.size(); ++k) { 1124 | auto const& inner_dilation_band_indices = 1125 | dilation_bands[dilation_band_areas[k].first]; 1126 | assert(!inner_dilation_band_indices.empty()); 1127 | auto const inner_narrow_band_indices = NarrowBandDilationBandCells( 1128 | inner_dilation_band_indices, boundary_mask_grid); 1129 | assert(!inner_narrow_band_indices.empty()); 1130 | assert(none_of(std::begin(inner_narrow_band_indices), 1131 | std::end(inner_narrow_band_indices), 1132 | [=](auto const& distance_grid_index) { 1133 | return !Inside(distance_grid_index, grid_size); 1134 | })); 1135 | inside_narrow_band_indices.insert(end(inside_narrow_band_indices), 1136 | begin(inner_narrow_band_indices), 1137 | std::end(inner_narrow_band_indices)); 1138 | } 1139 | } 1140 | } 1141 | 1142 | return {outside_narrow_band_indices, inside_narrow_band_indices}; 1143 | } 1144 | 1145 | //! Returns true if the (distance) value @a d is considered frozen, 1146 | //! otherwise false. 1147 | //! 1148 | //! Preconditions: 1149 | //! - @a d is not NaN. 1150 | template 1151 | bool Frozen(T const d) { 1152 | static_assert(std::is_floating_point::value, 1153 | "scalar type must be floating point"); 1154 | 1155 | assert(!std::isnan(d)); 1156 | return -std::numeric_limits::max() < d && 1157 | d < std::numeric_limits::max(); 1158 | } 1159 | 1160 | //! Set boundary times on @a time_grid. Times are multiplied by 1161 | //! @a multiplier (typically 1 or -1). 1162 | //! 1163 | //! Preconditions: 1164 | //! - Sizes of @a boundary_indices and @a boundary_times are equal. 1165 | //! 1166 | //! Throws std::invalid_argument if: 1167 | //! - The @a check_duplicate_indices is true and there is one or more 1168 | //! duplicate in @a indices. 1169 | //! - Not every element in @a boundary_indices is inside @a time_grid. 1170 | template 1171 | void SetBoundaryCondition( 1172 | std::vector> const& boundary_indices, 1173 | std::vector const& boundary_times, T const multiplier, 1174 | bool const check_duplicate_indices, Grid* const time_grid) { 1175 | assert(time_grid != nullptr); 1176 | assert(boundary_indices.size() == boundary_times.size() && "Precondition"); 1177 | 1178 | for (auto i = std::size_t{0}; i < boundary_indices.size(); ++i) { 1179 | auto const index = boundary_indices[i]; 1180 | auto const time = multiplier * boundary_times[i]; 1181 | assert(Inside(index, time_grid->size()) && "Precondition"); 1182 | 1183 | auto& time_cell = time_grid->Cell(index); 1184 | if (check_duplicate_indices) { 1185 | ThrowIfDuplicateBoundaryIndex(Frozen(time_cell), index); 1186 | } 1187 | time_cell = time; 1188 | assert(Frozen(time_cell)); 1189 | } 1190 | } 1191 | 1192 | //! Returns a (non-null) non-empty narrow band store containing estimated 1193 | //! distances for the cells in @a narrow_band_indices. Note that 1194 | //! @a narrow_band_indices may contain duplicates. 1195 | //! 1196 | //! Preconditions: 1197 | //! - Boundary condition distances have been set in @a time_grid. 1198 | //! - List of narrow band indices is not empty. 1199 | //! - Narrow band indices are inside @a time_grid. 1200 | //! - Narrow band indices are not frozen in @a time_grid. 1201 | template 1202 | std::unique_ptr> InitializedNarrowBand( 1203 | std::vector> const& narrow_band_indices, 1204 | Grid const& time_grid, E const& eikonal_solver) { 1205 | assert(!narrow_band_indices.empty() && "Precondition"); 1206 | 1207 | auto narrow_band = 1208 | std::unique_ptr>(new NarrowBandStore()); 1209 | for (auto const& narrow_band_index : narrow_band_indices) { 1210 | assert(Inside(narrow_band_index, time_grid.size()) && "Precondition"); 1211 | assert(!Frozen(time_grid.Cell(narrow_band_index)) && "Precondition"); 1212 | narrow_band->Push({eikonal_solver.Solve(narrow_band_index, time_grid), 1213 | narrow_band_index}); 1214 | } 1215 | assert(!narrow_band->empty()); 1216 | 1217 | return narrow_band; 1218 | } 1219 | 1220 | //! Compute arrival times using the @a eikonal_solver for the face-neighbors of 1221 | //! the cell at @a index. The arrival times are not written to the @a time_grid, 1222 | //! but are instead stored in the @a narrow_band. 1223 | template 1224 | void UpdateNeighbors(std::array const& index, 1225 | E const& eikonal_solver, Grid* const time_grid, 1226 | NarrowBandStore* const narrow_band) { 1227 | static_assert(N > 0, "dimensionality cannot be zero"); 1228 | static_assert(N == E::kDimension, "mismatching eikonal solver dimension"); 1229 | 1230 | assert(time_grid != nullptr); 1231 | assert(narrow_band != nullptr); 1232 | assert(Inside(index, time_grid->size())); 1233 | assert(Frozen(time_grid->Cell(index))); 1234 | 1235 | // Update the narrow band. Check face-neighbors in all dimensions. 1236 | auto const kNeighborOffsets = std::array{{-1, 1}}; 1237 | for (auto i = std::size_t{0}; i < N; ++i) { 1238 | for (auto const neighbor_offset : kNeighborOffsets) { 1239 | auto neighbor_index = index; 1240 | neighbor_index[i] += neighbor_offset; 1241 | 1242 | if (Inside(neighbor_index, time_grid->size())) { 1243 | // If the neighbor is not frozen compute a distance for it. 1244 | // Note that we don't check if there is an entry for this index 1245 | // in the narrow band already. If we happen to insert multiple 1246 | // distances for the same index the smallest one will be frozen first 1247 | // when marching and the larger distances will be ignored. 1248 | auto& distance_cell = time_grid->Cell(neighbor_index); 1249 | if (!Frozen(distance_cell)) { 1250 | narrow_band->Push({eikonal_solver.Solve(neighbor_index, *time_grid), 1251 | neighbor_index}); 1252 | } 1253 | } 1254 | } 1255 | } 1256 | } 1257 | 1258 | //! Compute distances using @a eikonal_solver for all non-frozen cells in 1259 | //! @a distance_grid that have a face-connected path to at least one of the 1260 | //! cells in @a narrow_band. 1261 | //! 1262 | //! Preconditions: 1263 | //! - @a narrow_band is not empty. 1264 | template 1265 | void MarchNarrowBand(E const& eikonal_solver, 1266 | NarrowBandStore* const narrow_band, 1267 | Grid* const time_grid) { 1268 | assert(time_grid != nullptr); 1269 | assert(narrow_band != nullptr); 1270 | assert(!narrow_band->empty() && "Precondition"); 1271 | 1272 | while (!narrow_band->empty()) { 1273 | // Take smallest time from the narrow band and freeze it, i.e. 1274 | // write it to the time grid. 1275 | auto const narrow_band_cell = narrow_band->Pop(); 1276 | auto const time = narrow_band_cell.first; 1277 | auto const index = narrow_band_cell.second; 1278 | 1279 | assert(Inside(index, time_grid->size())); 1280 | auto& time_cell = time_grid->Cell(index); 1281 | 1282 | // Since we allow multiple values for the same cell index in the 1283 | // narrow band it could happen that this grid cell has already been 1284 | // frozen. In that case just ignore subsequent values from the narrow 1285 | // band for that grid cell and move on. 1286 | if (!Frozen(time_cell)) { 1287 | time_cell = time; 1288 | assert(Frozen(time_cell)); 1289 | 1290 | // Update distances for non-frozen face-neighbors of the newly 1291 | // frozen cell. 1292 | UpdateNeighbors(index, eikonal_solver, time_grid, narrow_band); 1293 | } 1294 | } 1295 | } 1296 | 1297 | //! DOCS 1298 | //! 1299 | //! 1300 | //! Throws std::invalid_argument if: 1301 | //! - Not the same number of @a indices and @a distances, or 1302 | //! - @a indices (and @a distances) are empty, or 1303 | //! - Any index is outside the @a distance_grid, or 1304 | //! - Any duplicate in @a indices, or 1305 | //! - Any value in @a distances does not pass the @a distance_predicate test. 1306 | template 1307 | std::vector ArrivalTime( 1308 | std::array const& grid_size, 1309 | std::vector> const& boundary_indices, 1310 | std::vector const& boundary_times, 1311 | EikonalSolverType const& eikonal_solver, P const boundary_time_predicate, 1312 | bool const negative_inside) { 1313 | typedef T TimeType; 1314 | 1315 | static_assert(N >= 2, "dimensions must be >= 2"); 1316 | static_assert(N == EikonalSolverType::kDimension, 1317 | "mismatching eikonal solver dimension"); 1318 | 1319 | // Check input. 1320 | ThrowIfZeroElementInSize(grid_size); 1321 | ThrowIfEmptyBoundaryIndices(boundary_indices); 1322 | ThrowIfFullGridBoundaryIndices(boundary_indices, grid_size); 1323 | ThrowIfBoundaryIndicesTimesSizeMismatch(boundary_indices, boundary_times); 1324 | std::for_each(std::begin(boundary_indices), std::end(boundary_indices), 1325 | [=](auto const& boundary_index) { 1326 | ThrowIfBoundaryIndexOutsideGrid(boundary_index, grid_size); 1327 | }); 1328 | std::for_each(std::begin(boundary_times), std::end(boundary_times), 1329 | [=](auto const& boundary_time) { 1330 | ThrowIfInvalidBoundaryTime( 1331 | boundary_time_predicate(boundary_time), boundary_time); 1332 | }); 1333 | 1334 | auto narrow_band_indices = 1335 | OutsideInsideNarrowBandIndices(boundary_indices, grid_size); 1336 | auto const& outside_narrow_band_indices = narrow_band_indices.first; 1337 | auto const& inside_narrow_band_indices = narrow_band_indices.second; 1338 | 1339 | auto time_buffer = std::vector( 1340 | LinearSize(grid_size), std::numeric_limits::max()); 1341 | assert(std::none_of(std::begin(time_buffer), std::end(time_buffer), 1342 | [](TimeType const t) { return detail::Frozen(t); })); 1343 | auto time_grid = Grid(grid_size, time_buffer); 1344 | 1345 | if (!inside_narrow_band_indices.empty()) { 1346 | // Set boundaries for marching inside. Always check for duplicate indices. 1347 | auto const check_duplicate_indices = true; 1348 | SetBoundaryCondition( 1349 | boundary_indices, boundary_times, 1350 | TimeType{-1}, // Multiplier, negate boundary times for inside. 1351 | check_duplicate_indices, &time_grid); 1352 | 1353 | // Initialize inside narrow band with negated boundary times. 1354 | auto inside_narrow_band = InitializedNarrowBand(inside_narrow_band_indices, 1355 | time_grid, eikonal_solver); 1356 | MarchNarrowBand(eikonal_solver, inside_narrow_band.get(), &time_grid); 1357 | 1358 | if (negative_inside) { 1359 | // Negate all the inside times. Essentially, negate everything 1360 | // computed so far. Note that this also affects the boundary cells. 1361 | std::for_each(std::begin(time_buffer), std::end(time_buffer), 1362 | [](auto& t) { t = Frozen(t) ? t * TimeType{-1} : t; }); 1363 | } 1364 | } 1365 | 1366 | if (!outside_narrow_band_indices.empty()) { 1367 | // Set boundaries for marching outside. Only check for duplicate indices 1368 | // if this was not done already, i.e. if we marched an inside narrow band. 1369 | auto const check_duplicate_indices = inside_narrow_band_indices.empty(); 1370 | SetBoundaryCondition( 1371 | boundary_indices, boundary_times, 1372 | TimeType{1}, // Multiplier, original boundary distances for outside. 1373 | check_duplicate_indices, &time_grid); 1374 | 1375 | // Initialize outside narrow band with original boundary times. 1376 | auto outside_narrow_band = InitializedNarrowBand( 1377 | outside_narrow_band_indices, time_grid, eikonal_solver); 1378 | MarchNarrowBand(eikonal_solver, outside_narrow_band.get(), &time_grid); 1379 | } 1380 | 1381 | assert(all_of(std::begin(time_buffer), std::end(time_buffer), 1382 | [](TimeType const t) { return Frozen(t); })); 1383 | 1384 | return time_buffer; 1385 | } 1386 | 1387 | //! Polynomial coefficients are equivalent to std::array index, 1388 | //! i.e. Sum(q[i] * x^i) = 0, for i in [0, 2], or simpler 1389 | //! q[0] + q[1] * x + q[2] * x^2 = 0. 1390 | //! 1391 | //! Returns the largest real root, if any (otherwise a NaN value). 1392 | //! Note that we are not checking for errors here. 1393 | template 1394 | T SolveEikonalQuadratic(std::array const& q) { 1395 | static_assert(std::is_floating_point::value, 1396 | "quadratic coefficients must be floating point"); 1397 | 1398 | // No error-checking here, caller handles bad values. 1399 | auto const discriminant = q[1] * q[1] - T{4} * q[2] * q[0]; 1400 | auto const pos_root = (-q[1] + std::sqrt(discriminant)) / (T{2} * q[2]); 1401 | return pos_root; 1402 | } 1403 | 1404 | //! Solve the eikonal equation to get the arrival time (which is distance when 1405 | //! @a speed is one) at @a index. 1406 | //! 1407 | //! The returned value is guaranteed to be positive. 1408 | //! 1409 | //! Preconditions: 1410 | //! - @a speed must be greater than zero. 1411 | //! - All elements of @a grid_spacing must be greater than zero. 1412 | //! - @a index is inside @a distance_grid. 1413 | //! - The cell at @a index must not be frozen in @a distance_grid. 1414 | //! - There must be at least one cell in @a distance_grid that is a 1415 | //! frozen face-neighbor of @a index. 1416 | //! - Cells in @a distance_grid that are not frozen must have the value 1417 | //! std::numeric_limits::max(). 1418 | template 1419 | T SolveEikonal(std::array const& index, 1420 | Grid const& distance_grid, T const speed, 1421 | std::array const& grid_spacing) { 1422 | static_assert(std::is_floating_point::value, 1423 | "scalar type must be floating point"); 1424 | 1425 | assert(ValidSpeed(speed) && "Precondition"); 1426 | assert(ValidGridSpacing(grid_spacing) && "Precondition"); 1427 | assert(Inside(index, distance_grid.size()) && "Precondition"); 1428 | assert(!Frozen(distance_grid.Cell(index)) && "Precondition"); 1429 | 1430 | // Find the smallest frozen neighbor (if any) in each dimension. 1431 | auto frozen_neighbor_distances = std::array, N>(); 1432 | auto frozen_neighbor_distances_count = std::size_t{0}; 1433 | for (auto i = std::size_t{0}; i < N; ++i) { 1434 | auto neighbor_min_distance = std::numeric_limits::max(); 1435 | assert(!Frozen(neighbor_min_distance)); 1436 | 1437 | // Find the smallest face neighbor for this dimension. 1438 | auto neighbor_index = index; 1439 | 1440 | // -1 1441 | neighbor_index[i] -= int32_t{1}; 1442 | if (Inside(neighbor_index, distance_grid.size())) { 1443 | // Note that if the neighbor is not frozen it will have the default 1444 | // distance std::numeric_limits::max(). 1445 | auto const neighbor_distance = distance_grid.Cell(neighbor_index); 1446 | if (neighbor_distance < neighbor_min_distance) { 1447 | neighbor_min_distance = neighbor_distance; 1448 | assert(Frozen(neighbor_min_distance)); 1449 | } 1450 | } 1451 | 1452 | // +1 1453 | neighbor_index[i] += int32_t{2}; // -1 + 2 = 1 1454 | if (Inside(neighbor_index, distance_grid.size())) { 1455 | // Note that if the neighbor is not frozen it will have the default 1456 | // distance std::numeric_limits::max(). 1457 | auto const neighbor_distance = distance_grid.Cell(neighbor_index); 1458 | if (neighbor_distance < neighbor_min_distance) { 1459 | neighbor_min_distance = neighbor_distance; 1460 | assert(Frozen(neighbor_min_distance)); 1461 | } 1462 | } 1463 | 1464 | // If no frozen neighbor was found that dimension does not contribute 1465 | // to the arrival time. 1466 | if (neighbor_min_distance < std::numeric_limits::max()) { 1467 | frozen_neighbor_distances[frozen_neighbor_distances_count++] = { 1468 | neighbor_min_distance, i}; 1469 | } 1470 | } 1471 | assert(frozen_neighbor_distances_count > std::size_t{0} && "Precondition"); 1472 | 1473 | // Define and intiailise q out of if/else for error reporting convenience 1474 | std::array q = std::array{{T{0}, T{0}, T{0}}}; 1475 | 1476 | auto arrival_time = std::numeric_limits::quiet_NaN(); 1477 | if (frozen_neighbor_distances_count == 1) { 1478 | // If frozen neighbor in only one dimension we don't need to solve a 1479 | // quadratic. 1480 | auto const distance = frozen_neighbor_distances[0].first; 1481 | auto const j = frozen_neighbor_distances[0].second; 1482 | arrival_time = distance + grid_spacing[j] / speed; 1483 | } else { 1484 | // Initialize quadratic coefficients. 1485 | // auto q = std::array{{T{-1} / Squared(speed), T{0}, T{0}}}; 1486 | q[0] = T{-1} / Squared(speed); 1487 | auto const inverse_squared_grid_spacing = InverseSquared(grid_spacing); 1488 | for (auto i = std::size_t{0}; i < frozen_neighbor_distances_count; ++i) { 1489 | auto const distance = frozen_neighbor_distances[i].first; 1490 | auto const j = frozen_neighbor_distances[i].second; 1491 | auto const alpha = inverse_squared_grid_spacing[j]; 1492 | q[0] += Squared(distance) * alpha; 1493 | q[1] += T{-2} * distance * alpha; 1494 | q[2] += alpha; 1495 | } 1496 | arrival_time = SolveEikonalQuadratic(q); 1497 | 1498 | // Fallback when arrival time is negative or NaN. 1499 | if (use_eikonal_fallback && !(arrival_time >= T{0})) { 1500 | // In case the discriminant is negative, we revert to the smallest 1501 | // distance to a single neighbor 1502 | // TODO if dimension N>=3 we could try to find the smallest dimension 1503 | // for the Eikonal equations in dimension N-1 1504 | auto distance = frozen_neighbor_distances[0].first; 1505 | auto j = frozen_neighbor_distances[0].second; 1506 | arrival_time = distance + grid_spacing[j] / speed; 1507 | for (auto i = std::size_t{1}; i < frozen_neighbor_distances_count; ++i) { 1508 | distance = frozen_neighbor_distances[i].first; 1509 | j = frozen_neighbor_distances[i].second; 1510 | arrival_time = 1511 | std::min(arrival_time, distance + grid_spacing[j] / speed); 1512 | } 1513 | } 1514 | } 1515 | 1516 | ThrowIfInvalidArrivalTimeWithDetails( 1517 | arrival_time, index, speed, q, distance_grid.Cell(index), 1518 | frozen_neighbor_distances, frozen_neighbor_distances_count); 1519 | return arrival_time; 1520 | } 1521 | 1522 | //! Solve the eikonal equation to get the arrival time (which is distance when 1523 | //! @a speed is one) at @a index. Uses second order derivatives where possible, 1524 | //! this version is slower but more accurate. However, it is important to 1525 | //! have good boundary conditions that allow second order derivatives to be 1526 | //! used early when marching. Otherwise early errors will be propagated. 1527 | //! 1528 | //! The returned value is guaranteed to be positive. 1529 | //! 1530 | //! Preconditions: 1531 | //! - @a speed must be greater than zero. 1532 | //! - All elements of @a grid_spacing must be greater than zero. 1533 | //! - @a index is inside @a distance_grid. 1534 | //! - The cell at @a index must not be frozen in @a distance_grid. 1535 | //! - There must be at least one cell in @a distance_grid that is a 1536 | //! frozen face-neighbor of @a index. 1537 | //! - Cells in @a distance_grid that are not frozen must have the value 1538 | //! std::numeric_limits::max(). 1539 | template 1540 | T HighAccuracySolveEikonal(std::array const& index, 1541 | Grid const& distance_grid, T const speed, 1542 | std::array const& grid_spacing) { 1543 | static_assert(std::is_floating_point::value, 1544 | "scalar type must be floating point"); 1545 | 1546 | assert(ValidSpeed(speed) && "Precondition"); 1547 | assert(ValidGridSpacing(grid_spacing) && "Precondition"); 1548 | assert(Inside(index, distance_grid.size()) && "Precondition"); 1549 | assert(!Frozen(distance_grid.Cell(index)) && "Precondition"); 1550 | 1551 | // Find the smallest frozen neighbor(s) (if any) in each dimension. 1552 | auto const neighbor_offsets = std::array{{-1, 1}}; 1553 | auto frozen_neighbor_distances = 1554 | std::array, std::size_t>, N>(); 1555 | auto frozen_neighbor_distances_count = std::size_t{0}; 1556 | for (auto i = std::size_t{0}; i < N; ++i) { 1557 | auto neighbor_min_distance = std::numeric_limits::max(); 1558 | auto neighbor_min_distance2 = std::numeric_limits::max(); 1559 | assert(!Frozen(neighbor_min_distance)); 1560 | assert(!Frozen(neighbor_min_distance2)); 1561 | 1562 | // Check neighbors in both directions for this dimenion. 1563 | for (auto const neighbor_offset : neighbor_offsets) { 1564 | auto neighbor_index = index; 1565 | neighbor_index[i] += neighbor_offset; 1566 | if (Inside(neighbor_index, distance_grid.size())) { 1567 | auto const neighbor_distance = distance_grid.Cell(neighbor_index); 1568 | if (neighbor_distance < neighbor_min_distance) { 1569 | // Neighbor one step away is frozen. 1570 | assert(Frozen(neighbor_distance)); 1571 | neighbor_min_distance = neighbor_distance; 1572 | 1573 | // Check if neighbor two steps away is frozen and has smaller 1574 | // (or equal) distance than neighbor one step away. Reset 1575 | // the distance first since otherwise we might get the secondary 1576 | // distance from the previous neighbor offset. 1577 | neighbor_min_distance2 = std::numeric_limits::max(); 1578 | auto neighbor_index2 = neighbor_index; 1579 | neighbor_index2[i] += neighbor_offset; 1580 | if (Inside(neighbor_index2, distance_grid.size())) { 1581 | auto const neighbor_distance2 = distance_grid.Cell(neighbor_index2); 1582 | if (neighbor_distance2 <= neighbor_distance) { 1583 | // Neighbor index two steps away is frozen. 1584 | assert(Frozen(neighbor_distance2)); 1585 | neighbor_min_distance2 = neighbor_distance2; 1586 | } 1587 | } 1588 | } 1589 | } 1590 | } 1591 | 1592 | if (neighbor_min_distance2 < std::numeric_limits::max()) { 1593 | // Two frozen neighbors in this dimension. 1594 | assert(neighbor_min_distance < std::numeric_limits::max()); 1595 | frozen_neighbor_distances[frozen_neighbor_distances_count++] = { 1596 | {neighbor_min_distance, neighbor_min_distance2}, i}; 1597 | } else if (neighbor_min_distance < std::numeric_limits::max()) { 1598 | // One frozen neighbor in this dimension. 1599 | frozen_neighbor_distances[frozen_neighbor_distances_count++] = { 1600 | {neighbor_min_distance, std::numeric_limits::max()}, i}; 1601 | } 1602 | // else: no frozen neighbors in this dimension. 1603 | } 1604 | assert(frozen_neighbor_distances_count > std::size_t{0} && "Precondition"); 1605 | 1606 | // Define and intiailise q out of if/else for error reporting convenience 1607 | std::array q = std::array{{T{0}, T{0}, T{0}}}; 1608 | 1609 | auto arrival_time = std::numeric_limits::quiet_NaN(); 1610 | if (frozen_neighbor_distances_count == 1) { 1611 | // If frozen neighbor in only one dimension we don't need to solve a 1612 | // quadratic. 1613 | auto const distance = frozen_neighbor_distances[0].first.first; 1614 | auto const j = frozen_neighbor_distances[0].second; 1615 | arrival_time = distance + grid_spacing[j] / speed; 1616 | } else { 1617 | // Initialize quadratic coefficients. 1618 | // auto q = std::array{{T{-1} / Squared(speed), T{0}, T{0}}}; 1619 | q[0] = T{-1} / Squared(speed); 1620 | auto const inverse_squared_grid_spacing = InverseSquared(grid_spacing); 1621 | 1622 | for (auto i = std::size_t{0}; i < frozen_neighbor_distances_count; ++i) { 1623 | auto const distance = frozen_neighbor_distances[i].first.first; 1624 | auto const distance2 = frozen_neighbor_distances[i].first.second; 1625 | auto const j = frozen_neighbor_distances[i].second; 1626 | if (distance2 < std::numeric_limits::max()) { 1627 | // Second order coefficients. 1628 | assert(distance < std::numeric_limits::max()); 1629 | auto const alpha = (T{9} / T{4}) * inverse_squared_grid_spacing[j]; 1630 | auto const t = (T{1} / T{3}) * (T{4} * distance - distance2); 1631 | q[0] += Squared(t) * alpha; 1632 | q[1] += T{-2} * t * alpha; 1633 | q[2] += alpha; 1634 | } else if (distance < std::numeric_limits::max()) { 1635 | // First order coefficients. 1636 | auto const alpha = inverse_squared_grid_spacing[j]; 1637 | q[0] += Squared(distance) * alpha; 1638 | q[1] += T{-2} * distance * alpha; 1639 | q[2] += alpha; 1640 | } 1641 | } 1642 | arrival_time = SolveEikonalQuadratic(q); 1643 | 1644 | // Fallback when arrival time is negative or NaN. 1645 | if (use_eikonal_fallback && !(arrival_time >= T{0})) { 1646 | // In case the discriminant is negative, we revert to the smallest 1647 | // distance to a single neighbor 1648 | // TODO if dimension N>=3 we could try to find the smallest dimension 1649 | // for the Eikonal equations in dimension N-1 1650 | auto distance = frozen_neighbor_distances[0].first.first; 1651 | auto j = frozen_neighbor_distances[0].second; 1652 | arrival_time = distance + grid_spacing[j] / speed; 1653 | for (auto i = std::size_t{1}; i < frozen_neighbor_distances_count; ++i) { 1654 | distance = frozen_neighbor_distances[i].first.first; 1655 | j = frozen_neighbor_distances[i].second; 1656 | arrival_time = 1657 | std::min(arrival_time, distance + grid_spacing[j] / speed); 1658 | } 1659 | } 1660 | } 1661 | 1662 | ThrowIfInvalidArrivalTimeWithHighAccuracyDetails( 1663 | arrival_time, index, speed, q, distance_grid.Cell(index), 1664 | frozen_neighbor_distances, frozen_neighbor_distances_count); 1665 | return arrival_time; 1666 | } 1667 | 1668 | //! 1669 | //! 1670 | //! Implementation follows pseudo-code given in "Fluid Simulation for 1671 | //! Computer Graphics" by Robert Bridson. 1672 | //! 1673 | //! Preconditions: 1674 | //! - @a dx must be greater than zero. 1675 | //! - @a index is inside @a distance_grid. 1676 | //! - The cell at @a index must not be frozen in @a distance_grid. 1677 | //! - There must be at least one cell in @a distance_grid that is a 1678 | //! frozen face-neighbor of @a index. 1679 | //! - Cells in @a distance_grid that are not frozen must have the value 1680 | //! std::numeric_limits::max(). 1681 | //! 1682 | //! Note: Currently supports only uniformly spaced (square) cells. 1683 | //! Note: Currently supports only 1D, 2D, and 3D. 1684 | template 1685 | T SolveDistance(std::array const& index, 1686 | Grid const& distance_grid, T const dx) { 1687 | static_assert(1 <= N && N <= 3, "invalid dimensionality"); 1688 | 1689 | assert(Inside(index, distance_grid.size()) && "Precondition"); 1690 | assert(!Frozen(distance_grid.Cell(index)) && "Precondition"); 1691 | 1692 | auto phi = std::array(); 1693 | std::fill(std::begin(phi), std::end(phi), std::numeric_limits::max()); 1694 | auto phi_count = std::size_t{0}; 1695 | 1696 | // Find the smallest frozen neighbor(s) (if any) in each dimension. 1697 | for (auto i = std::size_t{0}; i < N; ++i) { 1698 | auto neighbor_min_distance = std::numeric_limits::max(); 1699 | assert(!Frozen(neighbor_min_distance)); 1700 | 1701 | // -1 1702 | auto neighbor_index = index; 1703 | neighbor_index[i] -= 1; 1704 | if (Inside(neighbor_index, distance_grid.size())) { 1705 | auto const neighbor_distance = distance_grid.Cell(neighbor_index); 1706 | if (neighbor_distance < neighbor_min_distance) { 1707 | neighbor_min_distance = neighbor_distance; 1708 | } 1709 | } 1710 | 1711 | // -1 + 2 = +1 1712 | neighbor_index[i] += 2; 1713 | if (Inside(neighbor_index, distance_grid.size())) { 1714 | auto const neighbor_distance = distance_grid.Cell(neighbor_index); 1715 | if (neighbor_distance < neighbor_min_distance) { 1716 | neighbor_min_distance = neighbor_distance; 1717 | } 1718 | } 1719 | 1720 | if (neighbor_min_distance < std::numeric_limits::max()) { 1721 | phi[phi_count++] = neighbor_min_distance; 1722 | } 1723 | } 1724 | assert(phi_count > 0 && "Precondition"); 1725 | 1726 | // Sort ascending using a sorting network approach. 1727 | if (N >= 2 && phi[0] > phi[1]) { 1728 | std::swap(phi[0], phi[1]); 1729 | } 1730 | if (N == 3 && phi[1] > phi[2]) { 1731 | std::swap(phi[1], phi[2]); 1732 | } 1733 | if (N == 3 && phi[0] > phi[1]) { 1734 | std::swap(phi[0], phi[1]); 1735 | } 1736 | 1737 | auto distance = phi[0] + dx; 1738 | if (N >= 2 && phi_count > 1 && distance > phi[1]) { 1739 | distance = 1740 | T(0.5) * (phi[0] + phi[1] + 1741 | std::sqrt(T(2) * Squared(dx) - Squared(phi[1] - phi[0]))); 1742 | if (N == 3 && phi_count == 3 && distance > phi[2]) { 1743 | auto const phi_sum = phi[0] + phi[1] + phi[2]; 1744 | auto phi_sum_squared = 1745 | Squared(phi[0]) + Squared(phi[1]) + Squared(phi[2]); 1746 | distance = 1747 | (T(1) / T(3)) * 1748 | (phi_sum + 1749 | std::sqrt(std::max(T(0), Squared(phi_sum) - T(3) * (phi_sum_squared - 1750 | Squared(dx))))); 1751 | } 1752 | } 1753 | 1754 | // Arrival time is distance here since we have assumed that speed is one. 1755 | ThrowIfInvalidArrivalTime(distance, index); 1756 | return distance; 1757 | } 1758 | 1759 | //! Base class for Eikonal solvers. 1760 | //! Note: dtor is not virtual! 1761 | template 1762 | class EikonalSolverBase { 1763 | public: 1764 | typedef T ScalarType; 1765 | static std::size_t const kDimension = N; 1766 | static bool const allowEikonalFallback = use_eikonal_fallback; 1767 | 1768 | protected: 1769 | explicit EikonalSolverBase(std::array const& grid_spacing) 1770 | : grid_spacing_(grid_spacing) { 1771 | ThrowIfInvalidGridSpacing(grid_spacing_); 1772 | } 1773 | 1774 | std::array const& grid_spacing() const { return grid_spacing_; } 1775 | 1776 | private: 1777 | std::array const grid_spacing_; 1778 | }; 1779 | 1780 | //! Base class for Eikonal solvers with uniform speed. 1781 | //! Note: dtor is not virtual! 1782 | template 1783 | class UniformSpeedEikonalSolverBase 1784 | : public EikonalSolverBase { 1785 | protected: 1786 | UniformSpeedEikonalSolverBase(std::array const& grid_spacing, 1787 | T const uniform_speed) 1788 | : EikonalSolverBase(grid_spacing), 1789 | uniform_speed_(uniform_speed) { 1790 | ThrowIfZeroOrNegativeOrNanSpeed(uniform_speed_); 1791 | } 1792 | 1793 | //! Returns the uniform speed, guaranteed to be: 1794 | //! - Non-zero 1795 | //! - Positive 1796 | //! - Not NaN 1797 | T uniform_speed() const { return uniform_speed_; } 1798 | 1799 | private: 1800 | T const uniform_speed_; 1801 | }; 1802 | 1803 | //! Base class for Eikonal solvers with varying speed. 1804 | //! Note: dtor is not virtual! 1805 | template 1806 | class VaryingSpeedEikonalSolverBase 1807 | : public EikonalSolverBase { 1808 | protected: 1809 | VaryingSpeedEikonalSolverBase( 1810 | std::array const& grid_spacing, 1811 | std::array const& speed_grid_size, 1812 | std::vector const& speed_buffer) 1813 | : EikonalSolverBase(grid_spacing), 1814 | speed_grid_(speed_grid_size, speed_buffer) { 1815 | for (auto const speed : speed_buffer) { 1816 | ThrowIfZeroOrNegativeOrNanSpeed(speed); 1817 | } 1818 | } 1819 | 1820 | //! Returns the speed at @a index in the speed grid, guaranteed to be: 1821 | //! - Non-zero 1822 | //! - Positive 1823 | //! - Not NaN 1824 | //! 1825 | //! Throws an std::invalid_argument exception if @a index is outside the 1826 | //! speed grid. 1827 | T Speed(std::array const& index) const { 1828 | ThrowIfSpeedIndexOutsideGrid(index, speed_grid_.size()); 1829 | return speed_grid_.Cell(index); 1830 | } 1831 | 1832 | private: 1833 | Grid const speed_grid_; 1834 | }; 1835 | 1836 | } // namespace detail 1837 | 1838 | //! Provides methods for solving the eikonal equation for a single grid cell 1839 | //! at a time using the current distance grid. Uses a uniform speed for 1840 | //! the entire grid. 1841 | template 1842 | class UniformSpeedEikonalSolver 1843 | : public detail::UniformSpeedEikonalSolverBase { 1844 | public: 1845 | explicit UniformSpeedEikonalSolver(std::array const& grid_spacing, 1846 | T const uniform_speed = T{1}) 1847 | : detail::UniformSpeedEikonalSolverBase( 1848 | grid_spacing, uniform_speed) {} 1849 | 1850 | //! Returns the distance for grid cell at @a index given the current 1851 | //! distances (@a distance_grid) of other cells. 1852 | T Solve(std::array const& index, 1853 | detail::Grid const& distance_grid) const { 1854 | return detail::SolveEikonal( 1855 | index, distance_grid, this->uniform_speed(), this->grid_spacing()); 1856 | } 1857 | }; 1858 | 1859 | //! Provides methods for solving the eikonal equation for a single grid cell 1860 | //! at a time using the current distance grid. Uses a uniform speed for 1861 | //! the entire grid. When possible uses second order derivates to achieve 1862 | //! better accuracy. 1863 | template 1864 | class HighAccuracyUniformSpeedEikonalSolver 1865 | : public detail::UniformSpeedEikonalSolverBase { 1866 | public: 1867 | explicit HighAccuracyUniformSpeedEikonalSolver( 1868 | std::array const& grid_spacing, T const uniform_speed = T{1}) 1869 | : detail::UniformSpeedEikonalSolverBase( 1870 | grid_spacing, uniform_speed) {} 1871 | 1872 | //! Returns the distance for grid cell at @a index given the current 1873 | //! distances (@a distance_grid) of other cells. 1874 | T Solve(std::array const& index, 1875 | detail::Grid const& distance_grid) const { 1876 | return detail::HighAccuracySolveEikonal( 1877 | index, distance_grid, this->uniform_speed(), this->grid_spacing()); 1878 | } 1879 | }; 1880 | 1881 | //! Provides methods for solving the eikonal equation for a single grid cell 1882 | //! at a time using the current distance grid. A speed grid must be provided 1883 | //! and that grid must cover the arrival time grid. 1884 | template 1885 | class VaryingSpeedEikonalSolver 1886 | : public detail::VaryingSpeedEikonalSolverBase { 1887 | public: 1888 | VaryingSpeedEikonalSolver(std::array const& grid_spacing, 1889 | std::array const& speed_grid_size, 1890 | std::vector const& speed_buffer) 1891 | : detail::VaryingSpeedEikonalSolverBase( 1892 | grid_spacing, speed_grid_size, speed_buffer) {} 1893 | 1894 | //! Returns the distance for grid cell at @a index given the current 1895 | //! distances (@a distance_grid) of other cells. 1896 | T Solve(std::array const& index, 1897 | detail::Grid const& distance_grid) const { 1898 | return detail::SolveEikonal( 1899 | index, distance_grid, this->Speed(index), this->grid_spacing()); 1900 | } 1901 | }; 1902 | 1903 | //! Provides methods for solving the eikonal equation for a single grid cell 1904 | //! at a time using the current distance grid. A speed grid must be provided 1905 | //! and that grid must cover the arrival time grid. When possible uses second 1906 | //! order derivates to achieve better accuracy. 1907 | template 1908 | class HighAccuracyVaryingSpeedEikonalSolver 1909 | : public detail::VaryingSpeedEikonalSolverBase { 1910 | public: 1911 | HighAccuracyVaryingSpeedEikonalSolver( 1912 | std::array const& grid_spacing, 1913 | std::array const& speed_grid_size, 1914 | std::vector const& speed_buffer) 1915 | : detail::VaryingSpeedEikonalSolverBase( 1916 | grid_spacing, speed_grid_size, speed_buffer) {} 1917 | 1918 | //! Returns the distance for grid cell at @a index given the current 1919 | //! distances (@a distance_grid) of other cells. 1920 | T Solve(std::array const& index, 1921 | detail::Grid const& distance_grid) const { 1922 | return detail::HighAccuracySolveEikonal( 1923 | index, distance_grid, this->Speed(index), this->grid_spacing()); 1924 | } 1925 | }; 1926 | 1927 | //! Provides methods for solving the eikonal equation for a single grid cell 1928 | //! at a time using the current distance grid. The speed is assumed to be 1929 | //! one for the entire grid, meaning that arrival time can be interpreted 1930 | //! as distance. 1931 | //! 1932 | //! Note: Currently only supports uniform grid spacing. 1933 | template 1934 | class DistanceSolver { 1935 | public: 1936 | typedef T ScalarType; 1937 | static std::size_t const kDimension = N; 1938 | 1939 | explicit DistanceSolver(T const dx) : dx_(dx) { 1940 | detail::ThrowIfInvalidGridSpacing(detail::FilledArray(dx_)); 1941 | } 1942 | 1943 | //! Returns the distance for grid cell at @a index given the current 1944 | //! distances (@a distance_grid) of other cells. 1945 | T Solve(std::array const& index, 1946 | detail::Grid const& distance_grid) const { 1947 | return detail::SolveDistance(index, distance_grid, dx_); 1948 | } 1949 | 1950 | private: 1951 | T const dx_; 1952 | }; 1953 | 1954 | //! Compute the signed distance on a grid. 1955 | //! 1956 | //! Input: 1957 | //! grid_size - Number of grid cells in each dimension. 1958 | //! boundary_indices - Integer coordinates of cells with provided distances. 1959 | //! boundary_distances - Signed distances assigned to boundary cells. 1960 | //! 1961 | //! Preconditions: 1962 | //! - grid_size may not have a zero element. 1963 | //! - frozen_indices, frozen_distances and normals must have the same size. 1964 | //! - frozen_indices must all be within size. 1965 | //! 1966 | //! TODO - example usage! 1967 | template 1968 | std::vector SignedArrivalTime( 1969 | std::array const& grid_size, 1970 | std::vector> const& boundary_indices, 1971 | std::vector const& boundary_times, 1972 | EikonalSolverType const& eikonal_solver) { 1973 | auto const boundary_time_predicate = [](auto const t) { 1974 | return !std::isnan(t) && detail::Frozen(t); 1975 | }; 1976 | auto constexpr negative_inside = true; 1977 | return detail::ArrivalTime(grid_size, boundary_indices, boundary_times, 1978 | eikonal_solver, boundary_time_predicate, 1979 | negative_inside); 1980 | } 1981 | 1982 | } // namespace fast_marching_method 1983 | } // namespace thinks 1984 | 1985 | #endif // INCLUDE_THINKS_FAST_MARCHING_METHOD_FAST_MARCHING_METHOD_HPP_ 1986 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | FetchContent_Declare( 3 | googletest 4 | GIT_REPOSITORY https://github.com/google/googletest.git 5 | GIT_TAG 750d67d809700ae8fca6d610f7b41b71aa161808 6 | SYSTEM 7 | ) 8 | # For Windows: Prevent overriding the parent project's compiler/linker settings 9 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 10 | FetchContent_MakeAvailable(googletest) 11 | 12 | set_target_properties(gtest PROPERTIES CXX_CLANG_TIDY "") 13 | set_target_properties(gtest_main PROPERTIES CXX_CLANG_TIDY "") 14 | set_target_properties(gmock PROPERTIES CXX_CLANG_TIDY "") 15 | set_target_properties(gmock_main PROPERTIES CXX_CLANG_TIDY "") 16 | 17 | add_executable(fast-marching-method-test 18 | main.cpp 19 | eikonal_solvers_test.cpp 20 | signed_arrival_time_test.cpp) 21 | 22 | target_link_libraries(fast-marching-method-test PRIVATE GTest::gtest GTest::gtest_main) 23 | 24 | add_test(NAME fast-marching-method-test COMMAND fast-marching-method-test) 25 | 26 | # Python bindings test 27 | add_test(NAME py-bindings-test 28 | COMMAND ${PYTHON_EXECUTABLE} py-bindings-test.py 29 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 30 | 31 | set_tests_properties(py-bindings-test PROPERTIES 32 | ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${CMAKE_BINARY_DIR}/bindings/python") -------------------------------------------------------------------------------- /tests/eikonal_solvers_test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #include 22 | 23 | #include "../include/thinks/fast_marching_method/fast_marching_method.hpp" 24 | #include "./util.hpp" 25 | 26 | namespace { 27 | 28 | // Fixtures. 29 | 30 | template 31 | class UniformSpeedEikonalSolverTest : public ::testing::Test { 32 | protected: 33 | virtual ~UniformSpeedEikonalSolverTest() {} 34 | }; 35 | 36 | template 37 | class HighAccuracyUniformSpeedEikonalSolverTest : public ::testing::Test { 38 | protected: 39 | virtual ~HighAccuracyUniformSpeedEikonalSolverTest() {} 40 | }; 41 | 42 | template 43 | class VaryingSpeedEikonalSolverTest : public ::testing::Test { 44 | protected: 45 | virtual ~VaryingSpeedEikonalSolverTest() {} 46 | }; 47 | 48 | template 49 | class HighAccuracyVaryingSpeedEikonalSolverTest : public ::testing::Test { 50 | protected: 51 | virtual ~HighAccuracyVaryingSpeedEikonalSolverTest() {} 52 | }; 53 | 54 | template 55 | class DistanceSolverTest : public ::testing::Test { 56 | protected: 57 | virtual ~DistanceSolverTest() {} 58 | }; 59 | 60 | // Associate types with fixtures. 61 | 62 | typedef ::testing::Types< 63 | util::ScalarDimensionPair, util::ScalarDimensionPair, 64 | util::ScalarDimensionPair, util::ScalarDimensionPair, 65 | util::ScalarDimensionPair, util::ScalarDimensionPair> 66 | EikonalSolverTypes; 67 | 68 | TYPED_TEST_SUITE(UniformSpeedEikonalSolverTest, EikonalSolverTypes); 69 | TYPED_TEST_SUITE(HighAccuracyUniformSpeedEikonalSolverTest, 70 | EikonalSolverTypes); 71 | TYPED_TEST_SUITE(VaryingSpeedEikonalSolverTest, EikonalSolverTypes); 72 | TYPED_TEST_SUITE(HighAccuracyVaryingSpeedEikonalSolverTest, 73 | EikonalSolverTypes); 74 | TYPED_TEST_SUITE(DistanceSolverTest, EikonalSolverTypes); 75 | 76 | // UniformSpeedEikonalSolverTest fixture. 77 | 78 | TYPED_TEST(UniformSpeedEikonalSolverTest, InvalidGridSpacingThrows) { 79 | typedef typename TypeParam::ScalarType ScalarType; 80 | static constexpr std::size_t kDimension = TypeParam::kDimension; 81 | namespace fmm = thinks::fast_marching_method; 82 | typedef fmm::UniformSpeedEikonalSolver 83 | EikonalSolverType; 84 | 85 | // Arrange. 86 | auto const invalid_grid_spacing_elements = std::array{ 87 | {ScalarType{0}, ScalarType{-1}, 88 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 89 | 90 | for (auto const invalid_grid_spacing_element : 91 | invalid_grid_spacing_elements) { 92 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 93 | auto grid_spacing = util::FilledArray(ScalarType{1}); 94 | grid_spacing[i] = invalid_grid_spacing_element; // Invalid i'th element. 95 | auto const speed = ScalarType{1}; 96 | 97 | auto expected_reason = std::stringstream(); 98 | expected_reason << "invalid grid spacing: " 99 | << util::ToString(grid_spacing); 100 | 101 | // Act. 102 | auto const ft = util::FunctionThrows([=]() { 103 | [[maybe_unused]] auto const eikonal_solver = 104 | EikonalSolverType(grid_spacing, speed); 105 | // (void)eikonal_solver; // pre-C++11 106 | }); 107 | 108 | // Assert. 109 | ASSERT_TRUE(ft.first); 110 | ASSERT_EQ(expected_reason.str(), ft.second); 111 | } 112 | } 113 | } 114 | 115 | TYPED_TEST(UniformSpeedEikonalSolverTest, InvalidSpeedThrows) { 116 | typedef typename TypeParam::ScalarType ScalarType; 117 | static constexpr std::size_t kDimension = TypeParam::kDimension; 118 | namespace fmm = thinks::fast_marching_method; 119 | typedef fmm::UniformSpeedEikonalSolver 120 | EikonalSolverType; 121 | 122 | // Arrange. 123 | auto const invalid_speeds = std::array{ 124 | {ScalarType{0}, ScalarType{-1}, 125 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 126 | 127 | for (auto const invalid_speed : invalid_speeds) { 128 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 129 | auto const speed = invalid_speed; // Invalid speed! 130 | 131 | auto expected_reason = std::stringstream(); 132 | expected_reason << "invalid speed: " << speed; 133 | 134 | // Act. 135 | auto const ft = util::FunctionThrows([=]() { 136 | [[maybe_unused]] auto const eikonal_solver = 137 | EikonalSolverType(grid_spacing, speed); 138 | }); 139 | 140 | // Assert. 141 | ASSERT_TRUE(ft.first); 142 | ASSERT_EQ(expected_reason.str(), ft.second); 143 | } 144 | } 145 | 146 | // HighAccuracyUniformSpeedEikonalSolverTest fixture. 147 | 148 | TYPED_TEST(HighAccuracyUniformSpeedEikonalSolverTest, 149 | InvalidGridSpacingThrows) { 150 | typedef typename TypeParam::ScalarType ScalarType; 151 | static constexpr std::size_t kDimension = TypeParam::kDimension; 152 | namespace fmm = thinks::fast_marching_method; 153 | typedef fmm::HighAccuracyUniformSpeedEikonalSolver 154 | EikonalSolverType; 155 | 156 | // Arrange. 157 | auto const invalid_grid_spacing_elements = std::array{ 158 | {ScalarType{0}, ScalarType{-1}, 159 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 160 | 161 | for (auto const invalid_grid_spacing_element : 162 | invalid_grid_spacing_elements) { 163 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 164 | auto grid_spacing = util::FilledArray(ScalarType{1}); 165 | grid_spacing[i] = invalid_grid_spacing_element; // Invalid i'th element. 166 | auto const speed = ScalarType{1}; 167 | 168 | auto expected_reason = std::stringstream(); 169 | expected_reason << "invalid grid spacing: " 170 | << util::ToString(grid_spacing); 171 | 172 | // Act. 173 | auto const ft = util::FunctionThrows([=]() { 174 | [[maybe_unused]] auto const eikonal_solver = 175 | EikonalSolverType(grid_spacing, speed); 176 | }); 177 | 178 | // Assert. 179 | ASSERT_TRUE(ft.first); 180 | ASSERT_EQ(expected_reason.str(), ft.second); 181 | } 182 | } 183 | } 184 | 185 | TYPED_TEST(HighAccuracyUniformSpeedEikonalSolverTest, InvalidSpeedThrows) { 186 | typedef typename TypeParam::ScalarType ScalarType; 187 | static constexpr std::size_t kDimension = TypeParam::kDimension; 188 | namespace fmm = thinks::fast_marching_method; 189 | typedef fmm::HighAccuracyUniformSpeedEikonalSolver 190 | EikonalSolverType; 191 | 192 | // Arrange. 193 | auto const invalid_speeds = std::array{ 194 | {ScalarType{0}, ScalarType{-1}, 195 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 196 | 197 | for (auto const invalid_speed : invalid_speeds) { 198 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 199 | auto const speed = invalid_speed; // Invalid speed! 200 | 201 | auto expected_reason = std::stringstream(); 202 | expected_reason << "invalid speed: " << speed; 203 | 204 | // Act. 205 | auto const ft = util::FunctionThrows([=]() { 206 | [[maybe_unused]] auto const eikonal_solver = 207 | EikonalSolverType(grid_spacing, speed); 208 | }); 209 | 210 | // Assert. 211 | ASSERT_TRUE(ft.first); 212 | ASSERT_EQ(expected_reason.str(), ft.second); 213 | } 214 | } 215 | 216 | // VaryingSpeedEikonalSolverTest fixture. 217 | 218 | TYPED_TEST(VaryingSpeedEikonalSolverTest, InvalidGridSpacingThrows) { 219 | typedef typename TypeParam::ScalarType ScalarType; 220 | static constexpr std::size_t kDimension = TypeParam::kDimension; 221 | namespace fmm = thinks::fast_marching_method; 222 | typedef fmm::VaryingSpeedEikonalSolver 223 | EikonalSolverType; 224 | 225 | // Arrange. 226 | auto const invalid_grid_spacing_elements = std::array{ 227 | {ScalarType{0}, ScalarType{-1}, 228 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 229 | 230 | for (auto const invalid_grid_spacing_element : 231 | invalid_grid_spacing_elements) { 232 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 233 | auto grid_spacing = util::FilledArray(ScalarType{1}); 234 | grid_spacing[i] = invalid_grid_spacing_element; // Invalid i'th element. 235 | auto const speed_grid_size = util::FilledArray(size_t{10}); 236 | auto const speed_buffer = std::vector( 237 | util::LinearSize(speed_grid_size), ScalarType{1}); 238 | 239 | auto expected_reason = std::stringstream(); 240 | expected_reason << "invalid grid spacing: " 241 | << util::ToString(grid_spacing); 242 | 243 | // Act. 244 | auto const ft = util::FunctionThrows([&]() { 245 | [[maybe_unused]] auto const eikonal_solver = 246 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 247 | }); 248 | 249 | // Assert. 250 | ASSERT_TRUE(ft.first); 251 | ASSERT_EQ(expected_reason.str(), ft.second); 252 | } 253 | } 254 | } 255 | 256 | TYPED_TEST(VaryingSpeedEikonalSolverTest, InvalidSpeedThrows) { 257 | typedef typename TypeParam::ScalarType ScalarType; 258 | static constexpr std::size_t kDimension = TypeParam::kDimension; 259 | namespace fmm = thinks::fast_marching_method; 260 | typedef fmm::VaryingSpeedEikonalSolver 261 | EikonalSolverType; 262 | 263 | // Arrange. 264 | auto const invalid_speeds = std::array{ 265 | {ScalarType{0}, ScalarType{-1}, 266 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 267 | 268 | for (auto const invalid_speed : invalid_speeds) { 269 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 270 | auto const speed_grid_size = util::FilledArray(size_t{10}); 271 | 272 | // Invalid speed in the middle of the buffer. 273 | auto speed_buffer = std::vector( 274 | util::LinearSize(speed_grid_size), ScalarType{1}); 275 | speed_buffer[speed_buffer.size() / 2] = invalid_speed; 276 | 277 | auto expected_reason = std::stringstream(); 278 | expected_reason << "invalid speed: " << invalid_speed; 279 | 280 | // Act. 281 | auto const ft = util::FunctionThrows([=]() { 282 | [[maybe_unused]] auto const eikonal_solver = 283 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 284 | }); 285 | 286 | // Assert. 287 | ASSERT_TRUE(ft.first); 288 | ASSERT_EQ(expected_reason.str(), ft.second); 289 | } 290 | } 291 | 292 | TYPED_TEST(VaryingSpeedEikonalSolverTest, InvalidSpeedBufferThrows) { 293 | typedef typename TypeParam::ScalarType ScalarType; 294 | static constexpr std::size_t kDimension = TypeParam::kDimension; 295 | namespace fmm = thinks::fast_marching_method; 296 | typedef fmm::VaryingSpeedEikonalSolver 297 | EikonalSolverType; 298 | 299 | // Arrange. 300 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 301 | auto const speed_grid_size = util::FilledArray(size_t{10}); 302 | 303 | // Buffer size is not linear grid size! 304 | auto const speed_buffer = std::vector( 305 | util::LinearSize(speed_grid_size) - 1, ScalarType{1}); 306 | 307 | auto expected_reason = std::stringstream(); 308 | expected_reason << "grid size " << util::ToString(speed_grid_size) 309 | << " does not match cell buffer size " << speed_buffer.size(); 310 | 311 | // Act. 312 | auto const ft = util::FunctionThrows([=]() { 313 | [[maybe_unused]] auto const eikonal_solver = 314 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 315 | }); 316 | 317 | // Assert. 318 | ASSERT_TRUE(ft.first); 319 | ASSERT_EQ(expected_reason.str(), ft.second); 320 | } 321 | 322 | TYPED_TEST(VaryingSpeedEikonalSolverTest, InvalidSpeedGridSizeThrows) { 323 | typedef typename TypeParam::ScalarType ScalarType; 324 | static constexpr std::size_t kDimension = TypeParam::kDimension; 325 | namespace fmm = thinks::fast_marching_method; 326 | typedef fmm::VaryingSpeedEikonalSolver 327 | EikonalSolverType; 328 | 329 | // Arrange. 330 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 331 | auto speed_grid_size = util::FilledArray(size_t{10}); 332 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 333 | speed_grid_size[i] = 0; // Invalid i'th element. 334 | auto const speed_buffer = std::vector( 335 | util::LinearSize(speed_grid_size), ScalarType{1}); 336 | 337 | auto expected_reason = std::stringstream(); 338 | expected_reason << "invalid size: " << util::ToString(speed_grid_size); 339 | 340 | // Act. 341 | auto const ft = util::FunctionThrows([=]() { 342 | [[maybe_unused]] auto const eikonal_solver = 343 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 344 | }); 345 | 346 | // Assert. 347 | ASSERT_TRUE(ft.first); 348 | ASSERT_EQ(expected_reason.str(), ft.second); 349 | } 350 | } 351 | 352 | TYPED_TEST(VaryingSpeedEikonalSolverTest, IndexOutsideSpeedGridThrows) { 353 | typedef typename TypeParam::ScalarType ScalarType; 354 | static constexpr std::size_t kDimension = TypeParam::kDimension; 355 | namespace fmm = thinks::fast_marching_method; 356 | typedef fmm::VaryingSpeedEikonalSolver 357 | EikonalSolverType; 358 | 359 | // Arrange. 360 | auto const grid_size = util::FilledArray(size_t{10}); 361 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 362 | 363 | // Speed grid smaller than distance grid! 364 | auto const speed_grid_size = util::FilledArray(size_t{9}); 365 | auto const speed_buffer = 366 | std::vector(util::LinearSize(speed_grid_size), ScalarType{1}); 367 | 368 | auto boundary_indices = std::vector>(); 369 | auto index_iter = util::IndexIterator(grid_size); 370 | boundary_indices.push_back(index_iter.index()); 371 | auto const boundary_distances = std::vector(1, ScalarType{1}); 372 | 373 | // Act. 374 | auto const ft = util::FunctionThrows([=]() { 375 | auto const unsigned_distance = fmm::SignedArrivalTime( 376 | grid_size, boundary_indices, boundary_distances, 377 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer)); 378 | }); 379 | 380 | // Assert. 381 | ASSERT_TRUE(ft.first); 382 | ASSERT_EQ("speed index outside grid - index:", ft.second.substr(0, 33)); 383 | } 384 | 385 | // HighAccuracyVaryingSpeedEikonalSolverTest fixture. 386 | 387 | TYPED_TEST(HighAccuracyVaryingSpeedEikonalSolverTest, 388 | InvalidGridSpacingThrows) { 389 | typedef typename TypeParam::ScalarType ScalarType; 390 | static constexpr std::size_t kDimension = TypeParam::kDimension; 391 | namespace fmm = thinks::fast_marching_method; 392 | typedef fmm::HighAccuracyVaryingSpeedEikonalSolver 393 | EikonalSolverType; 394 | 395 | // Arrange. 396 | auto const invalid_grid_spacing_elements = std::array{ 397 | {ScalarType{0}, ScalarType{-1}, 398 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 399 | 400 | for (auto const invalid_grid_spacing_element : 401 | invalid_grid_spacing_elements) { 402 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 403 | auto grid_spacing = util::FilledArray(ScalarType{1}); 404 | grid_spacing[i] = invalid_grid_spacing_element; // Invalid i'th element. 405 | auto const speed_grid_size = util::FilledArray(size_t{10}); 406 | auto const speed_buffer = std::vector( 407 | util::LinearSize(speed_grid_size), ScalarType{1}); 408 | 409 | auto expected_reason = std::stringstream(); 410 | expected_reason << "invalid grid spacing: " 411 | << util::ToString(grid_spacing); 412 | 413 | // Act. 414 | auto const ft = util::FunctionThrows([&]() { 415 | [[maybe_unused]] auto const eikonal_solver = 416 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 417 | }); 418 | 419 | // Assert. 420 | ASSERT_TRUE(ft.first); 421 | ASSERT_EQ(expected_reason.str(), ft.second); 422 | } 423 | } 424 | } 425 | 426 | TYPED_TEST(HighAccuracyVaryingSpeedEikonalSolverTest, InvalidSpeedThrows) { 427 | typedef typename TypeParam::ScalarType ScalarType; 428 | static constexpr std::size_t kDimension = TypeParam::kDimension; 429 | namespace fmm = thinks::fast_marching_method; 430 | typedef fmm::HighAccuracyVaryingSpeedEikonalSolver 431 | EikonalSolverType; 432 | 433 | // Arrange. 434 | auto const invalid_speeds = std::array{ 435 | {ScalarType{0}, ScalarType{-1}, 436 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 437 | 438 | for (auto const invalid_speed : invalid_speeds) { 439 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 440 | auto const speed_grid_size = util::FilledArray(size_t{10}); 441 | 442 | // Invalid speed in the middle of the buffer. 443 | auto speed_buffer = std::vector( 444 | util::LinearSize(speed_grid_size), ScalarType{1}); 445 | speed_buffer[speed_buffer.size() / 2] = invalid_speed; 446 | 447 | auto expected_reason = std::stringstream(); 448 | expected_reason << "invalid speed: " << invalid_speed; 449 | 450 | // Act. 451 | auto const ft = util::FunctionThrows([=]() { 452 | [[maybe_unused]] auto const eikonal_solver = 453 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 454 | }); 455 | 456 | // Assert. 457 | ASSERT_TRUE(ft.first); 458 | ASSERT_EQ(expected_reason.str(), ft.second); 459 | } 460 | } 461 | 462 | TYPED_TEST(HighAccuracyVaryingSpeedEikonalSolverTest, 463 | InvalidSpeedBufferThrows) { 464 | typedef typename TypeParam::ScalarType ScalarType; 465 | static constexpr std::size_t kDimension = TypeParam::kDimension; 466 | namespace fmm = thinks::fast_marching_method; 467 | typedef fmm::HighAccuracyVaryingSpeedEikonalSolver 468 | EikonalSolverType; 469 | 470 | // Arrange. 471 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 472 | auto const speed_grid_size = util::FilledArray(size_t{10}); 473 | // Buffer size is not the same as linear grid size! 474 | auto const speed_buffer = std::vector( 475 | util::LinearSize(speed_grid_size) - 1, ScalarType{1}); 476 | 477 | auto expected_reason = std::stringstream(); 478 | expected_reason << "grid size " << util::ToString(speed_grid_size) 479 | << " does not match cell buffer size " << speed_buffer.size(); 480 | 481 | // Act. 482 | auto const ft = util::FunctionThrows([=]() { 483 | [[maybe_unused]] auto const eikonal_solver = 484 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 485 | }); 486 | 487 | // Assert. 488 | ASSERT_TRUE(ft.first); 489 | ASSERT_EQ(expected_reason.str(), ft.second); 490 | } 491 | 492 | TYPED_TEST(HighAccuracyVaryingSpeedEikonalSolverTest, 493 | InvalidSpeedGridSizeThrows) { 494 | typedef typename TypeParam::ScalarType ScalarType; 495 | static constexpr std::size_t kDimension = TypeParam::kDimension; 496 | namespace fmm = thinks::fast_marching_method; 497 | typedef fmm::HighAccuracyVaryingSpeedEikonalSolver 498 | EikonalSolverType; 499 | 500 | // Arrange. 501 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 502 | auto speed_grid_size = util::FilledArray(size_t{10}); 503 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 504 | speed_grid_size[i] = 0; // Invalid i'th element. 505 | auto const speed_buffer = std::vector( 506 | util::LinearSize(speed_grid_size), ScalarType{1}); 507 | 508 | auto expected_reason = std::stringstream(); 509 | expected_reason << "invalid size: " << util::ToString(speed_grid_size); 510 | 511 | // Act. 512 | auto const ft = util::FunctionThrows([=]() { 513 | [[maybe_unused]] auto const eikonal_solver = 514 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer); 515 | }); 516 | 517 | // Assert. 518 | ASSERT_TRUE(ft.first); 519 | ASSERT_EQ(expected_reason.str(), ft.second); 520 | } 521 | } 522 | 523 | TYPED_TEST(HighAccuracyVaryingSpeedEikonalSolverTest, 524 | IndexOutsideSpeedGridThrows) { 525 | typedef typename TypeParam::ScalarType ScalarType; 526 | static constexpr std::size_t kDimension = TypeParam::kDimension; 527 | namespace fmm = thinks::fast_marching_method; 528 | typedef fmm::HighAccuracyVaryingSpeedEikonalSolver 529 | EikonalSolverType; 530 | 531 | // Arrange. 532 | auto const grid_size = util::FilledArray(size_t{10}); 533 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 534 | // Speed grid smaller than distance grid! 535 | auto const speed_grid_size = util::FilledArray(size_t{9}); 536 | auto const speed_buffer = 537 | std::vector(util::LinearSize(speed_grid_size), ScalarType{1}); 538 | 539 | auto boundary_indices = std::vector>(); 540 | auto index_iter = util::IndexIterator(grid_size); 541 | boundary_indices.push_back(index_iter.index()); 542 | auto const boundary_distances = std::vector(1, ScalarType{1}); 543 | 544 | // Act. 545 | auto const ft = util::FunctionThrows([=]() { 546 | auto const unsigned_distance = fmm::SignedArrivalTime( 547 | grid_size, boundary_indices, boundary_distances, 548 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer)); 549 | }); 550 | 551 | // Assert. 552 | ASSERT_TRUE(ft.first); 553 | ASSERT_EQ("speed index outside grid - index:", ft.second.substr(0, 33)); 554 | } 555 | 556 | // DistanceSolveTest fixture. 557 | 558 | TYPED_TEST(DistanceSolverTest, InvalidGridSpacingThrows) { 559 | typedef typename TypeParam::ScalarType ScalarType; 560 | static constexpr std::size_t kDimension = TypeParam::kDimension; 561 | namespace fmm = thinks::fast_marching_method; 562 | typedef fmm::DistanceSolver EikonalSolverType; 563 | 564 | // Arrange. 565 | auto const invalid_grid_spacing_elements = std::array{ 566 | {ScalarType{0}, ScalarType{-1}, 567 | std::numeric_limits::quiet_NaN(), ScalarType(1e-7)}}; 568 | 569 | for (auto const invalid_grid_spacing_element : 570 | invalid_grid_spacing_elements) { 571 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 572 | auto const dx = invalid_grid_spacing_element; // Invalid i'th element. 573 | 574 | auto expected_reason = std::stringstream(); 575 | expected_reason << "invalid grid spacing: " 576 | << util::ToString(util::FilledArray(dx)); 577 | 578 | // Act. 579 | auto const ft = util::FunctionThrows([=]() { 580 | [[maybe_unused]] auto const eikonal_solver = EikonalSolverType(dx); 581 | }); 582 | 583 | // Assert. 584 | ASSERT_TRUE(ft.first); 585 | ASSERT_EQ(expected_reason.str(), ft.second); 586 | } 587 | } 588 | } 589 | 590 | } // namespace 591 | -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #include 22 | 23 | int main(int argc, char* argv[]) { 24 | ::testing::InitGoogleTest(&argc, argv); 25 | 26 | return RUN_ALL_TESTS(); 27 | } 28 | -------------------------------------------------------------------------------- /tests/py-bindings-test.py: -------------------------------------------------------------------------------- 1 | import py_fast_marching_method as fmm 2 | import numpy as np 3 | from scipy import ndimage as ndi 4 | 5 | 6 | def test_uniform_2D(): 7 | grid_size = np.array([50, 101]) 8 | # grid_spacing = 1.0/grid_size 9 | grid_spacing = np.ones(grid_size.shape) 10 | boundary_indices = np.array([[20, 75]]) 11 | boundary_times = np.array([0.0]) 12 | uniform_speed = 1.0 13 | 14 | arrival_times = fmm.uniform_speed_signed_arrival_time( 15 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 16 | ) 17 | print("arrival_times\n", arrival_times) 18 | 19 | binary_input_grid = np.ones(grid_size) 20 | binary_input_grid[boundary_indices[:, 0], boundary_indices[:, 1]] = 0 21 | print("binary_input_grid\n", binary_input_grid) 22 | 23 | edt = ndi.distance_transform_edt(binary_input_grid) 24 | print("edt\n", edt) 25 | 26 | # FMM is not super accurate -> large tolerance 27 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.5) 28 | 29 | print("Done 2D") 30 | 31 | 32 | def test_uniform_3D(): 33 | grid_size = np.array([30, 76, 43]) 34 | # grid_spacing = 1.0/grid_size 35 | grid_spacing = np.ones(grid_size.shape) 36 | boundary_indices = np.array([[9, 50, 21]]) 37 | boundary_times = np.array([0.0]) 38 | uniform_speed = 1.0 39 | 40 | arrival_times = fmm.uniform_speed_signed_arrival_time( 41 | grid_size, boundary_indices, boundary_times, grid_spacing, uniform_speed 42 | ) 43 | print("arrival_times\n", arrival_times) 44 | 45 | binary_input_grid = np.ones(grid_size) 46 | binary_input_grid[ 47 | boundary_indices[:, 0], boundary_indices[:, 1], boundary_indices[:, 2] 48 | ] = 0 49 | print("binary_input_grid\n", binary_input_grid) 50 | 51 | edt = ndi.distance_transform_edt(binary_input_grid) 52 | print("edt\n", edt) 53 | 54 | # FMM is not super accurate -> large tolerance 55 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.7) 56 | 57 | print("Done 3D") 58 | 59 | 60 | def test_varying_2D(): 61 | grid_size = np.array([5, 8]) 62 | # grid_spacing = 1.0/grid_size 63 | grid_spacing = np.ones(grid_size.shape) 64 | boundary_indices = np.array([[2, 5]]) 65 | boundary_times = np.array([0.0]) 66 | varying_speed = np.ones(grid_size) 67 | 68 | arrival_times = fmm.varying_speed_signed_arrival_time( 69 | grid_size, boundary_indices, boundary_times, grid_spacing, varying_speed 70 | ) 71 | print("arrival_times\n", arrival_times) 72 | 73 | binary_input_grid = np.ones(grid_size) 74 | binary_input_grid[boundary_indices[:, 0], boundary_indices[:, 1]] = 0 75 | print("binary_input_grid\n", binary_input_grid) 76 | 77 | edt = ndi.distance_transform_edt(binary_input_grid) 78 | print("edt\n", edt) 79 | 80 | # FMM is not super accurate -> large tolerance 81 | np.testing.assert_allclose(arrival_times, edt, rtol=0, atol=0.5) 82 | 83 | print("Done 2D") 84 | 85 | 86 | if __name__ == "__main__": 87 | test_uniform_2D() 88 | test_uniform_3D() 89 | test_varying_2D() 90 | -------------------------------------------------------------------------------- /tests/signed_arrival_time_test.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #include 22 | 23 | #include "../include/thinks/fast_marching_method/fast_marching_method.hpp" 24 | #include "./util.hpp" 25 | 26 | namespace { 27 | 28 | // Fixtures. 29 | 30 | template 31 | class SignedArrivalTimeTest : public ::testing::Test { 32 | protected: 33 | virtual ~SignedArrivalTimeTest() {} 34 | }; 35 | 36 | template 37 | class SignedArrivalTimeAccuracyTest : public ::testing::Test { 38 | protected: 39 | virtual ~SignedArrivalTimeAccuracyTest() {} 40 | }; 41 | 42 | // Associate types with fixtures. 43 | 44 | typedef ::testing::Types< 45 | util::ScalarDimensionPair, util::ScalarDimensionPair, 46 | util::ScalarDimensionPair, util::ScalarDimensionPair, 47 | util::ScalarDimensionPair, util::ScalarDimensionPair> 48 | SignedArrivalTimeTypes; 49 | 50 | typedef ::testing::Types< 51 | util::ScalarDimensionPair, util::ScalarDimensionPair, 52 | util::ScalarDimensionPair, util::ScalarDimensionPair> 53 | AccuracyTypes; 54 | 55 | TYPED_TEST_SUITE(SignedArrivalTimeTest, SignedArrivalTimeTypes); 56 | TYPED_TEST_SUITE(SignedArrivalTimeAccuracyTest, AccuracyTypes); 57 | 58 | // SignedArrivalTime fixture. 59 | 60 | TYPED_TEST(SignedArrivalTimeTest, ZeroElementInGridSizeThrows) { 61 | typedef typename TypeParam::ScalarType ScalarType; 62 | static constexpr std::size_t kDimension = TypeParam::kDimension; 63 | namespace fmm = thinks::fast_marching_method; 64 | typedef fmm::UniformSpeedEikonalSolver 65 | EikonalSolverType; 66 | 67 | // Arrange. 68 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 69 | auto grid_size = util::FilledArray(size_t{10}); 70 | grid_size[i] = 0; // Zero element in i'th position. 71 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 72 | auto const speed = ScalarType{1}; 73 | 74 | auto boundary_indices = std::vector>(); 75 | boundary_indices.push_back(util::FilledArray(int32_t{0})); 76 | auto const boundary_distances = std::vector(1, ScalarType{1}); 77 | 78 | auto expected_reason = std::stringstream(); 79 | expected_reason << "invalid size: " << util::ToString(grid_size); 80 | 81 | // Act. 82 | auto const ft = util::FunctionThrows([=]() { 83 | auto const signed_times = fmm::SignedArrivalTime( 84 | grid_size, boundary_indices, boundary_distances, 85 | EikonalSolverType(grid_spacing, speed)); 86 | }); 87 | 88 | // Assert. 89 | ASSERT_TRUE(ft.first); 90 | ASSERT_EQ(expected_reason.str(), ft.second); 91 | } 92 | } 93 | 94 | TYPED_TEST(SignedArrivalTimeTest, EmptyBoundaryThrows) { 95 | typedef typename TypeParam::ScalarType ScalarType; 96 | static constexpr std::size_t kDimension = TypeParam::kDimension; 97 | namespace fmm = thinks::fast_marching_method; 98 | typedef fmm::UniformSpeedEikonalSolver 99 | EikonalSolverType; 100 | 101 | // Arrange. 102 | auto const grid_size = util::FilledArray(size_t{10}); 103 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 104 | auto const speed = ScalarType{1}; 105 | auto const boundary_indices = 106 | std::vector>{}; // Empty. 107 | auto const boundary_distances = std::vector{}; // Empty. 108 | 109 | // Act. 110 | auto const ft = util::FunctionThrows([=]() { 111 | auto const signed_times = 112 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_distances, 113 | EikonalSolverType(grid_spacing, speed)); 114 | }); 115 | 116 | // Assert. 117 | ASSERT_TRUE(ft.first); 118 | ASSERT_EQ("empty boundary condition", ft.second); 119 | } 120 | 121 | TYPED_TEST(SignedArrivalTimeTest, FullGridBoundaryIndicesThrows) { 122 | typedef typename TypeParam::ScalarType ScalarType; 123 | static constexpr std::size_t kDimension = TypeParam::kDimension; 124 | namespace fmm = thinks::fast_marching_method; 125 | typedef fmm::UniformSpeedEikonalSolver 126 | EikonalSolverType; 127 | 128 | // Arrange. 129 | auto const grid_size = util::FilledArray(size_t{10}); 130 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 131 | auto const speed = ScalarType{1}; 132 | 133 | // Add every cell in the grid! 134 | auto boundary_indices = std::vector>(); 135 | auto index_iter = util::IndexIterator(grid_size); 136 | while (index_iter.has_next()) { 137 | boundary_indices.push_back(index_iter.index()); 138 | index_iter.Next(); 139 | } 140 | 141 | auto const boundary_distances = 142 | std::vector(util::LinearSize(grid_size), ScalarType{1}); 143 | 144 | // Act. 145 | auto const ft = util::FunctionThrows([=]() { 146 | auto const signed_distance = 147 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_distances, 148 | EikonalSolverType(grid_spacing, speed)); 149 | }); 150 | 151 | // Assert. 152 | ASSERT_TRUE(ft.first); 153 | ASSERT_EQ("full grid boundary", ft.second); 154 | } 155 | 156 | TYPED_TEST(SignedArrivalTimeTest, DuplicateBoundaryIndicesThrows) { 157 | typedef typename TypeParam::ScalarType ScalarType; 158 | static constexpr std::size_t kDimension = TypeParam::kDimension; 159 | namespace fmm = thinks::fast_marching_method; 160 | typedef fmm::UniformSpeedEikonalSolver 161 | EikonalSolverType; 162 | 163 | // Arrange. 164 | auto const grid_size = util::FilledArray(size_t{10}); 165 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 166 | auto const speed = ScalarType{1}; 167 | 168 | auto boundary_indices = std::vector>(); 169 | auto index_iter = util::IndexIterator(grid_size); 170 | boundary_indices.push_back(index_iter.index()); 171 | boundary_indices.push_back(index_iter.index()); // Same index! 172 | 173 | auto const boundary_distances = std::vector(2, ScalarType{1}); 174 | 175 | auto expected_reason = std::stringstream(); 176 | expected_reason << "duplicate boundary index: " 177 | << util::ToString(index_iter.index()); 178 | 179 | // Act. 180 | auto const ft = util::FunctionThrows([=]() { 181 | auto const signed_distance = 182 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_distances, 183 | EikonalSolverType(grid_spacing, speed)); 184 | }); 185 | 186 | // Assert. 187 | ASSERT_TRUE(ft.first); 188 | ASSERT_EQ(expected_reason.str(), ft.second); 189 | } 190 | 191 | TYPED_TEST(SignedArrivalTimeTest, BoundaryIndexOutsideGridThrows) { 192 | typedef typename TypeParam::ScalarType ScalarType; 193 | static constexpr std::size_t kDimension = TypeParam::kDimension; 194 | namespace fmm = thinks::fast_marching_method; 195 | typedef fmm::UniformSpeedEikonalSolver 196 | EikonalSolverType; 197 | 198 | // Arrange. 199 | auto const grid_size = util::FilledArray(size_t{10}); 200 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 201 | auto const speed = ScalarType{1}; 202 | 203 | auto boundary_indices = std::vector>(); 204 | auto index_iter = util::IndexIterator(grid_size); 205 | boundary_indices.push_back(index_iter.index()); 206 | // Outside! 207 | boundary_indices.push_back(util::FilledArray(int32_t{-1})); 208 | 209 | auto const boundary_times = std::vector(2, ScalarType{1}); 210 | 211 | auto expected_reason = std::stringstream(); 212 | expected_reason << "boundary index outside grid - " 213 | << "index: " << util::ToString(boundary_indices.back()) 214 | << ", " 215 | << "grid size: " << util::ToString(grid_size); 216 | 217 | // Act. 218 | auto const ft = util::FunctionThrows([=]() { 219 | auto const signed_distance = 220 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 221 | EikonalSolverType(grid_spacing, speed)); 222 | }); 223 | 224 | // Assert. 225 | ASSERT_TRUE(ft.first); 226 | ASSERT_EQ(expected_reason.str(), ft.second); 227 | } 228 | 229 | TYPED_TEST(SignedArrivalTimeTest, BoundaryIndicesAndTimesSizeMismatchThrows) { 230 | typedef typename TypeParam::ScalarType ScalarType; 231 | static constexpr std::size_t kDimension = TypeParam::kDimension; 232 | namespace fmm = thinks::fast_marching_method; 233 | typedef fmm::UniformSpeedEikonalSolver 234 | EikonalSolverType; 235 | 236 | // Arrange. 237 | auto const grid_size = util::FilledArray(size_t{10}); 238 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 239 | auto const speed = ScalarType{1}; 240 | 241 | auto boundary_indices = std::vector>(); 242 | auto index_iter = util::IndexIterator(grid_size); 243 | boundary_indices.push_back(index_iter.index()); 244 | index_iter.Next(); 245 | boundary_indices.push_back(index_iter.index()); 246 | 247 | // Two indices, three distances. 248 | auto const boundary_times = std::vector(3, ScalarType{1}); 249 | 250 | // Act. 251 | auto const ft = util::FunctionThrows([=]() { 252 | auto const signed_distance = 253 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 254 | EikonalSolverType(grid_spacing, speed)); 255 | }); 256 | 257 | // Assert. 258 | ASSERT_TRUE(ft.first); 259 | ASSERT_EQ("boundary indices[2] / boundary times[3] size mismatch", ft.second); 260 | } 261 | 262 | TYPED_TEST(SignedArrivalTimeTest, InvalidBoundaryTimeThrows) { 263 | typedef typename TypeParam::ScalarType ScalarType; 264 | static constexpr std::size_t kDimension = TypeParam::kDimension; 265 | namespace fmm = thinks::fast_marching_method; 266 | typedef fmm::UniformSpeedEikonalSolver 267 | EikonalSolverType; 268 | 269 | // Arrange. 270 | auto const invalid_boundary_times = 271 | std::array{{std::numeric_limits::max(), 272 | -std::numeric_limits::max(), 273 | std::numeric_limits::quiet_NaN()}}; 274 | 275 | auto const grid_size = util::FilledArray(size_t{10}); 276 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 277 | auto const speed = ScalarType{1}; 278 | for (auto const invalid_boundary_time : invalid_boundary_times) { 279 | auto boundary_indices = std::vector>(); 280 | auto index_iter = util::IndexIterator(grid_size); 281 | boundary_indices.push_back(index_iter.index()); 282 | 283 | auto const boundary_times = 284 | std::vector(1, invalid_boundary_time); // Invalid! 285 | 286 | auto expected_reason = std::stringstream(); 287 | expected_reason << "invalid boundary time: " << invalid_boundary_time; 288 | 289 | // Act. 290 | auto const ft = util::FunctionThrows([=]() { 291 | auto const signed_times = 292 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 293 | EikonalSolverType(grid_spacing, speed)); 294 | }); 295 | 296 | // Assert. 297 | ASSERT_TRUE(ft.first); 298 | ASSERT_EQ(expected_reason.str(), ft.second); 299 | } 300 | } 301 | 302 | TYPED_TEST(SignedArrivalTimeTest, EikonalSolverFailThrows) { 303 | typedef typename TypeParam::ScalarType ScalarType; 304 | static constexpr std::size_t kDimension = TypeParam::kDimension; 305 | namespace fmm = thinks::fast_marching_method; 306 | // Test if eikonal solver fails when no fallback is requested 307 | typedef fmm::UniformSpeedEikonalSolver 308 | EikonalSolverType; 309 | 310 | // Arrange. 311 | auto const grid_size = util::FilledArray(size_t{10}); 312 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 313 | auto const speed = ScalarType{1}; 314 | 315 | // Create a scenario where solver has a very small value in one direction 316 | // and a very large value in another. Cannot resolve gradient for 317 | // this scenario. 318 | auto boundary_indices = std::vector>(); 319 | boundary_indices.push_back(util::FilledArray(int32_t{0})); 320 | boundary_indices.push_back(util::FilledArray(int32_t{1})); 321 | auto boundary_times = std::vector(); 322 | boundary_times.push_back(ScalarType{1000}); 323 | boundary_times.push_back(ScalarType{1}); 324 | 325 | // Act. 326 | auto const ft = util::FunctionThrows([=]() { 327 | auto const signed_times = 328 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 329 | EikonalSolverType(grid_spacing, speed)); 330 | }); 331 | 332 | // Assert. 333 | ASSERT_TRUE(ft.first); 334 | ASSERT_EQ("invalid arrival time (distance)", ft.second.substr(size_t{0}, 31)); 335 | } 336 | 337 | TYPED_TEST(SignedArrivalTimeTest, ContainedComponentThrows) { 338 | typedef typename TypeParam::ScalarType ScalarType; 339 | static constexpr auto kDimension = TypeParam::kDimension; 340 | namespace fmm = thinks::fast_marching_method; 341 | typedef fmm::UniformSpeedEikonalSolver 342 | EikonalSolverType; 343 | 344 | // Arrange. 345 | auto const grid_size = util::FilledArray(size_t{20}); 346 | auto const grid_spacing = util::FilledArray(ScalarType(0.05)); 347 | auto const uniform_speed = ScalarType{1}; 348 | 349 | auto const sphere_center1 = util::FilledArray(ScalarType{0.5}); 350 | auto const sphere_radius1 = ScalarType(0.2); 351 | auto const sphere_center2 = util::FilledArray(ScalarType{0.5}); 352 | auto const sphere_radius2 = ScalarType(0.45); 353 | 354 | auto sphere_boundary_indices1 = 355 | std::vector>(); 356 | auto sphere_boundary_times1 = std::vector(); 357 | util::HyperSphereBoundaryCells( 358 | sphere_center1, sphere_radius1, grid_size, grid_spacing, 359 | [](ScalarType const d) { return d; }, 360 | 0, // dilation_pass_count 361 | &sphere_boundary_indices1, &sphere_boundary_times1); 362 | auto sphere_boundary_indices2 = 363 | std::vector>(); 364 | auto sphere_boundary_times2 = std::vector(); 365 | util::HyperSphereBoundaryCells( 366 | sphere_center2, sphere_radius2, grid_size, grid_spacing, 367 | [](ScalarType const d) { return d; }, 368 | 0, // dilation_pass_count 369 | &sphere_boundary_indices2, &sphere_boundary_times2); 370 | 371 | auto boundary_indices = sphere_boundary_indices1; 372 | auto boundary_times = sphere_boundary_times1; 373 | for (auto i = std::size_t{0}; i < sphere_boundary_indices2.size(); ++i) { 374 | boundary_indices.push_back(sphere_boundary_indices2[i]); 375 | boundary_times.push_back(sphere_boundary_times2[i]); 376 | } 377 | 378 | // Act. 379 | auto const ft = util::FunctionThrows([=]() { 380 | auto const signed_distance = 381 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 382 | EikonalSolverType(grid_spacing, uniform_speed)); 383 | }); 384 | 385 | // Assert. 386 | ASSERT_TRUE(ft.first); 387 | ASSERT_EQ("contained component", ft.second); 388 | } 389 | 390 | TYPED_TEST(SignedArrivalTimeTest, DifferentUniformSpeed) { 391 | typedef typename TypeParam::ScalarType ScalarType; 392 | static constexpr std::size_t kDimension = TypeParam::kDimension; 393 | namespace fmm = thinks::fast_marching_method; 394 | typedef fmm::UniformSpeedEikonalSolver 395 | EikonalSolverType; 396 | 397 | // Arrange. 398 | auto const grid_size = util::FilledArray(size_t{10}); 399 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 400 | 401 | auto boundary_indices = std::vector>(); 402 | boundary_indices.push_back(util::FilledArray(int32_t{5})); 403 | 404 | auto boundary_times = std::vector(); 405 | boundary_times.push_back(ScalarType{0}); 406 | 407 | auto const speed1 = ScalarType{1}; 408 | auto const speed2 = ScalarType{2}; 409 | 410 | // Act. 411 | auto const signed_times1 = 412 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 413 | EikonalSolverType(grid_spacing, speed1)); 414 | 415 | auto const signed_times2 = 416 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 417 | EikonalSolverType(grid_spacing, speed2)); 418 | 419 | // Assert. 420 | // Check that the distance is halved when the speed is halved. 421 | // Note that the boundary distance is zero, which also passes this check. 422 | for (auto i = std::size_t{0}; i < signed_times1.size(); ++i) { 423 | auto const d1 = signed_times1[i]; 424 | auto const d2 = signed_times2[i]; 425 | ASSERT_LE(fabs(speed1 * d1 - speed2 * d2), ScalarType(1e-3)); 426 | } 427 | } 428 | 429 | TYPED_TEST(SignedArrivalTimeTest, VaryingSpeed) { 430 | typedef typename TypeParam::ScalarType ScalarType; 431 | static constexpr auto kDimension = TypeParam::kDimension; 432 | namespace fmm = thinks::fast_marching_method; 433 | typedef fmm::VaryingSpeedEikonalSolver 434 | EikonalSolverType; 435 | 436 | // Arrange. 437 | auto const grid_size = util::FilledArray(size_t{11}); 438 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 439 | 440 | auto boundary_indices = std::vector>(); 441 | boundary_indices.push_back(util::FilledArray(int32_t{5})); 442 | 443 | auto boundary_times = std::vector(); 444 | boundary_times.push_back(ScalarType{0}); 445 | 446 | auto const speed_grid_size = grid_size; 447 | auto speed_buffer = 448 | std::vector(util::LinearSize(speed_grid_size)); 449 | auto speed_grid = 450 | util::Grid(speed_grid_size, speed_buffer.front()); 451 | auto const speed = ScalarType(1); 452 | auto const mirror_speed = ScalarType(2); 453 | auto speed_index_iter = util::IndexIterator(speed_grid.size()); 454 | while (speed_index_iter.has_next()) { 455 | auto const index = speed_index_iter.index(); 456 | if (index[0] < boundary_indices[0][0]) { 457 | speed_grid.Cell(index) = mirror_speed; 458 | } else { 459 | speed_grid.Cell(index) = speed; 460 | } 461 | speed_index_iter.Next(); 462 | } 463 | 464 | // Act. 465 | auto signed_times = fmm::SignedArrivalTime( 466 | grid_size, boundary_indices, boundary_times, 467 | EikonalSolverType(grid_spacing, speed_grid_size, speed_buffer)); 468 | 469 | // Assert. 470 | auto time_grid = 471 | util::Grid(grid_size, signed_times.front()); 472 | 473 | auto time_index_iter = util::IndexIterator(grid_size); 474 | while (time_index_iter.has_next()) { 475 | auto const index = time_index_iter.index(); 476 | auto mid = true; 477 | for (auto i = std::size_t{1}; i < kDimension; ++i) { 478 | const int32_t half = static_cast(grid_size[i]) / 2; 479 | if (index[i] != half) { 480 | mid = false; 481 | break; 482 | } 483 | } 484 | if (index[0] > boundary_indices[0][0] && mid) { 485 | auto mirror_index = index; 486 | mirror_index[0] = 2 * boundary_indices[0][0] - index[0]; 487 | auto const time = time_grid.Cell(index); 488 | auto const mirror_time = time_grid.Cell(mirror_index); 489 | ASSERT_NEAR(time * speed, mirror_time * mirror_speed, 1e-6); 490 | } 491 | time_index_iter.Next(); 492 | } 493 | } 494 | 495 | TYPED_TEST(SignedArrivalTimeTest, NonUniformGridSpacing) { 496 | typedef typename TypeParam::ScalarType ScalarType; 497 | static constexpr std::size_t kDimension = TypeParam::kDimension; 498 | namespace fmm = thinks::fast_marching_method; 499 | typedef fmm::UniformSpeedEikonalSolver 500 | EikonalSolverType; 501 | 502 | // Arrange. 503 | auto const grid_size = util::FilledArray(size_t{11}); 504 | auto grid_spacing = util::FilledArray(ScalarType{0}); 505 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 506 | grid_spacing[i] = ScalarType(1) / (i + 1); 507 | } 508 | auto const uniform_speed = ScalarType(1); 509 | 510 | auto boundary_indices = std::vector>( 511 | std::size_t{1}, util::FilledArray(int32_t{5})); 512 | auto boundary_times = std::vector(size_t{1}, ScalarType{0}); 513 | 514 | // Act. 515 | auto signed_times = 516 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 517 | EikonalSolverType(grid_spacing, uniform_speed)); 518 | 519 | // Assert. 520 | auto time_grid = 521 | util::Grid(grid_size, signed_times.front()); 522 | auto center_position = util::FilledArray(ScalarType{0}); 523 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 524 | center_position[i] = 525 | (boundary_indices[0][i] + ScalarType(0.5)) * grid_spacing[i]; 526 | } 527 | auto index_iter = util::IndexIterator(grid_size); 528 | while (index_iter.has_next()) { 529 | auto const index = index_iter.index(); 530 | auto position = util::FilledArray(ScalarType{0}); 531 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 532 | position[i] = (index[i] + ScalarType(0.5)) * grid_spacing[i]; 533 | } 534 | auto delta = util::FilledArray(ScalarType{0}); 535 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 536 | delta[i] = center_position[i] - position[i]; 537 | } 538 | auto const gt = util::Magnitude(delta); 539 | 540 | auto const time = time_grid.Cell(index); 541 | auto const time_abs_error = fabs(time - gt); 542 | 543 | typedef util::PointSourceAccuracyBounds Bounds; 544 | ASSERT_LE(time_abs_error, ScalarType(Bounds::max_abs_error())); 545 | 546 | index_iter.Next(); 547 | } 548 | } 549 | 550 | TYPED_TEST(SignedArrivalTimeTest, BoxBoundary) { 551 | typedef typename TypeParam::ScalarType ScalarType; 552 | static constexpr std::size_t kDimension = TypeParam::kDimension; 553 | namespace fmm = thinks::fast_marching_method; 554 | typedef fmm::UniformSpeedEikonalSolver 555 | EikonalSolverType; 556 | 557 | // Arrange. 558 | auto const grid_size = util::FilledArray(size_t{10}); 559 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 560 | 561 | auto boundary_indices = std::vector>(); 562 | auto index_iter = util::IndexIterator(grid_size); 563 | while (index_iter.has_next()) { 564 | auto const index = index_iter.index(); 565 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 566 | const int32_t edge = static_cast(grid_size[i]) - 1; 567 | if (index[i] == 0 || index[i] == edge) { 568 | boundary_indices.push_back(index); 569 | break; 570 | } 571 | } 572 | index_iter.Next(); 573 | } 574 | 575 | auto boundary_times = 576 | std::vector(boundary_indices.size(), ScalarType{0}); 577 | 578 | auto const speed = ScalarType{1}; 579 | 580 | // Act. 581 | auto const signed_times = 582 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 583 | EikonalSolverType(grid_spacing, speed)); 584 | 585 | // Assert. 586 | for (auto const signed_time : signed_times) { 587 | ASSERT_LE(signed_time, ScalarType{0}); 588 | } 589 | } 590 | 591 | TYPED_TEST(SignedArrivalTimeTest, Checkerboard) { 592 | typedef typename TypeParam::ScalarType ScalarType; 593 | static constexpr auto kDimension = TypeParam::kDimension; 594 | namespace fmm = thinks::fast_marching_method; 595 | typedef fmm::UniformSpeedEikonalSolver 596 | EikonalSolverType; 597 | 598 | // Arrange. 599 | auto const grid_size = util::FilledArray(size_t{10}); 600 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 601 | 602 | auto const is_even = [](auto const i) { return i % 2 == 0; }; 603 | auto const is_boundary = [=](auto const index) { 604 | return is_even(std::reduce(begin(index), end(index))); 605 | }; 606 | 607 | auto boundary_indices = std::vector>(); 608 | { 609 | auto index_iter = util::IndexIterator(grid_size); 610 | while (index_iter.has_next()) { 611 | auto const index = index_iter.index(); 612 | if (is_boundary(index)) { 613 | boundary_indices.push_back(index); 614 | } 615 | index_iter.Next(); 616 | } 617 | } 618 | 619 | auto boundary_times = 620 | std::vector(boundary_indices.size(), ScalarType{0}); 621 | 622 | auto const speed = ScalarType{1}; 623 | 624 | // Act. 625 | auto signed_times = 626 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 627 | EikonalSolverType(grid_spacing, speed)); 628 | 629 | // Assert. 630 | auto time_grid = 631 | util::Grid(grid_size, signed_times.front()); 632 | { 633 | auto index_iter = util::IndexIterator(grid_size); 634 | while (index_iter.has_next()) { 635 | auto const index = index_iter.index(); 636 | if (!is_boundary(index)) { 637 | auto is_edge = false; 638 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 639 | const int32_t edge = static_cast(grid_size[i]) - 1; 640 | if (index[i] == 0 || index[i] == edge) { 641 | is_edge = true; 642 | break; 643 | } 644 | } 645 | 646 | auto const time = time_grid.Cell(index); 647 | if (is_edge) { 648 | ASSERT_GT(time, ScalarType{0}); 649 | } else { 650 | ASSERT_LT(time, ScalarType{0}); 651 | } 652 | } 653 | index_iter.Next(); 654 | } 655 | } 656 | } 657 | 658 | TYPED_TEST(SignedArrivalTimeTest, OverlappingBoxes) { 659 | typedef typename TypeParam::ScalarType ScalarType; 660 | static constexpr auto kDimension = TypeParam::kDimension; 661 | namespace fmm = thinks::fast_marching_method; 662 | typedef fmm::UniformSpeedEikonalSolver 663 | EikonalSolverType; 664 | 665 | // Arrange. 666 | auto const grid_size = util::FilledArray(size_t{16}); 667 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 668 | auto const uniform_speed = ScalarType{1}; 669 | 670 | auto box_corner1 = util::FilledArray(int32_t{1}); 671 | auto box_size1 = util::FilledArray(size_t{10}); 672 | auto box_corner2 = util::FilledArray(int32_t{5}); 673 | auto box_size2 = util::FilledArray(size_t{10}); 674 | 675 | auto box_boundary_indices1 = std::vector>(); 676 | auto box_boundary_times1 = std::vector(); 677 | util::BoxBoundaryCells(box_corner1, box_size1, grid_size, 678 | &box_boundary_indices1, &box_boundary_times1); 679 | auto box_boundary_indices2 = std::vector>(); 680 | auto box_boundary_times2 = std::vector(); 681 | util::BoxBoundaryCells(box_corner2, box_size2, grid_size, 682 | &box_boundary_indices2, &box_boundary_times2); 683 | 684 | // Merge and remove duplicate indices. 685 | auto boundary_indices = box_boundary_indices1; 686 | auto boundary_distances = box_boundary_times1; 687 | for (auto i = std::size_t{0}; i < box_boundary_indices2.size(); ++i) { 688 | auto const index = box_boundary_indices2[i]; 689 | if (find(std::begin(boundary_indices), std::end(boundary_indices), index) == 690 | std::end(boundary_indices)) { 691 | boundary_indices.push_back(index); 692 | boundary_distances.push_back(box_boundary_times2[i]); 693 | } 694 | } 695 | 696 | // Act. 697 | auto signed_times = 698 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_distances, 699 | EikonalSolverType(grid_spacing, uniform_speed)); 700 | 701 | // Assert. 702 | auto time_grid = 703 | util::Grid(grid_size, signed_times.front()); 704 | auto const time0 = time_grid.Cell(util::FilledArray(int32_t{0})); 705 | auto const time2 = time_grid.Cell(util::FilledArray(int32_t{2})); 706 | auto const time6 = time_grid.Cell(util::FilledArray(int32_t{6})); 707 | auto const time14 = 708 | time_grid.Cell(util::FilledArray(int32_t{14})); 709 | ASSERT_GT(time0, ScalarType{0}); 710 | ASSERT_LT(time2, ScalarType{0}); 711 | ASSERT_LT(time6, ScalarType{0}); 712 | ASSERT_LT(time14, ScalarType{0}); 713 | } 714 | 715 | TYPED_TEST(SignedArrivalTimeAccuracyTest, PointSourceAccuracy) { 716 | typedef typename TypeParam::ScalarType ScalarType; 717 | static constexpr auto kDimension = TypeParam::kDimension; 718 | namespace fmm = thinks::fast_marching_method; 719 | typedef fmm::UniformSpeedEikonalSolver 720 | EikonalSolverType; 721 | typedef fmm::DistanceSolver DistanceSolverType; 722 | typedef fmm::HighAccuracyUniformSpeedEikonalSolver 723 | HighAccuracyEikonalSolverType; 724 | 725 | // Arrange. 726 | auto const grid_size = util::FilledArray(size_t{41}); 727 | auto const grid_spacing = util::FilledArray(ScalarType{1}); 728 | auto const uniform_speed = ScalarType(1); 729 | 730 | // Simple point boundary for regular fast marching. 731 | auto boundary_indices = std::vector>( 732 | std::size_t{1}, util::FilledArray(int32_t{20})); 733 | auto boundary_times = std::vector(size_t{1}, ScalarType{0}); 734 | 735 | auto center_position = util::FilledArray(ScalarType(0)); 736 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 737 | center_position[i] = 738 | (boundary_indices[0][i] + ScalarType(0.5)) * grid_spacing[i]; 739 | } 740 | 741 | // Compute exact distances in vertex neighborhood for high accuracy 742 | // fast marching. 743 | auto ha_boundary_indices = std::vector>(); 744 | ha_boundary_indices.push_back( 745 | util::FilledArray(int32_t{20})); // Center. 746 | auto const vtx_neighbor_offsets = util::VertexNeighborOffsets(); 747 | for (auto const& vtx_neighbor_offset : vtx_neighbor_offsets) { 748 | auto index = boundary_indices[0]; 749 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 750 | index[i] += vtx_neighbor_offset[i]; 751 | } 752 | ha_boundary_indices.push_back(index); 753 | } 754 | auto ha_boundary_times = std::vector(); 755 | ha_boundary_times.push_back(ScalarType{0}); // Center. 756 | for (auto j = std::size_t{1}; j < ha_boundary_indices.size(); ++j) { 757 | auto const& index = ha_boundary_indices[j]; 758 | auto position = util::FilledArray(ScalarType(0)); 759 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 760 | position[i] = (index[i] + ScalarType(0.5)) * grid_spacing[i]; 761 | } 762 | auto delta = util::FilledArray(ScalarType(0)); 763 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 764 | delta[i] = center_position[i] - position[i]; 765 | } 766 | ha_boundary_times.push_back(util::Magnitude(delta)); 767 | } 768 | 769 | // Act. 770 | auto signed_time = 771 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 772 | EikonalSolverType(grid_spacing, uniform_speed)); 773 | auto signed_distance = 774 | fmm::SignedArrivalTime(grid_size, boundary_indices, boundary_times, 775 | DistanceSolverType(grid_spacing[0])); 776 | auto ha_signed_time = fmm::SignedArrivalTime( 777 | grid_size, ha_boundary_indices, ha_boundary_times, 778 | HighAccuracyEikonalSolverType(grid_spacing, uniform_speed)); 779 | 780 | // Compute errors. 781 | auto time_grid = 782 | util::Grid(grid_size, signed_time.front()); 783 | auto distance_grid = 784 | util::Grid(grid_size, signed_distance.front()); 785 | auto ha_time_grid = 786 | util::Grid(grid_size, ha_signed_time.front()); 787 | 788 | auto time_abs_errors = std::vector(); 789 | auto distance_abs_errors = std::vector(); 790 | auto ha_time_abs_errors = std::vector(); 791 | 792 | auto index_iter = util::IndexIterator(grid_size); 793 | while (index_iter.has_next()) { 794 | auto const index = index_iter.index(); 795 | auto position = util::FilledArray(ScalarType(0)); 796 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 797 | position[i] = (index[i] + ScalarType(0.5)) * grid_spacing[i]; 798 | } 799 | auto delta = util::FilledArray(ScalarType(0)); 800 | for (auto i = std::size_t{0}; i < kDimension; ++i) { 801 | delta[i] = center_position[i] - position[i]; 802 | } 803 | auto const gt = util::Magnitude(delta); 804 | 805 | auto const time = time_grid.Cell(index); 806 | auto const distance = distance_grid.Cell(index); 807 | auto const ha_time = ha_time_grid.Cell(index); 808 | auto const time_abs_error = abs(time - gt); 809 | auto const distance_abs_error = abs(distance - gt); 810 | auto const ha_time_abs_error = abs(ha_time - gt); 811 | if (gt <= ScalarType{20}) { 812 | time_abs_errors.push_back(time_abs_error); 813 | distance_abs_errors.push_back(distance_abs_error); 814 | ha_time_abs_errors.push_back(ha_time_abs_error); 815 | } 816 | index_iter.Next(); 817 | } 818 | 819 | auto time_max_abs_error = ScalarType{0}; 820 | auto time_avg_abs_error = ScalarType{0}; 821 | for (auto const& time_abs_error : time_abs_errors) { 822 | time_max_abs_error = std::max(time_max_abs_error, time_abs_error); 823 | time_avg_abs_error += time_abs_error; 824 | } 825 | time_avg_abs_error /= time_abs_errors.size(); 826 | 827 | auto distance_max_abs_error = ScalarType{0}; 828 | auto distance_avg_abs_error = ScalarType{0}; 829 | for (auto const& distance_abs_error : distance_abs_errors) { 830 | distance_max_abs_error = 831 | std::max(distance_max_abs_error, distance_abs_error); 832 | distance_avg_abs_error += distance_abs_error; 833 | } 834 | distance_avg_abs_error /= distance_abs_errors.size(); 835 | 836 | auto ha_time_max_abs_error = ScalarType{0}; 837 | auto ha_time_avg_abs_error = ScalarType{0}; 838 | for (auto const& ha_dist_abs_error : ha_time_abs_errors) { 839 | ha_time_max_abs_error = std::max(ha_time_max_abs_error, ha_dist_abs_error); 840 | ha_time_avg_abs_error += ha_dist_abs_error; 841 | } 842 | ha_time_avg_abs_error /= ha_time_abs_errors.size(); 843 | 844 | // Assert. 845 | typedef util::PointSourceAccuracyBounds Bounds; 846 | ASSERT_LE(time_max_abs_error, ScalarType(Bounds::max_abs_error())); 847 | ASSERT_LE(time_avg_abs_error, ScalarType(Bounds::avg_abs_error())); 848 | ASSERT_LE(distance_max_abs_error, ScalarType(Bounds::max_abs_error())); 849 | ASSERT_LE(distance_avg_abs_error, ScalarType(Bounds::avg_abs_error())); 850 | ASSERT_LE(ha_time_max_abs_error, 851 | ScalarType(Bounds::high_accuracy_max_abs_error())); 852 | ASSERT_LE(ha_time_avg_abs_error, 853 | ScalarType(Bounds::high_accuracy_avg_abs_error())); 854 | } 855 | 856 | } // namespace 857 | -------------------------------------------------------------------------------- /tests/util.hpp: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Tommy Hinks 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | // DEALINGS IN THE SOFTWARE. 20 | 21 | #ifndef TESTS_UTIL_HPP_ 22 | #define TESTS_UTIL_HPP_ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | namespace util { 37 | 38 | template 39 | class IndexIterator; 40 | 41 | template 42 | struct ScalarDimensionPair { 43 | typedef S ScalarType; 44 | static constexpr std::size_t kDimension = N; 45 | }; 46 | 47 | // Numbers below inspired by the paper "ON THE IMPLEMENTATION OF FAST 48 | // MARCHING METHODS FOR 3D LATTICES" by J. Andreas Bærentzen. 49 | template 50 | struct PointSourceAccuracyBounds; 51 | 52 | template <> 53 | struct PointSourceAccuracyBounds<1> { 54 | static constexpr double max_abs_error() { return double{1e-3}; } 55 | static constexpr double avg_abs_error() { return double{1e-3}; } 56 | static constexpr double high_accuracy_max_abs_error() { return double{1e-3}; } 57 | static constexpr double high_accuracy_avg_abs_error() { return double{1e-3}; } 58 | }; 59 | 60 | template <> 61 | struct PointSourceAccuracyBounds<2> { 62 | static constexpr double max_abs_error() { return double{1.48}; } 63 | static constexpr double avg_abs_error() { return double{0.89}; } 64 | static constexpr double high_accuracy_max_abs_error() { return double{0.29}; } 65 | static constexpr double high_accuracy_avg_abs_error() { return double{0.14}; } 66 | }; 67 | 68 | template <> 69 | struct PointSourceAccuracyBounds<3> { 70 | static constexpr double max_abs_error() { return double{1.51}; } 71 | static constexpr double avg_abs_error() { return double{0.92}; } 72 | static constexpr double high_accuracy_max_abs_error() { return double{0.28}; } 73 | static constexpr double high_accuracy_avg_abs_error() { return double{0.07}; } 74 | }; 75 | 76 | template <> 77 | struct PointSourceAccuracyBounds<4> { 78 | static constexpr double max_abs_error() { return double{1.98}; } 79 | static constexpr double avg_abs_error() { return double{1.27}; } 80 | static constexpr double high_accuracy_max_abs_error() { return double{0.28}; } 81 | static constexpr double high_accuracy_avg_abs_error() { return double{0.06}; } 82 | }; 83 | 84 | //! Returns array @a as a string. 85 | template 86 | std::string ToString(std::array const& a) { 87 | auto ss = std::stringstream(); 88 | ss << "["; 89 | for (auto i = std::size_t{0}; i < N; ++i) { 90 | ss << a[i]; 91 | if (i != (N - 1)) { 92 | ss << ", "; 93 | } 94 | } 95 | ss << "]"; 96 | 97 | return ss.str(); 98 | } 99 | 100 | //! Returns an array with all elements initialized to have value @a v. 101 | template 102 | inline std::array FilledArray(T const v) { 103 | auto r = std::array{}; 104 | std::fill(std::begin(r), std::end(r), v); 105 | return r; 106 | } 107 | 108 | //! Returns the product of the elements in array @a a. 109 | //! Note: Not checking for integer overflow here! 110 | template 111 | inline std::size_t LinearSize(std::array const& a) { 112 | return std::accumulate(std::begin(a), std::end(a), std::size_t{1}, 113 | std::multiplies()); 114 | } 115 | 116 | //! Returns the magnitude of the std::vector @a v. 117 | template 118 | T Magnitude(std::array const& v) { 119 | static_assert(std::is_floating_point::value, 120 | "scalar type must be floating point"); 121 | 122 | auto mag_squared = T{0}; 123 | for (auto i = std::size_t{0}; i < N; ++i) { 124 | mag_squared += v[i] * v[i]; 125 | } 126 | return sqrt(mag_squared); 127 | } 128 | 129 | //! Returns the distance between positions @a u and @a v. 130 | template 131 | T Distance(std::array const& u, std::array const& v) { 132 | static_assert(std::is_floating_point::value, 133 | "scalar type must be floating point"); 134 | 135 | auto distance_squared = T{0}; 136 | for (auto i = std::size_t{0}; i < N; ++i) { 137 | auto const delta = u[i] - v[i]; 138 | distance_squared += delta * delta; 139 | } 140 | return std::sqrt(distance_squared); 141 | } 142 | 143 | //! Returns a pair where the first element is true if the provided function 144 | //! (@a func) threw an exception of the expected type. The second element is 145 | //! set to the exception message. 146 | //! 147 | //! Exceptions that are not of the expected type are re-thrown. 148 | //! 149 | //! E - The expected exception type. 150 | //! F - A callable type. 151 | template 152 | std::pair FunctionThrows(F const func) { 153 | auto thrown_expected = false; 154 | auto reason = std::string(); 155 | try { 156 | func(); 157 | } catch (std::exception& ex) { 158 | auto const typed_ex = dynamic_cast(&ex); 159 | if (typed_ex != nullptr) { 160 | thrown_expected = true; 161 | reason = typed_ex->what(); 162 | } else { 163 | throw; // Unexpected exception type, re-throw. 164 | } 165 | } 166 | 167 | return {thrown_expected, reason}; 168 | } 169 | 170 | //! Returns @a base ^ @a exponent as a compile-time constant. 171 | constexpr std::size_t static_pow(std::size_t base, std::size_t const exponent) { 172 | // NOTE: Cannot use loops in constexpr functions in C++11, have to use 173 | // recursion here. 174 | return exponent == std::size_t{0} ? std::size_t{1} 175 | : base * static_pow(base, exponent - 1); 176 | } 177 | 178 | //! Returns true if @a index is inside @a size, otherwise false. 179 | template 180 | bool Inside(std::array const& index, 181 | std::array const& size) { 182 | static_assert(N > 0, "invalid dimensionality"); 183 | 184 | for (auto i = std::size_t{0}; i < N; ++i) { 185 | // Cast is safe since we check that index[i] is greater than or 186 | // equal to zero first. 187 | if (!(int32_t{0} <= index[i] && static_cast(index[i]) < size[i])) { 188 | return false; 189 | } 190 | } 191 | return true; 192 | } 193 | 194 | //! Returns a list of face neighbor offset for an N-dimensional cell. 195 | //! In 2D this is the 4-neighborhood, in 3D the 6-neighborhood, etc. 196 | template 197 | std::array, 2 * N> FaceNeighborOffsets() { 198 | static_assert(N > 0, "dimensionality cannot be zero"); 199 | 200 | auto offsets = std::array, std::size_t{2} * N>{}; 201 | for (auto i = std::size_t{0}; i < N; ++i) { 202 | for (auto j = std::size_t{0}; j < N; ++j) { 203 | if (j == i) { 204 | offsets[2 * i + 0][j] = int32_t{+1}; 205 | offsets[2 * i + 1][j] = int32_t{-1}; 206 | } else { 207 | offsets[2 * i + 0][j] = int32_t{0}; 208 | offsets[2 * i + 1][j] = int32_t{0}; 209 | } 210 | } 211 | } 212 | return offsets; 213 | } 214 | 215 | //! DOCS 216 | template 217 | inline std::array, static_pow(3, N) - 1> 218 | VertexNeighborOffsets() { 219 | auto neighbor_offsets = 220 | std::array, static_pow(3, N) - 1>(); 221 | auto offset_index = std::size_t{0}; 222 | auto index_size = std::array{}; 223 | std::fill(std::begin(index_size), std::end(index_size), std::size_t{3}); 224 | auto index_iter = IndexIterator(index_size); 225 | while (index_iter.has_next()) { 226 | auto offset = index_iter.index(); 227 | std::for_each(std::begin(offset), std::end(offset), 228 | [](auto& d) { d -= int32_t{1}; }); 229 | if (!std::all_of(std::begin(offset), std::end(offset), 230 | [](auto const i) { return i == 0; })) { 231 | neighbor_offsets[offset_index++] = offset; 232 | } 233 | index_iter.Next(); 234 | } 235 | assert(offset_index == static_pow(3, N) - 1); 236 | 237 | return neighbor_offsets; 238 | } 239 | 240 | //! Access a linear array as if it were an N-dimensional grid. 241 | //! 242 | //! Usage: 243 | //! auto size = std::array(); 244 | //! size[0] = 2; 245 | //! size[1] = 2; 246 | //! auto cells = std::vector(4); 247 | //! cells[0] = 0.f; 248 | //! cells[1] = 1.f; 249 | //! cells[2] = 2.f; 250 | //! cells[3] = 3.f; 251 | //! auto grid = Grid(size, cells.front()); 252 | //! std::cout << grid.cell({{0, 0}}) << std::endl; 253 | //! std::cout << grid.cell({{1, 0}}) << std::endl; 254 | //! std::cout << grid.cell({{0, 1}}) << std::endl; 255 | //! std::cout << grid.cell({{1, 1}}) << std::endl; 256 | //! 257 | //! Output: 258 | //! 0 259 | //! 1 260 | //! 2 261 | //! 3 262 | template 263 | class Grid { 264 | public: 265 | typedef T CellType; 266 | typedef std::array SizeType; 267 | typedef std::array IndexType; 268 | 269 | Grid(SizeType const& size, T& cells) : size_(size), cells_(&cells) { 270 | auto stride = std::size_t{1}; 271 | for (auto i = std::size_t{1}; i < N; ++i) { 272 | stride *= size_[i - 1]; 273 | strides_[i - 1] = stride; 274 | } 275 | } 276 | 277 | SizeType size() const { return size_; } 278 | 279 | //! Returns a reference to the cell at @a index. No range checking! 280 | CellType& Cell(IndexType const& index) { return cells_[LinearIndex_(index)]; } 281 | 282 | //! Returns a const reference to the cell at @a index. No range checking! 283 | CellType const& Cell(IndexType const& index) const { 284 | return cells_[LinearIndex_(index)]; 285 | } 286 | 287 | private: 288 | //! Returns a linear (scalar) index into an array representing an 289 | //! N-dimensional grid for integer coordinate @a index. 290 | //! Note that this function does not check for integer overflow! 291 | std::size_t LinearIndex_(IndexType const& index) const { 292 | assert(0 <= index[0] && static_cast(index[0]) < size_[0]); 293 | auto k = static_cast(index[0]); 294 | for (auto i = std::size_t{1}; i < N; ++i) { 295 | assert(0 <= index[i] && static_cast(index[i]) < size_[i]); 296 | k += index[i] * strides_[i - 1]; 297 | } 298 | return k; 299 | } 300 | 301 | std::array const size_; 302 | std::array strides_; 303 | CellType* const cells_; 304 | }; 305 | 306 | //! Iterates over a size in N dimensions. 307 | //! 308 | //! Usage: 309 | //! auto size = std::array(); 310 | //! size[0] = 2; 311 | //! size[1] = 2; 312 | //! auto index_iter = IndexIterator(size); 313 | //! while (index_iter.has_next()) { 314 | //! auto index = index_iter.index(); 315 | //! std::cout << "[" << index[0] << ", " << index[1] << "]" << std::endl; 316 | //! index_iter.Next(); 317 | //! } 318 | //! 319 | //! Output: 320 | //! [0, 0] 321 | //! [1, 0] 322 | //! [0, 1] 323 | //! [1, 1] 324 | template 325 | class IndexIterator { 326 | public: 327 | explicit IndexIterator(std::array const& size) 328 | : size_(size), 329 | index_(FilledArray(int32_t{0})) // Start at origin. 330 | , 331 | has_next_(true) { 332 | static_assert(N > 0, "must have at least one dimension"); 333 | 334 | for (auto const s : size) { 335 | if (s < 1) { 336 | throw std::runtime_error("zero element in size"); 337 | } 338 | } 339 | } 340 | 341 | //! Returns the current index of the iterator. 342 | std::array index() const { return index_; } 343 | 344 | //! Returns the size over which the iterator is iterating. 345 | std::array size() const { return size_; } 346 | 347 | //! Returns true if the iterator can be incremented (i.e. Next() can be 348 | //! called without throwing), otherwise false. 349 | bool has_next() const { return has_next_; } 350 | 351 | //! Increments the index of the iterator and returns true if it can be 352 | //! incremented again, otherwise false. 353 | //! 354 | //! Throws std::runtime_error if the iterator cannot be incremented. 355 | bool Next() { 356 | if (!has_next_) { 357 | throw std::runtime_error("index iterator cannot be incremented"); 358 | } 359 | 360 | auto i = std::size_t{0}; 361 | while (i < N) { 362 | if (static_cast(index_[i] + 1) < size_[i]) { 363 | ++index_[i]; 364 | return true; 365 | } else { 366 | index_[i++] = 0; 367 | } 368 | } 369 | 370 | // Nothing was incremented. 371 | has_next_ = false; 372 | return false; 373 | } 374 | 375 | private: 376 | std::array const size_; 377 | std::array index_; 378 | bool has_next_; 379 | }; 380 | 381 | //! Returns the center position of the cell at @a index using @a grid_spacing. 382 | template 383 | inline std::array CellCenter(std::array const& index, 384 | std::array const& grid_spacing) { 385 | auto cell_center = std::array{}; 386 | for (auto i = std::size_t{0}; i < N; ++i) { 387 | cell_center[i] = (index[i] + T(0.5)) * grid_spacing[i]; 388 | } 389 | return cell_center; 390 | } 391 | 392 | //! Returns an array of the corner positions of the cell at @a index using 393 | //! @a grid_spacing. The number of corner positions depends on the 394 | //! dimensionality of the cell. 395 | template 396 | inline std::array, static_pow(2, N)> CellCorners( 397 | std::array const& index, 398 | std::array const& grid_spacing) { 399 | auto cell_corners = std::array, static_pow(2, N)>{}; 400 | for (auto i = std::size_t{0}; i < static_pow(2, N); ++i) { 401 | auto const bits = std::bitset(i); 402 | for (auto k = std::size_t{0}; k < N; ++k) { 403 | cell_corners[i][k] = 404 | (index[k] + static_cast(bits[k])) * grid_spacing[k]; 405 | } 406 | } 407 | return cell_corners; 408 | } 409 | 410 | //! DOCS 411 | template 412 | void HyperSphereBoundaryCells( 413 | std::array const& center, T const radius, 414 | std::array const& grid_size, 415 | std::array const& grid_spacing, D const distance_modifier, 416 | std::size_t const dilation_pass_count, 417 | std::vector>* boundary_indices, 418 | std::vector* boundary_distances, 419 | std::vector* distance_ground_truth_buffer = nullptr) { 420 | auto distance_ground_truth_grid = std::unique_ptr>(); 421 | if (distance_ground_truth_buffer != nullptr) { 422 | distance_ground_truth_buffer->resize(LinearSize(grid_size)); 423 | distance_ground_truth_grid.reset( 424 | new Grid(grid_size, distance_ground_truth_buffer->front())); 425 | } 426 | 427 | auto foreground_indices = std::vector>{}; 428 | 429 | // Visit each cell exactly once. 430 | auto index_iter = IndexIterator(grid_size); 431 | while (index_iter.has_next()) { 432 | auto const index = index_iter.index(); 433 | auto const cell_corners = CellCorners(index, grid_spacing); 434 | 435 | auto inside_count = std::size_t{0}; 436 | auto outside_count = std::size_t{0}; 437 | 438 | for (auto const& cell_corner : cell_corners) { 439 | auto const d = Distance(center, cell_corner); 440 | if (d < radius) { 441 | ++inside_count; 442 | } else { 443 | ++outside_count; 444 | } 445 | } 446 | 447 | auto const cell_center = CellCenter(index, grid_spacing); 448 | auto const cell_distance = 449 | distance_modifier(Distance(center, cell_center) - radius); 450 | 451 | if (inside_count > 0 && outside_count > 0) { 452 | // The inferface passes through this cell so we freeze it. 453 | boundary_indices->push_back(index); 454 | boundary_distances->push_back(cell_distance); 455 | foreground_indices.push_back(index); 456 | } 457 | 458 | // Update ground truth for all cells. 459 | if (distance_ground_truth_grid != nullptr) { 460 | distance_ground_truth_grid->Cell(index) = cell_distance; 461 | } 462 | 463 | index_iter.Next(); 464 | } 465 | 466 | if (dilation_pass_count > 0 && !foreground_indices.empty()) { 467 | enum class LabelCell : uint8_t { kBackground = uint8_t{0}, kForeground }; 468 | 469 | auto label_buffer = 470 | std::vector(LinearSize(grid_size), LabelCell::kBackground); 471 | auto label_grid = Grid(grid_size, label_buffer.front()); 472 | for (auto const& foreground_index : foreground_indices) { 473 | label_grid.Cell(foreground_index) = LabelCell::kForeground; 474 | } 475 | 476 | auto const face_neighbor_offsets = util::FaceNeighborOffsets(); 477 | auto const neighbor_offset_begin = std::begin(face_neighbor_offsets); 478 | auto const neighbor_offset_end = std::end(face_neighbor_offsets); 479 | 480 | auto old_foreground_indices = foreground_indices; 481 | auto new_foreground_indices = std::vector>{}; 482 | for (auto i = std::size_t{0}; i < dilation_pass_count; ++i) { 483 | for (auto const foreground_index : old_foreground_indices) { 484 | for (auto neighbor_offset_iter = neighbor_offset_begin; 485 | neighbor_offset_iter != neighbor_offset_end; 486 | ++neighbor_offset_iter) { 487 | auto neighbor_index = foreground_index; 488 | for (auto i = std::size_t{0}; i < N; ++i) { 489 | neighbor_index[i] += (*neighbor_offset_iter)[i]; 490 | } 491 | 492 | if (Inside(neighbor_index, label_grid.size()) && 493 | label_grid.Cell(neighbor_index) == LabelCell::kBackground) { 494 | auto const cell_center = CellCenter(neighbor_index, grid_spacing); 495 | auto const cell_distance = 496 | distance_modifier(Distance(center, cell_center) - radius); 497 | boundary_indices->push_back(neighbor_index); 498 | boundary_distances->push_back(cell_distance); 499 | 500 | label_grid.Cell(neighbor_index) = LabelCell::kForeground; 501 | new_foreground_indices.push_back(neighbor_index); 502 | } 503 | } 504 | } 505 | } 506 | 507 | old_foreground_indices = new_foreground_indices; 508 | new_foreground_indices = std::vector>{}; 509 | } 510 | } 511 | 512 | //! DOCS 513 | template 514 | void BoxBoundaryCells( 515 | std::array const& box_corner, 516 | std::array const& box_size, 517 | std::array const& grid_size, 518 | std::vector>* boundary_indices, 519 | std::vector* boundary_distances) { 520 | auto box_min = box_corner; 521 | auto box_max = box_corner; 522 | for (auto i = std::size_t{0}; i < N; ++i) { 523 | box_max[i] += box_size[i]; 524 | } 525 | 526 | auto inside = [](auto const& index, auto const& box_min, 527 | auto const& box_max) { 528 | for (auto i = std::size_t{0}; i < N; ++i) { 529 | if (!(box_min[i] <= index[i] && index[i] <= box_max[i])) { 530 | return false; 531 | } 532 | } 533 | return true; 534 | }; 535 | 536 | auto index_iter = util::IndexIterator(grid_size); 537 | while (index_iter.has_next()) { 538 | auto const index = index_iter.index(); 539 | if (inside(index, box_min, box_max)) { 540 | for (auto i = std::size_t{0}; i < N; ++i) { 541 | if (index[i] == box_min[i] || index[i] == box_max[i]) { 542 | boundary_indices->push_back(index); 543 | break; 544 | } 545 | } 546 | } 547 | index_iter.Next(); 548 | } 549 | 550 | *boundary_distances = std::vector(boundary_indices->size(), T{0}); 551 | } 552 | 553 | } // namespace util 554 | 555 | #endif // TESTS_UTIL_HPP_ 556 | --------------------------------------------------------------------------------