Skip to content

PACS

1 数据库

前言:

要做一个简单的开源dcm浏览器 KISS Dicom Viewer ,小型pacs服务肯定必不可少。开发中到处找现成代码,基本上找到的资源都是一个发布版,很少有可以用来研究的源码。 KISS Dicom Viewer 目前处于开发阶段,最近几篇博客就用来记录下开发一个小型pacs数据库(Qt+Dcmtk)的过程。提供服务包括:通讯测试echo、远程查询findscu、远程下载get/move、本机存储storescp。

Dicom协议、通讯原理等等,网上有很多优秀的中文博客来说明,这里就不介绍了。

如果你需要定义自己的 dicom数据库,可以看下我整理的:

1.1 数据库结构

正常的完善的pacs系统的话一般是搞四张表,分别存储 PATIENT、STUDY、SERIES、IMAGE。因为我仅仅是开发一个小型的dcm浏览器,数据就建两张表 STUDY 和 IMAGE

创建语句

 str = "CREATE TABLE IF NOT EXISTS StudyTable("
          "StudyUid VARCHAR(128) PRIMARY KEY NOT NULL,"
          "AccNumber VARCHAR(64) NOT NULL, PatientId VARCHAR(64) NOT NULL,"
          "PatientName VARCHAR(64), "
          "PatientSex VARCHAR(2) NOT NULL,"
          "PatientBirth DATE NOT NULL,"
          "PatientAge VARCHAR(6),"
          "StudyTime DATETIME NOT NULL,"
          "Modality VARCHAR(2) NOT NULL, "
          "StudyDesc TEXT)";
 str = "CREATE TABLE IF NOT EXISTS ImageTable("
          "ImageUid VARCHAR(128) PRIMARY KEY NOT NULL,"
          "SopClassUid VARCHAR(128) NOT NULL,"
          "SeriesUid VARCHAR(128) NOT NULL, "
          "StudyUid VARCHAR(128) NOT NULL,"
          "RefImageUid VARCHAR(128),"
          "ImageNo VARCHAR(16), "
          "ImageTime DATETIME NOT NULL,"
          "ImageDesc TEXT,"
          "ImageFile TEXT,"
          "FOREIGN KEY(StudyUid) REFERENCES StudyTable(StudyUid))";

1.2 数据库可视化 QSqlTableModel+QTableView

qt 对于数据库的可视化封装基本上很完善了,sqlmode+tableview 可以快速实现数据库的可视化。只有两张表 STUDY 和 IMAGE,就是上边选中 STUDY 后下边的 IMAGE 会对应弹出该 STUDY 的 IMAGE。

class SqlImageModel : public QSqlTableModel {
    Q_OBJECT
  public:
    enum ColumnType {
        ImageUid,
        SopClassUid,
        SeriesUid,
        StudyUid,
        RefImageUid,
        ImageNo,
        ImageTime,
        ImageDesc,
        ImageFile,
        ColumnCount,
    };
    explicit SqlImageModel(QObject *parent = nullptr, QSqlDatabase db = QSqlDatabase());
    QVariant headerData(int section, Qt::Orientation orientation = Qt::Horizontal,
                        int role = Qt::DisplayRole) const;

    QStringList getAllImageFiles() const;
  Q_SIGNALS:
    void viewImages(const QStringList &imageFiles);
    void Signal_RemoveFinished();
  public Q_SLOTS:
    bool select();
  public Q_SLOTS:
    void SLot_ViewImages(const QModelIndexList &indexes);
    void SLot_ViewAllImages();
    void Slot_RemoveImages(const QModelIndexList &indexes);
    void Slot_RemoveAllImages();
    void Slot_StudySelected(const QStringList &studyUids);


};
class SqlStudyModel : public QSqlTableModel {
    Q_OBJECT
  public:
    enum ColumnType {
        StudyUid,
        AccNumber,
        PatientId,
        PatientName,
        PatientSex,
        PatientBirth,
        PatientAge,
        StudyTime,
        Modality,
        StudyDesc,
//        ColumnCount,
    };

    explicit SqlStudyModel(QObject *parent = nullptr,
                           QSqlDatabase db = QSqlDatabase());

    QVariant headerData(int section,
                        Qt::Orientation orientation, int role = Qt::DisplayRole) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
    QString getFirstSelectedStudyUid() const;

  public Q_SLOTS:
    bool select();

  Q_SIGNALS:
    void Signal_studySelectionChanged(const QStringList &studyUids);
    void Signal_NewStudy(const QSqlRecord &studyRec);
    void Signal_NewImage(const QSqlRecord &studyRec);
    void Signal_RemoveFinished();

  public Q_SLOTS:
    void Slot_SelectionChanged(const QModelIndexList &indexes);
    void Slot_RemoveStudies();
    void Slot_NewStudy(const QModelIndex &index);
    void Slot_NewImage(const QModelIndex &index);

  private:
    QStringList selected_study_uids_;
    StudyRecord *mod_study_;
    int modify_row_;

};
class SqlStudyTabView : public KissTabView {
    Q_OBJECT
  public:
    explicit SqlStudyTabView(QAbstractTableModel *model, QWidget *parent = nullptr);
    ~SqlStudyTabView() {}
  Q_SIGNALS:
    void Signal_ViewImages();
    void Signal_RemoveStudies();
    void Singal_StudySelectionChanged(const QModelIndexList &indexes);
  protected slots:
    void selectionChanged(const QItemSelection &selected,
                          const QItemSelection &deselected);
    void contextMenuEvent(QContextMenuEvent *e);
  private:
    void SetupContextMenu();
    void HideColumns();
  private:
    QStringList study_uids_;
    QAction *view_image_;
    QAction *remove_study_;
};
class SqlImageTabView : public KissTabView {
    Q_OBJECT
  public:
    explicit SqlImageTabView(QAbstractTableModel *model, QWidget *parent = nullptr);
    ~SqlImageTabView() {}
  Q_SIGNALS:
    void Signal_ViewImages(const QModelIndexList &indexes);
    void Signal_RemoveImages(const QModelIndexList &indexes);
  private:
    void SetupContextMenu();
    void HideColumns();
  private:
    QAction *view_image_action_;
    QAction *remove_image_action_;
};

1.3 数据库查询

QSqlTableModel 可以很快速便捷的实现模型的检索

void StudyExplorerWidget::SetStudyFilter() {
    QString filter, temp;
    if (ui->fromCheckBox->isChecked()) {
        filter = QString("StudyTime>\'%1\'").arg(
                     ui->fromDateTimeEdit->dateTime().toString("yyyy-MM-dd hh:mm:ss"));
    }
    if (ui->toCheckBox->isChecked()) {
        if (!filter.isEmpty()) {
            filter.append(" and ");
        }
        filter.append(QString("StudyTime<\'%1\'").arg(
                          ui->toDateTimeEdit->dateTime().toString("yyyy-MM-dd hh:mm:ss")));
    }
    if (!ui->modalityCombo->currentText().isEmpty()) {
        if (!filter.isEmpty()) {
            filter.append(" and ");
        }
        filter.append(QString("Modality=\'%1\'").arg(ui->modalityCombo->currentText()));
    }
    if (!ui->patientIDEdit->text().isEmpty()) {
        temp = ui->patientIDEdit->text();
        temp.replace(QChar('*'), QChar('%'));
        temp.replace(QChar('?'), QChar('_'));
        if (!filter.isEmpty()) {
            filter.append(" and ");
        }
        filter.append(QString("PatientId LIKE \'%%1%\'").arg(temp));
    }
    if (!ui->patientNameEdit->text().isEmpty()) {
        temp = ui->patientNameEdit->text();
        temp.replace(QChar('*'), QChar('%'));
        temp.replace(QChar('?'), QChar('_'));
        if (!filter.isEmpty()) {
            filter.append(" and ");
        }
        filter.append(QString("PatientName LIKE \'%%1%\'").arg(temp));
    }
    if (!ui->accNumberEdit->text().isEmpty()) {
        temp = ui->accNumberEdit->text();
        temp.replace(QChar('*'), QChar('%'));
        temp.replace(QChar('?'), QChar('_'));
        if (!filter.isEmpty()) {
            filter.append(" and ");
        }
        filter.append(QString("AccNumber LIKE \'%%1%\'").arg(temp));
    }
    this->RefreshReadStudyModel(filter);
}

void StudyExplorerWidget::RefreshReadStudyModel(const QString &filter) {
    bool close = false;
    if(DbManager::IsOpenedDb()) {
    } else {
        if (DbManager::OpenDb()) {
            close = true;
        }
    }
    study_model_->setFilter(filter);
    study_model_->select();
    if(close) {
        DbManager::CloseDb();
    }
}

1.4 数据库增删改查

需求很简单,我这里使用sql语句实现。

#ifndef STUDYDAO_H
#define STUDYDAO_H

#include "Db/dbmanager.h"

class StudyRecord;
class ImageRecord;

class StudyDao : public QObject {
    Q_OBJECT
  public:
    static const QString study_table_name_;
    static const QString image_table_name_;
  public:
    explicit StudyDao(QObject *parent = nullptr);
    virtual ~StudyDao() override;
    bool InsertStudyToDb(const StudyRecord &study, bool imported = false);
    bool RemoveStudyFromDb(const QString &study_uid);
    bool VerifyStudyByStuid(const QString &study_uid);
    //
    bool InsertImageToDb(const ImageRecord &image, bool imported = false);
    bool RemoveImageFromDb(const QString &image_uid, bool updateStudy = true);
    bool RemoveAllImagesOfStudyFromDb(const QString &study_uid, bool updateStudy = true);
    bool UpdateImageFile(const QString &image_uid, const QString &image_file);
    bool VerifyImageByIMmuid(const QString &image_uid);
  public:
    static bool Initial();
  private:
    static bool CreateTable();
    static bool CheckTable();
  private:
};

#endif // STUDYDAO_H
#include "studydao.h"
#include <Global/KissGlobal>


const QString StudyDao::study_table_name_ = "StudyTable";
const QString StudyDao::image_table_name_ = "ImageTable";

StudyDao::StudyDao(QObject *parent):
    QObject(parent) {
}

StudyDao::~StudyDao() {
}

bool StudyDao::InsertStudyToDb(const StudyRecord &study, bool imported) {
    Q_UNUSED(imported)
    bool success = false;
    if(DbManager::OpenDb()) {
        QMap<QString, QVariant> data;
        data.insert("StudyUid", study.study_uid_);
        data.insert("AccNumber", study.acc_number_);
        data.insert("PatientId", study.patient_id_);
        data.insert("PatientName", study.patient_name_);
        data.insert("PatientSex", study.patient_sex_);
        if(study.patient_birth_.toString("yyyy-MM-dd").isEmpty()) {
            data.insert("PatientBirth", "");
        } else {
            data.insert("PatientBirth", study.patient_birth_.toString("yyyy-MM-dd"));
        }
        data.insert("PatientAge", study.patient_age_);
        data.insert("StudyTime", study.study_time_.toString(NORMAL_DATETIME_FORMAT));
        data.insert("Modality", study.modality_);
        data.insert("StudyDesc", study.study_desc_);
        if (DbManager::insert(study_table_name_, data)) {
            success = true;
        }
    }
    DbManager::CloseDb();
    return success;
}

