Mercurial > hg > orthanc-stone
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 } |