Line data Source code
1 : #include "doctest/doctest.h"
2 :
3 : #include "SiddonsMethod.h"
4 : #include "Geometry.h"
5 : #include "Logger.h"
6 : #include "testHelpers.h"
7 : #include "VolumeDescriptor.h"
8 : #include "PlanarDetectorDescriptor.h"
9 :
10 : using namespace elsa;
11 : using namespace elsa::geometry;
12 : using namespace doctest;
13 :
14 : // TODO(dfrank): remove this and replace with checkApproxEq
15 : using doctest::Approx;
16 :
17 4 : TEST_CASE("SiddonMethod: Testing projector with only one ray")
18 : {
19 : // Turn logger of
20 4 : Logger::setLevel(Logger::LogLevel::OFF);
21 :
22 8 : IndexVector_t sizeDomain(2);
23 4 : sizeDomain << 5, 5;
24 :
25 8 : IndexVector_t sizeRange(2);
26 4 : sizeRange << 1, 1;
27 :
28 8 : auto domain = VolumeDescriptor(sizeDomain);
29 8 : auto range = VolumeDescriptor(sizeRange);
30 :
31 4 : auto stc = SourceToCenterOfRotation{100};
32 4 : auto ctr = CenterOfRotationToDetector{5};
33 12 : auto volData = VolumeData2D{Size2D{sizeDomain}};
34 12 : auto sinoData = SinogramData2D{Size2D{sizeRange}};
35 :
36 : Eigen::IOFormat CommaInitFmt(Eigen::StreamPrecision, Eigen::DontAlignCols, ", ", ", ", "", "",
37 12 : " << ", ";");
38 :
39 8 : GIVEN("A SiddonsMethod for 2D and a domain data with all ones")
40 : {
41 8 : std::vector<Geometry> geom;
42 :
43 8 : auto dataDomain = DataContainer(domain);
44 4 : dataDomain = 1;
45 :
46 8 : auto dataRange = DataContainer(range);
47 4 : dataRange = 0;
48 :
49 5 : WHEN("We have a single ray with 0 degrees")
50 : {
51 1 : geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData));
52 :
53 2 : auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom});
54 :
55 2 : auto op = SiddonsMethod(domain, detectorDesc);
56 :
57 2 : THEN("A^t A x should be close to the original data")
58 : {
59 2 : auto AtAx = DataContainer(domain);
60 :
61 1 : op.apply(dataDomain, dataRange);
62 1 : op.applyAdjoint(dataRange, AtAx);
63 :
64 1 : auto cmp = RealVector_t(sizeDomain.prod());
65 1 : cmp << 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0;
66 :
67 1 : REQUIRE_UNARY(isCwiseApprox(DataContainer(domain, cmp), AtAx));
68 1 : REQUIRE_EQ(dataRange[0], Approx(5));
69 : }
70 : }
71 :
72 5 : WHEN("We have a single ray with 180 degrees")
73 : {
74 1 : geom.emplace_back(stc, ctr, Degree{180}, std::move(volData), std::move(sinoData));
75 :
76 2 : auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom});
77 :
78 2 : auto op = SiddonsMethod(domain, detectorDesc);
79 :
80 2 : THEN("A^t A x should be close to the original data")
81 : {
82 2 : auto AtAx = DataContainer(domain);
83 :
84 1 : op.apply(dataDomain, dataRange);
85 1 : op.applyAdjoint(dataRange, AtAx);
86 :
87 1 : auto cmp = RealVector_t(sizeDomain.prod());
88 1 : cmp << 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0, 0, 0, 5, 0, 0;
89 :
90 1 : REQUIRE_UNARY(isCwiseApprox(DataContainer(domain, cmp), AtAx));
91 1 : REQUIRE_EQ(dataRange[0], Approx(5));
92 : }
93 : }
94 :
95 5 : WHEN("We have a single ray with 90 degrees")
96 : {
97 1 : geom.emplace_back(stc, ctr, Degree{90}, std::move(volData), std::move(sinoData));
98 :
99 2 : auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom});
100 :
101 2 : auto op = SiddonsMethod(domain, detectorDesc);
102 :
103 2 : THEN("A^t A x should be close to the original data")
104 : {
105 2 : auto AtAx = DataContainer(domain);
106 :
107 1 : op.apply(dataDomain, dataRange);
108 1 : op.applyAdjoint(dataRange, AtAx);
109 :
110 1 : auto cmp = RealVector_t(sizeDomain.prod());
111 1 : cmp << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
112 :
113 1 : REQUIRE_UNARY(isCwiseApprox(DataContainer(domain, cmp), AtAx));
114 1 : REQUIRE_EQ(dataRange[0], Approx(5));
115 : }
116 : }
117 :
118 5 : WHEN("We have a single ray with 270 degrees")
119 : {
120 1 : geom.emplace_back(stc, ctr, Degree{270}, std::move(volData), std::move(sinoData));
121 :
122 2 : auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom});
123 :
124 2 : auto op = SiddonsMethod(domain, detectorDesc);
125 :
126 2 : THEN("A^t A x should be close to the original data")
127 : {
128 2 : auto AtAx = DataContainer(domain);
129 :
130 1 : op.apply(dataDomain, dataRange);
131 1 : op.applyAdjoint(dataRange, AtAx);
132 :
133 1 : auto cmp = RealVector_t(sizeDomain.prod());
134 1 : cmp << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
135 :
136 1 : REQUIRE_UNARY(isCwiseApprox(DataContainer(domain, cmp), AtAx));
137 1 : REQUIRE_EQ(dataRange[0], Approx(5));
138 : }
139 : }
140 :
141 : // FIXME This does not yield the desired result/if fixed the overall results in a
142 : // reconstruction is bad
143 : /*
144 : WHEN("We have a single ray with 45 degrees")
145 : {
146 : geom.emplace_back(100, 5, 45 * pi_t / 180., domain, range);
147 : auto op = SiddonsMethod(domain, range, geom);
148 :
149 : THEN("A^t A x should be close to the original data")
150 : {
151 : auto AtAx = DataContainer(domain);
152 :
153 : op.apply(dataDomain, dataRange);
154 : op.applyAdjoint(dataRange, AtAx);
155 :
156 : std::cout << dataRange.getData().format(CommaInitFmt) << "\n";
157 : std::cout << AtAx.getData().format(CommaInitFmt) << "\n";
158 :
159 : auto cmp = RealVector_t(sizeDomain.prod());
160 : cmp << 10, 0, 0, 0, 0,
161 : 0, 10, 0, 0, 0,
162 : 0, 0, 10, 0, 0,
163 : 0, 0, 0, 10, 0,
164 : 0, 0, 0, 0, 10;
165 :
166 : REQUIRE_UNARY(cmp.isApprox(AtAx.getData(), 1e-2));
167 : REQUIRE_EQ(dataRange[0], Approx(7.071));
168 : }
169 : }
170 :
171 : WHEN("We have a single ray with 225 degrees")
172 : {
173 : geom.emplace_back(100, 5, 225 * pi_t / 180., domain, range);
174 : auto op = SiddonsMethod(domain, range, geom);
175 :
176 : THEN("A^t A x should be close to the original data")
177 : {
178 : auto AtAx = DataContainer(domain);
179 :
180 : op.apply(dataDomain, dataRange);
181 : op.applyAdjoint(dataRange, AtAx);
182 :
183 : std::cout << dataRange.getData().format(CommaInitFmt) << "\n";
184 : std::cout << AtAx.getData().format(CommaInitFmt) << "\n";
185 :
186 : auto cmp = RealVector_t(sizeDomain.prod());
187 : cmp << 10, 0, 0, 0, 0,
188 : 0, 10, 0, 0, 0,
189 : 0, 0, 10, 0, 0,
190 : 0, 0, 0, 10, 0,
191 : 0, 0, 0, 0, 10;
192 :
193 : REQUIRE_UNARY(cmp.isApprox(AtAx.getData()));
194 : REQUIRE_EQ(dataRange[0], Approx(7.071));
195 : }
196 : }*/
197 :
198 : // TODO fix this direction, currently it does not work correctly. Consider changing
199 : // geometry, to mirror stuff
200 : /*WHEN("We have a single ray with 135 degrees")
201 : {
202 : geom.emplace_back(100, 5, 135 * pi_t / 180., domain, range);
203 : auto op = SiddonsMethod(domain, range, geom);
204 :
205 : THEN("A^t A x should be close to the original data")
206 : {
207 : auto AtAx = DataContainer(domain);
208 :
209 : op.apply(dataDomain, dataRange);
210 : op.applyAdjoint(dataRange, AtAx);
211 :
212 : std::cout << dataRange.getData().format(CommaInitFmt) << "\n";
213 : std::cout << AtAx.getData().format(CommaInitFmt) << "\n";
214 :
215 : auto cmp = RealVector_t(sizeDomain.prod());
216 : cmp << 0, 0, 0, 0, 10,
217 : 0, 0, 0, 10, 0,
218 : 0, 0, 10, 0, 0,
219 : 0, 10, 0, 0, 0,
220 : 10, 0, 0, 0, 0;
221 :
222 : REQUIRE(cmp.isApprox(AtAx.getData()));
223 : REQUIRE_EQ(dataRange[0], Approx(7.071));
224 : }
225 : }*/
226 : }
227 4 : }
228 :
229 1 : TEST_CASE("SiddonMethod: Calls to functions of super class")
230 : {
231 : // Turn logger of
232 1 : Logger::setLevel(Logger::LogLevel::OFF);
233 :
234 2 : GIVEN("A projector")
235 : {
236 2 : IndexVector_t volumeDims(2), sinoDims(2);
237 1 : const index_t volSize = 50;
238 1 : const index_t detectorSize = 50;
239 1 : const index_t numImgs = 50;
240 1 : volumeDims << volSize, volSize;
241 1 : sinoDims << detectorSize, numImgs;
242 2 : VolumeDescriptor volumeDescriptor(volumeDims);
243 :
244 2 : RealVector_t randomStuff(volumeDescriptor.getNumberOfCoefficients());
245 1 : randomStuff.setRandom();
246 2 : DataContainer volume(volumeDescriptor, randomStuff);
247 :
248 1 : auto stc = SourceToCenterOfRotation{20 * volSize};
249 1 : auto ctr = CenterOfRotationToDetector{volSize};
250 :
251 2 : std::vector<Geometry> geom;
252 51 : for (std::size_t i = 0; i < numImgs; i++) {
253 50 : real_t angle = static_cast<real_t>(i * 2) * pi_t / 50;
254 100 : geom.emplace_back(stc, ctr, Radian{angle}, VolumeData2D{Size2D{volumeDims}},
255 150 : SinogramData2D{Size2D{sinoDims}});
256 : }
257 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
258 2 : DataContainer sino(sinoDescriptor);
259 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
260 :
261 2 : WHEN("Projector is cloned")
262 : {
263 2 : auto opClone = op.clone();
264 2 : auto sinoClone = DataContainer(sino.getDataDescriptor());
265 2 : auto volumeClone = DataContainer(volume.getDataDescriptor());
266 :
267 2 : THEN("Results do not change (may still be slightly different due to summation being "
268 : "performed in a different order)")
269 : {
270 1 : op.apply(volume, sino);
271 1 : opClone->apply(volume, sinoClone);
272 1 : REQUIRE_UNARY(isApprox(sino, sinoClone));
273 :
274 1 : op.applyAdjoint(sino, volume);
275 1 : opClone->applyAdjoint(sino, volumeClone);
276 1 : REQUIRE_UNARY(isApprox(volume, volumeClone));
277 : }
278 : }
279 : }
280 1 : }
281 :
282 4 : TEST_CASE("SiddoneMethod: Output DataContainer is not zero initialized")
283 : {
284 : // Turn logger of
285 4 : Logger::setLevel(Logger::LogLevel::OFF);
286 :
287 6 : GIVEN("A 2D setting")
288 : {
289 4 : IndexVector_t volumeDims(2), sinoDims(2);
290 2 : const index_t volSize = 5;
291 2 : const index_t detectorSize = 1;
292 2 : const index_t numImgs = 1;
293 2 : volumeDims << volSize, volSize;
294 2 : sinoDims << detectorSize, numImgs;
295 4 : VolumeDescriptor volumeDescriptor(volumeDims);
296 4 : DataContainer volume(volumeDescriptor);
297 :
298 2 : auto stc = SourceToCenterOfRotation{20 * volSize};
299 2 : auto ctr = CenterOfRotationToDetector{volSize};
300 6 : auto volData = VolumeData2D{Size2D{volumeDims}};
301 6 : auto sinoData = SinogramData2D{Size2D{sinoDims}};
302 :
303 4 : std::vector<Geometry> geom;
304 2 : geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData));
305 :
306 4 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
307 4 : DataContainer sino(sinoDescriptor);
308 :
309 4 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
310 :
311 3 : WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume")
312 : {
313 1 : volume = 0;
314 1 : sino = 1;
315 :
316 2 : THEN("Result is zero")
317 : {
318 1 : op.apply(volume, sino);
319 1 : DataContainer zero(sinoDescriptor);
320 1 : zero = 0;
321 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
322 : }
323 : }
324 :
325 3 : WHEN("Volume container is not zero initialized and we backproject from an empty sinogram")
326 : {
327 1 : sino = 0;
328 1 : volume = 1;
329 :
330 2 : THEN("Result is zero")
331 : {
332 1 : op.applyAdjoint(sino, volume);
333 1 : DataContainer zero(volumeDescriptor);
334 1 : zero = 0;
335 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
336 : }
337 : }
338 : }
339 :
340 6 : GIVEN("A 3D setting")
341 : {
342 4 : IndexVector_t volumeDims(3), sinoDims(3);
343 2 : const index_t volSize = 3;
344 2 : const index_t detectorSize = 1;
345 2 : const index_t numImgs = 1;
346 2 : volumeDims << volSize, volSize, volSize;
347 2 : sinoDims << detectorSize, detectorSize, numImgs;
348 4 : VolumeDescriptor volumeDescriptor(volumeDims);
349 4 : DataContainer volume(volumeDescriptor);
350 :
351 2 : auto stc = SourceToCenterOfRotation{20 * volSize};
352 2 : auto ctr = CenterOfRotationToDetector{volSize};
353 6 : auto volData = VolumeData3D{Size3D{volumeDims}};
354 6 : auto sinoData = SinogramData3D{Size3D{sinoDims}};
355 :
356 4 : std::vector<Geometry> geom;
357 2 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
358 4 : RotationAngles3D{Gamma{0}});
359 :
360 4 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
361 4 : DataContainer sino(sinoDescriptor);
362 :
363 4 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
364 :
365 3 : WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume")
366 : {
367 1 : volume = 0;
368 1 : sino = 1;
369 :
370 2 : THEN("Result is zero")
371 : {
372 1 : op.apply(volume, sino);
373 1 : DataContainer zero(sinoDescriptor);
374 1 : zero = 0;
375 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
376 : }
377 : }
378 :
379 3 : WHEN("Volume container is not zero initialized and we backproject from an empty sinogram")
380 : {
381 1 : sino = 0;
382 1 : volume = 1;
383 :
384 2 : THEN("Result is zero")
385 : {
386 1 : op.applyAdjoint(sino, volume);
387 1 : DataContainer zero(volumeDescriptor);
388 1 : zero = 0;
389 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
390 : }
391 : }
392 : }
393 4 : }
394 :
395 6 : TEST_CASE("SidddonMethod: Rays not intersecting the bounding box are present")
396 : {
397 : // Turn logger of
398 6 : Logger::setLevel(Logger::LogLevel::OFF);
399 :
400 10 : GIVEN("A 2D setting")
401 : {
402 8 : IndexVector_t volumeDims(2), sinoDims(2);
403 4 : const index_t volSize = 5;
404 4 : const index_t detectorSize = 1;
405 4 : const index_t numImgs = 1;
406 4 : volumeDims << volSize, volSize;
407 4 : sinoDims << detectorSize, numImgs;
408 8 : VolumeDescriptor volumeDescriptor(volumeDims);
409 8 : DataContainer volume(volumeDescriptor);
410 4 : volume = 1;
411 :
412 4 : auto stc = SourceToCenterOfRotation{20 * volSize};
413 4 : auto ctr = CenterOfRotationToDetector{volSize};
414 12 : auto volData = VolumeData2D{Size2D{volumeDims}};
415 12 : auto sinoData = SinogramData2D{Size2D{sinoDims}};
416 :
417 8 : std::vector<Geometry> geom;
418 :
419 5 : WHEN("Tracing along a y-axis-aligned ray with a negative x-coordinate of origin")
420 : {
421 1 : geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData),
422 2 : PrincipalPointOffset{}, RotationOffset2D{-volSize, 0});
423 :
424 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
425 2 : DataContainer sino(sinoDescriptor);
426 1 : sino = 1;
427 :
428 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
429 :
430 2 : THEN("Result of forward projection is zero")
431 : {
432 1 : op.apply(volume, sino);
433 2 : DataContainer zero(sinoDescriptor);
434 1 : zero = 0;
435 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
436 :
437 2 : AND_THEN("Result of backprojection is zero")
438 : {
439 1 : op.applyAdjoint(sino, volume);
440 1 : DataContainer zero(volumeDescriptor);
441 1 : zero = 0;
442 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
443 : }
444 : }
445 : }
446 :
447 5 : WHEN("Tracing along a y-axis-aligned ray with a x-coordinate of origin beyond the bounding "
448 : "box")
449 : {
450 1 : geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData),
451 2 : PrincipalPointOffset{}, RotationOffset2D{volSize, 0});
452 :
453 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
454 2 : DataContainer sino(sinoDescriptor);
455 1 : sino = 1;
456 :
457 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
458 :
459 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
460 :
461 2 : THEN("Result of forward projection is zero")
462 : {
463 1 : op.apply(volume, sino);
464 2 : DataContainer zero(sinoDescriptor);
465 1 : zero = 0;
466 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
467 :
468 2 : AND_THEN("Result of backprojection is zero")
469 : {
470 1 : op.applyAdjoint(sino, volume);
471 1 : DataContainer zero(volumeDescriptor);
472 1 : zero = 0;
473 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
474 : }
475 : }
476 : }
477 :
478 5 : WHEN("Tracing along a x-axis-aligned ray with a negative y-coordinate of origin")
479 : {
480 1 : geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData),
481 2 : PrincipalPointOffset{}, RotationOffset2D{0, -volSize});
482 :
483 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
484 2 : DataContainer sino(sinoDescriptor);
485 1 : sino = 1;
486 :
487 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
488 :
489 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
490 :
491 2 : THEN("Result of forward projection is zero")
492 : {
493 1 : op.apply(volume, sino);
494 2 : DataContainer zero(sinoDescriptor);
495 1 : zero = 0;
496 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
497 :
498 2 : AND_THEN("Result of backprojection is zero")
499 : {
500 1 : op.applyAdjoint(sino, volume);
501 1 : DataContainer zero(volumeDescriptor);
502 1 : zero = 0;
503 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
504 : }
505 : }
506 : }
507 :
508 5 : WHEN("Tracing along a x-axis-aligned ray with a y-coordinate of origin beyond the bounding "
509 : "box")
510 : {
511 1 : geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData),
512 2 : PrincipalPointOffset{}, RotationOffset2D{0, volSize});
513 :
514 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
515 2 : DataContainer sino(sinoDescriptor);
516 1 : sino = 1;
517 :
518 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
519 :
520 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
521 :
522 2 : THEN("Result of forward projection is zero")
523 : {
524 1 : op.apply(volume, sino);
525 2 : DataContainer zero(sinoDescriptor);
526 1 : zero = 0;
527 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
528 :
529 2 : AND_THEN("Result of backprojection is zero")
530 : {
531 1 : op.applyAdjoint(sino, volume);
532 1 : DataContainer zero(volumeDescriptor);
533 1 : zero = 0;
534 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
535 : }
536 : }
537 : }
538 : }
539 :
540 8 : GIVEN("A 3D setting")
541 : {
542 4 : IndexVector_t volumeDims(3), sinoDims(3);
543 2 : const index_t volSize = 5;
544 2 : const index_t detectorSize = 1;
545 2 : const index_t numImgs = 1;
546 2 : volumeDims << volSize, volSize, volSize;
547 2 : sinoDims << detectorSize, detectorSize, numImgs;
548 4 : VolumeDescriptor volumeDescriptor(volumeDims);
549 4 : DataContainer volume(volumeDescriptor);
550 2 : volume = 1;
551 :
552 2 : auto stc = SourceToCenterOfRotation{20 * volSize};
553 2 : auto ctr = CenterOfRotationToDetector{volSize};
554 6 : auto volData = VolumeData3D{Size3D{volumeDims}};
555 6 : auto sinoData = SinogramData3D{Size3D{sinoDims}};
556 :
557 4 : std::vector<Geometry> geom;
558 :
559 2 : const index_t numCases = 9;
560 2 : real_t alpha[numCases] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
561 2 : real_t beta[numCases] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, pi_t / 2};
562 2 : real_t gamma[numCases] = {0.0, 0.0, 0.0, pi_t / 2, pi_t / 2,
563 : pi_t / 2, pi_t / 2, pi_t / 2, pi_t / 2};
564 2 : real_t offsetx[numCases] = {volSize, 0.0, volSize, 0.0, 0.0, 0.0, volSize, 0.0, volSize};
565 2 : real_t offsety[numCases] = {0.0, volSize, volSize, volSize, 0.0, volSize, 0.0, 0.0, 0.0};
566 2 : real_t offsetz[numCases] = {0.0, 0.0, 0.0, 0.0, volSize, volSize, 0.0, volSize, volSize};
567 22 : std::string neg[numCases] = {"x", "y", "x and y", "y", "z", "y and z", "x", "z", "x and z"};
568 22 : std::string ali[numCases] = {"z", "z", "z", "x", "x", "x", "y", "y", "y"};
569 :
570 20 : for (int i = 0; i < numCases; i++) {
571 19 : WHEN("Tracing along axis-aligned ray trough origins")
572 : {
573 2 : INFO("Tracing along a ", ali[i], "-axis-aligned ray with negative ", neg[i],
574 : "-coodinate of origin");
575 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
576 1 : RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}, Alpha{alpha[i]}},
577 2 : PrincipalPointOffset2D{0, 0},
578 3 : RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]});
579 :
580 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
581 2 : DataContainer sino(sinoDescriptor);
582 1 : sino = 1;
583 :
584 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
585 :
586 2 : THEN("Result of forward projection is zero")
587 : {
588 1 : op.apply(volume, sino);
589 2 : DataContainer zero(sinoDescriptor);
590 1 : zero = 0;
591 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
592 :
593 2 : AND_THEN("Result of backprojection is zero")
594 : {
595 1 : op.applyAdjoint(sino, volume);
596 1 : DataContainer zero(volumeDescriptor);
597 1 : zero = 0;
598 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
599 : }
600 : }
601 : }
602 : }
603 : }
604 6 : }
605 :
606 9 : TEST_CASE("SiddonMethod: Axis-aligned rays are present")
607 : {
608 : // Turn logger of
609 9 : Logger::setLevel(Logger::LogLevel::OFF);
610 :
611 12 : GIVEN("A 2D setting with a single ray")
612 : {
613 6 : IndexVector_t volumeDims(2), sinoDims(2);
614 3 : const index_t volSize = 5;
615 3 : const index_t detectorSize = 1;
616 3 : const index_t numImgs = 1;
617 3 : volumeDims << volSize, volSize;
618 3 : sinoDims << detectorSize, numImgs;
619 6 : VolumeDescriptor volumeDescriptor(volumeDims);
620 6 : DataContainer volume(volumeDescriptor);
621 :
622 3 : auto stc = SourceToCenterOfRotation{20 * volSize};
623 3 : auto ctr = CenterOfRotationToDetector{volSize};
624 9 : auto volData = VolumeData2D{Size2D{volumeDims}};
625 9 : auto sinoData = SinogramData2D{Size2D{sinoDims}};
626 :
627 6 : std::vector<Geometry> geom;
628 :
629 3 : const index_t numCases = 4;
630 3 : const real_t angles[numCases] = {0.0, pi_t / 2, pi_t, 3 * pi_t / 2};
631 18 : RealVector_t backProj[2];
632 3 : backProj[0].resize(volSize * volSize);
633 3 : backProj[1].resize(volSize * volSize);
634 3 : backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
635 :
636 3 : backProj[0] << 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0;
637 :
638 15 : for (index_t i = 0; i < numCases; i++) {
639 13 : WHEN("An axis-aligned ray with an angle of different angles passes through the center "
640 : "of a pixel")
641 : {
642 2 : INFO("An axis-aligned ray with an angle of", angles[i],
643 : " radians passes through the center of a pixel");
644 1 : geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData),
645 2 : std::move(sinoData));
646 :
647 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
648 2 : DataContainer sino(sinoDescriptor);
649 :
650 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
651 2 : THEN("The result of projecting through a pixel is exactly the pixel value")
652 : {
653 6 : for (index_t j = 0; j < volSize; j++) {
654 5 : volume = 0;
655 5 : if (i % 2 == 0)
656 5 : volume(volSize / 2, j) = 1;
657 : else
658 0 : volume(j, volSize / 2) = 1;
659 :
660 5 : op.apply(volume, sino);
661 5 : REQUIRE_EQ(sino[0], Approx(1));
662 : }
663 :
664 2 : AND_THEN("The backprojection sets the values of all hit pixels to the detector "
665 : "value")
666 : {
667 1 : op.applyAdjoint(sino, volume);
668 :
669 1 : REQUIRE_UNARY(
670 : isApprox(volume, DataContainer(volumeDescriptor, backProj[i % 2])));
671 : }
672 : }
673 : }
674 : }
675 :
676 4 : WHEN("A y-axis-aligned ray runs along a voxel boundary")
677 : {
678 0 : geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0},
679 1 : std::move(volData), std::move(sinoData), PrincipalPointOffset{0},
680 2 : RotationOffset2D{-0.5, 0});
681 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
682 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
683 2 : DataContainer sino(sinoDescriptor);
684 :
685 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
686 2 : THEN("The result of projecting through a pixel is the value of the pixel with the "
687 : "higher index")
688 : {
689 6 : for (index_t j = 0; j < volSize; j++) {
690 5 : volume = 0;
691 5 : volume(volSize / 2, j) = 1;
692 :
693 5 : op.apply(volume, sino);
694 5 : REQUIRE_EQ(sino[0], Approx(1.0));
695 : }
696 :
697 2 : AND_THEN("The backprojection yields the exact adjoint")
698 : {
699 1 : sino[0] = 1;
700 1 : op.applyAdjoint(sino, volume);
701 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, backProj[0])));
702 : }
703 : }
704 : }
705 :
706 4 : WHEN("A y-axis-aligned ray runs along the right volume boundary")
707 : {
708 : // For Siddon's values in the range [0,boxMax) are considered, a ray running along
709 : // boxMax should be ignored
710 :
711 0 : geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0},
712 1 : std::move(volData), std::move(sinoData), PrincipalPointOffset{0},
713 2 : RotationOffset2D{volSize * 0.5, 0});
714 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
715 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
716 2 : DataContainer sino(sinoDescriptor);
717 :
718 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
719 :
720 2 : THEN("The result of projecting is zero")
721 : {
722 1 : volume = 0;
723 1 : op.apply(volume, sino);
724 1 : REQUIRE_EQ(sino[0], Approx(0.0));
725 :
726 2 : AND_THEN("The result of backprojection is also zero")
727 : {
728 1 : sino[0] = 1;
729 :
730 1 : op.applyAdjoint(sino, volume);
731 1 : DataContainer zero(volumeDescriptor);
732 1 : zero = 0;
733 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
734 : }
735 : }
736 : }
737 :
738 : /**
739 : * CPU version exhibits slightly different behaviour, ignoring all ray running along a
740 : * bounding box border
741 : */
742 : // backProj[0] << 1, 0, 0, 0, 0,
743 : // 1, 0, 0, 0, 0,
744 : // 1, 0, 0, 0, 0,
745 : // 1, 0, 0, 0, 0,
746 : // 1, 0, 0, 0, 0;
747 :
748 : // WHEN("A y-axis-aligned ray runs along the left volume boundary") {
749 : // geom.emplace_back(volSize*2000,volSize,0.0,volumeDescriptor,sinoDescriptor,0.0,volSize/2.0);
750 : // SiddonsMethod op(volumeDescriptor,sinoDescriptor,geom);
751 : // THEN("The result of projecting through a pixel is exactly the pixel's value") {
752 : // for (index_t j=0; j<volSize;j++) {
753 : // volume = 0;
754 : // volume(0,j) = 1;
755 :
756 : // op.apply(volume,sino);
757 : // REQUIRE_EQ(sino[0], 1);
758 : // }
759 :
760 : // AND_THEN("The backprojection yields the exact adjoint") {
761 : // sino[0] = 1;
762 : // op.applyAdjoint(sino,volume);
763 : // REQUIRE_UNARY(volume.getData().isApprox(backProj[0]));
764 : // }
765 : // }
766 : // }
767 : }
768 :
769 13 : GIVEN("A 3D setting with a single ray")
770 : {
771 8 : IndexVector_t volumeDims(3), sinoDims(3);
772 4 : const index_t volSize = 3;
773 4 : const index_t detectorSize = 1;
774 4 : const index_t numImgs = 1;
775 4 : volumeDims << volSize, volSize, volSize;
776 4 : sinoDims << detectorSize, detectorSize, numImgs;
777 8 : VolumeDescriptor volumeDescriptor(volumeDims);
778 8 : DataContainer volume(volumeDescriptor);
779 :
780 4 : auto stc = SourceToCenterOfRotation{20 * volSize};
781 4 : auto ctr = CenterOfRotationToDetector{volSize};
782 12 : auto volData = VolumeData3D{Size3D{volumeDims}};
783 12 : auto sinoData = SinogramData3D{Size3D{sinoDims}};
784 :
785 8 : std::vector<Geometry> geom;
786 :
787 4 : const index_t numCases = 6;
788 4 : real_t beta[numCases] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2};
789 4 : real_t gamma[numCases] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2};
790 32 : std::string al[numCases] = {"z", "-z", "x", "-x", "y", "-y"};
791 :
792 56 : RealVector_t backProj[numCases];
793 28 : for (auto& backPr : backProj)
794 24 : backPr.resize(volSize * volSize * volSize);
795 :
796 8 : backProj[2] << 0, 0, 0, 0, 0, 0, 0, 0, 0,
797 :
798 4 : 0, 1, 0, 0, 1, 0, 0, 1, 0,
799 :
800 8 : 0, 0, 0, 0, 0, 0, 0, 0, 0;
801 :
802 8 : backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0,
803 :
804 4 : 0, 0, 0, 1, 1, 1, 0, 0, 0,
805 :
806 8 : 0, 0, 0, 0, 0, 0, 0, 0, 0;
807 :
808 8 : backProj[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0,
809 :
810 4 : 0, 0, 0, 0, 1, 0, 0, 0, 0,
811 :
812 8 : 0, 0, 0, 0, 1, 0, 0, 0, 0;
813 :
814 28 : for (index_t i = 0; i < numCases; i++) {
815 25 : WHEN("An axis-aligned ray passes through the center of a pixel")
816 : {
817 2 : INFO("A ", al[i], "-axis-aligned ray passes through the center of a pixel");
818 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
819 2 : RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}});
820 :
821 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
822 2 : DataContainer sino(sinoDescriptor);
823 :
824 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
825 2 : THEN("The result of projecting through a voxel is exactly the voxel value")
826 : {
827 4 : for (index_t j = 0; j < volSize; j++) {
828 3 : volume = 0;
829 3 : if (i / 2 == 0)
830 3 : volume(volSize / 2, volSize / 2, j) = 1;
831 0 : else if (i / 2 == 1)
832 0 : volume(j, volSize / 2, volSize / 2) = 1;
833 0 : else if (i / 2 == 2)
834 0 : volume(volSize / 2, j, volSize / 2) = 1;
835 :
836 3 : op.apply(volume, sino);
837 3 : REQUIRE_EQ(sino[0], Approx(1));
838 : }
839 :
840 2 : AND_THEN("The backprojection sets the values of all hit voxels to the detector "
841 : "value")
842 : {
843 1 : op.applyAdjoint(sino, volume);
844 1 : REQUIRE_UNARY(
845 : isApprox(volume, DataContainer(volumeDescriptor, backProj[i / 2])));
846 : }
847 : }
848 : }
849 : }
850 :
851 : real_t offsetx[numCases];
852 : real_t offsety[numCases];
853 :
854 4 : offsetx[0] = volSize / 2.0;
855 4 : offsetx[3] = -(volSize / 2.0);
856 4 : offsetx[1] = 0.0;
857 4 : offsetx[4] = 0.0;
858 4 : offsetx[5] = -(volSize / 2.0);
859 4 : offsetx[2] = (volSize / 2.0);
860 :
861 4 : offsety[0] = 0.0;
862 4 : offsety[3] = 0.0;
863 4 : offsety[1] = volSize / 2.0;
864 4 : offsety[4] = -(volSize / 2.0);
865 4 : offsety[5] = -(volSize / 2.0);
866 4 : offsety[2] = (volSize / 2.0);
867 :
868 8 : backProj[0] << 0, 0, 0, 1, 0, 0, 0, 0, 0,
869 :
870 4 : 0, 0, 0, 1, 0, 0, 0, 0, 0,
871 :
872 8 : 0, 0, 0, 1, 0, 0, 0, 0, 0;
873 :
874 8 : backProj[1] << 0, 1, 0, 0, 0, 0, 0, 0, 0,
875 :
876 4 : 0, 1, 0, 0, 0, 0, 0, 0, 0,
877 :
878 8 : 0, 1, 0, 0, 0, 0, 0, 0, 0;
879 :
880 8 : backProj[2] << 1, 0, 0, 0, 0, 0, 0, 0, 0,
881 :
882 4 : 1, 0, 0, 0, 0, 0, 0, 0, 0,
883 :
884 8 : 1, 0, 0, 0, 0, 0, 0, 0, 0;
885 :
886 4 : al[0] = "left border";
887 4 : al[1] = "bottom border";
888 4 : al[2] = "bottom left edge";
889 4 : al[3] = "right border";
890 4 : al[4] = "top border";
891 4 : al[5] = "top right edge";
892 :
893 16 : for (index_t i = 0; i < numCases / 2; i++) {
894 13 : WHEN("A z-axis-aligned ray runs along the corners and edges of the volume")
895 : {
896 2 : INFO("A z-axis-aligned ray runs along the ", al[i], " of the volume");
897 : // x-ray source must be very far from the volume center to make testing of the op
898 : // backprojection simpler
899 1 : geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData),
900 2 : std::move(sinoData), RotationAngles3D{Gamma{0}},
901 1 : PrincipalPointOffset2D{0, 0},
902 3 : RotationOffset3D{-offsetx[i], -offsety[i], 0});
903 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
904 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
905 2 : DataContainer sino(sinoDescriptor);
906 :
907 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
908 2 : THEN("The result of projecting through a voxel is exactly the voxel's value")
909 : {
910 4 : for (index_t j = 0; j < volSize; j++) {
911 3 : volume = 0;
912 3 : switch (i) {
913 3 : case 0:
914 3 : volume(0, volSize / 2, j) = 1;
915 3 : break;
916 0 : case 1:
917 0 : volume(volSize / 2, 0, j) = 1;
918 0 : break;
919 : break;
920 0 : case 2:
921 0 : volume(0, 0, j) = 1;
922 0 : break;
923 0 : default:
924 0 : break;
925 : }
926 :
927 3 : op.apply(volume, sino);
928 3 : REQUIRE_EQ(sino[0], Approx(1));
929 : }
930 :
931 2 : AND_THEN("The backprojection yields the exact adjoints")
932 : {
933 1 : sino[0] = 1;
934 1 : op.applyAdjoint(sino, volume);
935 :
936 1 : REQUIRE_UNARY(
937 : isApprox(volume, DataContainer(volumeDescriptor, backProj[i])));
938 : }
939 : }
940 : }
941 : }
942 :
943 16 : for (index_t i = 3; i < numCases; i++) {
944 13 : WHEN("A z-axis-aligned ray runs along the corners and edges of the volume")
945 : {
946 2 : INFO("A z-axis-aligned ray runs along the ", al[i], " of the volume");
947 : // x-ray source must be very far from the volume center to make testing of the op
948 : // backprojection simpler
949 1 : geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData),
950 2 : std::move(sinoData), RotationAngles3D{Gamma{0}},
951 1 : PrincipalPointOffset2D{0, 0},
952 3 : RotationOffset3D{-offsetx[i], -offsety[i], 0});
953 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
954 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
955 2 : DataContainer sino(sinoDescriptor);
956 :
957 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
958 2 : THEN("The result of projecting is zero")
959 : {
960 1 : volume = 1;
961 :
962 1 : op.apply(volume, sino);
963 1 : REQUIRE_EQ(sino[0], Approx(0));
964 :
965 2 : AND_THEN("The result of backprojection is also zero")
966 : {
967 1 : sino[0] = 1;
968 1 : op.applyAdjoint(sino, volume);
969 :
970 1 : DataContainer zero(volumeDescriptor);
971 1 : zero = 0;
972 1 : REQUIRE_UNARY(isApprox(volume, zero, epsilon));
973 : }
974 : }
975 : }
976 : }
977 : }
978 :
979 10 : GIVEN("A 2D setting with multiple projection angles")
980 : {
981 2 : IndexVector_t volumeDims(2), sinoDims(2);
982 1 : const index_t volSize = 5;
983 1 : const index_t detectorSize = 1;
984 1 : const index_t numImgs = 4;
985 1 : volumeDims << volSize, volSize;
986 1 : sinoDims << detectorSize, numImgs;
987 2 : VolumeDescriptor volumeDescriptor(volumeDims);
988 2 : DataContainer volume(volumeDescriptor);
989 :
990 1 : auto stc = SourceToCenterOfRotation{20 * volSize};
991 1 : auto ctr = CenterOfRotationToDetector{volSize};
992 :
993 2 : std::vector<Geometry> geom;
994 :
995 2 : WHEN("Both x- and y-axis-aligned rays are present")
996 : {
997 2 : geom.emplace_back(stc, ctr, Degree{0}, VolumeData2D{Size2D{volumeDims}},
998 3 : SinogramData2D{Size2D{sinoDims}});
999 2 : geom.emplace_back(stc, ctr, Degree{90}, VolumeData2D{Size2D{volumeDims}},
1000 3 : SinogramData2D{Size2D{sinoDims}});
1001 2 : geom.emplace_back(stc, ctr, Degree{180}, VolumeData2D{Size2D{volumeDims}},
1002 3 : SinogramData2D{Size2D{sinoDims}});
1003 2 : geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}},
1004 3 : SinogramData2D{Size2D{sinoDims}});
1005 :
1006 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1007 2 : DataContainer sino(sinoDescriptor);
1008 :
1009 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1010 :
1011 2 : THEN("Values are accumulated correctly along each ray's path")
1012 : {
1013 1 : volume = 0;
1014 :
1015 : // set only values along the rays' path to one to make sure interpolation is dones
1016 : // correctly
1017 6 : for (index_t i = 0; i < volSize; i++) {
1018 5 : volume(i, volSize / 2) = 1;
1019 5 : volume(volSize / 2, i) = 1;
1020 : }
1021 :
1022 1 : op.apply(volume, sino);
1023 5 : for (index_t i = 0; i < numImgs; i++)
1024 4 : REQUIRE_EQ(sino[i], Approx(5.0));
1025 :
1026 2 : AND_THEN("Backprojection yields the exact adjoint")
1027 : {
1028 1 : RealVector_t cmp(volSize * volSize);
1029 :
1030 2 : cmp << 0, 0, 10, 0, 0, 0, 0, 10, 0, 0, 10, 10, 20, 10, 10, 0, 0, 10, 0, 0, 0, 0,
1031 2 : 10, 0, 0;
1032 :
1033 1 : op.applyAdjoint(sino, volume);
1034 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, cmp)));
1035 : }
1036 : }
1037 : }
1038 : }
1039 :
1040 10 : GIVEN("A 3D setting with multiple projection angles")
1041 : {
1042 2 : IndexVector_t volumeDims(3), sinoDims(3);
1043 1 : const index_t volSize = 3;
1044 1 : const index_t detectorSize = 1;
1045 1 : const index_t numImgs = 6;
1046 1 : volumeDims << volSize, volSize, volSize;
1047 1 : sinoDims << detectorSize, detectorSize, numImgs;
1048 2 : VolumeDescriptor volumeDescriptor(volumeDims);
1049 2 : DataContainer volume(volumeDescriptor);
1050 :
1051 1 : auto stc = SourceToCenterOfRotation{20 * volSize};
1052 1 : auto ctr = CenterOfRotationToDetector{volSize};
1053 :
1054 2 : std::vector<Geometry> geom;
1055 :
1056 2 : WHEN("x-, y and z-axis-aligned rays are present")
1057 : {
1058 1 : real_t beta[numImgs] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2};
1059 1 : real_t gamma[numImgs] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2};
1060 :
1061 7 : for (index_t i = 0; i < numImgs; i++)
1062 12 : geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}},
1063 12 : SinogramData3D{Size3D{sinoDims}},
1064 18 : RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}});
1065 :
1066 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1067 2 : DataContainer sino(sinoDescriptor);
1068 :
1069 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1070 :
1071 2 : THEN("Values are accumulated correctly along each ray's path")
1072 : {
1073 1 : volume = 0;
1074 :
1075 : // set only values along the rays' path to one to make sure interpolation is dones
1076 : // correctly
1077 4 : for (index_t i = 0; i < volSize; i++) {
1078 3 : volume(i, volSize / 2, volSize / 2) = 1;
1079 3 : volume(volSize / 2, i, volSize / 2) = 1;
1080 3 : volume(volSize / 2, volSize / 2, i) = 1;
1081 : }
1082 :
1083 1 : op.apply(volume, sino);
1084 7 : for (index_t i = 0; i < numImgs; i++)
1085 6 : REQUIRE_EQ(sino[i], Approx(3.0));
1086 :
1087 2 : AND_THEN("Backprojection yields the exact adjoint")
1088 : {
1089 1 : RealVector_t cmp(volSize * volSize * volSize);
1090 :
1091 2 : cmp << 0, 0, 0, 0, 6, 0, 0, 0, 0,
1092 :
1093 1 : 0, 6, 0, 6, 18, 6, 0, 6, 0,
1094 :
1095 2 : 0, 0, 0, 0, 6, 0, 0, 0, 0;
1096 :
1097 1 : op.applyAdjoint(sino, volume);
1098 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, cmp)));
1099 : }
1100 : }
1101 : }
1102 : }
1103 9 : }
1104 :
1105 12 : TEST_CASE("SiddonsMethod: Projection under an angle")
1106 : {
1107 : // Turn logger of
1108 12 : Logger::setLevel(Logger::LogLevel::OFF);
1109 :
1110 20 : GIVEN("A 2D setting with a single ray")
1111 : {
1112 16 : IndexVector_t volumeDims(2), sinoDims(2);
1113 8 : const index_t volSize = 4;
1114 8 : const index_t detectorSize = 1;
1115 8 : const index_t numImgs = 1;
1116 8 : volumeDims << volSize, volSize;
1117 8 : sinoDims << detectorSize, numImgs;
1118 16 : VolumeDescriptor volumeDescriptor(volumeDims);
1119 16 : DataContainer volume(volumeDescriptor);
1120 :
1121 8 : auto stc = SourceToCenterOfRotation{20 * volSize};
1122 8 : auto ctr = CenterOfRotationToDetector{volSize};
1123 24 : auto volData = VolumeData2D{Size2D{volumeDims}};
1124 24 : auto sinoData = SinogramData2D{Size2D{sinoDims}};
1125 :
1126 16 : std::vector<Geometry> geom;
1127 :
1128 9 : WHEN("Projecting under an angle of 30 degrees and ray goes through center of volume")
1129 : {
1130 : // In this case the ray enters and exits the volume through the borders along the main
1131 : // direction
1132 1 : geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData));
1133 :
1134 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1135 2 : DataContainer sino(sinoDescriptor);
1136 :
1137 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1138 :
1139 2 : THEN("Ray intersects the correct pixels")
1140 : {
1141 1 : volume = 1;
1142 1 : volume(3, 0) = 0;
1143 1 : volume(2, 0) = 0;
1144 1 : volume(2, 1) = 0;
1145 :
1146 1 : volume(1, 2) = 0;
1147 1 : volume(1, 3) = 0;
1148 1 : volume(0, 3) = 0;
1149 : // volume(2,2 also hit due to numerical errors)
1150 1 : volume(2, 2) = 0;
1151 :
1152 1 : op.apply(volume, sino);
1153 2 : DataContainer zero(sinoDescriptor);
1154 1 : zero = 0;
1155 1 : CHECK(std::abs(sino[0]) <= Approx(0.0001f).epsilon(epsilon));
1156 :
1157 2 : AND_THEN("The correct weighting is applied")
1158 : {
1159 1 : volume(3, 0) = 1;
1160 1 : volume(2, 0) = 2;
1161 1 : volume(2, 1) = 3;
1162 :
1163 1 : op.apply(volume, sino);
1164 1 : REQUIRE_EQ(sino[0], Approx(2 * std::sqrt(3.f) + 2));
1165 :
1166 : // on the other side of the center
1167 1 : volume = 0;
1168 1 : volume(1, 2) = 3;
1169 1 : volume(1, 3) = 2;
1170 1 : volume(0, 3) = 1;
1171 :
1172 1 : op.apply(volume, sino);
1173 1 : REQUIRE_EQ(sino[0], Approx(2 * std::sqrt(3.f) + 2));
1174 :
1175 1 : sino[0] = 1;
1176 :
1177 1 : RealVector_t expected(volSize * volSize);
1178 2 : expected << 0, 0, 2 - 2 / std::sqrt(3.f), 4 / std::sqrt(3.f) - 2, 0, 0,
1179 2 : 2 / std::sqrt(3.f), 0, 0, 2 / std::sqrt(3.f), 0, 0, 4 / std::sqrt(3.f) - 2,
1180 2 : 2 - 2 / std::sqrt(3.f), 0, 0;
1181 :
1182 1 : op.applyAdjoint(sino, volume);
1183 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, expected)));
1184 : }
1185 : }
1186 : }
1187 :
1188 9 : WHEN("Projecting under an angle of 30 degrees and ray enters through the right border")
1189 : {
1190 : // In this case the ray exits through a border along the main ray direction, but enters
1191 : // through a border not along the main direction
1192 1 : geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData),
1193 2 : PrincipalPointOffset{0}, RotationOffset2D{std::sqrt(3.f), 0});
1194 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1195 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1196 2 : DataContainer sino(sinoDescriptor);
1197 :
1198 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1199 :
1200 2 : THEN("Ray intersects the correct pixels")
1201 : {
1202 1 : volume = 1;
1203 1 : volume(3, 1) = 0;
1204 1 : volume(3, 2) = 0;
1205 1 : volume(3, 3) = 0;
1206 1 : volume(2, 3) = 0;
1207 :
1208 1 : op.apply(volume, sino);
1209 2 : DataContainer zero(sinoDescriptor);
1210 1 : zero = 0;
1211 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1212 :
1213 2 : AND_THEN("The correct weighting is applied")
1214 : {
1215 1 : volume(3, 1) = 4;
1216 1 : volume(3, 2) = 3;
1217 1 : volume(3, 3) = 2;
1218 1 : volume(2, 3) = 1;
1219 :
1220 1 : op.apply(volume, sino);
1221 1 : REQUIRE_EQ(sino[0], Approx(14 - 4 * std::sqrt(3.f)));
1222 :
1223 1 : sino[0] = 1;
1224 :
1225 1 : RealVector_t expected(volSize * volSize);
1226 2 : expected << 0, 0, 0, 0, 0, 0, 0, 4 - 2 * std::sqrt(3.f), 0, 0, 0,
1227 2 : 2 / std::sqrt(3.f), 0, 0, 2 - 2 / std::sqrt(3.f), 4 / std::sqrt(3.f) - 2;
1228 :
1229 1 : op.applyAdjoint(sino, volume);
1230 1 : REQUIRE_UNARY(
1231 : isApprox(volume, DataContainer(volumeDescriptor, expected), epsilon));
1232 : }
1233 : }
1234 : }
1235 :
1236 9 : WHEN("Projecting under an angle of 30 degrees and ray exits through the left border")
1237 : {
1238 : // In this case the ray enters through a border along the main ray direction, but exits
1239 : // through a border not along the main direction
1240 1 : geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData),
1241 2 : PrincipalPointOffset{0}, RotationOffset2D{-std::sqrt(3.f), 0});
1242 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1243 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1244 2 : DataContainer sino(sinoDescriptor);
1245 :
1246 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1247 :
1248 2 : THEN("Ray intersects the correct pixels")
1249 : {
1250 1 : volume = 1;
1251 1 : volume(0, 0) = 0;
1252 1 : volume(1, 0) = 0;
1253 1 : volume(0, 1) = 0;
1254 1 : volume(0, 2) = 0;
1255 :
1256 1 : op.apply(volume, sino);
1257 2 : DataContainer zero(sinoDescriptor);
1258 1 : zero = 0;
1259 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1260 :
1261 2 : AND_THEN("The correct weighting is applied")
1262 : {
1263 1 : volume(1, 0) = 1;
1264 1 : volume(0, 0) = 2;
1265 1 : volume(0, 1) = 3;
1266 1 : volume(0, 2) = 4;
1267 :
1268 1 : op.apply(volume, sino);
1269 1 : REQUIRE_EQ(sino[0], Approx(14 - 4 * std::sqrt(3.f)));
1270 :
1271 1 : sino[0] = 1;
1272 :
1273 1 : RealVector_t expected(volSize * volSize);
1274 2 : expected << 4 / std::sqrt(3.f) - 2, 2 - 2 / std::sqrt(3.f), 0, 0,
1275 2 : 2 / std::sqrt(3.f), 0, 0, 0, 4 - 2 * std::sqrt(3.f), 0, 0, 0, 0, 0, 0, 0;
1276 :
1277 1 : op.applyAdjoint(sino, volume);
1278 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, expected)));
1279 : }
1280 : }
1281 : }
1282 :
1283 9 : WHEN("Projecting under an angle of 30 degrees and ray only intersects a single pixel")
1284 : {
1285 1 : geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData),
1286 0 : PrincipalPointOffset{0},
1287 2 : RotationOffset2D{-2 - std::sqrt(3.f) / 2, 0});
1288 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1289 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1290 2 : DataContainer sino(sinoDescriptor);
1291 :
1292 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1293 :
1294 2 : THEN("Ray intersects the correct pixels")
1295 : {
1296 1 : volume = 1;
1297 1 : volume(0, 0) = 0;
1298 :
1299 1 : op.apply(volume, sino);
1300 2 : DataContainer zero(sinoDescriptor);
1301 1 : zero = 0;
1302 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1303 :
1304 2 : AND_THEN("The correct weighting is applied")
1305 : {
1306 1 : volume(0, 0) = 1;
1307 :
1308 1 : op.apply(volume, sino);
1309 1 : REQUIRE_EQ(sino[0], Approx(1 / std::sqrt(3.f)));
1310 :
1311 1 : sino[0] = 1;
1312 :
1313 1 : RealVector_t expected(volSize * volSize);
1314 1 : expected << 1 / std::sqrt(3.f), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
1315 :
1316 1 : op.applyAdjoint(sino, volume);
1317 1 : REQUIRE_UNARY(
1318 : isApprox(volume, DataContainer(volumeDescriptor, expected), epsilon));
1319 : }
1320 : }
1321 : }
1322 :
1323 9 : WHEN("Projecting under an angle of 120 degrees and ray goes through center of volume")
1324 : {
1325 : // In this case the ray enters and exits the volume through the borders along the main
1326 : // direction
1327 1 : geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData),
1328 2 : std::move(sinoData));
1329 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1330 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1331 2 : DataContainer sino(sinoDescriptor);
1332 :
1333 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1334 :
1335 2 : THEN("Ray intersects the correct pixels")
1336 : {
1337 1 : volume = 1;
1338 1 : volume(0, 0) = 0;
1339 1 : volume(0, 1) = 0;
1340 1 : volume(1, 1) = 0;
1341 1 : volume(2, 2) = 0;
1342 1 : volume(3, 2) = 0;
1343 1 : volume(3, 3) = 0;
1344 : // volume(1,2) hit due to numerical error
1345 1 : volume(1, 2) = 0;
1346 :
1347 1 : op.apply(volume, sino);
1348 2 : DataContainer zero(sinoDescriptor);
1349 1 : zero = 0;
1350 1 : CHECK(std::abs(sino[0]) <= Approx(0.0001f).epsilon(epsilon));
1351 :
1352 2 : AND_THEN("The correct weighting is applied")
1353 : {
1354 1 : volume(0, 0) = 1;
1355 1 : volume(0, 1) = 2;
1356 1 : volume(1, 1) = 3;
1357 :
1358 1 : op.apply(volume, sino);
1359 1 : REQUIRE_EQ(sino[0], Approx(2 * std::sqrt(3.f) + 2));
1360 :
1361 : // on the other side of the center
1362 1 : volume = 0;
1363 1 : volume(2, 2) = 3;
1364 1 : volume(3, 2) = 2;
1365 1 : volume(3, 3) = 1;
1366 :
1367 1 : op.apply(volume, sino);
1368 1 : REQUIRE_EQ(sino[0], Approx(2 * std::sqrt(3.f) + 2));
1369 :
1370 1 : sino[0] = 1;
1371 :
1372 1 : RealVector_t expected(volSize * volSize);
1373 :
1374 2 : expected << 4 / std::sqrt(3.f) - 2, 0, 0, 0, 2 - 2 / std::sqrt(3.f),
1375 2 : 2 / std::sqrt(3.f), 0, 0, 0, 0, 2 / std::sqrt(3.f), 2 - 2 / std::sqrt(3.f),
1376 2 : 0, 0, 0, 4 / std::sqrt(3.f) - 2;
1377 :
1378 1 : op.applyAdjoint(sino, volume);
1379 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, expected)));
1380 : }
1381 : }
1382 : }
1383 :
1384 9 : WHEN("Projecting under an angle of 120 degrees and ray enters through the top border")
1385 : {
1386 : // In this case the ray exits through a border along the main ray direction, but enters
1387 : // through a border not along the main direction
1388 1 : geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData),
1389 1 : std::move(sinoData), PrincipalPointOffset{0},
1390 2 : RotationOffset2D{0, std::sqrt(3.f)});
1391 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1392 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1393 2 : DataContainer sino(sinoDescriptor);
1394 :
1395 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1396 :
1397 2 : THEN("Ray intersects the correct pixels")
1398 : {
1399 1 : volume = 1;
1400 1 : volume(0, 2) = 0;
1401 1 : volume(0, 3) = 0;
1402 1 : volume(1, 3) = 0;
1403 1 : volume(2, 3) = 0;
1404 :
1405 1 : op.apply(volume, sino);
1406 2 : DataContainer zero(sinoDescriptor);
1407 1 : zero = 0;
1408 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1409 :
1410 2 : AND_THEN("The correct weighting is applied")
1411 : {
1412 1 : volume(0, 2) = 1;
1413 1 : volume(0, 3) = 2;
1414 1 : volume(1, 3) = 3;
1415 1 : volume(2, 3) = 4;
1416 :
1417 1 : op.apply(volume, sino);
1418 1 : REQUIRE_EQ(sino[0], Approx(14 - 4 * std::sqrt(3.f)));
1419 :
1420 1 : sino[0] = 1;
1421 :
1422 1 : RealVector_t expected(volSize * volSize);
1423 :
1424 2 : expected << 0, 0, 0, 0, 0, 0, 0, 0, 2 - 2 / std::sqrt(3.f), 0, 0, 0,
1425 2 : 4 / std::sqrt(3.f) - 2, 2 / std::sqrt(3.f), 4 - 2 * std::sqrt(3.f), 0;
1426 :
1427 1 : op.applyAdjoint(sino, volume);
1428 1 : REQUIRE_UNARY(
1429 : isApprox(volume, DataContainer(volumeDescriptor, expected), epsilon));
1430 : }
1431 : }
1432 : }
1433 :
1434 9 : WHEN("Projecting under an angle of 120 degrees and ray exits through the bottom border")
1435 : {
1436 : // In this case the ray enters through a border along the main ray direction, but exits
1437 : // through a border not along the main direction
1438 1 : geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData),
1439 1 : std::move(sinoData), PrincipalPointOffset{0},
1440 2 : RotationOffset2D{0, -std::sqrt(3.f)});
1441 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1442 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1443 2 : DataContainer sino(sinoDescriptor);
1444 :
1445 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1446 :
1447 2 : THEN("Ray intersects the correct pixels")
1448 : {
1449 1 : volume = 1;
1450 1 : volume(1, 0) = 0;
1451 1 : volume(2, 0) = 0;
1452 1 : volume(3, 0) = 0;
1453 1 : volume(3, 1) = 0;
1454 :
1455 1 : op.apply(volume, sino);
1456 2 : DataContainer zero(sinoDescriptor);
1457 1 : zero = 0;
1458 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1459 :
1460 2 : AND_THEN("The correct weighting is applied")
1461 : {
1462 1 : volume(1, 0) = 4;
1463 1 : volume(2, 0) = 3;
1464 1 : volume(3, 0) = 2;
1465 1 : volume(3, 1) = 1;
1466 :
1467 1 : op.apply(volume, sino);
1468 1 : REQUIRE_EQ(sino[0], Approx(14 - 4 * std::sqrt(3.f)));
1469 :
1470 1 : sino[0] = 1;
1471 :
1472 1 : RealVector_t expected(volSize * volSize);
1473 :
1474 2 : expected << 0, 4 - 2 * std::sqrt(3.f), 2 / std::sqrt(3.f),
1475 2 : 4 / std::sqrt(3.f) - 2, 0, 0, 0, 2 - 2 / std::sqrt(3.f), 0, 0, 0, 0, 0, 0,
1476 2 : 0, 0;
1477 :
1478 1 : op.applyAdjoint(sino, volume);
1479 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, expected)));
1480 : }
1481 : }
1482 : }
1483 :
1484 9 : WHEN("Projecting under an angle of 120 degrees and ray only intersects a single pixel")
1485 : {
1486 : // This is a special case that is handled separately in both forward and backprojection
1487 1 : geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData),
1488 1 : std::move(sinoData), PrincipalPointOffset{0},
1489 2 : RotationOffset2D{0, -2 - std::sqrt(3.f) / 2});
1490 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1491 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1492 2 : DataContainer sino(sinoDescriptor);
1493 :
1494 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1495 :
1496 2 : THEN("Ray intersects the correct pixels")
1497 : {
1498 1 : volume = 1;
1499 1 : volume(3, 0) = 0;
1500 :
1501 1 : op.apply(volume, sino);
1502 2 : DataContainer zero(sinoDescriptor);
1503 1 : zero = 0;
1504 1 : REQUIRE_UNARY(isApprox(sino, zero, epsilon));
1505 :
1506 2 : AND_THEN("The correct weighting is applied")
1507 : {
1508 1 : volume(3, 0) = 1;
1509 :
1510 1 : op.apply(volume, sino);
1511 1 : REQUIRE_EQ(sino[0], Approx(1 / std::sqrt(3.f)).epsilon(epsilon));
1512 :
1513 1 : sino[0] = 1;
1514 :
1515 1 : RealVector_t expected(volSize * volSize);
1516 1 : expected << 0, 0, 0, 1 / std::sqrt(3.f), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0;
1517 :
1518 1 : op.applyAdjoint(sino, volume);
1519 1 : REQUIRE_UNARY(
1520 : isApprox(volume, DataContainer(volumeDescriptor, expected), epsilon));
1521 : }
1522 : }
1523 : }
1524 : }
1525 :
1526 16 : GIVEN("A 3D setting with a single ray")
1527 : {
1528 8 : IndexVector_t volumeDims(3), sinoDims(3);
1529 4 : const index_t volSize = 3;
1530 4 : const index_t detectorSize = 1;
1531 4 : const index_t numImgs = 1;
1532 4 : volumeDims << volSize, volSize, volSize;
1533 4 : sinoDims << detectorSize, detectorSize, numImgs;
1534 8 : VolumeDescriptor volumeDescriptor(volumeDims);
1535 8 : DataContainer volume(volumeDescriptor);
1536 :
1537 4 : auto stc = SourceToCenterOfRotation{20 * volSize};
1538 4 : auto ctr = CenterOfRotationToDetector{volSize};
1539 12 : auto volData = VolumeData3D{Size3D{volumeDims}};
1540 12 : auto sinoData = SinogramData3D{Size3D{sinoDims}};
1541 :
1542 8 : std::vector<Geometry> geom;
1543 :
1544 8 : RealVector_t backProj(volSize * volSize * volSize);
1545 :
1546 5 : WHEN("A ray with an angle of 30 degrees goes through the center of the volume")
1547 : {
1548 : // In this case the ray enters and exits the volume along the main direction
1549 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
1550 2 : RotationAngles3D{Gamma{pi_t / 6}});
1551 :
1552 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1553 2 : DataContainer sino(sinoDescriptor);
1554 :
1555 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1556 :
1557 2 : THEN("The ray intersects the correct voxels")
1558 : {
1559 1 : volume = 1;
1560 1 : volume(1, 1, 1) = 0;
1561 1 : volume(2, 1, 0) = 0;
1562 1 : volume(1, 1, 0) = 0;
1563 1 : volume(0, 1, 2) = 0;
1564 1 : volume(1, 1, 2) = 0;
1565 :
1566 1 : op.apply(volume, sino);
1567 1 : REQUIRE_EQ(sino[0], Approx(0).epsilon(1e-5));
1568 :
1569 2 : AND_THEN("The correct weighting is applied")
1570 : {
1571 1 : volume(1, 1, 1) = 1;
1572 1 : volume(2, 1, 0) = 3;
1573 1 : volume(1, 1, 2) = 2;
1574 :
1575 1 : op.apply(volume, sino);
1576 1 : REQUIRE_EQ(sino[0], Approx(3 * std::sqrt(3.f) - 1));
1577 :
1578 1 : sino[0] = 1;
1579 2 : backProj << 0, 0, 0, 0, 1 - 1 / std::sqrt(3.f), std::sqrt(3.f) - 1, 0, 0, 0,
1580 :
1581 2 : 0, 0, 0, 0, 2 / std::sqrt(3.f), 0, 0, 0, 0,
1582 :
1583 2 : 0, 0, 0, std::sqrt(3.f) - 1, 1 - 1 / std::sqrt(3.f), 0, 0, 0, 0;
1584 :
1585 1 : op.applyAdjoint(sino, volume);
1586 1 : REQUIRE_UNARY(
1587 : isApprox(volume, DataContainer(volumeDescriptor, backProj), epsilon));
1588 : }
1589 : }
1590 : }
1591 :
1592 5 : WHEN("A ray with an angle of 30 degrees enters through the right border")
1593 : {
1594 : // In this case the ray enters through a border orthogonal to a non-main direction
1595 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
1596 1 : RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0},
1597 3 : RotationOffset3D{1, 0, 0});
1598 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1599 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1600 2 : DataContainer sino(sinoDescriptor);
1601 :
1602 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1603 :
1604 2 : THEN("The ray intersects the correct voxels")
1605 : {
1606 1 : volume = 1;
1607 1 : volume(2, 1, 1) = 0;
1608 1 : volume(2, 1, 0) = 0;
1609 1 : volume(2, 1, 2) = 0;
1610 1 : volume(1, 1, 2) = 0;
1611 :
1612 1 : op.apply(volume, sino);
1613 1 : REQUIRE_EQ(sino[0], Approx(0).epsilon(1e-5));
1614 :
1615 2 : AND_THEN("The correct weighting is applied")
1616 : {
1617 1 : volume(2, 1, 0) = 4;
1618 1 : volume(1, 1, 2) = 3;
1619 1 : volume(2, 1, 1) = 1;
1620 :
1621 1 : op.apply(volume, sino);
1622 1 : REQUIRE_EQ(sino[0], Approx(1 - 2 / std::sqrt(3.f) + 3 * std::sqrt(3.f)));
1623 :
1624 1 : sino[0] = 1;
1625 2 : backProj << 0, 0, 0, 0, 0, 1 - 1 / std::sqrt(3.f), 0, 0, 0,
1626 :
1627 2 : 0, 0, 0, 0, 0, 2 / std::sqrt(3.f), 0, 0, 0,
1628 :
1629 2 : 0, 0, 0, 0, std::sqrt(3.f) - 1, 1 - 1 / std::sqrt(3.f), 0, 0, 0;
1630 :
1631 1 : op.applyAdjoint(sino, volume);
1632 1 : REQUIRE_UNARY(isApprox(volume, DataContainer(volumeDescriptor, backProj)));
1633 : }
1634 : }
1635 : }
1636 :
1637 5 : WHEN("A ray with an angle of 30 degrees exits through the left border")
1638 : {
1639 : // In this case the ray exit through a border orthogonal to a non-main direction
1640 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
1641 1 : RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0},
1642 3 : RotationOffset3D{-1, 0, 0});
1643 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1644 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1645 2 : DataContainer sino(sinoDescriptor);
1646 :
1647 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1648 :
1649 2 : THEN("The ray intersects the correct voxels")
1650 : {
1651 1 : volume = 1;
1652 1 : volume(0, 1, 0) = 0;
1653 1 : volume(1, 1, 0) = 0;
1654 1 : volume(0, 1, 1) = 0;
1655 1 : volume(0, 1, 2) = 0;
1656 :
1657 1 : op.apply(volume, sino);
1658 1 : REQUIRE_EQ(sino[0], Approx(0).epsilon(1e-5));
1659 :
1660 2 : AND_THEN("The correct weighting is applied")
1661 : {
1662 1 : volume(0, 1, 2) = 4;
1663 1 : volume(1, 1, 0) = 3;
1664 1 : volume(0, 1, 1) = 1;
1665 :
1666 1 : op.apply(volume, sino);
1667 1 : REQUIRE_EQ(
1668 : sino[0],
1669 : Approx(3 * std::sqrt(3.f) + 1 - 2 / std::sqrt(3.f)).epsilon(epsilon));
1670 :
1671 1 : sino[0] = 1;
1672 2 : backProj << 0, 0, 0, 1 - 1 / std::sqrt(3.f), std::sqrt(3.f) - 1, 0, 0, 0, 0,
1673 :
1674 2 : 0, 0, 0, 2 / std::sqrt(3.f), 0, 0, 0, 0, 0,
1675 :
1676 2 : 0, 0, 0, 1 - 1 / std::sqrt(3.f), 0, 0, 0, 0, 0;
1677 :
1678 1 : op.applyAdjoint(sino, volume);
1679 1 : REQUIRE_UNARY(
1680 : isApprox(volume, DataContainer(volumeDescriptor, backProj), epsilon));
1681 : }
1682 : }
1683 : }
1684 :
1685 5 : WHEN("A ray with an angle of 30 degrees only intersects a single voxel")
1686 : {
1687 : // special case - no interior voxels, entry and exit voxels are the same
1688 1 : geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData),
1689 1 : RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0},
1690 3 : RotationOffset3D{-2, 0, 0});
1691 : // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom);
1692 2 : PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom);
1693 2 : DataContainer sino(sinoDescriptor);
1694 :
1695 2 : SiddonsMethod op(volumeDescriptor, sinoDescriptor);
1696 :
1697 2 : THEN("The ray intersects the correct voxels")
1698 : {
1699 1 : volume = 1;
1700 1 : volume(0, 1, 0) = 0;
1701 :
1702 1 : op.apply(volume, sino);
1703 1 : REQUIRE_EQ(sino[0], Approx(0).epsilon(1e-5));
1704 :
1705 2 : AND_THEN("The correct weighting is applied")
1706 : {
1707 1 : volume(0, 1, 0) = 1;
1708 :
1709 1 : op.apply(volume, sino);
1710 1 : REQUIRE_EQ(sino[0], Approx(std::sqrt(3.f) - 1).epsilon(epsilon));
1711 :
1712 1 : sino[0] = 1;
1713 2 : backProj << 0, 0, 0, std::sqrt(3.f) - 1, 0, 0, 0, 0, 0,
1714 :
1715 1 : 0, 0, 0, 0, 0, 0, 0, 0, 0,
1716 :
1717 2 : 0, 0, 0, 0, 0, 0, 0, 0, 0;
1718 :
1719 1 : op.applyAdjoint(sino, volume);
1720 1 : REQUIRE_UNARY(
1721 : isApprox(volume, DataContainer(volumeDescriptor, backProj), epsilon));
1722 : }
1723 : }
1724 : }
1725 : }
1726 12 : }
|