bool StudyDao::RemoveStudyFromDb(const QString &study_uid) {
    bool success = false;
    if (study_uid.isEmpty()) {
        return false;
    }
    if(DbManager::OpenDb()) {
        QString where = QString("StudyUid = '%1'").arg(study_uid);
        if (DbManager::remove(study_table_name_, where)) {
            success = true;
        }
    }
    DbManager::CloseDb();
    this->RemoveAllImagesOfStudyFromDb(study_uid, false);
    return success;
}

/**
 * @brief StudyDao::VerifyStudyByStuid
 * @param study_uid
 * @return
 */
bool StudyDao::VerifyStudyByStuid(const QString &study_uid) {
    bool success = false;
    if (study_uid.isEmpty()) {
        return false;
    }
    if(DbManager::OpenDb()) {
        QStringList key_list;
        key_list.append("StudyUid");
        QString where = QString("StudyUid = '%1'").arg(study_uid);
        QList<QMap<QString, QVariant>> res;
        if (DbManager::select(study_table_name_, key_list, res, where)) {
            if (res.size() == 1) {
                success = true;
            }
        }
    }
    DbManager::CloseDb();
    return success;
}

bool StudyDao::InsertImageToDb(const ImageRecord &image, bool imported) {
    Q_UNUSED(imported)
    bool success = false;
    if(DbManager::OpenDb()) {
        QMap<QString, QVariant> data;
        data.insert("ImageUid", image.image_uid_);
        data.insert("SopClassUid", image.sop_class_uid_);
        data.insert("SeriesUid", image.series_uid_);
        data.insert("StudyUid", image.study_uid_);
        data.insert("RefImageUid", image.ref_image_uid_);
        data.insert("ImageNo", image.image_number_);
        data.insert("ImageTime", image.image_yime_.toString(NORMAL_DATETIME_FORMAT));
        data.insert("ImageDesc", image.image_desc_);
        data.insert("ImageFile", image.image_file_);
        if (DbManager::insert(image_table_name_, data)) {
            success = true;
        }
    }
    DbManager::CloseDb();
    return success;
}

bool StudyDao::RemoveImageFromDb(const QString &image_uid, bool updateStudy) {
    Q_UNUSED(updateStudy)
    bool success = false;
    // select data && Remove file
    if (image_uid.isEmpty()) {
        return false;
    }
    if (DbManager::OpenDb()) {
        QStringList key_list;
        key_list.append("ImageFile");
        QString where = QString("ImageUid = '%1'").arg(image_uid);
        QList<QMap<QString, QVariant>> res;
        if (DbManager::select(image_table_name_, key_list, res, where)) {
            if (res.size() == 1) {
                const QMap<QString, QVariant> &res0 = res.at(0);
                if (res0.size() == 1) {
                    QString image_file = res0.value("ImageFile").toString();
                    QString file = QString("./DcmFile/%2").arg(image_file);
                    // QString dir_name = file.left(file.lastIndexOf('/'));
                    FileUtil::DeleteFileOrFolder(file);
                    success = true;
                } else {
                }
            } else {
            }
        }
    }
    DbManager::CloseDb();
    // remove data
    if(DbManager::OpenDb()) {
        QString where = QString("ImageUid = '%1'").arg(image_uid);
        if (DbManager::remove(image_table_name_, where)) {
            success = true;
        }
    }
    DbManager::CloseDb();
    return success;
}

bool StudyDao::RemoveAllImagesOfStudyFromDb(
    const QString &study_uid, bool updateStudy) {
    Q_UNUSED(updateStudy)
    if (study_uid.isEmpty()) {
        return false;
    }
    bool result = false;
    //
    QStringList image_uids;
    // select data
    if (DbManager::OpenDb()) {
        QStringList key_list;
        key_list.append("ImageUid");
        QString where = QString("StudyUid = '%1'").arg(study_uid);
        QList<QMap<QString, QVariant>> res;
        if (DbManager::select(image_table_name_, key_list, res, where)) {
            if (res.size() >= 1) {
                for (int i = 0; i < res.size(); i++) {
                    const QMap<QString, QVariant> &res0 = res.at(i);
                    if (res0.size() == 1) {
                        image_uids << res0.value("ImageUid").toString();
                    }
                }
            }
        }
    }
    DbManager::CloseDb();
    // remove data
    foreach (auto var, image_uids) {
        RemoveImageFromDb(var);
    }
    return result;
}

bool StudyDao::UpdateImageFile(const QString &image_uid, const QString &image_file) {
    if (image_uid.isEmpty()) {
        return false;
    }
    if (image_file.isEmpty()) {
        return false;
    }
    bool result = false;
    // Create StudyTable
    QString str ;
    str = "UPDATE ImageTable SET ImageFile=%1 WHERE ImageUid=%2";
    str = str.arg(image_uid, image_file);
    result = DbManager::ExecSqlStr(str);
    return result;
}

bool StudyDao::VerifyImageByIMmuid(const QString &image_uid) {
    bool success = false;
    if (image_uid.isEmpty()) {
        return false;
    }
    if(DbManager::OpenDb()) {
        QStringList key_list;
        key_list.append("ImageUid");
        QString where = QString("ImageUid = '%1'").arg(image_uid);
        QList<QMap<QString, QVariant>> res;
        if (DbManager::select(image_table_name_, key_list, res, where)) {
            if (res.size() == 1) {
                success = true;
            }
        }
    }
    DbManager::CloseDb();
    return success;
}

bool StudyDao::Initial() {
    bool result = false;
    if (DbManager::OpenDb()) {
        bool exist;
        if (DbManager::IsExistTable(study_table_name_, exist)) {
            if (!exist) {
                result = CreateTable();
            } else {
                if (CheckTable()) {
                    result = true;
                } else {
                    if (DbManager::RemoveTable(study_table_name_)) {
                        result = CreateTable();
                    }
                }
            }
        }
    }
    DbManager::CloseDb();
    return result;
}

bool StudyDao::CreateTable() {
    bool result = false;
    // Create StudyTable
    QString str ;
    str = "CREATE TABLE IF NOT EXISTS StudyTable("
          "StudyUid VARCHAR(128) PRIMARY KEY NOT NULL,"
          "AccNumber VARCHAR(64) NOT NULL, PatientId VARCHAR(64) NOT NULL,"
          "PatientName VARCHAR(64), "
          "PatientSex VARCHAR(2) NOT NULL,"
          "PatientBirth DATE NOT NULL,"
          "PatientAge VARCHAR(6),"
          "StudyTime DATETIME NOT NULL,"
          "Modality VARCHAR(2) NOT NULL, "
          "StudyDesc TEXT)";
    result = DbManager::ExecSqlStr(str);
    str = "CREATE INDEX IF NOT EXISTS IX_StudyTable_StudyDate ON StudyTable(StudyTime)";
    result = DbManager::ExecSqlStr(str);
    // Create ImageTable
    str = "CREATE TABLE IF NOT EXISTS ImageTable("
          "ImageUid VARCHAR(128) PRIMARY KEY NOT NULL,"
          "SopClassUid VARCHAR(128) NOT NULL,"
          "SeriesUid VARCHAR(128) NOT NULL, "
          "StudyUid VARCHAR(128) NOT NULL,"
          "RefImageUid VARCHAR(128),"
          "ImageNo VARCHAR(16), "
          "ImageTime DATETIME NOT NULL,"
          "ImageDesc TEXT,"
          "ImageFile TEXT,"
          "FOREIGN KEY(StudyUid) REFERENCES StudyTable(StudyUid))";
    result = DbManager::ExecSqlStr(str);
    str = "CREATE INDEX IF NOT EXISTS IX_ImageTable_ImageTime ON ImageTable(ImageTime)";
    result = DbManager::ExecSqlStr(str);
    return result;
}

bool StudyDao::CheckTable() {
    bool ok1 = false;
    bool ok2 = false;
    if (DbManager::IsExistTable(study_table_name_, ok1) &&
            DbManager::IsExistTable(image_table_name_, ok2) ) {
        if (ok1 && ok2) {
            return true;
        }
    }
    return false;
}
#ifndef DBMANAGER_H
#define DBMANAGER_H

#include <QObject>
#include <QMutex>
#include <QSqlDatabase>

namespace Kiss {
    class DbManager : public QObject {
        Q_OBJECT
      public :
        enum SQLiteType {
            dtNull = 0,//空值类型
            dtInteger = 1,//有符号整数
            dtReal = 2,//有符号浮点数,8字节
            dtText = 3,//文本字符串
            dtBlob = 4,//根据输入类型
            dtVarchar_64 = 5,
            dtTimeStamp = 6,
            dtTimeStamp_NotNull = 7,
        };
      public:
        static bool DbInitial();
        static bool Deallocate();
        static bool CreateDbFile();
        static bool IsOpenedDb();
        static bool OpenDb();
        static bool CloseDb();
        static bool IsExistTable(const QString &table_name, bool &result);
        static bool CreateTable(const QString &table_name,
                                const QStringList &key_list,
                                const QList<SQLiteType> &type_list);
        static bool RemoveTable(const QString &table_name);
        static bool IsExistColumn(const QString &table_name,
                                  const QString &column_name,
                                  bool &result);
        static bool update(const QString &table_name,
                           const QMap<QString, QVariant> &values,
                           const QString &where);
        static bool remove(const QString &table_name,
                           const QString &where = "");
        static bool insert(const QString &table_name,
                           const QMap<QString, QVariant> &values);
        static bool select(const QString &table_name,
                           const QStringList &colunms,
                           QList<QMap<QString, QVariant>> &values,
                           const QString &where = "");
        static bool ExecSqlStr(const QString &sql_str);
      signals:
      public slots:
      private:
        explicit DbManager(QObject *parent = nullptr);
        virtual ~DbManager() override;
      public:
        static QSqlDatabase data_base;
      private:
        static QMutex file_mutex_;
        static QMutex data_mutex_;
        static QString db_name_;
        static QString file_name_;
        static QStringList sqlite_type_string_;
        static bool init_;
    };
}

using namespace Kiss;
#endif // DBMANAGER_H
#include "dbmanager.h"

#include <Global/KissGlobal>

QSqlDatabase DbManager::data_base;
QMutex DbManager::file_mutex_;
QMutex DbManager::data_mutex_;
QString DbManager::db_name_ = DB_CONNECTION_NAME;
QString DbManager::file_name_ = DB_NAME;
QStringList DbManager::sqlite_type_string_;
bool DbManager::init_ = false;

bool DbManager::DbInitial() {
    QMutexLocker locker(&data_mutex_);
    if (!init_) {
        init_ = true;
        if (QSqlDatabase::contains(db_name_)) {
            data_base = QSqlDatabase::database(db_name_);
        } else {
            data_base = QSqlDatabase::addDatabase("QSQLITE", db_name_);
        }
        sqlite_type_string_.append("NULL");
        sqlite_type_string_.append("INTEGER");
        sqlite_type_string_.append("REAL");
        sqlite_type_string_.append("TEXT");
        sqlite_type_string_.append("BLOB");
        sqlite_type_string_.append("VARCHAR ( 64 )");
        sqlite_type_string_.append("TimeStamp");
        sqlite_type_string_.append("TimeStamp NOT NULL");
        return true;
    }
    return true;
}

bool DbManager::Deallocate() {
    QMutexLocker locker(&data_mutex_);
    data_base = QSqlDatabase();
    if (QSqlDatabase::contains(db_name_)) {
        QSqlDatabase::removeDatabase(db_name_);
    }
    return true;
}

