1 /*=========================================================================
2 
3   Library:   CTK
4 
5   Copyright (c) Kitware Inc.
6 
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10 
11       http://www.apache.org/licenses/LICENSE-2.0.txt
12 
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18 
19 =========================================================================*/
20 
21 // Qt includes
22 #include <QSqlQuery>
23 #include <QSqlRecord>
24 #include <QSqlError>
25 #include <QVariant>
26 #include <QDate>
27 #include <QStringList>
28 #include <QSet>
29 #include <QFile>
30 #include <QDirIterator>
31 #include <QFileInfo>
32 #include <QDebug>
33 
34 // ctkDICOM includes
35 #include "ctkLogger.h"
36 #include "ctkDICOMIndexer.h"
37 #include "ctkDICOMIndexer_p.h"
38 #include "ctkDICOMDatabase.h"
39 
40 // DCMTK includes
41 #include <dcmtk/dcmdata/dcfilefo.h>
42 #include <dcmtk/dcmdata/dcfilefo.h>
43 #include <dcmtk/dcmdata/dcdeftag.h>
44 #include <dcmtk/dcmdata/dcdatset.h>
45 #include <dcmtk/ofstd/ofcond.h>
46 #include <dcmtk/ofstd/ofstring.h>
47 #include <dcmtk/ofstd/ofstd.h>        /* for class OFStandard */
48 #include <dcmtk/dcmdata/dcddirif.h>   /* for class DicomDirInterface */
49 #include <dcmtk/dcmimgle/dcmimage.h>  /* for class DicomImage */
50 #include <dcmtk/dcmimage/diregist.h>  /* include support for color images */
51 
52 
53 //------------------------------------------------------------------------------
54 static ctkLogger logger("org.commontk.dicom.DICOMIndexer" );
55 //------------------------------------------------------------------------------
56 
57 
58 //------------------------------------------------------------------------------
59 // ctkDICOMIndexerPrivate methods
60 
61 //------------------------------------------------------------------------------
ctkDICOMIndexerPrivate(ctkDICOMIndexer & o)62 ctkDICOMIndexerPrivate::ctkDICOMIndexerPrivate(ctkDICOMIndexer& o)
63   : q_ptr(&o)
64   , Canceled(false)
65   , StartedIndexing(0)
66 {
67 }
68 
69 //------------------------------------------------------------------------------
~ctkDICOMIndexerPrivate()70 ctkDICOMIndexerPrivate::~ctkDICOMIndexerPrivate()
71 {
72 }
73 
74 //------------------------------------------------------------------------------
75 
76 //------------------------------------------------------------------------------
77 // ctkDICOMIndexer methods
78 
79 //------------------------------------------------------------------------------
ctkDICOMIndexer(QObject * parent)80 ctkDICOMIndexer::ctkDICOMIndexer(QObject *parent):d_ptr(new ctkDICOMIndexerPrivate(*this))
81 {
82   Q_UNUSED(parent);
83 }
84 
85 //------------------------------------------------------------------------------
~ctkDICOMIndexer()86 ctkDICOMIndexer::~ctkDICOMIndexer()
87 {
88 }
89 
90 //------------------------------------------------------------------------------
addFile(ctkDICOMDatabase & database,const QString filePath,const QString & destinationDirectoryName)91 void ctkDICOMIndexer::addFile(ctkDICOMDatabase& database,
92                                    const QString filePath,
93                                    const QString& destinationDirectoryName)
94 {
95   ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database);
96   if (!destinationDirectoryName.isEmpty())
97   {
98     logger.warn("Ignoring destinationDirectoryName parameter, just taking it as indication we should copy!");
99   }
100 
101   emit indexingFilePath(filePath);
102 
103   database.insert(filePath, !destinationDirectoryName.isEmpty(), true);
104 }
105 
106 //------------------------------------------------------------------------------
addDirectory(ctkDICOMDatabase & database,const QString & directoryName,const QString & destinationDirectoryName,bool includeHidden)107 void ctkDICOMIndexer::addDirectory(ctkDICOMDatabase& database,
108                                    const QString& directoryName,
109                                    const QString& destinationDirectoryName,
110                                    bool includeHidden/*=true*/)
111 {
112   ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database);
113   QStringList listOfFiles;
114   QDir directory(directoryName);
115 
116   if(directory.exists("DICOMDIR"))
117   {
118     addDicomdir(database,directoryName,destinationDirectoryName);
119   }
120   else
121   {
122     QDir::Filters filters = QDir::Files;
123     if (includeHidden)
124     {
125       filters |= QDir::Hidden;
126     }
127     QDirIterator it(directoryName, filters, QDirIterator::Subdirectories);
128     while(it.hasNext())
129     {
130       listOfFiles << it.next();
131     }
132     emit foundFilesToIndex(listOfFiles.count());
133     addListOfFiles(database,listOfFiles,destinationDirectoryName);
134   }
135 }
136 
137 //------------------------------------------------------------------------------
addListOfFiles(ctkDICOMDatabase & database,const QStringList & listOfFiles,const QString & destinationDirectoryName)138 void ctkDICOMIndexer::addListOfFiles(ctkDICOMDatabase& database,
139                                      const QStringList& listOfFiles,
140                                      const QString& destinationDirectoryName)
141 {
142   Q_D(ctkDICOMIndexer);
143   ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database);
144   QTime timeProbe;
145   timeProbe.start();
146   d->Canceled = false;
147   int CurrentFileIndex = 0;
148   int lastReportedPercent = 0;
149   foreach(QString filePath, listOfFiles)
150   {
151     int percent = ( 100 * CurrentFileIndex ) / listOfFiles.size();
152     if (lastReportedPercent / 10 < percent / 10)
153       {
154       // Reporting progress has a huge overhead (pending events are processed,
155       // database is updated), therefore only report progress at every 10% increase
156       emit this->progress(percent);
157       lastReportedPercent = percent;
158       }
159     this->addFile(database, filePath, destinationDirectoryName);
160     CurrentFileIndex++;
161 
162     if( d->Canceled )
163       {
164       break;
165       }
166   }
167   float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0;
168   qDebug()
169       << QString("DICOM indexer has successfully processed %1 files [%2s]")
170          .arg(CurrentFileIndex)
171          .arg(QString::number(elapsedTimeInSeconds,'f', 2));
172 }
173 
174 //------------------------------------------------------------------------------
addDicomdir(ctkDICOMDatabase & database,const QString & directoryName,const QString & destinationDirectoryName)175 bool ctkDICOMIndexer::addDicomdir(ctkDICOMDatabase& database,
176                  const QString& directoryName,
177                  const QString& destinationDirectoryName
178                  )
179 {
180   ctkDICOMIndexer::ScopedIndexing indexingBatch(*this, database);
181   //Initialize dicomdir with directory path
182   QString dcmFilePath = directoryName;
183   dcmFilePath.append("/DICOMDIR");
184   DcmDicomDir* dicomDir = new DcmDicomDir(dcmFilePath.toStdString().c_str());
185 
186   //Values to store records data at the moment only uid needed
187   OFString patientsName, studyInstanceUID, seriesInstanceUID, sopInstanceUID, referencedFileName ;
188 
189   //Variables for progress operations
190   QString instanceFilePath;
191   QStringList listOfInstances;
192 
193   DcmDirectoryRecord* rootRecord = &(dicomDir->getRootRecord());
194   DcmDirectoryRecord* patientRecord = NULL;
195   DcmDirectoryRecord* studyRecord = NULL;
196   DcmDirectoryRecord* seriesRecord = NULL;
197   DcmDirectoryRecord* fileRecord = NULL;
198 
199   QTime timeProbe;
200   timeProbe.start();
201 
202   /*Iterate over all records in dicomdir and setup path to the dataset of the filerecord
203   then insert. the filerecord into the database.
204   If any UID is missing the record and all of it's subelements won't be added to the database*/
205   bool success = true;
206   if(rootRecord != NULL)
207   {
208     while ((patientRecord = rootRecord->nextSub(patientRecord)) != NULL)
209     {
210       logger.debug( "Reading new Patient:" );
211       if (patientRecord->findAndGetOFString(DCM_PatientName, patientsName).bad())
212       {
213         logger.warn( "DICOMDIR file at "+directoryName+" is invalid: patient name not found. All records belonging to this patient will be ignored.");
214         success = false;
215         continue;
216       }
217       logger.debug( "Patient's Name: " + QString(patientsName.c_str()) );
218       while ((studyRecord = patientRecord->nextSub(studyRecord)) != NULL)
219       {
220         logger.debug( "Reading new Study:" );
221         if (studyRecord->findAndGetOFString(DCM_StudyInstanceUID, studyInstanceUID).bad())
222         {
223           logger.warn( "DICOMDIR file at "+directoryName+" is invalid: study instance UID not found for patient "+ QString(patientsName.c_str())+". All records belonging to this study will be ignored.");
224           success = false;
225           continue;
226         }
227         logger.debug( "Study instance UID: " + QString(studyInstanceUID.c_str()) );
228 
229         while ((seriesRecord = studyRecord->nextSub(seriesRecord)) != NULL)
230         {
231           logger.debug( "Reading new Series:" );
232           if (seriesRecord->findAndGetOFString(DCM_SeriesInstanceUID, seriesInstanceUID).bad())
233           {
234             logger.warn( "DICOMDIR file at "+directoryName+" is invalid: series instance UID not found for patient "+ QString(patientsName.c_str())+", study "+ QString(studyInstanceUID.c_str())+". All records belonging to this series will be ignored.");
235             success = false;
236             continue;
237           }
238           logger.debug( "Series instance UID: " + QString(seriesInstanceUID.c_str()) );
239 
240           while ((fileRecord = seriesRecord->nextSub(fileRecord)) != NULL)
241           {
242             if (fileRecord->findAndGetOFStringArray(DCM_ReferencedSOPInstanceUIDInFile, sopInstanceUID).bad()
243               || fileRecord->findAndGetOFStringArray(DCM_ReferencedFileID,referencedFileName).bad())
244             {
245               logger.warn( "DICOMDIR file at "+directoryName+" is invalid: referenced SOP instance UID or file name is invalid for patient "
246                 + QString(patientsName.c_str())+", study "+ QString(studyInstanceUID.c_str())+", series "+ QString(seriesInstanceUID.c_str())+
247                 ". This file will be ignored.");
248               success = false;
249               continue;
250             }
251 
252             //Get the filepath of the instance and insert it into a list
253             instanceFilePath = directoryName;
254             instanceFilePath.append("/");
255             instanceFilePath.append(QString( referencedFileName.c_str() ));
256             instanceFilePath.replace("\\","/");
257             listOfInstances << instanceFilePath;
258           }
259         }
260       }
261     }
262     float elapsedTimeInSeconds = timeProbe.elapsed() / 1000.0;
263     qDebug()
264         << QString("DICOM indexer has successfully processed DICOMDIR in %1 [%2s]")
265            .arg(directoryName)
266            .arg(QString::number(elapsedTimeInSeconds,'f', 2));
267     emit foundFilesToIndex(listOfInstances.count());
268     addListOfFiles(database,listOfInstances,destinationDirectoryName);
269   }
270   return success;
271 }
272 
273 //------------------------------------------------------------------------------
refreshDatabase(ctkDICOMDatabase & database,const QString & directoryName)274 void ctkDICOMIndexer::refreshDatabase(ctkDICOMDatabase& database, const QString& directoryName)
275 {
276   Q_UNUSED(database);
277   Q_UNUSED(directoryName);
278   /*
279    * Probably this should go to the database class as well
280    * Or we have to extend the interface to make possible what we do here
281    * without using SQL directly
282 
283   /// get all filenames from the database
284   QSqlQuery allFilesQuery(database.database());
285   QStringList databaseFileNames;
286   QStringList filesToRemove;
287   this->loggedExec(allFilesQuery, "SELECT Filename from Images;");
288 
289   while (allFilesQuery.next())
290     {
291     QString fileName = allFilesQuery.value(0).toString();
292     databaseFileNames.append(fileName);
293     if (! QFile::exists(fileName) )
294       {
295       filesToRemove.append(fileName);
296       }
297     }
298 
299   QSet<QString> filesytemFiles;
300   QDirIterator dirIt(directoryName);
301   while (dirIt.hasNext())
302     {
303     filesytemFiles.insert(dirIt.next());
304     }
305 
306   // TODO: it looks like this function was never finished...
307   //
308   // I guess the next step is to remove all filesToRemove from the database
309   // and also to add filesystemFiles into the database tables
310   */
311   }
312 
313 //------------------------------------------------------------------------------
waitForImportFinished()314 void ctkDICOMIndexer::waitForImportFinished()
315 {
316   // No-op - this had been used when the indexing was multi-threaded,
317   // and has only been retained for API compatibility.
318 }
319 
320 //----------------------------------------------------------------------------
cancel()321 void ctkDICOMIndexer::cancel()
322 {
323   Q_D(ctkDICOMIndexer);
324   d->Canceled = true;
325 }
326 
327 //----------------------------------------------------------------------------
startIndexing(ctkDICOMDatabase & database)328 void ctkDICOMIndexer::startIndexing(ctkDICOMDatabase& database)
329 {
330   Q_D(ctkDICOMIndexer);
331   if (d->StartedIndexing == 0)
332   {
333     // Indexing has just been started
334     database.prepareInsert();
335   }
336   d->StartedIndexing++;
337 }
338 
339 //----------------------------------------------------------------------------
endIndexing()340 void ctkDICOMIndexer::endIndexing()
341 {
342   Q_D(ctkDICOMIndexer);
343   d->StartedIndexing--;
344   if (d->StartedIndexing == 0)
345   {
346     // Indexing has just been completed
347     emit this->indexingComplete();
348   }
349   if (d->StartedIndexing < 0)
350   {
351     qWarning() << QString("ctkDICOMIndexer::endIndexing called without matching startIndexing");
352     d->StartedIndexing = 0;
353   }
354 }
355