Mercurial > hg > orthanc-client
comparison OrthancCppClient/Series.cpp @ 0:ebc1e38ef615
initial commit
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Mon, 01 Jun 2015 09:47:32 +0200 |
parents | |
children | d5027f9f676a |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:ebc1e38ef615 |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2015 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 "../Core/PrecompiledHeaders.h" | |
34 #include "Series.h" | |
35 | |
36 #include "OrthancConnection.h" | |
37 | |
38 #include <set> | |
39 #include <boost/lexical_cast.hpp> | |
40 | |
41 namespace OrthancClient | |
42 { | |
43 namespace | |
44 { | |
45 class SliceLocator | |
46 { | |
47 private: | |
48 float normal_[3]; | |
49 | |
50 public: | |
51 SliceLocator(Instance& someSlice) | |
52 { | |
53 /** | |
54 * Compute the slice normal from Image Orientation Patient. | |
55 * http://nipy.sourceforge.net/nibabel/dicom/dicom_orientation.html#dicom-z-from-slice | |
56 * http://dicomiseasy.blogspot.be/2013/06/getting-oriented-using-image-plane.html | |
57 * http://www.itk.org/pipermail/insight-users/2003-September/004762.html | |
58 **/ | |
59 | |
60 std::vector<float> cosines; | |
61 someSlice.SplitVectorOfFloats(cosines, "ImageOrientationPatient"); // 0020-0037 | |
62 | |
63 if (cosines.size() != 6) | |
64 { | |
65 throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat); | |
66 } | |
67 | |
68 normal_[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4]; | |
69 normal_[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5]; | |
70 normal_[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3]; | |
71 } | |
72 | |
73 | |
74 /** | |
75 * Compute the distance of some slice along the slice normal. | |
76 **/ | |
77 float ComputeSliceLocation(Instance& instance) const | |
78 { | |
79 std::vector<float> ipp; | |
80 instance.SplitVectorOfFloats(ipp, "ImagePositionPatient"); // 0020-0032 | |
81 if (ipp.size() != 3) | |
82 { | |
83 throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat); | |
84 } | |
85 | |
86 float dist = 0; | |
87 | |
88 for (int i = 0; i < 3; i++) | |
89 { | |
90 dist += normal_[i] * ipp[i]; | |
91 } | |
92 | |
93 return dist; | |
94 } | |
95 }; | |
96 | |
97 class ImageDownloadCommand : public Orthanc::ICommand | |
98 { | |
99 private: | |
100 Orthanc::PixelFormat format_; | |
101 Orthanc::ImageExtractionMode mode_; | |
102 Instance& instance_; | |
103 void* target_; | |
104 size_t lineStride_; | |
105 | |
106 public: | |
107 ImageDownloadCommand(Instance& instance, | |
108 Orthanc::PixelFormat format, | |
109 Orthanc::ImageExtractionMode mode, | |
110 void* target, | |
111 size_t lineStride) : | |
112 format_(format), | |
113 mode_(mode), | |
114 instance_(instance), | |
115 target_(target), | |
116 lineStride_(lineStride) | |
117 { | |
118 instance_.SetImageExtractionMode(mode); | |
119 } | |
120 | |
121 virtual bool Execute() | |
122 { | |
123 using namespace Orthanc; | |
124 | |
125 unsigned int width = instance_.GetHeight(); | |
126 | |
127 for (unsigned int y = 0; y < instance_.GetHeight(); y++) | |
128 { | |
129 uint8_t* p = reinterpret_cast<uint8_t*>(target_) + y * lineStride_; | |
130 | |
131 if (instance_.GetPixelFormat() == format_) | |
132 { | |
133 memcpy(p, instance_.GetBuffer(y), GetBytesPerPixel(instance_.GetPixelFormat()) * instance_.GetWidth()); | |
134 } | |
135 else if (instance_.GetPixelFormat() == PixelFormat_Grayscale8 && | |
136 format_ == PixelFormat_RGB24) | |
137 { | |
138 const uint8_t* s = reinterpret_cast<const uint8_t*>(instance_.GetBuffer(y)); | |
139 for (unsigned int x = 0; x < width; x++, s++, p += 3) | |
140 { | |
141 p[0] = *s; | |
142 p[1] = *s; | |
143 p[2] = *s; | |
144 } | |
145 } | |
146 else | |
147 { | |
148 throw OrthancClientException(ErrorCode_NotImplemented); | |
149 } | |
150 } | |
151 | |
152 // Do not keep the image in memory, as we are loading 3D images | |
153 instance_.DiscardImage(); | |
154 | |
155 return true; | |
156 } | |
157 }; | |
158 | |
159 | |
160 class ProgressToFloatListener : public Orthanc::ThreadedCommandProcessor::IListener | |
161 { | |
162 private: | |
163 float* target_; | |
164 | |
165 public: | |
166 ProgressToFloatListener(float* target) : target_(target) | |
167 { | |
168 } | |
169 | |
170 virtual void SignalProgress(unsigned int current, | |
171 unsigned int total) | |
172 { | |
173 if (total == 0) | |
174 { | |
175 *target_ = 0; | |
176 } | |
177 else | |
178 { | |
179 *target_ = static_cast<float>(current) / static_cast<float>(total); | |
180 } | |
181 } | |
182 | |
183 virtual void SignalSuccess(unsigned int total) | |
184 { | |
185 *target_ = 1; | |
186 } | |
187 | |
188 virtual void SignalFailure() | |
189 { | |
190 *target_ = 0; | |
191 } | |
192 | |
193 virtual void SignalCancel() | |
194 { | |
195 *target_ = 0; | |
196 } | |
197 }; | |
198 | |
199 } | |
200 | |
201 | |
202 void Series::Check3DImage() | |
203 { | |
204 if (!Is3DImage()) | |
205 { | |
206 throw OrthancClientException(Orthanc::ErrorCode_NotImplemented); | |
207 } | |
208 } | |
209 | |
210 bool Series::Is3DImageInternal() | |
211 { | |
212 try | |
213 { | |
214 if (GetInstanceCount() == 0) | |
215 { | |
216 // Empty image, use some default value (should never happen) | |
217 voxelSizeX_ = 1; | |
218 voxelSizeY_ = 1; | |
219 voxelSizeZ_ = 1; | |
220 sliceThickness_ = 1; | |
221 | |
222 return true; | |
223 } | |
224 | |
225 // Choose a reference slice | |
226 Instance& reference = GetInstance(0); | |
227 | |
228 // Check that all the child instances share the same 3D parameters | |
229 for (unsigned int i = 0; i < GetInstanceCount(); i++) | |
230 { | |
231 Instance& i2 = GetInstance(i); | |
232 | |
233 if (std::string(reference.GetTagAsString("Columns")) != std::string(i2.GetTagAsString("Columns")) || | |
234 std::string(reference.GetTagAsString("Rows")) != std::string(i2.GetTagAsString("Rows")) || | |
235 std::string(reference.GetTagAsString("ImageOrientationPatient")) != std::string(i2.GetTagAsString("ImageOrientationPatient")) || | |
236 std::string(reference.GetTagAsString("SliceThickness")) != std::string(i2.GetTagAsString("SliceThickness")) || | |
237 std::string(reference.GetTagAsString("PixelSpacing")) != std::string(i2.GetTagAsString("PixelSpacing"))) | |
238 { | |
239 return false; | |
240 } | |
241 } | |
242 | |
243 | |
244 // Extract X/Y voxel size and slice thickness | |
245 std::string s = GetInstance(0).GetTagAsString("PixelSpacing"); // 0028-0030 | |
246 size_t pos = s.find('\\'); | |
247 assert(pos != std::string::npos); | |
248 std::string sy = s.substr(0, pos); | |
249 std::string sx = s.substr(pos + 1); | |
250 | |
251 try | |
252 { | |
253 voxelSizeX_ = boost::lexical_cast<float>(sx); | |
254 voxelSizeY_ = boost::lexical_cast<float>(sy); | |
255 } | |
256 catch (boost::bad_lexical_cast) | |
257 { | |
258 throw OrthancClientException(Orthanc::ErrorCode_BadFileFormat); | |
259 } | |
260 | |
261 sliceThickness_ = GetInstance(0).GetTagAsFloat("SliceThickness"); // 0018-0050 | |
262 | |
263 | |
264 // Compute the location of each slice to extract the voxel size along Z | |
265 voxelSizeZ_ = std::numeric_limits<float>::infinity(); | |
266 | |
267 SliceLocator locator(reference); | |
268 float referenceSliceLocation = locator.ComputeSliceLocation(reference); | |
269 | |
270 std::set<float> l; | |
271 for (unsigned int i = 0; i < GetInstanceCount(); i++) | |
272 { | |
273 float location = locator.ComputeSliceLocation(GetInstance(i)); | |
274 float distanceToReferenceSlice = fabs(location - referenceSliceLocation); | |
275 | |
276 l.insert(location); | |
277 | |
278 if (distanceToReferenceSlice > std::numeric_limits<float>::epsilon() && | |
279 distanceToReferenceSlice < voxelSizeZ_) | |
280 { | |
281 voxelSizeZ_ = distanceToReferenceSlice; | |
282 } | |
283 } | |
284 | |
285 | |
286 // Make sure that 2 slices do not share the same Z location | |
287 return l.size() == GetInstanceCount(); | |
288 } | |
289 catch (OrthancClientException) | |
290 { | |
291 return false; | |
292 } | |
293 } | |
294 | |
295 void Series::ReadSeries() | |
296 { | |
297 Orthanc::HttpClient client(connection_.GetHttpClient()); | |
298 | |
299 client.SetUrl(std::string(connection_.GetOrthancUrl()) + "/series/" + id_); | |
300 Json::Value v; | |
301 if (!client.Apply(series_)) | |
302 { | |
303 throw OrthancClientException(Orthanc::ErrorCode_NetworkProtocol); | |
304 } | |
305 } | |
306 | |
307 Orthanc::IDynamicObject* Series::GetFillerItem(size_t index) | |
308 { | |
309 Json::Value::ArrayIndex tmp = static_cast<Json::Value::ArrayIndex>(index); | |
310 std::string id = series_["Instances"][tmp].asString(); | |
311 return new Instance(connection_, id.c_str()); | |
312 } | |
313 | |
314 Series::Series(const OrthancConnection& connection, | |
315 const char* id) : | |
316 connection_(connection), | |
317 id_(id), | |
318 instances_(*this) | |
319 { | |
320 ReadSeries(); | |
321 status_ = Status3DImage_NotTested; | |
322 url_ = std::string(connection_.GetOrthancUrl()) + "/series/" + id_; | |
323 | |
324 voxelSizeX_ = 0; | |
325 voxelSizeY_ = 0; | |
326 voxelSizeZ_ = 0; | |
327 sliceThickness_ = 0; | |
328 | |
329 instances_.SetThreadCount(connection.GetThreadCount()); | |
330 } | |
331 | |
332 | |
333 bool Series::Is3DImage() | |
334 { | |
335 if (status_ == Status3DImage_NotTested) | |
336 { | |
337 status_ = Is3DImageInternal() ? Status3DImage_True : Status3DImage_False; | |
338 } | |
339 | |
340 return status_ == Status3DImage_True; | |
341 } | |
342 | |
343 unsigned int Series::GetInstanceCount() | |
344 { | |
345 return instances_.GetSize(); | |
346 } | |
347 | |
348 Instance& Series::GetInstance(unsigned int index) | |
349 { | |
350 return dynamic_cast<Instance&>(instances_.GetItem(index)); | |
351 } | |
352 | |
353 unsigned int Series::GetWidth() | |
354 { | |
355 Check3DImage(); | |
356 | |
357 if (GetInstanceCount() == 0) | |
358 return 0; | |
359 else | |
360 return GetInstance(0).GetTagAsInt("Columns"); | |
361 } | |
362 | |
363 unsigned int Series::GetHeight() | |
364 { | |
365 Check3DImage(); | |
366 | |
367 if (GetInstanceCount() == 0) | |
368 return 0; | |
369 else | |
370 return GetInstance(0).GetTagAsInt("Rows"); | |
371 } | |
372 | |
373 const char* Series::GetMainDicomTag(const char* tag, const char* defaultValue) const | |
374 { | |
375 if (series_["MainDicomTags"].isMember(tag)) | |
376 { | |
377 return series_["MainDicomTags"][tag].asCString(); | |
378 } | |
379 else | |
380 { | |
381 return defaultValue; | |
382 } | |
383 } | |
384 | |
385 | |
386 | |
387 void Series::Load3DImageInternal(void* target, | |
388 Orthanc::PixelFormat format, | |
389 size_t lineStride, | |
390 size_t stackStride, | |
391 Orthanc::ThreadedCommandProcessor::IListener* listener) | |
392 { | |
393 using namespace Orthanc; | |
394 | |
395 // Choose the extraction mode, depending on the format of the | |
396 // target image. | |
397 | |
398 uint8_t bytesPerPixel; | |
399 ImageExtractionMode mode; | |
400 | |
401 switch (format) | |
402 { | |
403 case PixelFormat_RGB24: | |
404 bytesPerPixel = 3; | |
405 mode = ImageExtractionMode_Preview; | |
406 break; | |
407 | |
408 case PixelFormat_Grayscale8: | |
409 bytesPerPixel = 1; | |
410 mode = ImageExtractionMode_UInt8; // Preview ??? | |
411 break; | |
412 | |
413 case PixelFormat_Grayscale16: | |
414 bytesPerPixel = 2; | |
415 mode = ImageExtractionMode_UInt16; | |
416 break; | |
417 | |
418 case PixelFormat_SignedGrayscale16: | |
419 bytesPerPixel = 2; | |
420 mode = ImageExtractionMode_UInt16; | |
421 format = PixelFormat_Grayscale16; | |
422 break; | |
423 | |
424 default: | |
425 throw OrthancClientException(ErrorCode_NotImplemented); | |
426 } | |
427 | |
428 | |
429 // Check that the target image is properly sized | |
430 unsigned int sx = GetWidth(); | |
431 unsigned int sy = GetHeight(); | |
432 | |
433 if (lineStride < sx * bytesPerPixel || | |
434 stackStride < sx * sy * bytesPerPixel) | |
435 { | |
436 throw OrthancClientException(ErrorCode_BadRequest); | |
437 } | |
438 | |
439 if (sx == 0 || sy == 0 || GetInstanceCount() == 0) | |
440 { | |
441 // Empty image, nothing to do | |
442 if (listener) | |
443 listener->SignalSuccess(0); | |
444 return; | |
445 } | |
446 | |
447 | |
448 /** | |
449 * Order the stacks according to their distance along the slice | |
450 * normal (using the "Image Position Patient" tag). This works | |
451 * even if the "SliceLocation" tag is absent. | |
452 **/ | |
453 SliceLocator locator(GetInstance(0)); | |
454 | |
455 typedef std::map<float, Instance*> Instances; | |
456 Instances instances; | |
457 for (unsigned int i = 0; i < GetInstanceCount(); i++) | |
458 { | |
459 float dist = locator.ComputeSliceLocation(GetInstance(i)); | |
460 instances[dist] = &GetInstance(i); | |
461 } | |
462 | |
463 if (instances.size() != GetInstanceCount()) | |
464 { | |
465 // Several instances have the same Z coordinate | |
466 throw OrthancClientException(ErrorCode_NotImplemented); | |
467 } | |
468 | |
469 | |
470 // Submit the download of each stack as a set of commands | |
471 ThreadedCommandProcessor processor(connection_.GetThreadCount()); | |
472 | |
473 if (listener != NULL) | |
474 { | |
475 processor.SetListener(*listener); | |
476 } | |
477 | |
478 uint8_t* stackTarget = reinterpret_cast<uint8_t*>(target); | |
479 for (Instances::iterator it = instances.begin(); it != instances.end(); ++it) | |
480 { | |
481 processor.Post(new ImageDownloadCommand(*it->second, format, mode, stackTarget, lineStride)); | |
482 stackTarget += stackStride; | |
483 } | |
484 | |
485 | |
486 // Wait for all the stacks to be downloaded | |
487 if (!processor.Join()) | |
488 { | |
489 throw OrthancClientException(ErrorCode_NetworkProtocol); | |
490 } | |
491 } | |
492 | |
493 float Series::GetVoxelSizeX() | |
494 { | |
495 Check3DImage(); // Is3DImageInternal() will compute the voxel sizes | |
496 return voxelSizeX_; | |
497 } | |
498 | |
499 float Series::GetVoxelSizeY() | |
500 { | |
501 Check3DImage(); // Is3DImageInternal() will compute the voxel sizes | |
502 return voxelSizeY_; | |
503 } | |
504 | |
505 float Series::GetVoxelSizeZ() | |
506 { | |
507 Check3DImage(); // Is3DImageInternal() will compute the voxel sizes | |
508 return voxelSizeZ_; | |
509 } | |
510 | |
511 float Series::GetSliceThickness() | |
512 { | |
513 Check3DImage(); // Is3DImageInternal() will compute the voxel sizes | |
514 return sliceThickness_; | |
515 } | |
516 | |
517 void Series::Load3DImage(void* target, | |
518 Orthanc::PixelFormat format, | |
519 int64_t lineStride, | |
520 int64_t stackStride, | |
521 float* progress) | |
522 { | |
523 ProgressToFloatListener listener(progress); | |
524 Load3DImageInternal(target, format, static_cast<size_t>(lineStride), | |
525 static_cast<size_t>(stackStride), &listener); | |
526 } | |
527 } |