bool DbManager::CreateDbFile() {
    if (!QFile::exists(file_name_)) {
        QFile db_file(file_name_);
        if (!db_file.open(QIODevice::WriteOnly)) {
            db_file.close();
            qWarning() << "dbFile open failed";
            return false;
        }
        db_file.close();
    }
    return true;
}

bool DbManager::IsOpenedDb() {
    QMutexLocker locker(&data_mutex_);
    return data_base.isOpen();
}

bool DbManager::OpenDb() {
    file_mutex_.lock();
    if (!IsOpenedDb()) {
        QMutexLocker locker(&data_mutex_);
        data_base.setDatabaseName(file_name_);
        if (!data_base.open()) {
            qWarning() << "database open error:" << data_base.lastError().text();
            return false;
        }
    }
    return true;
}

bool DbManager::CloseDb() {
    file_mutex_.unlock();
    if (IsOpenedDb()) {
        QMutexLocker locker(&data_mutex_);
        data_base.close();
    }
    return true;
}

bool DbManager::IsExistTable(const QString &table_name, bool &result) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str = QString("SELECT 1 FROM sqlite_master "
                              "WHERE type = 'table' AND  "
                              "name = '%1'").arg(table_name);
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        if (query.next()) {
            qint32 sql_result = query.value(0).toInt(); //有表时返回1,无表时返回null
            if (sql_result) {
                result = true;
                return true;
            } else {
                result = false;
                return true;
            }
        } else {
            result = false;
            return true;
        }
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::CreateTable(const QString &table_name,
                            const QStringList &key_list,
                            const QList<DbManager::SQLiteType> &type_list) {
    if (key_list.size() != type_list.size()) {
        qWarning() << "keylist != typelist error";
        return false;
    }
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str_1 = QString("CREATE TABLE %1 (").arg(table_name);
    QString sql_str_2 = "%1 %2 PRIMARY KEY ,";
    QString sql_str_temp = "%1 %2 ,";
    sql_str_2 = sql_str_2
                .arg(key_list.at(0))
                .arg(sqlite_type_string_.at(type_list.at(0)));
    for (qint32 i = 1; i < type_list.size(); ++i) {
        sql_str_2 += sql_str_temp.arg(key_list.at(i))
                     .arg(sqlite_type_string_.at(type_list.at(i)));
    }
    sql_str_2 = sql_str_2.left(sql_str_2.size() - 1);
    QString sql_str_3 = ");";
    QString sql_str = sql_str_1 + sql_str_2 + sql_str_3;
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::RemoveTable(const QString &table_name) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str = QString("DROP TABLE '%1'").arg(table_name);
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::IsExistColumn(const QString &table_name,
                              const QString &column_name,
                              bool &result) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str = QString("SELECT 1 FROM sqlite_master "
                              "WHERE type = 'table' and "
                              "name = '%1' and sql like '%%2%' "
                             ).arg(table_name).arg(column_name);
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        if (query.next()) {
            qint32 sql_result = query.value(0).toInt(); //有此字段时返回1,无字段时返回null
            if (sql_result) {
                result = true;
                return true;
            } else {
                result = false;
                return true;
            }
        } else {
            result = false;
            return true;
        }
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::update(const QString &table_name,
                       const QMap<QString, QVariant> &values,
                       const QString &where) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str_data;
    QList<QString> key_list = values.keys();
    foreach (QString key, key_list) {
        if (!sql_str_data.isEmpty()) {
            sql_str_data += ",";
        }
        sql_str_data += QString("%1=?").arg(key);
    }
    QString sql_str;
    if (where.isEmpty()) {
        sql_str = QString("UPDATE %1 SET %2"
                         ).arg(table_name).arg(sql_str_data);
    } else {
        sql_str = QString("UPDATE %1 SET %2 WHERE %3"
                         ).arg(table_name).arg(sql_str_data).arg(where);
    }
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    query.prepare(sql_str);
    for (qint32 i = 0; i < key_list.count(); ++i) {
        query.bindValue(i, values.value(key_list.at(i)));
    }
    if (query.exec()) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::remove(const QString &table_name,
                       const QString &where) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QMutexLocker locker(&data_mutex_);
    QString sql_str = QString("DELETE FROM %1 WHERE %2"
                             ).arg(table_name).arg(where);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::insert(const QString &table_name,
                       const QMap<QString, QVariant> &values) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str_column, sql_str_data;
    QList<QString> key_list = values.keys();
    foreach (QString key, key_list) {
        if (!sql_str_column.isEmpty()) {
            sql_str_column += ",";
        }
        sql_str_column += key;
        if (!sql_str_data.isEmpty()) {
            sql_str_data += ",";
        }
        sql_str_data += "?";
    }
    QString sql_str = QString("INSERT INTO %1(%2) VALUES(%3)")
                      .arg(table_name).arg(sql_str_column).arg(sql_str_data);
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    query.prepare(sql_str);
    for (qint32 i = 0; i < key_list.count(); ++i) {
        query.bindValue(i, values.value(key_list.at(i)));
    }
    if (query.exec()) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

bool DbManager::select(const QString &table_name,
                       const QStringList &colunms,
                       QList<QMap<QString, QVariant>> &values,
                       const QString &where) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QString sql_str_columns;
    if (colunms.size()) {
        sql_str_columns = colunms.join(",");
    } else {
//        sql_str_columns = "*";
        qWarning() << "colunms is null";
        return false;
    }
    QString sql_str;
    if (where.isEmpty()) {
        sql_str = QString("SELECT %1 FROM %2")
                  .arg(sql_str_columns)
                  .arg(table_name);
    } else {
        sql_str = QString("SELECT %1 FROM %2 WHERE %3")
                  .arg(sql_str_columns)
                  .arg(table_name).arg(where);
    }
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        qint32 columns_sum = query.record().count();
        while (query.next()) {
            QMap<QString, QVariant> row;
            for (qint32 i = 0; i < columns_sum; ++i) {
                row.insert(colunms.at(i), query.value(i));
            }
            values.append(row);
        }
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError();
        return false;
    }
}

bool DbManager::ExecSqlStr(const QString &sql_str) {
    if (!IsOpenedDb()) {
        qWarning() << "database not open error!";
        return false;
    }
    QMutexLocker locker(&data_mutex_);
    QSqlQuery query(data_base);
    if (query.exec(sql_str)) {
        return true;
    } else {
        qWarning() << "sqlstr exec error:" << data_base.lastError().text();
        return false;
    }
}

DbManager::DbManager(QObject *parent) : QObject(parent) {
}

DbManager::~DbManager() {
}

1.5 添加数据 StoreScp

1.6 添加数据本地加载

两个线程,一个负责打开本地dcm文件,一个往数据库添加

#include "importdcmfilethread.h"

#include <Db/KissDb>
#include <Global/KissGlobal>

#include "dcmtk/dcmdata/dcuid.h"


ImportDcmFileThread::ImportDcmFileThread(ImportStudyModel *model, QObject *parent) :
    QThread(parent) {
    this->abort_ = false;
    this->import_model_ = model;
}

void ImportDcmFileThread::run() {
    StudyDao dao;
    foreach (StudyRecord *study, import_model_->getStudyList()) {
        if (abort_) {
            break;
        }
        int images = 0;
        QString study_dir_name =
            QString("%1/%2_%3").arg(study->study_time_.date().toString("yyyyMM"),
                                    study->study_time_.toString(DICOM_DATETIME_FORMAT),
                                    study->acc_number_);
        if(!dao.VerifyStudyByStuid(study->study_uid_)) {
            dao.InsertStudyToDb(*study, true);
        }
        FileUtil::DirMake(QString("%1/%2").arg(DICOM_SAVE_PATH, study_dir_name));
        foreach (ImageRecord *image, study->image_list_) {
            bool raw = image->sop_class_uid_ == QString(UID_XRayAngiographicImageStorage);
            QString src_file = image->image_file_;
            image->image_file_ = QString("%1/%2_%3.dcm").arg(study_dir_name,
                                 raw ? "angio" : "", image->image_uid_);
            QFileInfo info(QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_));
            if (FileUtil::FileCopy(src_file, QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_))) {
                if (!dao.VerifyImageByIMmuid(image->image_uid_)) {
                    if (dao.InsertImageToDb(*image, true)) {
                        images++;
                    } else {
                    }
                } else {
                    if (dao.UpdateImageFile(image->image_uid_, image->image_file_)) {
                        images++;
                    } else {
                        FileUtil::DeleteFileOrFolder(
                            QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_));
                    }
                }
            }
            image->image_file_ = src_file;
            emit Signal_ResultReady();
        }
        study->status_ = tr("Imported: Images %1.").arg(images);
        import_model_->resetStudyStatus(study);
    }
}


void ImportDcmFileThread::SetAbort(bool yes) {
    abort_ = yes;
}
#include "scandcmfilethread.h"

#include <Global/KissGlobal>

#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmdata/dcfilefo.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmsr/dsrdoc.h"
#include "dcmtk/dcmimgle/dcmimage.h"
#include "dcmtk/dcmdata/dcuid.h"


ScanDcmFileThread::ScanDcmFileThread(QObject *parent) :
    QThread(parent) {
    this->abort_ = false;
}

void ScanDcmFileThread::run() {
    using namespace Kiss;
    foreach (QString file, file_list_) {
        if (abort_) {
            break;
        }
        StudyRecord *study = nullptr;
        DcmFileFormat dcmFile;
        OFCondition cond = dcmFile.loadFile(file.toLocal8Bit().data());
        DcmDataset *dset = dcmFile.getDataset();
        if (cond.good() && dset) {
            const char *value = nullptr;
            QString studyUid, seriesUid, instUid, sopClassUid;
            dset->findAndGetString(DCM_StudyInstanceUID, value);
            studyUid = QString::fromLatin1(value);
            dset->findAndGetString(DCM_SeriesInstanceUID, value);
            seriesUid = QString::fromLatin1(value);
            dset->findAndGetString(DCM_SOPInstanceUID, value);
            instUid = QString::fromLatin1(value);
            dset->findAndGetString(DCM_SOPClassUID, value);
            sopClassUid = QString::fromLatin1(value);
            if (!(studyUid.isEmpty() || seriesUid.isEmpty() ||
                    instUid.isEmpty() || sopClassUid.isEmpty())) {
                study = new StudyRecord(studyUid);
                dset->findAndGetString(DCM_AccessionNumber, value);
                study->acc_number_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_PatientID, value);
                study->patient_id_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_PatientName, value);
                study->patient_name_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_PatientSex, value);
                study->patient_sex_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_PatientBirthDate, value);
                study->patient_birth_ = QDate::fromString(QString::fromLatin1(value), "yyyyMMdd");
                dset->findAndGetString(DCM_PatientAge, value);
                study->patient_age_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_StudyDate, value);
                study->study_time_.setDate(QDate::fromString(QString::fromLatin1(value), "yyyyMMdd"));
                dset->findAndGetString(DCM_StudyTime, value);
                study->study_time_.setTime(formatDicomTime(QString::fromLatin1(value)));
                dset->findAndGetString(DCM_StudyDescription, value);
                study->study_desc_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_InstitutionName, value);
                study->institution_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_Modality, value);
                study->modality_ = QString::fromLatin1(value);
                if (sopClassUid == UID_XRayAngiographicImageStorage ||// 造影血管
                        true) {
                    ImageRecord *image = new ImageRecord(instUid);
                    image->sop_class_uid_ = sopClassUid;
                    image->series_uid_ = seriesUid;
                    image->study_uid_ = studyUid;
                    image->image_file_ = file;
                    study->image_list_.append(image);
                    dset->findAndGetString(DCM_ReferencedSOPInstanceUID, value, true);
                    image->ref_image_uid_ = QString::fromLatin1(value);
                    dset->findAndGetString(DCM_InstanceNumber, value);
                    image->image_number_ = QString::fromLatin1(value);
                    dset->findAndGetString(DCM_SeriesDescription, value);
                    image->image_desc_ = QString::fromLocal8Bit(value);
                    dset->findAndGetString(DCM_ContentDate, value);
                    image->image_yime_.setDate(
                        QDate::fromString(QString::fromLatin1(value), "yyyyMMdd"));
                    dset->findAndGetString(DCM_ContentTime, value);
                    image->image_yime_.setTime(formatDicomTime(QString::fromLatin1(value)));
                }
            }
        }
        if (study && (study->image_list_.isEmpty())) {
            delete study;
            study = nullptr;
        }
        emit Signal_ResultRecord(study);
        emit Signal_ResultReady();
    }
}

