Selaa lähdekoodia

ENH: Added filtering and multi-file export to DICOM metadata dialog

To further improve ability to collect DICOM metadata information from users, the following features were added to the DICOM metadata window:
- free-text search in tag, name, vale fields
- export of filtered results (useful for easily excluding patient identifiable information)
- exporting of tags in all files in the files list (useful for example for getting image position information that requires all values), it works combined with filtering
- expand/collapse of all branches
- show total number of files and 1-based file index
- resize column widths to content by default
Andras Lasso 7 vuotta sitten
vanhempi
commit
d9a1d5871c

+ 55 - 13
Libs/DICOM/Widgets/Resources/UI/ctkDICOMObjectListWidget.ui

@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>486</width>
-    <height>297</height>
+    <width>883</width>
+    <height>516</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -21,7 +21,7 @@
        <item>
         <widget class="QLabel" name="label">
          <property name="text">
-          <string>File Path:</string>
+          <string>File path:</string>
          </property>
         </widget>
        </item>
@@ -44,14 +44,7 @@
           <string>Copy the file full path to the clipboard.</string>
          </property>
          <property name="text">
-          <string>Copy Path</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="copyMetadataPushButton">
-         <property name="text">
-          <string>Copy Metadata</string>
+          <string>Copy path</string>
          </property>
         </widget>
        </item>
@@ -65,12 +58,56 @@
       </widget>
      </item>
      <item>
-      <layout class="QHBoxLayout" name="horizontalLayout_3"/>
+      <layout class="QHBoxLayout" name="horizontalLayout_3">
+       <property name="topMargin">
+        <number>8</number>
+       </property>
+       <item>
+        <widget class="ctkSearchBox" name="metadataSearchBox"/>
+       </item>
+       <item>
+        <widget class="QPushButton" name="expandAllPushButton">
+         <property name="text">
+          <string>Expand all</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="collapseAllPushButton">
+         <property name="text">
+          <string>Collapse all</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="copyMetadataPushButton">
+         <property name="toolTip">
+          <string>Copy to clipboard metadata of this file</string>
+         </property>
+         <property name="text">
+          <string>Copy metadata</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="copyAllFilesMetadataPushButton">
+         <property name="toolTip">
+          <string>Copy to clipboard metadata of all files in the series</string>
+         </property>
+         <property name="text">
+          <string>Copy all files metadata</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="horizontalLayout_2"/>
      </item>
      <item>
       <widget class="QTreeView" name="dcmObjectTreeView">
        <property name="toolTip">
-        <string>Double-click Tag to show its definition.</string>
+        <string>Double-click to show DICOM tag definition.</string>
        </property>
       </widget>
      </item>
@@ -80,6 +117,11 @@
  </widget>
  <customwidgets>
   <customwidget>
+   <class>ctkSearchBox</class>
+   <extends>QLineEdit</extends>
+   <header>ctkSearchBox.h</header>
+  </customwidget>
+  <customwidget>
    <class>ctkSliderWidget</class>
    <extends>QWidget</extends>
    <header>ctkSliderWidget.h</header>

+ 194 - 41
Libs/DICOM/Widgets/ctkDICOMObjectListWidget.cpp

@@ -26,6 +26,7 @@
 #include <QApplication>
 #include <QClipboard>
 #include <QDesktopServices>
+#include <QSortFilterProxyModel>
 #include <QString>
 #include <QStringList>
 #include <QUrl>
@@ -35,6 +36,54 @@
 #include <ctkLogger.h>
 static ctkLogger logger("org.commontk.DICOM.Widgets.ctkDICOMObjectListWidget");
 
