0
|
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 *
|
|
6 * This program is free software: you can redistribute it and/or
|
|
7 * modify it under the terms of the GNU Affero General Public License
|
|
8 * as published by the Free Software Foundation, either version 3 of
|
|
9 * the License, or (at your option) any later version.
|
|
10 *
|
|
11 * This program is distributed in the hope that it will be useful, but
|
|
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
14 * Affero General Public License for more details.
|
|
15 *
|
|
16 * You should have received a copy of the GNU Affero General Public License
|
|
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18 **/
|
|
19
|
|
20
|
|
21 #include "../Framework/Algorithms/ReconstructPyramidCommand.h"
|
|
22 #include "../Framework/Algorithms/TranscodeTileCommand.h"
|
|
23 #include "../Framework/DicomToolbox.h"
|
|
24 #include "../Framework/DicomizerParameters.h"
|
|
25 #include "../Framework/ImagedVolumeParameters.h"
|
|
26 #include "../Framework/Inputs/HierarchicalTiff.h"
|
|
27 #include "../Framework/Inputs/OpenSlidePyramid.h"
|
|
28 #include "../Framework/Inputs/TiledJpegImage.h"
|
|
29 #include "../Framework/Inputs/TiledPngImage.h"
|
|
30 #include "../Framework/Inputs/TiledPyramidStatistics.h"
|
|
31 #include "../Framework/Orthanc/Core/HttpClient.h"
|
|
32 #include "../Framework/Orthanc/Core/Logging.h"
|
|
33 #include "../Framework/Orthanc/Core/MultiThreading/BagOfTasksProcessor.h"
|
|
34 #include "../Framework/Orthanc/Core/Toolbox.h"
|
|
35 #include "../Framework/Orthanc/OrthancServer/FromDcmtkBridge.h"
|
|
36 #include "../Framework/Outputs/DicomPyramidWriter.h"
|
|
37 #include "../Framework/Outputs/TruncatedPyramidWriter.h"
|
|
38
|
|
39 #include "ApplicationToolbox.h"
|
|
40
|
|
41 #include <EmbeddedResources.h>
|
|
42
|
|
43 #include <boost/program_options.hpp>
|
|
44
|
|
45 #include <dcmtk/dcmdata/dcdeftag.h>
|
|
46 #include <dcmtk/dcmdata/dcuid.h>
|
|
47 #include <dcmtk/dcmdata/dcvrobow.h>
|
|
48 #include <dcmtk/dcmdata/dcvrat.h>
|
|
49
|
|
50
|
|
51 static void TranscodePyramid(OrthancWSI::PyramidWriterBase& target,
|
|
52 OrthancWSI::ITiledPyramid& source,
|
|
53 const OrthancWSI::DicomizerParameters& parameters)
|
|
54 {
|
|
55 Orthanc::BagOfTasks tasks;
|
|
56
|
|
57 for (unsigned int i = 0; i < source.GetLevelCount(); i++)
|
|
58 {
|
|
59 LOG(WARNING) << "Creating level " << i << " of size "
|
|
60 << source.GetLevelWidth(i) << "x" << source.GetLevelHeight(i);
|
|
61 target.AddLevel(source.GetLevelWidth(i), source.GetLevelHeight(i));
|
|
62 }
|
|
63
|
|
64 LOG(WARNING) << "Transcoding the source pyramid";
|
|
65
|
|
66 OrthancWSI::TranscodeTileCommand::PrepareBagOfTasks(tasks, target, source, parameters);
|
|
67 OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
|
|
68 }
|
|
69
|
|
70
|
|
71 static void ReconstructPyramid(OrthancWSI::PyramidWriterBase& target,
|
|
72 OrthancWSI::ITiledPyramid& source,
|
|
73 const OrthancWSI::DicomizerParameters& parameters)
|
|
74 {
|
|
75 Orthanc::BagOfTasks tasks;
|
|
76
|
|
77 unsigned int levelsCount = parameters.GetPyramidLevelsCount(target, source);
|
|
78 LOG(WARNING) << "The target pyramid will have " << levelsCount << " levels";
|
|
79 assert(levelsCount >= 1);
|
|
80
|
|
81 for (unsigned int i = 0; i < levelsCount; i++)
|
|
82 {
|
|
83 unsigned int width = OrthancWSI::CeilingDivision(source.GetLevelWidth(0), 1 << i);
|
|
84 unsigned int height = OrthancWSI::CeilingDivision(source.GetLevelHeight(0), 1 << i);
|
|
85
|
|
86 LOG(WARNING) << "Creating level " << i << " of size " << width << "x" << height;
|
|
87 target.AddLevel(width, height);
|
|
88 }
|
|
89
|
|
90 unsigned int lowerLevelsCount = parameters.GetPyramidLowerLevelsCount(target, source);
|
|
91 if (lowerLevelsCount > levelsCount)
|
|
92 {
|
|
93 LOG(WARNING) << "The number of lower levels (" << lowerLevelsCount
|
|
94 << ") exceeds the number of levels (" << levelsCount
|
|
95 << "), cropping it";
|
|
96 lowerLevelsCount = levelsCount;
|
|
97 }
|
|
98
|
|
99 assert(lowerLevelsCount <= levelsCount);
|
|
100 if (lowerLevelsCount != levelsCount)
|
|
101 {
|
|
102 LOG(WARNING) << "Constructing the " << lowerLevelsCount << " lower levels of the pyramid";
|
|
103 OrthancWSI::TruncatedPyramidWriter truncated(target, lowerLevelsCount);
|
|
104 OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
|
|
105 (tasks, truncated, source, lowerLevelsCount + 1, 0, parameters);
|
|
106 OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
|
|
107
|
|
108 assert(tasks.GetSize() == 0);
|
|
109
|
|
110 const unsigned int upperLevelsCount = levelsCount - lowerLevelsCount;
|
|
111 LOG(WARNING) << "Constructing the " << upperLevelsCount << " upper levels of the pyramid";
|
|
112 OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
|
|
113 (tasks, target, truncated.GetUpperLevel(),
|
|
114 upperLevelsCount, lowerLevelsCount, parameters);
|
|
115 OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
|
|
116 }
|
|
117 else
|
|
118 {
|
|
119 LOG(WARNING) << "Constructing the pyramid";
|
|
120 OrthancWSI::ReconstructPyramidCommand::PrepareBagOfTasks
|
|
121 (tasks, target, source, levelsCount, 0, parameters);
|
|
122 OrthancWSI::ApplicationToolbox::Execute(tasks, parameters.GetThreadsCount());
|
|
123 }
|
|
124 }
|
|
125
|
|
126
|
|
127 static void Recompress(OrthancWSI::IFileTarget& output,
|
|
128 OrthancWSI::ITiledPyramid& source,
|
|
129 const DcmDataset& dataset,
|
|
130 const OrthancWSI::DicomizerParameters& parameters,
|
|
131 const OrthancWSI::ImagedVolumeParameters& volume)
|
|
132 {
|
|
133 OrthancWSI::TiledPyramidStatistics stats(source);
|
|
134
|
|
135 LOG(WARNING) << "Size of source tiles: " << stats.GetTileWidth() << "x" << stats.GetTileHeight();
|
|
136 LOG(WARNING) << "Source image compression: " << OrthancWSI::EnumerationToString(stats.GetImageCompression());
|
|
137 LOG(WARNING) << "Pixel format: " << Orthanc::EnumerationToString(stats.GetPixelFormat());
|
|
138 LOG(WARNING) << "Smoothing is " << (parameters.IsSmoothEnabled() ? "enabled" : "disabled");
|
|
139
|
|
140 if (parameters.IsRepaintBackground())
|
|
141 {
|
|
142 LOG(WARNING) << "Repainting the background with color: ("
|
|
143 << static_cast<int>(parameters.GetBackgroundColorRed()) << ","
|
|
144 << static_cast<int>(parameters.GetBackgroundColorGreen()) << ","
|
|
145 << static_cast<int>(parameters.GetBackgroundColorBlue()) << ")";
|
|
146 }
|
|
147 else
|
|
148 {
|
|
149 LOG(WARNING) << "No repainting of the background";
|
|
150 }
|
|
151
|
|
152 OrthancWSI::DicomPyramidWriter target(output, dataset,
|
|
153 source.GetPixelFormat(),
|
|
154 parameters.GetTargetCompression(),
|
|
155 parameters.GetTargetTileWidth(source),
|
|
156 parameters.GetTargetTileHeight(source),
|
|
157 parameters.GetDicomMaxFileSize(),
|
|
158 volume);
|
|
159
|
|
160 LOG(WARNING) << "Size of target tiles: " << target.GetTileWidth() << "x" << target.GetTileHeight();
|
|
161
|
|
162 if (target.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg)
|
|
163 {
|
|
164 LOG(WARNING) << "Target image compression: Jpeg with quality " << static_cast<int>(target.GetJpegQuality());
|
|
165 target.SetJpegQuality(target.GetJpegQuality());
|
|
166 }
|
|
167 else
|
|
168 {
|
|
169 LOG(WARNING) << "Target image compression: " << OrthancWSI::EnumerationToString(target.GetImageCompression());
|
|
170 }
|
|
171
|
|
172 if (stats.GetTileWidth() % target.GetTileWidth() != 0 ||
|
|
173 stats.GetTileHeight() % target.GetTileHeight() != 0)
|
|
174 {
|
|
175 LOG(ERROR) << "When resampling the tile size, it must be a integer divisor of the original tile size";
|
|
176 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
|
|
177 }
|
|
178
|
|
179 if (target.GetTileWidth() <= 16 ||
|
|
180 target.GetTileHeight() <= 16)
|
|
181 {
|
|
182 LOG(ERROR) << "Tiles are too small (16 pixels minimum): "
|
|
183 << target.GetTileWidth() << "x" << target.GetTileHeight();
|
|
184 throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageSize);
|
|
185 }
|
|
186
|
|
187 if (parameters.IsReconstructPyramid())
|
|
188 {
|
|
189 ReconstructPyramid(target, stats, parameters);
|
|
190 }
|
|
191 else
|
|
192 {
|
|
193 TranscodePyramid(target, stats, parameters);
|
|
194 }
|
|
195
|
|
196 target.Flush();
|
|
197 }
|
|
198
|
|
199
|
|
200
|
|
201 static DcmDataset* ParseDataset(const std::string& path)
|
|
202 {
|
|
203 Json::Value json;
|
|
204
|
|
205 if (path.empty())
|
|
206 {
|
|
207 json = Json::objectValue; // Empty dataset => TODO EMBED
|
|
208 }
|
|
209 else
|
|
210 {
|
|
211 std::string content;
|
|
212 Orthanc::Toolbox::ReadFile(content, path);
|
|
213
|
|
214 Json::Reader reader;
|
|
215 if (!reader.parse(content, json, false))
|
|
216 {
|
|
217 LOG(ERROR) << "Cannot parse the JSON file in: " << path;
|
|
218 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
219 }
|
|
220 }
|
|
221
|
|
222 std::auto_ptr<DcmDataset> dataset(Orthanc::FromDcmtkBridge::FromJson(json, true, true, Orthanc::Encoding_Latin1));
|
|
223 if (dataset.get() == NULL)
|
|
224 {
|
|
225 LOG(ERROR) << "Cannot convert to JSON file to a DICOM dataset: " << path;
|
|
226 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
227 }
|
|
228
|
|
229 // VL Whole Slide Microscopy Image IOD
|
|
230 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.77.1.6");
|
|
231
|
|
232 // Slide Microscopy
|
|
233 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_Modality, "SM");
|
|
234
|
|
235 // Patient orientation makes no sense in whole-slide images
|
|
236 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_PatientOrientation, "");
|
|
237
|
|
238 // Some basic coordinate information
|
|
239 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_VolumetricProperties, "VOLUME");
|
|
240 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ImageOrientationSlide, "0\\-1\\0\\-1\\0\\0");
|
|
241
|
|
242 std::string date, time;
|
|
243 Orthanc::Toolbox::GetNowDicom(date, time);
|
|
244 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_StudyDate, date);
|
|
245 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_StudyTime, time);
|
|
246 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SeriesDate, date);
|
|
247 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_SeriesTime, time);
|
|
248 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ContentDate, date);
|
|
249 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_ContentTime, time);
|
|
250 OrthancWSI::DicomToolbox::SetStringTag(*dataset, DCM_AcquisitionDateTime, date + time);
|
|
251
|
|
252 return dataset.release();
|
|
253 }
|
|
254
|
|
255
|
|
256
|
|
257 static void SetupDimension(DcmDataset& dataset,
|
|
258 const std::string& opticalPathId,
|
|
259 const OrthancWSI::ITiledPyramid& source,
|
|
260 const OrthancWSI::ImagedVolumeParameters& volume)
|
|
261 {
|
|
262 std::string uid;
|
|
263 DcmItem* previous = OrthancWSI::DicomToolbox::ExtractSingleSequenceItem(dataset, DCM_DimensionOrganizationSequence);
|
|
264
|
|
265 if (previous != NULL)
|
|
266 {
|
|
267 const char* tmp = NULL;
|
|
268 if (previous->findAndGetString(DCM_DimensionOrganizationUID, tmp).good() &&
|
|
269 tmp != NULL)
|
|
270 {
|
|
271 uid.assign(tmp);
|
|
272 }
|
|
273 }
|
|
274
|
|
275 if (uid.empty())
|
|
276 {
|
|
277 // Generate an unique identifier for the Dimension Organization
|
|
278 uid = Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Instance);
|
|
279 }
|
|
280
|
|
281 dataset.remove(DCM_DimensionIndexSequence);
|
|
282
|
|
283 std::auto_ptr<DcmItem> item(new DcmItem);
|
|
284 std::auto_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_DimensionOrganizationSequence));
|
|
285
|
|
286 if (!item->putAndInsertString(DCM_DimensionOrganizationUID, uid.c_str()).good() ||
|
|
287 !sequence->insert(item.release(), false, false).good() ||
|
|
288 !dataset.insert(sequence.release(), true, false).good())
|
|
289 {
|
|
290 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
291 }
|
|
292
|
|
293 item.reset(new DcmItem);
|
|
294 sequence.reset(new DcmSequenceOfItems(DCM_DimensionIndexSequence));
|
|
295
|
|
296 std::auto_ptr<DcmAttributeTag> a1(new DcmAttributeTag(DCM_FunctionalGroupPointer));
|
|
297 std::auto_ptr<DcmAttributeTag> a2(new DcmAttributeTag(DCM_DimensionIndexPointer));
|
|
298
|
|
299 if (!item->putAndInsertString(DCM_DimensionOrganizationUID, uid.c_str()).good() ||
|
|
300 !a1->putTagVal(DCM_FrameContentSequence).good() ||
|
|
301 !a2->putTagVal(DCM_DimensionIndexValues).good() ||
|
|
302 !item->insert(a1.release(), uid.c_str()).good() ||
|
|
303 !item->insert(a2.release(), uid.c_str()).good() ||
|
|
304 !sequence->insert(item.release(), false, false).good() ||
|
|
305 !dataset.insert(sequence.release(), true, false).good())
|
|
306 {
|
|
307 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
308 }
|
|
309
|
|
310 float spacingX = volume.GetWidth() / static_cast<float>(source.GetLevelHeight(0)); // Remember to switch X/Y!
|
|
311 float spacingY = volume.GetHeight() / static_cast<float>(source.GetLevelWidth(0)); // Remember to switch X/Y!
|
|
312 std::string spacing = (boost::lexical_cast<std::string>(spacingX) + '\\' +
|
|
313 boost::lexical_cast<std::string>(spacingY));
|
|
314
|
|
315 item.reset(new DcmItem);
|
|
316 sequence.reset(new DcmSequenceOfItems(DCM_SharedFunctionalGroupsSequence));
|
|
317 std::auto_ptr<DcmItem> item2(new DcmItem);
|
|
318 std::auto_ptr<DcmItem> item3(new DcmItem);
|
|
319 std::auto_ptr<DcmSequenceOfItems> sequence2(new DcmSequenceOfItems(DCM_PixelMeasuresSequence));
|
|
320 std::auto_ptr<DcmSequenceOfItems> sequence3(new DcmSequenceOfItems(DCM_OpticalPathIdentificationSequence));
|
|
321
|
|
322 OrthancWSI::DicomToolbox::SetStringTag(*item2, DCM_SliceThickness, boost::lexical_cast<std::string>(volume.GetDepth()));
|
|
323 OrthancWSI::DicomToolbox::SetStringTag(*item2, DCM_PixelSpacing, spacing);
|
|
324 OrthancWSI::DicomToolbox::SetStringTag(*item3, DCM_OpticalPathIdentifier, opticalPathId);
|
|
325
|
|
326 if (!sequence2->insert(item2.release(), false, false).good() ||
|
|
327 !sequence3->insert(item3.release(), false, false).good() ||
|
|
328 !item->insert(sequence2.release(), false, false).good() ||
|
|
329 !item->insert(sequence3.release(), false, false).good() ||
|
|
330 !sequence->insert(item.release(), false, false).good() ||
|
|
331 !dataset.insert(sequence.release(), true, false).good())
|
|
332 {
|
|
333 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
334 }
|
|
335 }
|
|
336
|
|
337
|
|
338 static void EnrichDataset(DcmDataset& dataset,
|
|
339 const OrthancWSI::ITiledPyramid& source,
|
|
340 const OrthancWSI::DicomizerParameters& parameters,
|
|
341 const OrthancWSI::ImagedVolumeParameters& volume)
|
|
342 {
|
|
343 Orthanc::Encoding encoding = Orthanc::FromDcmtkBridge::DetectEncoding(dataset, Orthanc::Encoding_Latin1);
|
|
344
|
|
345 if (source.GetImageCompression() == OrthancWSI::ImageCompression_Jpeg ||
|
|
346 parameters.GetTargetCompression() == OrthancWSI::ImageCompression_Jpeg)
|
|
347 {
|
|
348 // Takes as estimation a 1:10 compression ratio
|
|
349 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompression, "01");
|
|
350 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompressionRatio, "10");
|
|
351 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompressionMethod, "ISO_10918_1"); // JPEG Lossy Compression
|
|
352 }
|
|
353 else
|
|
354 {
|
|
355 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_LossyImageCompression, "00");
|
|
356 }
|
|
357
|
|
358 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeWidth, boost::lexical_cast<std::string>(volume.GetWidth()));
|
|
359 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeHeight, boost::lexical_cast<std::string>(volume.GetHeight()));
|
|
360 OrthancWSI::DicomToolbox::SetStringTag(dataset, DCM_ImagedVolumeDepth, boost::lexical_cast<std::string>(volume.GetDepth()));
|
|
361
|
|
362 std::auto_ptr<DcmItem> origin(new DcmItem);
|
|
363 OrthancWSI::DicomToolbox::SetStringTag(*origin, DCM_XOffsetInSlideCoordinateSystem,
|
|
364 boost::lexical_cast<std::string>(volume.GetOffsetX()));
|
|
365 OrthancWSI::DicomToolbox::SetStringTag(*origin, DCM_YOffsetInSlideCoordinateSystem,
|
|
366 boost::lexical_cast<std::string>(volume.GetOffsetY()));
|
|
367
|
|
368 std::auto_ptr<DcmSequenceOfItems> sequenceOrigin(new DcmSequenceOfItems(DCM_TotalPixelMatrixOriginSequence));
|
|
369 if (!sequenceOrigin->insert(origin.release(), false, false).good() ||
|
|
370 !dataset.insert(sequenceOrigin.release(), false, false).good())
|
|
371 {
|
|
372 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
373 }
|
|
374
|
|
375
|
|
376 if (parameters.GetOpticalPath() == OrthancWSI::OpticalPath_Brightfield)
|
|
377 {
|
|
378 if (dataset.tagExists(DCM_OpticalPathSequence))
|
|
379 {
|
|
380 LOG(ERROR) << "The user DICOM dataset already contains an optical path sequence, giving up";
|
|
381 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
382 }
|
|
383
|
|
384 std::string brightfield;
|
|
385 Orthanc::EmbeddedResources::GetFileResource(brightfield, Orthanc::EmbeddedResources::BRIGHTFIELD_OPTICAL_PATH);
|
|
386
|
|
387 Json::Value json;
|
|
388 Json::Reader reader;
|
|
389 if (!reader.parse(brightfield, json, false))
|
|
390 {
|
|
391 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
392 }
|
|
393
|
|
394 std::auto_ptr<DcmElement> element(Orthanc::FromDcmtkBridge::FromJson(
|
|
395 Orthanc::DicomTag(DCM_OpticalPathSequence.getGroup(),
|
|
396 DCM_OpticalPathSequence.getElement()),
|
|
397 json, false, encoding));
|
|
398 if (!dataset.insert(element.release()).good())
|
|
399 {
|
|
400 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
401 }
|
|
402 }
|
|
403
|
|
404
|
|
405 std::string profile;
|
|
406 if (parameters.GetIccProfilePath().empty())
|
|
407 {
|
|
408 Orthanc::EmbeddedResources::GetFileResource(profile, Orthanc::EmbeddedResources::SRGB_ICC_PROFILE);
|
|
409 }
|
|
410 else
|
|
411 {
|
|
412 Orthanc::Toolbox::ReadFile(profile, parameters.GetIccProfilePath());
|
|
413 }
|
|
414
|
|
415
|
|
416 DcmItem* opticalPath = OrthancWSI::DicomToolbox::ExtractSingleSequenceItem(dataset, DCM_OpticalPathSequence);
|
|
417 if (opticalPath == NULL)
|
|
418 {
|
|
419 LOG(ERROR) << "No optical path specified";
|
|
420 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
421 }
|
|
422
|
|
423 if (!opticalPath->tagExists(DCM_ICCProfile))
|
|
424 {
|
|
425 std::auto_ptr<DcmOtherByteOtherWord> icc(new DcmOtherByteOtherWord(DCM_ICCProfile));
|
|
426
|
|
427 if (!icc->putUint8Array(reinterpret_cast<const Uint8*>(profile.c_str()), profile.size()).good() ||
|
|
428 !opticalPath->insert(icc.release()).good())
|
|
429 {
|
|
430 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
|
431 }
|
|
432 }
|
|
433
|
|
434 const char* opticalPathId = NULL;
|
|
435 if (!opticalPath->findAndGetString(DCM_OpticalPathIdentifier, opticalPathId).good() ||
|
|
436 opticalPathId == NULL)
|
|
437 {
|
|
438 LOG(ERROR) << "No identifier in the optical path";
|
|
439 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
440 }
|
|
441
|
|
442 SetupDimension(dataset, opticalPathId, source, volume);
|
|
443 }
|
|
444
|
|
445
|
|
446 static bool ParseParameters(int& exitStatus,
|
|
447 OrthancWSI::DicomizerParameters& parameters,
|
|
448 OrthancWSI::ImagedVolumeParameters& volume,
|
|
449 int argc,
|
|
450 char* argv[])
|
|
451 {
|
|
452 // Declare the supported parameters
|
|
453 boost::program_options::options_description generic("Generic options");
|
|
454 generic.add_options()
|
|
455 ("help", "Display this help and exit")
|
|
456 ("verbose", "Be verbose in logs")
|
|
457 ("threads", boost::program_options::value<int>()->default_value(parameters.GetThreadsCount()),
|
|
458 "Number of processing threads to be used")
|
|
459 ("openslide", boost::program_options::value<std::string>(),
|
|
460 "Path to the shared library of OpenSlide (not necessary if converting from standard hierarchical TIFF)")
|
|
461 ;
|
|
462
|
|
463 boost::program_options::options_description source("Options for the source image");
|
|
464 source.add_options()
|
|
465 ("dataset", boost::program_options::value<std::string>(), "Path to a JSON file containing the DICOM dataset")
|
|
466 ("sample-dataset", "Display a minimalistic sample DICOM dataset in JSON format, then exit")
|
|
467 ("reencode", boost::program_options::value<bool>(), "Whether to reencode each tile (no transcoding, much slower) (Boolean)")
|
|
468 ("repaint", boost::program_options::value<bool>(), "Whether to repaint the background of the image (Boolean)")
|
|
469 ("color", boost::program_options::value<std::string>(), "Color of the background (e.g. \"255,0,0\")")
|
|
470 ;
|
|
471
|
|
472 boost::program_options::options_description pyramid("Options to construct the pyramid");
|
|
473 pyramid.add_options()
|
|
474 ("pyramid", boost::program_options::value<bool>()->default_value(false),
|
|
475 "Reconstruct the full pyramid (slow) (Boolean)")
|
|
476 ("smooth", boost::program_options::value<bool>()->default_value(false),
|
|
477 "Apply smoothing when reconstructing the pyramid "
|
|
478 "(slower, but higher quality) (Boolean)")
|
|
479 ("levels", boost::program_options::value<int>(), "Number of levels in the target pyramid")
|
|
480 ;
|
|
481
|
|
482 boost::program_options::options_description target("Options for the target image");
|
|
483 target.add_options()
|
|
484 ("tile-width", boost::program_options::value<int>(), "Width of the tiles in the target image")
|
|
485 ("tile-height", boost::program_options::value<int>(), "Height of the tiles in the target image")
|
|
486 ("compression", boost::program_options::value<std::string>(),
|
|
487 "Compression of the target image (\"none\", \"jpeg\" or \"jpeg2000\")")
|
|
488 ("jpeg-quality", boost::program_options::value<int>(), "Set quality level for JPEG (0..100)")
|
|
489 ("max-size", boost::program_options::value<int>()->default_value(10), "Maximum size per DICOM instance (in MB)")
|
|
490 ("folder", boost::program_options::value<std::string>(),
|
|
491 "Folder where to store the output DICOM instances")
|
|
492 ("folder-pattern", boost::program_options::value<std::string>()->default_value("wsi-%06d.dcm"),
|
|
493 "Pattern for the files in the output folder")
|
|
494 ("orthanc", boost::program_options::value<std::string>()->default_value("http://localhost:8042/"),
|
|
495 "URL to the REST API of the target Orthanc server")
|
|
496 ("username", boost::program_options::value<std::string>(), "Username for the target Orthanc server")
|
|
497 ("password", boost::program_options::value<std::string>(), "Password for the target Orthanc server")
|
|
498 ;
|
|
499
|
|
500 boost::program_options::options_description volumeOptions("Description of the imaged volume");
|
|
501 volumeOptions.add_options()
|
|
502 ("imaged-width", boost::program_options::value<float>()->default_value(15), "With of the specimen (in mm)")
|
|
503 ("imaged-height", boost::program_options::value<float>()->default_value(15), "Height of the specimen (in mm)")
|
|
504 ("imaged-depth", boost::program_options::value<float>()->default_value(1), "Depth of the specimen (in mm)")
|
|
505 ("offset-x", boost::program_options::value<float>()->default_value(20),
|
|
506 "X offset the specimen, wrt. slide coordinates origin (in mm)")
|
|
507 ("offset-y", boost::program_options::value<float>()->default_value(40),
|
|
508 "Y offset the specimen, wrt. slide coordinates origin (in mm)")
|
|
509 ;
|
|
510
|
|
511 boost::program_options::options_description advancedOptions("Advanced options");
|
|
512 advancedOptions.add_options()
|
|
513 ("optical-path", boost::program_options::value<std::string>()->default_value("brightfield"),
|
|
514 "Optical path to be automatically added to the DICOM dataset (\"none\" or \"brightfield\")")
|
|
515 ("icc-profile", boost::program_options::value<std::string>(),
|
|
516 "Path to the ICC profile to be included. If empty, a default sRGB profile will be added.")
|
|
517 ("safety", boost::program_options::value<bool>()->default_value(true),
|
|
518 "Whether to do additional checks to verify the source image is supported (might slow down) (Boolean)")
|
|
519 ("lower-levels", boost::program_options::value<int>(), "Number of pyramid levels up to which multithreading "
|
|
520 "should be applied (only for performance/memory tuning)")
|
|
521 ;
|
|
522
|
|
523 boost::program_options::options_description hidden;
|
|
524 hidden.add_options()
|
|
525 ("input", boost::program_options::value<std::string>(), "Input file");
|
|
526 ;
|
|
527
|
|
528 boost::program_options::options_description allWithoutHidden;
|
|
529 allWithoutHidden.add(generic).add(source).add(pyramid).add(target).add(volumeOptions).add(advancedOptions);
|
|
530
|
|
531 boost::program_options::options_description all = allWithoutHidden;
|
|
532 all.add(hidden);
|
|
533
|
|
534 boost::program_options::positional_options_description positional;
|
|
535 positional.add("input", 1);
|
|
536
|
|
537 boost::program_options::variables_map options;
|
|
538 bool error = false;
|
|
539
|
|
540 try
|
|
541 {
|
|
542 boost::program_options::store(boost::program_options::command_line_parser(argc, argv).
|
|
543 options(all).positional(positional).run(), options);
|
|
544 boost::program_options::notify(options);
|
|
545 }
|
|
546 catch (boost::program_options::error& e)
|
|
547 {
|
|
548 LOG(ERROR) << "Error while parsing the command-line arguments: " << e.what();
|
|
549 error = true;
|
|
550 }
|
|
551
|
|
552 if (!error &&
|
|
553 options.count("sample-dataset"))
|
|
554 {
|
|
555 std::string sample;
|
|
556 Orthanc::EmbeddedResources::GetFileResource(sample, Orthanc::EmbeddedResources::SAMPLE_DATASET);
|
|
557
|
|
558 std::cout << std::endl << sample << std::endl;
|
|
559
|
|
560 return false;
|
|
561 }
|
|
562
|
|
563 if (!error &&
|
|
564 options.count("help") == 0 &&
|
|
565 options.count("input") != 1)
|
|
566 {
|
|
567 LOG(ERROR) << "No input file was specified";
|
|
568 error = true;
|
|
569 }
|
|
570
|
|
571 if (error || options.count("help"))
|
|
572 {
|
|
573 std::cout << std::endl
|
|
574 << "Usage: " << argv[0] << " [OPTION]... [INPUT]"
|
|
575 << std::endl
|
|
576 << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research."
|
|
577 << std::endl << std::endl
|
|
578 << "Create a DICOM file from a digital pathology image."
|
|
579 << std::endl;
|
|
580
|
|
581 std::cout << allWithoutHidden << "\n";
|
|
582
|
|
583 if (error)
|
|
584 {
|
|
585 exitStatus = -1;
|
|
586 }
|
|
587
|
|
588 return false;
|
|
589 }
|
|
590
|
|
591 if (options.count("verbose"))
|
|
592 {
|
|
593 Orthanc::Logging::EnableInfoLevel(true);
|
|
594 }
|
|
595
|
|
596 if (options.count("openslide"))
|
|
597 {
|
|
598 OrthancWSI::OpenSlideLibrary::Initialize(options["openslide"].as<std::string>());
|
|
599 }
|
|
600
|
|
601 if (options.count("pyramid") &&
|
|
602 options["pyramid"].as<bool>())
|
|
603 {
|
|
604 parameters.SetReconstructPyramid(true);
|
|
605 }
|
|
606
|
|
607 if (options.count("smooth") &&
|
|
608 options["smooth"].as<bool>())
|
|
609 {
|
|
610 parameters.SetSmoothEnabled(true);
|
|
611 }
|
|
612
|
|
613 if (options.count("safety") &&
|
|
614 options["safety"].as<bool>())
|
|
615 {
|
|
616 parameters.SetSafetyCheck(true);
|
|
617 }
|
|
618
|
|
619 if (options.count("reencode") &&
|
|
620 options["reencode"].as<bool>())
|
|
621 {
|
|
622 parameters.SetForceReencode(true);
|
|
623 }
|
|
624
|
|
625 if (options.count("repaint") &&
|
|
626 options["repaint"].as<bool>())
|
|
627 {
|
|
628 parameters.SetRepaintBackground(true);
|
|
629 }
|
|
630
|
|
631 if (options.count("tile-width") ||
|
|
632 options.count("tile-height"))
|
|
633 {
|
|
634 int w = 0;
|
|
635 if (options.count("tile-width"))
|
|
636 {
|
|
637 w = options["tile-width"].as<int>();
|
|
638 }
|
|
639
|
|
640 unsigned int h = 0;
|
|
641 if (options.count("tile-height"))
|
|
642 {
|
|
643 h = options["tile-height"].as<int>();
|
|
644 }
|
|
645
|
|
646 if (w < 0 || h < 0)
|
|
647 {
|
|
648 LOG(ERROR) << "Negative target tile size specified: " << w << "x" << h;
|
|
649 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
|
|
650 }
|
|
651
|
|
652 parameters.SetTargetTileSize(w, h);
|
|
653 }
|
|
654
|
|
655 parameters.SetInputFile(options["input"].as<std::string>());
|
|
656
|
|
657 if (options.count("color"))
|
|
658 {
|
|
659 uint8_t r, g, b;
|
|
660 OrthancWSI::ApplicationToolbox::ParseColor(r, g, b, options["color"].as<std::string>());
|
|
661 parameters.SetBackgroundColor(r, g, b);
|
|
662 }
|
|
663
|
|
664 if (options.count("compression"))
|
|
665 {
|
|
666 std::string s = options["compression"].as<std::string>();
|
|
667 if (s == "none")
|
|
668 {
|
|
669 parameters.SetTargetCompression(OrthancWSI::ImageCompression_None);
|
|
670 }
|
|
671 else if (s == "jpeg")
|
|
672 {
|
|
673 parameters.SetTargetCompression(OrthancWSI::ImageCompression_Jpeg);
|
|
674 }
|
|
675 else if (s == "jpeg2000")
|
|
676 {
|
|
677 parameters.SetTargetCompression(OrthancWSI::ImageCompression_Jpeg2000);
|
|
678 }
|
|
679 else
|
|
680 {
|
|
681 LOG(ERROR) << "Unknown image compression for the target image: " << s;
|
|
682 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
|
|
683 }
|
|
684 }
|
|
685
|
|
686 if (options.count("jpeg-quality"))
|
|
687 {
|
|
688 parameters.SetJpegQuality(options["jpeg-quality"].as<int>());
|
|
689 }
|
|
690
|
|
691 if (options.count("levels"))
|
|
692 {
|
|
693 parameters.SetPyramidLevelsCount(options["levels"].as<int>());
|
|
694 }
|
|
695
|
|
696 if (options.count("lower-levels"))
|
|
697 {
|
|
698 parameters.SetPyramidLowerLevelsCount(options["lower-levels"].as<int>());
|
|
699 }
|
|
700
|
|
701 if (options.count("threads"))
|
|
702 {
|
|
703 parameters.SetThreadsCount(options["threads"].as<int>());
|
|
704 }
|
|
705
|
|
706 if (options.count("max-size"))
|
|
707 {
|
|
708 parameters.SetDicomMaxFileSize(options["max-size"].as<int>() * 1024 * 1024);
|
|
709 }
|
|
710
|
|
711 if (options.count("folder"))
|
|
712 {
|
|
713 parameters.SetTargetFolder(options["folder"].as<std::string>());
|
|
714 }
|
|
715
|
|
716 if (options.count("folder-pattern"))
|
|
717 {
|
|
718 parameters.SetTargetFolderPattern(options["folder-pattern"].as<std::string>());
|
|
719 }
|
|
720
|
|
721 if (options.count("orthanc"))
|
|
722 {
|
|
723 parameters.GetOrthancParameters().SetUrl(options["orthanc"].as<std::string>());
|
|
724
|
|
725 if (options.count("username") &&
|
|
726 options.count("password"))
|
|
727 {
|
|
728 parameters.GetOrthancParameters().SetUsername(options["username"].as<std::string>());
|
|
729 parameters.GetOrthancParameters().SetPassword(options["password"].as<std::string>());
|
|
730 }
|
|
731 }
|
|
732
|
|
733 if (options.count("dataset"))
|
|
734 {
|
|
735 parameters.SetDatasetPath(options["dataset"].as<std::string>());
|
|
736 }
|
|
737
|
|
738 if (options.count("imaged-width"))
|
|
739 {
|
|
740 volume.SetWidth(options["imaged-width"].as<float>());
|
|
741 }
|
|
742
|
|
743 if (options.count("imaged-height"))
|
|
744 {
|
|
745 volume.SetHeight(options["imaged-height"].as<float>());
|
|
746 }
|
|
747
|
|
748 if (options.count("imaged-depth"))
|
|
749 {
|
|
750 volume.SetDepth(options["imaged-depth"].as<float>());
|
|
751 }
|
|
752
|
|
753 if (options.count("offset-x"))
|
|
754 {
|
|
755 volume.SetOffsetX(options["offset-x"].as<float>());
|
|
756 }
|
|
757
|
|
758 if (options.count("offset-y"))
|
|
759 {
|
|
760 volume.SetOffsetY(options["offset-y"].as<float>());
|
|
761 }
|
|
762
|
|
763 if (options.count("optical-path"))
|
|
764 {
|
|
765 std::string s = options["optical-path"].as<std::string>();
|
|
766 if (s == "none")
|
|
767 {
|
|
768 parameters.SetOpticalPath(OrthancWSI::OpticalPath_None);
|
|
769 }
|
|
770 else if (s == "brightfield")
|
|
771 {
|
|
772 parameters.SetOpticalPath(OrthancWSI::OpticalPath_Brightfield);
|
|
773 }
|
|
774 else
|
|
775 {
|
|
776 LOG(ERROR) << "Unknown optical path definition: " << s;
|
|
777 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
|
|
778 }
|
|
779 }
|
|
780
|
|
781 if (options.count("icc-profile"))
|
|
782 {
|
|
783 parameters.SetIccProfilePath(options["icc-profile"].as<std::string>());
|
|
784 }
|
|
785
|
|
786 return true;
|
|
787 }
|
|
788
|
|
789
|
|
790 OrthancWSI::ITiledPyramid* OpenInputPyramid(const std::string& path,
|
|
791 const OrthancWSI::DicomizerParameters& parameters)
|
|
792 {
|
|
793 LOG(WARNING) << "The input image is: " << path;
|
|
794
|
|
795 OrthancWSI::ImageCompression format = OrthancWSI::DetectFormatFromFile(path);
|
|
796 LOG(WARNING) << "File format of the input image: " << EnumerationToString(format);
|
|
797
|
|
798 switch (format)
|
|
799 {
|
|
800 case OrthancWSI::ImageCompression_Png:
|
|
801 return new OrthancWSI::TiledPngImage(path,
|
|
802 parameters.GetTargetTileWidth(512),
|
|
803 parameters.GetTargetTileHeight(512));
|
|
804
|
|
805 case OrthancWSI::ImageCompression_Jpeg:
|
|
806 return new OrthancWSI::TiledJpegImage(path,
|
|
807 parameters.GetTargetTileWidth(512),
|
|
808 parameters.GetTargetTileHeight(512));
|
|
809
|
|
810 case OrthancWSI::ImageCompression_Tiff:
|
|
811 {
|
|
812 try
|
|
813 {
|
|
814 return new OrthancWSI::HierarchicalTiff(path);
|
|
815 }
|
|
816 catch (Orthanc::OrthancException&)
|
|
817 {
|
|
818 LOG(WARNING) << "This is not a standard hierarchical TIFF file";
|
|
819 }
|
|
820 }
|
|
821
|
|
822 default:
|
|
823 break;
|
|
824 }
|
|
825
|
|
826 try
|
|
827 {
|
|
828 LOG(WARNING) << "Trying to open the input pyramid with OpenSlide";
|
|
829 return new OrthancWSI::OpenSlidePyramid(path,
|
|
830 parameters.GetTargetTileWidth(512),
|
|
831 parameters.GetTargetTileHeight(512));
|
|
832 }
|
|
833 catch (Orthanc::OrthancException&)
|
|
834 {
|
|
835 LOG(ERROR) << "This file is not supported by OpenSlide";
|
|
836 return NULL;
|
|
837 }
|
|
838 }
|
|
839
|
|
840
|
|
841 int main(int argc, char* argv[])
|
|
842 {
|
|
843 OrthancWSI::ApplicationToolbox::GlobalInitialize();
|
|
844
|
|
845 int exitStatus = 0;
|
|
846
|
|
847 try
|
|
848 {
|
|
849 OrthancWSI::DicomizerParameters parameters;
|
|
850 OrthancWSI::ImagedVolumeParameters volume;
|
|
851
|
|
852 if (ParseParameters(exitStatus, parameters, volume, argc, argv))
|
|
853 {
|
|
854 std::auto_ptr<OrthancWSI::ITiledPyramid> source(OpenInputPyramid(parameters.GetInputFile(), parameters));
|
|
855 if (source.get() == NULL)
|
|
856 {
|
|
857 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
|
|
858 }
|
|
859
|
|
860 // Create the shared DICOM tags
|
|
861 std::auto_ptr<DcmDataset> dataset(ParseDataset(parameters.GetDatasetPath()));
|
|
862 EnrichDataset(*dataset, *source, parameters, volume);
|
|
863
|
|
864 std::auto_ptr<OrthancWSI::IFileTarget> output(parameters.CreateTarget());
|
|
865 Recompress(*output, *source, *dataset, parameters, volume);
|
|
866 }
|
|
867 }
|
|
868 catch (Orthanc::OrthancException& e)
|
|
869 {
|
|
870 LOG(ERROR) << "Terminating on exception: " << e.What();
|
|
871 exitStatus = -1;
|
|
872 }
|
|
873
|
|
874 OrthancWSI::ApplicationToolbox::GlobalFinalize();
|
|
875
|
|
876 return exitStatus;
|
|
877 }
|