void ScanDcmFileThread::SetFiles(const QStringList &files) {
    file_list_ = files;
}

void ScanDcmFileThread::SetAbort(bool yes) {
    abort_ = yes;
}

2 Echo

前言:
要做一个简单的开源dcm浏览器 KISS Dicom Viewer ,小型pacs服务肯定必不可少。开发中到处找现成代码,基本上找到的资源都是一个发布版,很少有可以用来研究的源码。 KISS Dicom Viewer 目前处于开发阶段,最近几篇博客就用来记录下开发一个小型pacs数据库(Qt+Dcmtk)的过程。提供服务包括:通讯测试echo、远程查询findscu、远程下载get/move、本机存储storescp。

Dicom协议、通讯原理等等,网上有很多优秀的中文博客来说明,这里就不介绍了。


Dcmtk 封装的echoscu 服务说明 https://support.dcmtk.org/docs/echoscu.html
如果你需要定义自己的echoscu服务,可以看下我整理的:

2.1 如何使用

传入参数 依次是 aec aet ip part

    LocalSettings settings;
    QString msg;
    if (EchoSCU(settings.statInfo.aetitle, "Echo",
                Kiss::getLocalIP(), settings.statInfo.store_port, msg)) {
        QMessageBox::information(
            this, QString("Echo SCP"), QString("Echo succeeded."));
    } else {
        QMessageBox::critical(this, QString("Echo SCP"), msg);
    }

2.2 获取本机 ip

    GLOBAL_EXTERN bool isIP(const QString &ip);
    GLOBAL_EXTERN QString getLocalIP();

    QString getLocalIP() {
        QStringList ips;
        QList<QHostAddress> addrs = QNetworkInterface::allAddresses();
        foreach (QHostAddress addr, addrs) {
            QString ip = addr.toString();
            if (isIP(ip)) {
                ips << ip;
            }
        }
        //优先取192开头的IP,如果获取不到IP则取127.0.0.1
        QString ip = "127.0.0.1";
        foreach (QString str, ips) {
            if (str.startsWith("192.168.1") || str.startsWith("192")) {
                ip = str;
                break;
            }
        }
        return ip;
    }

    bool isIP(const QString &ip) {
        QRegExp RegExp("((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)");
        return RegExp.exactMatch(ip);
    }

2.3 端口、aetitle 配置文件记录

#define LOCALSETTINGS_CFG "etc/localsettings.cfg"

struct StationInfo {
    QString aetitle;
    ushort store_port;
    friend QDataStream &operator<<(QDataStream &out, const StationInfo &info) {
        return out << info.aetitle << info.store_port;
    }
    friend QDataStream &operator>>(QDataStream &in, StationInfo &info) {
        return in >> info.aetitle >> info.store_port;
    }
};

class LocalSettings {
  public:
    LocalSettings();
    void saveConfig();
    void loadConfig();
    StationInfo statInfo;
};
LocalSettings::LocalSettings() {
    loadConfig();
}

void LocalSettings::saveConfig() {
    QFile file(LOCALSETTINGS_CFG);
    if (file.open(QIODevice::WriteOnly)) {
        QDataStream out(&file);
        out << statInfo;
        file.close();
    }
}

void LocalSettings::loadConfig() {
    QFile file(LOCALSETTINGS_CFG);
    if (file.open(QIODevice::ReadOnly)) {
        QDataStream in(&file);
        in >> statInfo ;
        file.close();
    }
}

2.4 调用dcmtk echoscu接口 服务

#ifndef ECHOSCU_H
#define ECHOSCU_H
#ifdef ECHOSCU_CPP
    #define ECHOSCU_EXTERN extern
#else
    #define ECHOSCU_EXTERN
#endif
class QString;
ECHOSCU_EXTERN bool EchoSCU(const QString &peer_title, const QString &our_title,
                            const QString &hostname, int port, QString &msg);
#endif // ECHOSCU_H
#define ECHOSCU_CPP
#include "echoscu.h"
#include <QString>

#include "dcmtk/config/osconfig.h"
/* make sure OS specific configuration is included first */

#define INCLUDE_CSTDLIB
#define INCLUDE_CSTDIO
#define INCLUDE_CSTRING
#define INCLUDE_CSTDARG
#include "dcmtk/ofstd/ofstdinc.h"

#include "dcmtk/dcmnet/dimse.h"
#include "dcmtk/dcmnet/diutil.h"
#include "dcmtk/dcmdata/dcfilefo.h"
#include "dcmtk/dcmdata/dcdict.h"
#include "dcmtk/dcmdata/dcuid.h"

/* DICOM 标准转移语法 */
static const char *transferSyntaxes[] = {
    UID_LittleEndianImplicitTransferSyntax, /* 默认 */
    UID_LittleEndianExplicitTransferSyntax,
    UID_BigEndianExplicitTransferSyntax,
    UID_JPEGProcess1TransferSyntax,
    UID_JPEGProcess2_4TransferSyntax,
    UID_JPEGProcess3_5TransferSyntax,
    UID_JPEGProcess6_8TransferSyntax,
    UID_JPEGProcess7_9TransferSyntax,
    UID_JPEGProcess10_12TransferSyntax,
    UID_JPEGProcess11_13TransferSyntax,
    UID_JPEGProcess14TransferSyntax,
    UID_JPEGProcess15TransferSyntax,
    UID_JPEGProcess16_18TransferSyntax,
    UID_JPEGProcess17_19TransferSyntax,
    UID_JPEGProcess20_22TransferSyntax,
    UID_JPEGProcess21_23TransferSyntax,
    UID_JPEGProcess24_26TransferSyntax,
    UID_JPEGProcess25_27TransferSyntax,
    UID_JPEGProcess28TransferSyntax,
    UID_JPEGProcess29TransferSyntax,
    UID_JPEGProcess14SV1TransferSyntax,
    UID_RLELosslessTransferSyntax,
    UID_JPEGLSLosslessTransferSyntax,
    UID_JPEGLSLossyTransferSyntax,
    UID_DeflatedExplicitVRLittleEndianTransferSyntax,
    UID_JPEG2000LosslessOnlyTransferSyntax,
    UID_JPEG2000TransferSyntax,
    UID_MPEG2MainProfileAtMainLevelTransferSyntax,
    UID_MPEG2MainProfileAtHighLevelTransferSyntax,
    UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax,
    UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax
};

bool EchoSCU(const QString &peer_title,
             const QString &our_title,
             const QString &hostname,
             int port,
             QString &msg) {
    //------------------------------Initialization Work----------------------------//
    T_ASC_Network *net;
    T_ASC_Parameters *params;
    T_ASC_Association *assoc;
    OFString temp_str;
    bool ret = false;
    DIC_NODENAME local_host;
    DIC_NODENAME peer_host;
    DIC_US msg_id;
    DIC_US status;
    DcmDataset *status_detail = nullptr;
    int presentation_context_id = 1;
    OFCondition cond = ASC_initializeNetwork(NET_REQUESTOR, 0, 6, &net);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        msg = QString::fromLatin1(temp_str.c_str());
        goto Cleanup;
    }
    cond = ASC_createAssociationParameters(&params, ASC_DEFAULTMAXPDU);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        msg = QString::fromLatin1(temp_str.c_str());
        goto Cleanup;
    }
    ASC_setAPTitles(params,
                    our_title.toLocal8Bit().data(),
                    peer_title.toLocal8Bit().data(),
                    nullptr);
    cond = ASC_setTransportLayerType(params, OFFalse);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        msg = QString::fromLatin1(temp_str.c_str());
        goto Cleanup;
    }
    gethostname(local_host, sizeof(local_host) - 1);
    sprintf(peer_host, "%s:%d", hostname.toLocal8Bit().data(), port);
    ASC_setPresentationAddresses(params, local_host, peer_host);
    cond = ASC_addPresentationContext(
               params, static_cast<unsigned char>(presentation_context_id),
               UID_VerificationSOPClass, transferSyntaxes, 3);
    presentation_context_id += 2;
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        msg = QString::fromLatin1(temp_str.c_str());
        goto Cleanup;
    }
    cond = ASC_requestAssociation(net, params, &assoc);
    if (cond.bad()) {
        if (cond == DUL_ASSOCIATIONREJECTED) {
            T_ASC_RejectParameters rej;
            ASC_getRejectParameters(params, &rej);
            ASC_printRejectParameters(temp_str, &rej);
            msg = QString("Association rejected: %1").arg(temp_str.c_str());
            goto Cleanup;
        } else {
            DimseCondition::dump(temp_str, cond);
            msg = QString("Association request failed: %1").arg(temp_str.c_str());
            goto Cleanup;
        }
    }
    if (ASC_countAcceptedPresentationContexts(params) == 0) {
        msg = QString("No Acceptable Presentation Contexts");
        goto Cleanup;
    }
    //------------------------------Real Work----------------------------//
    msg_id = assoc->nextMsgID++;
    cond = DIMSE_echoUser(
               /* in */ assoc, msg_id,
               /* blocking info for response */ DIMSE_BLOCKING, 0,
               /* out */ &status,
               /* Detail */ &status_detail);
    if (status_detail != nullptr) {
        delete status_detail;
    }
    if (cond == EC_Normal) {
        cond = ASC_releaseAssociation(assoc);
        ret = true;
    } else if (cond == DUL_PEERABORTEDASSOCIATION) {
    } else {
        DimseCondition::dump(temp_str, cond);
        msg = QString::fromLatin1(temp_str.c_str());
        cond = ASC_abortAssociation(assoc);
    }
    //------------------------------Cleanup Work----------------------------//
Cleanup:
    cond = ASC_destroyAssociation(&assoc);
    cond = ASC_dropNetwork(&net);
    return ret;
}

3 StoreScp

前言:
要做一个简单的开源dcm浏览器 KISS Dicom Viewer ,小型pacs服务肯定必不可少。开发中到处找现成代码,基本上找到的资源都是一个发布版,很少有可以用来研究的源码。 KISS Dicom Viewer 目前处于开发阶段,最近几篇博客就用来记录下开发一个小型pacs数据库(Qt+Dcmtk)的过程。提供服务包括:通讯测试echo、远程查询findscu、远程下载get/move、本机存储storescp。

