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