go home Home | Main Page | Topics | Namespace List | Class Hierarchy | Alphabetical List | Data Structures | File List | Namespace Members | Data Fields | Globals | Related Pages
Loading...
Searching...
No Matches
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{
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
147template <typename TPixel, unsigned int VImageDimension>
148void
149FillImageRegionWithSequenceOfNaturalNumbers(itk::Image<TPixel, VImageDimension> & image,
150 const itk::Index<VImageDimension> & regionIndex,
151 const itk::Size<VImageDimension> & regionSize)
152{
153 const itk::ImageRegionRange<itk::Image<TPixel, VImageDimension>> imageRegionRange{
154 image, itk::ImageRegion<VImageDimension>{ regionIndex, regionSize }
155 };
156 std::iota(std::begin(imageRegionRange), std::end(imageRegionRange), TPixel{ 1 });
157}
158
159
160// Converts the specified strings to a vector of double.
161// Assumes that each string represents a floating point number.
162inline std::vector<double>
163ConvertStringsToVectorOfDouble(const std::vector<std::string> & strings)
164{
165 std::vector<double> vectorOfDouble(strings.size());
166
167 std::transform(strings.cbegin(), strings.cend(), vectorOfDouble.begin(), [](const std::string & str) {
168 std::size_t index{};
169 const auto result = std::stod(str, &index);
170
171 // Test that all characters have been processed, by std::stod.
172 EXPECT_EQ(index, str.size());
173 return result;
174 });
175
176 return vectorOfDouble;
177}
178
179
180// Converts the specified vector of double to itk::Offset, by rounding each element.
181template <std::size_t VDimension>
182itk::Offset<VDimension>
183ConvertToOffset(const std::vector<double> & doubles)
184{
185 ELX_GTEST_EXPECT_FALSE_AND_THROW_EXCEPTION_IF(doubles.size() != VDimension);
186
187 itk::Offset<VDimension> result;
188 std::size_t i{};
189
190 for (const double value : doubles)
191 {
192 const auto roundedValue = std::round(value);
193
194 EXPECT_GE(roundedValue, std::numeric_limits<itk::OffsetValueType>::min());
195 EXPECT_LE(roundedValue, std::numeric_limits<itk::OffsetValueType>::max());
196
197 result[i] = static_cast<itk::OffsetValueType>(roundedValue);
198 ++i;
199 }
200
201 return result;
202}
203
204
205inline std::map<std::string, std::vector<std::string>>
206CreateParameterMap(std::initializer_list<std::pair<std::string, std::vector<std::string>>> initializerList)
207{
208 std::map<std::string, std::vector<std::string>> result;
209
210 for (const auto & pair : initializerList)
211 {
212 EXPECT_TRUE(result.insert(pair).second);
213 }
214 return result;
215}
216
217
218inline std::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;
222
223 for (const auto & pair : initializerList)
224 {
225 EXPECT_TRUE(result.insert({ pair.first, { pair.second } }).second);
226 }
227 return result;
228}
229
230
231template <unsigned VImageDimension>
232std::map<std::string, std::vector<std::string>>
233CreateParameterMap(std::initializer_list<std::pair<std::string, std::string>> initializerList)
234{
235 std::map<std::string, std::vector<std::string>> result = CreateParameterMap(initializerList);
236
237 for (const auto & key : { "FixedImageDimension", "MovingImageDimension" })
238 {
239 EXPECT_TRUE(result.insert({ key, { std::to_string(VImageDimension) } }).second);
240 }
241 return result;
242}
243
244
245inline ParameterObject::Pointer
246CreateParameterObject(std::initializer_list<std::pair<std::string, std::string>> initializerList)
247{
248 const auto parameterObject = ParameterObject::New();
249 parameterObject->SetParameterMap(CreateParameterMap(initializerList));
250 return parameterObject;
251}
252
253
256{
257 const auto parameterObject = ParameterObject::New();
258 parameterObject->SetParameterMap(parameterMap);
259 return parameterObject;
260}
261
262
263inline std::vector<double>
264GetTransformParametersFromMaps(const std::vector<ParameterObject::ParameterMapType> & transformParameterMaps)
265{
266 // For the time being, only support a single parameter map here.
267 EXPECT_EQ(transformParameterMaps.size(), 1);
268
269 if (transformParameterMaps.empty())
270 {
271 throw Exception("Error: GetTransformParametersFromMaps should not return an empty ParameterMap!");
272 }
273
274 const auto & transformParameterMap = transformParameterMaps.front();
275 const auto found = transformParameterMap.find("TransformParameters");
276
277 if (found == transformParameterMap.cend())
278 {
279 throw Exception("Error: GetTransformParametersFromMaps did not find TransformParameters!");
280 }
281 return ConvertStringsToVectorOfDouble(found->second);
282}
283
284
285template <typename TFilter>
286std::vector<double>
288{
289 const auto transformParameterObject = filter.GetTransformParameterObject();
290 const auto & transformParameterMaps = itk::Deref(transformParameterObject).GetParameterMaps();
291 return GetTransformParametersFromMaps(transformParameterMaps);
292}
293
294
295// ITK's RecursiveSeparableImageFilter "requires a minimum of four pixels along the dimension to be processed", at
296// https://github.com/InsightSoftwareConsortium/ITK/blob/v5.3.0/Modules/Filtering/ImageFilterBase/include/itkRecursiveSeparableImageFilter.hxx#L226
297constexpr itk::SizeValueType minimumImageSizeValue{ 4 };
298
299
300// The image domain. ITK calls it the "geometry" of an image. ("The geometry of an image is defined by its position,
301// orientation, spacing, and extent", according to https://itk.org/Doxygen52/html/classitk_1_1ImageBase.html#details).
302// The elastix manual (elastix-5.1.0-manual.pdf, January 16, 2023) simply calls it "the
303// Size/Spacing/Origin/Index/Direction settings".
304template <unsigned int VDimension>
306{
307 using ImageBaseType = itk::ImageBase<VDimension>;
308
309 using DirectionType = typename ImageBaseType::DirectionType;
310 using IndexType = typename ImageBaseType::IndexType;
311 using SizeType = typename ImageBaseType::SizeType;
312 using SpacingType = typename ImageBaseType::SpacingType;
313 using PointType = typename ImageBaseType::PointType;
314
315 DirectionType direction{ DirectionType::GetIdentity() };
318 SpacingType spacing{ itk::MakeFilled<SpacingType>(1.0) };
320
321 // Default-constructor
322 ImageDomain() = default;
323
324 // Explicit constructor
325 explicit ImageDomain(const SizeType & initialSize)
326 : size(initialSize)
327 {}
328
329 explicit ImageDomain(const ImageBaseType & image)
330 : direction(image.GetDirection())
331 , index(image.GetLargestPossibleRegion().GetIndex())
332 , size(image.GetLargestPossibleRegion().GetSize())
333 , spacing(image.GetSpacing())
334 , origin(image.GetOrigin())
335 {}
336
337 // Constructor, allowing to explicitly specify all the settings of the domain.
338 ImageDomain(const DirectionType & initialDirection,
339 const IndexType & initialIndex,
340 const SizeType & initialSize,
341 const SpacingType & initialSpacing,
342 const PointType & initialOrigin)
343 : direction(initialDirection)
344 , index(initialIndex)
345 , size(initialSize)
346 , spacing(initialSpacing)
347 , origin(initialOrigin)
348 {}
349
350 // Puts the domain settings into the specified image.
351 void
352 ToImage(itk::ImageBase<VDimension> & image) const
353 {
354 image.SetDirection(direction);
355 image.SetRegions({ index, size });
356 image.SetSpacing(spacing);
357 image.SetOrigin(origin);
358 }
359
360 // Returns the data of this image domain as an elastix/transformix parameter map.
363 {
364 return {
365 // Parameters in alphabetic order:
371 };
372 }
373
374 friend bool
375 operator==(const ImageDomain & lhs, const ImageDomain & rhs)
376 {
377 return lhs.direction == rhs.direction && lhs.index == rhs.index && lhs.size == rhs.size &&
378 lhs.spacing == rhs.spacing && lhs.origin == rhs.origin;
379 }
380
381 friend bool
382 operator!=(const ImageDomain & lhs, const ImageDomain & rhs)
383 {
384 return !(lhs == rhs);
385 }
386};
387
388
389template <typename TRandomNumberEngine>
390int
391GenerateRandomSign(TRandomNumberEngine & randomNumberEngine)
392{
393 return (randomNumberEngine() % 2 == 0) ? -1 : 1;
394}
395
396
397template <unsigned int VImageDimension>
398auto
399CreateRandomImageDomain(std::mt19937 & randomNumberEngine)
400{
401 using ImageDomainType = ImageDomain<VImageDimension>;
402
403 const auto createRandomDirection = [&randomNumberEngine] {
404 using DirectionType = typename ImageDomainType::DirectionType;
405 auto randomDirection = DirectionType::GetIdentity();
406
407 // For now, just a single random rotation
408 const auto randomRotation = std::uniform_real_distribution<>{ -M_PI, M_PI }(randomNumberEngine);
409 const auto cosRandomRotation = std::cos(randomRotation);
410 const auto sinRandomRotation = std::sin(randomRotation);
411
412 randomDirection[0][0] = cosRandomRotation;
413 randomDirection[0][1] = sinRandomRotation;
414 randomDirection[1][0] = -sinRandomRotation;
415 randomDirection[1][1] = cosRandomRotation;
416
417 return randomDirection;
418 };
419 const auto createRandomIndex = [&randomNumberEngine] {
420 typename ImageDomainType::IndexType randomIndex{};
421 // Originally tried `std::uniform_int_distribution<itk::IndexValueType>` with
422 // `std::numeric_limits<itk::IndexValueType>`, but that caused errors from ImageSamplerBase::CropInputImageRegion(),
423 // saying "ERROR: the bounding box of the mask lies entirely out of the InputImageRegion!"
424 std::generate(randomIndex.begin(), randomIndex.end(), [&randomNumberEngine] {
425 return std::uniform_int_distribution{ std::numeric_limits<int>::min() / 2,
426 std::numeric_limits<int>::max() / 2 }(randomNumberEngine);
427 });
428 return randomIndex;
429 };
430 const auto createRandomSmallImageSize = [&randomNumberEngine] {
431 typename ImageDomainType::SizeType randomImageSize{};
432 std::generate(randomImageSize.begin(), randomImageSize.end(), [&randomNumberEngine] {
433 return std::uniform_int_distribution<itk::SizeValueType>{ minimumImageSizeValue,
434 2 * minimumImageSizeValue }(randomNumberEngine);
435 });
436 return randomImageSize;
437 };
438 const auto createRandomSpacing = [&randomNumberEngine] {
439 typename ImageDomainType::SpacingType randomSpacing{};
440 std::generate(randomSpacing.begin(), randomSpacing.end(), [&randomNumberEngine] {
441 // Originally tried the maximum interval from std::numeric_limits<itk::SpacePrecisionType>::min() to
442 // std::numeric_limits<itk::SpacePrecisionType>::max(), but that caused errors during inverse matrix computation.
443 return std::uniform_real_distribution<itk::SpacePrecisionType>{ 0.1, 10.0 }(randomNumberEngine);
444 });
445 return randomSpacing;
446 };
447 const auto createRandomPoint = [&randomNumberEngine] {
448 typename ImageDomainType::PointType randomPoint{};
449 std::generate(randomPoint.begin(), randomPoint.end(), [&randomNumberEngine] {
450 // Originally tried an interval up to `std::numeric_limits<itk::SpacePrecisionType>::max() / 2.0`, but that caused
451 // errors from ImageSamplerBase::CropInputImageRegion(), saying "ERROR: the bounding box of the mask lies entirely
452 // out of the InputImageRegion!"
453 return std::uniform_real_distribution<itk::SpacePrecisionType>{
454 std::numeric_limits<int>::min(), std::numeric_limits<int>::max()
455 }(randomNumberEngine);
456 });
457 return randomPoint;
458 };
459
460 return ImageDomainType{ createRandomDirection(),
461 createRandomIndex(),
462 createRandomSmallImageSize(),
463 createRandomSpacing(),
464 createRandomPoint() };
465}
466
467
468// Creates a test image, filled with zero.
469template <typename TPixel, unsigned VImageDimension>
470auto
471CreateImage(const itk::Size<VImageDimension> & imageSize)
472{
473 const auto image = itk::Image<TPixel, VImageDimension>::New();
474 image->SetRegions(imageSize);
475 image->AllocateInitialized();
476 return image;
477}
478
479// Creates a test image, filled with zero.
480template <typename TPixel, unsigned VImageDimension>
481auto
483{
484 const auto image = itk::Image<TPixel, VImageDimension>::New();
485 imageDomain.ToImage(*image);
486 image->AllocateInitialized();
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
495{
496 using ImageType = itk::Image<TPixel, VImageDimension>;
497 const auto image = ImageType::New();
498 imageDomain.ToImage(*image);
499 image->Allocate();
500 const itk::ImageBufferRange<ImageType> imageBufferRange{ *image };
501 std::iota(imageBufferRange.begin(), imageBufferRange.end(), TPixel{ 1 });
502 return image;
503}
504
505
506// Creates a test image, filled with a sequence of natural numbers, 1, 2, 3, ..., N.
507template <typename TPixel, unsigned VImageDimension>
508auto
513
514
515std::string
517
518// Returns CMAKE_CURRENT_BINARY_DIR: the path to Core Main GTesting subdirectory of the elastix build tree (without
519// trailing slash).
520std::string
522
523// Returns the name of a test defined by `GTEST_TEST(TestSuiteName, TestName)` as "TestSuiteName_TestName_Test".
524std::string
525GetNameOfTest(const testing::Test &);
526
527} // namespace CoreMainGTestUtilities
528} // namespace elastix
529
530
531#endif
static std::vector< std::string > ToVectorOfStrings(const TContainer &container)
Simple exception class, to be used by unit tests.
const char * what() const noexcept override
itk::SmartPointer< Self > Pointer
std::map< ParameterKeyType, ParameterValueVectorType > ParameterMapType
static Pointer New()
#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)
void FillImageRegionWithSequenceOfNaturalNumbers(itk::Image< TPixel, VImageDimension > &image, const itk::Index< VImageDimension > &regionIndex, const itk::Size< VImageDimension > &regionSize)
Fills the specified image region with pixel values 1, 2, 3, ...
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 1773719373 for elastix by doxygen 1.15.0 elastix logo