医学图像应用程序往往需要有效地搜索和查询DICOM的代数据,而DicOM文件存储富有的代码,将其提取到数据库,可以快速查找,分析和与医院信息系统的整合。
为什么 DICOM 存储数据在数据库中?
DICOM 文件包含大量的代数据,但搜索数千个文件是缓慢的。
- 快速查询:按患者、日期、模式或任何属性立即查找研究
- 分析:报告和统计数据集成
- 集成:将图像数据连接到EMR/EHR系统
- Scalability:以适当的索引处理数百万项研究
- 全文本搜索:用描述关键词找到研究
DICOM Metadata Extraction 基金会
首先,创建一个将 DICOM 转换为数据库准备的对象的数据提取器:
using Aspose.Medical.Dicom;
public class DicomMetadataExtractor
{
public DicomStudyRecord ExtractStudyRecord(string filePath)
{
DicomFile dicomFile = DicomFile.Open(filePath);
var dataset = dicomFile.Dataset;
return new DicomStudyRecord
{
// Identifiers
StudyInstanceUID = dataset.GetString(DicomTag.StudyInstanceUID),
SeriesInstanceUID = dataset.GetString(DicomTag.SeriesInstanceUID),
SOPInstanceUID = dataset.GetString(DicomTag.SOPInstanceUID),
AccessionNumber = dataset.GetString(DicomTag.AccessionNumber),
// Patient
PatientID = dataset.GetString(DicomTag.PatientID),
PatientName = dataset.GetString(DicomTag.PatientName),
PatientBirthDate = ParseDicomDate(dataset.GetString(DicomTag.PatientBirthDate)),
PatientSex = dataset.GetString(DicomTag.PatientSex),
// Study
StudyDate = ParseDicomDate(dataset.GetString(DicomTag.StudyDate)),
StudyTime = dataset.GetString(DicomTag.StudyTime),
StudyDescription = dataset.GetString(DicomTag.StudyDescription),
ReferringPhysicianName = dataset.GetString(DicomTag.ReferringPhysicianName),
// Series
Modality = dataset.GetString(DicomTag.Modality),
SeriesDescription = dataset.GetString(DicomTag.SeriesDescription),
SeriesNumber = ParseInt(dataset.GetString(DicomTag.SeriesNumber)),
BodyPartExamined = dataset.GetString(DicomTag.BodyPartExamined),
// Image
InstanceNumber = ParseInt(dataset.GetString(DicomTag.InstanceNumber)),
Rows = ParseInt(dataset.GetString(DicomTag.Rows)),
Columns = ParseInt(dataset.GetString(DicomTag.Columns)),
// Equipment
Manufacturer = dataset.GetString(DicomTag.Manufacturer),
InstitutionName = dataset.GetString(DicomTag.InstitutionName),
StationName = dataset.GetString(DicomTag.StationName),
// File Info
FilePath = filePath,
FileSize = new FileInfo(filePath).Length,
IndexedAt = DateTime.UtcNow
};
}
private DateTime? ParseDicomDate(string dicomDate)
{
if (string.IsNullOrEmpty(dicomDate) || dicomDate.Length != 8)
return null;
if (DateTime.TryParseExact(dicomDate, "yyyyMMdd", null,
System.Globalization.DateTimeStyles.None, out var date))
{
return date;
}
return null;
}
private int? ParseInt(string value)
{
if (int.TryParse(value, out var result))
return result;
return null;
}
}
public class DicomStudyRecord
{
public string StudyInstanceUID { get; set; }
public string SeriesInstanceUID { get; set; }
public string SOPInstanceUID { get; set; }
public string AccessionNumber { get; set; }
public string PatientID { get; set; }
public string PatientName { get; set; }
public DateTime? PatientBirthDate { get; set; }
public string PatientSex { get; set; }
public DateTime? StudyDate { get; set; }
public string StudyTime { get; set; }
public string StudyDescription { get; set; }
public string ReferringPhysicianName { get; set; }
public string Modality { get; set; }
public string SeriesDescription { get; set; }
public int? SeriesNumber { get; set; }
public string BodyPartExamined { get; set; }
public int? InstanceNumber { get; set; }
public int? Rows { get; set; }
public int? Columns { get; set; }
public string Manufacturer { get; set; }
public string InstitutionName { get; set; }
public string StationName { get; set; }
public string FilePath { get; set; }
public long FileSize { get; set; }
public DateTime IndexedAt { get; set; }
}
SQL Server 集成
存储在 SQL Server 中使用 Entity Framework Core 的 DICOM 数据:
using Microsoft.EntityFrameworkCore;
public class DicomDbContext : DbContext
{
public DbSet<DicomStudyEntity> Studies { get; set; }
public DbSet<DicomSeriesEntity> Series { get; set; }
public DbSet<DicomInstanceEntity> Instances { get; set; }
public DicomDbContext(DbContextOptions<DicomDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Study entity
modelBuilder.Entity<DicomStudyEntity>(entity =>
{
entity.HasKey(e => e.StudyInstanceUID);
entity.HasIndex(e => e.PatientID);
entity.HasIndex(e => e.StudyDate);
entity.HasIndex(e => e.AccessionNumber);
entity.HasIndex(e => e.Modality);
});
// Series entity
modelBuilder.Entity<DicomSeriesEntity>(entity =>
{
entity.HasKey(e => e.SeriesInstanceUID);
entity.HasIndex(e => e.StudyInstanceUID);
entity.HasOne(e => e.Study)
.WithMany(s => s.SeriesList)
.HasForeignKey(e => e.StudyInstanceUID);
});
// Instance entity
modelBuilder.Entity<DicomInstanceEntity>(entity =>
{
entity.HasKey(e => e.SOPInstanceUID);
entity.HasIndex(e => e.SeriesInstanceUID);
entity.HasOne(e => e.Series)
.WithMany(s => s.Instances)
.HasForeignKey(e => e.SeriesInstanceUID);
});
}
}
public class DicomStudyEntity
{
public string StudyInstanceUID { get; set; }
public string AccessionNumber { get; set; }
public string PatientID { get; set; }
public string PatientName { get; set; }
public DateTime? PatientBirthDate { get; set; }
public string PatientSex { get; set; }
public DateTime? StudyDate { get; set; }
public string StudyDescription { get; set; }
public string Modality { get; set; }
public string ReferringPhysicianName { get; set; }
public string InstitutionName { get; set; }
public int ImageCount { get; set; }
public DateTime IndexedAt { get; set; }
public List<DicomSeriesEntity> SeriesList { get; set; }
}
public class DicomSeriesEntity
{
public string SeriesInstanceUID { get; set; }
public string StudyInstanceUID { get; set; }
public string Modality { get; set; }
public string SeriesDescription { get; set; }
public int? SeriesNumber { get; set; }
public string BodyPartExamined { get; set; }
public int ImageCount { get; set; }
public DicomStudyEntity Study { get; set; }
public List<DicomInstanceEntity> Instances { get; set; }
}
public class DicomInstanceEntity
{
public string SOPInstanceUID { get; set; }
public string SeriesInstanceUID { get; set; }
public int? InstanceNumber { get; set; }
public int? Rows { get; set; }
public int? Columns { get; set; }
public string FilePath { get; set; }
public long FileSize { get; set; }
public DicomSeriesEntity Series { get; set; }
}
SQL Server 存储库用于 CRUD 操作:
public class SqlDicomRepository
{
private readonly DicomDbContext _context;
private readonly DicomMetadataExtractor _extractor;
public SqlDicomRepository(DicomDbContext context)
{
_context = context;
_extractor = new DicomMetadataExtractor();
}
public async Task IndexDicomFileAsync(string filePath)
{
var record = _extractor.ExtractStudyRecord(filePath);
// Upsert Study
var study = await _context.Studies.FindAsync(record.StudyInstanceUID);
if (study == null)
{
study = new DicomStudyEntity
{
StudyInstanceUID = record.StudyInstanceUID,
AccessionNumber = record.AccessionNumber,
PatientID = record.PatientID,
PatientName = record.PatientName,
PatientBirthDate = record.PatientBirthDate,
PatientSex = record.PatientSex,
StudyDate = record.StudyDate,
StudyDescription = record.StudyDescription,
Modality = record.Modality,
ReferringPhysicianName = record.ReferringPhysicianName,
InstitutionName = record.InstitutionName,
IndexedAt = DateTime.UtcNow
};
_context.Studies.Add(study);
}
// Upsert Series
var series = await _context.Series.FindAsync(record.SeriesInstanceUID);
if (series == null)
{
series = new DicomSeriesEntity
{
SeriesInstanceUID = record.SeriesInstanceUID,
StudyInstanceUID = record.StudyInstanceUID,
Modality = record.Modality,
SeriesDescription = record.SeriesDescription,
SeriesNumber = record.SeriesNumber,
BodyPartExamined = record.BodyPartExamined
};
_context.Series.Add(series);
}
// Upsert Instance
var instance = await _context.Instances.FindAsync(record.SOPInstanceUID);
if (instance == null)
{
instance = new DicomInstanceEntity
{
SOPInstanceUID = record.SOPInstanceUID,
SeriesInstanceUID = record.SeriesInstanceUID,
InstanceNumber = record.InstanceNumber,
Rows = record.Rows,
Columns = record.Columns,
FilePath = record.FilePath,
FileSize = record.FileSize
};
_context.Instances.Add(instance);
}
await _context.SaveChangesAsync();
}
public async Task<List<DicomStudyEntity>> SearchStudiesAsync(
string patientId = null,
string patientName = null,
DateTime? startDate = null,
DateTime? endDate = null,
string modality = null)
{
var query = _context.Studies.AsQueryable();
if (!string.IsNullOrEmpty(patientId))
query = query.Where(s => s.PatientID == patientId);
if (!string.IsNullOrEmpty(patientName))
query = query.Where(s => s.PatientName.Contains(patientName));
if (startDate.HasValue)
query = query.Where(s => s.StudyDate >= startDate);
if (endDate.HasValue)
query = query.Where(s => s.StudyDate <= endDate);
if (!string.IsNullOrEmpty(modality))
query = query.Where(s => s.Modality == modality);
return await query
.OrderByDescending(s => s.StudyDate)
.Take(100)
.ToListAsync();
}
}
MongoDB 集成
在 MongoDB 中将 DICOM 代码存储为 JSON 文件:
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
public class MongoDicomDocument
{
[BsonId]
public string SOPInstanceUID { get; set; }
public string StudyInstanceUID { get; set; }
public string SeriesInstanceUID { get; set; }
public string AccessionNumber { get; set; }
public PatientInfo Patient { get; set; }
public StudyInfo Study { get; set; }
public SeriesInfo Series { get; set; }
public ImageInfo Image { get; set; }
public EquipmentInfo Equipment { get; set; }
public FileInfo FileInfo { get; set; }
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime IndexedAt { get; set; }
}
public class PatientInfo
{
public string ID { get; set; }
public string Name { get; set; }
public DateTime? BirthDate { get; set; }
public string Sex { get; set; }
}
public class StudyInfo
{
public DateTime? Date { get; set; }
public string Time { get; set; }
public string Description { get; set; }
public string ReferringPhysician { get; set; }
}
public class SeriesInfo
{
public string Modality { get; set; }
public string Description { get; set; }
public int? Number { get; set; }
public string BodyPart { get; set; }
}
public class ImageInfo
{
public int? InstanceNumber { get; set; }
public int? Rows { get; set; }
public int? Columns { get; set; }
}
public class EquipmentInfo
{
public string Manufacturer { get; set; }
public string Institution { get; set; }
public string Station { get; set; }
}
public class FileInfo
{
public string Path { get; set; }
public long Size { get; set; }
}
public class MongoDicomRepository
{
private readonly IMongoCollection<MongoDicomDocument> _collection;
private readonly DicomMetadataExtractor _extractor;
public MongoDicomRepository(string connectionString, string databaseName)
{
var client = new MongoClient(connectionString);
var database = client.GetDatabase(databaseName);
_collection = database.GetCollection<MongoDicomDocument>("dicom_instances");
_extractor = new DicomMetadataExtractor();
CreateIndexes();
}
private void CreateIndexes()
{
var indexKeys = Builders<MongoDicomDocument>.IndexKeys;
_collection.Indexes.CreateMany(new[]
{
new CreateIndexModel<MongoDicomDocument>(indexKeys.Ascending(x => x.StudyInstanceUID)),
new CreateIndexModel<MongoDicomDocument>(indexKeys.Ascending(x => x.Patient.ID)),
new CreateIndexModel<MongoDicomDocument>(indexKeys.Ascending(x => x.Study.Date)),
new CreateIndexModel<MongoDicomDocument>(indexKeys.Ascending(x => x.Series.Modality)),
new CreateIndexModel<MongoDicomDocument>(
indexKeys.Text(x => x.Study.Description)
.Text(x => x.Series.Description)
.Text(x => x.Patient.Name))
});
}
public async Task IndexDicomFileAsync(string filePath)
{
var record = _extractor.ExtractStudyRecord(filePath);
var document = new MongoDicomDocument
{
SOPInstanceUID = record.SOPInstanceUID,
StudyInstanceUID = record.StudyInstanceUID,
SeriesInstanceUID = record.SeriesInstanceUID,
AccessionNumber = record.AccessionNumber,
Patient = new PatientInfo
{
ID = record.PatientID,
Name = record.PatientName,
BirthDate = record.PatientBirthDate,
Sex = record.PatientSex
},
Study = new StudyInfo
{
Date = record.StudyDate,
Time = record.StudyTime,
Description = record.StudyDescription,
ReferringPhysician = record.ReferringPhysicianName
},
Series = new SeriesInfo
{
Modality = record.Modality,
Description = record.SeriesDescription,
Number = record.SeriesNumber,
BodyPart = record.BodyPartExamined
},
Image = new ImageInfo
{
InstanceNumber = record.InstanceNumber,
Rows = record.Rows,
Columns = record.Columns
},
Equipment = new EquipmentInfo
{
Manufacturer = record.Manufacturer,
Institution = record.InstitutionName,
Station = record.StationName
},
FileInfo = new FileInfo
{
Path = record.FilePath,
Size = record.FileSize
},
IndexedAt = DateTime.UtcNow
};
await _collection.ReplaceOneAsync(
x => x.SOPInstanceUID == document.SOPInstanceUID,
document,
new ReplaceOptions { IsUpsert = true });
}
public async Task<List<MongoDicomDocument>> FullTextSearchAsync(string searchText)
{
var filter = Builders<MongoDicomDocument>.Filter.Text(searchText);
return await _collection.Find(filter).Limit(100).ToListAsync();
}
public async Task<List<BsonDocument>> GetStudyAggregationAsync()
{
var pipeline = new[]
{
new BsonDocument("$group", new BsonDocument
{
{ "_id", "$StudyInstanceUID" },
{ "patientId", new BsonDocument("$first", "$Patient.ID") },
{ "patientName", new BsonDocument("$first", "$Patient.Name") },
{ "studyDate", new BsonDocument("$first", "$Study.Date") },
{ "modality", new BsonDocument("$first", "$Series.Modality") },
{ "description", new BsonDocument("$first", "$Study.Description") },
{ "imageCount", new BsonDocument("$sum", 1) }
}),
new BsonDocument("$sort", new BsonDocument("studyDate", -1)),
new BsonDocument("$limit", 100)
};
return await _collection.Aggregate<BsonDocument>(pipeline).ToListAsync();
}
}
Elasticsearch 集成
通过 Elasticsearch 启用强大的完整文本搜索:
using Elasticsearch.Net;
using Nest;
[ElasticsearchType(IdProperty = nameof(SOPInstanceUID))]
public class ElasticDicomDocument
{
public string SOPInstanceUID { get; set; }
public string StudyInstanceUID { get; set; }
public string SeriesInstanceUID { get; set; }
public string AccessionNumber { get; set; }
[Text(Analyzer = "standard")]
public string PatientID { get; set; }
[Text(Analyzer = "standard")]
public string PatientName { get; set; }
public DateTime? PatientBirthDate { get; set; }
[Keyword]
public string PatientSex { get; set; }
public DateTime? StudyDate { get; set; }
[Text(Analyzer = "english")]
public string StudyDescription { get; set; }
[Keyword]
public string Modality { get; set; }
[Text(Analyzer = "english")]
public string SeriesDescription { get; set; }
[Keyword]
public string BodyPartExamined { get; set; }
[Text(Analyzer = "standard")]
public string InstitutionName { get; set; }
public string FilePath { get; set; }
public long FileSize { get; set; }
public DateTime IndexedAt { get; set; }
}
public class ElasticDicomRepository
{
private readonly ElasticClient _client;
private readonly DicomMetadataExtractor _extractor;
private const string IndexName = "dicom-metadata";
public ElasticDicomRepository(string elasticsearchUrl)
{
var settings = new ConnectionSettings(new Uri(elasticsearchUrl))
.DefaultIndex(IndexName);
_client = new ElasticClient(settings);
_extractor = new DicomMetadataExtractor();
CreateIndex();
}
private void CreateIndex()
{
var existsResponse = _client.Indices.Exists(IndexName);
if (!existsResponse.Exists)
{
_client.Indices.Create(IndexName, c => c
.Map<ElasticDicomDocument>(m => m.AutoMap())
.Settings(s => s
.NumberOfShards(1)
.NumberOfReplicas(0)));
}
}
public async Task IndexDicomFileAsync(string filePath)
{
var record = _extractor.ExtractStudyRecord(filePath);
var document = new ElasticDicomDocument
{
SOPInstanceUID = record.SOPInstanceUID,
StudyInstanceUID = record.StudyInstanceUID,
SeriesInstanceUID = record.SeriesInstanceUID,
AccessionNumber = record.AccessionNumber,
PatientID = record.PatientID,
PatientName = record.PatientName,
PatientBirthDate = record.PatientBirthDate,
PatientSex = record.PatientSex,
StudyDate = record.StudyDate,
StudyDescription = record.StudyDescription,
Modality = record.Modality,
SeriesDescription = record.SeriesDescription,
BodyPartExamined = record.BodyPartExamined,
InstitutionName = record.InstitutionName,
FilePath = record.FilePath,
FileSize = record.FileSize,
IndexedAt = DateTime.UtcNow
};
await _client.IndexDocumentAsync(document);
}
public async Task<List<ElasticDicomDocument>> SearchAsync(string query)
{
var response = await _client.SearchAsync<ElasticDicomDocument>(s => s
.Query(q => q
.MultiMatch(mm => mm
.Query(query)
.Fields(f => f
.Field(d => d.PatientName, 2.0)
.Field(d => d.PatientID, 2.0)
.Field(d => d.StudyDescription)
.Field(d => d.SeriesDescription)
.Field(d => d.BodyPartExamined))
.Fuzziness(Fuzziness.Auto)))
.Size(100));
return response.Documents.ToList();
}
public async Task<List<ElasticDicomDocument>> AdvancedSearchAsync(
string patientName = null,
string modality = null,
DateTime? startDate = null,
DateTime? endDate = null,
string bodyPart = null)
{
var response = await _client.SearchAsync<ElasticDicomDocument>(s => s
.Query(q => q
.Bool(b =>
{
var must = new List<Func<QueryContainerDescriptor<ElasticDicomDocument>, QueryContainer>>();
if (!string.IsNullOrEmpty(patientName))
must.Add(m => m.Match(mt => mt.Field(f => f.PatientName).Query(patientName)));
if (!string.IsNullOrEmpty(modality))
must.Add(m => m.Term(t => t.Field(f => f.Modality).Value(modality)));
if (startDate.HasValue || endDate.HasValue)
{
must.Add(m => m.DateRange(dr =>
{
dr = dr.Field(f => f.StudyDate);
if (startDate.HasValue) dr = dr.GreaterThanOrEquals(startDate.Value);
if (endDate.HasValue) dr = dr.LessThanOrEquals(endDate.Value);
return dr;
}));
}
if (!string.IsNullOrEmpty(bodyPart))
must.Add(m => m.Term(t => t.Field(f => f.BodyPartExamined).Value(bodyPart)));
return b.Must(must.ToArray());
}))
.Sort(sort => sort.Descending(d => d.StudyDate))
.Size(100));
return response.Documents.ToList();
}
public async Task<Dictionary<string, long>> GetModalityStatisticsAsync()
{
var response = await _client.SearchAsync<ElasticDicomDocument>(s => s
.Size(0)
.Aggregations(a => a
.Terms("modalities", t => t
.Field(f => f.Modality)
.Size(50))));
var buckets = response.Aggregations.Terms("modalities").Buckets;
return buckets.ToDictionary(b => b.Key, b => b.DocCount ?? 0);
}
}
Batch 指数服务
索引整个 DICOM 存档有效:
public class DicomIndexingService
{
private readonly SqlDicomRepository _sqlRepo;
private readonly MongoDicomRepository _mongoRepo;
private readonly ElasticDicomRepository _elasticRepo;
public DicomIndexingService(
SqlDicomRepository sqlRepo = null,
MongoDicomRepository mongoRepo = null,
ElasticDicomRepository elasticRepo = null)
{
_sqlRepo = sqlRepo;
_mongoRepo = mongoRepo;
_elasticRepo = elasticRepo;
}
public async Task IndexDirectoryAsync(
string directory,
int maxParallelism = 4,
IProgress<IndexingProgress> progress = null)
{
var files = Directory.GetFiles(directory, "*.dcm", SearchOption.AllDirectories);
var total = files.Length;
var processed = 0;
var errors = 0;
var semaphore = new SemaphoreSlim(maxParallelism);
var tasks = new List<Task>();
foreach (var file in files)
{
await semaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
try
{
if (_sqlRepo != null)
await _sqlRepo.IndexDicomFileAsync(file);
if (_mongoRepo != null)
await _mongoRepo.IndexDicomFileAsync(file);
if (_elasticRepo != null)
await _elasticRepo.IndexDicomFileAsync(file);
Interlocked.Increment(ref processed);
}
catch (Exception ex)
{
Interlocked.Increment(ref errors);
Console.WriteLine($"Error indexing {file}: {ex.Message}");
}
finally
{
semaphore.Release();
progress?.Report(new IndexingProgress
{
Total = total,
Processed = processed,
Errors = errors
});
}
}));
}
await Task.WhenAll(tasks);
}
}
public class IndexingProgress
{
public int Total { get; set; }
public int Processed { get; set; }
public int Errors { get; set; }
public double PercentComplete => Total > 0 ? (double)Processed / Total * 100 : 0;
}
使用例子
class Program
{
static async Task Main(string[] args)
{
// Initialize license
Metered metered = new Metered();
metered.SetMeteredKey("your-public-key", "your-private-key");
// SQL Server
var optionsBuilder = new DbContextOptionsBuilder<DicomDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=DicomDB;Trusted_Connection=True;");
var sqlRepo = new SqlDicomRepository(new DicomDbContext(optionsBuilder.Options));
// MongoDB
var mongoRepo = new MongoDicomRepository("mongodb://localhost:27017", "dicom_archive");
// Elasticsearch
var elasticRepo = new ElasticDicomRepository("http://localhost:9200");
// Index a directory
var indexer = new DicomIndexingService(sqlRepo, mongoRepo, elasticRepo);
var progress = new Progress<IndexingProgress>(p =>
{
Console.WriteLine($"Progress: {p.PercentComplete:F1}% ({p.Processed}/{p.Total})");
});
await indexer.IndexDirectoryAsync(@"C:\DICOM\Archive", maxParallelism: 8, progress);
// Search examples
var sqlResults = await sqlRepo.SearchStudiesAsync(modality: "CT", startDate: DateTime.Today.AddMonths(-1));
var mongoResults = await mongoRepo.FullTextSearchAsync("chest pain");
var elasticResults = await elasticRepo.SearchAsync("lung nodule");
Console.WriteLine($"SQL results: {sqlResults.Count}");
Console.WriteLine($"MongoDB results: {mongoResults.Count}");
Console.WriteLine($"Elasticsearch results: {elasticResults.Count}");
}
}
结论
在数据库中存储DICOM代数据将医学图像搜索和分析能力转化为优越的SQL数据基在结构化查询和报告中,MongoDB提供灵活的文档保存理想的不同代码图表,Elasticsearch允许在研究描述中进行强大的全文本搜索。
有关提取 DICOM 数据的更多信息,请参阅 ASPOSE.医学文档.