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