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