Skip to content

Commit 5f58132

Browse files
committed
Merge pull request #610 from naucoin/3163-export-series-to-file-system
ENH: add the capability to export DICOM to disk
2 parents 8bbb327 + 86837e6 commit 5f58132

File tree

2 files changed

+252
-5
lines changed

2 files changed

+252
-5
lines changed

Libs/DICOM/Widgets/ctkDICOMBrowser.cpp

Lines changed: 241 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class ctkDICOMBrowserPrivate: public Ui_ctkDICOMBrowser
7575
QSharedPointer<ctkDICOMIndexer> DICOMIndexer;
7676
QProgressDialog *IndexerProgress;
7777
QProgressDialog *UpdateSchemaProgress;
78+
QProgressDialog *ExportProgress;
7879

7980
void showIndexerDialog();
8081
void showUpdateSchemaDialog();
@@ -99,6 +100,7 @@ ctkDICOMBrowserPrivate::ctkDICOMBrowserPrivate(ctkDICOMBrowser* parent): q_ptr(p
99100
DICOMIndexer = QSharedPointer<ctkDICOMIndexer> (new ctkDICOMIndexer);
100101
IndexerProgress = 0;
101102
UpdateSchemaProgress = 0;
103+
ExportProgress = 0;
102104
DisplayImportSummary = true;
103105
PatientsAddedDuringImport = 0;
104106
StudiesAddedDuringImport = 0;
@@ -116,6 +118,10 @@ ctkDICOMBrowserPrivate::~ctkDICOMBrowserPrivate()
116118
{
117119
delete UpdateSchemaProgress;
118120
}
121+
if ( ExportProgress )
122+
{
123+
delete ExportProgress;
124+
}
119125
}
120126

121127
void ctkDICOMBrowserPrivate::showUpdateSchemaDialog()
@@ -669,7 +675,7 @@ void ctkDICOMBrowser::onModelSelected(const QItemSelection &item1, const QItemSe
669675
}
670676

671677
//----------------------------------------------------------------------------
672-
bool ctkDICOMBrowser::confirmDeleteSelectedIUDs(QStringList uids)
678+
bool ctkDICOMBrowser::confirmDeleteSelectedUIDs(QStringList uids)
673679
{
674680
Q_D(ctkDICOMBrowser);
675681

@@ -753,19 +759,41 @@ void ctkDICOMBrowser::onPatientsRightClicked(const QPoint &point)
753759

754760
patientsMenu->addAction(deleteAction);
755761

762+
QString exportString = QString("Export ")
763+
+ QString::number(numPatients)
764+
+ QString(" selected patients to file system");
765+
QAction *exportAction = new QAction(exportString, patientsMenu);
766+
767+
patientsMenu->addAction(exportAction);
768+
756769
// the table took care of mapping it to a global position so that the
757770
// menu will pop up at the correct place over this table.
758771
QAction *selectedAction = patientsMenu->exec(point);
759772

760773
if (selectedAction == deleteAction
761-
&& this->confirmDeleteSelectedIUDs(selectedPatientsUIDs))
774+
&& this->confirmDeleteSelectedUIDs(selectedPatientsUIDs))
762775
{
763776
qDebug() << "Deleting " << numPatients << " patients";
764777
foreach (const QString& uid, selectedPatientsUIDs)
765778
{
766779
d->DICOMDatabase->removePatient(uid);
767780
}
768781
}
782+
else if (selectedAction == exportAction)
783+
{
784+
ctkFileDialog* directoryDialog = new ctkFileDialog();
785+
directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
786+
directoryDialog->setOption(QFileDialog::ShowDirsOnly);
787+
directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
788+
bool res = directoryDialog->exec();
789+
if (res)
790+
{
791+
QStringList dirs = directoryDialog->selectedFiles();
792+
QString dirPath = dirs[0];
793+
this->exportSelectedPatients(dirPath, selectedPatientsUIDs);
794+
}
795+
delete directoryDialog;
796+
}
769797
}
770798

