Mercurial > hg > orthanc-stone
annotate Framework/Toolbox/OrthancSeriesLoader.cpp @ 40:7207a407bcd8
shared copyright with osimis
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 04 Jan 2017 16:37:42 +0100 |
parents | 6465fbd23bce |
children | 28956ed68280 |
rev | line source |
---|---|
0 | 1 /** |
2 * Stone of Orthanc | |
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics | |
4 * Department, University Hospital of Liege, Belgium | |
40
7207a407bcd8
shared copyright with osimis
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
35
diff
changeset
|
5 * Copyright (C) 2017 Osimis, Belgium |
0 | 6 * |
7 * This program is free software: you can redistribute it and/or | |
8 * modify it under the terms of the GNU General Public License as | |
9 * published by the Free Software Foundation, either version 3 of the | |
10 * License, or (at your option) any later version. | |
11 * | |
12 * In addition, as a special exception, the copyright holders of this | |
13 * program give permission to link the code of its release with the | |
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
15 * that use the same license as the "OpenSSL" library), and distribute | |
16 * the linked executables. You must obey the GNU General Public License | |
17 * in all respects for all of the code used other than "OpenSSL". If you | |
18 * modify file(s) with this exception, you may extend this exception to | |
19 * your version of the file(s), but you are not obligated to do so. If | |
20 * you do not wish to do so, delete this exception statement from your | |
21 * version. If you delete this exception statement from all source files | |
22 * in the program, then also delete it here. | |
23 * | |
24 * This program is distributed in the hope that it will be useful, but | |
25 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
27 * General Public License for more details. | |
28 * | |
29 * You should have received a copy of the GNU General Public License | |
30 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
31 **/ | |
32 | |
33 | |
34 #include "OrthancSeriesLoader.h" | |
35 | |
35 | 36 #include "../Toolbox/MessagingToolbox.h" |
16 | 37 #include "../../Resources/Orthanc/Core/Images/Image.h" |
38 #include "../../Resources/Orthanc/Core/Images/ImageProcessing.h" | |
39 #include "../../Resources/Orthanc/Core/Logging.h" | |
40 #include "../../Resources/Orthanc/Core/OrthancException.h" | |
32 | 41 #include "../../Resources/Orthanc/Plugins/Samples/Common/FullOrthancDataset.h" |
0 | 42 #include "DicomFrameConverter.h" |
43 | |
44 namespace OrthancStone | |
45 { | |
46 class OrthancSeriesLoader::Slice : public boost::noncopyable | |
47 { | |
48 private: | |
49 std::string instanceId_; | |
50 SliceGeometry geometry_; | |
51 double projectionAlongNormal_; | |
52 | |
53 public: | |
54 Slice(const std::string& instanceId, | |
55 const std::string& imagePositionPatient, | |
56 const std::string& imageOrientationPatient) : | |
57 instanceId_(instanceId), | |
58 geometry_(imagePositionPatient, imageOrientationPatient) | |
59 { | |
60 } | |
61 | |
62 const std::string GetInstanceId() const | |
63 { | |
64 return instanceId_; | |
65 } | |
66 | |
67 const SliceGeometry& GetGeometry() const | |
68 { | |
69 return geometry_; | |
70 } | |
71 | |
72 void SetNormal(const Vector& normal) | |
73 { | |
74 projectionAlongNormal_ = boost::numeric::ublas::inner_prod(geometry_.GetOrigin(), normal); | |
75 } | |
76 | |
77 double GetProjectionAlongNormal() const | |
78 { | |
79 return projectionAlongNormal_; | |
80 } | |
81 }; | |
82 | |
83 | |
84 class OrthancSeriesLoader::SetOfSlices : public boost::noncopyable | |
85 { | |
86 private: | |
87 std::vector<Slice*> slices_; | |
88 | |
89 struct Comparator | |
90 { | |
91 bool operator() (const Slice* const a, | |
92 const Slice* const b) const | |
93 { | |
94 return a->GetProjectionAlongNormal() < b->GetProjectionAlongNormal(); | |
95 } | |
96 }; | |
97 | |
98 public: | |
99 ~SetOfSlices() | |
100 { | |
101 for (size_t i = 0; i < slices_.size(); i++) | |
102 { | |
103 assert(slices_[i] != NULL); | |
104 delete slices_[i]; | |
105 } | |
106 } | |
107 | |
108 void Reserve(size_t size) | |
109 { | |
110 slices_.reserve(size); | |
111 } | |
112 | |
113 void AddSlice(const std::string& instanceId, | |
114 const std::string& imagePositionPatient, | |
115 const std::string& imageOrientationPatient) | |
116 { | |
117 slices_.push_back(new Slice(instanceId, imagePositionPatient, imageOrientationPatient)); | |
118 } | |
119 | |
120 size_t GetSliceCount() const | |
121 { | |
122 return slices_.size(); | |
123 } | |
124 | |
125 const Slice& GetSlice(size_t index) const | |
126 { | |
127 assert(slices_[index] != NULL); | |
128 return *slices_[index]; | |
129 } | |
130 | |
131 void Sort(const Vector& normal) | |
132 { | |
133 for (size_t i = 0; i < slices_.size(); i++) | |
134 { | |
135 slices_[i]->SetNormal(normal); | |
136 } | |
137 | |
138 Comparator comparator; | |
139 std::sort(slices_.begin(), slices_.end(), comparator); | |
140 } | |
141 | |
31
9aace933cb64
sharing code with the Orthanc core
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
16
diff
changeset
|
142 void LoadSeriesFast(OrthancPlugins::IOrthancConnection& orthanc, |
0 | 143 const std::string& series) |
144 { | |
145 // Retrieve the orientation of this series | |
146 Json::Value info; | |
147 MessagingToolbox::RestApiGet(info, orthanc, "/series/" + series); | |
148 | |
149 if (info.type() != Json::objectValue || | |
150 !info.isMember("MainDicomTags") || | |
151 info["MainDicomTags"].type() != Json::objectValue || | |
152 !info["MainDicomTags"].isMember("ImageOrientationPatient") || | |
153 info["MainDicomTags"]["ImageOrientationPatient"].type() != Json::stringValue) | |
154 { | |
155 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
156 } | |
157 | |
158 std::string imageOrientationPatient = info["MainDicomTags"]["ImageOrientationPatient"].asString(); | |
159 | |
160 | |
161 // Retrieve the Orthanc ID of all the instances of this series | |
162 Json::Value instances; | |
163 MessagingToolbox::RestApiGet(instances, orthanc, "/series/" + series + "/instances"); | |
164 | |
165 if (instances.type() != Json::arrayValue) | |
166 { | |
167 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
168 } | |
169 | |
170 if (instances.size() == 0) | |
171 { | |
172 LOG(ERROR) << "This series is empty"; | |
173 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); | |
174 } | |
175 | |
176 | |
177 // Retrieve the DICOM tags of all the instances | |
178 std::vector<std::string> instancesId; | |
179 | |
180 instancesId.resize(instances.size()); | |
181 Reserve(instances.size()); | |
182 | |
183 for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++) | |
184 { | |
185 if (instances[i].type() != Json::objectValue || | |
186 !instances[i].isMember("ID") || | |
187 !instances[i].isMember("MainDicomTags") || | |
188 instances[i]["ID"].type() != Json::stringValue || | |
189 instances[i]["MainDicomTags"].type() != Json::objectValue || | |
190 !instances[i]["MainDicomTags"].isMember("ImagePositionPatient") || | |
191 instances[i]["MainDicomTags"]["ImagePositionPatient"].type() != Json::stringValue) | |
192 { | |
193 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
194 } | |
195 else | |
196 { | |
197 instancesId[i] = instances[i]["ID"].asString(); | |
198 AddSlice(instancesId[i], | |
199 instances[i]["MainDicomTags"]["ImagePositionPatient"].asString(), | |
200 imageOrientationPatient); | |
201 } | |
202 } | |
203 | |
204 assert(GetSliceCount() == instances.size()); | |
205 } | |
206 | |
207 | |
31
9aace933cb64
sharing code with the Orthanc core
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
16
diff
changeset
|
208 void LoadSeriesSafe(OrthancPlugins::IOrthancConnection& orthanc, |
0 | 209 const std::string& seriesId) |
210 { | |
211 Json::Value series; | |
212 MessagingToolbox::RestApiGet(series, orthanc, "/series/" + seriesId + "/instances-tags?simplify"); | |
213 | |
214 if (series.type() != Json::objectValue) | |
215 { | |
216 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
217 } | |
218 | |
219 if (series.size() == 0) | |
220 { | |
221 LOG(ERROR) << "This series is empty"; | |
222 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); | |
223 } | |
224 | |
225 Json::Value::Members instances = series.getMemberNames(); | |
226 | |
227 Reserve(instances.size()); | |
228 | |
229 for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++) | |
230 { | |
231 const Json::Value& tags = series[instances[i]]; | |
232 | |
233 if (tags.type() != Json::objectValue || | |
234 !tags.isMember("ImagePositionPatient") || | |
235 !tags.isMember("ImageOrientationPatient") || | |
236 tags["ImagePositionPatient"].type() != Json::stringValue || | |
237 tags["ImageOrientationPatient"].type() != Json::stringValue) | |
238 { | |
239 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
240 } | |
241 else | |
242 { | |
243 AddSlice(instances[i], | |
244 tags["ImagePositionPatient"].asString(), | |
245 tags["ImageOrientationPatient"].asString()); | |
246 } | |
247 } | |
248 | |
249 assert(GetSliceCount() == instances.size()); | |
250 } | |
251 | |
252 | |
253 void SelectNormal(Vector& normal) const | |
254 { | |
255 std::vector<Vector> normalCandidates; | |
256 std::vector<unsigned int> normalCount; | |
257 | |
258 bool found = false; | |
259 | |
260 for (size_t i = 0; !found && i < GetSliceCount(); i++) | |
261 { | |
262 const Vector& normal = GetSlice(i).GetGeometry().GetNormal(); | |
263 | |
264 bool add = true; | |
265 for (size_t j = 0; add && j < normalCandidates.size(); j++) // (*) | |
266 { | |
267 if (GeometryToolbox::IsParallel(normal, normalCandidates[j])) | |
268 { | |
269 normalCount[j] += 1; | |
270 add = false; | |
271 } | |
272 } | |
273 | |
274 if (add) | |
275 { | |
276 if (normalCount.size() > 2) | |
277 { | |
278 // To get linear-time complexity in (*). This heuristics | |
279 // allows the series to have one single frame that is | |
280 // not parallel to the others (such a frame could be a | |
281 // generated preview) | |
282 found = false; | |
283 } | |
284 else | |
285 { | |
286 normalCandidates.push_back(normal); | |
287 normalCount.push_back(1); | |
288 } | |
289 } | |
290 } | |
291 | |
292 for (size_t i = 0; !found && i < normalCandidates.size(); i++) | |
293 { | |
294 unsigned int count = normalCount[i]; | |
295 if (count == GetSliceCount() || | |
296 count + 1 == GetSliceCount()) | |
297 { | |
298 normal = normalCandidates[i]; | |
299 found = true; | |
300 } | |
301 } | |
302 | |
303 if (!found) | |
304 { | |
305 LOG(ERROR) << "Cannot select a normal that is shared by most of the slices of this series"; | |
306 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
307 } | |
308 } | |
309 | |
310 | |
311 void FilterNormal(const Vector& normal) | |
312 { | |
313 size_t pos = 0; | |
314 | |
315 for (size_t i = 0; i < slices_.size(); i++) | |
316 { | |
317 if (GeometryToolbox::IsParallel(normal, slices_[i]->GetGeometry().GetNormal())) | |
318 { | |
319 // This slice is compatible with the selected normal | |
320 slices_[pos] = slices_[i]; | |
321 pos += 1; | |
322 } | |
323 else | |
324 { | |
325 delete slices_[i]; | |
326 slices_[i] = NULL; | |
327 } | |
328 } | |
329 | |
330 slices_.resize(pos); | |
331 } | |
332 }; | |
333 | |
334 | |
335 | |
31
9aace933cb64
sharing code with the Orthanc core
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
16
diff
changeset
|
336 OrthancSeriesLoader::OrthancSeriesLoader(OrthancPlugins::IOrthancConnection& orthanc, |
0 | 337 const std::string& series) : |
338 orthanc_(orthanc), | |
339 slices_(new SetOfSlices) | |
340 { | |
341 /** | |
342 * The function "LoadSeriesFast()" might now behave properly if | |
343 * some slice has some outsider value for its normal, which | |
344 * happens sometimes on reprojected series (e.g. coronal and | |
345 * sagittal of Delphine). Don't use it. | |
346 **/ | |
347 | |
348 slices_->LoadSeriesSafe(orthanc, series); | |
349 | |
350 Vector normal; | |
351 slices_->SelectNormal(normal); | |
352 slices_->FilterNormal(normal); | |
353 slices_->Sort(normal); | |
354 | |
355 if (slices_->GetSliceCount() == 0) // Sanity check | |
356 { | |
357 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | |
358 } | |
359 | |
360 for (size_t i = 0; i < slices_->GetSliceCount(); i++) | |
361 { | |
362 assert(GeometryToolbox::IsParallel(normal, slices_->GetSlice(i).GetGeometry().GetNormal())); | |
363 geometry_.AddSlice(slices_->GetSlice(i).GetGeometry()); | |
364 } | |
365 | |
32 | 366 std::string uri = "/instances/" + slices_->GetSlice(0).GetInstanceId() + "/tags"; |
367 | |
368 OrthancPlugins::FullOrthancDataset dataset(orthanc_, uri); | |
369 OrthancPlugins::DicomDatasetReader reader(dataset); | |
370 | |
371 if (!reader.GetUnsignedIntegerValue(width_, OrthancPlugins::DICOM_TAG_COLUMNS) || | |
372 !reader.GetUnsignedIntegerValue(height_, OrthancPlugins::DICOM_TAG_ROWS)) | |
0 | 373 { |
374 throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag); | |
375 } | |
376 | |
377 DicomFrameConverter converter; | |
32 | 378 converter.ReadParameters(dataset); |
0 | 379 format_ = converter.GetExpectedPixelFormat(); |
380 } | |
381 | |
382 | |
32 | 383 OrthancPlugins::IDicomDataset* OrthancSeriesLoader::DownloadDicom(size_t index) |
0 | 384 { |
32 | 385 std::string uri = "/instances/" + slices_->GetSlice(index).GetInstanceId() + "/tags"; |
0 | 386 |
32 | 387 std::auto_ptr<OrthancPlugins::IDicomDataset> dataset(new OrthancPlugins::FullOrthancDataset(orthanc_, uri)); |
388 OrthancPlugins::DicomDatasetReader reader(*dataset); | |
389 | |
390 unsigned int frames; | |
391 if (reader.GetUnsignedIntegerValue(frames, OrthancPlugins::DICOM_TAG_NUMBER_OF_FRAMES) && | |
392 frames != 1) | |
0 | 393 { |
394 LOG(ERROR) << "One instance in this series has more than 1 frame"; | |
395 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
396 } | |
397 | |
398 return dataset.release(); | |
399 } | |
400 | |
401 | |
402 void OrthancSeriesLoader::CheckFrame(const Orthanc::ImageAccessor& frame) const | |
403 { | |
404 if (frame.GetFormat() != format_ || | |
405 frame.GetWidth() != width_ || | |
406 frame.GetHeight() != height_) | |
407 { | |
408 LOG(ERROR) << "The parameters of this series vary accross its slices"; | |
409 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
410 } | |
411 } | |
412 | |
413 | |
414 Orthanc::ImageAccessor* OrthancSeriesLoader::DownloadFrame(size_t index) | |
415 { | |
416 const Slice& slice = slices_->GetSlice(index); | |
417 | |
418 std::auto_ptr<Orthanc::ImageAccessor> frame | |
419 (MessagingToolbox::DecodeFrame(orthanc_, slice.GetInstanceId(), 0, format_)); | |
420 | |
421 if (frame.get() != NULL) | |
422 { | |
423 CheckFrame(*frame); | |
424 } | |
425 | |
426 return frame.release(); | |
427 } | |
428 | |
429 | |
430 Orthanc::ImageAccessor* OrthancSeriesLoader::DownloadJpegFrame(size_t index, | |
431 unsigned int quality) | |
432 { | |
433 const Slice& slice = slices_->GetSlice(index); | |
434 | |
435 std::auto_ptr<Orthanc::ImageAccessor> frame | |
436 (MessagingToolbox::DecodeJpegFrame(orthanc_, slice.GetInstanceId(), 0, quality, format_)); | |
437 | |
438 if (frame.get() != NULL) | |
439 { | |
440 CheckFrame(*frame); | |
441 } | |
442 | |
443 return frame.release(); | |
444 } | |
445 | |
446 | |
447 bool OrthancSeriesLoader::IsJpegAvailable() | |
448 { | |
449 return MessagingToolbox::HasWebViewerInstalled(orthanc_); | |
450 } | |
451 } |