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