771799
//----------------------------------------------------------------------------
@@ -791,18 +819,40 @@ void ctkDICOMBrowser::onStudiesRightClicked(const QPoint &point)
791819

792820
studiesMenu->addAction(deleteAction);
793821

822+
QString exportString = QString("Export ")
823+
+ QString::number(numStudies)
824+
+ QString(" selected studies to file system");
825+
QAction *exportAction = new QAction(exportString, studiesMenu);
826+
827+
studiesMenu->addAction(exportAction);
828+
794829
// the table took care of mapping it to a global position so that the
795830
// menu will pop up at the correct place over this table.
796831
QAction *selectedAction = studiesMenu->exec(point);
797832

798833
if (selectedAction == deleteAction
799-
&& this->confirmDeleteSelectedIUDs(selectedStudiesUIDs))
834+
&& this->confirmDeleteSelectedUIDs(selectedStudiesUIDs))
800835
{
801836
foreach (const QString& uid, selectedStudiesUIDs)
802837
{
803838
d->DICOMDatabase->removeStudy(uid);
804839
}
805840
}
841+
else if (selectedAction == exportAction)
842+
{
843+
ctkFileDialog* directoryDialog = new ctkFileDialog();
844+
directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
845+
directoryDialog->setOption(QFileDialog::ShowDirsOnly);
846+
directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
847+
bool res = directoryDialog->exec();
848+
if (res)
849+
{
850+
QStringList dirs = directoryDialog->selectedFiles();
851+
QString dirPath = dirs[0];
852+
this->exportSelectedStudies(dirPath, selectedStudiesUIDs);
853+
}
854+
delete directoryDialog;
855+
}
806856
}
807857

808858
//----------------------------------------------------------------------------
@@ -828,16 +878,203 @@ void ctkDICOMBrowser::onSeriesRightClicked(const QPoint &point)
828878

829879
seriesMenu->addAction(deleteAction);
830880

881+
QString exportString = QString("Export ")
882+
+ QString::number(numSeries)
883+
+ QString(" selected series to file system");
884+
QAction *exportAction = new QAction(exportString, seriesMenu);
885+
seriesMenu->addAction(exportAction);
886+
831887
// the table took care of mapping it to a global position so that the
832888
// menu will pop up at the correct place over this table.
833889
QAction *selectedAction = seriesMenu->exec(point);
834890

