Преглед изворни кода

ENH: add the capability to export DICOM to disk

Add the export series, study, patient option to the right click menus.
Gets the files associated with a series and constructs
an export path based on the selected directory, using the patient
ID and name, study date and description, series number and
description.
The destination file is named sequentially, so a series will
be named 000000.dcm through to n-1.dcm.
Call the export series method for each selected study.
Call the export study for each selected patient.
Use a progress dialog to give the user feedback on the
status of the DICOM export.
Use message box pop ups to report errors to the user.
Fix a typo in a method name:
confirmDeleteSelectedIUDs -> confirmDeleteSelectedUIDs

Slicer Issue #3163
Nicole Aucoin пре 9 година
родитељ
комит
accb8df91b
2 измењених фајлова са 252 додато и 5 уклоњено
  1. 241 4
      Libs/DICOM/Widgets/ctkDICOMBrowser.cpp
  2. 11 1
      Libs/DICOM/Widgets/ctkDICOMBrowser.h

+ 241 - 4
Libs/DICOM/Widgets/ctkDICOMBrowser.cpp

@@ -75,6 +75,7 @@ public:
   QSharedPointer<ctkDICOMIndexer> DICOMIndexer;
   QProgressDialog *IndexerProgress;
   QProgressDialog *UpdateSchemaProgress;
+  QProgressDialog *ExportProgress;
 
   void showIndexerDialog();
   void showUpdateSchemaDialog();
@@ -99,6 +100,7 @@ ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent): q_ptr(p
   DICOMIndexer = QSharedPointer<ctkDICOMIndexer> (new ctkDICOMIndexer);
   IndexerProgress = 0;
   UpdateSchemaProgress = 0;
+  ExportProgress = 0;
   DisplayImportSummary = true;
   PatientsAddedDuringImport = 0;
   StudiesAddedDuringImport = 0;
@@ -116,6 +118,10 @@ ctkDICOMBrowserPrivate::~ctkDICOMBrowserPrivate()
     {
     delete UpdateSchemaProgress;
     }
+  if ( ExportProgress )
+    {
+    delete ExportProgress;
+    }
 }
 
 void ctkDICOMBrowserPrivate::showUpdateSchemaDialog()
@@ -669,7 +675,7 @@ void ctkDICOMBrowser::onModelSelected(const QItemSelection &item1, const QItemSe
 }
 
 //----------------------------------------------------------------------------
-bool ctkDICOMBrowser::confirmDeleteSelectedIUDs(QStringList uids)
+bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids)
 {
   Q_D(ctkDICOMBrowser);
 
@@ -753,12 +759,19 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point)
 
   patientsMenu->addAction(deleteAction);
 
+  QString exportString = QString("Export ")
+    + QString::number(numPatients)
+    + QString(" selected patients to file system");
+  QAction *exportAction = new QAction(exportString, patientsMenu);
+
+  patientsMenu->addAction(exportAction);
+
   // the table took care of mapping it to a global position so that the
   // menu will pop up at the correct place over this table.
   QAction *selectedAction = patientsMenu->exec(point);
 
   if (selectedAction == deleteAction
-      && this->confirmDeleteSelectedIUDs(selectedPatientsUIDs))
+      && this->confirmDeleteSelectedUIDs(selectedPatientsUIDs))
     {
     qDebug() << "Deleting " << numPatients << " patients";
     foreach (const QString& uid, selectedPatientsUIDs)
@@ -766,6 +779,21 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point)
       d->DICOMDatabase->removePatient(uid);
       }
     }
+  else if (selectedAction == exportAction)
+    {
+    ctkFileDialog* directoryDialog = new ctkFileDialog();
+    directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
+    directoryDialog->setOption(QFileDialog::ShowDirsOnly);
+    directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
+    bool res = directoryDialog->exec();
+    if (res)
+      {
+      QStringList dirs = directoryDialog->selectedFiles();
+      QString dirPath = dirs[0];
+      this->exportSelectedPatients(dirPath, selectedPatientsUIDs);
+      }
+    delete directoryDialog;
+    }
 }
 
 //----------------------------------------------------------------------------
@@ -791,18 +819,40 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point)
 
   studiesMenu->addAction(deleteAction);
 
+  QString exportString = QString("Export ")
+    + QString::number(numStudies)
+    + QString(" selected studies to file system");
+  QAction *exportAction = new QAction(exportString, studiesMenu);
+
+  studiesMenu->addAction(exportAction);
+
   // the table took care of mapping it to a global position so that the
   // menu will pop up at the correct place over this table.
   QAction *selectedAction = studiesMenu->exec(point);
 
   if (selectedAction == deleteAction
-      && this->confirmDeleteSelectedIUDs(selectedStudiesUIDs))
+      && this->confirmDeleteSelectedUIDs(selectedStudiesUIDs))
     {
     foreach (const QString& uid, selectedStudiesUIDs)
       {
       d->DICOMDatabase->removeStudy(uid);
       }
     }