Dicom协议、通讯原理等等,网上有很多优秀的中文博客来说明,这里就不介绍了。


Dcmtk 封装的echoscu 服务说明 https://support.dcmtk.org/docs/storescp.html
如果你需要定义自己的 STORE 服务,可以看下我整理的:

3.1 如何使用

    // 初始化函数加上
    store_scp_ = new StoreScpThread(this);
    store_scp_->start();
    // 析构里加上
    store_scp_->terminate();

3.2 StoreScp接口服务

**run**就是一个死循环的线程,先打开端口让后反复开启scp服务(每次仅开启和调用一个服务,如果你要求高我这个应该不满足你)。

StoreScpThread::AcceptAssociation:开启StoreScp,接受远程请求。
StoreScpThread::ProcessCommands:处理远程命令,目前仅支持 ECHO 和 STORESCU。没有支持find、get、move因为我这个本身就是一个临时的本地存储pacs,而不是作为服务器用的。
StoreScpThread::EchoSCP:处理echo。
StoreScpThread::StoreSCP:处理storescu,并在本地数据库添加信息和存储dcm文件。
StoreSCPCallback:StoreSCU服务的回调函数(因为每次传输dcm文件数量不确定)。
StoreScpThread::insertImageToDB:是用来向数据库存储dcm文件的,我这里比较傻先把dcm文件接受到本地一个cache目录,让后再从本地拷贝到目标目录。自己开发的pacs数据库跟我肯定不一样,insertImageToDB这个函数替换成自己的。

#ifndef STORESCPTHREAD_H
#define STORESCPTHREAD_H

#include <QThread>

#include "dcmtk/ofstd/ofcond.h"
#include "dcmtk/dcmnet/assoc.h"
#include "dcmtk/config/osconfig.h"
#include "dcmtk/dcmnet/dimse.h"

struct T_ASC_Network;
struct T_ASC_Association;
struct T_DIMSE_Message;
class DcmFileFormat;
class StudyRecord;
class DcmAssociationConfiguration;

class StoreScpThread : public QThread {
    Q_OBJECT
  public:
    explicit StoreScpThread(QObject *parent = nullptr);
    void setAbort(const bool &yes);
    void run();
  private:
    OFCondition AcceptAssociation(T_ASC_Network *net,
                                  DcmAssociationConfiguration &asccfg);
    OFCondition ProcessCommands(T_ASC_Association *assoc);
    OFCondition EchoSCP( T_ASC_Association *assoc,
                         T_DIMSE_Message *msg, T_ASC_PresentationContextID presID);
    OFCondition StoreSCP(T_ASC_Association *assoc,
                         T_DIMSE_Message *msg, T_ASC_PresentationContextID presID);
  private:
    bool abort_;

};



#endif // STORESCPTHREAD_H
#include "storescpthread.h"

#include <Db/KissDb>
#include "Global/global.h"
#include "Global/studyrecord.h"
#include "Global/KissDicomViewConfig.h"

#include <QDir>
#include <QDebug>

#include "dcmtk/config/osconfig.h"
/* make sure OS specific configuration is included first */

#define INCLUDE_CSTDLIB
#define INCLUDE_CSTRING
#define INCLUDE_CSTDARG
#define INCLUDE_CCTYPE
#define INCLUDE_CSIGNAL

BEGIN_EXTERN_C
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
END_EXTERN_C

#include "dcmtk/ofstd/ofstdinc.h"
#include "dcmtk/ofstd/ofstd.h"
#include "dcmtk/dcmnet/cond.h"
#include "dcmtk/ofstd/ofdatime.h"
#include "dcmtk/dcmnet/dicom.h"
#include "dcmtk/dcmnet/dimse.h"
#include "dcmtk/dcmnet/diutil.h"
#include "dcmtk/dcmnet/dcasccfg.h"
#include "dcmtk/dcmnet/dcasccff.h"
#include "dcmtk/dcmdata/dcfilefo.h"
#include "dcmtk/dcmdata/dcuid.h"
#include "dcmtk/dcmdata/dcdict.h"
#include "dcmtk/dcmsr/dsrdoc.h"
#include "dcmtk/dcmdata/dcmetinf.h"
#include "dcmtk/dcmdata/dcuid.h"
#include "dcmtk/dcmdata/dcdeftag.h"
#include "dcmtk/dcmdata/dcostrmz.h"

static void insertImageToDB(
    DcmFileFormat *ff, StudyRecord *study, QString &patientName);

struct StoreCallbackData {
    StudyRecord *study;
    DcmFileFormat *dcmff;
    T_ASC_Association *assoc;
    QString patientInfo;
};

static void StoreSCPCallback(
    void *callbackData,
    T_DIMSE_StoreProgress *progress,
    T_DIMSE_C_StoreRQ *req,
    char * /*imageFileName*/, DcmDataset **imageDataSet,
    T_DIMSE_C_StoreRSP *rsp,
    DcmDataset **statusDetail) {
    DIC_UI sopClass;
    DIC_UI sopInstance;
    if (progress->state == DIMSE_StoreEnd) {
        *statusDetail = nullptr;
        StoreCallbackData *cbdata = OFstatic_cast(StoreCallbackData *, callbackData);
        insertImageToDB(cbdata->dcmff, cbdata->study, cbdata->patientInfo);
        if (rsp->DimseStatus == STATUS_Success) {
            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet,
                    sopClass, sizeof(sopClass),
                    sopInstance, sizeof(sopInstance))) {
                rsp->DimseStatus = STATUS_STORE_Error_CannotUnderstand;
            } else if (strcmp(sopClass, req->AffectedSOPClassUID) != 0) {
                rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
            } else if (strcmp(sopInstance, req->AffectedSOPInstanceUID) != 0) {
                rsp->DimseStatus = STATUS_STORE_Error_DataSetDoesNotMatchSOPClass;
            }
        }
    }
}

StoreScpThread::StoreScpThread(QObject *parent) :
    QThread(parent),
    abort_(false) {
}

void StoreScpThread::setAbort(const bool &yes) {
    abort_ = yes;
}

void StoreScpThread::run() {
    //-----------------------------初始化端口监听----------------------------------------//
    /* 创建T_ASC_Network*的实例。 */
    T_ASC_Network *net;
    DcmAssociationConfiguration asccfg;
    OFString temp_str;
    LocalSettings settings;
    int port = settings.statInfo.store_port;
    OFCondition cond = ASC_initializeNetwork(NET_ACCEPTOR, port, 30, &net);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString("无法创建网络: %1.")
                 .arg(temp_str.c_str());
    }
    //-------------------------------绑定端口提供scp服务--------------------------------------//
    while (cond.good() && (!abort_)) {
        /* 接收关联并确认或拒绝它。
         * 如果这个联系得到承认,
         * 提供相应的服务,并根据需要调用一个或多个服务。 */
        cond = AcceptAssociation(net, asccfg);
    }
    //--------------------------------销毁端口监听-------------------------------------//
    /* 释放内存 T_ASC_Network*.
     * 此调用与上面调用的ASC_initializeNetwork(…)相对应。 */
    if (cond.good()) {
        cond = ASC_dropNetwork(&net);
    }
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString(temp_str.c_str());
    }
}

/**
 * @brief StoreScpThread::AcceptAssociation
 * @param net
 * @return
 */
OFCondition StoreScpThread::AcceptAssociation(
    T_ASC_Network *net, DcmAssociationConfiguration &/*asccfg*/) {
    //------------------------------Initialization Work----------------------------//
    char buf[BUFSIZ];
    T_ASC_Association *assoc;
    OFCondition cond;
    OFString temp_str;
    const char *knownAbstractSyntaxes[] = {
        UID_VerificationSOPClass
    };
    const char *transferSyntaxes[] = {
        nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
        nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr
    };
    int numTransferSyntaxes = 0;
    // 尝试接收关联。在这里,我们要么使用阻塞,要么使用非阻塞,这取决于是否设置了选项--eostudy timeout。
    cond = ASC_receiveAssociation(net, &assoc, ASC_DEFAULTMAXPDU);
    // 如果出了什么差错,一定要处理好
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString("接收关联失败: %1.").arg(temp_str.c_str());
    }
    if (gLocalByteOrder == EBO_LittleEndian) { /* defined in dcxfer.h */
        transferSyntaxes[0] = UID_LittleEndianExplicitTransferSyntax;
        transferSyntaxes[1] = UID_BigEndianExplicitTransferSyntax;
    } else {
        transferSyntaxes[0] = UID_BigEndianExplicitTransferSyntax;
        transferSyntaxes[1] = UID_LittleEndianExplicitTransferSyntax;
    }
    transferSyntaxes[2] = UID_LittleEndianImplicitTransferSyntax;
    numTransferSyntaxes = 3;
    /* 接受验证SOP类(如有) */
    if (cond.good()) {
        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
                   assoc->params, knownAbstractSyntaxes,
                   DIM_OF(knownAbstractSyntaxes), transferSyntaxes, numTransferSyntaxes);
        if (cond.bad()) {
            DimseCondition::dump(temp_str, cond);
            qDebug() << QString(temp_str.c_str());
        }
    }
    /* 存储SOP类uid的数组来自dcuid.h */
    if (cond.good()) {
        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
                   assoc->params, dcmAllStorageSOPClassUIDs,
                   numberOfDcmAllStorageSOPClassUIDs,
                   transferSyntaxes, numTransferSyntaxes);
        if (cond.bad()) {
            DimseCondition::dump(temp_str, cond);
            qDebug() << QString(temp_str.c_str());
        }
    }
    /* 设置应用程序标题 */
    LocalSettings settings;
    QString aetitle = settings.statInfo.aetitle;
    if (aetitle.isEmpty()) {
        qDebug() << "aetitle is DEFAULT 'DRDCM' ";
        aetitle = "DRDCM";
    }
    ASC_setAPTitles(assoc->params, nullptr, nullptr, aetitle.toLocal8Bit().data());
    /* 承认或拒绝此关联 */
    if (cond.good()) {
        cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
        if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0) {
            /* 拒绝:不支持应用程序上下文名称 */
            T_ASC_RejectParameters rej = {
                ASC_RESULT_REJECTEDPERMANENT,
                ASC_SOURCE_SERVICEUSER,
                ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
            };
            DimseCondition::dump(temp_str, cond);
            qDebug() << QString("关联被拒绝:应用程序上下文名称错误: %1.").arg(buf);
            cond = ASC_rejectAssociation(assoc, &rej);
            if (cond.bad()) {
                DimseCondition::dump(temp_str, cond);
                qDebug() << QString(temp_str.c_str());
            }
        } else {
            cond = ASC_acknowledgeAssociation(assoc);
            if (cond.bad()) {
                DimseCondition::dump(temp_str, cond);
                qDebug() << QString(temp_str.c_str());
            }
        }
    }
    //------------------------------Real Work----------------------------//
    if (cond.good()) {
        // 将调用和调用的 aetitle 存储在全局变量中,以启用使用它们的--exec选项。 aetitles 可能包含空格字符。
        DIC_AE callingTitle;
        DIC_AE calledTitle;
        ASC_getAPTitles(assoc->params, callingTitle, sizeof(callingTitle),
                        calledTitle,  sizeof(calledTitle), nullptr, 0).good();
        // 现在做实际工作,即通过建立的网络连接接收DIMSE命令,并相应地处理这些命令.
        // 对于storscp,只能处理 C-ECHO-RQ 和 C-STORE-RQ 命令.
        cond = ProcessCommands(assoc);
        if (cond == DUL_PEERREQUESTEDRELEASE) {
            cond = ASC_acknowledgeRelease(assoc);
        } else {
            DimseCondition::dump(temp_str, cond);
            qDebug() << QString("DIMSE失败(中止关联): %1.").arg(temp_str.c_str());
            /* 某种错误,所以中止了关联n */
            cond = ASC_abortAssociation(assoc);
        }
    }
    //------------------------------Cleanup Work----------------------------//
    cond = ASC_dropSCPAssociation(assoc);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString(temp_str.c_str());
    }
    cond = ASC_destroyAssociation(&assoc);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString(temp_str.c_str());
    }
    return cond;
}

