Skip to content

Commit

Permalink
Merge pull request #1 from HicServices/release/1.0.1
Browse files Browse the repository at this point in the history
Release/1.0.1
  • Loading branch information
tznind authored Dec 9, 2019
2 parents 8096b13 + 533e873 commit 7fc71f0
Show file tree
Hide file tree
Showing 42 changed files with 2,811 additions and 91 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dist: bionic
jobs:
include:
- language: csharp
mono: none
dotnet: 2.2.100

script:
- dotnet test ./Tests/Tests.csproj
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.1] - 2019-12-09

### Added

- Added initial iteration of dicom tag repopulator (anonymises dicom images based on CSV data)
- Updated [DicomTypeTranslation] to 2.1.2

## [1.0.0] - 2019-10-01

Initial version


[Unreleased]: https://github.com/HicServices/DicomTemplateBuilder/compare/v1.0.0...develop
[1.0.1]: https://github.com/HicServices/DicomTemplateBuilder/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/HicServices/DicomTemplateBuilder/tree/v1.0.0
[DicomTypeTranslation]: https://github.com/HicServices/DicomTypeTranslation
Binary file added Icons/CopyToClipboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# DicomTemplateBuilder

[![Build Status](https://travis-ci.org/HicServices/DicomTemplateBuilder.svg?branch=master)](https://travis-ci.org/HicServices/DicomTemplateBuilder) [![Total alerts](https://img.shields.io/lgtm/alerts/g/HicServices/DicomTemplateBuilder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HicServices/DicomTemplateBuilder/alerts/)

[Dicom Template Builder](https://github.com/HicServices/DicomTemplateBuilder/releases) is a small windows application to assist researchers in building simple relational database schemas for storing DICOM images.

It supports:
Expand Down
41 changes: 41 additions & 0 deletions Repopulator/CsvToDicomColumn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dicom;

namespace Repopulator
{
public class CsvToDicomColumn
{
public string Name { get; }
public int Index { get; }
public HashSet<DicomTag> TagsToPopulate { get; }
public bool IsFilePath { get; set; }


public CsvToDicomColumn(string colName, int index, bool isFileColumn,params DicomTag[] mappedTags)
{
if (mappedTags != null && mappedTags.Any() && isFileColumn)
throw new ArgumentException("Column has ambiguous role, it should either provide dicom tag substitutions or be the file path column not both");

if ((mappedTags == null || !mappedTags.Any())&& !isFileColumn)
throw new ArgumentException("Column has no clear role, it should either provide dicom tag substitutions or be the file path column");

if (index < 0)
throw new ArgumentException("index cannot be negative");

if (mappedTags != null)
{
var sq = mappedTags.FirstOrDefault(t => t.DictionaryEntry.ValueRepresentations.Contains(DicomVR.SQ));
if(sq != null)
throw new ArgumentException($"Sequence tags are not supported ({sq.DictionaryEntry.Keyword})");
}


Name = colName;
Index = index;
TagsToPopulate = new HashSet<DicomTag>(mappedTags?? new DicomTag[0]);
IsFilePath = isFileColumn;
}
}
}
196 changes: 196 additions & 0 deletions Repopulator/CsvToDicomTagMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;
using Dicom;
using Repopulator.Matchers;

namespace Repopulator
{
public class CsvToDicomTagMapping
{
/// <summary>
/// The column of the CSV which records the file path to the image(s) being processed. These should be expressed
/// relatively (i.e. not absolute path names)
/// </summary>
public CsvToDicomColumn FilenameColumn;

/// <summary>
/// Columns which contain dicom tag values. This is not all the columns in the CSV. It does not include <see cref="FilenameColumn"/>
/// or any columns which could not be mapped;
/// </summary>
public List<CsvToDicomColumn> TagColumns = new List<CsvToDicomColumn>();

/// <summary>
/// The file that was read during <see cref="BuildMap"/>
/// </summary>
public FileInfo CsvFile { get; private set; }

/// <summary>
/// Clears current column mappings
/// </summary>
public void Clear()
{
FilenameColumn = null;
CsvFile = null;
TagColumns.Clear();
IsBuilt = false;
}

/// <summary>
/// True if the class has been built yet
/// </summary>
public bool IsBuilt { get; private set; }

/// <summary>
/// Reads the headers from the CSV specified in <paramref name="options"/> and builds <see cref="TagColumns"/> and <see cref="FilenameColumn"/>.
/// Returns true if the headers constitute a valid set (at least 1 and <see cref="FilenameColumn"/> found).
/// </summary>
/// <param name="options"></param>
/// <param name="log"></param>
/// <returns></returns>
public bool BuildMap(DicomRepopulatorOptions options, out string log)
{
Clear();

StringBuilder sb = new StringBuilder();

//how we will tie CSV rows to files
IRepopulatorMatcher matcher = null;

try
{
var extraMappings = GetExtraMappings(options);

CsvFile = options.CsvFileInfo;

using (var reader = new CsvReader(CsvFile.OpenText()))
{
reader.Configuration.TrimOptions = TrimOptions.Trim;
reader.Read();
var couldReadHeader = reader.ReadHeader();

sb.AppendLine("Could Read Header:" + couldReadHeader);

if (couldReadHeader)
{
for (var index = 0; index < reader.Context.HeaderRecord.Length; index++)
{
var header = reader.Context.HeaderRecord[index];
var match = GetKeyDicomTagAndColumnName(options, header, index,extraMappings);

if (match != null)
{
if(match.IsFilePath)
if (FilenameColumn != null)
throw new Exception("There are 2+ FilenameColumn in the CSV");
else
FilenameColumn = match;
else
{
if(TagColumns.Any(c=>c.TagsToPopulate.Intersect(match.TagsToPopulate).Any()))
throw new Exception($"There are 2+ columns that both populate for one of the DicomTag(s) '{string.Join(",",match.TagsToPopulate)}'");

TagColumns.Add(match);
}

sb.AppendLine($"Validated header '{header}'");
}
else
sb.AppendLine($"Could not determine tag for '{header}'");
}
}

sb.AppendLine($"Found {TagColumns.Count} valid mappings");
sb.AppendLine($"FilenameColumn is: {FilenameColumn?.Name ?? "Not Set"}");
}

IsBuilt = true;

var matcherFactory = new MatcherFactory();
using(matcher = matcherFactory.Create(this,options))
sb.AppendLine($"Matching Strategy is: { matcher?.ToString() ?? "No Strategy Found"}");
}
catch (Exception e)
{
sb.AppendLine(e.ToString());
log = sb.ToString();
return false;
}

log = sb.ToString();


return TagColumns.Count > 0 && matcher != null;
}



private Dictionary<string, HashSet<DicomTag>> GetExtraMappings(DicomRepopulatorOptions state)
{
Dictionary<string, HashSet<DicomTag>> toReturn = new Dictionary<string, HashSet<DicomTag>>(StringComparer.CurrentCultureIgnoreCase);

if(string.IsNullOrWhiteSpace(state.InputExtraMappings))
return null;

var extraMappingsFile = state.ExtraMappings;

int lineNumber = 0;
foreach (string[] pair in File.ReadAllLines(extraMappingsFile.FullName).Select(l => l.Split(new []{':'},StringSplitOptions.RemoveEmptyEntries)))
{
lineNumber++;

//ignore blank lines
if(pair.Length == 0)
continue;

if(pair.Length != 2)
throw new Exception($"Bad line in extra mappings file (line number {lineNumber}). Line did not match expected format 'ColumnName:TagName'");


var found = DicomDictionary.Default.SingleOrDefault(entry => string.Equals(entry.Keyword ,pair[1],StringComparison.CurrentCultureIgnoreCase));

if (found == null)
throw new Exception(
$"Bad tag '{pair[1]}' on line number {lineNumber} of ExtraMappings file '{extraMappingsFile.FullName}'. It is not a valid DicomTag name");

if(!toReturn.ContainsKey(pair[0]))
toReturn.Add(pair[0],new HashSet<DicomTag>());

toReturn[pair[0]].Add(found.Tag);
}

return toReturn;
}

/// <summary>
/// Creates a mapping between a single CSV file column and one or more <see cref="DicomTag"/>
/// </summary>
public CsvToDicomColumn GetKeyDicomTagAndColumnName(DicomRepopulatorOptions state, string columnName,int index,Dictionary<string,HashSet<DicomTag>> extraMappings)
{
CsvToDicomColumn toReturn = null;
if(columnName.Equals(state.FileNameColumn,StringComparison.CurrentCultureIgnoreCase))
toReturn = new CsvToDicomColumn(columnName,index,true);

var found = DicomDictionary.Default.SingleOrDefault(entry => string.Equals(entry.Keyword ,columnName,StringComparison.CurrentCultureIgnoreCase));

if(found != null)
if (toReturn == null)
toReturn = new CsvToDicomColumn(columnName,index,false,found.Tag);
else
toReturn.TagsToPopulate.Add(found.Tag); //it's a file path AND a tag! ok...


if (extraMappings != null && extraMappings.ContainsKey(columnName))
if(toReturn == null)
toReturn = new CsvToDicomColumn(columnName,index,false,extraMappings[columnName].ToArray());
else
toReturn.TagsToPopulate.UnionWith(extraMappings[columnName]);

return toReturn;
}
}
}
106 changes: 106 additions & 0 deletions Repopulator/DicomRepopulator.cd
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<ClassDiagram MajorVersion="1" MinorVersion="1">
<Class Name="Repopulator.Matchers.FilePathMatcher" Collapsed="true">
<Position X="4.5" Y="4" Width="1.5" />
<TypeIdentifier>
<HashCode>AgAAAAAAACAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAA=</HashCode>
<FileName>Matchers\FilePathMatcher.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Repopulator.Matchers.MatcherFactory">
<Position X="3.5" Y="2.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA=</HashCode>
<FileName>Matchers\MatcherFactory.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Repopulator.Matchers.RepopulatorMatcher" Collapsed="true">
<Position X="5.5" Y="2.5" Width="2.25" />
<TypeIdentifier>
<HashCode>AgAAAAAAADIAAAAAAAAAAAAAAAAAAAAAIAAAAABAAAA=</HashCode>
<FileName>Matchers\RepopulatorMatcher.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Map" />
</ShowAsAssociation>
<Lollipop Position="0.2" />
</Class>
<Class Name="Repopulator.Matchers.TagMatcher" Collapsed="true">
<Position X="7" Y="4" Width="1.5" />
<TypeIdentifier>
<HashCode>AgAAEAAAACAAAAABAAAAAgAAAAAAAAAAJAQgBAAAAAA=</HashCode>
<FileName>Matchers\TagMatcher.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Field Name="_indexer" />
</ShowAsAssociation>
</Class>
<Class Name="Repopulator.CsvToDicomColumn">
<Position X="9.25" Y="6.5" Width="2.25" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAgAAAAQAAAAAAAAAAAEAABAAAAA=</HashCode>
<FileName>CsvToDicomColumn.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Repopulator.CsvToDicomTagMapping">
<Position X="5" Y="5.75" Width="2" />
<AssociationLine Name="FilenameColumn" Type="Repopulator.CsvToDicomColumn" FixedToPoint="true">
<Path>
<Point X="7" Y="7.923" />
<Point X="9.25" Y="7.923" />
</Path>
</AssociationLine>
<TypeIdentifier>
<HashCode>AAACAAGAAAAAAAAAAAAAAABAAAAAAAAABAAAAARBAAA=</HashCode>
<FileName>CsvToDicomTagMapping.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Field Name="FilenameColumn" />
</ShowAsAssociation>
<ShowAsCollectionAssociation>
<Field Name="TagColumns" />
</ShowAsCollectionAssociation>
</Class>
<Class Name="Repopulator.DicomRepopulatorOptions">
<Position X="12" Y="0.5" Width="2.75" />
<Compartments>
<Compartment Name="Properties" Collapsed="true" />
</Compartments>
<TypeIdentifier>
<HashCode>AAAAgQgABAAAAAAAQAECAAQQCAAAEAgEASAAAAACAAA=</HashCode>
<FileName>DicomRepopulatorOptions.cs</FileName>
</TypeIdentifier>
</Class>
<Class Name="Repopulator.DicomRepopulatorProcessor" BaseTypeListCollapsed="true">
<Position X="9.25" Y="0.5" Width="2.25" />
<Compartments>
<Compartment Name="Fields" Collapsed="true" />
</Compartments>
<TypeIdentifier>
<HashCode>AAhAAAAIACAAQQAAAgAAAIBAMAAABACAAAAQAAAAgAA=</HashCode>
<FileName>DicomRepopulatorProcessor.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Matcher" />
</ShowAsAssociation>
<Lollipop Position="0.2" Collapsed="true" />
</Class>
<Class Name="Repopulator.RepopulatorJob">
<Position X="9.25" Y="4.5" Width="2" />
<TypeIdentifier>
<HashCode>AAAAAAAAABAAAAAAAAIAAAAAAAAEAAAAAAAAAAAAAAA=</HashCode>
<FileName>RepopulatorJob.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Map" />
</ShowAsAssociation>
</Class>
<Interface Name="Repopulator.Matchers.IRepopulatorMatcher">
<Position X="5.25" Y="0.5" Width="2.75" />
<TypeIdentifier>
<HashCode>AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA=</HashCode>
<FileName>Matchers\IRepopulatorMatcher.cs</FileName>
</TypeIdentifier>
</Interface>
<Font Name="Segoe UI" Size="9" />
</ClassDiagram>
Loading

0 comments on commit 7fc71f0

Please sign in to comment.