Frisbee——一个简单的DICOM编辑器

目录

介绍

关于Frisbee DICOM编辑器

使用代码

兴趣点


介绍

DICOM(医学数字成像和通信)是用于超声、X射线、CTMRIIGT等模态设备以存储扫描图像的标准。简单来说,DICOM图像具有标题和像素数据。标题包含图像的属性,如患者人口统计数据,如ID、姓名、年龄、出生日期、性别等,以及定义所进行研究的其他属性。像素数据包含扫描的身体部位的图片。DICOM标记以二进制格式存储在文件中。

DICOM图像属性具有基于典型成像工作流程的层次结构。患者就诊的医院要求对患者进行唯一标识,这需要患者ID、姓名、年龄、出生日期、性别等。这构成了患者属性。现在,患者接受了一项手术,该手术导致沿着一系列图像创建研究。假设在一次就诊中,患者接受了超声和X射线检查,那么超声和X射线将只有一项研究和不同的系列。每次新的访问都会创建一个新的研究。

下图显示了DICOM文件的结构(取自NEMA网站)。每个DICOM文件对应于每次扫描生成的图像。在特定检查期间,可以使用不同的扫描设置生成多个此类图像。

除了患者人口统计数据外,还有许多其他标签定义了创建图像的程序。例如,有标识研究(研究实例UID)、系列(系列实例UID)、图像(SOP实例UID)等的标签。有一些标签定义像素数据的属性。同样,也有表示像素数据本身的标签。还有另一组称为序列的标签。这是特定序列标签下的标签集合,例如引用的系列序列,它捕获特定于序列的属性,如序列日期、序列时间、序列实例UID等。此外,一个序列可以包含另一个序列。

下图显示了DICOM标签的编码方式(取自NEMA网站)。标签是标识标签的唯一编号(组号+元素号 -> 16 位),VR(值表示 -> 双字节字符串)是存储的数据类型,value长度是值字段(1632位,取决于VR)的大小(以字节为单位),value 字段包含实际的标签值(偶数字节)。

这是一个明确的总结。您可以从以下资源中阅读有关DICOM标准各个方面的更多信息:

关于Frisbee DICOM编辑器

此工具是基于C# WPF的应用程序。它有助于查看和编辑DICOM标题以及像素数据的更新。它使用该fo-dicom库来读取和写入DICOM文件。fo-dicom是一个开源库,共享@GitHubhttps://github.com/fo-dicom/fo-dicom

UI基于WPF,使用HandyControl用于更丰富的网格和其他 UI 元素。

最新版本的Frisbee可以在这里找到:

UI主要有四个显示区域,DICOM标题显示、序列显示、图像显示和图像属性显示。可以通过单击Value列并编辑值来编辑DICOM标记值。这在标题显示、序列属性显示和图像属性等所有领域都是可能的。此外,可以通过单击网格中的复选框并按“cross”按钮来选择项目来删除所有这些位置的标签。除此之外,在图像显示区域中,可以通过浏览新图像并相应地选择图像属性来更新像素数据。

从目录加载图像后,可以单击前进按钮移动到目录中的下一个图像,或单击后退按钮查看上一个图像。

标记显示设计为可重用的WPF用户控件:DicomTagView。它将DICOM标签显示所需的所有功能包装为网格、更新和删除功能。这用于显示标签的所有位置。对于任何基于WPF MVVM的应用程序,代码的其余部分都相当简单。

DicomTagView.xaml

