Mercurial > hg > orthanc-stone
comparison Framework/Loaders/SeriesFramesLoader.cpp @ 1228:c471a0aa137b broker
adding the next generation of loaders
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 09 Dec 2019 13:58:37 +0100 |
parents | |
children | 0ca50d275b9a |
comparison
equal
deleted
inserted
replaced
1227:a1c0c9c9f9af | 1228:c471a0aa137b |
---|---|
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 "SeriesFramesLoader.h" | |
23 | |
24 #include "../Oracle/ParseDicomFromFileCommand.h" | |
25 #include "../Oracle/ParseDicomFromWadoCommand.h" | |
26 | |
27 #if ORTHANC_ENABLE_DCMTK == 1 | |
28 # include <Core/DicomParsing/Internals/DicomImageDecoder.h> | |
29 #endif | |
30 | |
31 #include <Core/DicomFormat/DicomInstanceHasher.h> | |
32 #include <Core/Images/Image.h> | |
33 #include <Core/Images/ImageProcessing.h> | |
34 #include <Core/Images/JpegReader.h> | |
35 | |
36 #include <boost/algorithm/string/predicate.hpp> | |
37 | |
38 namespace OrthancStone | |
39 { | |
40 class SeriesFramesLoader::Payload : public Orthanc::IDynamicObject | |
41 { | |
42 private: | |
43 DicomSource source_; | |
44 size_t seriesIndex_; | |
45 std::string sopInstanceUid_; // Only used for debug purpose | |
46 unsigned int quality_; | |
47 bool hasWindowing_; | |
48 float windowingCenter_; | |
49 float windowingWidth_; | |
50 std::auto_ptr<Orthanc::IDynamicObject> userPayload_; | |
51 | |
52 public: | |
53 Payload(const DicomSource& source, | |
54 size_t seriesIndex, | |
55 const std::string& sopInstanceUid, | |
56 unsigned int quality, | |
57 Orthanc::IDynamicObject* userPayload) : | |
58 source_(source), | |
59 seriesIndex_(seriesIndex), | |
60 sopInstanceUid_(sopInstanceUid), | |
61 quality_(quality), | |
62 hasWindowing_(false), | |
63 userPayload_(userPayload) | |
64 { | |
65 } | |
66 | |
67 size_t GetSeriesIndex() const | |
68 { | |
69 return seriesIndex_; | |
70 } | |
71 | |
72 const std::string& GetSopInstanceUid() const | |
73 { | |
74 return sopInstanceUid_; | |
75 } | |
76 | |
77 unsigned int GetQuality() const | |
78 { | |
79 return quality_; | |
80 } | |
81 | |
82 void SetWindowing(float center, | |
83 float width) | |
84 { | |
85 hasWindowing_ = true; | |
86 windowingCenter_ = center; | |
87 windowingWidth_ = width; | |
88 } | |
89 | |
90 bool HasWindowing() const | |
91 { | |
92 return hasWindowing_; | |
93 } | |
94 | |
95 float GetWindowingCenter() const | |
96 { | |
97 if (hasWindowing_) | |
98 { | |
99 return windowingCenter_; | |
100 } | |
101 else | |
102 { | |
103 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
104 } | |
105 } | |
106 | |
107 float GetWindowingWidth() const | |
108 { | |
109 if (hasWindowing_) | |
110 { | |
111 return windowingWidth_; | |
112 } | |
113 else | |
114 { | |
115 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
116 } | |
117 } | |
118 | |
119 const DicomSource& GetSource() const | |
120 { | |
121 return source_; | |
122 } | |
123 | |
124 Orthanc::IDynamicObject* GetUserPayload() const | |
125 { | |
126 return userPayload_.get(); | |
127 } | |
128 }; | |
129 | |
130 | |
131 SeriesFramesLoader::SeriesFramesLoader(ILoadersContext& context, | |
132 LoadedDicomResources& instances, | |
133 const std::string& dicomDirPath, | |
134 boost::shared_ptr<LoadedDicomResources> dicomDir) : | |
135 context_(context), | |
136 frames_(instances), | |
137 dicomDirPath_(dicomDirPath), | |
138 dicomDir_(dicomDir) | |
139 { | |
140 } | |
141 | |
142 | |
143 void SeriesFramesLoader::EmitMessage(const Payload& payload, | |
144 const Orthanc::ImageAccessor& image) | |
145 { | |
146 const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); | |
147 const Orthanc::DicomMap& instance = frames_.GetInstance(payload.GetSeriesIndex()); | |
148 size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); | |
149 | |
150 if (frameIndex >= parameters.GetImageInformation().GetNumberOfFrames() || | |
151 payload.GetSopInstanceUid() != parameters.GetSopInstanceUid()) | |
152 { | |
153 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
154 } | |
155 | |
156 LOG(TRACE) << "Decoded instance " << payload.GetSopInstanceUid() << ", frame " | |
157 << frameIndex << ": " << image.GetWidth() << "x" | |
158 << image.GetHeight() << ", " << Orthanc::EnumerationToString(image.GetFormat()) | |
159 << ", quality " << payload.GetQuality(); | |
160 | |
161 FrameLoadedMessage message(*this, frameIndex, payload.GetQuality(), image, instance, parameters, payload.GetUserPayload()); | |
162 BroadcastMessage(message); | |
163 } | |
164 | |
165 | |
166 #if ORTHANC_ENABLE_DCMTK == 1 | |
167 void SeriesFramesLoader::HandleDicom(const Payload& payload, | |
168 Orthanc::ParsedDicomFile& dicom) | |
169 { | |
170 size_t frameIndex = frames_.GetFrameIndex(payload.GetSeriesIndex()); | |
171 | |
172 std::auto_ptr<Orthanc::ImageAccessor> decoded; | |
173 decoded.reset(Orthanc::DicomImageDecoder::Decode(dicom, frameIndex)); | |
174 | |
175 if (decoded.get() == NULL) | |
176 { | |
177 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); | |
178 } | |
179 | |
180 EmitMessage(payload, *decoded); | |
181 } | |
182 #endif | |
183 | |
184 | |
185 void SeriesFramesLoader::HandleDicomWebRendered(const Payload& payload, | |
186 const std::string& body, | |
187 const std::map<std::string, std::string>& headers) | |
188 { | |
189 assert(payload.GetSource().IsDicomWeb() && | |
190 payload.HasWindowing()); | |
191 | |
192 bool ok = false; | |
193 for (std::map<std::string, std::string>::const_iterator it = headers.begin(); | |
194 it != headers.end(); ++it) | |
195 { | |
196 if (boost::iequals("content-type", it->first) && | |
197 boost::iequals(Orthanc::MIME_JPEG, it->second)) | |
198 { | |
199 ok = true; | |
200 break; | |
201 } | |
202 } | |
203 | |
204 if (!ok) | |
205 { | |
206 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol, | |
207 "The WADO-RS server has not generated a JPEG image on /rendered"); | |
208 } | |
209 | |
210 Orthanc::JpegReader reader; | |
211 reader.ReadFromMemory(body); | |
212 | |
213 switch (reader.GetFormat()) | |
214 { | |
215 case Orthanc::PixelFormat_RGB24: | |
216 EmitMessage(payload, reader); | |
217 break; | |
218 | |
219 case Orthanc::PixelFormat_Grayscale8: | |
220 { | |
221 const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(payload.GetSeriesIndex()); | |
222 | |
223 Orthanc::Image scaled(parameters.GetExpectedPixelFormat(), reader.GetWidth(), reader.GetHeight(), false); | |
224 Orthanc::ImageProcessing::Convert(scaled, reader); | |
225 | |
226 float w = payload.GetWindowingWidth(); | |
227 if (w <= 0.01f) | |
228 { | |
229 w = 0.01; // Prevent division by zero | |
230 } | |
231 | |
232 const float c = payload.GetWindowingCenter(); | |
233 const float scaling = w / 255.0f; | |
234 const float offset = (c - w / 2.0f) / scaling; | |
235 | |
236 Orthanc::ImageProcessing::ShiftScale(scaled, offset, scaling, false /* truncation to speed up */); | |
237 EmitMessage(payload, scaled); | |
238 break; | |
239 } | |
240 | |
241 default: | |
242 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
243 } | |
244 } | |
245 | |
246 | |
247 #if ORTHANC_ENABLE_DCMTK == 1 | |
248 void SeriesFramesLoader::Handle(const ParseDicomSuccessMessage& message) | |
249 { | |
250 assert(message.GetOrigin().HasPayload()); | |
251 | |
252 const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); | |
253 if ((payload.GetSource().IsDicomDir() || | |
254 payload.GetSource().IsDicomWeb()) && | |
255 message.HasPixelData()) | |
256 { | |
257 HandleDicom(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), message.GetDicom()); | |
258 } | |
259 else | |
260 { | |
261 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
262 } | |
263 } | |
264 #endif | |
265 | |
266 | |
267 void SeriesFramesLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message) | |
268 { | |
269 assert(message.GetOrigin().HasPayload()); | |
270 | |
271 const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); | |
272 assert(payload.GetSource().IsOrthanc()); | |
273 | |
274 EmitMessage(payload, message.GetImage()); | |
275 } | |
276 | |
277 | |
278 void SeriesFramesLoader::Handle(const GetOrthancWebViewerJpegCommand::SuccessMessage& message) | |
279 { | |
280 assert(message.GetOrigin().HasPayload()); | |
281 | |
282 const Payload& payload = dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()); | |
283 assert(payload.GetSource().IsOrthanc()); | |
284 | |
285 EmitMessage(payload, message.GetImage()); | |
286 } | |
287 | |
288 | |
289 void SeriesFramesLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message) | |
290 { | |
291 // This is to handle "/rendered" in DICOMweb | |
292 assert(message.GetOrigin().HasPayload()); | |
293 HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), | |
294 message.GetAnswer(), message.GetAnswerHeaders()); | |
295 } | |
296 | |
297 | |
298 void SeriesFramesLoader::Handle(const HttpCommand::SuccessMessage& message) | |
299 { | |
300 // This is to handle "/rendered" in DICOMweb | |
301 assert(message.GetOrigin().HasPayload()); | |
302 HandleDicomWebRendered(dynamic_cast<const Payload&>(message.GetOrigin().GetPayload()), | |
303 message.GetAnswer(), message.GetAnswerHeaders()); | |
304 } | |
305 | |
306 | |
307 void SeriesFramesLoader::GetPreviewWindowing(float& center, | |
308 float& width, | |
309 size_t index) const | |
310 { | |
311 const Orthanc::DicomMap& instance = frames_.GetInstance(index); | |
312 const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); | |
313 | |
314 if (parameters.HasDefaultWindowing()) | |
315 { | |
316 // TODO - Handle multiple presets (take the largest width) | |
317 center = parameters.GetDefaultWindowingCenter(); | |
318 width = parameters.GetDefaultWindowingWidth(); | |
319 } | |
320 else | |
321 { | |
322 float a, b; | |
323 if (instance.ParseFloat(a, Orthanc::DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE) && | |
324 instance.ParseFloat(b, Orthanc::DICOM_TAG_LARGEST_IMAGE_PIXEL_VALUE) && | |
325 a < b) | |
326 { | |
327 center = (a + b) / 2.0f; | |
328 width = (b - a); | |
329 } | |
330 else | |
331 { | |
332 // Cannot infer a suitable windowing from the available tags | |
333 center = 128.0f; | |
334 width = 256.0f; | |
335 } | |
336 } | |
337 } | |
338 | |
339 | |
340 Orthanc::IDynamicObject& SeriesFramesLoader::FrameLoadedMessage::GetUserPayload() const | |
341 { | |
342 if (userPayload_) | |
343 { | |
344 return *userPayload_; | |
345 } | |
346 else | |
347 { | |
348 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | |
349 } | |
350 } | |
351 | |
352 | |
353 void SeriesFramesLoader::Factory::SetDicomDir(const std::string& dicomDirPath, | |
354 boost::shared_ptr<LoadedDicomResources> dicomDir) | |
355 { | |
356 dicomDirPath_ = dicomDirPath; | |
357 dicomDir_ = dicomDir; | |
358 } | |
359 | |
360 | |
361 boost::shared_ptr<IObserver> SeriesFramesLoader::Factory::Create(ILoadersContext::ILock& stone) | |
362 { | |
363 boost::shared_ptr<SeriesFramesLoader> loader( | |
364 new SeriesFramesLoader(stone.GetContext(), instances_, dicomDirPath_, dicomDir_)); | |
365 loader->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); | |
366 loader->Register<GetOrthancWebViewerJpegCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); | |
367 loader->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); | |
368 loader->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); | |
369 | |
370 #if ORTHANC_ENABLE_DCMTK == 1 | |
371 loader->Register<ParseDicomSuccessMessage>(stone.GetOracleObservable(), &SeriesFramesLoader::Handle); | |
372 #endif | |
373 | |
374 return loader; | |
375 } | |
376 | |
377 | |
378 void SeriesFramesLoader::ScheduleLoadFrame(int priority, | |
379 const DicomSource& source, | |
380 size_t index, | |
381 unsigned int quality, | |
382 Orthanc::IDynamicObject* userPayload) | |
383 { | |
384 std::auto_ptr<Orthanc::IDynamicObject> protection(userPayload); | |
385 | |
386 if (index >= frames_.GetFramesCount() || | |
387 quality >= source.GetQualityCount()) | |
388 { | |
389 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
390 } | |
391 | |
392 const Orthanc::DicomMap& instance = frames_.GetInstance(index); | |
393 | |
394 std::string sopInstanceUid; | |
395 if (!instance.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) | |
396 { | |
397 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, | |
398 "Missing SOPInstanceUID in a DICOM instance"); | |
399 } | |
400 | |
401 if (source.IsDicomDir()) | |
402 { | |
403 if (dicomDir_.get() == NULL) | |
404 { | |
405 // Should have been set in the factory | |
406 throw Orthanc::OrthancException( | |
407 Orthanc::ErrorCode_BadSequenceOfCalls, | |
408 "SeriesFramesLoader::Factory::SetDicomDir() should have been called"); | |
409 } | |
410 | |
411 assert(quality == 0); | |
412 | |
413 std::string file; | |
414 if (dicomDir_->LookupStringValue(file, sopInstanceUid, Orthanc::DICOM_TAG_REFERENCED_FILE_ID)) | |
415 { | |
416 std::auto_ptr<ParseDicomFromFileCommand> command(new ParseDicomFromFileCommand(dicomDirPath_, file)); | |
417 command->SetPixelDataIncluded(true); | |
418 command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); | |
419 | |
420 { | |
421 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock()); | |
422 lock->Schedule(GetSharedObserver(), priority, command.release()); | |
423 } | |
424 } | |
425 else | |
426 { | |
427 LOG(WARNING) << "Missing tag ReferencedFileID in a DICOMDIR entry"; | |
428 } | |
429 } | |
430 else if (source.IsDicomWeb()) | |
431 { | |
432 std::string studyInstanceUid, seriesInstanceUid; | |
433 if (!instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || | |
434 !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) | |
435 { | |
436 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, | |
437 "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); | |
438 } | |
439 | |
440 const std::string uri = ("/studies/" + studyInstanceUid + | |
441 "/series/" + seriesInstanceUid + | |
442 "/instances/" + sopInstanceUid); | |
443 | |
444 if (source.HasDicomWebRendered() && | |
445 quality == 0) | |
446 { | |
447 float c, w; | |
448 GetPreviewWindowing(c, w, index); | |
449 | |
450 std::map<std::string, std::string> arguments, headers; | |
451 arguments["window"] = (boost::lexical_cast<std::string>(c) + "," + | |
452 boost::lexical_cast<std::string>(w) + ",linear"); | |
453 headers["Accept"] = "image/jpeg"; | |
454 | |
455 std::auto_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); | |
456 payload->SetWindowing(c, w); | |
457 | |
458 { | |
459 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock()); | |
460 lock->Schedule(GetSharedObserver(), priority, | |
461 source.CreateDicomWebCommand(uri + "/rendered", arguments, headers, payload.release())); | |
462 } | |
463 } | |
464 else | |
465 { | |
466 assert((source.HasDicomWebRendered() && quality == 1) || | |
467 (!source.HasDicomWebRendered() && quality == 0)); | |
468 | |
469 #if ORTHANC_ENABLE_DCMTK == 1 | |
470 std::auto_ptr<Payload> payload(new Payload(source, index, sopInstanceUid, quality, protection.release())); | |
471 | |
472 const std::map<std::string, std::string> empty; | |
473 | |
474 std::auto_ptr<ParseDicomFromWadoCommand> command( | |
475 new ParseDicomFromWadoCommand(sopInstanceUid, source.CreateDicomWebCommand(uri, empty, empty, NULL))); | |
476 command->AcquirePayload(payload.release()); | |
477 | |
478 { | |
479 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock()); | |
480 lock->Schedule(GetSharedObserver(), priority, command.release()); | |
481 } | |
482 #else | |
483 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, | |
484 "DCMTK is not enabled, cannot parse a DICOM instance"); | |
485 #endif | |
486 } | |
487 } | |
488 else if (source.IsOrthanc()) | |
489 { | |
490 std::string orthancId; | |
491 | |
492 { | |
493 std::string patientId, studyInstanceUid, seriesInstanceUid; | |
494 if (!instance.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || | |
495 !instance.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || | |
496 !instance.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) | |
497 { | |
498 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, | |
499 "Missing StudyInstanceUID or SeriesInstanceUID in a DICOM instance"); | |
500 } | |
501 | |
502 Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); | |
503 orthancId = hasher.HashInstance(); | |
504 } | |
505 | |
506 const DicomInstanceParameters& parameters = frames_.GetInstanceParameters(index); | |
507 | |
508 if (quality == 0 && source.HasOrthancWebViewer1()) | |
509 { | |
510 std::auto_ptr<GetOrthancWebViewerJpegCommand> command(new GetOrthancWebViewerJpegCommand); | |
511 command->SetInstance(orthancId); | |
512 command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); | |
513 command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); | |
514 | |
515 { | |
516 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock()); | |
517 lock->Schedule(GetSharedObserver(), priority, command.release()); | |
518 } | |
519 } | |
520 else if (quality == 0 && source.HasOrthancAdvancedPreview()) | |
521 { | |
522 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
523 } | |
524 else | |
525 { | |
526 assert(quality <= 1); | |
527 assert(quality == 0 || | |
528 source.HasOrthancWebViewer1() || | |
529 source.HasOrthancAdvancedPreview()); | |
530 | |
531 std::auto_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand); | |
532 command->SetFrameUri(orthancId, frames_.GetFrameIndex(index), parameters.GetExpectedPixelFormat()); | |
533 command->SetExpectedPixelFormat(parameters.GetExpectedPixelFormat()); | |
534 command->SetHttpHeader("Accept", Orthanc::MIME_PAM); | |
535 command->AcquirePayload(new Payload(source, index, sopInstanceUid, quality, protection.release())); | |
536 | |
537 { | |
538 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock()); | |
539 lock->Schedule(GetSharedObserver(), priority, command.release()); | |
540 } | |
541 } | |
542 } | |
543 else | |
544 { | |
545 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
546 } | |
547 } | |
548 } |