+  else if (selectedAction == exportAction)
+    {
+    ctkFileDialog* directoryDialog = new ctkFileDialog();
+    directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
+    directoryDialog->setOption(QFileDialog::ShowDirsOnly);
+    directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
+    bool res = directoryDialog->exec();
+    if (res)
+      {
+      QStringList dirs = directoryDialog->selectedFiles();
+      QString dirPath = dirs[0];
+      this->exportSelectedStudies(dirPath, selectedStudiesUIDs);
+      }
+    delete directoryDialog;
+    }
 }
 
 //----------------------------------------------------------------------------
@@ -828,16 +878,203 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point)
 
   seriesMenu->addAction(deleteAction);
 
+  QString exportString = QString("Export ")
+    + QString::number(numSeries)
+    + QString(" selected series to file system");
+  QAction *exportAction = new QAction(exportString, seriesMenu);
+  seriesMenu->addAction(exportAction);
+
   // the table took care of mapping it to a global position so that the
   // menu will pop up at the correct place over this table.
   QAction *selectedAction = seriesMenu->exec(point);
 
   if (selectedAction == deleteAction
-      && this->confirmDeleteSelectedIUDs(selectedSeriesUIDs))
+      && this->confirmDeleteSelectedUIDs(selectedSeriesUIDs))
     {
     foreach (const QString& uid, selectedSeriesUIDs)
       {
       d->DICOMDatabase->removeSeries(uid);
       }
     }