<UserControl x:Class="FrisbeeDicomEditor.Views.DicomTagView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:FrisbeeDicomEditor.Views" 
             xmlns:hc="https://handyorg.github.io/handycontrol"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             x:Name="DicomTagViewControl">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
         Height="30" Background="#326CF3" BorderBrush="#326CF3" BorderThickness="1">
            <TextBlock x:Name="header" Foreground="White" 
             HorizontalAlignment="Center" VerticalAlignment="Center" 
                       Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:DicomTagView}}, 
                       Path=Header}" FontSize="12"/>
        </Border>
        <hc:SearchBar x:Name="searchBar"  Grid.Column="1" Margin="5 0 0 0" 
         HorizontalAlignment="Right" Width="160" IsRealTime="True" 
                      Command="{Binding RelativeSource={RelativeSource FindAncestor, 
                      AncestorType={x:Type local:DicomTagView}}, 
                      Path=SearchTextChangedCommand}" 
                      CommandParameter="{Binding Text,RelativeSource=
                                        {RelativeSource Self}}"/>
        <Button x:Name="deleteButton" Grid.Row="0" Grid.Column="2" 
                Width="50" Padding="16,3" Margin="5" 
                Style="{StaticResource ButtonDanger.Small}" 
                hc:IconElement.Geometry="{StaticResource DeleteGeometry}"
                Command="{Binding RelativeSource={RelativeSource FindAncestor, 
                AncestorType={x:Type local:DicomTagView}}, Path=DeleteDicomItemCommand}"
                CommandParameter="{Binding RelativeSource=
                {RelativeSource FindAncestor, AncestorType={x:Type local:DicomTagView}}, 
                 Path=DeleteDicomItemCommandParam}"/>
        <DataGrid x:Name="dataGrid" Grid.Row="1" Grid.Column="0" 
                  Grid.RowSpan="4" Grid.ColumnSpan="3" 
                  ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, 
                  AncestorType={x:Type local:DicomTagView}}, Path=DicomAttributes}" 
                  AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridCheckBoxColumn Binding="{Binding IsSelected, 
                 UpdateSourceTrigger=PropertyChanged}"/>
                <DataGridTextColumn Header="Tag" IsReadOnly="True" 
                 Binding="{Binding DicomTag}"/>
                <DataGridTextColumn Header="VR" IsReadOnly="True" 
                 Binding="{Binding DicomVR}"/>
                <DataGridTextColumn Header="Value" 
                 Binding="{Binding Value, UpdateSourceTrigger=PropertyChanged}" 
                           Width="{Binding RelativeSource=
                           {RelativeSource FindAncestor, 
                           AncestorType={x:Type local:DicomTagView}}, 
                           Path=ValueColumnWidth}" MaxWidth="600">
                    <DataGridTextColumn.ElementStyle>
                        <Style>
                            <Setter Property="TextBlock.TextWrapping" 
                             Value="WrapWithOverflow" />
                            <Setter Property="TextBlock.TextAlignment" Value="Left"/>
                        </Style>
                    </DataGridTextColumn.ElementStyle>
                </DataGridTextColumn>
                <DataGridTextColumn Header="Description" IsReadOnly="True" 
                 Binding="{Binding Description}" Width="400" MaxWidth="600">
                    <DataGridTextColumn.ElementStyle>
                        <Style>
                            <Setter Property="TextBlock.TextWrapping" 
                             Value="WrapWithOverflow" />
                            <Setter Property="TextBlock.TextAlignment" Value="Left"/>
                        </Style>
                    </DataGridTextColumn.ElementStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</UserControl>

使用代码

此工具的代码共享@GitHub https://github.com/sudheeshps/FrisbeeDicomEditor

加载和保存DICOM文件的核心代码是DicomDataService类。

public class DicomDataService : ObservableObject
{
    private DicomDataset _dataset;
    private List<string> _files;
    private string _currentDir;
    private int _fileIndex = 0;

    public EventHandler<DicomFileStateEventArgs> FileOpenSuccess;
    public EventHandler<DicomFileStateEventArgs> FileOpenFailed;
    public EventHandler<DicomFileStateEventArgs> FileSaveSuccess;
    public EventHandler<DicomFileStateEventArgs> FileSaveFailed;
    public EventHandler<DicomFileStateEventArgs> ReplaceImageSuccess;
    public EventHandler<DicomFileStateEventArgs> ReplaceImageFailed;

