Mercurial > hg > orthanc-stone
comparison Samples/Sdl/Loader.cpp @ 814:aead999345e0
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 28 May 2019 21:16:39 +0200 |
parents | abc08ac721d3 |
children | df442f1ba0c6 |
comparison
equal
deleted
inserted
replaced
813:bc7ee59420a1 | 814:aead999345e0 |
---|---|
17 * You should have received a copy of the GNU Affero General Public License | 17 * You should have received a copy of the GNU Affero General Public License |
18 * along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 **/ | 19 **/ |
20 | 20 |
21 | 21 |
22 #include "../../Framework/Toolbox/DicomInstanceParameters.h" | 22 #include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h" |
23 #include "../../Framework/Volumes/DicomVolumeImageMPRSlicer.h" | |
24 #include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h" | |
25 #include "../../Framework/Scene2D/LookupTableStyleConfigurator.h" | |
23 #include "../../Framework/Oracle/ThreadedOracle.h" | 26 #include "../../Framework/Oracle/ThreadedOracle.h" |
24 #include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h" | 27 #include "../../Framework/Oracle/GetOrthancWebViewerJpegCommand.h" |
25 #include "../../Framework/Oracle/GetOrthancImageCommand.h" | 28 #include "../../Framework/Oracle/GetOrthancImageCommand.h" |
26 #include "../../Framework/Oracle/OrthancRestApiCommand.h" | 29 #include "../../Framework/Oracle/OrthancRestApiCommand.h" |
27 #include "../../Framework/Oracle/SleepOracleCommand.h" | 30 #include "../../Framework/Oracle/SleepOracleCommand.h" |
58 | 61 |
59 | 62 |
60 namespace OrthancStone | 63 namespace OrthancStone |
61 { | 64 { |
62 /** | 65 /** |
63 This interface is implemented by objects able to create an ISceneLayer | |
64 suitable to display the Orthanc image supplied to the CreateTextureXX | |
65 factory methods (taking Dicom parameters into account if relevant). | |
66 | |
67 It can also refresh the style of an existing layer afterwards, to match | |
68 the configurator settings. | |
69 */ | |
70 class ILayerStyleConfigurator | |
71 { | |
72 public: | |
73 virtual ~ILayerStyleConfigurator() | |
74 { | |
75 } | |
76 | |
77 virtual uint64_t GetRevision() const = 0; | |
78 | |
79 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0; | |
80 | |
81 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
82 const DicomInstanceParameters& parameters) const = 0; | |
83 | |
84 virtual void ApplyStyle(ISceneLayer& layer) const = 0; | |
85 }; | |
86 | |
87 /** | |
88 This configurator supplies an API to set a display range and a LUT. | |
89 */ | |
90 class LookupTableStyleConfigurator : public ILayerStyleConfigurator | |
91 { | |
92 private: | |
93 uint64_t revision_; | |
94 bool hasLut_; | |
95 std::string lut_; | |
96 bool hasRange_; | |
97 float minValue_; | |
98 float maxValue_; | |
99 | |
100 public: | |
101 LookupTableStyleConfigurator() : | |
102 revision_(0), | |
103 hasLut_(false), | |
104 hasRange_(false) | |
105 { | |
106 } | |
107 | |
108 void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource) | |
109 { | |
110 hasLut_ = true; | |
111 Orthanc::EmbeddedResources::GetFileResource(lut_, resource); | |
112 } | |
113 | |
114 void SetLookupTable(const std::string& lut) | |
115 { | |
116 hasLut_ = true; | |
117 lut_ = lut; | |
118 } | |
119 | |
120 void SetRange(float minValue, | |
121 float maxValue) | |
122 { | |
123 if (minValue > maxValue) | |
124 { | |
125 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
126 } | |
127 else | |
128 { | |
129 hasRange_ = true; | |
130 minValue_ = minValue; | |
131 maxValue_ = maxValue; | |
132 } | |
133 } | |
134 | |
135 virtual uint64_t GetRevision() const | |
136 { | |
137 return revision_; | |
138 } | |
139 | |
140 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const | |
141 { | |
142 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
143 } | |
144 | |
145 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
146 const DicomInstanceParameters& parameters) const | |
147 { | |
148 return parameters.CreateLookupTableTexture(frame); | |
149 } | |
150 | |
151 virtual void ApplyStyle(ISceneLayer& layer) const | |
152 { | |
153 LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer); | |
154 | |
155 if (hasLut_) | |
156 { | |
157 l.SetLookupTable(lut_); | |
158 } | |
159 | |
160 if (hasRange_) | |
161 { | |
162 l.SetRange(minValue_, maxValue_); | |
163 } | |
164 else | |
165 { | |
166 l.FitRange(); | |
167 } | |
168 } | |
169 }; | |
170 | |
171 /** | |
172 Creates layers to display the supplied image in grayscale. No dynamic | |
173 style is available. | |
174 */ | |
175 class GrayscaleStyleConfigurator : public ILayerStyleConfigurator | |
176 { | |
177 private: | |
178 uint64_t revision_; | |
179 | |
180 public: | |
181 GrayscaleStyleConfigurator() : | |
182 revision_(0) | |
183 { | |
184 } | |
185 | |
186 virtual uint64_t GetRevision() const | |
187 { | |
188 return revision_; | |
189 } | |
190 | |
191 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const | |
192 { | |
193 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
194 } | |
195 | |
196 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
197 const DicomInstanceParameters& parameters) const | |
198 { | |
199 return parameters.CreateTexture(frame); | |
200 } | |
201 | |
202 virtual void ApplyStyle(ISceneLayer& layer) const | |
203 { | |
204 } | |
205 }; | |
206 | |
207 /** | |
208 This interface is implemented by objects representing 3D volume data and | |
209 that are able to return an object that: | |
210 - represent a slice of their data | |
211 - are able to create the corresponding slice visual representation. | |
212 */ | |
213 class IVolumeSlicer : public boost::noncopyable | |
214 { | |
215 public: | |
216 /** | |
217 This interface is implemented by objects representing a slice of | |
218 volume data and that are able to create a 2D layer to display a this | |
219 slice. | |
220 | |
221 The CreateSceneLayer factory method is called with an optional | |
222 configurator that possibly impacts the ISceneLayer subclass that is | |
223 created (for instance, if a LUT must be applied on the texture when | |
224 displaying it) | |
225 */ | |
226 class IExtractedSlice : public boost::noncopyable | |
227 { | |
228 public: | |
229 virtual ~IExtractedSlice() | |
230 { | |
231 } | |
232 | |
233 /** | |
234 Invalid slices are created when the data is not ready yet or if the | |
235 cut is outside of the available geometry. | |
236 */ | |
237 virtual bool IsValid() = 0; | |
238 | |
239 /** | |
240 This retrieves the *revision* that gets incremented every time the | |
241 underlying object undergoes a mutable operation (that it, changes its | |
242 state). | |
243 This **must** be a cheap call. | |
244 */ | |
245 virtual uint64_t GetRevision() = 0; | |
246 | |
247 /** Creates the slice visual representation */ | |
248 virtual ISceneLayer* CreateSceneLayer( | |
249 const ILayerStyleConfigurator* configurator, // possibly absent | |
250 const CoordinateSystem3D& cuttingPlane) = 0; | |
251 }; | |
252 | |
253 /** | |
254 See IExtractedSlice.IsValid() | |
255 */ | |
256 class InvalidSlice : public IExtractedSlice | |
257 { | |
258 public: | |
259 virtual bool IsValid() | |
260 { | |
261 return false; | |
262 } | |
263 | |
264 virtual uint64_t GetRevision() | |
265 { | |
266 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
267 } | |
268 | |
269 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, | |
270 const CoordinateSystem3D& cuttingPlane) | |
271 { | |
272 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
273 } | |
274 }; | |
275 | |
276 | |
277 virtual ~IVolumeSlicer() | |
278 { | |
279 } | |
280 | |
281 /** | |
282 This method is implemented by the objects representing volumetric data | |
283 and must returns an IExtractedSlice subclass that contains all the data | |
284 needed to, later on, create its visual representation through | |
285 CreateSceneLayer. | |
286 Subclasses a.o.: | |
287 - InvalidSlice, | |
288 - DicomVolumeImageMPRSlicer::Slice, | |
289 - DicomVolumeImageReslicer::Slice | |
290 - DicomStructureSetLoader::Slice | |
291 */ | |
292 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; | |
293 }; | |
294 | |
295 /** | |
296 This class combines a 3D image buffer, a 3D volume geometry and | |
297 information about the DICOM parameters of the series. | |
298 (MPR means MultiPlanar Reconstruction) | |
299 */ | |
300 class DicomVolumeImage : public boost::noncopyable | |
301 { | |
302 public: | |
303 ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage); | |
304 ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage); | |
305 | |
306 private: | |
307 uint64_t revision_; | |
308 std::auto_ptr<VolumeImageGeometry> geometry_; | |
309 std::auto_ptr<ImageBuffer3D> image_; | |
310 std::auto_ptr<DicomInstanceParameters> parameters_; | |
311 | |
312 void CheckHasGeometry() const | |
313 { | |
314 if (!HasGeometry()) | |
315 { | |
316 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
317 } | |
318 } | |
319 | |
320 public: | |
321 DicomVolumeImage() : | |
322 revision_(0) | |
323 { | |
324 } | |
325 | |
326 void IncrementRevision() | |
327 { | |
328 revision_ ++; | |
329 } | |
330 | |
331 void Initialize(const VolumeImageGeometry& geometry, | |
332 Orthanc::PixelFormat format) | |
333 { | |
334 geometry_.reset(new VolumeImageGeometry(geometry)); | |
335 image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(), | |
336 geometry_->GetDepth(), false /* don't compute range */)); | |
337 | |
338 revision_ ++; | |
339 } | |
340 | |
341 void SetDicomParameters(const DicomInstanceParameters& parameters) | |
342 { | |
343 parameters_.reset(parameters.Clone()); | |
344 revision_ ++; | |
345 } | |
346 | |
347 uint64_t GetRevision() const | |
348 { | |
349 return revision_; | |
350 } | |
351 | |
352 bool HasGeometry() const | |
353 { | |
354 return (geometry_.get() != NULL && | |
355 image_.get() != NULL); | |
356 } | |
357 | |
358 ImageBuffer3D& GetPixelData() | |
359 { | |
360 CheckHasGeometry(); | |
361 return *image_; | |
362 } | |
363 | |
364 const ImageBuffer3D& GetPixelData() const | |
365 { | |
366 CheckHasGeometry(); | |
367 return *image_; | |
368 } | |
369 | |
370 const VolumeImageGeometry& GetGeometry() const | |
371 { | |
372 CheckHasGeometry(); | |
373 return *geometry_; | |
374 } | |
375 | |
376 bool HasDicomParameters() const | |
377 { | |
378 return parameters_.get() != NULL; | |
379 } | |
380 | |
381 const DicomInstanceParameters& GetDicomParameters() const | |
382 { | |
383 if (HasDicomParameters()) | |
384 { | |
385 return *parameters_; | |
386 } | |
387 else | |
388 { | |
389 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
390 } | |
391 } | |
392 }; | |
393 | |
394 /** | |
395 Implements the IVolumeSlicer on Dicom volume data when the cutting plane | |
396 that is supplied to the slicer is either axial, sagittal or coronal. | |
397 Arbitrary planes are *not* supported | |
398 */ | |
399 class DicomVolumeImageMPRSlicer : public IVolumeSlicer | |
400 { | |
401 public: | |
402 class Slice : public IExtractedSlice | |
403 { | |
404 private: | |
405 const DicomVolumeImage& volume_; | |
406 bool valid_; | |
407 VolumeProjection projection_; | |
408 unsigned int sliceIndex_; | |
409 | |
410 void CheckValid() const | |
411 { | |
412 if (!valid_) | |
413 { | |
414 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
415 } | |
416 } | |
417 | |
418 protected: | |
419 // Can be overloaded in subclasses | |
420 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
421 unsigned int sliceIndex) const | |
422 { | |
423 return volume_.GetRevision(); | |
424 } | |
425 | |
426 public: | |
427 /** | |
428 Represents a slice of a volume image that is parallel to the | |
429 coordinate system axis. | |
430 The constructor initializes the type of projection (axial, sagittal or | |
431 coronal) and the corresponding slice index, from the cutting plane. | |
432 */ | |
433 Slice(const DicomVolumeImage& volume, | |
434 const CoordinateSystem3D& cuttingPlane) : | |
435 volume_(volume) | |
436 { | |
437 valid_ = (volume_.HasDicomParameters() && | |
438 volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); | |
439 } | |
440 | |
441 VolumeProjection GetProjection() const | |
442 { | |
443 CheckValid(); | |
444 return projection_; | |
445 } | |
446 | |
447 unsigned int GetSliceIndex() const | |
448 { | |
449 CheckValid(); | |
450 return sliceIndex_; | |
451 } | |
452 | |
453 virtual bool IsValid() | |
454 { | |
455 return valid_; | |
456 } | |
457 | |
458 virtual uint64_t GetRevision() | |
459 { | |
460 CheckValid(); | |
461 return GetRevisionInternal(projection_, sliceIndex_); | |
462 } | |
463 | |
464 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, | |
465 const CoordinateSystem3D& cuttingPlane) | |
466 { | |
467 CheckValid(); | |
468 | |
469 if (configurator == NULL) | |
470 { | |
471 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer, | |
472 "A style configurator is mandatory for textures"); | |
473 } | |
474 | |
475 std::auto_ptr<TextureBaseSceneLayer> texture; | |
476 | |
477 { | |
478 const DicomInstanceParameters& parameters = volume_.GetDicomParameters(); | |
479 ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); | |
480 texture.reset(dynamic_cast<TextureBaseSceneLayer*> | |
481 (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters))); | |
482 } | |
483 | |
484 const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_); | |
485 | |
486 double x0, y0, x1, y1; | |
487 cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); | |
488 cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); | |
489 texture->SetOrigin(x0, y0); | |
490 | |
491 double dx = x1 - x0; | |
492 double dy = y1 - y0; | |
493 if (!LinearAlgebra::IsCloseToZero(dx) || | |
494 !LinearAlgebra::IsCloseToZero(dy)) | |
495 { | |
496 texture->SetAngle(atan2(dy, dx)); | |
497 } | |
498 | |
499 Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_); | |
500 texture->SetPixelSpacing(tmp[0], tmp[1]); | |
501 | |
502 return texture.release(); | |
503 | |
504 #if 0 | |
505 double w = texture->GetTexture().GetWidth() * tmp[0]; | |
506 double h = texture->GetTexture().GetHeight() * tmp[1]; | |
507 printf("%.1f %.1f %.1f => %.1f %.1f => %.1f %.1f\n", | |
508 system.GetOrigin() [0], | |
509 system.GetOrigin() [1], | |
510 system.GetOrigin() [2], | |
511 x0, y0, x0 + w, y0 + h); | |
512 | |
513 std::auto_ptr<PolylineSceneLayer> toto(new PolylineSceneLayer); | |
514 | |
515 PolylineSceneLayer::Chain c; | |
516 c.push_back(ScenePoint2D(x0, y0)); | |
517 c.push_back(ScenePoint2D(x0 + w, y0)); | |
518 c.push_back(ScenePoint2D(x0 + w, y0 + h)); | |
519 c.push_back(ScenePoint2D(x0, y0 + h)); | |
520 | |
521 toto->AddChain(c, true); | |
522 | |
523 return toto.release(); | |
524 #endif | |
525 } | |
526 }; | |
527 | |
528 private: | |
529 boost::shared_ptr<DicomVolumeImage> volume_; | |
530 | |
531 public: | |
532 DicomVolumeImageMPRSlicer(const boost::shared_ptr<DicomVolumeImage>& volume) : | |
533 volume_(volume) | |
534 { | |
535 } | |
536 | |
537 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE | |
538 { | |
539 if (volume_->HasGeometry()) | |
540 { | |
541 return new Slice(*volume_, cuttingPlane); | |
542 } | |
543 else | |
544 { | |
545 return new IVolumeSlicer::InvalidSlice; | |
546 } | |
547 } | |
548 }; | |
549 | |
550 | |
551 | |
552 | |
553 /** | |
554 This class is used to manage the progressive loading of a volume that | |
555 is stored in a Dicom series. | |
556 | |
557 // TODO - Refactor using LoaderStateMachine? | |
558 // TODO: | |
559 */ | |
560 class OrthancSeriesVolumeProgressiveLoader : | |
561 public IObserver, | |
562 public IObservable, | |
563 public IVolumeSlicer | |
564 { | |
565 private: | |
566 static const unsigned int LOW_QUALITY = 0; | |
567 static const unsigned int MIDDLE_QUALITY = 1; | |
568 static const unsigned int BEST_QUALITY = 2; | |
569 | |
570 /** Helper class internal to OrthancSeriesVolumeProgressiveLoader */ | |
571 class SeriesGeometry : public boost::noncopyable | |
572 { | |
573 private: | |
574 void CheckSlice(size_t index, | |
575 const DicomInstanceParameters& reference) const | |
576 { | |
577 const DicomInstanceParameters& slice = *slices_[index]; | |
578 | |
579 if (!GeometryToolbox::IsParallel( | |
580 reference.GetGeometry().GetNormal(), | |
581 slice.GetGeometry().GetNormal())) | |
582 { | |
583 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
584 "A slice in the volume image is not parallel to the others"); | |
585 } | |
586 | |
587 if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) | |
588 { | |
589 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, | |
590 "The pixel format changes across the slices of the volume image"); | |
591 } | |
592 | |
593 if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || | |
594 reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) | |
595 { | |
596 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, | |
597 "The width/height of slices are not constant in the volume image"); | |
598 } | |
599 | |
600 if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || | |
601 !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) | |
602 { | |
603 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
604 "The pixel spacing of the slices change across the volume image"); | |
605 } | |
606 } | |
607 | |
608 | |
609 void CheckVolume() const | |
610 { | |
611 for (size_t i = 0; i < slices_.size(); i++) | |
612 { | |
613 assert(slices_[i] != NULL); | |
614 if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) | |
615 { | |
616 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
617 "This class does not support multi-frame images"); | |
618 } | |
619 } | |
620 | |
621 if (slices_.size() != 0) | |
622 { | |
623 const DicomInstanceParameters& reference = *slices_[0]; | |
624 | |
625 for (size_t i = 1; i < slices_.size(); i++) | |
626 { | |
627 CheckSlice(i, reference); | |
628 } | |
629 } | |
630 } | |
631 | |
632 | |
633 void Clear() | |
634 { | |
635 for (size_t i = 0; i < slices_.size(); i++) | |
636 { | |
637 assert(slices_[i] != NULL); | |
638 delete slices_[i]; | |
639 } | |
640 | |
641 slices_.clear(); | |
642 slicesRevision_.clear(); | |
643 } | |
644 | |
645 | |
646 void CheckSliceIndex(size_t index) const | |
647 { | |
648 if (!HasGeometry()) | |
649 { | |
650 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
651 } | |
652 else if (index >= slices_.size()) | |
653 { | |
654 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
655 } | |
656 else | |
657 { | |
658 assert(slices_.size() == GetImageGeometry().GetDepth() && | |
659 slices_.size() == slicesRevision_.size()); | |
660 } | |
661 } | |
662 | |
663 | |
664 std::auto_ptr<VolumeImageGeometry> geometry_; | |
665 std::vector<DicomInstanceParameters*> slices_; | |
666 std::vector<uint64_t> slicesRevision_; | |
667 | |
668 public: | |
669 ~SeriesGeometry() | |
670 { | |
671 Clear(); | |
672 } | |
673 | |
674 // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" | |
675 // (called with the slices created in LoadGeometry) | |
676 void ComputeGeometry(SlicesSorter& slices) | |
677 { | |
678 Clear(); | |
679 | |
680 if (!slices.Sort()) | |
681 { | |
682 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, | |
683 "Cannot sort the 3D slices of a DICOM series"); | |
684 } | |
685 | |
686 if (slices.GetSlicesCount() == 0) | |
687 { | |
688 geometry_.reset(new VolumeImageGeometry); | |
689 } | |
690 else | |
691 { | |
692 slices_.reserve(slices.GetSlicesCount()); | |
693 slicesRevision_.resize(slices.GetSlicesCount(), 0); | |
694 | |
695 for (size_t i = 0; i < slices.GetSlicesCount(); i++) | |
696 { | |
697 const DicomInstanceParameters& slice = | |
698 dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i)); | |
699 slices_.push_back(new DicomInstanceParameters(slice)); | |
700 } | |
701 | |
702 CheckVolume(); | |
703 | |
704 const double spacingZ = slices.ComputeSpacingBetweenSlices(); | |
705 LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; | |
706 | |
707 const DicomInstanceParameters& parameters = *slices_[0]; | |
708 | |
709 geometry_.reset(new VolumeImageGeometry); | |
710 geometry_->SetSize(parameters.GetImageInformation().GetWidth(), | |
711 parameters.GetImageInformation().GetHeight(), | |
712 static_cast<unsigned int>(slices.GetSlicesCount())); | |
713 geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); | |
714 geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), | |
715 parameters.GetPixelSpacingY(), spacingZ); | |
716 } | |
717 } | |
718 | |
719 bool HasGeometry() const | |
720 { | |
721 return geometry_.get() != NULL; | |
722 } | |
723 | |
724 const VolumeImageGeometry& GetImageGeometry() const | |
725 { | |
726 if (!HasGeometry()) | |
727 { | |
728 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
729 } | |
730 else | |
731 { | |
732 assert(slices_.size() == geometry_->GetDepth()); | |
733 return *geometry_; | |
734 } | |
735 } | |
736 | |
737 const DicomInstanceParameters& GetSliceParameters(size_t index) const | |
738 { | |
739 CheckSliceIndex(index); | |
740 return *slices_[index]; | |
741 } | |
742 | |
743 uint64_t GetSliceRevision(size_t index) const | |
744 { | |
745 CheckSliceIndex(index); | |
746 return slicesRevision_[index]; | |
747 } | |
748 | |
749 void IncrementSliceRevision(size_t index) | |
750 { | |
751 CheckSliceIndex(index); | |
752 slicesRevision_[index] ++; | |
753 } | |
754 }; | |
755 | |
756 | |
757 class ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice | |
758 { | |
759 private: | |
760 const OrthancSeriesVolumeProgressiveLoader& that_; | |
761 | |
762 protected: | |
763 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
764 unsigned int sliceIndex) const | |
765 { | |
766 if (projection == VolumeProjection_Axial) | |
767 { | |
768 return that_.seriesGeometry_.GetSliceRevision(sliceIndex); | |
769 } | |
770 else | |
771 { | |
772 // For coronal and sagittal projections, we take the global | |
773 // revision of the volume because even if a single slice changes, | |
774 // this means the projection will yield a different result --> | |
775 // we must increase the revision as soon as any slice changes | |
776 return that_.volume_->GetRevision(); | |
777 } | |
778 } | |
779 | |
780 public: | |
781 ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that, | |
782 const CoordinateSystem3D& plane) : | |
783 DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane), | |
784 that_(that) | |
785 { | |
786 if (that_.strategy_.get() != NULL && | |
787 IsValid() && | |
788 GetProjection() == VolumeProjection_Axial) | |
789 { | |
790 that_.strategy_->SetCurrent(GetSliceIndex()); | |
791 } | |
792 } | |
793 }; | |
794 | |
795 | |
796 | |
797 static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) | |
798 { | |
799 return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); | |
800 } | |
801 | |
802 | |
803 void ScheduleNextSliceDownload() | |
804 { | |
805 assert(strategy_.get() != NULL); | |
806 | |
807 unsigned int sliceIndex, quality; | |
808 | |
809 if (strategy_->GetNext(sliceIndex, quality)) | |
810 { | |
811 assert(quality <= BEST_QUALITY); | |
812 | |
813 const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); | |
814 | |
815 const std::string& instance = slice.GetOrthancInstanceIdentifier(); | |
816 if (instance.empty()) | |
817 { | |
818 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
819 } | |
820 | |
821 std::auto_ptr<OracleCommandWithPayload> command; | |
822 | |
823 if (quality == BEST_QUALITY) | |
824 { | |
825 std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand); | |
826 tmp->SetHttpHeader("Accept-Encoding", "gzip"); | |
827 tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam))); | |
828 tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat()); | |
829 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); | |
830 command.reset(tmp.release()); | |
831 } | |
832 else | |
833 { | |
834 std::auto_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand); | |
835 tmp->SetHttpHeader("Accept-Encoding", "gzip"); | |
836 tmp->SetInstance(instance); | |
837 tmp->SetQuality((quality == 0 ? 50 : 90)); | |
838 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat()); | |
839 command.reset(tmp.release()); | |
840 } | |
841 | |
842 command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex)); | |
843 oracle_.Schedule(*this, command.release()); | |
844 } | |
845 } | |
846 | |
847 /** | |
848 This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags" | |
849 */ | |
850 void LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message) | |
851 { | |
852 Json::Value body; | |
853 message.ParseJsonBody(body); | |
854 | |
855 if (body.type() != Json::objectValue) | |
856 { | |
857 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); | |
858 } | |
859 | |
860 { | |
861 Json::Value::Members instances = body.getMemberNames(); | |
862 | |
863 SlicesSorter slices; | |
864 | |
865 for (size_t i = 0; i < instances.size(); i++) | |
866 { | |
867 Orthanc::DicomMap dicom; | |
868 dicom.FromDicomAsJson(body[instances[i]]); | |
869 | |
870 std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom)); | |
871 instance->SetOrthancInstanceIdentifier(instances[i]); | |
872 | |
873 // the 3D plane corresponding to the slice | |
874 CoordinateSystem3D geometry = instance->GetGeometry(); | |
875 slices.AddSlice(geometry, instance.release()); | |
876 } | |
877 | |
878 seriesGeometry_.ComputeGeometry(slices); | |
879 } | |
880 | |
881 size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); | |
882 | |
883 if (slicesCount == 0) | |
884 { | |
885 volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); | |
886 } | |
887 else | |
888 { | |
889 const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); | |
890 | |
891 volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); | |
892 volume_->SetDicomParameters(parameters); | |
893 volume_->GetPixelData().Clear(); | |
894 | |
895 strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY)); | |
896 | |
897 assert(simultaneousDownloads_ != 0); | |
898 for (unsigned int i = 0; i < simultaneousDownloads_; i++) | |
899 { | |
900 ScheduleNextSliceDownload(); | |
901 } | |
902 } | |
903 | |
904 slicesQuality_.resize(slicesCount, 0); | |
905 | |
906 BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); | |
907 } | |
908 | |
909 | |
910 void SetSliceContent(unsigned int sliceIndex, | |
911 const Orthanc::ImageAccessor& image, | |
912 unsigned int quality) | |
913 { | |
914 assert(sliceIndex < slicesQuality_.size() && | |
915 slicesQuality_.size() == volume_->GetPixelData().GetDepth()); | |
916 | |
917 if (quality >= slicesQuality_[sliceIndex]) | |
918 { | |
919 { | |
920 ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex); | |
921 Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); | |
922 } | |
923 | |
924 volume_->IncrementRevision(); | |
925 seriesGeometry_.IncrementSliceRevision(sliceIndex); | |
926 slicesQuality_[sliceIndex] = quality; | |
927 | |
928 BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); | |
929 } | |
930 | |
931 ScheduleNextSliceDownload(); | |
932 } | |
933 | |
934 | |
935 void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) | |
936 { | |
937 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); | |
938 } | |
939 | |
940 | |
941 void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) | |
942 { | |
943 unsigned int quality; | |
944 | |
945 switch (message.GetOrigin().GetQuality()) | |
946 { | |
947 case 50: | |
948 quality = LOW_QUALITY; | |
949 break; | |
950 | |
951 case 90: | |
952 quality = MIDDLE_QUALITY; | |
953 break; | |
954 | |
955 default: | |
956 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
957 } | |
958 | |
959 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); | |
960 } | |
961 | |
962 | |
963 IOracle& oracle_; | |
964 bool active_; | |
965 unsigned int simultaneousDownloads_; | |
966 SeriesGeometry seriesGeometry_; | |
967 | |
968 boost::shared_ptr<DicomVolumeImage> volume_; | |
969 std::auto_ptr<IFetchingItemsSorter::IFactory> sorter_; | |
970 std::auto_ptr<IFetchingStrategy> strategy_; | |
971 std::vector<unsigned int> slicesQuality_; | |
972 | |
973 | |
974 public: | |
975 OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume, | |
976 IOracle& oracle, | |
977 IObservable& oracleObservable) : | |
978 IObserver(oracleObservable.GetBroker()), | |
979 IObservable(oracleObservable.GetBroker()), | |
980 oracle_(oracle), | |
981 active_(false), | |
982 simultaneousDownloads_(4), | |
983 volume_(volume), | |
984 sorter_(new BasicFetchingItemsSorter::Factory) | |
985 { | |
986 oracleObservable.RegisterObserverCallback( | |
987 new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage> | |
988 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); | |
989 | |
990 oracleObservable.RegisterObserverCallback( | |
991 new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage> | |
992 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent)); | |
993 | |
994 oracleObservable.RegisterObserverCallback( | |
995 new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage> | |
996 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent)); | |
997 } | |
998 | |
999 void SetSimultaneousDownloads(unsigned int count) | |
1000 { | |
1001 if (active_) | |
1002 { | |
1003 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1004 } | |
1005 else if (count == 0) | |
1006 { | |
1007 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
1008 } | |
1009 else | |
1010 { | |
1011 simultaneousDownloads_ = count; | |
1012 } | |
1013 } | |
1014 | |
1015 void LoadSeries(const std::string& seriesId) | |
1016 { | |
1017 if (active_) | |
1018 { | |
1019 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1020 } | |
1021 else | |
1022 { | |
1023 active_ = true; | |
1024 | |
1025 std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand); | |
1026 command->SetUri("/series/" + seriesId + "/instances-tags"); | |
1027 | |
1028 oracle_.Schedule(*this, command.release()); | |
1029 } | |
1030 } | |
1031 | |
1032 /** | |
1033 When a slice is requested, the strategy algorithm (that defines the | |
1034 sequence of resources to be loaded from the server) is modified to | |
1035 take into account this request (this is done in the ExtractedSlice ctor) | |
1036 */ | |
1037 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) | |
1038 { | |
1039 if (volume_->HasGeometry()) | |
1040 { | |
1041 return new ExtractedSlice(*this, cuttingPlane); | |
1042 } | |
1043 else | |
1044 { | |
1045 return new IVolumeSlicer::InvalidSlice; | |
1046 } | |
1047 } | |
1048 }; | |
1049 | |
1050 | |
1051 /** | |
1052 This class is supplied with Oracle commands and will schedule up to | 66 This class is supplied with Oracle commands and will schedule up to |
1053 simultaneousDownloads_ of them at the same time, then will schedule the | 67 simultaneousDownloads_ of them at the same time, then will schedule the |
1054 rest once slots become available. It is used, a.o., by the | 68 rest once slots become available. It is used, a.o., by the |
1055 OrtancMultiframeVolumeLoader class. | 69 OrtancMultiframeVolumeLoader class. |
1056 */ | 70 */ |