comparison Framework/Deprecated/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp @ 1225:16738485e457 broker

deprecating DicomStructureSetLoader, OrthancMultiframeVolumeLoader and OrthancSeriesVolumeProgressiveLoader
author Sebastien Jodogne <s.jodogne@gmail.com>
date Sun, 08 Dec 2019 11:45:09 +0100
parents Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp@34ee7204fde3
children 0ca50d275b9a
comparison
equal deleted inserted replaced
1224:37bc7f115f81 1225:16738485e457
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 "../../Loaders/BasicFetchingItemsSorter.h"
25 #include "../../Loaders/BasicFetchingStrategy.h"
26 #include "../../Toolbox/GeometryToolbox.h"
27 #include "../../Volumes/DicomVolumeImageMPRSlicer.h"
28
29 #include <Core/Images/ImageProcessing.h>
30 #include <Core/OrthancException.h>
31
32 namespace Deprecated
33 {
34 class OrthancSeriesVolumeProgressiveLoader::ExtractedSlice : public OrthancStone::DicomVolumeImageMPRSlicer::Slice
35 {
36 private:
37 const OrthancSeriesVolumeProgressiveLoader& that_;
38
39 public:
40 ExtractedSlice(const OrthancSeriesVolumeProgressiveLoader& that,
41 const OrthancStone::CoordinateSystem3D& plane) :
42 OrthancStone::DicomVolumeImageMPRSlicer::Slice(*that.volume_, plane),
43 that_(that)
44 {
45 if (IsValid())
46 {
47 if (GetProjection() == OrthancStone::VolumeProjection_Axial)
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 SetRevision(that_.seriesGeometry_.GetSliceRevision(GetSliceIndex()));
54 }
55
56 if (that_.strategy_.get() != NULL &&
57 GetProjection() == OrthancStone::VolumeProjection_Axial)
58 {
59 that_.strategy_->SetCurrent(GetSliceIndex());
60 }
61 }
62 }
63 };
64
65
66
67 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSlice(size_t index,
68 const OrthancStone::DicomInstanceParameters& reference) const
69 {
70 const OrthancStone::DicomInstanceParameters& slice = *slices_[index];
71
72 if (!OrthancStone::GeometryToolbox::IsParallel(
73 reference.GetGeometry().GetNormal(),
74 slice.GetGeometry().GetNormal()))
75 {
76 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
77 "A slice in the volume image is not parallel to the others");
78 }
79
80 if (reference.GetExpectedPixelFormat() != slice.GetExpectedPixelFormat())
81 {
82 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat,
83 "The pixel format changes across the slices of the volume image");
84 }
85
86 if (reference.GetImageInformation().GetWidth() != slice.GetImageInformation().GetWidth() ||
87 reference.GetImageInformation().GetHeight() != slice.GetImageInformation().GetHeight())
88 {
89 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize,
90 "The width/height of slices are not constant in the volume image");
91 }
92
93 if (!OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingX(), slice.GetPixelSpacingX()) ||
94 !OrthancStone::LinearAlgebra::IsNear(reference.GetPixelSpacingY(), slice.GetPixelSpacingY()))
95 {
96 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
97 "The pixel spacing of the slices change across the volume image");
98 }
99 }
100
101
102 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckVolume() const
103 {
104 for (size_t i = 0; i < slices_.size(); i++)
105 {
106 assert(slices_[i] != NULL);
107 if (slices_[i]->GetImageInformation().GetNumberOfFrames() != 1)
108 {
109 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
110 "This class does not support multi-frame images");
111 }
112 }
113
114 if (slices_.size() != 0)
115 {
116 const OrthancStone::DicomInstanceParameters& reference = *slices_[0];
117
118 for (size_t i = 1; i < slices_.size(); i++)
119 {
120 CheckSlice(i, reference);
121 }
122 }
123 }
124
125
126 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::Clear()
127 {
128 for (size_t i = 0; i < slices_.size(); i++)
129 {
130 assert(slices_[i] != NULL);
131 delete slices_[i];
132 }
133
134 slices_.clear();
135 slicesRevision_.clear();
136 }
137
138
139 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index) const
140 {
141 if (!HasGeometry())
142 {
143 LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::CheckSliceIndex(size_t index): (!HasGeometry())";
144 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
145 }
146 else if (index >= slices_.size())
147 {
148 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
149 }
150 else
151 {
152 assert(slices_.size() == GetImageGeometry().GetDepth() &&
153 slices_.size() == slicesRevision_.size());
154 }
155 }
156
157
158 // WARNING: The payload of "slices" must be of class "DicomInstanceParameters"
159 // (called with the slices created in LoadGeometry)
160 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::ComputeGeometry(OrthancStone::SlicesSorter& slices)
161 {
162 Clear();
163
164 if (!slices.Sort())
165 {
166 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
167 "Cannot sort the 3D slices of a DICOM series");
168 }
169
170 if (slices.GetSlicesCount() == 0)
171 {
172 geometry_.reset(new OrthancStone::VolumeImageGeometry);
173 }
174 else
175 {
176 slices_.reserve(slices.GetSlicesCount());
177 slicesRevision_.resize(slices.GetSlicesCount(), 0);
178
179 for (size_t i = 0; i < slices.GetSlicesCount(); i++)
180 {
181 const OrthancStone::DicomInstanceParameters& slice =
182 dynamic_cast<const OrthancStone::DicomInstanceParameters&>(slices.GetSlicePayload(i));
183 slices_.push_back(new OrthancStone::DicomInstanceParameters(slice));
184 }
185
186 CheckVolume();
187
188 double spacingZ;
189
190 if (slices.ComputeSpacingBetweenSlices(spacingZ))
191 {
192 LOG(INFO) << "Computed spacing between slices: " << spacingZ << "mm";
193
194 const OrthancStone::DicomInstanceParameters& parameters = *slices_[0];
195
196 geometry_.reset(new OrthancStone::VolumeImageGeometry);
197 geometry_->SetSizeInVoxels(parameters.GetImageInformation().GetWidth(),
198 parameters.GetImageInformation().GetHeight(),
199 static_cast<unsigned int>(slices.GetSlicesCount()));
200 geometry_->SetAxialGeometry(slices.GetSliceGeometry(0));
201 geometry_->SetVoxelDimensions(parameters.GetPixelSpacingX(),
202 parameters.GetPixelSpacingY(), spacingZ);
203 }
204 else
205 {
206 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadGeometry,
207 "The origins of the slices of a volume image are not regularly spaced");
208 }
209 }
210 }
211
212
213 const OrthancStone::VolumeImageGeometry& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry() const
214 {
215 if (!HasGeometry())
216 {
217 LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetImageGeometry(): (!HasGeometry())";
218 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
219 }
220 else
221 {
222 assert(slices_.size() == geometry_->GetDepth());
223 return *geometry_;
224 }
225 }
226
227
228 const OrthancStone::DicomInstanceParameters& OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceParameters(size_t index) const
229 {
230 CheckSliceIndex(index);
231 return *slices_[index];
232 }
233
234
235 uint64_t OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::GetSliceRevision(size_t index) const
236 {
237 CheckSliceIndex(index);
238 return slicesRevision_[index];
239 }
240
241
242 void OrthancSeriesVolumeProgressiveLoader::SeriesGeometry::IncrementSliceRevision(size_t index)
243 {
244 CheckSliceIndex(index);
245 slicesRevision_[index] ++;
246 }
247
248
249 static unsigned int GetSliceIndexPayload(const OrthancStone::OracleCommandBase& command)
250 {
251 assert(command.HasPayload());
252 return dynamic_cast< const Orthanc::SingleValueObject<unsigned int>& >(command.GetPayload()).GetValue();
253 }
254
255
256 void OrthancSeriesVolumeProgressiveLoader::ScheduleNextSliceDownload()
257 {
258 assert(strategy_.get() != NULL);
259
260 unsigned int sliceIndex, quality;
261
262 if (strategy_->GetNext(sliceIndex, quality))
263 {
264 assert(quality <= BEST_QUALITY);
265
266 const OrthancStone::DicomInstanceParameters& slice = seriesGeometry_.GetSliceParameters(sliceIndex);
267
268 const std::string& instance = slice.GetOrthancInstanceIdentifier();
269 if (instance.empty())
270 {
271 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
272 }
273
274 std::auto_ptr<OrthancStone::OracleCommandBase> command;
275
276 if (quality == BEST_QUALITY)
277 {
278 std::auto_ptr<OrthancStone::GetOrthancImageCommand> tmp(new OrthancStone::GetOrthancImageCommand);
279 // TODO: review the following comment.
280 // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases
281 // where gzipping the uint16 image took 11 sec to produce 5mb.
282 // The unzipped request was much much faster.
283 // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser
284 // does not use the Accept-Encoding header and always requests
285 // compression. Furthermore, NOT
286 tmp->SetHttpHeader("Accept-Encoding", "gzip");
287 tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
288 tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());
289 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
290 command.reset(tmp.release());
291 }
292 else
293 {
294 std::auto_ptr<OrthancStone::GetOrthancWebViewerJpegCommand> tmp(new OrthancStone::GetOrthancWebViewerJpegCommand);
295 // TODO: review the following comment. Commented out by bgo on 2019-07-19
296 // (gzip for jpeg seems overkill)
297 //tmp->SetHttpHeader("Accept-Encoding", "gzip");
298 tmp->SetInstance(instance);
299 tmp->SetQuality((quality == 0 ? 50 : 90));
300 tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
301 command.reset(tmp.release());
302 }
303
304 command->AcquirePayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
305
306 boost::shared_ptr<IObserver> observer(GetSharedObserver());
307 oracle_.Schedule(observer, command.release());
308 }
309 else
310 {
311 // loading is finished!
312 volumeImageReadyInHighQuality_ = true;
313 BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this));
314 }
315 }
316
317 /**
318 This is called in response to GET "/series/XXXXXXXXXXXXX/instances-tags"
319 */
320 void OrthancSeriesVolumeProgressiveLoader::LoadGeometry(const OrthancStone::OrthancRestApiCommand::SuccessMessage& message)
321 {
322 Json::Value body;
323 message.ParseJsonBody(body);
324
325 if (body.type() != Json::objectValue)
326 {
327 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
328 }
329
330 {
331 Json::Value::Members instances = body.getMemberNames();
332
333 OrthancStone::SlicesSorter slices;
334
335 for (size_t i = 0; i < instances.size(); i++)
336 {
337 Orthanc::DicomMap dicom;
338 dicom.FromDicomAsJson(body[instances[i]]);
339
340 std::auto_ptr<OrthancStone::DicomInstanceParameters> instance(new OrthancStone::DicomInstanceParameters(dicom));
341 instance->SetOrthancInstanceIdentifier(instances[i]);
342
343 // the 3D plane corresponding to the slice
344 OrthancStone::CoordinateSystem3D geometry = instance->GetGeometry();
345 slices.AddSlice(geometry, instance.release());
346 }
347
348 seriesGeometry_.ComputeGeometry(slices);
349 }
350
351 size_t slicesCount = seriesGeometry_.GetImageGeometry().GetDepth();
352
353 if (slicesCount == 0)
354 {
355 volume_->Initialize(seriesGeometry_.GetImageGeometry(), Orthanc::PixelFormat_Grayscale8);
356 }
357 else
358 {
359 const OrthancStone::DicomInstanceParameters& parameters = seriesGeometry_.GetSliceParameters(0);
360
361 volume_->Initialize(seriesGeometry_.GetImageGeometry(), parameters.GetExpectedPixelFormat());
362 volume_->SetDicomParameters(parameters);
363 volume_->GetPixelData().Clear();
364
365 strategy_.reset(new OrthancStone::BasicFetchingStrategy(sorter_->CreateSorter(static_cast<unsigned int>(slicesCount)), BEST_QUALITY));
366
367 assert(simultaneousDownloads_ != 0);
368 for (unsigned int i = 0; i < simultaneousDownloads_; i++)
369 {
370 ScheduleNextSliceDownload();
371 }
372 }
373
374 slicesQuality_.resize(slicesCount, 0);
375
376 BroadcastMessage(OrthancStone::DicomVolumeImage::GeometryReadyMessage(*volume_));
377 }
378
379
380 void OrthancSeriesVolumeProgressiveLoader::SetSliceContent(unsigned int sliceIndex,
381 const Orthanc::ImageAccessor& image,
382 unsigned int quality)
383 {
384 assert(sliceIndex < slicesQuality_.size() &&
385 slicesQuality_.size() == volume_->GetPixelData().GetDepth());
386
387 if (quality >= slicesQuality_[sliceIndex])
388 {
389 {
390 OrthancStone::ImageBuffer3D::SliceWriter writer(volume_->GetPixelData(), OrthancStone::VolumeProjection_Axial, sliceIndex);
391 Orthanc::ImageProcessing::Copy(writer.GetAccessor(), image);
392 }
393
394 volume_->IncrementRevision();
395 seriesGeometry_.IncrementSliceRevision(sliceIndex);
396 slicesQuality_[sliceIndex] = quality;
397
398 BroadcastMessage(OrthancStone::DicomVolumeImage::ContentUpdatedMessage(*volume_));
399 }
400
401 ScheduleNextSliceDownload();
402 }
403
404
405 void OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent(const OrthancStone::GetOrthancImageCommand::SuccessMessage& message)
406 {
407 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), BEST_QUALITY);
408 }
409
410
411 void OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent(const OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage& message)
412 {
413 unsigned int quality;
414
415 switch (dynamic_cast<const OrthancStone::GetOrthancWebViewerJpegCommand&>(message.GetOrigin()).GetQuality())
416 {
417 case 50:
418 quality = LOW_QUALITY;
419 break;
420
421 case 90:
422 quality = MIDDLE_QUALITY;
423 break;
424
425 default:
426 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
427 }
428
429 SetSliceContent(GetSliceIndexPayload(message.GetOrigin()), message.GetImage(), quality);
430 }
431
432
433 OrthancSeriesVolumeProgressiveLoader::OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<OrthancStone::DicomVolumeImage>& volume,
434 OrthancStone::IOracle& oracle,
435 OrthancStone::IObservable& oracleObservable) :
436 oracle_(oracle),
437 active_(false),
438 simultaneousDownloads_(4),
439 volume_(volume),
440 sorter_(new OrthancStone::BasicFetchingItemsSorter::Factory),
441 volumeImageReadyInHighQuality_(false)
442 {
443 // TODO => Move this out of constructor
444 Register<OrthancStone::OrthancRestApiCommand::SuccessMessage>
445 (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadGeometry);
446
447 Register<OrthancStone::GetOrthancImageCommand::SuccessMessage>
448 (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadBestQualitySliceContent);
449
450 Register<OrthancStone::GetOrthancWebViewerJpegCommand::SuccessMessage>
451 (oracleObservable, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent);
452 }
453
454 OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()
455 {
456 LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()";
457 }
458
459 void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count)
460 {
461 if (active_)
462 {
463 LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(): (active_)";
464 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
465 }
466 else if (count == 0)
467 {
468 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
469 }
470 else
471 {
472 simultaneousDownloads_ = count;
473 }
474 }
475
476
477 void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId)
478 {
479 // LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries seriesId=" << seriesId;
480 if (active_)
481 {
482 // LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries NOT ACTIVE! --> ERROR";
483 LOG(ERROR) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId): (active_)";
484 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
485 }
486 else
487 {
488 active_ = true;
489
490 std::auto_ptr<OrthancStone::OrthancRestApiCommand> command(new OrthancStone::OrthancRestApiCommand);
491 command->SetUri("/series/" + seriesId + "/instances-tags");
492
493 // LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries about to call oracle_.Schedule";
494 boost::shared_ptr<IObserver> observer(GetSharedObserver());
495 oracle_.Schedule(observer, command.release());
496 // LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries called oracle_.Schedule";
497 }
498 }
499
500
501 OrthancStone::IVolumeSlicer::IExtractedSlice*
502 OrthancSeriesVolumeProgressiveLoader::ExtractSlice(const OrthancStone::CoordinateSystem3D& cuttingPlane)
503 {
504 if (volume_->HasGeometry())
505 {
506 return new ExtractedSlice(*this, cuttingPlane);
507 }
508 else
509 {
510 return new IVolumeSlicer::InvalidSlice;
511 }
512 }
513 }