+class qRecursiveTreeProxyFilter : public QSortFilterProxyModel
+{
+public:
+  qRecursiveTreeProxyFilter(QObject *parent = NULL):
+    QSortFilterProxyModel(parent)
+  {
+  }
+
+  bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
+  {
+    if (filterRegExp().isEmpty())
+      {
+      return true;
+      }
+    QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+    return filterAcceptsIndex(index);
+  }
+
+private:
+  bool filterAcceptsIndex(const QModelIndex index) const
+  {
+    // Accept item if its tag, attribute, or value text matches
+    if ((sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::TagColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
+      || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::AttributeColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp()))
+      || (sourceModel()->data(sourceModel()->index(index.row(), ctkDICOMObjectModel::ValueColumn,
+      index.parent()), Qt::DisplayRole).toString().contains(filterRegExp())))
+      {
+      return true;
+      }
+    // Accept item if any child matches
+    for (int row = 0; row < sourceModel()->rowCount(index); row++)
+      {
+      QModelIndex childIndex = sourceModel()->index(row, 0, index);
+      if (!childIndex.isValid())
+        {
+        break;
+        }
+      if (filterAcceptsIndex(childIndex))
+        {
+        return true;
+        }
+      }
+    return false;
+  }
+};
+
 //----------------------------------------------------------------------------
 class ctkDICOMObjectListWidgetPrivate: public Ui_ctkDICOMObjectListWidget
 {
@@ -43,11 +92,13 @@ public:
   ~ctkDICOMObjectListWidgetPrivate();
   void populateDICOMObjectTreeView(const QString& fileName);
   void setPathLabel(const QString& currentFile);
-  QString dicomObjectModelAsString(QModelIndex parent = QModelIndex(), int indent = 0);
+  QString dicomObjectModelAsString(QAbstractItemModel* dicomObjectModel, QModelIndex parent = QModelIndex(), int indent = 0, QString rowPrefix = QString());
 
+  QString endOfLine;
   QString currentFile;
   QStringList fileList;
   ctkDICOMObjectModel* dicomObjectModel;
+  qRecursiveTreeProxyFilter* filterModel;
 };
 
 //----------------------------------------------------------------------------
@@ -56,6 +107,13 @@ public:
 //----------------------------------------------------------------------------
 ctkDICOMObjectListWidgetPrivate::ctkDICOMObjectListWidgetPrivate()
 {
+#ifdef WIN32
+  this->endOfLine = "\r\n";
+#else
+  this->endOfLine = "\n";
+#endif
+  this->dicomObjectModel = 0;
+  this->filterModel = 0;
 }
 
 //----------------------------------------------------------------------------
@@ -67,7 +125,8 @@ ctkDICOMObjectListWidgetPrivate::~ctkDICOMObjectListWidgetPrivate()
 void ctkDICOMObjectListWidgetPrivate::populateDICOMObjectTreeView(const QString& fileName)
 {
   this->dicomObjectModel->setFile(fileName);
-  this->dcmObjectTreeView->setModel(this->dicomObjectModel);
+  this->filterModel->invalidate();
+  this->dcmObjectTreeView->setModel(this->filterModel);
   this->dcmObjectTreeView->expandAll();
 }
 
@@ -78,22 +137,16 @@ void ctkDICOMObjectListWidgetPrivate::setPathLabel(const QString& currentFile)
 }
 
 // --------------------------------------------------------------------------
-QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex parent /*=QModelIndex()*/, int indent /*=0*/)
+QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QAbstractItemModel* aDicomObjectModel, QModelIndex parent /*=QModelIndex()*/, int indent /*=0*/, QString rowPrefix /*=QString()*/)
 {
   QString dump;
   QString indentString(indent, '\t'); // add tab characters, (indent) number of times
-#ifdef WIN32
-  QString newLine = "\r\n";
-#else
-  QString newLine = "\n";
-#endif
-  for (int r = 0; r < this->dicomObjectModel->rowCount(parent); ++r)
+  for (int r = 0; r < aDicomObjectModel->rowCount(parent); ++r)
     {
-    dump += indentString;
-    for (int c = 0; c < this->dicomObjectModel->columnCount(); ++c)
+    for (int c = 0; c < aDicomObjectModel->columnCount(); ++c)
       {
-      QModelIndex index = this->dicomObjectModel->index(r, c, parent);
-      QString name = this->dicomObjectModel->data(index).toString();
+      QModelIndex index = aDicomObjectModel->index(r, c, parent);
+      QString name = aDicomObjectModel->data(index).toString();
       if (c == 0)
         {
         // Replace round brackets by square brackets.
@@ -101,20 +154,19 @@ QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex pa
         // as a negative number (-80,012). Instead, [0008,0012] is displayed fine.
         name.replace('(', '[');
         name.replace(')', ']');
-        dump += name;
+        dump += rowPrefix + indentString + name;
         }
       else
         {
         dump += "\t" + name;
         }
-      
       }
-    dump += newLine;
-    // here is your applicable code
-    QModelIndex index0 = this->dicomObjectModel->index(r, 0, parent);
-    if (this->dicomObjectModel->hasChildren(index0))
+    dump += endOfLine;
+    // Print children
+    QModelIndex index0 = aDicomObjectModel->index(r, 0, parent);
+    if (aDicomObjectModel->hasChildren(index0))
       {
-      dump += dicomObjectModelAsString(index0, indent+1);
+      dump += dicomObjectModelAsString(aDicomObjectModel, index0, indent + 1, rowPrefix);
       }
     }
   return dump;
@@ -124,22 +176,39 @@ QString ctkDICOMObjectListWidgetPrivate::dicomObjectModelAsString(QModelIndex pa
 // ctkDICOMObjectListWidget methods
 
 //----------------------------------------------------------------------------
-ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass(_parent), 
+ctkDICOMObjectListWidget::ctkDICOMObjectListWidget(QWidget* _parent):Superclass(_parent),
   d_ptr(new ctkDICOMObjectListWidgetPrivate)
 {
   Q_D(ctkDICOMObjectListWidget);
 
   d->setupUi(this);
+
+  d->metadataSearchBox->setAlwaysShowClearIcon(true);
+  d->metadataSearchBox->setShowSearchIcon(true);
+
   d->dicomObjectModel = new ctkDICOMObjectModel(this);
+  d->filterModel = new qRecursiveTreeProxyFilter(this);
+  d->filterModel->setSourceModel(d->dicomObjectModel);
+  d->filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
 
+  d->fileSliderWidget->setMaximum(1);
+  d->fileSliderWidget->setMinimum(1);
   d->fileSliderWidget->setPageStep(1);
 
   d->currentPathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
   connect(d->fileSliderWidget, SIGNAL(valueChanged(double)), this, SLOT(updateWidget()));
-  connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&))
-                               ,this, SLOT(openLookupUrl(const QModelIndex&)));
+  connect(d->dcmObjectTreeView, SIGNAL(doubleClicked(const QModelIndex&)),
+    this, SLOT(itemDoubleClicked(const QModelIndex&)));
   connect(d->copyPathPushButton , SIGNAL(clicked(bool)),this, SLOT(copyPath()));
+
+  connect(d->expandAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(expandAll()));
+  connect(d->collapseAllPushButton, SIGNAL(clicked(bool)), d->dcmObjectTreeView, SLOT(collapseAll()));
   connect(d->copyMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyMetadata()));
+  connect(d->copyAllFilesMetadataPushButton, SIGNAL(clicked(bool)), this, SLOT(copyAllFilesMetadata()));
+
+  QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)),
+    d->filterModel, SLOT(setFilterWildcard(QString)));
+  QObject::connect(d->metadataSearchBox, SIGNAL(textChanged(QString)), this, SLOT(onFilterChanged()));
 }
 
 //----------------------------------------------------------------------------
@@ -147,6 +216,7 @@ ctkDICOMObjectListWidget::~ctkDICOMObjectListWidget()
 {
   Q_D(ctkDICOMObjectListWidget);
   d->dicomObjectModel->deleteLater();
+  d->filterModel->deleteLater();
 }
 
 //----------------------------------------------------------------------------