/**
 * @brief StoreScpThread::ProcessCommands
 * @param assoc
 * @return
 */
OFCondition StoreScpThread::ProcessCommands(T_ASC_Association *assoc) {
    OFCondition cond = EC_Normal;
    T_DIMSE_Message msg;
    T_ASC_PresentationContextID presID = 0;
    DcmDataset *statusDetail = nullptr;
    // 启动循环以能够接收多个DIMSE命令
    while( cond == EC_Normal || cond == DIMSE_NODATAAVAILABLE || cond == DIMSE_OUTOFRESOURCES ) {
        // 通过网络接收DIMSE命令
        cond = DIMSE_receiveCommand(assoc, DIMSE_BLOCKING, 0, &presID, &msg, &statusDetail);
        // 如果收到的命令有额外的状态详细信息,则转储此信息
        if (statusDetail != nullptr) {
            delete statusDetail;
        }
        // 检查对等机是否释放或中止,或者我们是否有有效的消息
        if (cond == EC_Normal) { // 收到正常请求
            switch (msg.CommandField) {
                case DIMSE_C_ECHO_RQ:
                    // 处理 C-ECHO-Request
                    qDebug() << QString("收到 C-ECHO-Request 服务请求,开始处理");
                    cond = EchoSCP(assoc, &msg, presID);
                    break;
                case DIMSE_C_STORE_RQ:
                    // 处理 C-STORE-Request
                    qDebug() << QString("收到 C-STORE-Request 服务请求,开始处理");
                    cond = StoreSCP(assoc, &msg, presID);
                    break;
                default:
                    // 其他服务不处理 (查询和下载 还没空开发)
                    qDebug() << QString("无法处理命令: 0x%1.").arg(static_cast<unsigned>(msg.CommandField));
                    cond = DIMSE_BADCOMMANDTYPE;
                    break;
            }
        }
    }
    return cond;
}

/**
 * @brief StoreScpThread::EchoSCP
 * 处理 C-ECHO-Request
 * @param assoc
 * @param msg
 * @param presID
 * @return
 */
OFCondition StoreScpThread::EchoSCP(
    T_ASC_Association *assoc, T_DIMSE_Message *msg, T_ASC_PresentationContextID presID) {
    // 初始化一些变量
    OFString temp_str;
    OFCondition cond = DIMSE_sendEchoResponse(assoc, presID,
                       &msg->msg.CEchoRQ, STATUS_Success, nullptr);
    if (cond.bad()) {
        DimseCondition::dump(temp_str, cond);
        qDebug() << QString("Echo SCP 服务失败: %1.").arg(temp_str.c_str());
    } else {
        qDebug() << QString("Echo SCP 测试成功: %1").arg(
                     QTime::currentTime().toString(NORMAL_DATETIME_FORMAT));
    }
    return cond;
}

/**
 * @brief StoreScpThread::StoreSCP
 * 处理 C-STORE-Request
 * @param assoc
 * @param msg
 * @param presID
 * @return
 */
OFCondition StoreScpThread::StoreSCP(
    T_ASC_Association *assoc,
    T_DIMSE_Message *msg,
    T_ASC_PresentationContextID presID) {
    OFCondition cond = EC_Normal;
    T_DIMSE_C_StoreRQ *req;
    // 将C-STORE-RQ命令的实际信息分配给局部变量
    req = &msg->msg.CStoreRQ;
    // 初始化一些变量
    StoreCallbackData callbackData;
    DcmFileFormat dcmff;
    StudyRecord study;
    callbackData.assoc = assoc;
    callbackData.dcmff = &dcmff;
    callbackData.study = &study;
    const char *aet = nullptr;
    const char *aec = nullptr;
    // 将 SourceApplicationEntityTitle 存储在 metaheader 中
    if (assoc && assoc->params) {
        aet = assoc->params->DULparams.callingAPTitle;
        aec = assoc->params->DULparams.calledAPTitle;
        if (aet) {
            dcmff.getMetaInfo()->putAndInsertString(DCM_SourceApplicationEntityTitle, aet);
        }
    }
    LocalSettings settings;
    QString aetitle = settings.statInfo.aetitle;
    if(QString(aec) != aetitle) {
        qDebug() << "名称校验失败" << aet << QString(aec) << aetitle;
    } else {
        // 定义一个地址,用于存储通过网络接收的信息
        DcmDataset *dset = dcmff.getDataset();
        cond = DIMSE_storeProvider(assoc, presID, req, nullptr, OFTrue, &dset,
                                   StoreSCPCallback, &callbackData, DIMSE_BLOCKING, 0);
        // 如果出现错误,请转储相应的信息,必要时删除输出文件
        if (cond.bad()) {
            OFString temp_str;
            DimseCondition::dump(temp_str, cond);
            qDebug() << QString("Store SCP 失败: %1.").arg(temp_str.c_str());
        } else {
            qDebug() << QString("Store SCP 成功: %1 : %2, %3").arg(
                         callbackData.patientInfo,
                         QString::fromLocal8Bit(aet),
                         QTime::currentTime().toString(NORMAL_DATETIME_FORMAT));
        }
    }
    // 返回返回值
    return cond;
}

static void insertImageToDB(
    DcmFileFormat *ff, StudyRecord *study, QString &) {
    DcmDataset *dset;
    if (ff && (dset = ff->getDataset()) && study) {
        const char *value = nullptr;
        QString studyUid, seriesUid, instUid, sopClassUid;
        dset->findAndGetString(DCM_StudyInstanceUID, value);
        studyUid = QString::fromLatin1(value);
        dset->findAndGetString(DCM_SeriesInstanceUID, value);
        seriesUid = QString::fromLatin1(value);
        dset->findAndGetString(DCM_SOPInstanceUID, value);
        instUid = QString::fromLatin1(value);
        dset->findAndGetString(DCM_SOPClassUID, value);
        sopClassUid = QString::fromLatin1(value);
        if (!(studyUid.isEmpty() || seriesUid.isEmpty() ||
                instUid.isEmpty() || sopClassUid.isEmpty())) {
            if (study->study_uid_ != studyUid) {
                study->study_uid_ = studyUid;
                study = new StudyRecord(studyUid);
                dset->findAndGetString(DCM_AccessionNumber, value);
                study->acc_number_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_PatientID, value);
                study->patient_id_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_PatientName, value);
                study->patient_name_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_PatientSex, value);
                study->patient_sex_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_PatientBirthDate, value);
                study->patient_birth_ = QDate::fromString(QString::fromLatin1(value), "yyyyMMdd");
                dset->findAndGetString(DCM_PatientAge, value);
                study->patient_age_ = QString::fromLocal8Bit(value).remove(QChar(' '));
                dset->findAndGetString(DCM_StudyDate, value);
                study->study_time_.setDate(QDate::fromString(QString::fromLatin1(value), "yyyyMMdd"));
                dset->findAndGetString(DCM_StudyTime, value);
                study->study_time_.setTime(formatDicomTime(QString::fromLatin1(value)));
                dset->findAndGetString(DCM_StudyDescription, value);
                study->study_desc_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_InstitutionName, value);
                study->institution_ = QString::fromLocal8Bit(value);
                dset->findAndGetString(DCM_Modality, value);
                study->modality_ = QString::fromLatin1(value);
                if (sopClassUid == UID_XRayAngiographicImageStorage ||// 造影血管
                        true) {
                    OFCondition cond =
                        ff->saveFile(
                            QString("./ScpCache/tmp.dcm").toLocal8Bit().data(),
                            dset->getOriginalXfer(),
                            EET_ExplicitLength, EGL_recalcGL,
                            EPD_withoutPadding, 0, 0, EWM_fileformat);
                    if (cond.bad()) {
                        qDebug() << QString("无法写入DICOM文件: %1.").arg(cond.text());
                    } else {
                        ImageRecord *image = new ImageRecord(instUid);
                        image->sop_class_uid_ = sopClassUid;
                        image->series_uid_ = seriesUid;
                        image->study_uid_ = studyUid;
                        image->image_file_ = QString("./ScpCache/tmp.dcm");
                        study->image_list_.append(image);
                        dset->findAndGetString(DCM_ReferencedSOPInstanceUID, value, true);
                        image->ref_image_uid_ = QString::fromLatin1(value);
                        dset->findAndGetString(DCM_InstanceNumber, value);
                        image->image_number_ = QString::fromLatin1(value);
                        dset->findAndGetString(DCM_SeriesDescription, value);
                        image->image_desc_ = QString::fromLocal8Bit(value);
                        dset->findAndGetString(DCM_ContentDate, value);
                        image->image_yime_.setDate(
                            QDate::fromString(QString::fromLatin1(value), "yyyyMMdd"));
                        dset->findAndGetString(DCM_ContentTime, value);
                        image->image_yime_.setTime(formatDicomTime(QString::fromLatin1(value)));
                    }
                }
            }
        }
        //
        StudyDao dao;
        int images = 0;
        QString study_dir_name =
            QString("%1/%2_%3").arg(study->study_time_.date().toString("yyyyMM"),
                                    study->study_time_.toString(DICOM_DATETIME_FORMAT),
                                    study->acc_number_);
        if(!dao.VerifyStudyByStuid(study->study_uid_)) {
            dao.InsertStudyToDb(*study, true);
        }
        FileUtil::DirMake(QString("%1/%2").arg(DICOM_SAVE_PATH, study_dir_name));
        foreach (ImageRecord *image, study->image_list_) {
            bool raw = image->sop_class_uid_ == QString(UID_XRayAngiographicImageStorage);
            QString src_file = image->image_file_;
            image->image_file_ = QString("%1/%2_%3.dcm").arg(study_dir_name,
                                 raw ? "angio" : "", image->image_uid_);
            QFileInfo info(QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_));
            if (FileUtil::FileCopy(src_file, QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_))) {
                if (!dao.VerifyImageByIMmuid(image->image_uid_)) {
                    if (dao.InsertImageToDb(*image, true)) {
                        images++;
                    } else {
                    }
                } else {
                    if (dao.UpdateImageFile(image->image_uid_, image->image_file_)) {
                        images++;
                    } else {
                        FileUtil::DeleteFileOrFolder(
                            QString("%1/%2").arg(DICOM_SAVE_PATH, image->image_file_));
                    }
                }
            }
            image->image_file_ = src_file;
        }
    }
}

4 DicomImage封装

尽量保证不出现内存泄露,先使用 QSharedData 和 **QExplicitlySharedDataPointer**等都开发完成一起检查。

