comparison UnitTestsSources/UnitTestsMain.cpp @ 73:ffa6dded91bd wasm

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 24 May 2017 11:59:24 +0200
parents c1cc3bdba18c
children f5f54ed8d307
comparison
equal deleted inserted replaced
72:c1cc3bdba18c 73:ffa6dded91bd
20 20
21 21
22 #include "gtest/gtest.h" 22 #include "gtest/gtest.h"
23 23
24 #include "../Framework/Toolbox/OrthancAsynchronousWebService.h" 24 #include "../Framework/Toolbox/OrthancAsynchronousWebService.h"
25 #include "../Framework/Toolbox/OrthancSlicesLoader.h"
25 #include "../Resources/Orthanc/Core/Logging.h" 26 #include "../Resources/Orthanc/Core/Logging.h"
26 #include "../Framework/Toolbox/OrthancSynchronousWebService.h"
27 #include "../Framework/Layers/OrthancFrameLayerSource.h"
28 #include "../Framework/Widgets/LayerWidget.h"
29
30
31 #include "../Resources/Orthanc/Core/Images/PngReader.h"
32 #include "../Framework/Toolbox/MessagingToolbox.h"
33 #include "../Framework/Toolbox/DicomFrameConverter.h"
34 27
35 #include <boost/lexical_cast.hpp> 28 #include <boost/lexical_cast.hpp>
36 #include <boost/date_time/posix_time/posix_time.hpp> 29 #include <boost/date_time/posix_time/posix_time.hpp>
37 #include <boost/thread/thread.hpp> 30 #include <boost/thread/thread.hpp>
38 31
39 namespace OrthancStone 32 namespace OrthancStone
40 { 33 {
41 class Slice
42 {
43 private:
44 enum Type
45 {
46 Type_Invalid,
47 Type_OrthancInstance
48 // TODO A slice could come from some DICOM file (URL)
49 };
50
51 Type type_;
52 std::string orthancInstanceId_;
53 unsigned int frame_;
54 SliceGeometry geometry_;
55 double pixelSpacingX_;
56 double pixelSpacingY_;
57 double thickness_;
58 unsigned int width_;
59 unsigned int height_;
60 DicomFrameConverter converter_;
61
62 public:
63 Slice() : type_(Type_Invalid)
64 {
65 }
66
67 bool ParseOrthancFrame(const OrthancPlugins::IDicomDataset& dataset,
68 const std::string& instanceId,
69 unsigned int frame)
70 {
71 OrthancPlugins::DicomDatasetReader reader(dataset);
72
73 unsigned int frameCount;
74 if (!reader.GetUnsignedIntegerValue(frameCount, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES))
75 {
76 frameCount = 1; // Assume instance with one frame
77 }
78
79 if (frame >= frameCount)
80 {
81 return false;
82 }
83
84 if (!reader.GetDoubleValue(thickness_, OrthancPlugins::DICOM_TAG_SLICE_THICKNESS))
85 {
86 thickness_ = 100.0 * std::numeric_limits<double>::epsilon();
87 }
88
89 GeometryToolbox::GetPixelSpacing(pixelSpacingX_, pixelSpacingY_, dataset);
90
91 std::string position, orientation;
92 if (dataset.GetStringValue(position, OrthancPlugins::DICOM_TAG_IMAGE_POSITION_PATIENT) &&
93 dataset.GetStringValue(orientation, OrthancPlugins::DICOM_TAG_IMAGE_ORIENTATION_PATIENT))
94 {
95 geometry_ = SliceGeometry(position, orientation);
96 }
97
98 if (reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) &&
99 reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS))
100 {
101 orthancInstanceId_ = instanceId;
102 frame_ = frame;
103 converter_.ReadParameters(dataset);
104
105 type_ = Type_OrthancInstance;
106 return true;
107 }
108 else
109 {
110 return false;
111 }
112 }
113
114 bool IsOrthancInstance() const
115 {
116 return type_ == Type_OrthancInstance;
117 }
118
119 const std::string GetOrthancInstanceId() const
120 {
121 if (type_ != Type_OrthancInstance)
122 {
123 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
124 }
125
126 return orthancInstanceId_;
127 }
128
129 unsigned int GetFrame() const
130 {
131 if (type_ == Type_Invalid)
132 {
133 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
134 }
135
136 return frame_;
137 }
138
139 const SliceGeometry& GetGeometry() const
140 {
141 if (type_ == Type_Invalid)
142 {
143 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
144 }
145
146 return geometry_;
147 }
148
149 double GetThickness() const
150 {
151 if (type_ == Type_Invalid)
152 {
153 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
154 }
155
156 return thickness_;
157 }
158
159 double GetPixelSpacingX() const
160 {
161 if (type_ == Type_Invalid)
162 {
163 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
164 }
165
166 return pixelSpacingX_;
167 }
168
169 double GetPixelSpacingY() const
170 {
171 if (type_ == Type_Invalid)
172 {
173 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
174 }
175
176 return pixelSpacingY_;
177 }
178
179 unsigned int GetWidth() const
180 {
181 if (type_ == Type_Invalid)
182 {
183 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
184 }
185
186 return width_;
187 }
188
189 unsigned int GetHeight() const
190 {
191 if (type_ == Type_Invalid)
192 {
193 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
194 }
195
196 return height_;
197 }
198
199 const DicomFrameConverter& GetConverter() const
200 {
201 if (type_ == Type_Invalid)
202 {
203 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
204 }
205
206 return converter_;
207 }
208 };
209
210
211 class SliceSorter : public boost::noncopyable
212 {
213 private:
214 class SliceWithDepth : public boost::noncopyable
215 {
216 private:
217 Slice slice_;
218 double depth_;
219
220 public:
221 SliceWithDepth(const Slice& slice) :
222 slice_(slice),
223 depth_(0)
224 {
225 }
226
227 void SetNormal(const Vector& normal)
228 {
229 depth_ = boost::numeric::ublas::inner_prod
230 (slice_.GetGeometry().GetOrigin(), normal);
231 }
232
233 double GetDepth() const
234 {
235 return depth_;
236 }
237
238 const Slice& GetSlice() const
239 {
240 return slice_;
241 }
242 };
243
244 struct Comparator
245 {
246 bool operator() (const SliceWithDepth* const& a,
247 const SliceWithDepth* const& b) const
248 {
249 return a->GetDepth() < b->GetDepth();
250 }
251 };
252
253 typedef std::vector<SliceWithDepth*> Slices;
254
255 Slices slices_;
256 bool hasNormal_;
257
258 public:
259 SliceSorter() : hasNormal_(false)
260 {
261 }
262
263 ~SliceSorter()
264 {
265 for (size_t i = 0; i < slices_.size(); i++)
266 {
267 assert(slices_[i] != NULL);
268 delete slices_[i];
269 }
270 }
271
272 void Reserve(size_t count)
273 {
274 slices_.reserve(count);
275 }
276
277 void AddSlice(const Slice& slice)
278 {
279 slices_.push_back(new SliceWithDepth(slice));
280 }
281
282 size_t GetSliceCount() const
283 {
284 return slices_.size();
285 }
286
287 const Slice& GetSlice(size_t i) const
288 {
289 if (i >= slices_.size())
290 {
291 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
292 }
293
294 assert(slices_[i] != NULL);
295 return slices_[i]->GetSlice();
296 }
297
298 void SetNormal(const Vector& normal)
299 {
300 for (size_t i = 0; i < slices_.size(); i++)
301 {
302 slices_[i]->SetNormal(normal);
303 }
304
305 hasNormal_ = true;
306 }
307
308 void Sort()
309 {
310 if (!hasNormal_)
311 {
312 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
313 }
314
315 Comparator comparator;
316 std::sort(slices_.begin(), slices_.end(), comparator);
317 }
318
319 void FilterNormal(const Vector& normal)
320 {
321 size_t pos = 0;
322
323 for (size_t i = 0; i < slices_.size(); i++)
324 {
325 if (GeometryToolbox::IsParallel(normal, slices_[i]->GetSlice().GetGeometry().GetNormal()))
326 {
327 // This slice is compatible with the selected normal
328 slices_[pos] = slices_[i];
329 pos += 1;
330 }
331 else
332 {
333 delete slices_[i];
334 slices_[i] = NULL;
335 }
336 }
337
338 slices_.resize(pos);
339 }
340
341 bool SelectNormal(Vector& normal) const
342 {
343 std::vector<Vector> normalCandidates;
344 std::vector<unsigned int> normalCount;
345
346 bool found = false;
347
348 for (size_t i = 0; !found && i < GetSliceCount(); i++)
349 {
350 const Vector& normal = GetSlice(i).GetGeometry().GetNormal();
351
352 bool add = true;
353 for (size_t j = 0; add && j < normalCandidates.size(); j++) // (*)
354 {
355 if (GeometryToolbox::IsParallel(normal, normalCandidates[j]))
356 {
357 normalCount[j] += 1;
358 add = false;
359 }
360 }
361
362 if (add)
363 {
364 if (normalCount.size() > 2)
365 {
366 // To get linear-time complexity in (*). This heuristics
367 // allows the series to have one single frame that is
368 // not parallel to the others (such a frame could be a
369 // generated preview)
370 found = false;
371 }
372 else
373 {
374 normalCandidates.push_back(normal);
375 normalCount.push_back(1);
376 }
377 }
378 }
379
380 for (size_t i = 0; !found && i < normalCandidates.size(); i++)
381 {
382 unsigned int count = normalCount[i];
383 if (count == GetSliceCount() ||
384 count + 1 == GetSliceCount())
385 {
386 normal = normalCandidates[i];
387 found = true;
388 }
389 }
390
391 return found;
392 }
393 };
394
395
396
397 class OrthancSlicesLoader : public boost::noncopyable
398 {
399 public:
400 class ICallback : public boost::noncopyable
401 {
402 public:
403 virtual ~ICallback()
404 {
405 }
406
407 virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) = 0;
408
409 virtual void NotifyGeometryError(const OrthancSlicesLoader& loader) = 0;
410
411 virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader,
412 unsigned int sliceIndex,
413 const Slice& slice,
414 Orthanc::ImageAccessor* image) = 0;
415
416 virtual void NotifySliceImageError(const OrthancSlicesLoader& loader,
417 unsigned int sliceIndex,
418 const Slice& slice) = 0;
419 };
420
421 private:
422 enum State
423 {
424 State_Error,
425 State_Initialization,
426 State_LoadingGeometry,
427 State_GeometryReady
428 };
429
430 enum Mode
431 {
432 Mode_SeriesGeometry,
433 Mode_InstanceGeometry,
434 Mode_LoadImage
435 };
436
437 class Operation : public Orthanc::IDynamicObject
438 {
439 private:
440 Mode mode_;
441 unsigned int frame_;
442 unsigned int sliceIndex_;
443 const Slice* slice_;
444 std::string instanceId_;
445
446 Operation(Mode mode) :
447 mode_(mode)
448 {
449 }
450
451 public:
452 Mode GetMode() const
453 {
454 return mode_;
455 }
456
457 unsigned int GetSliceIndex() const
458 {
459 assert(mode_ == Mode_LoadImage);
460 return sliceIndex_;
461 }
462
463 const Slice& GetSlice() const
464 {
465 assert(mode_ == Mode_LoadImage && slice_ != NULL);
466 return *slice_;
467 }
468
469 unsigned int GetFrame() const
470 {
471 assert(mode_ == Mode_InstanceGeometry);
472 return frame_;
473 }
474
475 const std::string& GetInstanceId() const
476 {
477 assert(mode_ == Mode_InstanceGeometry);
478 return instanceId_;
479 }
480
481 static Operation* DownloadSeriesGeometry()
482 {
483 return new Operation(Mode_SeriesGeometry);
484 }
485
486 static Operation* DownloadInstanceGeometry(const std::string& instanceId,
487 unsigned int frame)
488 {
489 std::auto_ptr<Operation> operation(new Operation(Mode_InstanceGeometry));
490 operation->instanceId_ = instanceId;
491 operation->frame_ = frame;
492 return operation.release();
493 }
494
495 static Operation* DownloadSliceImage(unsigned int sliceIndex,
496 const Slice& slice)
497 {
498 std::auto_ptr<Operation> tmp(new Operation(Mode_LoadImage));
499 tmp->sliceIndex_ = sliceIndex;
500 tmp->slice_ = &slice;
501 return tmp.release();
502 }
503 };
504
505
506 class WebCallback : public IWebService::ICallback
507 {
508 private:
509 OrthancSlicesLoader& that_;
510
511 public:
512 WebCallback(OrthancSlicesLoader& that) :
513 that_(that)
514 {
515 }
516
517 virtual void NotifySuccess(const std::string& uri,
518 const void* answer,
519 size_t answerSize,
520 Orthanc::IDynamicObject* payload)
521 {
522 std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload));
523
524 switch (operation->GetMode())
525 {
526 case Mode_SeriesGeometry:
527 that_.ParseSeriesGeometry(answer, answerSize);
528 break;
529
530 case Mode_InstanceGeometry:
531 that_.ParseInstanceGeometry(operation->GetInstanceId(),
532 operation->GetFrame(), answer, answerSize);
533 break;
534
535 case Mode_LoadImage:
536 that_.ParseSliceImage(*operation, answer, answerSize);
537 break;
538
539 default:
540 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
541 }
542 }
543
544 virtual void NotifyError(const std::string& uri,
545 Orthanc::IDynamicObject* payload)
546 {
547 std::auto_ptr<Operation> operation(dynamic_cast<Operation*>(payload));
548 LOG(ERROR) << "Cannot download " << uri;
549
550 switch (operation->GetMode())
551 {
552 case Mode_SeriesGeometry:
553 that_.userCallback_.NotifyGeometryError(that_);
554 that_.state_ = State_Error;
555 break;
556
557 case Mode_LoadImage:
558 that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(),
559 operation->GetSlice());
560 break;
561
562 default:
563 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
564 }
565 }
566 };
567
568
569 WebCallback webCallback_;
570 ICallback& userCallback_;
571 IWebService& orthanc_;
572 State state_;
573 SliceSorter slices_;
574
575
576 void ParseSeriesGeometry(const void* answer,
577 size_t size)
578 {
579 Json::Value series;
580 if (!MessagingToolbox::ParseJson(series, answer, size) ||
581 series.type() != Json::objectValue)
582 {
583 userCallback_.NotifyGeometryError(*this);
584 return;
585 }
586
587 Json::Value::Members instances = series.getMemberNames();
588
589 slices_.Reserve(instances.size());
590
591 for (size_t i = 0; i < instances.size(); i++)
592 {
593 OrthancPlugins::FullOrthancDataset dataset(series[instances[i]]);
594
595 Slice slice;
596 if (slice.ParseOrthancFrame(dataset, instances[i], 0 /* todo */))
597 {
598 slices_.AddSlice(slice);
599 }
600 else
601 {
602 LOG(WARNING) << "Skipping invalid instance " << instances[i];
603 }
604 }
605
606 bool ok = false;
607
608 if (slices_.GetSliceCount() > 0)
609 {
610 Vector normal;
611 if (slices_.SelectNormal(normal))
612 {
613 slices_.FilterNormal(normal);
614 slices_.SetNormal(normal);
615 slices_.Sort();
616 ok = true;
617 }
618 }
619
620 state_ = State_GeometryReady;
621
622 if (ok)
623 {
624 LOG(INFO) << "Loaded a series with " << slices_.GetSliceCount() << " slice(s)";
625 userCallback_.NotifyGeometryReady(*this);
626 }
627 else
628 {
629 LOG(ERROR) << "This series is empty";
630 userCallback_.NotifyGeometryError(*this);
631 }
632 }
633
634
635 void ParseInstanceGeometry(const std::string& instanceId,
636 unsigned int frame,
637 const void* answer,
638 size_t size)
639 {
640 Json::Value tags;
641 if (!MessagingToolbox::ParseJson(tags, answer, size) ||
642 tags.type() != Json::objectValue)
643 {
644 userCallback_.NotifyGeometryError(*this);
645 return;
646 }
647
648 OrthancPlugins::FullOrthancDataset dataset(tags);
649
650 state_ = State_GeometryReady;
651
652 Slice slice;
653 if (slice.ParseOrthancFrame(dataset, instanceId, frame))
654 {
655 LOG(INFO) << "Loaded instance " << instanceId;
656 slices_.AddSlice(slice);
657 userCallback_.NotifyGeometryReady(*this);
658 }
659 else
660 {
661 LOG(WARNING) << "Skipping invalid instance " << instanceId;
662 userCallback_.NotifyGeometryError(*this);
663 }
664 }
665
666
667 void ParseSliceImage(const Operation& operation,
668 const void* answer,
669 size_t size)
670 {
671 std::auto_ptr<Orthanc::PngReader> image(new Orthanc::PngReader);
672 image->ReadFromMemory(answer, size);
673
674 bool ok = (image->GetWidth() == operation.GetSlice().GetWidth() ||
675 image->GetHeight() == operation.GetSlice().GetHeight());
676
677 if (ok &&
678 operation.GetSlice().GetConverter().GetExpectedPixelFormat() ==
679 Orthanc::PixelFormat_SignedGrayscale16)
680 {
681 if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16)
682 {
683 image->SetFormat(Orthanc::PixelFormat_SignedGrayscale16);
684 }
685 else
686 {
687 ok = false;
688 }
689 }
690
691 if (ok)
692 {
693 userCallback_.NotifySliceImageReady(*this, operation.GetSliceIndex(),
694 operation.GetSlice(), image.release());
695 }
696 else
697 {
698 userCallback_.NotifySliceImageError(*this, operation.GetSliceIndex(),
699 operation.GetSlice());
700 }
701 }
702
703
704 public:
705 OrthancSlicesLoader(ICallback& callback,
706 IWebService& orthanc) :
707 webCallback_(*this),
708 userCallback_(callback),
709 orthanc_(orthanc),
710 state_(State_Initialization)
711 {
712 }
713
714 void ScheduleLoadSeries(const std::string& seriesId)
715 {
716 if (state_ != State_Initialization)
717 {
718 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
719 }
720 else
721 {
722 state_ = State_LoadingGeometry;
723 std::string uri = "/series/" + seriesId + "/instances-tags";
724 orthanc_.ScheduleGetRequest(webCallback_, uri, Operation::DownloadSeriesGeometry());
725 }
726 }
727
728 void ScheduleLoadInstance(const std::string& instanceId,
729 unsigned int frame)
730 {
731 if (state_ != State_Initialization)
732 {
733 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
734 }
735 else
736 {
737 state_ = State_LoadingGeometry;
738 std::string uri = "/instances/" + instanceId + "/tags";
739 orthanc_.ScheduleGetRequest
740 (webCallback_, uri, Operation::DownloadInstanceGeometry(instanceId, frame));
741 }
742 }
743
744 size_t GetSliceCount() const
745 {
746 if (state_ != State_GeometryReady)
747 {
748 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
749 }
750
751 return slices_.GetSliceCount();
752 }
753
754 const Slice& GetSlice(size_t index) const
755 {
756 if (state_ != State_GeometryReady)
757 {
758 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
759 }
760
761 return slices_.GetSlice(index);
762 }
763
764 void ScheduleLoadSliceImage(size_t index)
765 {
766 if (state_ != State_GeometryReady)
767 {
768 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
769 }
770 else
771 {
772 const Slice& slice = GetSlice(index);
773
774 std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" +
775 boost::lexical_cast<std::string>(slice.GetFrame()));
776
777 switch (slice.GetConverter().GetExpectedPixelFormat())
778 {
779 case Orthanc::PixelFormat_RGB24:
780 uri += "/preview";
781 break;
782
783 case Orthanc::PixelFormat_Grayscale16:
784 uri += "/image-uint16";
785 break;
786
787 case Orthanc::PixelFormat_SignedGrayscale16:
788 uri += "/image-int16";
789 break;
790
791 default:
792 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
793 }
794
795 orthanc_.ScheduleGetRequest(webCallback_, uri, Operation::DownloadSliceImage(index, slice));
796 }
797 }
798 };
799
800
801 class Tata : public OrthancSlicesLoader::ICallback 34 class Tata : public OrthancSlicesLoader::ICallback
802 { 35 {
803 public: 36 public:
804 virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader) 37 virtual void NotifyGeometryReady(const OrthancSlicesLoader& loader)
805 { 38 {