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(¶ms, 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 ¢er, const double &width);
void GetWindow(double ¢er, 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 ¢er, const double &width);
void GetWindow(double ¢er, 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
单帧时 每帧影像尺寸高度未知
- 单帧模式高度代表当前时间 !
- 多帧模式高度代表空间位置 !
涉及到Series,2D可视化肯定会有方向
(多帧模式下)区分平面
(单帧模式下)只有XY平面显示模式,另外两个平面表示**时间密度曲线**,与其相关打算作为插件用opencv做,所以这里封装的Series其余两个平面均指多帧模式。
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 ¢er, const double &width);
void GetWindow(double ¢er, 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