    public EventHandler<DicomDatasetLoadStartedEventArgs> DicomDatasetLoadStarted;
    public EventHandler<DicomItemReadEventArgs> DicomItemRead;
    public EventHandler<DicomDatasetLoadCompletedArgs> DicomDatasetLoadCompleted;
    public ObservableCollection<Models.DicomItem>
    DicomItems { get; } = new ObservableCollection<Models.DicomItem>();
    public ObservableCollection<Models.DicomItem>
    ImageAttributes { get; } = new ObservableCollection<Models.DicomItem>();
    private ObservableCollection<TreeViewItem> _sequences =
                                 new ObservableCollection<TreeViewItem>();
    public ObservableCollection<TreeViewItem> Sequences
    {
        get => _sequences;
        set => SetProperty(ref _sequences, value);
    }
    public async Task<bool> LoadDicomFileAsync(string fileName)
    {
        try
        {
            using (var fileStream =
            new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                var dicomFile = await DicomFile.OpenAsync
                                (fileStream, FileReadOption.ReadAll);
                if (dicomFile == null)
                {
                    FileOpenFailed?.Invoke(this, new DicomFileStateEventArgs()
                                          { FileName = fileName });
                    return false;
                }
                _dataset = dicomFile.Dataset.Clone();
                LoadDicomDataset();
                FileOpenSuccess?.Invoke(this, new DicomFileStateEventArgs()
                                       { FileName = fileName });
                LoadFilesInDirectory(fileName);
                return true;
            }
        }
        catch (Exception ex)
        {
            FileOpenFailed?.Invoke(this, new DicomFileStateEventArgs()
                                  { FileName = fileName, Exception = ex });
            return false;
        }
    }
    public async Task<bool> LoadNextFile()
    {
        if (_files.Count == 1)
        {
            return true;
        }

        if (_fileIndex == _files.Count - 1)
        {
            _fileIndex = 0;
        }
        return await LoadDicomFileAsync(_files[_fileIndex++]);
    }
    public async Task<bool> LoadPreviousFile()
    {
        if (_files.Count == 1)
        {
            return true;
        }

        if (_fileIndex == 0)
        {
            _fileIndex = _files.Count - 1;
        }
        return await LoadDicomFileAsync(_files[--_fileIndex]);
    }
    public async Task<bool> SaveDicomFileAsync(string fileName)
    {
        try
        {
            var dicomFile = new DicomFile(_dataset);
            await dicomFile.SaveAsync(fileName);
            FileSaveSuccess?.Invoke(this, new DicomFileStateEventArgs()
                                   { FileName = fileName });
            return true;
        }
        catch (Exception ex)
        {
            FileSaveFailed?.Invoke(this, new DicomFileStateEventArgs()
                                  { FileName = fileName, Exception = ex });
            return false;
        }
    }
    public void ReplacePixelData
           (string fileName, SelectedImageInfo selectedImageInfo)
    {
        try
        {
            var bitmap = new Bitmap(fileName);
            var imageFormat = GetImageFormat(fileName);
            var pixels = GetPixels(bitmap, imageFormat,
                         out var rows, out var columns);
            var buffer = new MemoryByteBuffer(pixels);
            AddOrUpdatePixelTags(selectedImageInfo, rows, columns);
            AddPixelData(selectedImageInfo, rows, columns, buffer);
            LoadDicomDataset();
            ReplaceImageSuccess?.Invoke(this, new DicomFileStateEventArgs()
                                       { FileName = fileName });
        }
        catch (Exception ex)
        {
            ReplaceImageFailed?.Invoke(this, new DicomFileStateEventArgs()
                                      { Exception = ex });
        }
    }
    private void LoadFilesInDirectory(string fileName)
    {
        var dir = Path.GetDirectoryName(fileName);
        if (_currentDir != dir)
        {
            _files = Directory.GetFiles(dir).ToList();
            _fileIndex = 0;
            _currentDir = dir;
        }
    }
    private void AddPixelData(SelectedImageInfo selectedImageInfo,
            int rows, int columns, MemoryByteBuffer buffer)
    {
        var pixelData = DicomPixelData.Create(_dataset, true);
        pixelData.BitsStored = selectedImageInfo.BitsStored;
        pixelData.SamplesPerPixel = selectedImageInfo.SamplesPerPixel;
        pixelData.HighBit = selectedImageInfo.HighBit;
        pixelData.PhotometricInterpretation =
                  selectedImageInfo.PhotometricInterpretation;
        pixelData.PixelRepresentation = selectedImageInfo.PixelRepresentation;
        pixelData.PlanarConfiguration = selectedImageInfo.PlanarConfiguration;
        pixelData.Height = (ushort)rows;
        pixelData.Width = (ushort)columns;
        pixelData.AddFrame(buffer);
    }

