Mercurial > hg > orthanc-stone
comparison Samples/Sdl/Loader.cpp @ 788:e76c4eef1054
Merge from default
author | Benjamin Golinvaux <bgo@osimis.io> |
---|---|
date | Mon, 27 May 2019 16:01:47 +0200 |
parents | 1a28fce57ff3 cd13a062c9bd |
children | c83a45f864b2 |
comparison
equal
deleted
inserted
replaced
787:1a28fce57ff3 | 788:e76c4eef1054 |
---|---|
37 #include "../../Framework/StoneInitialization.h" | 37 #include "../../Framework/StoneInitialization.h" |
38 #include "../../Framework/Toolbox/GeometryToolbox.h" | 38 #include "../../Framework/Toolbox/GeometryToolbox.h" |
39 #include "../../Framework/Toolbox/SlicesSorter.h" | 39 #include "../../Framework/Toolbox/SlicesSorter.h" |
40 #include "../../Framework/Volumes/ImageBuffer3D.h" | 40 #include "../../Framework/Volumes/ImageBuffer3D.h" |
41 #include "../../Framework/Volumes/VolumeImageGeometry.h" | 41 #include "../../Framework/Volumes/VolumeImageGeometry.h" |
42 #include "../../Framework/Volumes/VolumeReslicer.h" | |
42 | 43 |
43 // From Orthanc framework | 44 // From Orthanc framework |
44 #include <Core/DicomFormat/DicomArray.h> | 45 #include <Core/DicomFormat/DicomArray.h> |
45 #include <Core/Images/Image.h> | 46 #include <Core/Images/Image.h> |
46 #include <Core/Images/ImageProcessing.h> | 47 #include <Core/Images/ImageProcessing.h> |
55 #include <EmbeddedResources.h> | 56 #include <EmbeddedResources.h> |
56 | 57 |
57 | 58 |
58 namespace OrthancStone | 59 namespace OrthancStone |
59 { | 60 { |
61 // Application-configurable, can be shared between 3D/2D | |
62 class ILayerStyleConfigurator | |
63 { | |
64 public: | |
65 virtual ~ILayerStyleConfigurator() | |
66 { | |
67 } | |
68 | |
69 virtual uint64_t GetRevision() const = 0; | |
70 | |
71 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const = 0; | |
72 | |
73 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
74 const DicomInstanceParameters& parameters) const = 0; | |
75 | |
76 virtual void ApplyStyle(ISceneLayer& layer) const = 0; | |
77 }; | |
78 | |
79 | |
80 | |
81 class LookupTableStyleConfigurator : public ILayerStyleConfigurator | |
82 { | |
83 private: | |
84 uint64_t revision_; | |
85 bool hasLut_; | |
86 std::string lut_; | |
87 bool hasRange_; | |
88 float minValue_; | |
89 float maxValue_; | |
90 | |
91 public: | |
92 LookupTableStyleConfigurator() : | |
93 revision_(0), | |
94 hasLut_(false), | |
95 hasRange_(false) | |
96 { | |
97 } | |
98 | |
99 void SetLookupTable(Orthanc::EmbeddedResources::FileResourceId resource) | |
100 { | |
101 hasLut_ = true; | |
102 Orthanc::EmbeddedResources::GetFileResource(lut_, resource); | |
103 } | |
104 | |
105 void SetLookupTable(const std::string& lut) | |
106 { | |
107 hasLut_ = true; | |
108 lut_ = lut; | |
109 } | |
110 | |
111 void SetRange(float minValue, | |
112 float maxValue) | |
113 { | |
114 if (minValue > maxValue) | |
115 { | |
116 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
117 } | |
118 else | |
119 { | |
120 hasRange_ = true; | |
121 minValue_ = minValue; | |
122 maxValue_ = maxValue; | |
123 } | |
124 } | |
125 | |
126 virtual uint64_t GetRevision() const | |
127 { | |
128 return revision_; | |
129 } | |
130 | |
131 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const | |
132 { | |
133 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
134 } | |
135 | |
136 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
137 const DicomInstanceParameters& parameters) const | |
138 { | |
139 return parameters.CreateLookupTableTexture(frame); | |
140 } | |
141 | |
142 virtual void ApplyStyle(ISceneLayer& layer) const | |
143 { | |
144 LookupTableTextureSceneLayer& l = dynamic_cast<LookupTableTextureSceneLayer&>(layer); | |
145 | |
146 if (hasLut_) | |
147 { | |
148 l.SetLookupTable(lut_); | |
149 } | |
150 | |
151 if (hasRange_) | |
152 { | |
153 l.SetRange(minValue_, maxValue_); | |
154 } | |
155 else | |
156 { | |
157 l.FitRange(); | |
158 } | |
159 } | |
160 }; | |
161 | |
162 | |
163 class GrayscaleStyleConfigurator : public ILayerStyleConfigurator | |
164 { | |
165 private: | |
166 uint64_t revision_; | |
167 | |
168 public: | |
169 GrayscaleStyleConfigurator() : | |
170 revision_(0) | |
171 { | |
172 } | |
173 | |
174 virtual uint64_t GetRevision() const | |
175 { | |
176 return revision_; | |
177 } | |
178 | |
179 virtual TextureBaseSceneLayer* CreateTextureFromImage(const Orthanc::ImageAccessor& image) const | |
180 { | |
181 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
182 } | |
183 | |
184 virtual TextureBaseSceneLayer* CreateTextureFromDicom(const Orthanc::ImageAccessor& frame, | |
185 const DicomInstanceParameters& parameters) const | |
186 { | |
187 return parameters.CreateTexture(frame); | |
188 } | |
189 | |
190 virtual void ApplyStyle(ISceneLayer& layer) const | |
191 { | |
192 } | |
193 }; | |
194 | |
195 | |
60 class IVolumeSlicer : public boost::noncopyable | 196 class IVolumeSlicer : public boost::noncopyable |
61 { | 197 { |
62 public: | 198 public: |
63 class ExtractedSlice : public boost::noncopyable | 199 class IExtractedSlice : public boost::noncopyable |
64 { | 200 { |
65 public: | 201 public: |
66 virtual ~ExtractedSlice() | 202 virtual ~IExtractedSlice() |
67 { | 203 { |
68 } | 204 } |
69 | 205 |
70 virtual bool IsValid() = 0; | 206 virtual bool IsValid() = 0; |
71 | 207 |
72 // Must be a cheap call | 208 // Must be a cheap call |
73 virtual uint64_t GetRevision() = 0; | 209 virtual uint64_t GetRevision() = 0; |
74 | 210 |
75 // This call can take some time | 211 // This call can take some time |
76 virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) = 0; | 212 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, // possibly absent |
213 const CoordinateSystem3D& cuttingPlane) = 0; | |
77 }; | 214 }; |
78 | 215 |
216 | |
217 class InvalidSlice : public IExtractedSlice | |
218 { | |
219 public: | |
220 virtual bool IsValid() | |
221 { | |
222 return false; | |
223 } | |
224 | |
225 virtual uint64_t GetRevision() | |
226 { | |
227 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
228 } | |
229 | |
230 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, | |
231 const CoordinateSystem3D& cuttingPlane) | |
232 { | |
233 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
234 } | |
235 }; | |
236 | |
237 | |
79 virtual ~IVolumeSlicer() | 238 virtual ~IVolumeSlicer() |
80 { | 239 { |
81 } | 240 } |
82 | 241 |
83 virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const = 0; | 242 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) = 0; |
84 }; | 243 }; |
85 | 244 |
86 | 245 |
87 class IVolumeImageSlicer : public IVolumeSlicer | 246 |
247 // This class combines a 3D image buffer, a 3D volume geometry and | |
248 // information about the DICOM parameters of the series. | |
249 class DicomVolumeImage : public boost::noncopyable | |
88 { | 250 { |
89 public: | 251 public: |
90 virtual bool HasGeometry() const = 0; | 252 ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, GeometryReadyMessage, DicomVolumeImage); |
91 | 253 ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, ContentUpdatedMessage, DicomVolumeImage); |
92 virtual const VolumeImageGeometry& GetGeometry() const = 0; | 254 |
255 private: | |
256 uint64_t revision_; | |
257 std::auto_ptr<VolumeImageGeometry> geometry_; | |
258 std::auto_ptr<ImageBuffer3D> image_; | |
259 std::auto_ptr<DicomInstanceParameters> parameters_; | |
260 | |
261 void CheckHasGeometry() const | |
262 { | |
263 if (!HasGeometry()) | |
264 { | |
265 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
266 } | |
267 } | |
268 | |
269 public: | |
270 DicomVolumeImage() : | |
271 revision_(0) | |
272 { | |
273 } | |
274 | |
275 void IncrementRevision() | |
276 { | |
277 revision_ ++; | |
278 } | |
279 | |
280 void Initialize(const VolumeImageGeometry& geometry, | |
281 Orthanc::PixelFormat format) | |
282 { | |
283 geometry_.reset(new VolumeImageGeometry(geometry)); | |
284 image_.reset(new ImageBuffer3D(format, geometry_->GetWidth(), geometry_->GetHeight(), | |
285 geometry_->GetDepth(), false /* don't compute range */)); | |
286 | |
287 revision_ ++; | |
288 } | |
289 | |
290 void SetDicomParameters(const DicomInstanceParameters& parameters) | |
291 { | |
292 parameters_.reset(parameters.Clone()); | |
293 revision_ ++; | |
294 } | |
295 | |
296 uint64_t GetRevision() const | |
297 { | |
298 return revision_; | |
299 } | |
300 | |
301 bool HasGeometry() const | |
302 { | |
303 return (geometry_.get() != NULL && | |
304 image_.get() != NULL); | |
305 } | |
306 | |
307 ImageBuffer3D& GetPixelData() | |
308 { | |
309 CheckHasGeometry(); | |
310 return *image_; | |
311 } | |
312 | |
313 const ImageBuffer3D& GetPixelData() const | |
314 { | |
315 CheckHasGeometry(); | |
316 return *image_; | |
317 } | |
318 | |
319 const VolumeImageGeometry& GetGeometry() const | |
320 { | |
321 CheckHasGeometry(); | |
322 return *geometry_; | |
323 } | |
324 | |
325 bool HasDicomParameters() const | |
326 { | |
327 return parameters_.get() != NULL; | |
328 } | |
329 | |
330 const DicomInstanceParameters& GetDicomParameters() const | |
331 { | |
332 if (HasDicomParameters()) | |
333 { | |
334 return *parameters_; | |
335 } | |
336 else | |
337 { | |
338 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
339 } | |
340 } | |
93 }; | 341 }; |
94 | 342 |
95 | 343 |
96 class InvalidExtractedSlice : public IVolumeSlicer::ExtractedSlice | 344 |
97 { | 345 class DicomVolumeImageOrthogonalSlice : public IVolumeSlicer::IExtractedSlice |
346 { | |
347 private: | |
348 const DicomVolumeImage& volume_; | |
349 bool valid_; | |
350 VolumeProjection projection_; | |
351 unsigned int sliceIndex_; | |
352 | |
353 void CheckValid() const | |
354 { | |
355 if (!valid_) | |
356 { | |
357 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
358 } | |
359 } | |
360 | |
361 protected: | |
362 // Can be overloaded in subclasses | |
363 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
364 unsigned int sliceIndex) const | |
365 { | |
366 return volume_.GetRevision(); | |
367 } | |
368 | |
98 public: | 369 public: |
99 virtual bool IsValid() | 370 DicomVolumeImageOrthogonalSlice(const DicomVolumeImage& volume, |
100 { | |
101 return false; | |
102 } | |
103 | |
104 virtual uint64_t GetRevision() | |
105 { | |
106 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
107 } | |
108 | |
109 virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) | |
110 { | |
111 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
112 } | |
113 }; | |
114 | |
115 | |
116 class DicomVolumeImageOrthogonalSlice : public IVolumeSlicer::ExtractedSlice | |
117 { | |
118 private: | |
119 const ImageBuffer3D& image_; | |
120 const VolumeImageGeometry& geometry_; | |
121 bool valid_; | |
122 VolumeProjection projection_; | |
123 unsigned int sliceIndex_; | |
124 | |
125 void CheckValid() const | |
126 { | |
127 if (!valid_) | |
128 { | |
129 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
130 } | |
131 } | |
132 | |
133 protected: | |
134 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
135 unsigned int sliceIndex) const = 0; | |
136 | |
137 virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection, | |
138 unsigned int sliceIndex) const = 0; | |
139 | |
140 public: | |
141 DicomVolumeImageOrthogonalSlice(const ImageBuffer3D& image, | |
142 const VolumeImageGeometry& geometry, | |
143 const CoordinateSystem3D& cuttingPlane) : | 371 const CoordinateSystem3D& cuttingPlane) : |
144 image_(image), | 372 volume_(volume) |
145 geometry_(geometry) | 373 { |
146 { | 374 valid_ = (volume_.HasDicomParameters() && |
147 valid_ = geometry_.DetectSlice(projection_, sliceIndex_, cuttingPlane); | 375 volume_.GetGeometry().DetectSlice(projection_, sliceIndex_, cuttingPlane)); |
148 } | 376 } |
149 | 377 |
150 VolumeProjection GetProjection() const | 378 VolumeProjection GetProjection() const |
151 { | 379 { |
152 CheckValid(); | 380 CheckValid(); |
168 { | 396 { |
169 CheckValid(); | 397 CheckValid(); |
170 return GetRevisionInternal(projection_, sliceIndex_); | 398 return GetRevisionInternal(projection_, sliceIndex_); |
171 } | 399 } |
172 | 400 |
173 virtual ISceneLayer* CreateSceneLayer(const CoordinateSystem3D& cuttingPlane) | 401 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, |
402 const CoordinateSystem3D& cuttingPlane) | |
174 { | 403 { |
175 CheckValid(); | 404 CheckValid(); |
405 | |
406 if (configurator == NULL) | |
407 { | |
408 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer, | |
409 "A style configurator is mandatory for textures"); | |
410 } | |
176 | 411 |
177 std::auto_ptr<TextureBaseSceneLayer> texture; | 412 std::auto_ptr<TextureBaseSceneLayer> texture; |
178 | 413 |
179 { | 414 { |
180 const DicomInstanceParameters& parameters = GetDicomParameters(projection_, sliceIndex_); | 415 const DicomInstanceParameters& parameters = volume_.GetDicomParameters(); |
181 ImageBuffer3D::SliceReader reader(image_, projection_, sliceIndex_); | 416 ImageBuffer3D::SliceReader reader(volume_.GetPixelData(), projection_, sliceIndex_); |
182 | 417 texture.reset(dynamic_cast<TextureBaseSceneLayer*> |
183 static unsigned int i = 1; | 418 (configurator->CreateTextureFromDicom(reader.GetAccessor(), parameters))); |
184 | 419 } |
185 if (i % 2) | 420 |
186 { | 421 const CoordinateSystem3D& system = volume_.GetGeometry().GetProjectionGeometry(projection_); |
187 texture.reset(parameters.CreateTexture(reader.GetAccessor())); | |
188 } | |
189 else | |
190 { | |
191 std::string lut; | |
192 Orthanc::EmbeddedResources::GetFileResource(lut, Orthanc::EmbeddedResources::COLORMAP_HOT); | |
193 | |
194 std::auto_ptr<LookupTableTextureSceneLayer> tmp(parameters.CreateLookupTableTexture(reader.GetAccessor())); | |
195 tmp->FitRange(); | |
196 tmp->SetLookupTable(lut); | |
197 texture.reset(tmp.release()); | |
198 } | |
199 | |
200 i++; | |
201 } | |
202 | |
203 const CoordinateSystem3D& system = geometry_.GetProjectionGeometry(projection_); | |
204 | 422 |
205 double x0, y0, x1, y1; | 423 double x0, y0, x1, y1; |
206 cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); | 424 cuttingPlane.ProjectPoint(x0, y0, system.GetOrigin()); |
207 cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); | 425 cuttingPlane.ProjectPoint(x1, y1, system.GetOrigin() + system.GetAxisX()); |
208 texture->SetOrigin(x0, y0); | 426 texture->SetOrigin(x0, y0); |
213 !LinearAlgebra::IsCloseToZero(dy)) | 431 !LinearAlgebra::IsCloseToZero(dy)) |
214 { | 432 { |
215 texture->SetAngle(atan2(dy, dx)); | 433 texture->SetAngle(atan2(dy, dx)); |
216 } | 434 } |
217 | 435 |
218 Vector tmp = geometry_.GetVoxelDimensions(projection_); | 436 Vector tmp = volume_.GetGeometry().GetVoxelDimensions(projection_); |
219 texture->SetPixelSpacing(tmp[0], tmp[1]); | 437 texture->SetPixelSpacing(tmp[0], tmp[1]); |
220 | 438 |
221 return texture.release(); | 439 return texture.release(); |
222 | 440 |
223 #if 0 | 441 #if 0 |
242 return toto.release(); | 460 return toto.release(); |
243 #endif | 461 #endif |
244 } | 462 } |
245 }; | 463 }; |
246 | 464 |
247 | 465 |
248 // This class combines a 3D image buffer, a 3D volume geometry and | |
249 // information about the DICOM parameters of each slice. | |
250 class DicomSeriesVolumeImage : public boost::noncopyable | |
251 { | |
252 public: | |
253 class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice | |
254 { | |
255 private: | |
256 const DicomSeriesVolumeImage& that_; | |
257 | |
258 protected: | |
259 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
260 unsigned int sliceIndex) const | |
261 { | |
262 if (projection == VolumeProjection_Axial) | |
263 { | |
264 return that_.GetSliceRevision(sliceIndex); | |
265 } | |
266 else | |
267 { | |
268 // For coronal and sagittal projections, we take the global | |
269 // revision of the volume | |
270 return that_.GetRevision(); | |
271 } | |
272 } | |
273 | |
274 virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection, | |
275 unsigned int sliceIndex) const | |
276 { | |
277 return that_.GetSliceParameters(projection == VolumeProjection_Axial ? sliceIndex : 0); | |
278 } | |
279 | |
280 public: | |
281 ExtractedOrthogonalSlice(const DicomSeriesVolumeImage& that, | |
282 const CoordinateSystem3D& plane) : | |
283 DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane), | |
284 that_(that) | |
285 { | |
286 } | |
287 }; | |
288 | |
289 | |
290 private: | |
291 std::auto_ptr<ImageBuffer3D> image_; | |
292 std::auto_ptr<VolumeImageGeometry> geometry_; | |
293 std::vector<DicomInstanceParameters*> slices_; | |
294 uint64_t revision_; | |
295 std::vector<uint64_t> slicesRevision_; | |
296 std::vector<unsigned int> slicesQuality_; | |
297 | |
298 void CheckSlice(size_t index, | |
299 const DicomInstanceParameters& reference) const | |
300 { | |
301 const DicomInstanceParameters& slice = *slices_[index]; | |
302 | |
303 if (!GeometryToolbox::IsParallel( | |
304 reference.GetGeometry().GetNormal(), | |
305 slice.GetGeometry().GetNormal())) | |
306 { | |
307 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
308 "A slice in the volume image is not parallel to the others"); | |
309 } | |
310 | |
311 if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) | |
312 { | |
313 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, | |
314 "The pixel format changes across the slices of the volume image"); | |
315 } | |
316 | |
317 if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || | |
318 reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) | |
319 { | |
320 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, | |
321 "The width/height of slices are not constant in the volume image"); | |
322 } | |
323 | |
324 if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || | |
325 !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) | |
326 { | |
327 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
328 "The pixel spacing of the slices change across the volume image"); | |
329 } | |
330 } | |
331 | |
332 | |
333 void CheckVolume() const | |
334 { | |
335 for (size_t i = 0; i < slices_.size(); i++) | |
336 { | |
337 assert(slices_[i] != NULL); | |
338 if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) | |
339 { | |
340 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
341 "This class does not support multi-frame images"); | |
342 } | |
343 } | |
344 | |
345 if (slices_.size() != 0) | |
346 { | |
347 const DicomInstanceParameters& reference = *slices_[0]; | |
348 | |
349 for (size_t i = 1; i < slices_.size(); i++) | |
350 { | |
351 CheckSlice(i, reference); | |
352 } | |
353 } | |
354 } | |
355 | |
356 | |
357 void Clear() | |
358 { | |
359 image_.reset(); | |
360 geometry_.reset(); | |
361 | |
362 for (size_t i = 0; i < slices_.size(); i++) | |
363 { | |
364 assert(slices_[i] != NULL); | |
365 delete slices_[i]; | |
366 } | |
367 | |
368 slices_.clear(); | |
369 slicesRevision_.clear(); | |
370 slicesQuality_.clear(); | |
371 } | |
372 | |
373 | |
374 void CheckSliceIndex(size_t index) const | |
375 { | |
376 assert(slices_.size() == image_->GetDepth() && | |
377 slices_.size() == slicesRevision_.size()); | |
378 | |
379 if (!HasGeometry()) | |
380 { | |
381 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
382 } | |
383 else if (index >= slices_.size()) | |
384 { | |
385 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
386 } | |
387 } | |
388 | |
389 | |
390 public: | |
391 DicomSeriesVolumeImage() : | |
392 revision_(0) | |
393 { | |
394 } | |
395 | |
396 ~DicomSeriesVolumeImage() | |
397 { | |
398 Clear(); | |
399 } | |
400 | |
401 // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" | |
402 void SetGeometry(SlicesSorter& slices) | |
403 { | |
404 Clear(); | |
405 | |
406 if (!slices.Sort()) | |
407 { | |
408 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, | |
409 "Cannot sort the 3D slices of a DICOM series"); | |
410 } | |
411 | |
412 geometry_.reset(new VolumeImageGeometry); | |
413 | |
414 if (slices.GetSlicesCount() == 0) | |
415 { | |
416 // Empty volume | |
417 image_.reset(new ImageBuffer3D(Orthanc::PixelFormat_Grayscale8, 0, 0, 0, | |
418 false /* don't compute range */)); | |
419 } | |
420 else | |
421 { | |
422 slices_.reserve(slices.GetSlicesCount()); | |
423 slicesRevision_.resize(slices.GetSlicesCount(), 0); | |
424 slicesQuality_.resize(slices.GetSlicesCount(), 0); | |
425 | |
426 for (size_t i = 0; i < slices.GetSlicesCount(); i++) | |
427 { | |
428 const DicomInstanceParameters& slice = | |
429 dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i)); | |
430 slices_.push_back(new DicomInstanceParameters(slice)); | |
431 } | |
432 | |
433 CheckVolume(); | |
434 | |
435 const double spacingZ = slices.ComputeSpacingBetweenSlices(); | |
436 LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; | |
437 | |
438 const DicomInstanceParameters& parameters = *slices_[0]; | |
439 | |
440 image_.reset(new ImageBuffer3D(parameters.GetExpectedPixelFormat(), | |
441 parameters.GetImageInformation().GetWidth(), | |
442 parameters.GetImageInformation().GetHeight(), | |
443 static_cast<unsigned int>(slices.GetSlicesCount()), | |
444 false /* don't compute range */)); | |
445 | |
446 geometry_->SetSize(image_->GetWidth(), image_->GetHeight(), image_->GetDepth()); | |
447 geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); | |
448 geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), | |
449 parameters.GetPixelSpacingY(), spacingZ); | |
450 } | |
451 | |
452 image_->Clear(); | |
453 | |
454 revision_++; | |
455 } | |
456 | |
457 uint64_t GetRevision() const | |
458 { | |
459 return revision_; | |
460 } | |
461 | |
462 bool HasGeometry() const | |
463 { | |
464 return (image_.get() != NULL && | |
465 geometry_.get() != NULL); | |
466 } | |
467 | |
468 const ImageBuffer3D& GetImage() const | |
469 { | |
470 if (!HasGeometry()) | |
471 { | |
472 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
473 } | |
474 else | |
475 { | |
476 return *image_; | |
477 } | |
478 } | |
479 | |
480 const VolumeImageGeometry& GetGeometry() const | |
481 { | |
482 if (!HasGeometry()) | |
483 { | |
484 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
485 } | |
486 else | |
487 { | |
488 return *geometry_; | |
489 } | |
490 } | |
491 | |
492 size_t GetSlicesCount() const | |
493 { | |
494 if (!HasGeometry()) | |
495 { | |
496 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
497 } | |
498 else | |
499 { | |
500 return slices_.size(); | |
501 } | |
502 } | |
503 | |
504 const DicomInstanceParameters& GetSliceParameters(size_t index) const | |
505 { | |
506 CheckSliceIndex(index); | |
507 return *slices_[index]; | |
508 } | |
509 | |
510 uint64_t GetSliceRevision(size_t index) const | |
511 { | |
512 CheckSliceIndex(index); | |
513 return slicesRevision_[index]; | |
514 } | |
515 | |
516 void SetSliceContent(size_t index, | |
517 const Orthanc::ImageAccessor& image, | |
518 unsigned int quality) | |
519 { | |
520 CheckSliceIndex(index); | |
521 | |
522 // If a better image quality is already available, don't update the content | |
523 if (quality >= slicesQuality_[index]) | |
524 { | |
525 { | |
526 ImageBuffer3D::SliceWriter writer | |
527 (*image_, VolumeProjection_Axial, static_cast<unsigned int>(index)); | |
528 Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); | |
529 } | |
530 | |
531 revision_ ++; | |
532 slicesRevision_[index] += 1; | |
533 } | |
534 } | |
535 }; | |
536 | 466 |
537 | 467 |
538 | 468 |
539 class OrthancSeriesVolumeProgressiveLoader : | 469 class OrthancSeriesVolumeProgressiveLoader : |
540 public IObserver | 470 public IObserver, |
471 public IObservable, | |
472 public IVolumeSlicer | |
541 { | 473 { |
542 private: | 474 private: |
543 static const unsigned int LOW_QUALITY = 0; | 475 static const unsigned int LOW_QUALITY = 0; |
544 static const unsigned int MIDDLE_QUALITY = 1; | 476 static const unsigned int MIDDLE_QUALITY = 1; |
545 static const unsigned int BEST_QUALITY = 2; | 477 static const unsigned int BEST_QUALITY = 2; |
546 | 478 |
547 | 479 |
480 // Helper class internal to OrthancSeriesVolumeProgressiveLoader | |
481 class SeriesGeometry : public boost::noncopyable | |
482 { | |
483 private: | |
484 void CheckSlice(size_t index, | |
485 const DicomInstanceParameters& reference) const | |
486 { | |
487 const DicomInstanceParameters& slice = *slices_[index]; | |
488 | |
489 if (!GeometryToolbox::IsParallel( | |
490 reference.GetGeometry().GetNormal(), | |
491 slice.GetGeometry().GetNormal())) | |
492 { | |
493 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
494 "A slice in the volume image is not parallel to the others"); | |
495 } | |
496 | |
497 if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat()) | |
498 { | |
499 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat, | |
500 "The pixel format changes across the slices of the volume image"); | |
501 } | |
502 | |
503 if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() || | |
504 reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight()) | |
505 { | |
506 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize, | |
507 "The width/height of slices are not constant in the volume image"); | |
508 } | |
509 | |
510 if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) || | |
511 !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY())) | |
512 { | |
513 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
514 "The pixel spacing of the slices change across the volume image"); | |
515 } | |
516 } | |
517 | |
518 | |
519 void CheckVolume() const | |
520 { | |
521 for (size_t i = 0; i < slices_.size(); i++) | |
522 { | |
523 assert(slices_[i] != NULL); | |
524 if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1) | |
525 { | |
526 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry, | |
527 "This class does not support multi-frame images"); | |
528 } | |
529 } | |
530 | |
531 if (slices_.size() != 0) | |
532 { | |
533 const DicomInstanceParameters& reference = *slices_[0]; | |
534 | |
535 for (size_t i = 1; i < slices_.size(); i++) | |
536 { | |
537 CheckSlice(i, reference); | |
538 } | |
539 } | |
540 } | |
541 | |
542 | |
543 void Clear() | |
544 { | |
545 for (size_t i = 0; i < slices_.size(); i++) | |
546 { | |
547 assert(slices_[i] != NULL); | |
548 delete slices_[i]; | |
549 } | |
550 | |
551 slices_.clear(); | |
552 slicesRevision_.clear(); | |
553 } | |
554 | |
555 | |
556 void CheckSliceIndex(size_t index) const | |
557 { | |
558 if (!HasGeometry()) | |
559 { | |
560 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
561 } | |
562 else if (index >= slices_.size()) | |
563 { | |
564 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
565 } | |
566 else | |
567 { | |
568 assert(slices_.size() == GetImageGeometry().GetDepth() && | |
569 slices_.size() == slicesRevision_.size()); | |
570 } | |
571 } | |
572 | |
573 | |
574 std::auto_ptr<VolumeImageGeometry> geometry_; | |
575 std::vector<DicomInstanceParameters*> slices_; | |
576 std::vector<uint64_t> slicesRevision_; | |
577 | |
578 public: | |
579 ~SeriesGeometry() | |
580 { | |
581 Clear(); | |
582 } | |
583 | |
584 // WARNING: The payload of "slices" must be of class "DicomInstanceParameters" | |
585 void ComputeGeometry(SlicesSorter& slices) | |
586 { | |
587 Clear(); | |
588 | |
589 if (!slices.Sort()) | |
590 { | |
591 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, | |
592 "Cannot sort the 3D slices of a DICOM series"); | |
593 } | |
594 | |
595 if (slices.GetSlicesCount() == 0) | |
596 { | |
597 geometry_.reset(new VolumeImageGeometry); | |
598 } | |
599 else | |
600 { | |
601 slices_.reserve(slices.GetSlicesCount()); | |
602 slicesRevision_.resize(slices.GetSlicesCount(), 0); | |
603 | |
604 for (size_t i = 0; i < slices.GetSlicesCount(); i++) | |
605 { | |
606 const DicomInstanceParameters& slice = | |
607 dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i)); | |
608 slices_.push_back(new DicomInstanceParameters(slice)); | |
609 } | |
610 | |
611 CheckVolume(); | |
612 | |
613 const double spacingZ = slices.ComputeSpacingBetweenSlices(); | |
614 LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm"; | |
615 | |
616 const DicomInstanceParameters& parameters = *slices_[0]; | |
617 | |
618 geometry_.reset(new VolumeImageGeometry); | |
619 geometry_->SetSize(parameters.GetImageInformation().GetWidth(), | |
620 parameters.GetImageInformation().GetHeight(), | |
621 static_cast<unsigned int>(slices.GetSlicesCount())); | |
622 geometry_->SetAxialGeometry(slices.GetSliceGeometry(0)); | |
623 geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(), | |
624 parameters.GetPixelSpacingY(), spacingZ); | |
625 } | |
626 } | |
627 | |
628 bool HasGeometry() const | |
629 { | |
630 return geometry_.get() != NULL; | |
631 } | |
632 | |
633 const VolumeImageGeometry& GetImageGeometry() const | |
634 { | |
635 if (!HasGeometry()) | |
636 { | |
637 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
638 } | |
639 else | |
640 { | |
641 assert(slices_.size() == geometry_->GetDepth()); | |
642 return *geometry_; | |
643 } | |
644 } | |
645 | |
646 const DicomInstanceParameters& GetSliceParameters(size_t index) const | |
647 { | |
648 CheckSliceIndex(index); | |
649 return *slices_[index]; | |
650 } | |
651 | |
652 uint64_t GetSliceRevision(size_t index) const | |
653 { | |
654 CheckSliceIndex(index); | |
655 return slicesRevision_[index]; | |
656 } | |
657 | |
658 void IncrementSliceRevision(size_t index) | |
659 { | |
660 CheckSliceIndex(index); | |
661 slicesRevision_[index] ++; | |
662 } | |
663 }; | |
664 | |
665 | |
666 class Slice : public DicomVolumeImageOrthogonalSlice | |
667 { | |
668 private: | |
669 const OrthancSeriesVolumeProgressiveLoader& that_; | |
670 | |
671 protected: | |
672 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
673 unsigned int sliceIndex) const | |
674 { | |
675 if (projection == VolumeProjection_Axial) | |
676 { | |
677 return that_.seriesGeometry_.GetSliceRevision(sliceIndex); | |
678 } | |
679 else | |
680 { | |
681 // For coronal and sagittal projections, we take the global | |
682 // revision of the volume | |
683 return that_.volume_->GetRevision(); | |
684 } | |
685 } | |
686 | |
687 public: | |
688 Slice(const OrthancSeriesVolumeProgressiveLoader& that, | |
689 const CoordinateSystem3D& plane) : | |
690 DicomVolumeImageOrthogonalSlice(*that.volume_, plane), | |
691 that_(that) | |
692 { | |
693 } | |
694 }; | |
695 | |
696 | |
697 | |
548 static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) | 698 static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command) |
549 { | 699 { |
550 return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); | 700 return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue(); |
551 } | 701 } |
552 | 702 |
559 | 709 |
560 if (strategy_->GetNext(sliceIndex, quality)) | 710 if (strategy_->GetNext(sliceIndex, quality)) |
561 { | 711 { |
562 assert(quality <= BEST_QUALITY); | 712 assert(quality <= BEST_QUALITY); |
563 | 713 |
564 const DicomInstanceParameters& slice = volume_.GetSliceParameters(sliceIndex); | 714 const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex); |
565 | 715 |
566 const std::string& instance = slice.GetOrthancInstanceIdentifier(); | 716 const std::string& instance = slice.GetOrthancInstanceIdentifier(); |
567 if (instance.empty()) | 717 if (instance.empty()) |
568 { | 718 { |
569 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 719 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
621 | 771 |
622 CoordinateSystem3D geometry = instance->GetGeometry(); | 772 CoordinateSystem3D geometry = instance->GetGeometry(); |
623 slices.AddSlice(geometry, instance.release()); | 773 slices.AddSlice(geometry, instance.release()); |
624 } | 774 } |
625 | 775 |
626 volume_.SetGeometry(slices); | 776 seriesGeometry_.ComputeGeometry(slices); |
627 } | 777 } |
628 | 778 |
629 if (volume_.GetSlicesCount() != 0) | 779 size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth(); |
630 { | 780 |
631 strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter( | 781 if (slicesCount == 0) |
632 static_cast<unsigned int>(volume_.GetSlicesCount())), BEST_QUALITY)); | 782 { |
633 | 783 volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8); |
784 } | |
785 else | |
786 { | |
787 const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0); | |
788 | |
789 volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat()); | |
790 volume_->SetDicomParameters(parameters); | |
791 volume_->GetPixelData().Clear(); | |
792 | |
793 strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(slicesCount), BEST_QUALITY)); | |
794 | |
634 assert(simultaneousDownloads_ != 0); | 795 assert(simultaneousDownloads_ != 0); |
635 for (unsigned int i = 0; i < simultaneousDownloads_; i++) | 796 for (unsigned int i = 0; i < simultaneousDownloads_; i++) |
636 { | 797 { |
637 ScheduleNextSliceDownload(); | 798 ScheduleNextSliceDownload(); |
638 } | 799 } |
639 } | 800 } |
801 | |
802 slicesQuality_.resize(slicesCount, 0); | |
803 | |
804 BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); | |
805 } | |
806 | |
807 | |
808 void SetSliceContent(unsigned int sliceIndex, | |
809 const Orthanc::ImageAccessor& image, | |
810 unsigned int quality) | |
811 { | |
812 assert(sliceIndex < slicesQuality_.size() && | |
813 slicesQuality_.size() == volume_->GetPixelData().GetDepth()); | |
814 | |
815 if (quality >= slicesQuality_[sliceIndex]) | |
816 { | |
817 { | |
818 ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex); | |
819 Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image); | |
820 } | |
821 | |
822 volume_->IncrementRevision(); | |
823 seriesGeometry_.IncrementSliceRevision(sliceIndex); | |
824 slicesQuality_[sliceIndex] = quality; | |
825 | |
826 BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); | |
827 } | |
828 | |
829 ScheduleNextSliceDownload(); | |
640 } | 830 } |
641 | 831 |
642 | 832 |
643 void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) | 833 void LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message) |
644 { | 834 { |
645 volume_.SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), | 835 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY); |
646 message.GetImage(), BEST_QUALITY); | |
647 | |
648 ScheduleNextSliceDownload(); | |
649 } | 836 } |
650 | 837 |
651 | 838 |
652 void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) | 839 void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) |
653 { | 840 { |
665 | 852 |
666 default: | 853 default: |
667 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 854 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
668 } | 855 } |
669 | 856 |
670 volume_.SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); | 857 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality); |
671 | 858 } |
672 ScheduleNextSliceDownload(); | 859 |
673 } | 860 |
674 | 861 IOracle& oracle_; |
675 | 862 bool active_; |
676 IOracle& oracle_; | 863 unsigned int simultaneousDownloads_; |
677 bool active_; | 864 SeriesGeometry seriesGeometry_; |
678 DicomSeriesVolumeImage volume_; | 865 |
679 unsigned int simultaneousDownloads_; | 866 boost::shared_ptr<DicomVolumeImage> volume_; |
680 | |
681 std::auto_ptr<IFetchingItemsSorter::IFactory> sorter_; | 867 std::auto_ptr<IFetchingItemsSorter::IFactory> sorter_; |
682 std::auto_ptr<IFetchingStrategy> strategy_; | 868 std::auto_ptr<IFetchingStrategy> strategy_; |
683 | 869 std::vector<unsigned int> slicesQuality_; |
684 | 870 |
685 IVolumeSlicer::ExtractedSlice* ExtractOrthogonalSlice(const CoordinateSystem3D& cuttingPlane) const | 871 |
686 { | |
687 if (volume_.HasGeometry() && | |
688 volume_.GetSlicesCount() != 0) | |
689 { | |
690 std::auto_ptr<DicomVolumeImageOrthogonalSlice> slice | |
691 (new DicomSeriesVolumeImage::ExtractedOrthogonalSlice(volume_, cuttingPlane)); | |
692 | |
693 assert(slice.get() != NULL && | |
694 strategy_.get() != NULL); | |
695 | |
696 if (slice->IsValid() && | |
697 slice->GetProjection() == VolumeProjection_Axial) | |
698 { | |
699 strategy_->SetCurrent(slice->GetSliceIndex()); | |
700 } | |
701 | |
702 return slice.release(); | |
703 } | |
704 else | |
705 { | |
706 return new InvalidExtractedSlice; | |
707 } | |
708 } | |
709 | |
710 | |
711 public: | 872 public: |
712 class MPRSlicer : public IVolumeImageSlicer | 873 OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume, |
713 { | 874 IOracle& oracle, |
714 private: | |
715 boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> that_; | |
716 | |
717 public: | |
718 MPRSlicer(const boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>& that) : | |
719 that_(that) | |
720 { | |
721 } | |
722 | |
723 virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const | |
724 { | |
725 return that_->ExtractOrthogonalSlice(cuttingPlane); | |
726 } | |
727 | |
728 virtual bool HasGeometry() const | |
729 { | |
730 return that_->GetVolume().HasGeometry(); | |
731 } | |
732 | |
733 virtual const VolumeImageGeometry& GetGeometry() const | |
734 { | |
735 return that_->GetVolume().GetGeometry(); | |
736 } | |
737 }; | |
738 | |
739 OrthancSeriesVolumeProgressiveLoader(IOracle& oracle, | |
740 IObservable& oracleObservable) : | 875 IObservable& oracleObservable) : |
741 IObserver(oracleObservable.GetBroker()), | 876 IObserver(oracleObservable.GetBroker()), |
877 IObservable(oracleObservable.GetBroker()), | |
742 oracle_(oracle), | 878 oracle_(oracle), |
743 active_(false), | 879 active_(false), |
744 simultaneousDownloads_(4), | 880 simultaneousDownloads_(4), |
881 volume_(volume), | |
745 sorter_(new BasicFetchingItemsSorter::Factory) | 882 sorter_(new BasicFetchingItemsSorter::Factory) |
746 { | 883 { |
747 oracleObservable.RegisterObserverCallback( | 884 oracleObservable.RegisterObserverCallback( |
748 new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage> | 885 new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage> |
749 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); | 886 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry)); |
787 command->SetUri("/series/" + seriesId + "/instances-tags"); | 924 command->SetUri("/series/" + seriesId + "/instances-tags"); |
788 | 925 |
789 oracle_.Schedule(*this, command.release()); | 926 oracle_.Schedule(*this, command.release()); |
790 } | 927 } |
791 } | 928 } |
792 | 929 |
793 | 930 |
794 const DicomSeriesVolumeImage& GetVolume() const | 931 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) |
795 { | 932 { |
796 return volume_; | 933 if (volume_->HasGeometry()) |
934 { | |
935 std::auto_ptr<Slice> slice(new Slice(*this, cuttingPlane)); | |
936 | |
937 if (strategy_.get() != NULL && | |
938 slice->IsValid() && | |
939 slice->GetProjection() == VolumeProjection_Axial) | |
940 { | |
941 strategy_->SetCurrent(slice->GetSliceIndex()); | |
942 } | |
943 | |
944 return slice.release(); | |
945 } | |
946 else | |
947 { | |
948 return new IVolumeSlicer::InvalidSlice; | |
949 } | |
797 } | 950 } |
798 }; | 951 }; |
799 | 952 |
800 | 953 |
801 | 954 |
802 class OrthancMultiframeVolumeLoader : public IObserver | 955 class OrthancMultiframeVolumeLoader : |
956 public IObserver, | |
957 public IObservable, | |
958 public IVolumeSlicer | |
803 { | 959 { |
804 private: | 960 private: |
805 class State : public Orthanc::IDynamicObject | 961 class State : public Orthanc::IDynamicObject |
806 { | 962 { |
807 private: | 963 private: |
947 } | 1103 } |
948 }; | 1104 }; |
949 | 1105 |
950 | 1106 |
951 | 1107 |
1108 boost::shared_ptr<DicomVolumeImage> volume_; | |
952 IOracle& oracle_; | 1109 IOracle& oracle_; |
953 bool active_; | 1110 bool active_; |
954 std::string instanceId_; | 1111 std::string instanceId_; |
955 std::string transferSyntaxUid_; | 1112 std::string transferSyntaxUid_; |
956 uint64_t revision_; | |
957 | |
958 std::auto_ptr<DicomInstanceParameters> dicom_; | |
959 std::auto_ptr<VolumeImageGeometry> geometry_; | |
960 std::auto_ptr<ImageBuffer3D> image_; | |
961 | 1113 |
962 | 1114 |
963 const std::string& GetInstanceId() const | 1115 const std::string& GetInstanceId() const |
964 { | 1116 { |
965 if (active_) | 1117 if (active_) |
974 | 1126 |
975 | 1127 |
976 void ScheduleFrameDownloads() | 1128 void ScheduleFrameDownloads() |
977 { | 1129 { |
978 if (transferSyntaxUid_.empty() || | 1130 if (transferSyntaxUid_.empty() || |
979 !HasGeometry()) | 1131 !volume_->HasGeometry()) |
980 { | 1132 { |
981 return; | 1133 return; |
982 } | 1134 } |
983 | 1135 |
984 if (transferSyntaxUid_ == "1.2.840.10008.1.2" || | 1136 if (transferSyntaxUid_ == "1.2.840.10008.1.2" || |
1008 } | 1160 } |
1009 | 1161 |
1010 | 1162 |
1011 void SetGeometry(const Orthanc::DicomMap& dicom) | 1163 void SetGeometry(const Orthanc::DicomMap& dicom) |
1012 { | 1164 { |
1013 dicom_.reset(new DicomInstanceParameters(dicom)); | 1165 DicomInstanceParameters parameters(dicom); |
1014 | 1166 volume_->SetDicomParameters(parameters); |
1167 | |
1015 Orthanc::PixelFormat format; | 1168 Orthanc::PixelFormat format; |
1016 if (!dicom_->GetImageInformation().ExtractPixelFormat(format, true)) | 1169 if (!parameters.GetImageInformation().ExtractPixelFormat(format, true)) |
1017 { | 1170 { |
1018 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | 1171 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); |
1019 } | 1172 } |
1020 | 1173 |
1021 double spacingZ; | 1174 double spacingZ; |
1022 switch (dicom_->GetSopClassUid()) | 1175 switch (parameters.GetSopClassUid()) |
1023 { | 1176 { |
1024 case SopClassUid_RTDose: | 1177 case SopClassUid_RTDose: |
1025 spacingZ = dicom_->GetThickness(); | 1178 spacingZ = parameters.GetThickness(); |
1026 break; | 1179 break; |
1027 | 1180 |
1028 default: | 1181 default: |
1029 throw Orthanc::OrthancException( | 1182 throw Orthanc::OrthancException( |
1030 Orthanc::ErrorCode_NotImplemented, | 1183 Orthanc::ErrorCode_NotImplemented, |
1031 "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); | 1184 "No support for multiframe instances with SOP class UID: " + GetSopClassUid(dicom)); |
1032 } | 1185 } |
1033 | 1186 |
1034 const unsigned int width = dicom_->GetImageInformation().GetWidth(); | 1187 const unsigned int width = parameters.GetImageInformation().GetWidth(); |
1035 const unsigned int height = dicom_->GetImageInformation().GetHeight(); | 1188 const unsigned int height = parameters.GetImageInformation().GetHeight(); |
1036 const unsigned int depth = dicom_->GetImageInformation().GetNumberOfFrames(); | 1189 const unsigned int depth = parameters.GetImageInformation().GetNumberOfFrames(); |
1037 | 1190 |
1038 geometry_.reset(new VolumeImageGeometry); | 1191 { |
1039 geometry_->SetSize(width, height, depth); | 1192 VolumeImageGeometry geometry; |
1040 geometry_->SetAxialGeometry(dicom_->GetGeometry()); | 1193 geometry.SetSize(width, height, depth); |
1041 geometry_->SetVoxelDimensions(dicom_->GetPixelSpacingX(), | 1194 geometry.SetAxialGeometry(parameters.GetGeometry()); |
1042 dicom_->GetPixelSpacingY(), | 1195 geometry.SetVoxelDimensions(parameters.GetPixelSpacingX(), |
1043 spacingZ); | 1196 parameters.GetPixelSpacingY(), spacingZ); |
1044 | 1197 volume_->Initialize(geometry, format); |
1045 image_.reset(new ImageBuffer3D(format, width, height, depth, | 1198 } |
1046 false /* don't compute range */)); | 1199 |
1047 image_->Clear(); | 1200 volume_->GetPixelData().Clear(); |
1048 | 1201 |
1049 ScheduleFrameDownloads(); | 1202 ScheduleFrameDownloads(); |
1203 | |
1204 BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_)); | |
1050 } | 1205 } |
1051 | 1206 |
1052 | 1207 |
1053 ORTHANC_FORCE_INLINE | 1208 ORTHANC_FORCE_INLINE |
1054 static void CopyPixel(uint32_t& target, | 1209 static void CopyPixel(uint32_t& target, |
1060 | 1215 |
1061 | 1216 |
1062 template <typename T> | 1217 template <typename T> |
1063 void CopyPixelData(const std::string& pixelData) | 1218 void CopyPixelData(const std::string& pixelData) |
1064 { | 1219 { |
1065 const Orthanc::PixelFormat format = image_->GetFormat(); | 1220 ImageBuffer3D& target = volume_->GetPixelData(); |
1066 const unsigned int bpp = image_->GetBytesPerPixel(); | 1221 |
1067 const unsigned int width = image_->GetWidth(); | 1222 const Orthanc::PixelFormat format = target.GetFormat(); |
1068 const unsigned int height = image_->GetHeight(); | 1223 const unsigned int bpp = target.GetBytesPerPixel(); |
1069 const unsigned int depth = image_->GetDepth(); | 1224 const unsigned int width = target.GetWidth(); |
1225 const unsigned int height = target.GetHeight(); | |
1226 const unsigned int depth = target.GetDepth(); | |
1070 | 1227 |
1071 if (pixelData.size() != bpp * width * height * depth) | 1228 if (pixelData.size() != bpp * width * height * depth) |
1072 { | 1229 { |
1073 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, | 1230 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, |
1074 "The pixel data has not the proper size"); | 1231 "The pixel data has not the proper size"); |
1081 | 1238 |
1082 const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); | 1239 const uint8_t* source = reinterpret_cast<const uint8_t*>(pixelData.c_str()); |
1083 | 1240 |
1084 for (unsigned int z = 0; z < depth; z++) | 1241 for (unsigned int z = 0; z < depth; z++) |
1085 { | 1242 { |
1086 ImageBuffer3D::SliceWriter writer(*image_, VolumeProjection_Axial, z); | 1243 ImageBuffer3D::SliceWriter writer(target, VolumeProjection_Axial, z); |
1087 | 1244 |
1088 assert (writer.GetAccessor().GetWidth() == width && | 1245 assert (writer.GetAccessor().GetWidth() == width && |
1089 writer.GetAccessor().GetHeight() == height); | 1246 writer.GetAccessor().GetHeight() == height); |
1090 | 1247 |
1091 for (unsigned int y = 0; y < height; y++) | 1248 for (unsigned int y = 0; y < height; y++) |
1106 } | 1263 } |
1107 | 1264 |
1108 | 1265 |
1109 void SetUncompressedPixelData(const std::string& pixelData) | 1266 void SetUncompressedPixelData(const std::string& pixelData) |
1110 { | 1267 { |
1111 switch (image_->GetFormat()) | 1268 switch (volume_->GetPixelData().GetFormat()) |
1112 { | 1269 { |
1113 case Orthanc::PixelFormat_Grayscale32: | 1270 case Orthanc::PixelFormat_Grayscale32: |
1114 CopyPixelData<uint32_t>(pixelData); | 1271 CopyPixelData<uint32_t>(pixelData); |
1115 break; | 1272 break; |
1116 | 1273 |
1117 default: | 1274 default: |
1118 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | 1275 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); |
1119 } | 1276 } |
1120 | 1277 |
1121 revision_ ++; | 1278 volume_->IncrementRevision(); |
1122 } | 1279 |
1123 | 1280 BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_)); |
1124 | 1281 } |
1125 private: | 1282 |
1126 class ExtractedOrthogonalSlice : public DicomVolumeImageOrthogonalSlice | 1283 |
1127 { | |
1128 private: | |
1129 const OrthancMultiframeVolumeLoader& that_; | |
1130 | |
1131 protected: | |
1132 virtual uint64_t GetRevisionInternal(VolumeProjection projection, | |
1133 unsigned int sliceIndex) const | |
1134 { | |
1135 return that_.revision_; | |
1136 } | |
1137 | |
1138 virtual const DicomInstanceParameters& GetDicomParameters(VolumeProjection projection, | |
1139 unsigned int sliceIndex) const | |
1140 { | |
1141 return that_.GetDicomParameters(); | |
1142 } | |
1143 | |
1144 public: | |
1145 ExtractedOrthogonalSlice(const OrthancMultiframeVolumeLoader& that, | |
1146 const CoordinateSystem3D& plane) : | |
1147 DicomVolumeImageOrthogonalSlice(that.GetImage(), that.GetGeometry(), plane), | |
1148 that_(that) | |
1149 { | |
1150 } | |
1151 }; | |
1152 | |
1153 | |
1154 public: | 1284 public: |
1155 class MPRSlicer : public IVolumeImageSlicer | 1285 OrthancMultiframeVolumeLoader(const boost::shared_ptr<DicomVolumeImage>& volume, |
1156 { | 1286 IOracle& oracle, |
1157 private: | |
1158 boost::shared_ptr<OrthancMultiframeVolumeLoader> that_; | |
1159 | |
1160 public: | |
1161 MPRSlicer(const boost::shared_ptr<OrthancMultiframeVolumeLoader>& that) : | |
1162 that_(that) | |
1163 { | |
1164 } | |
1165 | |
1166 virtual ExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) const | |
1167 { | |
1168 if (that_->HasGeometry()) | |
1169 { | |
1170 return new ExtractedOrthogonalSlice(*that_, cuttingPlane); | |
1171 } | |
1172 else | |
1173 { | |
1174 return new InvalidExtractedSlice; | |
1175 } | |
1176 } | |
1177 | |
1178 virtual bool HasGeometry() const | |
1179 { | |
1180 return that_->HasGeometry(); | |
1181 } | |
1182 | |
1183 virtual const VolumeImageGeometry& GetGeometry() const | |
1184 { | |
1185 return that_->GetGeometry(); | |
1186 } | |
1187 }; | |
1188 | |
1189 | |
1190 OrthancMultiframeVolumeLoader(IOracle& oracle, | |
1191 IObservable& oracleObservable) : | 1287 IObservable& oracleObservable) : |
1192 IObserver(oracleObservable.GetBroker()), | 1288 IObserver(oracleObservable.GetBroker()), |
1289 IObservable(oracleObservable.GetBroker()), | |
1290 volume_(volume), | |
1193 oracle_(oracle), | 1291 oracle_(oracle), |
1194 active_(false), | 1292 active_(false) |
1195 revision_(0) | 1293 { |
1196 { | 1294 if (volume.get() == NULL) |
1295 { | |
1296 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); | |
1297 } | |
1298 | |
1197 oracleObservable.RegisterObserverCallback( | 1299 oracleObservable.RegisterObserverCallback( |
1198 new Callable<OrthancMultiframeVolumeLoader, OrthancRestApiCommand::SuccessMessage> | 1300 new Callable<OrthancMultiframeVolumeLoader, OrthancRestApiCommand::SuccessMessage> |
1199 (*this, &OrthancMultiframeVolumeLoader::Handle)); | 1301 (*this, &OrthancMultiframeVolumeLoader::Handle)); |
1200 } | 1302 } |
1201 | 1303 |
1202 | |
1203 bool HasGeometry() const | |
1204 { | |
1205 return (dicom_.get() != NULL && | |
1206 geometry_.get() != NULL && | |
1207 image_.get() != NULL); | |
1208 } | |
1209 | |
1210 | |
1211 const ImageBuffer3D& GetImage() const | |
1212 { | |
1213 if (!HasGeometry()) | |
1214 { | |
1215 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1216 } | |
1217 else | |
1218 { | |
1219 return *image_; | |
1220 } | |
1221 } | |
1222 | |
1223 | |
1224 const VolumeImageGeometry& GetGeometry() const | |
1225 { | |
1226 if (!HasGeometry()) | |
1227 { | |
1228 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1229 } | |
1230 else | |
1231 { | |
1232 return *geometry_; | |
1233 } | |
1234 } | |
1235 | |
1236 | |
1237 const DicomInstanceParameters& GetDicomParameters() const | |
1238 { | |
1239 if (!HasGeometry()) | |
1240 { | |
1241 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1242 } | |
1243 else | |
1244 { | |
1245 return *dicom_; | |
1246 } | |
1247 } | |
1248 | |
1249 | 1304 |
1250 void LoadInstance(const std::string& instanceId) | 1305 void LoadInstance(const std::string& instanceId) |
1251 { | 1306 { |
1252 if (active_) | 1307 if (active_) |
1253 { | 1308 { |
1272 command->SetPayload(new LoadTransferSyntax(*this)); | 1327 command->SetPayload(new LoadTransferSyntax(*this)); |
1273 oracle_.Schedule(*this, command.release()); | 1328 oracle_.Schedule(*this, command.release()); |
1274 } | 1329 } |
1275 } | 1330 } |
1276 } | 1331 } |
1332 | |
1333 | |
1334 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) | |
1335 { | |
1336 if (volume_->HasGeometry()) | |
1337 { | |
1338 return new DicomVolumeImageOrthogonalSlice(*volume_, cuttingPlane); | |
1339 } | |
1340 else | |
1341 { | |
1342 return new IVolumeSlicer::InvalidSlice; | |
1343 } | |
1344 } | |
1277 }; | 1345 }; |
1278 | 1346 |
1279 | 1347 |
1280 | 1348 |
1349 class VolumeImageReslicer : public IVolumeSlicer | |
1350 { | |
1351 private: | |
1352 class Slice : public IExtractedSlice | |
1353 { | |
1354 private: | |
1355 VolumeImageReslicer& that_; | |
1356 CoordinateSystem3D cuttingPlane_; | |
1357 | |
1358 public: | |
1359 Slice(VolumeImageReslicer& that, | |
1360 const CoordinateSystem3D& cuttingPlane) : | |
1361 that_(that), | |
1362 cuttingPlane_(cuttingPlane) | |
1363 { | |
1364 } | |
1365 | |
1366 virtual bool IsValid() | |
1367 { | |
1368 return true; | |
1369 } | |
1370 | |
1371 virtual uint64_t GetRevision() | |
1372 { | |
1373 return that_.volume_->GetRevision(); | |
1374 } | |
1375 | |
1376 virtual ISceneLayer* CreateSceneLayer(const ILayerStyleConfigurator* configurator, // possibly absent | |
1377 const CoordinateSystem3D& cuttingPlane) | |
1378 { | |
1379 VolumeReslicer& reslicer = that_.reslicer_; | |
1380 | |
1381 if (configurator == NULL) | |
1382 { | |
1383 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, | |
1384 "Must provide a layer style configurator"); | |
1385 } | |
1386 | |
1387 reslicer.SetOutputFormat(that_.volume_->GetPixelData().GetFormat()); | |
1388 reslicer.Apply(that_.volume_->GetPixelData(), | |
1389 that_.volume_->GetGeometry(), | |
1390 cuttingPlane); | |
1391 | |
1392 if (reslicer.IsSuccess()) | |
1393 { | |
1394 std::auto_ptr<TextureBaseSceneLayer> layer | |
1395 (configurator->CreateTextureFromDicom(reslicer.GetOutputSlice(), | |
1396 that_.volume_->GetDicomParameters())); | |
1397 if (layer.get() == NULL) | |
1398 { | |
1399 return NULL; | |
1400 } | |
1401 | |
1402 double s = reslicer.GetPixelSpacing(); | |
1403 layer->SetPixelSpacing(s, s); | |
1404 layer->SetOrigin(reslicer.GetOutputExtent().GetX1() + 0.5 * s, | |
1405 reslicer.GetOutputExtent().GetY1() + 0.5 * s); | |
1406 | |
1407 // TODO - Angle!! | |
1408 | |
1409 return layer.release(); | |
1410 } | |
1411 else | |
1412 { | |
1413 return NULL; | |
1414 } | |
1415 } | |
1416 }; | |
1417 | |
1418 boost::shared_ptr<DicomVolumeImage> volume_; | |
1419 VolumeReslicer reslicer_; | |
1420 | |
1421 public: | |
1422 VolumeImageReslicer(const boost::shared_ptr<DicomVolumeImage>& volume) : | |
1423 volume_(volume) | |
1424 { | |
1425 if (volume.get() == NULL) | |
1426 { | |
1427 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); | |
1428 } | |
1429 } | |
1430 | |
1431 ImageInterpolation GetInterpolation() const | |
1432 { | |
1433 return reslicer_.GetInterpolation(); | |
1434 } | |
1435 | |
1436 void SetInterpolation(ImageInterpolation interpolation) | |
1437 { | |
1438 reslicer_.SetInterpolation(interpolation); | |
1439 } | |
1440 | |
1441 bool IsFastMode() const | |
1442 { | |
1443 return reslicer_.IsFastMode(); | |
1444 } | |
1445 | |
1446 void SetFastMode(bool fast) | |
1447 { | |
1448 reslicer_.EnableFastMode(fast); | |
1449 } | |
1450 | |
1451 virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) | |
1452 { | |
1453 if (volume_->HasGeometry()) | |
1454 { | |
1455 return new Slice(*this, cuttingPlane); | |
1456 } | |
1457 else | |
1458 { | |
1459 return new IVolumeSlicer::InvalidSlice; | |
1460 } | |
1461 } | |
1462 }; | |
1463 | |
1464 | |
1465 | |
1281 class VolumeSceneLayerSource : public boost::noncopyable | 1466 class VolumeSceneLayerSource : public boost::noncopyable |
1282 { | 1467 { |
1283 private: | 1468 private: |
1284 int layerDepth_; | 1469 Scene2D& scene_; |
1285 boost::shared_ptr<IVolumeSlicer> slicer_; | 1470 int layerDepth_; |
1286 bool linearInterpolation_; | 1471 boost::shared_ptr<IVolumeSlicer> slicer_; |
1287 std::auto_ptr<CoordinateSystem3D> lastPlane_; | 1472 std::auto_ptr<ILayerStyleConfigurator> configurator_; |
1288 uint64_t lastRevision_; | 1473 std::auto_ptr<CoordinateSystem3D> lastPlane_; |
1474 uint64_t lastRevision_; | |
1475 uint64_t lastConfiguratorRevision_; | |
1289 | 1476 |
1290 static bool IsSameCuttingPlane(const CoordinateSystem3D& a, | 1477 static bool IsSameCuttingPlane(const CoordinateSystem3D& a, |
1291 const CoordinateSystem3D& b) | 1478 const CoordinateSystem3D& b) |
1292 { | 1479 { |
1480 // TODO - What if the normal is reversed? | |
1293 double distance; | 1481 double distance; |
1294 return (CoordinateSystem3D::ComputeDistance(distance, a, b) && | 1482 return (CoordinateSystem3D::ComputeDistance(distance, a, b) && |
1295 LinearAlgebra::IsCloseToZero(distance)); | 1483 LinearAlgebra::IsCloseToZero(distance)); |
1296 } | 1484 } |
1297 | 1485 |
1486 void ClearLayer() | |
1487 { | |
1488 scene_.DeleteLayer(layerDepth_); | |
1489 lastPlane_.reset(NULL); | |
1490 } | |
1491 | |
1298 public: | 1492 public: |
1299 VolumeSceneLayerSource(int layerDepth, | 1493 VolumeSceneLayerSource(Scene2D& scene, |
1300 IVolumeSlicer* slicer) : // Takes ownership | 1494 int layerDepth, |
1495 const boost::shared_ptr<IVolumeSlicer>& slicer) : | |
1496 scene_(scene), | |
1301 layerDepth_(layerDepth), | 1497 layerDepth_(layerDepth), |
1302 slicer_(slicer), | 1498 slicer_(slicer) |
1303 linearInterpolation_(false) | |
1304 { | 1499 { |
1305 if (slicer == NULL) | 1500 if (slicer == NULL) |
1306 { | 1501 { |
1307 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); | 1502 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); |
1308 } | 1503 } |
1311 const IVolumeSlicer& GetSlicer() const | 1506 const IVolumeSlicer& GetSlicer() const |
1312 { | 1507 { |
1313 return *slicer_; | 1508 return *slicer_; |
1314 } | 1509 } |
1315 | 1510 |
1316 void SetLinearInterpolation(bool enabled) | 1511 void RemoveConfigurator() |
1317 { | 1512 { |
1318 linearInterpolation_ = enabled; | 1513 configurator_.reset(); |
1319 } | 1514 lastPlane_.reset(); |
1320 | 1515 } |
1321 bool IsLinearInterpolation() const | 1516 |
1322 { | 1517 void SetConfigurator(ILayerStyleConfigurator* configurator) // Takes ownership |
1323 return linearInterpolation_; | 1518 { |
1324 } | 1519 if (configurator == NULL) |
1325 | 1520 { |
1326 void Update(Scene2D& scene, | 1521 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); |
1327 const CoordinateSystem3D& plane) | 1522 } |
1523 | |
1524 configurator_.reset(configurator); | |
1525 | |
1526 // Invalidate the layer | |
1527 lastPlane_.reset(NULL); | |
1528 } | |
1529 | |
1530 bool HasConfigurator() const | |
1531 { | |
1532 return configurator_.get() != NULL; | |
1533 } | |
1534 | |
1535 ILayerStyleConfigurator& GetConfigurator() const | |
1536 { | |
1537 if (configurator_.get() == NULL) | |
1538 { | |
1539 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
1540 } | |
1541 | |
1542 return *configurator_; | |
1543 } | |
1544 | |
1545 void Update(const CoordinateSystem3D& plane) | |
1328 { | 1546 { |
1329 assert(slicer_.get() != NULL); | 1547 assert(slicer_.get() != NULL); |
1330 std::auto_ptr<IVolumeSlicer::ExtractedSlice> slice(slicer_->ExtractSlice(plane)); | 1548 std::auto_ptr<IVolumeSlicer::IExtractedSlice> slice(slicer_->ExtractSlice(plane)); |
1331 | 1549 |
1332 if (slice.get() == NULL) | 1550 if (slice.get() == NULL) |
1333 { | 1551 { |
1334 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 1552 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
1335 } | 1553 } |
1336 | 1554 |
1337 if (!slice->IsValid()) | 1555 if (!slice->IsValid()) |
1338 { | 1556 { |
1339 // The slicer cannot handle this cutting plane: Clear the layer | 1557 // The slicer cannot handle this cutting plane: Clear the layer |
1340 scene.DeleteLayer(layerDepth_); | 1558 ClearLayer(); |
1341 lastPlane_.reset(NULL); | |
1342 } | 1559 } |
1343 else if (lastPlane_.get() != NULL && | 1560 else if (lastPlane_.get() != NULL && |
1344 IsSameCuttingPlane(*lastPlane_, plane) && | 1561 IsSameCuttingPlane(*lastPlane_, plane) && |
1345 lastRevision_ == slice->GetRevision()) | 1562 lastRevision_ == slice->GetRevision()) |
1346 { | 1563 { |
1347 // The content of the slice has not changed: Do nothing | 1564 // The content of the slice has not changed: Don't update the |
1565 // layer content, but possibly update its style | |
1566 | |
1567 if (configurator_.get() != NULL && | |
1568 configurator_->GetRevision() != lastConfiguratorRevision_ && | |
1569 scene_.HasLayer(layerDepth_)) | |
1570 { | |
1571 configurator_->ApplyStyle(scene_.GetLayer(layerDepth_)); | |
1572 } | |
1348 } | 1573 } |
1349 else | 1574 else |
1350 { | 1575 { |
1351 // Content has changed: An update is needed | 1576 // Content has changed: An update is needed |
1352 lastPlane_.reset(new CoordinateSystem3D(plane)); | 1577 lastPlane_.reset(new CoordinateSystem3D(plane)); |
1353 lastRevision_ = slice->GetRevision(); | 1578 lastRevision_ = slice->GetRevision(); |
1354 | 1579 |
1355 std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(plane)); | 1580 std::auto_ptr<ISceneLayer> layer(slice->CreateSceneLayer(configurator_.get(), plane)); |
1356 if (layer.get() == NULL) | 1581 if (layer.get() == NULL) |
1357 { | 1582 { |
1358 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 1583 ClearLayer(); |
1359 } | 1584 } |
1360 | 1585 else |
1361 if (layer->GetType() == ISceneLayer::Type_ColorTexture || | 1586 { |
1362 layer->GetType() == ISceneLayer::Type_FloatTexture) | 1587 if (configurator_.get() != NULL) |
1363 { | 1588 { |
1364 dynamic_cast<TextureBaseSceneLayer&>(*layer).SetLinearInterpolation(linearInterpolation_); | 1589 lastConfiguratorRevision_ = configurator_->GetRevision(); |
1365 } | 1590 configurator_->ApplyStyle(*layer); |
1366 | 1591 } |
1367 scene.SetLayer(layerDepth_, layer.release()); | 1592 |
1593 scene_.SetLayer(layerDepth_, layer.release()); | |
1594 } | |
1368 } | 1595 } |
1369 } | 1596 } |
1370 }; | 1597 }; |
1371 | 1598 |
1372 | 1599 |
1408 NativeApplicationContext& that_; | 1635 NativeApplicationContext& that_; |
1409 boost::shared_lock<boost::shared_mutex> lock_; | 1636 boost::shared_lock<boost::shared_mutex> lock_; |
1410 | 1637 |
1411 public: | 1638 public: |
1412 ReaderLock(NativeApplicationContext& that) : | 1639 ReaderLock(NativeApplicationContext& that) : |
1413 that_(that), | 1640 that_(that), |
1414 lock_(that.mutex_) | 1641 lock_(that.mutex_) |
1415 { | 1642 { |
1416 } | 1643 } |
1417 }; | 1644 }; |
1418 | 1645 |
1419 | 1646 |
1423 NativeApplicationContext& that_; | 1650 NativeApplicationContext& that_; |
1424 boost::unique_lock<boost::shared_mutex> lock_; | 1651 boost::unique_lock<boost::shared_mutex> lock_; |
1425 | 1652 |
1426 public: | 1653 public: |
1427 WriterLock(NativeApplicationContext& that) : | 1654 WriterLock(NativeApplicationContext& that) : |
1428 that_(that), | 1655 that_(that), |
1429 lock_(that.mutex_) | 1656 lock_(that.mutex_) |
1430 { | 1657 { |
1431 } | 1658 } |
1432 | 1659 |
1433 MessageBroker& GetBroker() | 1660 MessageBroker& GetBroker() |
1434 { | 1661 { |
1446 | 1673 |
1447 | 1674 |
1448 class Toto : public OrthancStone::IObserver | 1675 class Toto : public OrthancStone::IObserver |
1449 { | 1676 { |
1450 private: | 1677 private: |
1451 OrthancStone::IOracle& oracle_; | 1678 OrthancStone::CoordinateSystem3D plane_; |
1679 OrthancStone::IOracle& oracle_; | |
1452 OrthancStone::Scene2D scene_; | 1680 OrthancStone::Scene2D scene_; |
1453 std::auto_ptr<OrthancStone::VolumeSceneLayerSource> source1_, source2_; | 1681 std::auto_ptr<OrthancStone::VolumeSceneLayerSource> source1_, source2_; |
1454 | 1682 |
1683 | |
1684 void Refresh() | |
1685 { | |
1686 if (source1_.get() != NULL) | |
1687 { | |
1688 source1_->Update(plane_); | |
1689 } | |
1690 | |
1691 if (source2_.get() != NULL) | |
1692 { | |
1693 source2_->Update(plane_); | |
1694 } | |
1695 | |
1696 scene_.FitContent(1024, 768); | |
1697 | |
1698 { | |
1699 OrthancStone::CairoCompositor compositor(scene_, 1024, 768); | |
1700 compositor.Refresh(); | |
1701 | |
1702 Orthanc::ImageAccessor accessor; | |
1703 compositor.GetCanvas().GetReadOnlyAccessor(accessor); | |
1704 | |
1705 Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false); | |
1706 Orthanc::ImageProcessing::Convert(tmp, accessor); | |
1707 | |
1708 static unsigned int count = 0; | |
1709 char buf[64]; | |
1710 sprintf(buf, "scene-%06d.png", count++); | |
1711 | |
1712 Orthanc::PngWriter writer; | |
1713 writer.WriteToFile(buf, tmp); | |
1714 } | |
1715 } | |
1716 | |
1455 | 1717 |
1456 OrthancStone::CoordinateSystem3D GetSamplePlane | 1718 void Handle(const OrthancStone::DicomVolumeImage::GeometryReadyMessage& message) |
1457 (const OrthancStone::VolumeSceneLayerSource& source) const | 1719 { |
1458 { | 1720 printf("Geometry ready\n"); |
1459 const OrthancStone::IVolumeImageSlicer& slicer = | 1721 |
1460 dynamic_cast<const OrthancStone::IVolumeImageSlicer&>(source.GetSlicer()); | 1722 //plane_ = message.GetOrigin().GetGeometry().GetSagittalGeometry(); |
1461 | 1723 //plane_ = message.GetOrigin().GetGeometry().GetAxialGeometry(); |
1462 OrthancStone::CoordinateSystem3D plane; | 1724 plane_ = message.GetOrigin().GetGeometry().GetCoronalGeometry(); |
1463 | 1725 plane_.SetOrigin(message.GetOrigin().GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); |
1464 if (slicer.HasGeometry()) | 1726 |
1465 { | 1727 Refresh(); |
1466 //plane = slicer.GetGeometry().GetSagittalGeometry(); | |
1467 //plane = slicer.GetGeometry().GetAxialGeometry(); | |
1468 plane = slicer.GetGeometry().GetCoronalGeometry(); | |
1469 plane.SetOrigin(slicer.GetGeometry().GetCoordinates(0.5f, 0.5f, 0.5f)); | |
1470 } | |
1471 | |
1472 return plane; | |
1473 } | 1728 } |
1474 | 1729 |
1475 | 1730 |
1476 void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message) | 1731 void Handle(const OrthancStone::SleepOracleCommand::TimeoutMessage& message) |
1477 { | 1732 { |
1478 if (message.GetOrigin().HasPayload()) | 1733 if (message.GetOrigin().HasPayload()) |
1479 { | 1734 { |
1480 printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue()); | 1735 printf("TIMEOUT! %d\n", dynamic_cast<const Orthanc::SingleValueObject<unsigned int>& >(message.GetOrigin().GetPayload()).GetValue()); |
1481 } | 1736 } |
1482 else | 1737 else |
1483 { | 1738 { |
1484 printf("TIMEOUT\n"); | 1739 printf("TIMEOUT\n"); |
1485 | 1740 |
1486 OrthancStone::CoordinateSystem3D plane; | 1741 Refresh(); |
1487 | |
1488 if (source1_.get() != NULL) | |
1489 { | |
1490 plane = GetSamplePlane(*source1_); | |
1491 } | |
1492 else if (source2_.get() != NULL) | |
1493 { | |
1494 plane = GetSamplePlane(*source2_); | |
1495 } | |
1496 | |
1497 if (source1_.get() != NULL) | |
1498 { | |
1499 source1_->Update(scene_, plane); | |
1500 } | |
1501 | |
1502 if (source2_.get() != NULL) | |
1503 { | |
1504 source2_->Update(scene_, plane); | |
1505 } | |
1506 | |
1507 scene_.FitContent(1024, 768); | |
1508 | |
1509 { | |
1510 OrthancStone::CairoCompositor compositor(scene_, 1024, 768); | |
1511 compositor.Refresh(); | |
1512 | |
1513 Orthanc::ImageAccessor accessor; | |
1514 compositor.GetCanvas().GetReadOnlyAccessor(accessor); | |
1515 | |
1516 Orthanc::Image tmp(Orthanc::PixelFormat_RGB24, accessor.GetWidth(), accessor.GetHeight(), false); | |
1517 Orthanc::ImageProcessing::Convert(tmp, accessor); | |
1518 | |
1519 static unsigned int count = 0; | |
1520 char buf[64]; | |
1521 sprintf(buf, "scene-%06d.png", count++); | |
1522 | |
1523 Orthanc::PngWriter writer; | |
1524 writer.WriteToFile(buf, tmp); | |
1525 } | |
1526 | 1742 |
1527 /** | 1743 /** |
1528 * The sleep() leads to a crash if the oracle is still running, | 1744 * The sleep() leads to a crash if the oracle is still running, |
1529 * while this object is destroyed. Always stop the oracle before | 1745 * while this object is destroyed. Always stop the oracle before |
1530 * destroying active objects. (*) | 1746 * destroying active objects. (*) |
1594 oracleObservable.RegisterObserverCallback | 1810 oracleObservable.RegisterObserverCallback |
1595 (new OrthancStone::Callable | 1811 (new OrthancStone::Callable |
1596 <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle)); | 1812 <Toto, OrthancStone::OracleCommandExceptionMessage>(*this, &Toto::Handle)); |
1597 } | 1813 } |
1598 | 1814 |
1815 void SetReferenceLoader(OrthancStone::IObservable& loader) | |
1816 { | |
1817 loader.RegisterObserverCallback | |
1818 (new OrthancStone::Callable | |
1819 <Toto, OrthancStone::DicomVolumeImage::GeometryReadyMessage>(*this, &Toto::Handle)); | |
1820 } | |
1821 | |
1599 void SetVolume1(int depth, | 1822 void SetVolume1(int depth, |
1600 OrthancStone::IVolumeSlicer* volume) | 1823 const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume, |
1601 { | 1824 OrthancStone::ILayerStyleConfigurator* style) |
1602 source1_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume)); | 1825 { |
1826 source1_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume)); | |
1827 | |
1828 if (style != NULL) | |
1829 { | |
1830 source1_->SetConfigurator(style); | |
1831 } | |
1603 } | 1832 } |
1604 | 1833 |
1605 void SetVolume2(int depth, | 1834 void SetVolume2(int depth, |
1606 OrthancStone::IVolumeSlicer* volume) | 1835 const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume, |
1607 { | 1836 OrthancStone::ILayerStyleConfigurator* style) |
1608 source2_.reset(new OrthancStone::VolumeSceneLayerSource(depth, volume)); | 1837 { |
1838 source2_.reset(new OrthancStone::VolumeSceneLayerSource(scene_, depth, volume)); | |
1839 | |
1840 if (style != NULL) | |
1841 { | |
1842 source2_->SetConfigurator(style); | |
1843 } | |
1609 } | 1844 } |
1610 }; | 1845 }; |
1611 | 1846 |
1612 | 1847 |
1613 void Run(OrthancStone::NativeApplicationContext& context, | 1848 void Run(OrthancStone::NativeApplicationContext& context, |
1614 OrthancStone::ThreadedOracle& oracle) | 1849 OrthancStone::ThreadedOracle& oracle) |
1615 { | 1850 { |
1851 boost::shared_ptr<OrthancStone::DicomVolumeImage> ct(new OrthancStone::DicomVolumeImage); | |
1852 boost::shared_ptr<OrthancStone::DicomVolumeImage> dose(new OrthancStone::DicomVolumeImage); | |
1853 | |
1854 | |
1616 boost::shared_ptr<Toto> toto; | 1855 boost::shared_ptr<Toto> toto; |
1617 boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> loader1, loader2; | 1856 boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> ctLoader; |
1618 boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> loader3; | 1857 boost::shared_ptr<OrthancStone::OrthancMultiframeVolumeLoader> doseLoader; |
1619 | 1858 |
1620 { | 1859 { |
1621 OrthancStone::NativeApplicationContext::WriterLock lock(context); | 1860 OrthancStone::NativeApplicationContext::WriterLock lock(context); |
1622 toto.reset(new Toto(oracle, lock.GetOracleObservable())); | 1861 toto.reset(new Toto(oracle, lock.GetOracleObservable())); |
1623 loader1.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable())); | 1862 ctLoader.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct, oracle, lock.GetOracleObservable())); |
1624 loader2.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(oracle, lock.GetOracleObservable())); | 1863 doseLoader.reset(new OrthancStone::OrthancMultiframeVolumeLoader(dose, oracle, lock.GetOracleObservable())); |
1625 loader3.reset(new OrthancStone::OrthancMultiframeVolumeLoader(oracle, lock.GetOracleObservable())); | 1864 } |
1865 | |
1866 | |
1867 toto->SetReferenceLoader(*ctLoader); | |
1868 | |
1869 | |
1870 #if 1 | |
1871 toto->SetVolume1(0, ctLoader, new OrthancStone::GrayscaleStyleConfigurator); | |
1872 #else | |
1873 { | |
1874 boost::shared_ptr<OrthancStone::IVolumeSlicer> reslicer(new OrthancStone::VolumeImageReslicer(ct)); | |
1875 toto->SetVolume1(0, reslicer, new OrthancStone::GrayscaleStyleConfigurator); | |
1876 } | |
1877 #endif | |
1878 | |
1879 | |
1880 { | |
1881 std::auto_ptr<OrthancStone::LookupTableStyleConfigurator> config(new OrthancStone::LookupTableStyleConfigurator); | |
1882 config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT); | |
1883 toto->SetVolume2(1, doseLoader, config.release()); | |
1626 } | 1884 } |
1627 | 1885 |
1628 oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100)); | 1886 oracle.Schedule(*toto, new OrthancStone::SleepOracleCommand(100)); |
1629 | 1887 |
1630 if (0) | 1888 if (0) |
1701 oracle.Schedule(*toto, command.release()); | 1959 oracle.Schedule(*toto, command.release()); |
1702 } | 1960 } |
1703 } | 1961 } |
1704 | 1962 |
1705 // 2017-11-17-Anonymized | 1963 // 2017-11-17-Anonymized |
1706 loader1->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT | 1964 ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618"); // CT |
1707 loader3->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE | 1965 doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6"); // RT-DOSE |
1708 | 1966 |
1709 // 2015-01-28-Multiframe | 1967 // 2015-01-28-Multiframe |
1710 //loader3->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279"); // Multiframe CT | 1968 //doseLoader->LoadInstance("88f71e2a-5fad1c61-96ed14d6-5b3d3cf7-a5825279"); // Multiframe CT |
1711 | 1969 |
1712 // Delphine | 1970 // Delphine |
1713 //loader1->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // CT | 1971 //ctLoader->LoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // CT |
1714 //loader1->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); // Lung 1/10mm | 1972 //ctLoader->LoadSeries("67f1b334-02c16752-45026e40-a5b60b6b-030ecab5"); // Lung 1/10mm |
1715 | 1973 |
1716 | 1974 |
1717 toto->SetVolume2(1, new OrthancStone::OrthancMultiframeVolumeLoader::MPRSlicer(loader3)); | 1975 { |
1718 toto->SetVolume1(0, new OrthancStone::OrthancSeriesVolumeProgressiveLoader::MPRSlicer(loader1)); | 1976 LOG(WARNING) << "...Waiting for Ctrl-C..."; |
1719 | 1977 |
1720 { | |
1721 oracle.Start(); | 1978 oracle.Start(); |
1722 | 1979 |
1723 LOG(WARNING) << "...Waiting for Ctrl-C..."; | |
1724 Orthanc::SystemToolbox::ServerBarrier(); | 1980 Orthanc::SystemToolbox::ServerBarrier(); |
1725 | 1981 |
1726 /** | 1982 /** |
1727 * WARNING => The oracle must be stopped BEFORE the objects using | 1983 * WARNING => The oracle must be stopped BEFORE the objects using |
1728 * it are destroyed!!! This forces to wait for the completion of | 1984 * it are destroyed!!! This forces to wait for the completion of |
1744 * to `SDL_main'". https://wiki.libsdl.org/FAQWindows | 2000 * to `SDL_main'". https://wiki.libsdl.org/FAQWindows |
1745 **/ | 2001 **/ |
1746 int main(int argc, char* argv[]) | 2002 int main(int argc, char* argv[]) |
1747 { | 2003 { |
1748 OrthancStone::StoneInitialize(); | 2004 OrthancStone::StoneInitialize(); |
1749 Orthanc::Logging::EnableInfoLevel(true); | 2005 //Orthanc::Logging::EnableInfoLevel(true); |
1750 | 2006 |
1751 try | 2007 try |
1752 { | 2008 { |
1753 OrthancStone::NativeApplicationContext context; | 2009 OrthancStone::NativeApplicationContext context; |
1754 | 2010 |