Click here to Skip to main content
15,888,461 members
Articles / Desktop Programming / WPF

Frisbee - A Simple DICOM Editor

Rate me:
Please Sign up or sign in to vote.
2.87/5 (4 votes)
26 Jun 2023CPOL4 min read 4.5K   234   2  
Frisbee is a simple DICOM Editor which helps to edit DICOM files
This is a simple DICOM editor which supports searching and editing the values of DICOM tags, delete tags and save it. A DICOM file can be opened from a directory and then allows easy back and forth navigation of files present in the directory. It displays the pixel data contained in the image and also supports updating a new image.

Introduction

DICOM (Digital Imaging and Communication in Medicine) is a standard used in modality devices like Ultrasound, X-Ray, CT, MRI, IGT to store scanned images. In simple terms, a DICOM image has a header and pixel data. The header contains the attributes of image like the patient demographics such as ID, name, age, DOB, gender, etc. and other attributes that define the study performed. The pixel data contains the picture of the body part scanned. The DICOM tags are stored in binary format in the file.

The DICOM image attributes have a hierarchy which is based on a typical imaging workflow. The patient visits a hospital which requires the patient to be uniquely identified which mandates a patient ID, name, age, DOB, gender, etc. This constitutes the patient attributes. Now, the patient undergoes a procedure, which causes a study to be created along series of images. Suppose in a single visit, the patient undergoes Ultrasound and X-Ray, then there will be only one study and different series for Ultrasound and X-Ray. Each new visit creates a new study.

Image 1

The following figure shows the structure of DICOM file (taken from NEMA website). Each DICOM file corresponds to an image generated per scan. There can be multiple such images generated with different scan settings during a specific examination.

Image 2

Apart from patient demographics, there are lot of other tags that define the procedure that has created the image. For example, there are tags that identify the study (study instance UID), the series (series instance UID), the image (SOP instance UID), etc. There are tags that define attributes of the pixel data. Same way, there is tag that represents pixel data itself. There is another set of tags called "sequence". This is a collection of tags under a specific sequence tag, for example, "Referenced Series Sequences", this captures the attributes specific to a series like Series Date, Series Time, Series Instance UID, etc. Also, a sequence can contain another sequence.

The following picture shows how DICOM tag is encoded (taken from NEMA website). The tag is a unique number that identifies the tag (group number + element number -> 16 bits), VR (Value Representation -> two-byte character string) is the type of data stored, value length is the size in bytes of the value field (16 or 32 bit depending on VR) and value field contains the actual tag value (even number of bytes).

Image 3

That was an express summary. You can read more about various aspects of the DICOM standard from the following resources:

About Frisbee DICOM Editor

Image 4

This tool is a C# WPF based application. It helps to view and edit the DICOM header along with update of pixel data. It uses the fo-dicom library for reading and writing DICOM files. fo-dicom is an open-source library shared @ GitHub: https://github.com/fo-dicom/fo-dicom.

The UI is based on WPF and uses HandyControl for more rich grid and other UI elements.

Latest release of Frisbee can be found here:

The UI has mainly four display areas, the DICOM header display, sequence display, image display and image attributes display below. The DICOM tag values can be edited by clicking on the “Value” column and editing the values. This is possible in all the areas like header display, sequence attributes display and image attributes. Also, the tags can be deleted in all these places by selecting item by clicking checkbox in the grid and pressing the "cross" button. Apart from this, in the image display area, the pixel data can be updated by browsing a new image and selecting image attributes accordingly.

Image 5

Once an image is loaded from a directory, it is possible to click forward button to move to the next image in the directory or back button to see previous image.

The tag display is designed as a reusable WPF user control: DicomTagView. It wraps all the functionality required for DICOM tag display as grid, update and delete features. This is used in all the places wherever the tags are displayed. The rest of the code is fairly straightforward as for any WPF MVVM based application.

DicomTagView.xaml

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>

Using the Code

The code for this tool is shared @ GitHub: https://github.com/sudheeshps/FrisbeeDicomEditor.

The core piece of code that loads and saves DICOM files is DicomDataService class.

C#
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
            });
        }
    }
}

The application is designed based on WPF MVVM pattern. The fo-dicom library requires the application to be built for 32 bit or 64 bit platform. It is tested only for 64 bit Windows. So while building x64 platform needs to be selected.

Points of Interest

The tool supports essential features like editing and deleting tags, updating pixel data, etc. But having the following features makes it much more powerful:

  • Adding tags
  • Display multi-frame images
  • etc.

Testing is done only with test data that is available for free use. So feedback on using different test DICOM images will be a good testimonial. PRs to the GitHub repository for adding features or fixing issues are appreciated.

History

  • This is the first version published.
This article was originally posted at https://github.com/sudheeshps/FrisbeeDicomEditor

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
India India
A senior engineer who likes reading and coding Smile | :) . I can be reached at sudheeshps@gmail.com

Comments and Discussions

 
-- There are no messages in this forum --