go home Home | Main Page | Modules | Namespace List | Class Hierarchy | Alphabetical List | Data Structures | File List | Namespace Members | Data Fields | Globals | Related Pages
elxCoreMainGTestUtilities.h
Go to the documentation of this file.
1/*=========================================================================
2 *
3 * Copyright UMC Utrecht and contributors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0.txt
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 *=========================================================================*/
18
19#ifndef elxCoreMainGTestUtilities_h
20#define elxCoreMainGTestUtilities_h
21
22#include <elxBaseComponent.h> // For elx.
23#include <elxParameterObject.h>
24#include <elxConversion.h>
25
26#include <itkDeref.h>
27#include <itkImage.h>
28#include <itkImageBase.h>
29#include <itkImageBufferRange.h>
30#include <itkImageRegionRange.h>
31#include <itkIndex.h>
32#include <itkSize.h>
33
34#include <algorithm> // For fill and transform.
35#include <array>
36#include <cmath> // For round.
37#include <initializer_list>
38#include <iterator> // For begin and end.
39#include <map>
40#include <numeric> // For iota.
41#include <random>
42#include <string>
43#include <type_traits> // For is_pointer, is_same, and integral_constant.
44#include <vector>
45
46// GoogleTest header file:
47#include <gtest/gtest.h>
48
49
50namespace elastix
51{
52namespace CoreMainGTestUtilities
53{
54
56template <typename TNested>
58{
59 using Type = TNested;
60};
61
63class Exception : public std::exception
64{
65 const char * m_message = "";
66
67public:
68 explicit Exception(const char * const message)
69 : m_message(message)
70 {}
71
72 const char *
73 what() const noexcept override
74 {
75 return m_message;
76 }
77};
78
79
81#define ELX_GTEST_EXPECT_FALSE_AND_THROW_EXCEPTION_IF(condition) \
82 if (condition) \
83 { \
84 EXPECT_FALSE(true) << "Expected to be false: " #condition; \
85 throw ::elastix::CoreMainGTestUtilities::Exception("Exception thrown because " #condition); \
86 } \
87 static_assert(true, "Expect a semi-colon ';' at the end of a macro call")
88
89template <typename TSmartPointer>
90decltype(auto)
91DerefSmartPointer(const TSmartPointer & ptr)
92{
93 static_assert(!std::is_pointer_v<TSmartPointer>, "For raw pointers, use itk::Deref instead!");
94
95 if (ptr == nullptr)
96 {
97 throw Exception("DerefSmartPointer error: the (smart) pointer should not be null!");
98 }
99 return *ptr;
100}
101
102
105template <typename T>
106decltype(T().front())
107Front(T & container)
108{
109 if (container.empty())
110 {
111 throw Exception("Front error: the container should be non-empty!");
112 }
113 return container.front();
114}
115
116
117template <typename T>
118itk::SmartPointer<T>
120{
121 static_assert(std::is_same<decltype(T::New()), itk::SmartPointer<T>>{},
122 "T::New() must return an itk::SmartPointer<T>!");
123
124 const auto ptr = T::New();
125 if (ptr == nullptr)
126 {
127 throw Exception("New() error: should not return null!");
128 }
129 return ptr;
130}
131
133template <typename TPixel, unsigned int VImageDimension>
134void
135FillImageRegion(itk::Image<TPixel, VImageDimension> & image,
136 const itk::Index<VImageDimension> & regionIndex,
137 const itk::Size<VImageDimension> & regionSize)
138{
139 const itk::ImageRegionRange<itk::Image<TPixel, VImageDimension>> imageRegionRange{
140 image, itk::ImageRegion<VImageDimension>{ regionIndex, regionSize }
141 };
142 std::fill(std::begin(imageRegionRange), std::end(imageRegionRange), 1);
143}
144
145
146// Converts the specified strings to a vector of double.
147// Assumes that each string represents a floating point number.
148inline std::vector<double>
149ConvertStringsToVectorOfDouble(const std::vector<std::string> & strings)
150{
151 std::vector<double> vectorOfDouble(strings.size());
152
153 std::transform(strings.cbegin(), strings.cend(), vectorOfDouble.begin(), [](const std::string & str) {
154 std::size_t index{};
155 const auto result = std::stod(str, &index);
156
157 // Test that all characters have been processed, by std::stod.
158 EXPECT_EQ(index, str.size());
159 return result;
160 });
161
162 return vectorOfDouble;
163}
164
165
166// Converts the specified vector of double to itk::Offset, by rounding each element.
167template <std::size_t VDimension>
168itk::Offset<VDimension>
169ConvertToOffset(const std::vector<double> & doubles)
170{
171 ELX_GTEST_EXPECT_FALSE_AND_THROW_EXCEPTION_IF(doubles.size() != VDimension);
172
173 itk::Offset<VDimension> result;
174 std::size_t i{};
175
176 for (const double value : doubles)
177 {
178 const auto roundedValue = std::round(value);
179
180 EXPECT_GE(roundedValue, std::numeric_limits<itk::OffsetValueType>::min());
181 EXPECT_LE(roundedValue, std::numeric_limits<itk::OffsetValueType>::max());
182
183 result[i] = static_cast<itk::OffsetValueType>(roundedValue);
184 ++i;
185 }
186
187 return result;
188}
189
190
191std::map<std::string, std::vector<std::string>> inline CreateParameterMap(
192 std::initializer_list<std::pair<std::string, std::vector<std::string>>> initializerList)
193{
194 std::map<std::string, std::vector<std::string>> result;
195
196 for (const auto & pair : initializerList)
197 {
198 EXPECT_TRUE(result.insert(pair).second);
199 }
200 return result;
201}
202
203
204std::map<std::string, std::vector<std::string>> inline CreateParameterMap(
205 std::initializer_list<std::pair<std::string, std::string>> initializerList)
206{
207 std::map<std::string, std::vector<std::string>> result;
208
209 for (const auto & pair : initializerList)
210 {
211 EXPECT_TRUE(result.insert({ pair.first, { pair.second } }).second);
212 }
213 return result;
214}
215
216
217template <unsigned VImageDimension>
218std::map<std::string, std::vector<std::string>>
219CreateParameterMap(std::initializer_list<std::pair<std::string, std::string>> initializerList)
220{
221 std::map<std::string, std::vector<std::string>> result = CreateParameterMap(initializerList);
222
223 for (const auto & key : { "FixedImageDimension", "MovingImageDimension" })
224 {
225 EXPECT_TRUE(result.insert({ key, { std::to_string(VImageDimension) } }).second);
226 }
227 return result;
228}
229
230
232 std::initializer_list<std::pair<std::string, std::string>> initializerList)
233{
234 const auto parameterObject = ParameterObject::New();
235 parameterObject->SetParameterMap(CreateParameterMap(initializerList));
236 return parameterObject;
237}
238
239
241{
242 const auto parameterObject = ParameterObject::New();
243 parameterObject->SetParameterMap(parameterMap);
244 return parameterObject;
245}
246
247
248inline std::vector<double>
249GetTransformParametersFromMaps(const std::vector<ParameterObject::ParameterMapType> & transformParameterMaps)
250{
251 // For the time being, only support a single parameter map here.
252 EXPECT_EQ(transformParameterMaps.size(), 1);
253
254 if (transformParameterMaps.empty())
255 {
256 throw Exception("Error: GetTransformParametersFromMaps should not return an empty ParameterMap!");
257 }
258
259 const auto & transformParameterMap = transformParameterMaps.front();
260 const auto found = transformParameterMap.find("TransformParameters");
261
262 if (found == transformParameterMap.cend())
263 {
264 throw Exception("Error: GetTransformParametersFromMaps did not find TransformParameters!");
265 }
266 return ConvertStringsToVectorOfDouble(found->second);
267}
268
269
270template <typename TFilter>
271std::vector<double>
273{
274 const auto transformParameterObject = filter.GetTransformParameterObject();
275 const auto & transformParameterMaps = itk::Deref(transformParameterObject).GetParameterMaps();
276 return GetTransformParametersFromMaps(transformParameterMaps);
277}
278
279
280// ITK's RecursiveSeparableImageFilter "requires a minimum of four pixels along the dimension to be processed", at
281// https://github.com/InsightSoftwareConsortium/ITK/blob/v5.3.0/Modules/Filtering/ImageFilterBase/include/itkRecursiveSeparableImageFilter.hxx#L226
282constexpr itk::SizeValueType minimumImageSizeValue{ 4 };
283
284
285// The image domain. ITK calls it the "geometry" of an image. ("The geometry of an image is defined by its position,
286// orientation, spacing, and extent", according to https://itk.org/Doxygen52/html/classitk_1_1ImageBase.html#details).
287// The elastix manual (elastix-5.1.0-manual.pdf, January 16, 2023) simply calls it "the
288// Size/Spacing/Origin/Index/Direction settings".
289template <unsigned int VDimension>
291{
292 using ImageBaseType = itk::ImageBase<VDimension>;
293
294 using DirectionType = typename ImageBaseType::DirectionType;
295 using IndexType = typename ImageBaseType::IndexType;
296 using SizeType = typename ImageBaseType::SizeType;
297 using SpacingType = typename ImageBaseType::SpacingType;
298 using PointType = typename ImageBaseType::PointType;
299
300 DirectionType direction{ DirectionType::GetIdentity() };
301 IndexType index{};
302 SizeType size{};
303 SpacingType spacing{ itk::MakeFilled<SpacingType>(1.0) };
304 PointType origin{};
305
306 // Default-constructor
307 ImageDomain() = default;
308
309 // Explicit constructor
310 explicit ImageDomain(const SizeType & initialSize)
311 : size(initialSize)
312 {}
313
314 explicit ImageDomain(const ImageBaseType & image)
315 : direction(image.GetDirection())
316 , index(image.GetLargestPossibleRegion().GetIndex())
317 , size(image.GetLargestPossibleRegion().GetSize())
318 , spacing(image.GetSpacing())
319 , origin(image.GetOrigin())
320 {}
321
322 // Constructor, allowing to explicitly specify all the settings of the domain.
323 ImageDomain(const DirectionType & initialDirection,
324 const IndexType & initialIndex,
325 const SizeType & initialSize,
326 const SpacingType & initialSpacing,
327 const PointType & initialOrigin)
328 : direction(initialDirection)
329 , index(initialIndex)
330 , size(initialSize)
331 , spacing(initialSpacing)
332 , origin(initialOrigin)
333 {}
334
335 // Puts the domain settings into the specified image.
336 void
337 ToImage(itk::ImageBase<VDimension> & image) const
338 {
339 image.SetDirection(direction);
340 image.SetRegions({ index, size });
341 image.SetSpacing(spacing);
342 image.SetOrigin(origin);
343 }
344
345 // Returns the data of this image domain as an elastix/transformix parameter map.
348 {
349 return {
350 // Parameters in alphabetic order:
351 { "Direction", elx::Conversion::ToVectorOfStrings(direction) },
352 { "Index", elx::Conversion::ToVectorOfStrings(index) },
353 { "Origin", elx::Conversion::ToVectorOfStrings(origin) },
354 { "Size", elx::Conversion::ToVectorOfStrings(size) },
355 { "Spacing", elx::Conversion::ToVectorOfStrings(spacing) },
356 };
357 }
358
359 friend bool
360 operator==(const ImageDomain & lhs, const ImageDomain & rhs)
361 {
362 return lhs.direction == rhs.direction && lhs.index == rhs.index && lhs.size == rhs.size &&
363 lhs.spacing == rhs.spacing && lhs.origin == rhs.origin;
364 }
365
366 friend bool
367 operator!=(const ImageDomain & lhs, const ImageDomain & rhs)
368 {
369 return !(lhs == rhs);
370 }
371};
372
373
374template <typename TRandomNumberEngine>
375int
376GenerateRandomSign(TRandomNumberEngine & randomNumberEngine)
377{
378 return (randomNumberEngine() % 2 == 0) ? -1 : 1;
379}
380
381
382template <unsigned int VImageDimension>
383auto
384CreateRandomImageDomain(std::mt19937 & randomNumberEngine)
385{
386 using ImageDomainType = ImageDomain<VImageDimension>;
387
388 const auto createRandomDirection = [&randomNumberEngine] {
389 using DirectionType = typename ImageDomainType::DirectionType;
390 auto randomDirection = DirectionType::GetIdentity();
391
392 // For now, just a single random rotation
393 const auto randomRotation = std::uniform_real_distribution<>{ -M_PI, M_PI }(randomNumberEngine);
394 const auto cosRandomRotation = std::cos(randomRotation);
395 const auto sinRandomRotation = std::sin(randomRotation);
396
397 randomDirection[0][0] = cosRandomRotation;
398 randomDirection[0][1] = sinRandomRotation;
399 randomDirection[1][0] = -sinRandomRotation;
400 randomDirection[1][1] = cosRandomRotation;
401
402 return randomDirection;
403 };
404 const auto createRandomIndex = [&randomNumberEngine] {
405 typename ImageDomainType::IndexType randomIndex{};
406 // Originally tried `std::uniform_int_distribution<itk::IndexValueType>` with
407 // `std::numeric_limits<itk::IndexValueType>`, but that caused errors from ImageSamplerBase::CropInputImageRegion(),
408 // saying "ERROR: the bounding box of the mask lies entirely out of the InputImageRegion!"
409 std::generate(randomIndex.begin(), randomIndex.end(), [&randomNumberEngine] {
410 return std::uniform_int_distribution{ std::numeric_limits<int>::min() / 2,
411 std::numeric_limits<int>::max() / 2 }(randomNumberEngine);
412 });
413 return randomIndex;
414 };
415 const auto createRandomSmallImageSize = [&randomNumberEngine] {
416 typename ImageDomainType::SizeType randomImageSize{};
417 std::generate(randomImageSize.begin(), randomImageSize.end(), [&randomNumberEngine] {
418 return std::uniform_int_distribution<itk::SizeValueType>{ minimumImageSizeValue,
419 2 * minimumImageSizeValue }(randomNumberEngine);
420 });
421 return randomImageSize;
422 };
423 const auto createRandomSpacing = [&randomNumberEngine] {
424 typename ImageDomainType::SpacingType randomSpacing{};
425 std::generate(randomSpacing.begin(), randomSpacing.end(), [&randomNumberEngine] {
426 // Originally tried the maximum interval from std::numeric_limits<itk::SpacePrecisionType>::min() to
427 // std::numeric_limits<itk::SpacePrecisionType>::max(), but that caused errors during inverse matrix computation.
428 return std::uniform_real_distribution<itk::SpacePrecisionType>{ 0.1, 10.0 }(randomNumberEngine);
429 });
430 return randomSpacing;
431 };
432 const auto createRandomPoint = [&randomNumberEngine] {
433 typename ImageDomainType::PointType randomPoint{};
434 std::generate(randomPoint.begin(), randomPoint.end(), [&randomNumberEngine] {
435 // Originally tried an interval up to `std::numeric_limits<itk::SpacePrecisionType>::max() / 2.0`, but that caused
436 // errors from ImageSamplerBase::CropInputImageRegion(), saying "ERROR: the bounding box of the mask lies entirely
437 // out of the InputImageRegion!"
438 return std::uniform_real_distribution<itk::SpacePrecisionType>{
439 std::numeric_limits<int>::min(), std::numeric_limits<int>::max()
440 }(randomNumberEngine);
441 });
442 return randomPoint;
443 };
444
445 return ImageDomainType{ createRandomDirection(),
446 createRandomIndex(),
447 createRandomSmallImageSize(),
448 createRandomSpacing(),
449 createRandomPoint() };
450}
451
452
453// Creates a test image, filled with zero.
454template <typename TPixel, unsigned VImageDimension>
455auto
456CreateImage(const itk::Size<VImageDimension> & imageSize)
457{
458 const auto image = itk::Image<TPixel, VImageDimension>::New();
459 image->SetRegions(imageSize);
460 image->AllocateInitialized();
461 return image;
462}
463
464// Creates a test image, filled with zero.
465template <typename TPixel, unsigned VImageDimension>
466auto
468{
469 const auto image = itk::Image<TPixel, VImageDimension>::New();
470 imageDomain.ToImage(*image);
471 image->AllocateInitialized();
472 return image;
473}
474
475
476// Creates a test image, filled with a sequence of natural numbers, 1, 2, 3, ..., N.
477template <typename TPixel, unsigned VImageDimension>
478auto
480{
481 using ImageType = itk::Image<TPixel, VImageDimension>;
482 const auto image = ImageType::New();
483 imageDomain.ToImage(*image);
484 image->Allocate();
485 const itk::ImageBufferRange<ImageType> imageBufferRange{ *image };
486 std::iota(imageBufferRange.begin(), imageBufferRange.end(), TPixel{ 1 });
487 return image;
488}
489
490
491// Creates a test image, filled with a sequence of natural numbers, 1, 2, 3, ..., N.
492template <typename TPixel, unsigned VImageDimension>
493auto
494CreateImageFilledWithSequenceOfNaturalNumbers(const itk::Size<VImageDimension> & imageSize)
495{
496 return CreateImageFilledWithSequenceOfNaturalNumbers<TPixel>(ImageDomain<VImageDimension>{ imageSize });
497}
498
499
500std::string
502
503// Returns CMAKE_CURRENT_BINARY_DIR: the path to Core Main GTesting subdirectory of the elastix build tree (without
504// trailing slash).
505std::string
507
508// Returns the name of a test defined by `GTEST_TEST(TestSuiteName, TestName)` as "TestSuiteName_TestName_Test".
509std::string
510GetNameOfTest(const testing::Test &);
511
512} // namespace CoreMainGTestUtilities
513} // namespace elastix
514
515
516#endif
Simple exception class, to be used by unit tests.
const char * what() const noexcept override
itk::SmartPointer< Self > Pointer
std::map< ParameterKeyType, ParameterValueVectorType > ParameterMapType
#define ELX_GTEST_EXPECT_FALSE_AND_THROW_EXCEPTION_IF(condition)
Expect the specified condition to be false, and throw an exception if it is true.
auto CreateImageFilledWithSequenceOfNaturalNumbers(const ImageDomain< VImageDimension > &imageDomain)
decltype(auto) DerefSmartPointer(const TSmartPointer &ptr)
auto CreateRandomImageDomain(std::mt19937 &randomNumberEngine)
std::vector< double > GetTransformParametersFromMaps(const std::vector< ParameterObject::ParameterMapType > &transformParameterMaps)
std::string GetNameOfTest(const testing::Test &)
std::string GetCurrentBinaryDirectoryPath()
std::map< std::string, std::vector< std::string > > CreateParameterMap(std::initializer_list< std::pair< std::string, std::vector< std::string > > > initializerList)
decltype(T().front()) Front(T &container)
std::vector< double > GetTransformParametersFromFilter(TFilter &filter)
constexpr itk::SizeValueType minimumImageSizeValue
int GenerateRandomSign(TRandomNumberEngine &randomNumberEngine)
void FillImageRegion(itk::Image< TPixel, VImageDimension > &image, const itk::Index< VImageDimension > &regionIndex, const itk::Size< VImageDimension > &regionSize)
Fills the specified image region with pixel values 1.
ParameterObject::Pointer CreateParameterObject(std::initializer_list< std::pair< std::string, std::string > > initializerList)
auto CreateImage(const itk::Size< VImageDimension > &imageSize)
itk::Offset< VDimension > ConvertToOffset(const std::vector< double > &doubles)
std::vector< double > ConvertStringsToVectorOfDouble(const std::vector< std::string > &strings)
ParameterObject::ParameterMapType AsParameterMap() const
friend bool operator==(const ImageDomain &lhs, const ImageDomain &rhs)
typename ImageBaseType::DirectionType DirectionType
friend bool operator!=(const ImageDomain &lhs, const ImageDomain &rhs)
ImageDomain(const DirectionType &initialDirection, const IndexType &initialIndex, const SizeType &initialSize, const SpacingType &initialSpacing, const PointType &initialOrigin)
void ToImage(itk::ImageBase< VDimension > &image) const
Eases passing a type as argument to a generic lambda.


Generated on 2024-07-17 for elastix by doxygen 1.11.0 (9b424b03c9833626cd435af22a444888fbbb192d) elastix logo