comparison OrthancServer/Sources/SliceOrdering.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/SliceOrdering.cpp@104e27133ebd
children 05b8fd21089c
comparison
equal deleted inserted replaced
4043:6c6239aec462 4044:d25f4c0fa160
1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2020 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 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 "PrecompiledHeadersServer.h"
35 #include "SliceOrdering.h"
36
37 #include "../Core/Logging.h"
38 #include "../Core/Toolbox.h"
39 #include "ServerEnumerations.h"
40 #include "ServerIndex.h"
41
42 #include <algorithm>
43 #include <boost/lexical_cast.hpp>
44 #include <boost/noncopyable.hpp>
45
46
47 namespace Orthanc
48 {
49 static bool TokenizeVector(std::vector<float>& result,
50 const std::string& value,
51 unsigned int expectedSize)
52 {
53 std::vector<std::string> tokens;
54 Toolbox::TokenizeString(tokens, value, '\\');
55
56 if (tokens.size() != expectedSize)
57 {
58 return false;
59 }
60
61 result.resize(tokens.size());
62
63 for (size_t i = 0; i < tokens.size(); i++)
64 {
65 try
66 {
67 result[i] = boost::lexical_cast<float>(tokens[i]);
68 }
69 catch (boost::bad_lexical_cast&)
70 {
71 return false;
72 }
73 }
74
75 return true;
76 }
77
78
79 static bool TokenizeVector(std::vector<float>& result,
80 const DicomMap& map,
81 const DicomTag& tag,
82 unsigned int expectedSize)
83 {
84 const DicomValue* value = map.TestAndGetValue(tag);
85
86 if (value == NULL ||
87 value->IsNull() ||
88 value->IsBinary())
89 {
90 return false;
91 }
92 else
93 {
94 return TokenizeVector(result, value->GetContent(), expectedSize);
95 }
96 }
97
98
99 static bool IsCloseToZero(double x)
100 {
101 return fabs(x) < 10.0 * std::numeric_limits<float>::epsilon();
102 }
103
104
105 bool SliceOrdering::ComputeNormal(Vector& normal,
106 const DicomMap& dicom)
107 {
108 std::vector<float> cosines;
109
110 if (TokenizeVector(cosines, dicom, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, 6))
111 {
112 assert(cosines.size() == 6);
113 normal[0] = cosines[1] * cosines[5] - cosines[2] * cosines[4];
114 normal[1] = cosines[2] * cosines[3] - cosines[0] * cosines[5];
115 normal[2] = cosines[0] * cosines[4] - cosines[1] * cosines[3];
116 return true;
117 }
118 else
119 {
120 return false;
121 }
122 }
123
124
125 bool SliceOrdering::IsParallelOrOpposite(const Vector& u,
126 const Vector& v)
127 {
128 // Check out "GeometryToolbox::IsParallelOrOpposite()" in Stone of
129 // Orthanc for explanations
130 const double u1 = u[0];
131 const double u2 = u[1];
132 const double u3 = u[2];
133 const double normU = sqrt(u1 * u1 + u2 * u2 + u3 * u3);
134
135 const double v1 = v[0];
136 const double v2 = v[1];
137 const double v3 = v[2];
138 const double normV = sqrt(v1 * v1 + v2 * v2 + v3 * v3);
139
140 if (IsCloseToZero(normU * normV))
141 {
142 return false;
143 }
144 else
145 {
146 const double cosAngle = (u1 * v1 + u2 * v2 + u3 * v3) / (normU * normV);
147
148 return (IsCloseToZero(cosAngle - 1.0) || // Close to +1: Parallel, non-opposite
149 IsCloseToZero(fabs(cosAngle) - 1.0)); // Close to -1: Parallel, opposite
150 }
151 }
152
153
154 struct SliceOrdering::Instance : public boost::noncopyable
155 {
156 private:
157 std::string instanceId_;
158 bool hasPosition_;
159 Vector position_;
160 bool hasNormal_;
161 Vector normal_;
162 bool hasIndexInSeries_;
163 size_t indexInSeries_;
164 unsigned int framesCount_;
165
166 public:
167 Instance(ServerIndex& index,
168 const std::string& instanceId) :
169 instanceId_(instanceId),
170 framesCount_(1)
171 {
172 DicomMap instance;
173 if (!index.GetMainDicomTags(instance, instanceId, ResourceType_Instance, ResourceType_Instance))
174 {
175 throw OrthancException(ErrorCode_UnknownResource);
176 }
177
178 const DicomValue* frames = instance.TestAndGetValue(DICOM_TAG_NUMBER_OF_FRAMES);
179 if (frames != NULL &&
180 !frames->IsNull() &&
181 !frames->IsBinary())
182 {
183 try
184 {
185 framesCount_ = boost::lexical_cast<unsigned int>(frames->GetContent());
186 }
187 catch (boost::bad_lexical_cast&)
188 {
189 }
190 }
191
192 std::vector<float> tmp;
193 hasPosition_ = TokenizeVector(tmp, instance, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
194
195 if (hasPosition_)
196 {
197 position_[0] = tmp[0];
198 position_[1] = tmp[1];
199 position_[2] = tmp[2];
200 }
201
202 hasNormal_ = ComputeNormal(normal_, instance);
203
204 std::string s;
205 hasIndexInSeries_ = false;
206
207 try
208 {
209 if (index.LookupMetadata(s, instanceId, MetadataType_Instance_IndexInSeries))
210 {
211 indexInSeries_ = boost::lexical_cast<size_t>(s);
212 hasIndexInSeries_ = true;
213 }
214 }
215 catch (boost::bad_lexical_cast&)
216 {
217 }
218 }
219
220 const std::string& GetIdentifier() const
221 {
222 return instanceId_;
223 }
224
225 bool HasPosition() const
226 {
227 return hasPosition_;
228 }
229
230 float ComputeRelativePosition(const Vector& normal) const
231 {
232 assert(HasPosition());
233 return (normal[0] * position_[0] +
234 normal[1] * position_[1] +
235 normal[2] * position_[2]);
236 }
237
238 bool HasIndexInSeries() const
239 {
240 return hasIndexInSeries_;
241 }
242
243 size_t GetIndexInSeries() const
244 {
245 assert(HasIndexInSeries());
246 return indexInSeries_;
247 }
248
249 unsigned int GetFramesCount() const
250 {
251 return framesCount_;
252 }
253
254 bool HasNormal() const
255 {
256 return hasNormal_;
257 }
258
259 const Vector& GetNormal() const
260 {
261 assert(hasNormal_);
262 return normal_;
263 }
264 };
265
266
267 class SliceOrdering::PositionComparator
268 {
269 private:
270 const Vector& normal_;
271
272 public:
273 PositionComparator(const Vector& normal) : normal_(normal)
274 {
275 }
276
277 int operator() (const Instance* a,
278 const Instance* b) const
279 {
280 return a->ComputeRelativePosition(normal_) < b->ComputeRelativePosition(normal_);
281 }
282 };
283
284
285 bool SliceOrdering::IndexInSeriesComparator(const SliceOrdering::Instance* a,
286 const SliceOrdering::Instance* b)
287 {
288 return a->GetIndexInSeries() < b->GetIndexInSeries();
289 }
290
291
292 void SliceOrdering::ComputeNormal()
293 {
294 DicomMap series;
295 if (!index_.GetMainDicomTags(series, seriesId_, ResourceType_Series, ResourceType_Series))
296 {
297 throw OrthancException(ErrorCode_UnknownResource);
298 }
299
300 hasNormal_ = ComputeNormal(normal_, series);
301 }
302
303
304 void SliceOrdering::CreateInstances()
305 {
306 std::list<std::string> instancesId;
307 index_.GetChildren(instancesId, seriesId_);
308
309 instances_.reserve(instancesId.size());
310 for (std::list<std::string>::const_iterator
311 it = instancesId.begin(); it != instancesId.end(); ++it)
312 {
313 instances_.push_back(new Instance(index_, *it));
314 }
315 }
316
317
318 bool SliceOrdering::SortUsingPositions()
319 {
320 if (instances_.size() <= 1)
321 {
322 // One single instance: It is sorted by default
323 return true;
324 }
325
326 if (!hasNormal_)
327 {
328 return false;
329 }
330
331 for (size_t i = 0; i < instances_.size(); i++)
332 {
333 assert(instances_[i] != NULL);
334
335 if (!instances_[i]->HasPosition() ||
336 (instances_[i]->HasNormal() &&
337 !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_)))
338 {
339 return false;
340 }
341 }
342
343 PositionComparator comparator(normal_);
344 std::sort(instances_.begin(), instances_.end(), comparator);
345
346 float a = instances_[0]->ComputeRelativePosition(normal_);
347 for (size_t i = 1; i < instances_.size(); i++)
348 {
349 float b = instances_[i]->ComputeRelativePosition(normal_);
350
351 if (std::fabs(b - a) <= 10.0f * std::numeric_limits<float>::epsilon())
352 {
353 // Not enough space between two slices along the normal of the volume
354 return false;
355 }
356
357 a = b;
358 }
359
360 // This is a 3D volume
361 isVolume_ = true;
362 return true;
363 }
364
365
366 bool SliceOrdering::SortUsingIndexInSeries()
367 {
368 if (instances_.size() <= 1)
369 {
370 // One single instance: It is sorted by default
371 return true;
372 }
373
374 for (size_t i = 0; i < instances_.size(); i++)
375 {
376 assert(instances_[i] != NULL);
377 if (!instances_[i]->HasIndexInSeries())
378 {
379 return false;
380 }
381 }
382
383 std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator);
384
385 for (size_t i = 1; i < instances_.size(); i++)
386 {
387 if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries())
388 {
389 // The current "IndexInSeries" occurs 2 times: Not a proper ordering
390 LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway";
391 break;
392 }
393 }
394
395 return true;
396 }
397
398
399 SliceOrdering::SliceOrdering(ServerIndex& index,
400 const std::string& seriesId) :
401 index_(index),
402 seriesId_(seriesId),
403 isVolume_(false)
404 {
405 ComputeNormal();
406 CreateInstances();
407
408 if (!SortUsingPositions() &&
409 !SortUsingIndexInSeries())
410 {
411 throw OrthancException(ErrorCode_CannotOrderSlices,
412 "Unable to order the slices of series " + seriesId);
413 }
414 }
415
416
417 SliceOrdering::~SliceOrdering()
418 {
419 for (std::vector<Instance*>::iterator
420 it = instances_.begin(); it != instances_.end(); ++it)
421 {
422 if (*it != NULL)
423 {
424 delete *it;
425 }
426 }
427 }
428
429
430 const std::string& SliceOrdering::GetInstanceId(size_t index) const
431 {
432 if (index >= instances_.size())
433 {
434 throw OrthancException(ErrorCode_ParameterOutOfRange);
435 }
436 else
437 {
438 return instances_[index]->GetIdentifier();
439 }
440 }
441
442
443 unsigned int SliceOrdering::GetFramesCount(size_t index) const
444 {
445 if (index >= instances_.size())
446 {
447 throw OrthancException(ErrorCode_ParameterOutOfRange);
448 }
449 else
450 {
451 return instances_[index]->GetFramesCount();
452 }
453 }
454
455
456 void SliceOrdering::Format(Json::Value& result) const
457 {
458 result = Json::objectValue;
459 result["Type"] = (isVolume_ ? "Volume" : "Sequence");
460
461 Json::Value tmp = Json::arrayValue;
462 for (size_t i = 0; i < GetInstancesCount(); i++)
463 {
464 tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file");
465 }
466
467 result["Dicom"] = tmp;
468
469 Json::Value slicesShort = Json::arrayValue;
470
471 tmp.clear();
472 for (size_t i = 0; i < GetInstancesCount(); i++)
473 {
474 std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i));
475 for (size_t j = 0; j < GetFramesCount(i); j++)
476 {
477 tmp.append(base + "/frames/" + boost::lexical_cast<std::string>(j));
478 }
479
480 Json::Value tmp2 = Json::arrayValue;
481 tmp2.append(GetInstanceId(i));
482 tmp2.append(0);
483 tmp2.append(GetFramesCount(i));
484
485 slicesShort.append(tmp2);
486 }
487
488 result["Slices"] = tmp;
489 result["SlicesShort"] = slicesShort;
490 }
491 }