@@ -164,11 +234,25 @@ void ctkDICOMObjectListWidget::setFileList(const QStringList& fileList)
   if (d->fileList.size() > 0)
     {
     d->currentFile = d->fileList[0];
-    d->setPathLabel(d->currentFile );
-    d->populateDICOMObjectTreeView(d->currentFile );
-    d->fileSliderWidget->setMaximum(fileList.size()-1);
+    
+    d->populateDICOMObjectTreeView(d->currentFile);
+    d->fileSliderWidget->setMaximum(fileList.size());
+    d->fileSliderWidget->setSuffix(QString(" / %1").arg(fileList.size()));
+    for (int columnIndex = 0; columnIndex < d->dicomObjectModel->columnCount(); ++columnIndex)
+      {
+      d->dcmObjectTreeView->resizeColumnToContents(columnIndex);
+      }
     }
+  else
+    {
+    d->currentFile.clear();
+    d->dicomObjectModel->clear();
+    }
+
+  d->setPathLabel(d->currentFile);
+  d->fileSliderWidget->setVisible(d->fileList.size() > 1);
 }
+
 // --------------------------------------------------------------------------
 QString ctkDICOMObjectListWidget::currentFile()
 {
@@ -182,29 +266,34 @@ QStringList ctkDICOMObjectListWidget::fileList()
   Q_D(ctkDICOMObjectListWidget);
   return d->fileList;
 }
-// --------------------------------------------------------------------------
 
-void ctkDICOMObjectListWidget::openLookupUrl(const QModelIndex& index)
+// --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::openLookupUrl(QString tag)
 {
-  if (index.column() == 0)
-  {
-    QVariant  data = index.data();
-    QString lookupUrl = "http://dicomlookup.com/lookup.asp?sw=Tnumber&q="+data.toString();
-    QUrl url(lookupUrl);
-    QDesktopServices::openUrl(url);
-  }
+  QString lookupUrl = "http://dicomlookup.com/lookup.asp?sw=Tnumber&q=" + tag;
+  QUrl url(lookupUrl);
+  QDesktopServices::openUrl(url);
 }
+
 // --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::itemDoubleClicked(const QModelIndex& index)
+{
+  Q_D(ctkDICOMObjectListWidget);
+  QModelIndex tagIndex = d->filterModel->index(index.row(), 0, index.parent());
+  QString tag = d->filterModel->data(tagIndex).toString();
+  openLookupUrl(tag);
+}
 
+// --------------------------------------------------------------------------
 void ctkDICOMObjectListWidget::updateWidget()
 {
   Q_D(ctkDICOMObjectListWidget);
-  d->currentFile = d->fileList[static_cast<int>(d->fileSliderWidget->value())];
+  d->currentFile = d->fileList[static_cast<int>(d->fileSliderWidget->value())-1];
   d->setPathLabel(d->currentFile);
   d->populateDICOMObjectTreeView(d->currentFile);
  }
-// --------------------------------------------------------------------------
 
+// --------------------------------------------------------------------------
 void ctkDICOMObjectListWidget::copyPath()
 {
   Q_D(ctkDICOMObjectListWidget);
@@ -213,18 +302,82 @@ void ctkDICOMObjectListWidget::copyPath()
 }
 
 // --------------------------------------------------------------------------
-
-QString ctkDICOMObjectListWidget::metadataAsText()
+QString ctkDICOMObjectListWidget::metadataAsText(bool allFiles /*=false*/)
 {
   Q_D(ctkDICOMObjectListWidget);
-  return d->dicomObjectModelAsString();
+  QString metadata;
+  if (allFiles)
+    {
+    foreach(QString fileName, d->fileList)
+      {
+      // copy metadata of all files
+
+      ctkDICOMObjectModel* aDicomObjectModel = new ctkDICOMObjectModel();
+      aDicomObjectModel->setFile(fileName);
+
+      qRecursiveTreeProxyFilter* afilterModel = new qRecursiveTreeProxyFilter();
+      afilterModel->setSourceModel(aDicomObjectModel);
+      afilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+      afilterModel->setFilterWildcard(d->metadataSearchBox->text());
+
+      QString thisFileMetadata = d->dicomObjectModelAsString(afilterModel, QModelIndex(), 0, fileName + "\t");
+
+      if (!thisFileMetadata.isEmpty())
+        {
+        metadata += thisFileMetadata;
+        }
+      else
+        {
+        metadata += fileName + "\t(none)" + d->endOfLine;
+        }
+
+      delete afilterModel;
+      delete aDicomObjectModel;
+      }
+    }
+  else
+    {
+    // single file
+    metadata = d->dicomObjectModelAsString(d->filterModel);
+    }
+  return metadata;
 }
 
 // --------------------------------------------------------------------------
