Mercurial > hg > orthanc-wsi
comparison Applications/Dicomizer.cpp @ 0:4a7a53257c7d
initial commit
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Sat, 22 Oct 2016 21:48:33 +0200 |
parents | |
children | 62adabb8c122 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4a7a53257c7d |
---|---|
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 } |