+  else if (selectedAction == exportAction)
+    {
+    ctkFileDialog* directoryDialog = new ctkFileDialog();
+    directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
+    directoryDialog->setOption(QFileDialog::ShowDirsOnly);
+    directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
+    bool res = directoryDialog->exec();
+    if (res)
+      {
+      QStringList dirs = directoryDialog->selectedFiles();
+      QString dirPath = dirs[0];
+      this->exportSelectedSeries(dirPath, selectedSeriesUIDs);
+      }
+    delete directoryDialog;
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids)
+{
+  Q_D(ctkDICOMBrowser);
+
+  foreach (const QString& uid, uids)
+    {
+    QStringList filesForSeries = d->DICOMDatabase->filesForSeries(uid);
+
+    // Use the first file to get the overall series information
+    QString firstFilePath = filesForSeries[0];
+    QHash<QString,QString> descriptions (d->DICOMDatabase->descriptionsForFile(firstFilePath));
+    QString patientName = descriptions["PatientsName"];
+    QString patientIDTag = QString("0010,0020");
+    QString patientID = d->DICOMDatabase->fileValue(firstFilePath, patientIDTag);
+    QString studyDescription = descriptions["StudyDescription"];
+    QString seriesDescription = descriptions["SeriesDescription"];
+    QString studyDateTag = QString("0008,0020");
+    QString studyDate = d->DICOMDatabase->fileValue(firstFilePath,studyDateTag);
+    QString seriesNumberTag = QString("0020,0011");
+    QString seriesNumber = d->DICOMDatabase->fileValue(firstFilePath,seriesNumberTag);
+
+    QString sep = "/";
+    QString nameSep = "-";
+    QString destinationDir = dirPath + sep + patientID;
+    if (!patientName.isEmpty())
+      {
+      destinationDir += nameSep + patientName;
+      }
+    destinationDir += sep + studyDate;
+    if (!studyDescription.isEmpty())
+      {
+      destinationDir += nameSep + studyDescription;
+      }
+    destinationDir += sep + seriesNumber;
+    if (!seriesDescription.isEmpty())
+      {
+      destinationDir += nameSep + seriesDescription;
+      }
+    destinationDir += sep;
+
+    // make sure only ascii characters are in the directory path
+    destinationDir = destinationDir.toLatin1();
+    // replace any question marks that were used as replacements for non ascii
+    // characters with underscore
+    destinationDir.replace("?", "_");
+
+    // create the destination directory if necessary
+    if (!QDir().exists(destinationDir))
+      {
+      if (!QDir().mkpath(destinationDir))
+        {
+        QString errorString =
+          QString("Unable to create export destination directory:\n\n")
+          + destinationDir;
+        ctkMessageBox createDirectoryErrorMessageBox;
+        createDirectoryErrorMessageBox.setText(errorString);
+        createDirectoryErrorMessageBox.setIcon(QMessageBox::Warning);
+        createDirectoryErrorMessageBox.exec();
+        // go on the the next series if present
+        continue;
+        }
+      }
+
+    // show progress
+    if (d->ExportProgress == 0)
+      {
+      d->ExportProgress = new QProgressDialog(this->tr("DICOM Export"), "Close", 0, 100, this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
+      d->ExportProgress->setWindowModality(Qt::ApplicationModal);
+      d->ExportProgress->setMinimumDuration(0);
+      }
+    QLabel *exportLabel = new QLabel(this->tr("Exporting series ") + seriesNumber);
+    d->ExportProgress->setLabel(exportLabel);
+    d->ExportProgress->setValue(0);
+
+    int fileNumber = 0;
+    int numFiles = filesForSeries.size();
+    d->ExportProgress->setMaximum(numFiles);
+    foreach (const QString& filePath, filesForSeries)
+      {
+      QString destinationFileName = destinationDir;
+
+      QString fileNumberString;
+      // sequentially number the files
+      fileNumberString.sprintf("%06d", fileNumber);
+
+      destinationFileName += fileNumberString + QString(".dcm");
+
+      // replace non ASCII characters
+      destinationFileName = destinationFileName.toLatin1();
+      // replace any question marks that were used as replacements for non ascii
+      // characters with underscore
+      destinationFileName.replace("?", "_");
+
+      if (!QFile::exists(filePath))
+        {
+        d->ExportProgress->setValue(numFiles);
+        QString errorString = QString("Export source file not found:\n\n")
+          + filePath
+          + QString("\n\nHalting export.");
+        ctkMessageBox copyErrorMessageBox;
+        copyErrorMessageBox.setText(errorString);
+        copyErrorMessageBox.setIcon(QMessageBox::Warning);
+        copyErrorMessageBox.exec();
+        return;
+      }
+      if (QFile::exists(destinationFileName))
+        {
+        d->ExportProgress->setValue(numFiles);
+        QString errorString = QString("Export destination file already exists:\n\n")
+          + destinationFileName
+          + QString("\n\nHalting export.");
+        ctkMessageBox copyErrorMessageBox;
+        copyErrorMessageBox.setText(errorString);
+        copyErrorMessageBox.setIcon(QMessageBox::Warning);
+        copyErrorMessageBox.exec();
+        return;
+        }
+
+      bool copyResult = QFile::copy(filePath, destinationFileName);
+      if (!copyResult)
+        {
+        d->ExportProgress->setValue(numFiles);
+        QString errorString = QString("Failed to copy\n\n")
+          + filePath
+          + QString("\n\nto\n\n")
+          + destinationFileName
+          + QString("\n\nHalting export.");
+        ctkMessageBox copyErrorMessageBox;
+        copyErrorMessageBox.setText(errorString);
+        copyErrorMessageBox.setIcon(QMessageBox::Warning);
+        copyErrorMessageBox.exec();
+        return;
+        }
+
+      fileNumber++;
+      d->ExportProgress->setValue(fileNumber);
+      }
+    d->ExportProgress->setValue(numFiles);
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::exportSelectedStudies(QString dirPath, QStringList uids)
+{
+  Q_D(ctkDICOMBrowser);
+
+  foreach (const QString& uid, uids)
+    {
+    QStringList seriesUIDs = d->DICOMDatabase->seriesForStudy(uid);
+    this->exportSelectedSeries(dirPath, seriesUIDs);
+    }
+}
+
+//----------------------------------------------------------------------------
+void ctkDICOMBrowser::exportSelectedPatients(QString dirPath, QStringList uids)
+{
+  Q_D(ctkDICOMBrowser);
+
+  foreach (const QString& uid, uids)
+    {
+    QStringList studiesUIDs = d->DICOMDatabase->studiesForPatient(uid);
+    this->exportSelectedStudies(dirPath, studiesUIDs);
+    }
 }

+ 11 - 1
Libs/DICOM/Widgets/ctkDICOMBrowser.h

@@ -120,7 +120,7 @@ protected:
     /// empty, uses the UID.
     /// Returns true if the user confirms the delete, false otherwise.
     /// Remembers if the user doesn't want to show the confirmation again.
-    bool confirmDeleteSelectedIUDs(QStringList uids);
+    bool confirmDeleteSelectedUIDs(QStringList uids);
 
 protected Q_SLOTS:
     void onModelSelected(const QItemSelection&, const QItemSelection&);
@@ -134,6 +134,16 @@ protected Q_SLOTS:
     /// Called when a right mouse click is made in the series table
     void onSeriesRightClicked(const QPoint &point);
 
+    /// Called to export the series associated with the selected UIDs
+    /// \sa exportSelectedStudies, exportSelectedPatients
+    void exportSelectedSeries(QString dirPath, QStringList uids);
+    /// Called to export the studies associated with the selected UIDs
+    /// \sa exportSelectedSeries, exportSelectedPatients
+    void exportSelectedStudies(QString dirPath, QStringList uids);
+    /// Called to export the patients associated with the selected UIDs
+    /// \sa exportSelectedStudies, exportSelectedSeries
+    void exportSelectedPatients(QString dirPath, QStringList uids);
+
     /// To be called when dialog finishes
     void onQueryRetrieveFinished();