comparison Framework/Loaders/SeriesThumbnailsLoader.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 7a0460c5e98e
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 "SeriesThumbnailsLoader.h"
23
24 #include <Core/DicomFormat/DicomMap.h>
25 #include <Core/DicomFormat/DicomInstanceHasher.h>
26 #include <Core/Images/ImageProcessing.h>
27 #include <Core/Images/JpegWriter.h>
28 #include <Core/OrthancException.h>
29
30 #include <boost/algorithm/string/predicate.hpp>
31
32 static const unsigned int JPEG_QUALITY = 70; // Only used for Orthanc source
33
34 namespace OrthancStone
35 {
36 static SeriesThumbnailType ExtractSopClassUid(const std::string& sopClassUid)
37 {
38 if (sopClassUid == "1.2.840.10008.5.1.4.1.1.104.1") // Encapsulated PDF Storage
39 {
40 return SeriesThumbnailType_Pdf;
41 }
42 else if (sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.1.1" || // Video Endoscopic Image Storage
43 sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.2.1" || // Video Microscopic Image Storage
44 sopClassUid == "1.2.840.10008.5.1.4.1.1.77.1.4.1") // Video Photographic Image Storage
45 {
46 return SeriesThumbnailType_Video;
47 }
48 else
49 {
50 return SeriesThumbnailType_Unknown;
51 }
52 }
53
54
55 SeriesThumbnailsLoader::Thumbnail::Thumbnail(const std::string& image,
56 const std::string& mime) :
57 type_(SeriesThumbnailType_Image),
58 image_(image),
59 mime_(mime)
60 {
61 }
62
63
64 SeriesThumbnailsLoader::Thumbnail::Thumbnail(SeriesThumbnailType type) :
65 type_(type)
66 {
67 if (type == SeriesThumbnailType_Image)
68 {
69 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
70 }
71 }
72
73
74 void SeriesThumbnailsLoader::AcquireThumbnail(const DicomSource& source,
75 const std::string& studyInstanceUid,
76 const std::string& seriesInstanceUid,
77 SeriesThumbnailsLoader::Thumbnail* thumbnail)
78 {
79 assert(thumbnail != NULL);
80
81 std::auto_ptr<Thumbnail> protection(thumbnail);
82
83 Thumbnails::iterator found = thumbnails_.find(seriesInstanceUid);
84 if (found == thumbnails_.end())
85 {
86 thumbnails_[seriesInstanceUid] = protection.release();
87 }
88 else
89 {
90 assert(found->second != NULL);
91 delete found->second;
92 found->second = protection.release();
93 }
94
95 ThumbnailLoadedMessage message(*this, source, studyInstanceUid, seriesInstanceUid, *thumbnail);
96 BroadcastMessage(message);
97 }
98
99
100 class SeriesThumbnailsLoader::Handler : public Orthanc::IDynamicObject
101 {
102 private:
103 boost::shared_ptr<SeriesThumbnailsLoader> loader_;
104 DicomSource source_;
105 std::string studyInstanceUid_;
106 std::string seriesInstanceUid_;
107
108 public:
109 Handler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
110 const DicomSource& source,
111 const std::string& studyInstanceUid,
112 const std::string& seriesInstanceUid) :
113 loader_(loader),
114 source_(source),
115 studyInstanceUid_(studyInstanceUid),
116 seriesInstanceUid_(seriesInstanceUid)
117 {
118 if (!loader)
119 {
120 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
121 }
122 }
123
124 boost::shared_ptr<SeriesThumbnailsLoader> GetLoader()
125 {
126 return loader_;
127 }
128
129 const DicomSource& GetSource() const
130 {
131 return source_;
132 }
133
134 const std::string& GetStudyInstanceUid() const
135 {
136 return studyInstanceUid_;
137 }
138
139 const std::string& GetSeriesInstanceUid() const
140 {
141 return seriesInstanceUid_;
142 }
143
144 virtual void HandleSuccess(const std::string& body,
145 const std::map<std::string, std::string>& headers) = 0;
146
147 virtual void HandleError()
148 {
149 LOG(INFO) << "Cannot generate thumbnail for SeriesInstanceUID: " << seriesInstanceUid_;
150 }
151 };
152
153
154 class SeriesThumbnailsLoader::DicomWebSopClassHandler : public SeriesThumbnailsLoader::Handler
155 {
156 private:
157 static bool GetSopClassUid(std::string& sopClassUid,
158 const Json::Value& json)
159 {
160 Orthanc::DicomMap dicom;
161 dicom.FromDicomWeb(json);
162
163 return dicom.LookupStringValue(sopClassUid, Orthanc::DICOM_TAG_SOP_CLASS_UID, false);
164 }
165
166 public:
167 DicomWebSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
168 const DicomSource& source,
169 const std::string& studyInstanceUid,
170 const std::string& seriesInstanceUid) :
171 Handler(loader, source, studyInstanceUid, seriesInstanceUid)
172 {
173 }
174
175 virtual void HandleSuccess(const std::string& body,
176 const std::map<std::string, std::string>& headers)
177 {
178 Json::Reader reader;
179 Json::Value value;
180
181 if (!reader.parse(body, value) ||
182 value.type() != Json::arrayValue)
183 {
184 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
185 }
186 else
187 {
188 SeriesThumbnailType type = SeriesThumbnailType_Unknown;
189
190 std::string sopClassUid;
191 if (value.size() > 0 &&
192 GetSopClassUid(sopClassUid, value[0]))
193 {
194 bool ok = true;
195
196 for (Json::Value::ArrayIndex i = 1; i < value.size() && ok; i++)
197 {
198 std::string s;
199 if (!GetSopClassUid(s, value[i]) ||
200 s != sopClassUid)
201 {
202 ok = false;
203 }
204 }
205
206 if (ok)
207 {
208 type = ExtractSopClassUid(sopClassUid);
209 }
210 }
211
212 GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
213 GetSeriesInstanceUid(), new Thumbnail(type));
214 }
215 }
216 };
217
218
219 class SeriesThumbnailsLoader::DicomWebThumbnailHandler : public SeriesThumbnailsLoader::Handler
220 {
221 public:
222 DicomWebThumbnailHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
223 const DicomSource& source,
224 const std::string& studyInstanceUid,
225 const std::string& seriesInstanceUid) :
226 Handler(loader, source, studyInstanceUid, seriesInstanceUid)
227 {
228 }
229
230 virtual void HandleSuccess(const std::string& body,
231 const std::map<std::string, std::string>& headers)
232 {
233 std::string mime = Orthanc::MIME_JPEG;
234 for (std::map<std::string, std::string>::const_iterator
235 it = headers.begin(); it != headers.end(); ++it)
236 {
237 if (boost::iequals(it->first, "content-type"))
238 {
239 mime = it->second;
240 }
241 }
242
243 GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
244 GetSeriesInstanceUid(), new Thumbnail(body, mime));
245 }
246
247 virtual void HandleError()
248 {
249 // The DICOMweb wasn't able to generate a thumbnail, try to
250 // retrieve the SopClassUID tag using QIDO-RS
251
252 std::map<std::string, std::string> arguments, headers;
253 arguments["0020000D"] = GetStudyInstanceUid();
254 arguments["0020000E"] = GetSeriesInstanceUid();
255 arguments["includefield"] = "00080016";
256
257 std::auto_ptr<IOracleCommand> command(
258 GetSource().CreateDicomWebCommand(
259 "/instances", arguments, headers, new DicomWebSopClassHandler(
260 GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid())));
261 GetLoader()->Schedule(command.release());
262 }
263 };
264
265
266 class SeriesThumbnailsLoader::ThumbnailInformation : public Orthanc::IDynamicObject
267 {
268 private:
269 DicomSource source_;
270 std::string studyInstanceUid_;
271 std::string seriesInstanceUid_;
272
273 public:
274 ThumbnailInformation(const DicomSource& source,
275 const std::string& studyInstanceUid,
276 const std::string& seriesInstanceUid) :
277 source_(source),
278 studyInstanceUid_(studyInstanceUid),
279 seriesInstanceUid_(seriesInstanceUid)
280 {
281 }
282
283 const DicomSource& GetDicomSource() const
284 {
285 return source_;
286 }
287
288 const std::string& GetStudyInstanceUid() const
289 {
290 return studyInstanceUid_;
291 }
292
293 const std::string& GetSeriesInstanceUid() const
294 {
295 return seriesInstanceUid_;
296 }
297 };
298
299
300 class SeriesThumbnailsLoader::OrthancSopClassHandler : public SeriesThumbnailsLoader::Handler
301 {
302 private:
303 std::string instanceId_;
304
305 public:
306 OrthancSopClassHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
307 const DicomSource& source,
308 const std::string& studyInstanceUid,
309 const std::string& seriesInstanceUid,
310 const std::string& instanceId) :
311 Handler(loader, source, studyInstanceUid, seriesInstanceUid),
312 instanceId_(instanceId)
313 {
314 }
315
316 virtual void HandleSuccess(const std::string& body,
317 const std::map<std::string, std::string>& headers)
318 {
319 SeriesThumbnailType type = ExtractSopClassUid(body);
320
321 if (type == SeriesThumbnailType_Pdf ||
322 type == SeriesThumbnailType_Video)
323 {
324 GetLoader()->AcquireThumbnail(GetSource(), GetStudyInstanceUid(),
325 GetSeriesInstanceUid(), new Thumbnail(type));
326 }
327 else
328 {
329 std::auto_ptr<GetOrthancImageCommand> command(new GetOrthancImageCommand);
330 command->SetUri("/instances/" + instanceId_ + "/preview");
331 command->SetHttpHeader("Accept", Orthanc::MIME_JPEG);
332 command->AcquirePayload(new ThumbnailInformation(
333 GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid()));
334 GetLoader()->Schedule(command.release());
335 }
336 }
337 };
338
339
340 class SeriesThumbnailsLoader::SelectOrthancInstanceHandler : public SeriesThumbnailsLoader::Handler
341 {
342 public:
343 SelectOrthancInstanceHandler(boost::shared_ptr<SeriesThumbnailsLoader> loader,
344 const DicomSource& source,
345 const std::string& studyInstanceUid,
346 const std::string& seriesInstanceUid) :
347 Handler(loader, source, studyInstanceUid, seriesInstanceUid)
348 {
349 }
350
351 virtual void HandleSuccess(const std::string& body,
352 const std::map<std::string, std::string>& headers)
353 {
354 static const char* const INSTANCES = "Instances";
355
356 Json::Value json;
357 Json::Reader reader;
358 if (!reader.parse(body, json) ||
359 json.type() != Json::objectValue)
360 {
361 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
362 }
363
364 if (json.isMember(INSTANCES) &&
365 json[INSTANCES].type() == Json::arrayValue &&
366 json[INSTANCES].size() > 0)
367 {
368 // Select one instance of the series to generate the thumbnail
369 Json::Value::ArrayIndex index = json[INSTANCES].size() / 2;
370 if (json[INSTANCES][index].type() == Json::stringValue)
371 {
372 std::map<std::string, std::string> arguments, headers;
373 arguments["quality"] = boost::lexical_cast<std::string>(JPEG_QUALITY);
374 headers["Accept"] = Orthanc::MIME_JPEG;
375
376 const std::string instance = json[INSTANCES][index].asString();
377
378 std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
379 command->SetUri("/instances/" + instance + "/metadata/SopClassUid");
380 command->AcquirePayload(
381 new OrthancSopClassHandler(
382 GetLoader(), GetSource(), GetStudyInstanceUid(), GetSeriesInstanceUid(), instance));
383 GetLoader()->Schedule(command.release());
384 }
385 }
386 }
387 };
388
389
390 void SeriesThumbnailsLoader::Schedule(IOracleCommand* command)
391 {
392 std::auto_ptr<ILoadersContext::ILock> lock(context_.Lock());
393 lock->Schedule(GetSharedObserver(), priority_, command);
394 }
395
396
397 void SeriesThumbnailsLoader::Handle(const HttpCommand::SuccessMessage& message)
398 {
399 assert(message.GetOrigin().HasPayload());
400 dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
401 }
402
403
404 void SeriesThumbnailsLoader::Handle(const OrthancRestApiCommand::SuccessMessage& message)
405 {
406 assert(message.GetOrigin().HasPayload());
407 dynamic_cast<Handler&>(message.GetOrigin().GetPayload()).HandleSuccess(message.GetAnswer(), message.GetAnswerHeaders());
408 }
409
410
411 void SeriesThumbnailsLoader::Handle(const GetOrthancImageCommand::SuccessMessage& message)
412 {
413 assert(message.GetOrigin().HasPayload());
414
415 std::auto_ptr<Orthanc::ImageAccessor> resized(Orthanc::ImageProcessing::FitSize(message.GetImage(), width_, height_));
416
417 std::string jpeg;
418 Orthanc::JpegWriter writer;
419 writer.SetQuality(JPEG_QUALITY);
420 writer.WriteToMemory(jpeg, *resized);
421
422 const ThumbnailInformation& info = dynamic_cast<ThumbnailInformation&>(message.GetOrigin().GetPayload());
423 AcquireThumbnail(info.GetDicomSource(), info.GetStudyInstanceUid(),
424 info.GetSeriesInstanceUid(), new Thumbnail(jpeg, Orthanc::MIME_JPEG));
425 }
426
427
428 void SeriesThumbnailsLoader::Handle(const OracleCommandExceptionMessage& message)
429 {
430 const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
431 assert(command.HasPayload());
432 dynamic_cast<Handler&>(command.GetPayload()).HandleError();
433 }
434
435
436 SeriesThumbnailsLoader::SeriesThumbnailsLoader(ILoadersContext& context,
437 int priority) :
438 context_(context),
439 priority_(priority),
440 width_(128),
441 height_(128)
442 {
443 }
444
445
446 boost::shared_ptr<IObserver> SeriesThumbnailsLoader::Factory::Create(ILoadersContext::ILock& stone)
447 {
448 boost::shared_ptr<SeriesThumbnailsLoader> result(new SeriesThumbnailsLoader(stone.GetContext(), priority_));
449 result->Register<GetOrthancImageCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
450 result->Register<HttpCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
451 result->Register<OracleCommandExceptionMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
452 result->Register<OrthancRestApiCommand::SuccessMessage>(stone.GetOracleObservable(), &SeriesThumbnailsLoader::Handle);
453 return result;
454 }
455
456
457 void SeriesThumbnailsLoader::SetThumbnailSize(unsigned int width,
458 unsigned int height)
459 {
460 if (width <= 0 ||
461 height <= 0)
462 {
463 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
464 }
465 else
466 {
467 width_ = width;
468 height_ = height;
469 }
470 }
471
472
473 void SeriesThumbnailsLoader::Clear()
474 {
475 for (Thumbnails::iterator it = thumbnails_.begin(); it != thumbnails_.end(); ++it)
476 {
477 assert(it->second != NULL);
478 delete it->second;
479 }
480
481 thumbnails_.clear();
482 }
483
484
485 SeriesThumbnailType SeriesThumbnailsLoader::GetSeriesThumbnail(std::string& image,
486 std::string& mime,
487 const std::string& seriesInstanceUid) const
488 {
489 Thumbnails::const_iterator found = thumbnails_.find(seriesInstanceUid);
490
491 if (found == thumbnails_.end())
492 {
493 return SeriesThumbnailType_Unknown;
494 }
495 else
496 {
497 assert(found->second != NULL);
498 image.assign(found->second->GetImage());
499 mime.assign(found->second->GetMime());
500 return found->second->GetType();
501 }
502 }
503
504
505 void SeriesThumbnailsLoader::ScheduleLoadThumbnail(const DicomSource& source,
506 const std::string& patientId,
507 const std::string& studyInstanceUid,
508 const std::string& seriesInstanceUid)
509 {
510 if (source.IsDicomWeb())
511 {
512 if (!source.HasDicomWebRendered())
513 {
514 // TODO - Could use DCMTK here
515 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol,
516 "DICOMweb server is not able to generate renderings of DICOM series");
517 }
518
519 const std::string uri = ("/studies/" + studyInstanceUid +
520 "/series/" + seriesInstanceUid + "/rendered");
521
522 std::map<std::string, std::string> arguments, headers;
523 arguments["viewport"] = (boost::lexical_cast<std::string>(width_) + "," +
524 boost::lexical_cast<std::string>(height_));
525
526 // Needed to set this header explicitly, as long as emscripten
527 // does not include macro "EMSCRIPTEN_FETCH_RESPONSE_HEADERS"
528 // https://github.com/emscripten-core/emscripten/pull/8486
529 headers["Accept"] = Orthanc::MIME_JPEG;
530
531 std::auto_ptr<IOracleCommand> command(
532 source.CreateDicomWebCommand(
533 uri, arguments, headers, new DicomWebThumbnailHandler(
534 shared_from_this(), source, studyInstanceUid, seriesInstanceUid)));
535 Schedule(command.release());
536 }
537 else if (source.IsOrthanc())
538 {
539 // Dummy SOP Instance UID, as we are working at the "series" level
540 Orthanc::DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "dummy");
541
542 std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
543 command->SetUri("/series/" + hasher.HashSeries());
544 command->AcquirePayload(new SelectOrthancInstanceHandler(
545 shared_from_this(), source, studyInstanceUid, seriesInstanceUid));
546 Schedule(command.release());
547 }
548 else
549 {
550 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented,
551 "Can only load thumbnails from Orthanc or DICOMweb");
552 }
553 }
554 }