1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4 
5 #include <OpenImageIO/fmath.h>
6 #include <OpenImageIO/imageio.h>
7 
8 #include <iostream>
9 #include <memory>
10 #include <set>
11 
12 #define HAVE_CONFIG_H /* Sometimes DCMTK seems to need this */
13 #include <dcmtk/config/osconfig.h>
14 #include <dcmtk/dcmdata/dctk.h>
15 #include <dcmtk/dcmimage/dicopx.h>
16 #include <dcmtk/dcmimage/diregist.h>
17 #include <dcmtk/dcmimgle/dcmimage.h>
18 
19 
20 // This plugin utilises DCMTK:
21 //   http://dicom.offis.de/
22 //   http://support.dcmtk.org/docs/index.html
23 //
24 // General information about DICOM:
25 //   http://dicom.nema.org/standard.html
26 //
27 // Sources of sample images:
28 //   http://www.osirix-viewer.com/resources/dicom-image-library/
29 //   http://barre.nom.fr/medical/samples/
30 
31 
32 
33 OIIO_PLUGIN_NAMESPACE_BEGIN
34 
35 class DICOMInput final : public ImageInput {
36 public:
DICOMInput()37     DICOMInput() {}
~DICOMInput()38     virtual ~DICOMInput() { close(); }
format_name(void) const39     virtual const char* format_name(void) const override { return "dicom"; }
supports(string_view) const40     virtual int supports(string_view /*feature*/) const override
41     {
42         return false;  // we don't support any optional features
43     }
44     virtual bool open(const std::string& name, ImageSpec& newspec) override;
45     virtual bool open(const std::string& name, ImageSpec& newspec,
46                       const ImageSpec& config) override;
47     virtual bool close() override;
48     virtual bool seek_subimage(int subimage, int miplevel) override;
49     virtual bool read_native_scanline(int subimage, int miplevel, int y, int z,
50                                       void* data) override;
51 
52 private:
53     std::unique_ptr<DicomImage> m_img;
54     int m_framecount, m_firstframe;
55     int m_bitspersample;
56     std::string m_filename;
57     int m_subimage              = -1;
58     const DiPixel* m_dipixel    = nullptr;
59     const char* m_internal_data = nullptr;
60 
61     void read_metadata();
62 };
63 
64 
65 
66 // Export version number and create function symbols
67 OIIO_PLUGIN_EXPORTS_BEGIN
68 
69 OIIO_EXPORT int dicom_imageio_version = OIIO_PLUGIN_VERSION;
70 
71 OIIO_EXPORT const char*
dicom_imageio_library_version()72 dicom_imageio_library_version()
73 {
74     return PACKAGE_NAME " " PACKAGE_VERSION;
75 }
76 
77 OIIO_EXPORT ImageInput*
dicom_input_imageio_create()78 dicom_input_imageio_create()
79 {
80     return new DICOMInput;
81 }
82 
83 OIIO_EXPORT const char* dicom_input_extensions[] = { "dcm", nullptr };
84 
85 OIIO_PLUGIN_EXPORTS_END
86 
87 
88 
89 bool
open(const std::string & name,ImageSpec & newspec)90 DICOMInput::open(const std::string& name, ImageSpec& newspec)
91 {
92     // If user doesn't want to provide any config, just use an empty spec.
93     ImageSpec config;
94     return open(name, newspec, config);
95 }
96 
97 
98 
99 bool
open(const std::string & name,ImageSpec & newspec,const ImageSpec &)100 DICOMInput::open(const std::string& name, ImageSpec& newspec,
101                  const ImageSpec& /*config*/)
102 {
103     m_filename = name;
104     m_subimage = -1;
105     m_img.reset();
106 
107     bool ok = seek_subimage(0, 0);
108     newspec = spec();
109     return ok;
110 }
111 
112 
113 
114 bool
close()115 DICOMInput::close()
116 {
117     m_img.reset();
118     m_subimage      = -1;
119     m_dipixel       = nullptr;
120     m_internal_data = nullptr;
121     return true;
122 }
123 
124 
125 
126 // Names of tags to ignore
127 static std::set<std::string> ignore_tags {
128     "Rows",           "Columns",        "PixelAspectRatio",    "BitsAllocated",
129     "BitsStored",     "HighBit",        "PixelRepresentation", "PixelData",
130     "NumberOfFrames", "SamplesPerPixel"
131 };
132 
133 
134 
135 bool
seek_subimage(int subimage,int miplevel)136 DICOMInput::seek_subimage(int subimage, int miplevel)
137 {
138     if (miplevel != 0)
139         return false;
140 
141     if (subimage == m_subimage) {
142         return true;  // already there
143     }
144 
145     if (subimage < m_subimage) {
146         // Want an earlier subimage, Easier to close and start again
147         close();
148         m_subimage = -1;
149     }
150 
151     // Open if it's not already opened
152     if (!m_img || m_subimage < 0) {
153         OFLog::configure(OFLogger::FATAL_LOG_LEVEL);
154         m_img.reset(new DicomImage(m_filename.c_str(),
155                                    CIF_UsePartialAccessToPixelData,
156                                    0 /*first frame*/, 1 /* fcount */));
157         m_subimage = 0;
158         if (m_img->getStatus() != EIS_Normal) {
159             m_img.reset();
160             errorf("Unable to open DICOM file %s", m_filename);
161             return false;
162         }
163         m_framecount = m_img->getFrameCount();
164         m_firstframe = m_img->getFirstFrame();
165     }
166 
167     if (subimage >= m_firstframe + m_framecount) {
168         errorf("Unable to seek to subimage %d", subimage);
169         return false;
170     }
171 
172     // Advance to the desired subimage
173     while (m_subimage < subimage) {
174         m_img->processNextFrames(1);
175         if (m_img->getStatus() != EIS_Normal) {
176             m_img.reset();
177             errorf("Unable to seek to subimage %d", subimage);
178             return false;
179         }
180         ++m_subimage;
181     }
182 
183     m_dipixel             = m_img->getInterData();
184     m_internal_data       = (const char*)m_dipixel->getData();
185     EP_Representation rep = m_dipixel->getRepresentation();
186     TypeDesc format;
187     switch (rep) {
188     case EPR_Uint8: format = TypeDesc::UINT8; break;
189     case EPR_Sint8: format = TypeDesc::INT8; break;
190     case EPR_Uint16: format = TypeDesc::UINT16; break;
191     case EPR_Sint16: format = TypeDesc::INT16; break;
192     case EPR_Uint32: format = TypeDesc::UINT32; break;
193     case EPR_Sint32: format = TypeDesc::INT32; break;
194     default: break;
195     }
196     m_internal_data = (const char*)m_img->getOutputData(0, m_subimage, 0);
197 
198     EP_Interpretation photo = m_img->getPhotometricInterpretation();
199     struct PhotoTable {
200         EP_Interpretation pi;
201         const char* name;
202         int chans;
203     };
204     static PhotoTable phototable[] = {
205         { EPI_Unknown, "Unknown", 1 },          /// unknown, undefined, invalid
206         { EPI_Missing, "Missing", 1 },          /// no element value available
207         { EPI_Monochrome1, "Monochrome1", 1 },  /// monochrome 1
208         { EPI_Monochrome2, "Monochrome2", 1 },  /// monochrome 2
209         { EPI_PaletteColor, "PaletteColor", 3 },        /// palette color
210         { EPI_RGB, "RGB", 3 },                          /// RGB color
211         { EPI_HSV, "HSV", 3 },                          /// HSV color (retired)
212         { EPI_ARGB, "ARGB", 4 },                        /// ARGB color (retired)
213         { EPI_CMYK, "CMYK", 4 },                        /// CMYK color (retired)
214         { EPI_YBR_Full, "YBR_Full", 3 },                /// YCbCr full
215         { EPI_YBR_Full_422, "YBR_Full_422", 3 },        /// YCbCr full 4:2:2
216         { EPI_YBR_Partial_422, "YBR_Partial_422", 3 },  /// YCbCr partial 4:2:2
217         { EPI_Unknown, NULL, 0 }
218     };
219     int nchannels         = 1;
220     const char* photoname = NULL;
221     for (int i = 0; phototable[i].name; ++i) {
222         if (photo == phototable[i].pi) {
223             nchannels = phototable[i].chans;
224             photoname = phototable[i].name;
225             break;
226         }
227     }
228 
229     m_spec = ImageSpec(m_img->getWidth(), m_img->getHeight(), nchannels,
230                        format);
231 
232     m_bitspersample = m_img->getDepth();
233     if (size_t(m_bitspersample) != m_spec.format.size() * 8)
234         m_spec.attribute("oiio:BitsPerSample", m_bitspersample);
235 
236     m_spec.attribute("PixelAspectRatio", (float)m_img->getWidthHeightRatio());
237     if (photoname)
238         m_spec.attribute("dicom:PhotometricInterpretation", photoname);
239     if (m_spec.nchannels > 1) {
240         m_spec.attribute(
241             "dicom:PlanarConfiguration",
242             (int)((DiColorPixel*)m_dipixel)->getPlanarConfiguration());
243     }
244 
245     read_metadata();
246 
247     return true;
248 }
249 
250 
251 
252 void
read_metadata()253 DICOMInput::read_metadata()
254 {
255     // Can't seem to figure out how to get the metadata from the
256     // DicomImage class. So open the file a second time (ugh) with
257     // DcmFileFormat.
258     std::unique_ptr<DcmFileFormat> dcm(new DcmFileFormat);
259     OFCondition status = dcm->loadFile (m_filename.c_str() /*, EXS_Unknown,
260                                           EGL_noChange, DCM_MaxReadLength,
261                                           ERM_metaOnly */);
262     if (status.good()) {
263         DcmDataset* dataset = dcm->getDataset();
264         DcmStack stack;
265         while (dcm->nextObject(stack, OFTrue).good()) {
266             DcmObject* object = stack.top();
267             OIIO_ASSERT(object);
268             DcmTag& tag         = const_cast<DcmTag&>(object->getTag());
269             std::string tagname = tag.getTagName();
270             if (ignore_tags.find(tagname) != ignore_tags.end())
271                 continue;
272             std::string name = Strutil::sprintf("dicom:%s", tag.getTagName());
273             DcmEVR evr       = tag.getEVR();
274             // VR codes explained:
275             // http://dicom.nema.org/Dicom/2013/output/chtml/part05/sect_6.2.html
276             if (evr == EVR_FL || evr == EVR_OF || evr == EVR_DS) {
277                 float val;
278                 if (dataset->findAndGetFloat32(tag, val).good())
279                     m_spec.attribute(name, val);
280             } else if (evr == EVR_FD
281 #if PACKAGE_VERSION_NUMBER >= 362
282                        || evr == EVR_OD
283 #endif
284             ) {
285                 double val;
286                 if (dataset->findAndGetFloat64(tag, val).good())
287                     m_spec.attribute(name, (float)val);
288                 // N.B. we cast to float. Will anybody care?
289             } else if (evr == EVR_SL || evr == EVR_IS) {
290                 Sint32 val;
291                 if (dataset->findAndGetSint32(tag, val).good())
292                     m_spec.attribute(name, static_cast<int>(val));
293             } else if (evr == EVR_UL) {
294                 Uint32 val;
295                 if (dataset->findAndGetUint32(tag, val).good())
296                     m_spec.attribute(name, static_cast<unsigned int>(val));
297             } else if (evr == EVR_US) {
298                 unsigned short val;
299                 if (dataset->findAndGetUint16(tag, val).good())
300                     m_spec.attribute(name, TypeDesc::INT16, &val);
301             } else if (evr == EVR_AS || evr == EVR_CS || evr == EVR_DA
302                        || evr == EVR_DT || evr == EVR_LT || evr == EVR_PN
303                        || evr == EVR_ST || evr == EVR_TM || evr == EVR_UI
304                        || evr == EVR_UT || evr == EVR_LO || evr == EVR_SH
305 #if PACKAGE_VERSION_NUMBER >= 362
306                        || evr == EVR_UC || evr == EVR_UR
307 #endif
308             ) {
309                 OFString val;
310                 if (dataset->findAndGetOFString(tag, val).good())
311                     m_spec.attribute(name, val.c_str());
312             } else {
313                 OFString val;
314                 if (dataset->findAndGetOFString(tag, val).good())
315                     m_spec.attribute(name, val.c_str());
316                 // m_spec.attribute (name+"-"+tag.getVRName(), val.c_str());
317             }
318         }
319         // dcm->writeXML (std::cout);
320     }
321 }
322 
323 
324 
325 bool
read_native_scanline(int subimage,int miplevel,int y,int,void * data)326 DICOMInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
327                                  void* data)
328 {
329     lock_guard lock(m_mutex);
330     if (!seek_subimage(subimage, miplevel))
331         return false;
332     if (y < 0 || y >= m_spec.height)  // out of range scanline
333         return false;
334 
335     OIIO_DASSERT(m_internal_data);
336     size_t size = m_spec.scanline_bytes();
337     memcpy(data, m_internal_data + y * size, size);
338 
339     // Handle non-full bit depths
340     int bits = m_spec.format.size() * 8;
341     if (bits != m_bitspersample) {
342         size_t n = m_spec.width * m_spec.nchannels;
343         if (m_spec.format == TypeDesc::UINT8) {
344             unsigned char* p = (unsigned char*)data;
345             for (size_t i = 0; i < n; ++i)
346                 p[i] = bit_range_convert(p[i], m_bitspersample, bits);
347         } else if (m_spec.format == TypeDesc::UINT16) {
348             unsigned short* p = (unsigned short*)data;
349             for (size_t i = 0; i < n; ++i)
350                 p[i] = bit_range_convert(p[i], m_bitspersample, bits);
351         } else if (m_spec.format == TypeDesc::UINT32) {
352             unsigned int* p = (unsigned int*)data;
353             for (size_t i = 0; i < n; ++i)
354                 p[i] = bit_range_convert(p[i], m_bitspersample, bits);
355         }
356     }
357 
358     return true;
359 }
360 
361 
362 OIIO_PLUGIN_NAMESPACE_END
363