835891
if (selectedAction == deleteAction
836-
&& this->confirmDeleteSelectedIUDs(selectedSeriesUIDs))
892+
&& this->confirmDeleteSelectedUIDs(selectedSeriesUIDs))
837893
{
838894
foreach (const QString& uid, selectedSeriesUIDs)
839895
{
840896
d->DICOMDatabase->removeSeries(uid);
841897
}
842898
}
899+
else if (selectedAction == exportAction)
900+
{
901+
ctkFileDialog* directoryDialog = new ctkFileDialog();
902+
directoryDialog->setOption(QFileDialog::DontUseNativeDialog);
903+
directoryDialog->setOption(QFileDialog::ShowDirsOnly);
904+
directoryDialog->setFileMode(QFileDialog::DirectoryOnly);
905+
bool res = directoryDialog->exec();
906+
if (res)
907+
{
908+
QStringList dirs = directoryDialog->selectedFiles();
909+
QString dirPath = dirs[0];
910+
this->exportSelectedSeries(dirPath, selectedSeriesUIDs);
911+
}
912+
delete directoryDialog;
913+
}
914+
}
915+
916+
//----------------------------------------------------------------------------
917+
void ctkDICOMBrowser::exportSelectedSeries(QString dirPath, QStringList uids)
918+
{
919+
Q_D(ctkDICOMBrowser);
920+
921+
foreach (const QString& uid, uids)
922+
{
923+
QStringList filesForSeries = d->DICOMDatabase->filesForSeries(uid);
924+
925+
// Use the first file to get the overall series information
926+
QString firstFilePath = filesForSeries[0];
927+
QHash<QString,QString> descriptions (d->DICOMDatabase->descriptionsForFile(firstFilePath));
928+
QString patientName = descriptions["PatientsName"];
929+
QString patientIDTag = QString("0010,0020");
930+
QString patientID = d->DICOMDatabase->fileValue(firstFilePath, patientIDTag);
931+
QString studyDescription = descriptions["StudyDescription"];
932+
QString seriesDescription = descriptions["SeriesDescription"];
933+
QString studyDateTag = QString("0008,0020");
934+
QString studyDate = d->DICOMDatabase->fileValue(firstFilePath,studyDateTag);
935+
QString seriesNumberTag = QString("0020,0011");
936+
QString seriesNumber = d->DICOMDatabase->fileValue(firstFilePath,seriesNumberTag);
937+
938+
QString sep = "/";
939+
QString nameSep = "-";
940+
QString destinationDir = dirPath + sep + patientID;
941+
if (!patientName.isEmpty())
942+
{
943+
destinationDir += nameSep + patientName;
944+
}
945+
destinationDir += sep + studyDate;
946+
if (!studyDescription.isEmpty())
947+
{
948+
destinationDir += nameSep + studyDescription;
949+
}
950+
destinationDir += sep + seriesNumber;
951+
if (!seriesDescription.isEmpty())
952+
{
953+
destinationDir += nameSep + seriesDescription;
954+
}
955+
destinationDir += sep;
956+
957+
// make sure only ascii characters are in the directory path
958+
destinationDir = destinationDir.toLatin1();
959+
// replace any question marks that were used as replacements for non ascii
960+
// characters with underscore
961+
destinationDir.replace("?", "_");
962+
963+
// create the destination directory if necessary
964+
if (!QDir().exists(destinationDir))
965+
{
966+
if (!QDir().mkpath(destinationDir))
967+
{
968+
QString errorString =
969+
QString("Unable to create export destination directory:\n\n")
970+
+ destinationDir
971+
+ QString("\n\nHalting export.");
972+
ctkMessageBox createDirectoryErrorMessageBox;
973+
createDirectoryErrorMessageBox.setText(errorString);
974+
createDirectoryErrorMessageBox.setIcon(QMessageBox::Warning);
975+
createDirectoryErrorMessageBox.exec();
976+
return;
977+
}
978+
}
979+
980+
// show progress
981+
if (d->ExportProgress == 0)
982+
{
983+
d->ExportProgress = new QProgressDialog(this->tr("DICOM Export"), "Close", 0, 100, this, Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
984+
d->ExportProgress->setWindowModality(Qt::ApplicationModal);
985+
d->ExportProgress->setMinimumDuration(0);
986+
}
987+
QLabel *exportLabel = new QLabel(this->tr("Exporting series ") + seriesNumber);
988+
d->ExportProgress->setLabel(exportLabel);
989+
d->ExportProgress->setValue(0);
990+
991+
int fileNumber = 0;
992+
int numFiles = filesForSeries.size();
993+
d->ExportProgress->setMaximum(numFiles);
994+
foreach (const QString& filePath, filesForSeries)
995+
{
996+
QString destinationFileName = destinationDir;
997+
998+
QString fileNumberString;
999+
// sequentially number the files
1000+
fileNumberString.sprintf("%06d", fileNumber);
1001+
1002+
destinationFileName += fileNumberString + QString(".dcm");
1003+
1004+
// replace non ASCII characters
1005+
destinationFileName = destinationFileName.toLatin1();
1006+
// replace any question marks that were used as replacements for non ascii
1007+
// characters with underscore
1008+
destinationFileName.replace("?", "_");
1009+
1010+
if (!QFile::exists(filePath))
1011+
{
1012+
d->ExportProgress->setValue(numFiles);
1013+
QString errorString = QString("Export source file not found:\n\n")
1014+
+ filePath
1015+
+ QString("\n\nHalting export.\n\nError may be fixed via Repair.");
1016+
ctkMessageBox copyErrorMessageBox;
1017+
copyErrorMessageBox.setText(errorString);
1018+
copyErrorMessageBox.setIcon(QMessageBox::Warning);
1019+
copyErrorMessageBox.exec();
1020+
return;
1021+
}
1022+
if (QFile::exists(destinationFileName))
1023+
{
1024+
d->ExportProgress->setValue(numFiles);
1025+
QString errorString = QString("Export destination file already exists:\n\n")
1026+
+ destinationFileName
1027+
+ QString("\n\nHalting export.");
1028+
ctkMessageBox copyErrorMessageBox;
1029+
copyErrorMessageBox.setText(errorString);
1030+
copyErrorMessageBox.setIcon(QMessageBox::Warning);
1031+
copyErrorMessageBox.exec();
1032+
return;
1033+
}
1034+
1035+
bool copyResult = QFile::copy(filePath, destinationFileName);
1036+
if (!copyResult)
1037+
{
1038+
d->ExportProgress->setValue(numFiles);
1039+
QString errorString = QString("Failed to copy\n\n")
1040+
+ filePath
1041+
+ QString("\n\nto\n\n")
1042+
+ destinationFileName
1043+
+ QString("\n\nHalting export.");
1044+
ctkMessageBox copyErrorMessageBox;
1045+
copyErrorMessageBox.setText(errorString);
1046+
copyErrorMessageBox.setIcon(QMessageBox::Warning);
1047+
copyErrorMessageBox.exec();
1048+
return;
1049+
}
1050+
1051+
fileNumber++;
1052+
d->ExportProgress->setValue(fileNumber);
1053+
}
1054+
d->ExportProgress->setValue(numFiles);
1055+
}
1056+
}
1057+
1058+
//----------------------------------------------------------------------------
1059+
void ctkDICOMBrowser::exportSelectedStudies(QString dirPath, QStringList uids)
1060+
{
1061+
Q_D(ctkDICOMBrowser);
1062+
1063+
foreach (const QString& uid, uids)
1064+
{
1065+
QStringList seriesUIDs = d->DICOMDatabase->seriesForStudy(uid);
1066+
this->exportSelectedSeries(dirPath, seriesUIDs);
1067+
}
1068+
}
1069+
1070+
//----------------------------------------------------------------------------
1071+
void ctkDICOMBrowser::exportSelectedPatients(QString dirPath, QStringList uids)
1072+
{
1073+
Q_D(ctkDICOMBrowser);
1074+
1075+
foreach (const QString& uid, uids)
1076+
{
1077+
QStringList studiesUIDs = d->DICOMDatabase->studiesForPatient(uid);
1078+
this->exportSelectedStudies(dirPath, studiesUIDs);
1079+
}
8431080
}

Libs/DICOM/Widgets/ctkDICOMBrowser.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public Q_SLOTS:
120120
/// empty, uses the UID.
121121
/// Returns true if the user confirms the delete, false otherwise.
122122
/// Remembers if the user doesn't want to show the confirmation again.
123-
bool confirmDeleteSelectedIUDs(QStringList uids);
123+
bool confirmDeleteSelectedUIDs(QStringList uids);
124124

125125
protected Q_SLOTS:
126126
void onModelSelected(const QItemSelection&, const QItemSelection&);
@@ -134,6 +134,16 @@ protected Q_SLOTS:
134134
/// Called when a right mouse click is made in the series table
135135
void onSeriesRightClicked(const QPoint &point);
136136

137+
/// Called to export the series associated with the selected UIDs
138+
/// \sa exportSelectedStudies, exportSelectedPatients
139+
void exportSelectedSeries(QString dirPath, QStringList uids);
140+
/// Called to export the studies associated with the selected UIDs
141+
/// \sa exportSelectedSeries, exportSelectedPatients
142+
void exportSelectedStudies(QString dirPath, QStringList uids);
143+
/// Called to export the patients associated with the selected UIDs
144+
/// \sa exportSelectedStudies, exportSelectedSeries
145+
void exportSelectedPatients(QString dirPath, QStringList uids);
146+
137147
/// To be called when dialog finishes
138148
void onQueryRetrieveFinished();
139149

0 commit comments

Comments
 (0)