    private void AddOrUpdatePixelTags(SelectedImageInfo selectedImageInfo,
                                      int rows, int columns)
    {
        _dataset.AddOrUpdate(DicomTag.PhotometricInterpretation,
                            selectedImageInfo.PhotometricInterpretation.Value);
        _dataset.AddOrUpdate(DicomTag.Rows, (ushort)rows);
        _dataset.AddOrUpdate(DicomTag.Columns, (ushort)columns);
        _dataset.AddOrUpdate(DicomTag.BitsAllocated,
                            (ushort)selectedImageInfo.BitsAllocated);
    }

    private System.Drawing.Imaging.ImageFormat GetImageFormat(string fileName)
    {
        var fileExtension = Path.GetExtension(fileName);
        switch (fileExtension)
        {
            case ".jpg":
            case ".jpeg": return System.Drawing.Imaging.ImageFormat.Jpeg;
            case ".bmp": return System.Drawing.Imaging.ImageFormat.Bmp;
            case ".png": return System.Drawing.Imaging.ImageFormat.Png;
        }
        return null;
    }

    private static byte[] GetPixels
    (Bitmap bitmap, System.Drawing.Imaging.ImageFormat imageFormat,
        out int rows, out int columns)
    {
        using (var stream = new MemoryStream())
        {
            bitmap.Save(stream, imageFormat);
            rows = bitmap.Height;
            columns = bitmap.Width;
            return stream.ToArray();
        }
    }
    private void LoadDicomDataset()
    {
        DicomDatasetLoadStarted?.Invoke(this,
        new DicomDatasetLoadStartedEventArgs() { DicomDataset = _dataset });
        foreach (var dataItem in _dataset)
        {
            var dicomDataType = DicomItemType.Normal;
            if (dataItem.Tag == DicomTag.Rows || dataItem.Tag == DicomTag.Columns ||
            dataItem.Tag == DicomTag.BitsAllocated ||
                            dataItem.Tag == DicomTag.PhotometricInterpretation)
            {
                dicomDataType = DicomItemType.ImageAttribute;
            }
            if (dataItem.ValueRepresentation == DicomVR.SQ)
            {
                dicomDataType = DicomItemType.SequenceItem;
            }
            DicomItemRead?.Invoke(this, new DicomItemReadEventArgs()
            {
                DicomDataset = _dataset,
                DicomItem = dataItem,
                DicomItemType = dicomDataType
            });
        }
        DicomDatasetLoadCompleted?.Invoke(this,
        new DicomDatasetLoadCompletedArgs() { DicomDataset = _dataset });
        if (_dataset.Contains(DicomTag.PixelData))
        {
            DicomItemRead?.Invoke(this, new DicomItemReadEventArgs()
            {
                DicomDataset = _dataset,
                DicomItemType = DicomItemType.PixelData
            });
        }
    }
}

该应用程序是基于WPF MVVM模式设计的。该fo-dicom库要求为32位或64位平台构建应用程序。它仅针对64Windows进行了测试。因此,在构建x64平台时需要选择。

兴趣点

该工具支持编辑和删除标签、更新像素数据等基本功能。但是具有以下功能使其功能更加强大:

  • 添加标签
  • 显示多帧图像
  • 等等。

测试仅使用可免费使用的测试数据进行。因此,关于使用不同测试DICOM图像的反馈将是一个很好的证明。感谢GitHub存储库的PR以添加功能或修复问题。

https://www.codeproject.com/Articles/5363606/Frisbee-A-Simple-DICOM-Editor

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值