814
|
1 /**
|
|
2 * Stone of Orthanc
|
|
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
|
4 * Department, University Hospital of Liege, Belgium
|
|
5 * Copyright (C) 2017-2019 Osimis S.A., Belgium
|
|
6 *
|
|
7 * This program is free software: you can redistribute it and/or
|
|
8 * modify it under the terms of the GNU Affero General Public License
|
|
9 * as published by the Free Software Foundation, either version 3 of
|
|
10 * the License, or (at your option) any later version.
|
|
11 *
|
|
12 * This program is distributed in the hope that it will be useful, but
|
|
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
15 * Affero General Public License for more details.
|
|
16 *
|
|
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/>.
|
|
19 **/
|
|
20
|
|
21
|
|
22 #include "OrthancSeriesVolumeProgressiveLoader.h"
|
|
23
|
|
24 #include "../Toolbox/GeometryToolbox.h"
|
|
25 #include "../Volumes/DicomVolumeImageMPRSlicer.h"
|
|
26 #include "BasicFetchingItemsSorter.h"
|
|
27 #include "BasicFetchingStrategy.h"
|
|
28
|
|
29 #include <Core/Images/ImageProcessing.h>
|
|
30 #include <Core/OrthancException.h>
|
|
31
|
|
32 namespace OrthancStone
|
|
33 {
|
|
34 class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public DicomVolumeImageMPRSlicer::Slice
|
|
35 {
|
|
36 private:
|
|
37 const OrthancSeriesVolumeProgressiveLoader& that_;
|
|
38
|
|
39 protected:
|
|
40 virtual uint64_t GetRevisionInternal(VolumeProjection projection,
|
|
41 unsigned int sliceIndex) const
|
|
42 {
|
|
43 if (projection == VolumeProjection_Axial)
|
|
44 {
|
|
45 return that_.seriesGeometry_.GetSliceRevision(sliceIndex);
|
|
46 }
|
|
47 else
|
|
48 {
|
|
49 // For coronal and sagittal projections, we take the global
|
|
50 // revision of the volume because even if a single slice changes,
|
|
51 // this means the projection will yield a different result -->
|
|
52 // we must increase the revision as soon as any slice changes
|
|
53 return that_.volume_->GetRevision();
|
|
54 }
|
|
55 }
|
|
56
|
|
57 public:
|
|
58 ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
|
|
59 const CoordinateSystem3D& plane) :
|
|
60 DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
|
|
61 that_(that)
|
|
62 {
|
|
63 if (that_.strategy_.get() != NULL &&
|
|
64 IsValid() &&
|
|
65 GetProjection() == VolumeProjection_Axial)
|
|
66 {
|
|
67 that_.strategy_->SetCurrent(GetSliceIndex());
|
|
68 }
|
|
69 }
|
|
70 };
|
|
71
|
|
72
|
|
73
|
|
74 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index,
|
|
75 const DicomInstanceParameters& reference) const
|
|
76 {
|
|
77 const DicomInstanceParameters& slice = *slices_[index];
|
|
78
|
|
79 if (!GeometryToolbox::IsParallel(
|
|
80 reference.GetGeometry().GetNormal(),
|
|
81 slice.GetGeometry().GetNormal()))
|
|
82 {
|
|
83 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
|
|
84 "A slice in the volume image is not parallel to the others");
|
|
85 }
|
|
86
|
|
87 if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
|
|
88 {
|
|
89 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
|
|
90 "The pixel format changes across the slices of the volume image");
|
|
91 }
|
|
92
|
|
93 if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
|
|
94 reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
|
|
95 {
|
|
96 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
|
|
97 "The width/height of slices are not constant in the volume image");
|
|
98 }
|
|
99
|
|
100 if (!LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
|
|
101 !LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
|
|
102 {
|
|
103 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
|
|
104 "The pixel spacing of the slices change across the volume image");
|
|
105 }
|
|
106 }
|
|
107
|
|
108
|
|
109 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const
|
|
110 {
|
|
111 for (size_t i = 0; i < slices_.size(); i++)
|
|
112 {
|
|
113 assert(slices_[i] != NULL);
|
|
114 if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
|
|
115 {
|
|
116 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
|
|
117 "This class does not support multi-frame images");
|
|
118 }
|
|
119 }
|
|
120
|
|
121 if (slices_.size() != 0)
|
|
122 {
|
|
123 const DicomInstanceParameters& reference = *slices_[0];
|
|
124
|
|
125 for (size_t i = 1; i < slices_.size(); i++)
|
|
126 {
|
|
127 CheckSlice(i, reference);
|
|
128 }
|
|
129 }
|
|
130 }
|
|
131
|
|
132
|
|
133 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear()
|
|
134 {
|
|
135 for (size_t i = 0; i < slices_.size(); i++)
|
|
136 {
|
|
137 assert(slices_[i] != NULL);
|
|
138 delete slices_[i];
|
|
139 }
|
|
140
|
|
141 slices_.clear();
|
|
142 slicesRevision_.clear();
|
|
143 }
|
|
144
|
|
145
|
|
146 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const
|
|
147 {
|
|
148 if (!HasGeometry())
|
|
149 {
|
|
150 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
|
|
151 }
|
|
152 else if (index >= slices_.size())
|
|
153 {
|
|
154 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
|
|
155 }
|
|
156 else
|
|
157 {
|
|
158 assert(slices_.size() == GetImageGeometry().GetDepth() &&
|
|
159 slices_.size() == slicesRevision_.size());
|
|
160 }
|
|
161 }
|
|
162
|
|
163
|
|
164 // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
|
|
165 // (called with the slices created in LoadGeometry)
|
|
166 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(SlicesSorter& slices)
|
|
167 {
|
|
168 Clear();
|
|
169
|
|
170 if (!slices.Sort())
|
|
171 {
|
|
172 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
|
|
173 "Cannot sort the 3D slices of a DICOM series");
|
|
174 }
|
|
175
|
|
176 if (slices.GetSlicesCount() == 0)
|
|
177 {
|
|
178 geometry_.reset(new VolumeImageGeometry);
|
|
179 }
|
|
180 else
|
|
181 {
|
|
182 slices_.reserve(slices.GetSlicesCount());
|
|
183 slicesRevision_.resize(slices.GetSlicesCount(), 0);
|
|
184
|
|
185 for (size_t i = 0; i < slices.GetSlicesCount(); i++)
|
|
186 {
|
|
187 const DicomInstanceParameters& slice =
|
|
188 dynamic_cast<const DicomInstanceParameters&>(slices.GetSlicePayload(i));
|
|
189 slices_.push_back(new DicomInstanceParameters(slice));
|
|
190 }
|
|
191
|
|
192 CheckVolume();
|
|
193
|
|
194 const double spacingZ = slices.ComputeSpacingBetweenSlices();
|
|
195 LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
|
|
196
|
|
197 const DicomInstanceParameters& parameters = *slices_[0];
|
|
198
|
|
199 geometry_.reset(new VolumeImageGeometry);
|
|
200 geometry_->SetSize(parameters.GetImageInformation().GetWidth(),
|
|
201 parameters.GetImageInformation().GetHeight(),
|
|
202 static_cast<unsigned int>(slices.GetSlicesCount()));
|
|
203 geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
|
|
204 geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
|
|
205 parameters.GetPixelSpacingY(), spacingZ);
|
|
206 }
|
|
207 }
|
|
208
|
|
209
|
|
210 const VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
|
|
211 {
|
|
212 if (!HasGeometry())
|
|
213 {
|
|
214 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
|
|
215 }
|
|
216 else
|
|
217 {
|
|
218 assert(slices_.size() == geometry_->GetDepth());
|
|
219 return *geometry_;
|
|
220 }
|
|
221 }
|
|
222
|
|
223
|
|
224 const DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const
|
|
225 {
|
|
226 CheckSliceIndex(index);
|
|
227 return *slices_[index];
|
|
228 }
|
|
229
|
|
230
|
|
231 uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const
|
|
232 {
|
|
233 CheckSliceIndex(index);
|
|
234 return slicesRevision_[index];
|
|
235 }
|
|
236
|
|
237
|
|
238 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index)
|
|
239 {
|
|
240 CheckSliceIndex(index);
|
|
241 slicesRevision_[index] ++;
|
|
242 }
|
|
243
|
|
244
|
|
245 static unsigned int GetSliceIndexPayload(const OracleCommandWithPayload& command)
|
|
246 {
|
|
247 return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
|
|
248 }
|
|
249
|
|
250
|
|
251 void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
|
|
252 {
|
|
253 assert(strategy_.get() != NULL);
|
|
254
|
|
255 unsigned int sliceIndex, quality;
|
|
256
|
|
257 if (strategy_->GetNext(sliceIndex, quality))
|
|
258 {
|
|
259 assert(quality <= BEST_QUALITY);
|
|
260
|
|
261 const DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
|
|
262
|
|
263 const std::string& instance = slice.GetOrthancInstanceIdentifier();
|
|
264 if (instance.empty())
|
|
265 {
|
|
266 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
267 }
|
|
268
|
|
269 std::auto_ptr<OracleCommandWithPayload> command;
|
|
270
|
|
271 if (quality == BEST_QUALITY)
|
|
272 {
|
|
273 std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand);
|
|
274 tmp->SetHttpHeader("Accept-Encoding", "gzip");
|
|
275 tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
|
|
276 tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());
|
|
277 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
|
|
278 command.reset(tmp.release());
|
|
279 }
|
|
280 else
|
|
281 {
|
|
282 std::auto_ptr<GetOrthancWebViewerJpegCommand> tmp(new GetOrthancWebViewerJpegCommand);
|
|
283 tmp->SetHttpHeader("Accept-Encoding", "gzip");
|
|
284 tmp->SetInstance(instance);
|
|
285 tmp->SetQuality((quality == 0 ? 50 : 90));
|
|
286 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
|
|
287 command.reset(tmp.release());
|
|
288 }
|
|
289
|
|
290 command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
|
|
291 oracle_.Schedule(*this, command.release());
|
|
292 }
|
|
293 }
|
|
294
|
|
295 /**
|
|
296 This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
|
|
297 */
|
|
298 void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancRestApiCommand::SuccessMessage& message)
|
|
299 {
|
|
300 Json::Value body;
|
|
301 message.ParseJsonBody(body);
|
|
302
|
|
303 if (body.type() != Json::objectValue)
|
|
304 {
|
|
305 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
|
|
306 }
|
|
307
|
|
308 {
|
|
309 Json::Value::Members instances = body.getMemberNames();
|
|
310
|
|
311 SlicesSorter slices;
|
|
312
|
|
313 for (size_t i = 0; i < instances.size(); i++)
|
|
314 {
|
|
315 Orthanc::DicomMap dicom;
|
|
316 dicom.FromDicomAsJson(body[instances[i]]);
|
|
317
|
|
318 std::auto_ptr<DicomInstanceParameters> instance(new DicomInstanceParameters(dicom));
|
|
319 instance->SetOrthancInstanceIdentifier(instances[i]);
|
|
320
|
|
321 // the 3D plane corresponding to the slice
|
|
322 CoordinateSystem3D geometry = instance->GetGeometry();
|
|
323 slices.AddSlice(geometry, instance.release());
|
|
324 }
|
|
325
|
|
326 seriesGeometry_.ComputeGeometry(slices);
|
|
327 }
|
|
328
|
|
329 size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth();
|
|
330
|
|
331 if (slicesCount == 0)
|
|
332 {
|
|
333 volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8);
|
|
334 }
|
|
335 else
|
|
336 {
|
|
337 const DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
|
|
338
|
|
339 volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
|
|
340 volume_->SetDicomParameters(parameters);
|
|
341 volume_->GetPixelData().Clear();
|
|
342
|
|
343 strategy_.reset(new BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY));
|
|
344
|
|
345 assert(simultaneousDownloads_ != 0);
|
|
346 for (unsigned int i = 0; i < simultaneousDownloads_; i++)
|
|
347 {
|
|
348 ScheduleNextSliceDownload();
|
|
349 }
|
|
350 }
|
|
351
|
|
352 slicesQuality_.resize(slicesCount, 0);
|
|
353
|
|
354 BroadcastMessage(DicomVolumeImage::GeometryReadyMessage(*volume_));
|
|
355 }
|
|
356
|
|
357
|
|
358 void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex,
|
|
359 const Orthanc::ImageAccessor& image,
|
|
360 unsigned int quality)
|
|
361 {
|
|
362 assert(sliceIndex < slicesQuality_.size() &&
|
|
363 slicesQuality_.size() == volume_->GetPixelData().GetDepth());
|
|
364
|
|
365 if (quality >= slicesQuality_[sliceIndex])
|
|
366 {
|
|
367 {
|
|
368 ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), VolumeProjection_Axial, sliceIndex);
|
|
369 Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
|
|
370 }
|
|
371
|
|
372 volume_->IncrementRevision();
|
|
373 seriesGeometry_.IncrementSliceRevision(sliceIndex);
|
|
374 slicesQuality_[sliceIndex] = quality;
|
|
375
|
|
376 BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
|
|
377 }
|
|
378
|
|
379 ScheduleNextSliceDownload();
|
|
380 }
|
|
381
|
|
382
|
|
383 void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const GetOrthancImageCommand::SuccessMessage& message)
|
|
384 {
|
|
385 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
|
|
386 }
|
|
387
|
|
388
|
|
389 void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message)
|
|
390 {
|
|
391 unsigned int quality;
|
|
392
|
|
393 switch (message.GetOrigin().GetQuality())
|
|
394 {
|
|
395 case 50:
|
|
396 quality = LOW_QUALITY;
|
|
397 break;
|
|
398
|
|
399 case 90:
|
|
400 quality = MIDDLE_QUALITY;
|
|
401 break;
|
|
402
|
|
403 default:
|
|
404 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
405 }
|
|
406
|
|
407 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
|
|
408 }
|
|
409
|
|
410
|
|
411 OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
|
|
412 IOracle& oracle,
|
|
413 IObservable& oracleObservable) :
|
|
414 IObserver(oracleObservable.GetBroker()),
|
|
415 IObservable(oracleObservable.GetBroker()),
|
|
416 oracle_(oracle),
|
|
417 active_(false),
|
|
418 simultaneousDownloads_(4),
|
|
419 volume_(volume),
|
|
420 sorter_(new BasicFetchingItemsSorter::Factory)
|
|
421 {
|
|
422 oracleObservable.RegisterObserverCallback(
|
|
423 new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
|
|
424 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry));
|
|
425
|
|
426 oracleObservable.RegisterObserverCallback(
|
|
427 new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancImageCommand::SuccessMessage>
|
|
428 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent));
|
|
429
|
|
430 oracleObservable.RegisterObserverCallback(
|
|
431 new Callable<OrthancSeriesVolumeProgressiveLoader, GetOrthancWebViewerJpegCommand::SuccessMessage>
|
|
432 (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent));
|
|
433 }
|
|
434
|
|
435
|
|
436 void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count)
|
|
437 {
|
|
438 if (active_)
|
|
439 {
|
|
440 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
|
|
441 }
|
|
442 else if (count == 0)
|
|
443 {
|
|
444 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
|
|
445 }
|
|
446 else
|
|
447 {
|
|
448 simultaneousDownloads_ = count;
|
|
449 }
|
|
450 }
|
|
451
|
|
452
|
|
453 void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId)
|
|
454 {
|
|
455 if (active_)
|
|
456 {
|
|
457 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
|
|
458 }
|
|
459 else
|
|
460 {
|
|
461 active_ = true;
|
|
462
|
|
463 std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
|
|
464 command->SetUri("/series/" + seriesId + "/instances-tags");
|
|
465
|
|
466 oracle_.Schedule(*this, command.release());
|
|
467 }
|
|
468 }
|
|
469
|
|
470
|
|
471 IVolumeSlicer::IExtractedSlice*
|
|
472 OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
|
|
473 {
|
|
474 if (volume_->HasGeometry())
|
|
475 {
|
|
476 return new ExtractedSlice(*this, cuttingPlane);
|
|
477 }
|
|
478 else
|
|
479 {
|
|
480 return new IVolumeSlicer::InvalidSlice;
|
|
481 }
|
|
482 }
|
|
483 }
|