4.1 简单了解DICOM 协议

需要先简单了解一下 DICOM 协议

DICOM 协议中 Patient Root 查询/检索信息模型建立在一个四等级的分层基础之上

  • PATIENT (病人)
  • STUDY (检查)
  • SERIES (序列)
  • IMAGE (影像)

├── PATIENT (病人)
│ │ └── STUDY (检查)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像)
│ │ │ │ │ │ └── IMAGE (影像)
│ │ └── STUDY (检查)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像)

ps: **Study Root**查询/检索信息模型与**Patient Root**查询/检索信息模型是相同的除了它的最高等级是检查等级之外。

还有一个比较重要

  • SOP (**DICOM**应用都提供了哪些服务)

简单概括就是,每一位病人可以做多次检查,每次检查有多个序列,每个序列有多张影像,每张影像可以有多层或者一层。
每个影像可以提供不同的**dicom**服务。


4.2 DicomImage 常用功能

4.2.1 DicomImage 转 QPixmap

static bool GetPixmap(const QString &dicomFile, QPixmap &pixmap);
static bool Dcm2BmpHelper(DicomImage &dcm_image_, QPixmap &pixmap, const qint32 frame = 0);

//---------------------------------------------------------------
void FreeBuffer(void *pBuf) {
    delete pBuf;
}

//---------------------------------------------------------------
bool ImageInstanceData::GetPixmap(const QString &dicomFile, QPixmap &pixmap) {
    ImageInstanceData image(dicomFile);
    return image.GetPixmap(pixmap);
}

//---------------------------------------------------------------
bool ImageInstanceData::Dcm2BmpHelper(
    DicomImage &dcmImage, QPixmap &pixmap, const qint32 frame) {
    qint32 w = static_cast<qint32>(dcmImage.getWidth());
    qint32 h = static_cast<qint32>(dcmImage.getHeight());
    void *pDIB = nullptr;
    qint32 size;
    if(dcmImage.getFrameCount() > 1) {
        quint64 tmp = static_cast<quint64>(frame);
        size = static_cast<qint32>(dcmImage.createWindowsDIB(pDIB, 0, tmp, 32, 0, 1));
    } else {
        size = static_cast<qint32>(dcmImage.createWindowsDIB(pDIB, 0, 0, 32, 0, 1));
    }
    if (size == w * h * 4) {
        QImage image( static_cast<uchar *>(pDIB), w, h,
                      QImage::Format_RGB32, FreeBuffer, pDIB);
        pixmap = QPixmap::fromImage(image);
        return !pixmap.isNull();
    }
    return false;
}

4.2.2 DicomImage 剪裁

//---------------------------------------------------------------
DicomImage *ImageInstanceData::CreateClippedImage(
    const QRect &rect, int angle, bool hflip, bool vflip, bool inverted) {
    DicomImage *image = dcm_image_;
    if (!image) {
        return image;
    }
    int ret = 1;
    double min, max;
    image->getMinMaxValues(min, max);
    double pvalue = image->getPhotometricInterpretation() ==
                    EPI_Monochrome1 ? max : min;
    DicomImage *newImage =
        image->createClippedImage(
            static_cast<long>( rect.left()),
            static_cast<long>( rect.top()),
            static_cast<unsigned long>( rect.width()),
            static_cast<unsigned long>( rect.height()),
            static_cast<unsigned short>( pvalue));
    if (newImage) {
        if (ret && angle) {
            ret = newImage->rotateImage(angle % 360);
        }
        if (ret && hflip) {
            ret = newImage->flipImage(1, 0);
        }
        if (ret && vflip) {
            ret = newImage->flipImage(0, 1);
        }
        if (ret && inverted) {
            ret = newImage->setPolarity(EPP_Reverse);
        }
        if (!ret) {
            delete newImage;
            newImage = nullptr;
        }
    }
    return newImage;
}

4.2.3 DcmFileFormat 获取 标签信息

//---------------------------------------------------------------
QString ImageInstanceData::GetTagKeyValue(const DcmTagKey &key) const {
    OFString val;
    if (dcmff_ && dcmff_->getDataset()) {
        dcmff_->getDataset()->findAndGetOFString(key, val);
    }
    return QString::fromLocal8Bit(val.c_str());
}

4.2.4 DcmFileFormat 获取 常用标签/图片

//---------------------------------------------------------------
void ImageInstanceData::InitImage() {
    DJDecoderRegistration::registerCodecs();
    DcmDataset *dset;
    OFCondition result;
    if (dcmff_ && (dset = dcmff_->getDataset())) {
        dcmff_->loadAllDataIntoMemory();
        dset->chooseRepresentation(EXS_LittleEndianExplicit, nullptr);
        const char *val = nullptr;
        result = dset->findAndGetString(DCM_StudyInstanceUID, val);
        study_uid_ = QString::fromLocal8Bit(val);
        result = dset->findAndGetString(DCM_SeriesInstanceUID, val);
        series_uid_ = QString::fromLocal8Bit(val);
        result = dset->findAndGetString(DCM_SOPInstanceUID, val);
        image_uid_ = QString::fromLocal8Bit(val);
        result = dset->findAndGetString(DCM_SOPClassUID, val);
        class_uid_ = QString::fromLocal8Bit(val);
        result = dset->findAndGetFloat64(DCM_PixelSpacing, pixel_y_, 0);
        result = dset->findAndGetFloat64(DCM_PixelSpacing, pixel_x_, 1);
        result = dset->findAndGetFloat64(DCM_WindowWidth, win_width_);
        result = dset->findAndGetFloat64(DCM_WindowCenter, win_center_);
        def_center_ = win_center_;
        def_width_ = win_width_;
        dcm_image_ = new DicomImage(dset, dset->getOriginalXfer());
        if (dcm_image_->getStatus() == EIS_Normal) {
            if (win_width_ < 1) {
                dcm_image_->setRoiWindow(0, 0, dcm_image_->getWidth(), dcm_image_->getHeight());
                dcm_image_->getWindow(win_center_, win_width_);
                def_center_ = win_center_;
                def_width_ = win_width_;
            }
        } else {
            delete dcm_image_;
            dcm_image_ = nullptr;
        }
    }
}

4.2.5 DicomImage 获取像素信息

//---------------------------------------------------------------
double ImageInstanceData::GetPixelValue(long x, long y) const {
    DicomImage *image = dcm_image_;
    if (image) {
        const DiPixel *pixel = image->getInterData();
        if (pixel && (x < static_cast<long>(image->getWidth())) && (x >= 0)
                && (y < static_cast<long>(image->getHeight())) && (y >= 0)) {
            EP_Representation r = pixel->getRepresentation();
            switch (r) {
                case EPR_Sint8:
                    return *((char *)(pixel->getData()) +
                             (y * image->getWidth() + x));
                case EPR_Uint8:
                    return *((uchar *)(pixel->getData()) +
                             (y * image->getWidth() + x));
                case EPR_Sint16:
                    return *((short *)(pixel->getData()) +
                             (y * image->getWidth() + x));
                case EPR_Uint16:
                    return *((ushort *)(pixel->getData()) +
                             (y * image->getWidth() + x));
                case EPR_Sint32:
                    return *((int *)(pixel->getData()) +
                             (y * image->getWidth() + x));
                case EPR_Uint32:
                    return *((uint *)(pixel->getData()) +
                             (y * image->getWidth() + x));
            }
        }
    }
    return 0;
}

4.3 ImageInstanceData 封装

我这里把每张影像(dcmtk**库中是**DicomImage)封装成一个**QSharedData**。预留接口方便Qt框架显示、调用。

注意:每张影像尺寸是三维的 比如
- 512 X 512 X 1 表示这个影像只有一帧
- 512 X 512 X 250 表示这个影像有很多帧

自定义的**QSharedData**影像包含数据:

    // Patient uid 不需要,最后显示按照 STUDY 分类
    QString study_uid_;// STUDY uid
    QString series_uid_;// SERIES uid
    QString image_uid_;// IMAGE uid
    QString class_uid_;// SOP uid
    double pixel_x_;// x方向间距
    double pixel_y_;// y方向间距
    double def_center_;// 窗位窗宽
    double def_width_;// 窗位窗宽
    double win_width_;// 窗位窗宽
    double win_center_;// 窗位窗宽
    DcmFileFormat *dcmff_;// dcm 文件
    DicomImage *dcm_image_;// 图片
    QString image_file_;// 文件名   
class DcmTagKey;
class DicomImage;
class DcmFileFormat;

class ImageInstanceData: public QSharedData {
  public:
    static bool GetPixmap(const QString &dicomFile, QPixmap &pixmap);
    static bool Dcm2BmpHelper(DicomImage &dcm_image_, QPixmap &pixmap, const qint32 frame = 0);
  public:
    explicit ImageInstanceData(const QString &file);
    explicit ImageInstanceData(DcmFileFormat *dff);
    ~ImageInstanceData();

    void SetWindow(const double &center, const double &width);
    void GetWindow(double &center, double &width) const;
    void SetWindowDelta(const double &dCenter, const double &dWidth);
    void SetRoiWindow(const QRectF &rect);
    void SetFullDynamic();
    void SetDefaultWindow();
    QString GetStudyUid() const;
    QString GetSeriesUid() const;
    QString GetImageUid() const;
    QString GetClassUid() const;
    QString GetImageFile() const;
    void SetPolarity(EP_Polarity polarity);
    EP_Polarity GetPolarity() const;
    bool GetPixmap(QPixmap &pixmap);
    bool GetPixmap(QPixmap &pixmap, const qint32 &frame);
    bool IsNormal() const;
    DicomImage *CreateClippedImage(const QRect &rect, int angle = 0,
                                   bool hflip = false, bool vflip = false,
                                   bool inverted = false);
    QString GetTagKeyValue(const DcmTagKey &key) const;
    double GetPixelValue(long x, long y) const;
    bool GetPixSpacing(double &spacingX, double &spacingY) const;
    bool GetImageSize(ulong &width, ulong &height) const;
    const short *GetInternalPtr() const;
    const ushort *GetRawData() const;
    const DicomImage *GetDcmImage() const;
    DcmFileFormat *GetFileFormat() const;
    bool SaveFileFormat();
    qint32 GetFrameCount() const;
  private:
    void InitImage();
  private:
    QString study_uid_;
    QString series_uid_;
    QString image_uid_;
    QString class_uid_;
    double pixel_x_;
    double pixel_y_;
    double def_center_;
    double def_width_;
    double win_width_;
    double win_center_;
    DcmFileFormat *dcmff_;
    DicomImage *dcm_image_;
    QString image_file_;
};

4.4 ImageInstance 封装

如果直接使用 QSharedData 后面会很麻烦。需要再搞一个**Instance**作为程序里传递使用。利用**QExplicitlySharedDataPointer**实现显式共享。

class DicomImage;