-
 void ctkDICOMObjectListWidget::copyMetadata()
 {
   Q_D(ctkDICOMObjectListWidget);
   QClipboard *clipboard = QApplication::clipboard();
   clipboard->setText(metadataAsText());
 }
+
+// --------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::copyAllFilesMetadata()
+{
+  Q_D(ctkDICOMObjectListWidget);
+  QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
+  QClipboard *clipboard = QApplication::clipboard();
+  clipboard->setText(metadataAsText(true));
+  QApplication::restoreOverrideCursor();
+}
+
+//------------------------------------------------------------------------------
+void ctkDICOMObjectListWidget::onFilterChanged()
+{
+  Q_D(ctkDICOMObjectListWidget);
+
+  // Change the searchbox background to yellow
+  // if there are no matches
+  bool showWarning = (d->filterModel->rowCount() == 0 &&
+    d->dicomObjectModel->rowCount() != 0);
+  QPalette palette;
+  if (showWarning)
+    {
+    palette.setColor(QPalette::Base, Qt::yellow);
+    }
+  else
+    {
+    palette.setColor(QPalette::Base, Qt::white);
+    }
+  d->metadataSearchBox->setPalette(palette);
+}

+ 8 - 3
Libs/DICOM/Widgets/ctkDICOMObjectListWidget.h

@@ -21,7 +21,7 @@
 #ifndef __ctkDICOMObjectListWidget_h
 #define __ctkDICOMObjectListWidget_h
 
-// Qt includes 
+// Qt includes
 #include <QItemSelection>
 #include <QWidget>
 
@@ -45,7 +45,10 @@ public:
   QStringList fileList();
 
   /// Get metadata tree as plain text
-  QString metadataAsText();
+  QString metadataAsText(bool allFiles = false);
+
+  /// Open DICOM tag definition in a web browser
+  void openLookupUrl(QString tag);
 
 protected:
   QScopedPointer<ctkDICOMObjectListWidgetPrivate> d_ptr;
@@ -62,10 +65,12 @@ public Q_SLOTS:
   void setFileList(const QStringList& fileList);
 
 protected Q_SLOTS:
-  void openLookupUrl(const QModelIndex&);
+  void itemDoubleClicked(const QModelIndex&);
+  void onFilterChanged();
   void updateWidget();
   void copyPath();
   void copyMetadata();
+  void copyAllFilesMetadata();
 };
 
 #endif

+ 10 - 0
Libs/DICOM/Widgets/ctkDICOMObjectModel.h

@@ -43,6 +43,7 @@ class CTK_DICOM_WIDGETS_EXPORT ctkDICOMObjectModel
   Q_OBJECT
   typedef QStandardItemModel Superclass;
   //Q_PROPERTY(setFile);
+  Q_ENUMS(ColumnIndex)
 
 public:
 
@@ -50,6 +51,15 @@ public:
   virtual ~ctkDICOMObjectModel();
   Q_INVOKABLE void setFile (const QString& fileName);
 
+  enum ColumnIndex
+    {
+    TagColumn = 0,
+    AttributeColumn = 1,
+    ValueColumn = 2,
+    VRColumn = 3,
+    LengthColumn = 4
+    };
+
 protected:
   QScopedPointer<ctkDICOMObjectModelPrivate> d_ptr;