1 /*=========================================================================
2
3 Program: GDCM (Grassroots DICOM). A DICOM library
4
5 Copyright (c) 2006-2011 Mathieu Malaterre
6 All rights reserved.
7 See Copyright.txt or http://gdcm.sourceforge.net/Copyright.html for details.
8
9 This software is distributed WITHOUT ANY WARRANTY; without even
10 the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
11 PURPOSE. See the above copyright notice for more information.
12
13 =========================================================================*/
14 #include "gdcmEmptyMaskGenerator.h"
15
16 #include "gdcmImage.h"
17 #include "gdcmImageWriter.h"
18 #include "gdcmFileDerivation.h"
19 #include "gdcmUIDGenerator.h"
20 #include "gdcmImageRegionReader.h"
21 #include "gdcmDirectory.h"
22 #include "gdcmScanner.h"
23 #include "gdcmFilename.h"
24 #include "gdcmFileStreamer.h"
25 #include "gdcmAnonymizer.h"
26 #include "gdcmAttribute.h"
27 #include "gdcmTagKeywords.h"
28
29 namespace gdcm {
30
31 struct EmptyMaskGenerator::impl
32 {
33 static const Tag TSOPClassUID;
34 static const Tag TSOPInstanceUID;
35 static const Tag TSeriesInstanceUID;
36 static const Tag TFrameOfReferenceUID;
37
38 SOPClassUIDMode mode;
39 std::string inputdir;
40 std::string outputdir;
41 UIDGenerator uid;
42 std::map< std::string, std::string > seriesuidhash;
43 std::map< std::string, std::string > framerefuidhash;
44 Scanner s;
45 bool collectuids(Tag const & tag, std::map< std::string, std::string > & hash);
46 bool setup(const char * dirname, const char * outdir);
47 bool setmask( File & file );
48 bool derive( const char * filename, File & file );
49 bool anonymizeattributes( const char * filename, File & file );
50 bool populateattributes( const char * filename, File const & orifile, File & file );
51 bool setts( File & file );
52 bool run(const char * filename, const char * outfile);
53 };
54
55 const Tag EmptyMaskGenerator::impl::TSOPClassUID = Tag(0x0008,0x0016);
56 const Tag EmptyMaskGenerator::impl::TSOPInstanceUID = Tag(0x0008,0x0018);
57 const Tag EmptyMaskGenerator::impl::TSeriesInstanceUID = Tag(0x0020,0x000e);
58 const Tag EmptyMaskGenerator::impl::TFrameOfReferenceUID = Tag(0x0020,0x0052);
59
collectuids(Tag const & tag,std::map<std::string,std::string> & hash)60 bool EmptyMaskGenerator::impl::collectuids( Tag const & tag, std::map< std::string, std::string > & hash)
61 {
62 Scanner::ValuesType vt = s.GetValues(tag);
63 for( Scanner::ValuesType::const_iterator it = vt.begin();
64 it != vt.end(); ++it )
65 {
66 const char * newuid = uid.Generate();
67 hash.insert( std::make_pair( *it, newuid ) );
68 }
69 return true;
70 }
71
setmask(File & file)72 bool EmptyMaskGenerator::impl::setmask( File & file )
73 {
74 DataSet& ds = file.GetDataSet();
75 namespace kwd = Keywords;
76 kwd::ImageType imtype;
77 kwd::ImageType copy;
78 imtype.SetFromDataSet( ds );
79 unsigned int nvalues = imtype.GetNumberOfValues();
80 unsigned int newvalues = std::max( nvalues, 4u );
81 copy.SetNumberOfValues( newvalues );
82 // copy original ones:
83 for( unsigned int i = 0u; i < nvalues; ++i )
84 {
85 copy.SetValue(i, imtype.GetValue(i) );
86 }
87 // Make up non empty values:
88 static const CSComp values[] = {"DERIVED","SECONDARY","OTHER"};
89 for( unsigned int i = nvalues; i < 3u; ++i )
90 {
91 copy.SetValue(i, values[i] );
92 }
93 // The fourth value is required to be set to 'MASK' for SD
94 copy.SetValue( 3u, "MASK" );
95 ds.Replace( copy.GetAsDataElement() );
96 return true;
97 }
98
setup(const char * dirname,const char * outdir)99 bool EmptyMaskGenerator::impl::setup(const char * dirname, const char * outdir)
100 {
101 if( !System::FileIsDirectory( dirname ) )
102 return false;
103 if( !System::MakeDirectory( outdir ) )
104 return false;
105 Directory d;
106 // recursive search by default
107 const unsigned int nfiles = d.Load( dirname, true );
108 if( nfiles == 0 )
109 {
110 gdcmDebugMacro( "No files found in: " << dirname );
111 return false;
112 }
113 Directory::FilenamesType const & filenames = d.GetFilenames();
114
115 s.AddTag( TSOPClassUID );
116 s.AddTag( TSOPInstanceUID );
117 s.AddTag( TSeriesInstanceUID );
118 s.AddTag( TFrameOfReferenceUID );
119 // reduce verbosity when looping over a set of files:
120 Trace::WarningOff();
121 if( !s.Scan( filenames ) )
122 {
123 gdcmDebugMacro( "Scanner failure for directory: " << dirname );
124 return false;
125 }
126 if( !collectuids( TSeriesInstanceUID, seriesuidhash ) ) return false;
127 // Frame of Reference are relative to Series UID
128 // http://dicom.nema.org/medical/Dicom/2015a/output/chtml/part03/sect_C.7.4.html
129 if( !collectuids( TFrameOfReferenceUID, framerefuidhash ) ) return false;
130
131 return true;
132 }
133
derive(const char * filename,File & file)134 bool EmptyMaskGenerator::impl::derive( const char * filename, File & file )
135 {
136 FileDerivation fd;
137 const char * referencedsopclassuid = s.GetValue (filename, TSOPClassUID);
138 const char * referencedsopinstanceuid = s.GetValue (filename, TSOPInstanceUID);
139 if( !fd.AddReference( referencedsopclassuid, referencedsopinstanceuid ) )
140 {
141 gdcmDebugMacro( "Impossible to AddRef: " << filename );
142 // This is not considered an error to not reference, eg. UID padded with 0
143 }
144
145 // CID 7202 Source Image Purposes of Reference
146 // DCM 121321 Mask image for image processing operation
147 fd.SetPurposeOfReferenceCodeSequenceCodeValue( 121321 );
148 // CID 7203 Image Derivation
149 // DCM 113047 Pixel by pixel mask
150 fd.SetDerivationCodeSequenceCodeValue( 113047 );
151 fd.SetDerivationDescription( "Empty Mask Derivation" );
152 // always append derivation history to any existing one:
153 fd.SetAppendDerivationHistory( true );
154 fd.SetFile( file );
155 if( !fd.Derive() )
156 {
157 gdcmDebugMacro( "Sorry could not derive using input info" );
158 return false;
159 }
160 return true;
161 }
162
anonymizeattributes(const char * filename,File & file)163 bool EmptyMaskGenerator::impl::anonymizeattributes( const char * filename, File & file )
164 {
165 Anonymizer ano;
166 ano.SetFile( file );
167 ano.RemoveGroupLength();
168 ano.RemovePrivateTags();
169 namespace kwd = Keywords;
170 ano.Remove( kwd::WindowCenter::GetTag() );
171 ano.Remove( kwd::WindowWidth::GetTag() );
172 if( !ano.Replace (TSOPInstanceUID, uid.Generate()) ) return false;
173 const char * oldseriesuid = s.GetValue (filename, TSeriesInstanceUID);
174 const char * oldframerefuid = s.GetValue (filename, TFrameOfReferenceUID);
175 if( oldseriesuid )
176 {
177 std::map< std::string, std::string >::const_iterator it1 = seriesuidhash.find( oldseriesuid );
178 if( !ano.Replace (TSeriesInstanceUID, it1->second.c_str() ) ) return false;
179 }
180 if( oldframerefuid )
181 {
182 std::map< std::string, std::string >::const_iterator it2 = framerefuidhash.find( oldframerefuid );
183 if( !ano.Replace (TFrameOfReferenceUID, it2->second.c_str() ) ) return false;
184 }
185 return true;
186 }
187
populateattributes(const char * filename,File const & orifile,File & file)188 bool EmptyMaskGenerator::impl::populateattributes( const char * filename, File const & orifile, File & file )
189 {
190 namespace kwd = Keywords;
191 DataSet & ds = file.GetDataSet();
192
193 // ContentDate
194 char date[22];
195 const size_t datelen = 8;
196 System::GetCurrentDateTime(date);
197 kwd::ContentDate contentdate;
198 // Do not copy the whole cstring:
199 contentdate.SetValue( DAComp( date, datelen ) );
200 ds.Insert( contentdate.GetAsDataElement() );
201 // ContentTime
202 const size_t timelen = 6 + 1 + 6; // time + milliseconds
203 kwd::ContentTime contenttime;
204 // Do not copy the whole cstring:
205 contenttime.SetValue( TMComp(date+datelen, timelen) );
206 ds.Insert( contenttime.GetAsDataElement() );
207
208 const DataSet & orids = orifile.GetDataSet();
209 kwd::SeriesInstanceUID seriesinstanceuid;
210 seriesinstanceuid.SetValue( uid.Generate() ); // In case original instance is missing Series Instance UID
211 kwd::SeriesNumber seriesnumber = { 1 };
212 const char * oldseriesuid = s.GetValue (filename, TSeriesInstanceUID);
213 if( oldseriesuid )
214 {
215 std::map< std::string, std::string >::iterator it =
216 seriesuidhash.find( oldseriesuid );
217 seriesinstanceuid.SetValue( it->second.c_str() );
218 std::map< std::string, std::string >::difference_type diff =
219 std::distance(seriesuidhash.begin(),it);
220 seriesnumber.SetValue( 1 + (int)diff ); // Start at one
221 }
222 ds.Insert( seriesinstanceuid.GetAsDataElement() );
223 ds.Insert( seriesnumber.GetAsDataElement() );
224 kwd::FrameOfReferenceUID frameref;
225 frameref.SetValue( uid.Generate() );
226 const char * oldframerefuid = s.GetValue (filename, TFrameOfReferenceUID);
227 if( oldframerefuid )
228 {
229 std::map< std::string, std::string >::const_iterator it = framerefuidhash.find( oldframerefuid );
230 frameref.SetValue( it->second.c_str() );
231 }
232 ds.Insert( frameref.GetAsDataElement() );
233 kwd::InstanceNumber instancenum = { 1 };
234 if( orids.FindDataElement( instancenum.GetTag() ) )
235 {
236 instancenum.SetFromDataSet( orids );
237 }
238 else
239 {
240 static unsigned int counter = 0; // unsigned will wrap properly
241 instancenum.SetValue( counter++ );
242 }
243 ds.Insert( instancenum.GetAsDataElement() );
244 kwd::StudyInstanceUID studyinstanceuid;
245 studyinstanceuid.SetFromDataSet( orids );
246 ds.Insert( studyinstanceuid.GetAsDataElement() );
247 kwd::StudyID studyid = { "ST1" };
248 studyid.SetFromDataSet( orids );
249 ds.Insert( studyid.GetAsDataElement() );
250 kwd::PatientID patientid;
251 patientid.SetFromDataSet( orids );
252 ds.Insert( patientid.GetAsDataElement() );
253 kwd::PositionReferenceIndicator pri;
254 ds.Insert( pri.GetAsDataElement() );
255 kwd::BodyPartExamined bodypartex;
256 bodypartex.SetFromDataSet( orids );
257 ds.Insert( bodypartex.GetAsDataElement() );
258 // Sync with Body Part Examined:
259 kwd::Laterality lat;
260 if( orids.FindDataElement( lat.GetTag() ) )
261 {
262 lat.SetFromDataSet( orids );
263 ds.Insert( lat.GetAsDataElement() );
264 }
265 kwd::PatientOrientation pator;
266 pator.SetFromDataSet( orids );
267 ds.Insert( pator.GetAsDataElement() );
268 kwd::BurnedInAnnotation bia = { "NO" };
269 ds.Insert( bia.GetAsDataElement() );
270 kwd::ConversionType convtype = { "SYN" };
271 ds.Insert( convtype.GetAsDataElement() );
272 kwd::PresentationLUTShape plutshape = { "IDENTITY" }; // MONOCHROME2
273 ds.Insert( plutshape.GetAsDataElement() );
274 kwd::SOPClassUID sopclassuid;
275 // gdcm will pick the Word in case Byte class is not compatible:
276 MediaStorage ms = MediaStorage::MultiframeGrayscaleByteSecondaryCaptureImageStorage;
277 sopclassuid.SetValue( ms.GetString() );
278 ds.Insert( sopclassuid.GetAsDataElement() );
279 return true;
280 }
281
setts(File & file)282 bool EmptyMaskGenerator::impl::setts( File & file )
283 {
284 FileMetaInformation & fmi = file.GetHeader();
285 const TransferSyntax & orits = fmi.GetDataSetTransferSyntax();
286 TransferSyntax::TSType newts = TransferSyntax::ImplicitVRLittleEndian;
287 if( orits.IsExplicit() )
288 {
289 newts = TransferSyntax::ExplicitVRLittleEndian;
290 }
291 fmi.Clear();
292 fmi.SetDataSetTransferSyntax( newts );
293 return true;
294 }
295
run(const char * filename,const char * outfile)296 bool EmptyMaskGenerator::impl::run(const char * filename, const char * outfile)
297 {
298 if( !s.IsKey( filename ) )
299 {
300 gdcmErrorMacro( "Not DICOM file: " << filename );
301 return false;
302 }
303
304 ImageRegionReader irr;
305 irr.SetFileName( filename );
306 if( !irr.ReadInformation() )
307 {
308 gdcmErrorMacro( "Impossible to ReadInformation (not an image?): " << filename );
309 return false;
310 }
311 size_t buflen = irr.ComputeBufferLength();
312 Image & img = irr.GetImage();
313 if( img.GetPhotometricInterpretation() != PhotometricInterpretation::MONOCHROME1
314 && img.GetPhotometricInterpretation() != PhotometricInterpretation::MONOCHROME2 )
315 {
316 gdcmErrorMacro( "Cannot process PhotometricInterpretation from: " << filename );
317 return false;
318 }
319
320 if( mode == UseOriginalSOPClassUID )
321 {
322 File & file = irr.GetFile();
323 // derive operation needs to operate on original attributes (before anonymization):
324 if( !derive( filename, file ) ) return false;
325 // copy original attributes:
326 if( !anonymizeattributes( filename, file ) ) return false;
327 if( !setmask( file ) ) return false;
328 if( !setts( file ) ) return false;
329
330 Writer w;
331 w.SetFile( file );
332 w.SetFileName( outfile );
333 if( !w.Write() )
334 {
335 return false;
336 }
337 }
338 else if ( mode == UseGrayscaleSecondaryImageStorage )
339 {
340 ImageWriter w;
341 File & file = w.GetFile();
342 if( !derive( filename, file ) ) return false;
343 // create attributes:
344 if( !populateattributes( filename, irr.GetFile(), file ) ) return false;
345 if( !setmask( file ) ) return false;
346 if( !setts( file ) ) return false;
347
348 PixelFormat & pf = img.GetPixelFormat();
349 pf.SetPixelRepresentation(0); // always overwrite to unsigned
350 img.SetSlope(1);
351 img.SetIntercept(0);
352 w.SetImage( img );
353 w.SetFileName( outfile );
354 // sentinel, SC is never acceptable:
355 if( w.ComputeTargetMediaStorage() == MediaStorage::SecondaryCaptureImageStorage )
356 {
357 gdcmErrorMacro( "Failure to compute MediaStorage: " << filename );
358 return false;
359 }
360 if( !w.Write() )
361 {
362 return false;
363 }
364 }
365 else
366 {
367 // possibly dead code, but make sure to report an error for invalid state:
368 return false;
369 }
370
371 // now create the empty pixel data element, a chunk at a time:
372 FileStreamer fs;
373 fs.SetTemplateFileName(outfile);
374 fs.SetOutputFileName(outfile);
375 Tag pixeldata (0x7fe0, 0x0010);
376 fs.CheckDataElement( pixeldata ); // double check generated output
377 if( !fs.StartDataElement( pixeldata ) )
378 {
379 gdcmErrorMacro( "StartDataElement" );
380 return false;
381 }
382 {
383 const unsigned int chunk = 4096u;
384 char bytes[chunk] = {};
385 const unsigned int nchunks = (unsigned int)( buflen / chunk);
386 const unsigned int remain = buflen % chunk;
387 for( unsigned int i = 0; i < nchunks; ++i )
388 {
389 // Read the source file into a byte array.
390 fs.AppendToDataElement( pixeldata, bytes, chunk );
391 }
392 fs.AppendToDataElement( pixeldata, bytes, remain );
393 }
394 if( !fs.StopDataElement( pixeldata ) )
395 {
396 // Most likely an issue with Pixel Data Length computation:
397 gdcmErrorMacro( "StopDataElement" );
398 return false;
399 }
400
401 return true;
402 }
403
EmptyMaskGenerator()404 EmptyMaskGenerator::EmptyMaskGenerator():pimpl(new impl)
405 {
406 }
407
~EmptyMaskGenerator()408 EmptyMaskGenerator::~EmptyMaskGenerator()
409 {
410 delete pimpl;
411 }
412
SetSOPClassUIDMode(SOPClassUIDMode mode)413 void EmptyMaskGenerator::SetSOPClassUIDMode( SOPClassUIDMode mode )
414 {
415 pimpl->mode = mode;
416 }
417
SetInputDirectory(const char * dirname)418 void EmptyMaskGenerator::SetInputDirectory(const char * dirname)
419 {
420 if( dirname )
421 pimpl->inputdir = dirname;
422 }
423
SetOutputDirectory(const char * dirname)424 void EmptyMaskGenerator::SetOutputDirectory(const char * dirname)
425 {
426 if( dirname )
427 pimpl->outputdir = dirname;
428 }
429
430
Execute()431 bool EmptyMaskGenerator::Execute()
432 {
433 const char * dirname = pimpl->inputdir.c_str();
434 const char * outdir = pimpl->outputdir.c_str();
435 if( !pimpl->setup(dirname, outdir) )
436 {
437 return false;
438 }
439 bool success = true;
440 Directory::FilenamesType const & filenames = pimpl->s.GetFilenames();
441 for( Directory::FilenamesType::const_iterator it = filenames.begin(); it != filenames.end(); ++it )
442 {
443 const char * filename = it->c_str();
444 Filename fn( filename );
445 std::string outfile = outdir;
446 outfile += '/';
447 outfile += fn.GetName();
448 if( !pimpl->run( filename, outfile.c_str() ) )
449 {
450 gdcmErrorMacro( "Failure to EmptyMask" );
451 // Since we may have failed in the middle of writing of the file, remove it:
452 if( System::FileExists(outfile.c_str()) && !System::RemoveFile(outfile.c_str()) )
453 {
454 gdcmErrorMacro( "Failure to RemoveFile: " << outfile );
455 // may want to call exit() here, since we failed to remove a file we created in this process:
456 return false;
457 }
458 // declare failure to process a file, but continue main loop:
459 success = false;
460 }
461 }
462 return success;
463 }
464
465 } // end namespace gdcm
466