class ImageInstance {
  public:
    ImageInstance(const QString &file);
    ImageInstance(DcmFileFormat *dff);
    ImageInstance(const ImageInstance &image);
    void SetWindow(const double &center, const double &width);
    void GetWindow(double &center, double &width) const;
    void SetWindowDelta(const double &dCenter, const double &dWidth);
    void SetRoiWindow(const QRectF &rect);
    void SetDefaultWindow();
    void SetFullDynamic();
    void GetPolarity(EP_Polarity p);
    EP_Polarity GetPolarity() const;
    QString GetStudyUid() const;
    QString GetSeriesUid() const;
    QString GetImageUid() const;
    QString GetClassUid() const;
    QString GetImageFile() const;
    bool GetPixmap(QPixmap &pixmap);
    bool GetPixmap(QPixmap &pixmap, const qint32 &frame);
    bool IsNormal() const;
    DicomImage *CreateClippedImage(
        const QRect &rect, int angle = 0, bool hflip = false,
        bool vflip = false, bool inverted = false);
    QString GetTagKeyValue(const DcmTagKey &key) const;
    uint GetPixelValue(long x, long y) const;
    bool GetPixSpacing(double &spacingX, double &spacingY) const;
    bool GetImageSize(ulong &width, ulong &height) const;
    const short *GetInternalPtr() const;
    const ushort *GetRawData() const;
    const DicomImage *GetDcmImage() const;
    DcmFileFormat *GetFileFormat();
    bool SaveFileFormat();
    qint32 GetFrameCount() const;
    //
    static bool GetPixmap(const QString &file, QPixmap &pixmap);
    static bool Dcm2BmpHelper(DicomImage &image, QPixmap &pixmap,
                              const qint32 frame = 0);
  private:
    QExplicitlySharedDataPointer<ImageInstanceData> d_;
};

Q_DECLARE_TYPEINFO(ImageInstance, Q_MOVABLE_TYPE);

5 序列封装

5.1 简单了解DICOM 协议

提到了Dicom影像的检索模型。同一个系列可能有多张dicom影像(比如CT),我们做可视化的时候肯定需要把一系列按照高度一起显示。

├── PATIENT (病人)
│ │ └── STUDY (检查)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像 高度为未知)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像 高度为1)
│ │ │ │ │ │ └── IMAGE (影像 高度为1)
│ │ └── STUDY (检查)
│ │ │ │ └── SERIES (序列)
│ │ │ │ │ │ └── IMAGE (影像)

DICOM 标签中 InstanceNumber**就表示 **IMAGE (影像)在**SERIES** (序列)中的位置。我们需要按照 **InstanceNumber**把**DicomImage**拼接起来当成一个整体。

ps: 一个**STUDY**可能有多个**SERIES**,可视化时候按照**SERIES**分。


5.2 DicomImage Series 类型

根据协议可以知道每个**Series**中可以有单帧或多帧。
多帧时 每帧影像尺寸高度是1
单帧时 每帧影像尺寸高度未知

  • 单帧模式高度代表当前时间 !
  • 多帧模式高度代表空间位置 !

    enum  SeriesPattern {
        Empty_Frame,  //
        Single_Frame, // 单帧
        Multi_Frame,  // 多帧
    };

涉及到Series,2D可视化肯定会有方向

(多帧模式下)区分平面
(单帧模式下)只有XY平面显示模式,另外两个平面表示**时间密度曲线**,与其相关打算作为插件用opencv做,所以这里封装的Series其余两个平面均指多帧模式。

    enum ViewType {
    VT_XYPlane,
    VT_XZPlane,
    VT_YZPlane,
    };


5.3 DicomImage Series 常用功能

5.3.1 DicomImage Series 上一帧、下一帧、当前帧、层高

显示 XY 平面时:

  • 单帧时**Series**层高是这帧影像层高。
  • 多帧时**Series**层高是帧数。

同理显示 XZ、YZ 平面是:帧数就是 影像 y轴尺寸、x轴尺寸。

5.3.2 DicomImage Series 获取像素

(多帧模式下)区分平面
(单帧模式下)只有XY平面显示模式

double SeriesInstance::GetPixelValue(long x, long y, ViewType type) const {
    if (!image_map_.isEmpty()) {
        switch (type) {
            case VT_XYPlane:
                ImageInstance *image;
                switch (m_pattern_) {
                    case Single_Frame: {
                            image = image_map_.values().at(0);
                            break;
                        }
                    case Multi_Frame: {
                            image = image_map_.values().at(cur_xy_frame_);
                            break;
                        }
                    default: {
                            return 0;
                        }
                }
                return image->GetPixelValue(x, y);
            case VT_XZPlane:
                if (y >= 0 && y < image_map_.values().size()) {
                    return image_map_.values().at(y)->GetPixelValue(x, cur_xz_frame_);
                }
                break;
            case VT_YZPlane:
                if (y >= 0 && y < image_map_.values().size()) {
                    return image_map_.values().at(y)->GetPixelValue(cur_yz_frame_, x);
                }
                break;
        }
    }
    return 0;
}

5.3.3 DicomImage Series 获取间隔

(多帧模式下)区分平面
(单帧模式下)只有XY平面显示模式

bool SeriesInstance::GetPixSpacing(double &spacingX, double &spacingY, ViewType type) const {
    double sx, sy, sz;
    if (image_map_.isEmpty()) {
        return false;
    }
    if (!image_map_.first()->GetPixSpacing(sx, sy)) {
        return false;
    }
    sz = image_map_.first()->GetTagKeyValue(DCM_SliceThickness).toDouble();
    switch (type) {
        case VT_XYPlane:
            spacingX = sx;
            spacingY = sy;
            break;
        case VT_XZPlane:
            if (sz <= 0) {
                return false;
            }
            spacingX = sx;
            spacingY = sz;
            break;
        case VT_YZPlane:
            if (sz <= 0) {
                return false;
            }
            spacingX = sy;
            spacingY = sz;
            break;
    }
    return true;
}

5.3.4 DicomImage Series 获取图片

(多帧模式下)区分平面
(单帧模式下)只有XY平面显示模式

bool SeriesInstance::GetPixmap(QPixmap &pixmap, ViewType type) {
    if (image_map_.isEmpty()) {
        return false;
    }
    ImageInstance *image;
    const short **volume;
    ulong w, h, s, rh;
    switch (type) {
        case VT_XYPlane:
            switch (m_pattern_) {
                case Single_Frame: {
                        image = image_map_.values().at(0);
                        break;
                    }
                case Multi_Frame: {
                        image = image_map_.values().at(cur_xy_frame_);
                        break;
                    }
                default: {
                        return false;
                    }
            }
            if (win_width_ < 1) {
                win_width_ = 1;
            }
            image->SetWindow(win_center_, win_width_);
            image->GetPolarity(m_pola_);
            return image->GetPixmap(pixmap, cur_xy_frame_);
        case VT_XZPlane:
            if (GetSeriesVolume(volume, w, h, s)) {
                double center = win_center_;
                double width = win_width_;
                double factor = 255 / width;
                double lower = center - width / 2;
                QImage srcImage(w, s, QImage::Format_Indexed8);
                QVector<QRgb> grayTable;
                for(int i = 0; i < 256; i++) {
                    grayTable.push_back(qRgb(i, i, i));
                }
                srcImage.setColorTable(grayTable);
                for (int i = 0; i < s; i++) {
                    const short *ptr = volume[i];
                    int idx = cur_xz_frame_ * h;
                    for (int j = 0; j < w; j++) {
                        short val = ptr[j * w + cur_xz_frame_];
                        if (val > lower + width) {
                            srcImage.setPixel(j, i, 255);
                        } else if (val > lower) {
                            qint32 value = (val - lower) * factor;
                            srcImage.setPixel(j, i, value);
                        } else {
                            srcImage.setPixel(j, i, 0);
                        }
                    }
                }
                pixmap = QPixmap::fromImage(srcImage);
                return true;
            }
            break;
        case VT_YZPlane:
            if (GetSeriesVolume(volume, w, h, s)) {
                double center = win_center_;
                double width = win_width_;
                double factor = 255 / width;
                double lower = center - width / 2;
                QImage srcImage(w, s, QImage::Format_Indexed8);
                QVector<QRgb> grayTable;
                for(int i = 0; i < 256; i++) {
                    grayTable.push_back(qRgb(i, i, i));
                }
                srcImage.setColorTable(grayTable);
                for (int i = 0; i < s; i++) {
                    const short *ptr = volume[i];
                    int idx = cur_yz_frame_ * h;
                    for (int j = 0; j < w; j++) {
                        short val = ptr[idx + j];
                        if (val > lower + width) {
                            srcImage.setPixel(j, i, 255);
                        } else if (val > lower) {
                            qint32 value = (val - lower) * factor;
                            srcImage.setPixel(j, i, value);
                        } else {
                            srcImage.setPixel(j, i, 0);
                        }
                    }
                }
                pixmap = QPixmap::fromImage(srcImage);
                return true;
            }
            break;
    }
    return false;
}

5.4 DicomImage Series 封装

大部分跟**ImageInstance**一样。

#ifndef SERIESINSTANCE_H
#define SERIESINSTANCE_H

#include <QObject>
#include <QMap>
#include "dcmtk/dcmimgle/diutils.h"
#include "../Global/structs.h"

class DicomImage;
class ImageInstance;
class DcmTagKey;

class SeriesInstance: public QObject {
    Q_OBJECT
  public:
    enum  SeriesPattern {
        Empty_Frame,  //
        Single_Frame, // 单帧
        Multi_Frame,  // 多帧
    };

  public:
    explicit SeriesInstance(const QString &seriesUID,
                            QObject *parent = nullptr);
    ~SeriesInstance();
    bool InsertImage(ImageInstance *image);
    bool RemoveImage(const QString &imgFile);
    bool IsEmpty() const;
    bool HasImage(const QString &file);
    QString GetTagKeyValue(const DcmTagKey &key, const ViewType &type = VT_XYPlane) const;
    qint32 GetFrameCount(ViewType type = VT_XYPlane) const;
    const short **GetSeriesVolume(const short** &volume,
                                  ulong &width, ulong &height, ulong &slice);
    const ushort **GetRawVolume(const ushort** &volume,
                                ulong &width, ulong &height, ulong &slice);
    ImageInstance *GetCurrImageInstance(ViewType type) const;
    bool GetPixmap(QPixmap &pixmap, ViewType type);
    void NextFrame(ViewType type);
    void PrevFrame(ViewType type);
    void GotoFrame(int index, ViewType type);
    int GetCurIndex(ViewType type);
    void SetWindow(const double &center, const double &width);
    void GetWindow(double &center, double &width) const;
    void SetWindowDelta(const double &dCenter, const double &dWidth);
    void SetRoiWindow(const QRectF &rect);
    void SetDefaultWindow();
    void SetFullDynamic();

    void SetPolarity(EP_Polarity polarity);
    EP_Polarity GetPolarity() const;
    double GetPixelValue(long x, long y, ViewType type) const;
    bool GetPixSpacing(double &spacingX, double &spacingY, ViewType type) const;
    void DelVolBuffer();

  Q_SIGNALS:
    void Signal_AboutToDelete();

  private:
    SeriesInstance(const SeriesInstance &);
    SeriesInstance &operator= (const SeriesInstance &);

  private:
    QString series_uid_;
    int cur_xy_frame_;
    int cur_xz_frame_;
    int cur_yz_frame_;
    ulong img_width_;
    ulong img_height_;
    double win_center_;
    double win_width_;
    double def_center_;
    double def_width_;
    EP_Polarity m_pola_;
    const short **vol_ptr_;
    ulong vol_slice_;
    const ushort **raw_ptr_;
    ulong raw_slice_;
    QMap<int, ImageInstance *> image_map_;
    SeriesPattern m_pattern_;
};

#endif // SERIESINSTANCE_H