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