From 05e168528c6aa462aea2d0c8d16dfb19b9a3a1bc Mon Sep 17 00:00:00 2001 From: James A Sutherland Date: Wed, 4 Oct 2023 09:18:43 -0500 Subject: [PATCH] Release 0.0.16 (#181) * Release 0.0.16 * Bump HIC.BadMedicine from 1.1.0 to 1.1.1 * Bump SixLabors.ImageSharp.Drawing from 1.0.0-beta15 to 2.0.0 * Bump SixLabors.ImageSharp from 2.1.3 to 3.0.2 --- .github/dependabot.yml | 11 +- .github/workflows/codeql.yml | 41 + .github/workflows/testpack.yml | 22 +- .lgtm.yml | 5 - BadDicom/BadDicom.csproj | 27 +- BadDicom/Configuration/Config.cs | 14 +- BadDicom/Configuration/ConfigContext.cs | 8 + BadDicom/Configuration/ExplicitUIDs.cs | 89 +- BadDicom/Configuration/TargetDatabase.cs | 73 +- BadDicom/Program.cs | 544 ++++++------- BadDicom/ProgramOptions.cs | 71 +- .../BadMedicine.Dicom.Tests.csproj | 10 +- .../DicomDataGeneratorTests.cs | 250 +++--- .../NuspecIsCorrectTests.cs | 111 --- .../PackageListIsCorrectTests.cs | 101 +++ BadMedicine.Dicom.Tests/StudyTests.cs | 67 +- BadMedicine.Dicom.sln | 1 - BadMedicine.Dicom/BadMedicine.Dicom.csproj | 14 +- BadMedicine.Dicom/DescBodyPart.cs | 61 +- BadMedicine.Dicom/DicomDataGenerator.cs | 763 +++++++++--------- BadMedicine.Dicom/DicomDataGeneratorStats.cs | 330 ++++---- BadMedicine.Dicom/FileSystemLayout.cs | 43 +- BadMedicine.Dicom/FileSystemLayoutProvider.cs | 81 +- BadMedicine.Dicom/ModalityStats.cs | 105 ++- BadMedicine.Dicom/PixelDrawer.cs | 18 +- BadMedicine.Dicom/Series.cs | 227 +++--- BadMedicine.Dicom/Study.cs | 227 +++--- BadMedicine.Dicom/UIDAllocator.cs | 98 +-- CHANGELOG.md | 42 +- Packages.md | 35 +- README.md | 13 +- SharedAssemblyInfo.cs | 8 +- 32 files changed, 1729 insertions(+), 1781 deletions(-) create mode 100644 .github/workflows/codeql.yml delete mode 100644 .lgtm.yml create mode 100644 BadDicom/Configuration/ConfigContext.cs delete mode 100644 BadMedicine.Dicom.Tests/NuspecIsCorrectTests.cs create mode 100644 BadMedicine.Dicom.Tests/PackageListIsCorrectTests.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7936720..61f9bb6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,13 +3,14 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: daily + interval: weekly + reviewers: + - SMI/reviewers - package-ecosystem: nuget directory: "/" schedule: - interval: daily - open-pull-requests-limit: 10 + interval: weekly + open-pull-requests-limit: 99 target-branch: develop reviewers: - - tznind - - jas88 + - SMI/reviewers diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..f10a37f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + schedule: + - cron: "22 19 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ csharp ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/testpack.yml b/.github/workflows/testpack.yml index 3d4fd9a..74691a7 100644 --- a/.github/workflows/testpack.yml +++ b/.github/workflows/testpack.yml @@ -8,16 +8,18 @@ jobs: steps: - name: Disable disk flush run: sudo apt-get install -y libeatmydata1 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: | + 6.0.x + 7.0.x - name: Start MySQL for testing run: sudo systemctl start mysql.service - name: Test run: | - dotnet test "./BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj" -nologo -c Release - curl https://raw.githubusercontent.com/HicServices/DicomTypeTranslation/master/Templates/CT.it > ./CT.it + dotnet test "./BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj" --nologo -c Release + curl -sL https://raw.githubusercontent.com/SMI/DicomTypeTranslation/master/Templates/CT.it > ./CT.it cp BadDicom/BadDicom.template.yaml BadDicom.yaml dotnet run --project BadDicom/BadDicom.csproj -- ./ 50000 10 CT sed -i "s/Batches: 1/Batches: 5/g" ./BadDicom.yaml @@ -27,18 +29,22 @@ jobs: run: | mkdir -p dist dotnet pack ./BadMedicine.Dicom/BadMedicine.Dicom.csproj -c Release -p:IncludeSymbols=true -p:Version=$(grep AssemblyInformationalVersion SharedAssemblyInfo.cs | cut -d'"' -f2) -nologo - for platform in linux win + for platform in linux-x64 win-x64 osx-{arm64,x64} do - dotnet publish BadDicom/BadDicom.csproj -c Release -r $platform-x64 -o $platform-x64 --self-contained true -nologo -v q -p:PublishSingleFile=true -p:DebugType=embedded -p:GenerateDocumentationFile=false + dotnet publish BadDicom/BadDicom.csproj -c Release -r $platform -o $platform --self-contained true -nologo -v q -p:PublishSingleFile=true -p:DebugType=embedded -p:GenerateDocumentationFile=false done zip -9r dist/baddicom-win-x64-v$(grep AssemblyInformationalVersion SharedAssemblyInfo.cs | cut -d'"' -f2).zip ./win-x64 - tar czf dist/baddicom-linux-x64-v$(grep AssemblyInformationalVersion SharedAssemblyInfo.cs | cut -d'"' -f2).tar.gz ./linux-x64 + for platform in linux-x64 win-x64 osx-{arm64,x64} + do + tar cJf dist/baddicom-${platform}-v$(grep AssemblyInformationalVersion SharedAssemblyInfo.cs | cut -d'"' -f2).tar.xz ./$platform + done + ls -lh dist - name: Nuget push if: contains(github.ref,'refs/tags/') run: | dotnet nuget push ./BadMedicine.Dicom/bin/Release/HIC.BadMedicine.Dicom.*.nupkg -k ${{ secrets.NUGET_KEY }} --skip-duplicate -s https://api.nuget.org/v3/index.json - name: Upload release binaries - uses: svenstaro/upload-release-action@2.3.0 + uses: svenstaro/upload-release-action@2.7.0 if: contains(github.ref, 'refs/tags/v') with: repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.lgtm.yml b/.lgtm.yml deleted file mode 100644 index 5d99e70..0000000 --- a/.lgtm.yml +++ /dev/null @@ -1,5 +0,0 @@ -extraction: - csharp: - index: - dotnet: - version: 6.0.103 diff --git a/BadDicom/BadDicom.csproj b/BadDicom/BadDicom.csproj index c7ae6bf..fceb6b4 100644 --- a/BadDicom/BadDicom.csproj +++ b/BadDicom/BadDicom.csproj @@ -1,34 +1,24 @@ - + - - net6.0 + net7.0 true - Exe en BadDicom.Program false - true + false true true + embedded snupkg + true + enable - - - - - - - - true - DEBUG;TRACE - MinimumRecommendedRules.ruleset - @@ -42,8 +32,9 @@ - - + + + diff --git a/BadDicom/Configuration/Config.cs b/BadDicom/Configuration/Config.cs index a048cf5..c73173a 100644 --- a/BadDicom/Configuration/Config.cs +++ b/BadDicom/Configuration/Config.cs @@ -1,8 +1,10 @@ -namespace BadDicom.Configuration +using YamlDotNet.Serialization; + +namespace BadDicom.Configuration; + +[YamlSerializable] +internal class Config { - class Config - { - public TargetDatabase Database { get;set; } - public ExplicitUIDs UIDs { get; set; } - } + public TargetDatabase? Database { get;set; } + public ExplicitUIDs? UIDs { get; set; } } \ No newline at end of file diff --git a/BadDicom/Configuration/ConfigContext.cs b/BadDicom/Configuration/ConfigContext.cs new file mode 100644 index 0000000..da925ac --- /dev/null +++ b/BadDicom/Configuration/ConfigContext.cs @@ -0,0 +1,8 @@ +using YamlDotNet.Serialization; + +namespace BadDicom.Configuration; + +[YamlStaticContext] +public partial class ConfigContext : StaticContext +{ +} \ No newline at end of file diff --git a/BadDicom/Configuration/ExplicitUIDs.cs b/BadDicom/Configuration/ExplicitUIDs.cs index c5c02ce..f118fc0 100644 --- a/BadDicom/Configuration/ExplicitUIDs.cs +++ b/BadDicom/Configuration/ExplicitUIDs.cs @@ -2,55 +2,56 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using YamlDotNet.Serialization; -namespace BadDicom.Configuration +namespace BadDicom.Configuration; + +/// +/// Config section for loading explicit UIDs from disk and using those in file creation +/// +[YamlSerializable] +public class ExplicitUIDs { /// - /// Config section for loading explicit UIDs from disk and using those in file creation + /// Path to a file containing a list of study instance UIDs to use + /// + public string? StudyInstanceUIDs { get; set; } + + /// + /// Path to a file containing a list of series instance UIDs to use + /// + public string? SeriesInstanceUIDs { get; set; } + + /// + /// Path to a file containing a list of SOP instance UIDs to use + /// + public string? SOPInstanceUIDs { get; set; } + + /// + /// Loads the UID files referenced (if they exist) in the configuration + /// and populates with the values. /// - public class ExplicitUIDs + public void Load() + { + // unlikely but if someone else has pre queued some stuff, this replaces that + UIDAllocator.StudyUIDs.Clear(); + UIDAllocator.SeriesUIDs.Clear(); + UIDAllocator.SOPUIDs.Clear(); + + foreach (var u in GetUIDsFrom(StudyInstanceUIDs)) + UIDAllocator.StudyUIDs.Enqueue(u); + + foreach (var u in GetUIDsFrom(SeriesInstanceUIDs)) + UIDAllocator.SeriesUIDs.Enqueue(u); + + foreach (var u in GetUIDsFrom(SOPInstanceUIDs)) + UIDAllocator.SOPUIDs.Enqueue(u); + } + + private static IEnumerable GetUIDsFrom(string? path) { - /// - /// Path to a file containing a list of study instance UIDs to use - /// - public string StudyInstanceUIDs { get; set; } - - /// - /// Path to a file containing a list of series instance UIDs to use - /// - public string SeriesInstanceUIDs { get; set; } - - /// - /// Path to a file containing a list of SOP instance UIDs to use - /// - public string SOPInstanceUIDs { get; set; } - - /// - /// Loads the UID files referenced (if they exist) in the configuration - /// and populates with the values. - /// - public void Load() - { - // unlikely but if someone else has pre queued some stuff, this replaces that - UIDAllocator.StudyUIDs.Clear(); - UIDAllocator.SeriesUIDs.Clear(); - UIDAllocator.SOPUIDs.Clear(); - - foreach (var u in GetUIDsFrom(StudyInstanceUIDs)) - UIDAllocator.StudyUIDs.Enqueue(u); - - foreach (var u in GetUIDsFrom(SeriesInstanceUIDs)) - UIDAllocator.SeriesUIDs.Enqueue(u); - - foreach (var u in GetUIDsFrom(SOPInstanceUIDs)) - UIDAllocator.SOPUIDs.Enqueue(u); - } - - private IEnumerable GetUIDsFrom(string path) - { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return Enumerable.Empty(); + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) return Enumerable.Empty(); - return File.ReadLines(StudyInstanceUIDs).Where(l => !string.IsNullOrWhiteSpace(l)); - } + return File.ReadLines(path).Where(l => !string.IsNullOrWhiteSpace(l)); } } \ No newline at end of file diff --git a/BadDicom/Configuration/TargetDatabase.cs b/BadDicom/Configuration/TargetDatabase.cs index 62f146d..dc24c7c 100644 --- a/BadDicom/Configuration/TargetDatabase.cs +++ b/BadDicom/Configuration/TargetDatabase.cs @@ -1,46 +1,47 @@ using FAnsi; +using YamlDotNet.Serialization; -namespace BadDicom.Configuration +namespace BadDicom.Configuration; + +/// +/// Identify the target database and configuration for generated data +/// +[YamlSerializable] +public class TargetDatabase { /// - /// Identify the target database and configuration for generated data + /// Which RDBMS the database is (MySQL, Microsoft SQL Server, etc) + /// + public DatabaseType DatabaseType { get; set; } + /// + /// The ConnectionString containing the server name, credentials and other parameters for the connection /// - public class TargetDatabase - { - /// - /// Which RDBMS the database is (MySQL, Microsoft SQL Server, etc) - /// - public DatabaseType DatabaseType { get; set; } - /// - /// The ConnectionString containing the server name, credentials and other parameters for the connection - /// - public string ConnectionString { get; set; } + public string? ConnectionString { get; set; } - /// - /// The name of database - /// - public string DatabaseName { get; set; } + /// + /// The name of database + /// + public string? DatabaseName { get; set; } - /// - /// The filename of a YAML template file to be used for this database - /// - public string Template { get; set; } + /// + /// The filename of a YAML template file to be used for this database + /// + public string? Template { get; set; } - /// - /// Pass true to create tables from template that do not have primary key. Do bulk insert - /// then deduplicate final tables and recreate primary key - /// - public bool MakeDistinct { get; set; } + /// + /// Pass true to create tables from template that do not have primary key. Do bulk insert + /// then deduplicate final tables and recreate primary key + /// + public bool MakeDistinct { get; set; } - /// - /// Set to true to drop and recreate tables described in the Template - /// - public bool DropTables { get; set; } + /// + /// Set to true to drop and recreate tables described in the Template + /// + public bool DropTables { get; set; } - /// - /// The number of parallel batches to execute (each batch gets the full count of studies - /// then they are merged at the end). - /// - public int Batches { get; set; } = 1; - } -} + /// + /// The number of parallel batches to execute (each batch gets the full count of studies + /// then they are merged at the end). + /// + public int Batches { get; set; } = 1; +} \ No newline at end of file diff --git a/BadDicom/Program.cs b/BadDicom/Program.cs index aae830a..ce11dea 100644 --- a/BadDicom/Program.cs +++ b/BadDicom/Program.cs @@ -10,7 +10,6 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using BadDicom.Configuration; -using FellowOakDicom; using DicomTypeTranslation; using DicomTypeTranslation.TableCreation; using FAnsi.Discovery; @@ -21,384 +20,361 @@ using FAnsi.Implementations.PostgreSql; using YamlDotNet.Serialization; -namespace BadDicom +namespace BadDicom; + +internal class Program { - class Program + private static int _returnCode; + public const string ConfigFile = "./BadDicom.yaml"; + + public static int Main(string[] args) { - private static int _returnCode; - public const string ConfigFile = "./BadDicom.yaml"; + _returnCode = 0; - public static int Main(string[] args) - { - _returnCode = 0; + Parser.Default.ParseArguments(args) + .WithParsed(RunOptionsAndReturnExitCode) + .WithNotParsed(HandleParseError); - Parser.Default.ParseArguments(args) - .WithParsed(opts => RunOptionsAndReturnExitCode(opts)) - .WithNotParsed((errs) => HandleParseError(errs)); + return _returnCode; + } - return _returnCode; - } + private static void HandleParseError(IEnumerable errs) + { + // if user wants help then return exit code 0 otherwise return a failed to parse error code + _returnCode = errs.Any(e => e.Tag == ErrorType.HelpRequestedError) ? 0 : 500; + } - private static void HandleParseError(IEnumerable errs) - { - // if user wants help then return exit code 0 otherwise return a failed to parse error code - _returnCode = errs.Any(e => e.Tag == ErrorType.HelpRequestedError) ? 0 : 500; - } + private static void RunOptionsAndReturnExitCode(ProgramOptions opts) + { - private static void RunOptionsAndReturnExitCode(ProgramOptions opts) - { + if (opts.NumberOfPatients <= 0) + opts.NumberOfPatients = 500; + if (opts.NumberOfStudies <= 0) + opts.NumberOfStudies = 2000; - if (opts.NumberOfPatients <= 0) - opts.NumberOfPatients = 500; - if (opts.NumberOfStudies <= 0) - opts.NumberOfStudies = 2000; + if(File.Exists(ConfigFile)) + { + Config config; - if(File.Exists(ConfigFile)) + try { - Config config; + var d = new StaticDeserializerBuilder(new ConfigContext()).Build(); + config = d.Deserialize(File.ReadAllText(ConfigFile)); + } + catch (Exception e) + { + Console.WriteLine($"Error deserializing '{ConfigFile}'{Environment.NewLine}{e}"); + _returnCode = -1; + return; + } + config.UIDs?.Load(); + + if (config.Database != null) + { try { - var d = new Deserializer(); - config = d.Deserialize(File.ReadAllText(ConfigFile)); - } - catch (Exception e) - { - Console.WriteLine($"Error deserializing '{ConfigFile}'"); - Console.Write(e.ToString()); - _returnCode = -1; + _returnCode = RunDatabaseTarget(config.Database, opts); return; } - - config.UIDs?.Load(); - - if (config.Database != null) + catch (Exception e) { - try - { - _returnCode = RunDatabaseTarget(config.Database, opts); - return; - } - catch (Exception e) - { - Console.WriteLine(e); - _returnCode = 3; - return; - } + Console.WriteLine(e); + _returnCode = 3; + return; } } + } - try - { - IPersonCollection identifiers = GetPeople(opts, out Random r); - using var dicomGenerator = GetDataGenerator(opts, identifiers,r, out DirectoryInfo dir); - Console.WriteLine($"{DateTime.Now} Starting file generation (to {dir?.FullName ?? "/dev/null"})" ); - var targetFile = new FileInfo(dir==null?(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "NUL" : "/dev/null") :Path.Combine(dir.FullName, "DicomFiles.csv")); - dicomGenerator.GenerateTestDataFile(identifiers,targetFile,opts.NumberOfStudies); - } - catch (Exception e) - { - Console.WriteLine(e); - _returnCode = 2; - return; - } - - Console.WriteLine($"{DateTime.Now} Finished" ); - - _returnCode = 0; + try + { + var identifiers = GetPeople(opts, out var r); + using var dicomGenerator = GetDataGenerator(opts,r, out var dir); + Console.WriteLine($"{DateTime.Now} Starting file generation (to {dir?.FullName ?? "/dev/null"})" ); + var targetFile = new FileInfo(dir==null?RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "NUL" : "/dev/null" :Path.Combine(dir.FullName, "DicomFiles.csv")); + dicomGenerator.GenerateTestDataFile(identifiers,targetFile,opts.NumberOfStudies); } - - private static DicomDataGenerator GetDataGenerator(ProgramOptions opts, IPersonCollection identifiers,Random r, out DirectoryInfo dir) + catch (Exception e) { - //Generate the dicom files (of the modalities that the user requested) - string[] modalities = !string.IsNullOrWhiteSpace(opts.Modalities)? opts.Modalities.Split(",") :Array.Empty(); - - dir = opts.OutputDirectory.Equals("/dev/null",StringComparison.InvariantCulture) ? null : Directory.CreateDirectory(opts.OutputDirectory); - return new(r, opts.OutputDirectory, modalities) - { - NoPixels = opts.NoPixels, - Anonymise = opts.Anonymise, - Layout = opts.Layout, - MaximumImages = opts.MaximumImages, - Csv = opts.csv, - }; + Console.WriteLine(e); + _returnCode = 2; + return; } - private static IPersonCollection GetPeople(ProgramOptions opts, out Random r) - { - r = opts.Seed == -1 ? new() : new Random(opts.Seed); + Console.WriteLine($"{DateTime.Now} Finished" ); - //create a cohort of people - IPersonCollection identifiers = new PersonCollection(); - identifiers.GeneratePeople(opts.NumberOfPatients,r); + _returnCode = 0; + } - return identifiers; - } + private static DicomDataGenerator GetDataGenerator(ProgramOptions opts,Random r, out DirectoryInfo? dir) + { + //Generate the dicom files (of the modalities that the user requested) + var modalities = string.IsNullOrWhiteSpace(opts.Modalities)? Array.Empty() :opts.Modalities.Split(","); - private static int RunDatabaseTarget(TargetDatabase configDatabase, ProgramOptions opts) + dir = opts.OutputDirectory?.Equals("/dev/null",StringComparison.InvariantCulture)!=false ? null : Directory.CreateDirectory(opts.OutputDirectory); + return new DicomDataGenerator(r, opts.OutputDirectory, modalities) { - var batchSize = Math.Max(1, configDatabase.Batches); + NoPixels = opts.NoPixels, + Anonymise = opts.Anonymise, + Layout = opts.Layout, + MaximumImages = opts.MaximumImages, + Csv = opts.csv + }; + } - //if we are going into a database we definitely do not need pixels! - if (opts.NoPixels == false) - opts.NoPixels = true; + private static IPersonCollection GetPeople(ProgramOptions opts, out Random r) + { + r = opts.Seed == -1 ? new Random() : new Random(opts.Seed); - - Stopwatch swTotal = new(); + //create a cohort of people + IPersonCollection identifiers = new PersonCollection(); + identifiers.GeneratePeople(opts.NumberOfPatients,r); - swTotal.Start(); + return identifiers; + } - string neverDistinct = "SOPInstanceUID"; + private static int RunDatabaseTarget(TargetDatabase configDatabase, ProgramOptions opts) + { + var batchSize = Math.Max(1, configDatabase.Batches); - if (!File.Exists(configDatabase.Template)) - { - Console.WriteLine($"Listed template file '{configDatabase.Template}' does not exist"); - return -1; - } + //if we are going into a database we definitely do not need pixels! + opts.NoPixels = true; - ImageTableTemplateCollection template; - try - { - template = ImageTableTemplateCollection.LoadFrom(File.ReadAllText(configDatabase.Template)); - } - catch (Exception e) - { - Console.WriteLine($"Error reading yaml from '{configDatabase.Template}'"); - Console.WriteLine(e.ToString()); - return -2; - } + + var swTotal = Stopwatch.StartNew(); + const string neverDistinct = "SOPInstanceUID"; + + if (!File.Exists(configDatabase.Template)) + { + Console.WriteLine($"Listed template file '{configDatabase.Template}' does not exist"); + return -1; + } + + ImageTableTemplateCollection template; + try + { + template = ImageTableTemplateCollection.LoadFrom(File.ReadAllText(configDatabase.Template)); + } + catch (Exception e) + { + Console.WriteLine($"Error reading yaml from '{configDatabase.Template}'{Environment.NewLine}{e}"); + return -2; + } - ImplementationManager.Load(); - ImplementationManager.Load(); - ImplementationManager.Load(); - ImplementationManager.Load(); + ImplementationManager.Load(); + ImplementationManager.Load(); + ImplementationManager.Load(); + ImplementationManager.Load(); - var server = new DiscoveredServer(configDatabase.ConnectionString, configDatabase.DatabaseType); + var server = new DiscoveredServer(configDatabase.ConnectionString, configDatabase.DatabaseType); - try - { - server.TestConnection(); - } - catch (Exception e) - { - Console.WriteLine($"Could not reach target server '{server.Name}'"); - Console.WriteLine(e); - return -2; - } + try + { + server.TestConnection(); + } + catch (Exception e) + { + Console.WriteLine($"Could not reach target server '{server.Name}'"); + Console.WriteLine(e); + return -2; + } - var db = server.ExpectDatabase(configDatabase.DatabaseName); + var db = server.ExpectDatabase(configDatabase.DatabaseName); - if (!db.Exists()) - { - Console.WriteLine($"Creating Database '{db.GetRuntimeName()}'"); - db.Create(); - Console.WriteLine("Database Created"); - } - else - { - Console.WriteLine($"Found Database '{db.GetRuntimeName()}'"); - } + if (!db.Exists()) + { + Console.WriteLine($"Creating Database '{db.GetRuntimeName()}'"); + db.Create(); + Console.WriteLine("Database Created"); + } + else + { + Console.WriteLine($"Found Database '{db.GetRuntimeName()}'"); + } - var creator = new ImagingTableCreation(db.Server.GetQuerySyntaxHelper()); + var creator = new ImagingTableCreation(db.Server.GetQuerySyntaxHelper()); - Console.WriteLine($"Image template contained schemas for {template.Tables.Count} tables. Looking for existing tables.."); + Console.WriteLine($"Image template contained schemas for {template.Tables.Count} tables. Looking for existing tables.."); - //setting up bulk inserters - DiscoveredTable[] tables = new DiscoveredTable[template.Tables.Count]; - DataTable[][] batches = new DataTable[batchSize][]; - - for (var i = 0; i < batches.Length; i++) - batches[i] = new DataTable[template.Tables.Count]; + //setting up bulk inserters + var tables = new DiscoveredTable[template.Tables.Count]; + var batches = new DataTable[batchSize][]; - IBulkCopy[][] uploaders= new IBulkCopy[batchSize][]; + for (var i = 0; i < batches.Length; i++) + batches[i] = new DataTable[template.Tables.Count]; - for (int i = 0; i < uploaders.Length; i++) - uploaders[i] = new IBulkCopy[template.Tables.Count]; + var uploaders= new IBulkCopy[batchSize][]; - string[] pks = new string[template.Tables.Count]; + for (var i = 0; i < uploaders.Length; i++) + uploaders[i] = new IBulkCopy[template.Tables.Count]; - for (var i = 0; i < template.Tables.Count; i++) - { - var tableSchema = template.Tables[i]; - var tbl = db.ExpectTable(tableSchema.TableName); - tables[i] = tbl; + var pks = new string?[template.Tables.Count]; - if (configDatabase.MakeDistinct) - { - var col = tableSchema.Columns.Where(c => c.IsPrimaryKey).ToArray(); + for (var i = 0; i < template.Tables.Count; i++) + { + var tableSchema = template.Tables[i]; + var tbl = db.ExpectTable(tableSchema.TableName); + tables[i] = tbl; - if (col.Length > 1) - Console.WriteLine("MakeDistinct only works with single column primary keys e.g. StudyInstanceUID / SeriesInstanceUID"); + if (configDatabase.MakeDistinct) + { + var col = tableSchema.Columns.Where(c => c.IsPrimaryKey).ToArray(); - pks[i] = col.SingleOrDefault()?.ColumnName; + if (col.Length > 1) + Console.WriteLine("MakeDistinct only works with single column primary keys e.g. StudyInstanceUID / SeriesInstanceUID"); - if (pks[i] != null) - { - //if it is sop instance uid then we shouldn't be trying to deduplicate - if (string.Equals(pks[i], neverDistinct, StringComparison.CurrentCultureIgnoreCase)) - pks[i] = null; - else - { - //we will make this a primary key later on - col.Single().IsPrimaryKey = false; - Console.WriteLine($"MakeDistinct will apply to '{pks[i]}' on '{tbl.GetFullyQualifiedName()}'"); - } - } - } + pks[i] = col.SingleOrDefault()?.ColumnName; - bool create = true; - - if (tbl.Exists()) + if (pks[i] != null) { - if (configDatabase.DropTables) - { - Console.WriteLine($"Dropping existing table '{tbl.GetFullyQualifiedName()}'"); - tbl.Drop(); - } + //if it is sop instance uid then we shouldn't be trying to deduplicate + if (string.Equals(pks[i], neverDistinct, StringComparison.CurrentCultureIgnoreCase)) + pks[i] = null; else { - Console.WriteLine($"Table '{tbl.GetFullyQualifiedName()}' already existed (so will not be created)"); - create = false; + //we will make this a primary key later on + col.Single().IsPrimaryKey = false; + Console.WriteLine($"MakeDistinct will apply to '{pks[i]}' on '{tbl.GetFullyQualifiedName()}'"); } } - - if(create) - { - Console.WriteLine($"About to create '{tbl.GetFullyQualifiedName()}'"); - creator.CreateTable(tbl, tableSchema); - Console.WriteLine($"Successfully created create '{tbl.GetFullyQualifiedName()}'"); - } + } - Console.WriteLine($"Creating uploader for '{tbl.GetRuntimeName()}''"); + var create = true; - for (int j = 0; j < batchSize; j++) + if (tbl.Exists()) + { + if (configDatabase.DropTables) { - //fetch schema - var dt = tbl.GetDataTable(); - dt.Rows.Clear(); - - batches[j][i] = dt; - uploaders[j][i] = tbl.BeginBulkInsert(); + Console.WriteLine($"Dropping existing table '{tbl.GetFullyQualifiedName()}'"); + tbl.Drop(); + } + else + { + Console.WriteLine($"Table '{tbl.GetFullyQualifiedName()}' already existed (so will not be created)"); + create = false; } } - var tasks = new Task[batchSize]; - - IPersonCollection identifiers = GetPeople(opts, out Random r); + + if(create) + { + Console.WriteLine($"About to create '{tbl.GetFullyQualifiedName()}'"); + creator.CreateTable(tbl, tableSchema); + Console.WriteLine($"Successfully created create '{tbl.GetFullyQualifiedName()}'"); + } - for (int i = 0; i < batchSize; i++) + Console.WriteLine($"Creating uploader for '{tbl.GetRuntimeName()}''"); + + for (var j = 0; j < batchSize; j++) { - var batch = i; - tasks[i] = new(() => // lgtm[cs/local-not-disposed] - { - RunBatch(identifiers,opts,r,batches[batch],uploaders[batch]); + //fetch schema + var dt = tbl.GetDataTable(); + dt.Rows.Clear(); - }); - tasks[i].Start(); + batches[j][i] = dt; + uploaders[j][i] = tbl.BeginBulkInsert(); } + } + var identifiers = GetPeople(opts, out var r); - Task.WaitAll(tasks); + Parallel.For(0, batchSize, i => RunBatch(identifiers, opts, r, batches[i], uploaders[i])); - swTotal.Stop(); + swTotal.Stop(); - for (var i = 0; i < tables.Length; i++) - { - if(pks[i] == null) - continue; + for (var i = 0; i < tables.Length; i++) + { + if(pks[i] == null) + continue; - Console.WriteLine( $"{DateTime.Now} Making table '{tables[i]}' distinct (this may take a long time)"); - var tbl = tables[i]; - tbl.MakeDistinct(500000000); + Console.WriteLine( $"{DateTime.Now} Making table '{tables[i]}' distinct (this may take a long time)"); + var tbl = tables[i]; + tbl.MakeDistinct(500000000); - Console.WriteLine( $"{DateTime.Now} Creating primary key on '{tables[i]}' of '{pks[i]}'"); - tbl.CreatePrimaryKey(500000000,tbl.DiscoverColumn(pks[i])); - } + Console.WriteLine( $"{DateTime.Now} Creating primary key on '{tables[i]}' of '{pks[i]}'"); + tbl.CreatePrimaryKey(500000000,tbl.DiscoverColumn(pks[i])); + } - Console.WriteLine("Final Row Counts:"); + Console.WriteLine("Final Row Counts:"); - foreach (DiscoveredTable t in tables) - Console.WriteLine($"{t.GetFullyQualifiedName()}: {t.GetRowCount():0,0}"); + foreach (var t in tables) + Console.WriteLine($"{t.GetFullyQualifiedName()}: {t.GetRowCount():0,0}"); - Console.WriteLine($"Total Running Time:{swTotal.Elapsed}"); - return 0; - } + Console.WriteLine($"Total Running Time:{swTotal.Elapsed}"); + return 0; + } - private static void RunBatch(IPersonCollection identifiers, ProgramOptions opts, Random r,DataTable[] batches, IBulkCopy[] uploaders) - { - Stopwatch swGeneration = new(); - Stopwatch swReading = new(); - Stopwatch swUploading = new(); + private static void RunBatch(IPersonCollection identifiers, ProgramOptions opts, Random r,DataTable[] batches, IBulkCopy[] uploaders) + { + Stopwatch swGeneration = new(); + Stopwatch swReading = new(); + Stopwatch swUploading = new(); - try + try + { + using var dicomGenerator = GetDataGenerator(opts,r, out _); + for (var i = 0; i < opts.NumberOfStudies; i++) { - using var dicomGenerator = GetDataGenerator(opts, identifiers,r, out _); - for (int i = 0; i < opts.NumberOfStudies; i++) - { - swGeneration.Start(); + swGeneration.Start(); - var p = identifiers.People[r.Next(identifiers.People.Length)]; - var ds = dicomGenerator.GenerateStudyImages(p,out Study s); + var p = identifiers.People[r.Next(identifiers.People.Length)]; + var ds = dicomGenerator.GenerateStudyImages(p,out _); - swGeneration.Stop(); + swGeneration.Stop(); - foreach (DicomDataset dataset in ds) - { - var rows = new DataRow[batches.Length]; + foreach (var dataset in ds) + { + var rows = new DataRow[batches.Length]; - for (int j = 0; j < batches.Length; j++) - rows[j] = batches[j].NewRow(); + for (var j = 0; j < batches.Length; j++) + rows[j] = batches[j].NewRow(); - swReading.Start(); - foreach (DicomItem item in dataset) - { - var column = DicomTypeTranslaterReader.GetColumnNameForTag(item.Tag, false); - var value = DicomTypeTranslater.Flatten(DicomTypeTranslaterReader.GetCSharpValue(dataset, item)); + swReading.Start(); + foreach (var item in dataset) + { + var column = DicomTypeTranslaterReader.GetColumnNameForTag(item.Tag, false); + var value = DicomTypeTranslater.Flatten(DicomTypeTranslaterReader.GetCSharpValue(dataset, item)); - foreach (DataRow row in rows) - { - if (row.Table.Columns.Contains(column)) - row[column] = value ?? DBNull.Value; - } - } + foreach (var row in rows.Where(row=>row.Table.Columns.Contains(column))) + row[column] = value ?? DBNull.Value; + } - for (int j = 0; j < batches.Length; j++) - batches[j].Rows.Add(rows[j]); + for (var j = 0; j < batches.Length; j++) + batches[j].Rows.Add(rows[j]); - swReading.Stop(); - } + swReading.Stop(); + } - //every 100 and last batch - if (i % 100 == 0 || i == opts.NumberOfStudies - 1) + //every 100 and last batch + if (i % 100 != 0 && i != opts.NumberOfStudies - 1) continue; + { + swUploading.Start(); + for (var j = 0; j < uploaders.Length; j++) { - swUploading.Start(); - for (var j = 0; j < uploaders.Length; j++) - { - uploaders[j].Upload(batches[j]); - batches[j].Rows.Clear(); - } - swUploading.Stop(); - Console.WriteLine($"{DateTime.Now} Done {i} studies"); + uploaders[j].Upload(batches[j]); + batches[j].Rows.Clear(); } - + swUploading.Stop(); + Console.WriteLine($"{DateTime.Now} Done {i} studies"); } + } - finally + } + finally + { + for (var i = 0; i < uploaders.Length; i++) { - for (var i = 0; i < uploaders.Length; i++) - { - uploaders[i].Dispose(); - batches[i].Dispose(); - } + uploaders[i].Dispose(); + batches[i].Dispose(); } + } - Console.WriteLine($"Total time Generating Dicoms:{swGeneration.Elapsed}"); - Console.WriteLine($"Total time Reading Dicoms:{swReading.Elapsed}"); - Console.WriteLine($"Total time Uploading Records:{swUploading.Elapsed}"); + Console.WriteLine($"Total time Generating Dicoms:{swGeneration.Elapsed}"); + Console.WriteLine($"Total time Reading Dicoms:{swReading.Elapsed}"); + Console.WriteLine($"Total time Uploading Records:{swUploading.Elapsed}"); - } } -} +} \ No newline at end of file diff --git a/BadDicom/ProgramOptions.cs b/BadDicom/ProgramOptions.cs index 8568c19..2ebd1d0 100644 --- a/BadDicom/ProgramOptions.cs +++ b/BadDicom/ProgramOptions.cs @@ -3,54 +3,53 @@ using CommandLine.Text; using System.Collections.Generic; -namespace BadDicom +namespace BadDicom; + +internal class ProgramOptions { - class ProgramOptions - { - [Value(0,HelpText = "Output directory to create CSV files in",Required=true)] - public string OutputDirectory { get; set; } + [Value(0,HelpText = "Output directory to create CSV files in",Required=true)] + public string? OutputDirectory { get; set; } - [Value(1, HelpText = "The number of unique patient identifiers to generate up front and then use in test data",Default = 500)] - public int NumberOfPatients { get; set; } = 500; + [Value(1, HelpText = "The number of unique patient identifiers to generate up front and then use in test data",Default = 500)] + public int NumberOfPatients { get; set; } = 500; - [Value(2, HelpText = "The number of dicom studies to generate (each study will have ", Default = 10)] - public int NumberOfStudies { get; set; } = 10; + [Value(2, HelpText = "The number of dicom studies to generate (each study will have ", Default = 10)] + public int NumberOfStudies { get; set; } = 10; - [Option('s', HelpText = "Seeds the random number generator with a specific number", Default = -1)] - public int Seed { get; set; } = -1; + [Option('s', HelpText = "Seeds the random number generator with a specific number", Default = -1)] + public int Seed { get; set; } = -1; - [Value(3, HelpText = "Comma separated list of modalities to generate from", Default = "CT")] - public string Modalities { get; set; } = "CT"; + [Value(3, HelpText = "Comma separated list of modalities to generate from", Default = "CT")] + public string Modalities { get; set; } = "CT"; - [Option("NoPixels",HelpText= "Generate dicom files without pixel data (only tags). This results in much smaller file sizes")] - public bool NoPixels{get;set;} + [Option("NoPixels",HelpText= "Generate dicom files without pixel data (only tags). This results in much smaller file sizes")] + public bool NoPixels{get;set;} - [Option('a',"Anonymise",HelpText= "Generate anonymous dicom files")] - public bool Anonymise { get;set;} + [Option('a',"Anonymise",HelpText= "Generate anonymous dicom files")] + public bool Anonymise { get;set;} - [Option("csv",HelpText= "Generate CSV files to be ingested in a database. This results in no dicom images being generated (i.e. only csv tag data in flat files)")] - public bool csv{get;set;} + [Option("csv",HelpText= "Generate CSV files to be ingested in a database. This results in no dicom images being generated (i.e. only csv tag data in flat files)")] + public bool csv{get;set;} - [Option('l',"Layout",HelpText= "The file system layout to use, defaults to Flat",Default = FileSystemLayout.StudyYearMonthDay)] - public FileSystemLayout Layout{get;set;} = FileSystemLayout.StudyYearMonthDay; + [Option('l',"Layout",HelpText= "The file system layout to use, defaults to Flat",Default = FileSystemLayout.StudyYearMonthDay)] + public FileSystemLayout Layout{get;set;} = FileSystemLayout.StudyYearMonthDay; - [Option('m',"MaxImages",HelpText= "The maximum number of images to generate (regardless of NumberOfStudies)",Default = int.MaxValue)] - public int MaximumImages { get; set; } = int.MaxValue; + [Option('m',"MaxImages",HelpText= "The maximum number of images to generate (regardless of NumberOfStudies)",Default = int.MaxValue)] + public int MaximumImages { get; set; } = int.MaxValue; - [Usage] - public static IEnumerable Examples + [Usage] + public static IEnumerable Examples + { + get { - get - { - yield return - new Example("Generate test data", - new ProgramOptions { OutputDirectory = @"c:/temp" }); - - yield return - new Example("Generate a custom amount of data", new ProgramOptions { OutputDirectory = @"c:/temp",NumberOfPatients = 5000, NumberOfStudies = 20000}); + yield return + new Example("Generate test data", + new ProgramOptions { OutputDirectory = @"c:/temp" }); + + yield return + new Example("Generate a custom amount of data", new ProgramOptions { OutputDirectory = @"c:/temp",NumberOfPatients = 5000, NumberOfStudies = 20000}); - } } + } -} -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj b/BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj index 408a937..607e377 100644 --- a/BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj +++ b/BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -14,13 +14,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/BadMedicine.Dicom.Tests/DicomDataGeneratorTests.cs b/BadMedicine.Dicom.Tests/DicomDataGeneratorTests.cs index c683bc6..ec871ac 100644 --- a/BadMedicine.Dicom.Tests/DicomDataGeneratorTests.cs +++ b/BadMedicine.Dicom.Tests/DicomDataGeneratorTests.cs @@ -6,175 +6,165 @@ using System.Linq; using CsvHelper; -namespace BadMedicine.Dicom.Tests +namespace BadMedicine.Dicom.Tests; + +public class DicomDataGeneratorTests { - public class DicomDataGeneratorTests + [Test] + public void Test_CreatingOnDisk_OneFile() { - [Test] - public void Test_CreatingOnDisk_OneFile() - { - var r = new Random(500); - var generator = new DicomDataGenerator(r, TestContext.CurrentContext.WorkDirectory) {Layout = FileSystemLayout.StudyUID, MaximumImages = 1}; + var r = new Random(500); + using var generator = new DicomDataGenerator(r, TestContext.CurrentContext.WorkDirectory) {Layout = FileSystemLayout.StudyUID, MaximumImages = 1}; - var person = new Person(r); + var person = new Person(r); - //generates a study but because of maximum images 1 we should only get 1 image being generated - string studyUid = (string)generator.GenerateTestDataRow(person)[0]; + //generates a study but because of maximum images 1 we should only get 1 image being generated + var studyUid = (string)generator.GenerateTestDataRow(person)[0]; - //should be a directory named after the Study UID - Assert.IsTrue(Directory.Exists(Path.Combine(TestContext.CurrentContext.WorkDirectory, studyUid))); + //should be a directory named after the Study UID + Assert.IsTrue(Directory.Exists(Path.Combine(TestContext.CurrentContext.WorkDirectory, studyUid))); - //should be a single file - var f = new FileInfo(Directory.GetFiles(Path.Combine(TestContext.CurrentContext.WorkDirectory, studyUid)).Single()); - Assert.IsTrue(f.Exists); + //should be a single file + var f = new FileInfo(Directory.GetFiles(Path.Combine(TestContext.CurrentContext.WorkDirectory, studyUid)).Single()); + Assert.IsTrue(f.Exists); - var datasetCreated = DicomFile.Open(f.FullName); + var datasetCreated = DicomFile.Open(f.FullName); - Assert.AreEqual(studyUid, + Assert.AreEqual(studyUid, datasetCreated.Dataset.GetValues(DicomTag.StudyInstanceUID)[0].UID, "UID in the dicom file generated did not match the one output into the CSV inventory file" - ); + ); - Assert.IsNotEmpty(datasetCreated.Dataset.GetSingleValue(DicomTag.AccessionNumber)); - - Console.WriteLine($"Created file {f.FullName}"); - - generator.Dispose(); - } + Assert.IsNotEmpty(datasetCreated.Dataset.GetSingleValue(DicomTag.AccessionNumber)); + } - [Test] - public void ExampleUsage() - { - //create a test person - var r = new Random(23); - var person = new Person(r); - - //create a generator - using var generator = new DicomDataGenerator(r, null, "CT"); - //create a dataset in memory - DicomDataset dataset = generator.GenerateTestDataset(person, r); - - //values should match the patient details - Assert.AreEqual(person.CHI,dataset.GetValue(DicomTag.PatientID,0)); - Assert.GreaterOrEqual(dataset.GetValue(DicomTag.StudyDate,0),person.DateOfBirth); - - //should have a study description - Assert.IsNotNull(dataset.GetValue(DicomTag.StudyDescription,0)); - //should have a study description - Assert.IsNotNull(dataset.GetSingleValue(DicomTag.StudyTime).TimeOfDay); - } + [Test] + public void ExampleUsage() + { + //create a test person + var r = new Random(23); + var person = new Person(r); + + //create a generator + using var generator = new DicomDataGenerator(r, null, "CT"); + //create a dataset in memory + var dataset = generator.GenerateTestDataset(person, r); + + //values should match the patient details + Assert.AreEqual(person.CHI,dataset.GetValue(DicomTag.PatientID,0)); + Assert.GreaterOrEqual(dataset.GetValue(DicomTag.StudyDate,0),person.DateOfBirth); + + //should have a study description + Assert.IsNotNull(dataset.GetValue(DicomTag.StudyDescription,0)); + //should have a study description + Assert.IsNotNull(dataset.GetSingleValue(DicomTag.StudyTime).TimeOfDay); + } - [Test] - public void Test_CreatingInMemory_ModalityCT() - { - var r = new Random(23); - var person = new Person(r); - using var generator = new DicomDataGenerator(r,new(TestContext.CurrentContext.WorkDirectory),"CT") {NoPixels = true}; + [Test] + public void Test_CreatingInMemory_ModalityCT() + { + var r = new Random(23); + var person = new Person(r); + using var generator = new DicomDataGenerator(r,new string(TestContext.CurrentContext.WorkDirectory),"CT") {NoPixels = true}; - //generate 100 images - for(int i = 0 ; i < 100 ; i++) - { - //all should be CT because we said CT only - var ds = generator.GenerateTestDataset(person, r); - Assert.AreEqual("CT",ds.GetSingleValue(DicomTag.Modality)); - } + //generate 100 images + for(var i = 0 ; i < 100 ; i++) + { + //all should be CT because we said CT only + var ds = generator.GenerateTestDataset(person, r); + Assert.AreEqual("CT",ds.GetSingleValue(DicomTag.Modality)); } + } - [Test] - public void Test_Anonymise() - { - var r = new Random(23); - var person = new Person(r); + [Test] + public void Test_Anonymise() + { + var r = new Random(23); + var person = new Person(r); - var generator = new DicomDataGenerator(r,new(TestContext.CurrentContext.WorkDirectory),"CT"); + using var generator = new DicomDataGenerator(r,new string(TestContext.CurrentContext.WorkDirectory),"CT"); - // without anonymisation (default) we get the normal patient ID - var ds = generator.GenerateTestDataset(person, r); + // without anonymisation (default) we get the normal patient ID + var ds = generator.GenerateTestDataset(person, r); - Assert.IsTrue(ds.Contains(DicomTag.PatientID)); - Assert.AreEqual(person.CHI,ds.GetValue(DicomTag.PatientID,0)); + Assert.IsTrue(ds.Contains(DicomTag.PatientID)); + Assert.AreEqual(person.CHI,ds.GetValue(DicomTag.PatientID,0)); - // with anonymisation - generator.Anonymise = true; + // with anonymisation + generator.Anonymise = true; - var ds2 = generator.GenerateTestDataset(person, r); + var ds2 = generator.GenerateTestDataset(person, r); - // we get a blank patient ID - Assert.IsTrue(ds2.Contains(DicomTag.PatientID)); - Assert.AreEqual(string.Empty,ds2.GetString(DicomTag.PatientID)); + // we get a blank patient ID + Assert.IsTrue(ds2.Contains(DicomTag.PatientID)); + Assert.AreEqual(string.Empty,ds2.GetString(DicomTag.PatientID)); + } + [Test] + public void Test_CreatingInMemory_Modality_CTAndMR() + { + var r = new Random(23); + var person = new Person(r); - generator.Dispose(); + using var generator = new DicomDataGenerator(r,new string(TestContext.CurrentContext.WorkDirectory),"CT","MR"); - } - [Test] - public void Test_CreatingInMemory_Modality_CTAndMR() + //generate 100 images + for(var i = 0 ; i < 100 ; i++) { - var r = new Random(23); - var person = new Person(r); - - var generator = new DicomDataGenerator(r,new(TestContext.CurrentContext.WorkDirectory),"CT","MR"); - - //generate 100 images - for(int i = 0 ; i < 100 ; i++) - { - //all should be CT because we said CT only - var ds = generator.GenerateTestDataset(person, r); - var modality = ds.GetSingleValue(DicomTag.Modality); - - Assert.IsTrue(modality == "CT" || modality == "MR","Unexpected modality {0}",modality); - } + //all should be CT because we said CT only + var ds = generator.GenerateTestDataset(person, r); + var modality = ds.GetSingleValue(DicomTag.Modality); - generator.Dispose(); + Assert.IsTrue(modality is "CT" or "MR","Unexpected modality {0}",modality); } + } - [Test] - public void TestFail_CreatingInMemory_Modality_Unknown() - { - var r = new Random(23); - Assert.Throws(()=>new DicomDataGenerator(r,new(TestContext.CurrentContext.WorkDirectory),"LOLZ")); + [Test] + public void TestFail_CreatingInMemory_Modality_Unknown() + { + var r = new Random(23); + Assert.Throws(()=>_=new DicomDataGenerator(r,new string(TestContext.CurrentContext.WorkDirectory),"LOLZ")); - } + } - [Test] - public void Test_CsvOption() - { - var r = new Random(500); + [Test] + public void Test_CsvOption() + { + var r = new Random(500); - var outputDir = new DirectoryInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory,nameof(Test_CsvOption))); - if (outputDir.Exists) - outputDir.Delete(true); - outputDir.Create(); + var outputDir = new DirectoryInfo(Path.Combine(TestContext.CurrentContext.WorkDirectory,nameof(Test_CsvOption))); + if (outputDir.Exists) + outputDir.Delete(true); + outputDir.Create(); - var people = new PersonCollection(); - people.GeneratePeople(100,r); + var people = new PersonCollection(); + people.GeneratePeople(100,r); - using (var generator = new DicomDataGenerator(r,outputDir.FullName, "CT")) - { - generator.Csv = true; - generator.NoPixels = true; - generator.MaximumImages = 500; + using (var generator = new DicomDataGenerator(r,outputDir.FullName, "CT")) + { + generator.Csv = true; + generator.NoPixels = true; + generator.MaximumImages = 500; - generator.GenerateTestDataFile(people,new(Path.Combine(outputDir.FullName,"index.csv")),500); - } + generator.GenerateTestDataFile(people,new FileInfo(Path.Combine(outputDir.FullName,"index.csv")),500); + } - //3 csv files + index.csv (the default one - Assert.AreEqual(4,outputDir.GetFiles().Length); + //3 csv files + index.csv (the default one + Assert.AreEqual(4,outputDir.GetFiles().Length); - foreach (FileInfo f in outputDir.GetFiles()) - { - using var reader = new CsvReader(new StreamReader(f.FullName),CultureInfo.CurrentCulture); - int rowcount = 0; + foreach (var f in outputDir.GetFiles()) + { + using var reader = new CsvReader(new StreamReader(f.FullName),CultureInfo.CurrentCulture); + var rowcount = 0; - //confirms that the CSV is intact (no dodgy commas, unquoted newlines etc) - while (reader.Read()) - rowcount++; + //confirms that the CSV is intact (no dodgy commas, unquoted newlines etc) + while (reader.Read()) + rowcount++; - //should be 1 row per image + 1 for header - if(f.Name == DicomDataGenerator.ImageCsvFilename) - Assert.AreEqual(501,rowcount); - } + //should be 1 row per image + 1 for header + if(f.Name == DicomDataGenerator.ImageCsvFilename) + Assert.AreEqual(501,rowcount); } } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom.Tests/NuspecIsCorrectTests.cs b/BadMedicine.Dicom.Tests/NuspecIsCorrectTests.cs deleted file mode 100644 index 2ca7148..0000000 --- a/BadMedicine.Dicom.Tests/NuspecIsCorrectTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) The University of Dundee 2018-2019 -// This file is part of the Research Data Management Platform (RDMP). -// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -// You should have received a copy of the GNU General Public License along with RDMP. If not, see . - -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using NUnit.Framework; - -namespace BadMedicine.Dicom.Tests -{ - /// - /// Tests to confirm that the dependencies in csproj files (NuGet packages) match those in the .nuspec files and that packages.md - /// lists the correct versions (in documentation) - /// - class NuspecIsCorrectTests - { - static readonly string[] Analyzers = new string[] { "SecurityCodeScan","Microsoft.SourceLink.GitHub" }; - - [TestCase("../../../../BadMedicine.Dicom/BadMedicine.Dicom.csproj", null, "../../../../Packages.md")] - [TestCase("../../../../BadDicom/BadDicom.csproj", null, "../../../../Packages.md")] - [TestCase("../../../../BadMedicine.Dicom.Tests/BadMedicine.Dicom.Tests.csproj", null, "../../../../Packages.md")] - public void TestDependencyCorrect(string csproj, string nuspec, string packagesMarkdown) - { - if(csproj != null && !Path.IsPathRooted(csproj)) - csproj = Path.Combine(TestContext.CurrentContext.TestDirectory,csproj); - if(nuspec != null && !Path.IsPathRooted(nuspec)) - nuspec = Path.Combine(TestContext.CurrentContext.TestDirectory,nuspec); - if(packagesMarkdown != null && !Path.IsPathRooted(packagesMarkdown)) - packagesMarkdown = Path.Combine(TestContext.CurrentContext.TestDirectory,packagesMarkdown); - - if (!File.Exists(csproj)) - Assert.Fail("Could not find file {0}", csproj); - if (nuspec != null && !File.Exists(nuspec)) - Assert.Fail("Could not find file {0}", nuspec); - - if (packagesMarkdown != null && !File.Exists(packagesMarkdown)) - Assert.Fail("Could not find file {0}", packagesMarkdown); - - // - Regex rPackageRef = new(@" - Regex rDependencyRef = new(@""; - } - - private static object BuildRecommendedMarkdownLine(string package, string version) - { - return - $"| {package} | [GitHub]() | [{version}](https://www.nuget.org/packages/{package}/{version}) | | | |"; - } - } -} diff --git a/BadMedicine.Dicom.Tests/PackageListIsCorrectTests.cs b/BadMedicine.Dicom.Tests/PackageListIsCorrectTests.cs new file mode 100644 index 0000000..51f2e57 --- /dev/null +++ b/BadMedicine.Dicom.Tests/PackageListIsCorrectTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using NUnit.Framework; + +namespace BadMedicine.Dicom.Tests; + +/// +/// Tests to confirm that the dependencies in csproj files (NuGet packages) match those in the .nuspec files and that packages.md +/// lists the correct versions (in documentation) +/// +public class PackageListIsCorrectTests +{ + private static readonly EnumerationOptions EnumerationOptions = new() { RecurseSubdirectories = true,MatchCasing = MatchCasing.CaseInsensitive,IgnoreInaccessible = true}; + + // + private static readonly Regex RPackageRef = new(@" + /// Enumerate non-test packages, check that they are listed in PACKAGES.md + /// + /// + [TestCase] + public void TestPackagesDocumentCorrect(string rootPath=null) + { + var root= FindRoot(rootPath); + var undocumented = new StringBuilder(); + + // Extract the named packages from PACKAGES.md + var packagesMarkdown = File.ReadAllLines(GetPackagesMarkdown(root)) + .Select(line => RMarkdownEntry.Match(line)) + .Where(m=>m.Success) + .Select(m => m.Groups[1].Value) + .ToHashSet(StringComparer.InvariantCultureIgnoreCase); + + // Extract the named packages from csproj files, then subtract those listed in PACKAGES.md (should be empty) + var undocumentedPackages = GetCsprojFiles(root).Select(File.ReadAllText).SelectMany(s => RPackageRef.Matches(s)) + .Select(m=>m.Groups[1].Value).Except(packagesMarkdown).Select(BuildRecommendedMarkdownLine); + undocumented.AppendJoin(Environment.NewLine, undocumentedPackages); + + Assert.IsEmpty(undocumented.ToString()); + } + + /// + /// Generate the report entry for an undocumented package + /// + /// + /// + private static object BuildRecommendedMarkdownLine(string package) => $"Package {package} is not documented in PACKAGES.md. Recommended line is:\r\n| {package} | [GitHub]() | LICENCE GOES HERE | |"; + + /// + /// Find the root of this repo, which is usually the directory containing the .sln file + /// If the .sln file lives elsewhere, you can override this by passing in a path explicitly. + /// + /// + /// + private static DirectoryInfo FindRoot(string path = null) + { + if (path != null) + { + if (!Path.IsPathRooted(path)) path = Path.Combine(TestContext.CurrentContext.TestDirectory, path); + return new DirectoryInfo(path); + } + var root = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + while (!root.EnumerateFiles("*.sln", SearchOption.TopDirectoryOnly).Any() && root.Parent != null) + root = root.Parent; + Assert.IsNotNull(root.Parent, "Could not find root of repository"); + return root; + } + + /// + /// Returns all csproj files in the repository, except those containing the string 'tests' + /// + /// + /// + private static IEnumerable GetCsprojFiles(DirectoryInfo root) + { + return root.EnumerateFiles("*.csproj", EnumerationOptions).Select(f => f.FullName).Where(f => !f.Contains("tests", StringComparison.InvariantCultureIgnoreCase)); + } + + /// + /// Find the sole packages.md file wherever in the repo it lives. Error if multiple or none. + /// + /// + /// + private static string GetPackagesMarkdown(DirectoryInfo root) + { + var path = root.EnumerateFiles("packages.md", EnumerationOptions).Select(f => f.FullName).SingleOrDefault(); + Assert.IsNotNull(path, "Could not find packages.md"); + return path; + } + +} diff --git a/BadMedicine.Dicom.Tests/StudyTests.cs b/BadMedicine.Dicom.Tests/StudyTests.cs index b0ff0d4..3123975 100644 --- a/BadMedicine.Dicom.Tests/StudyTests.cs +++ b/BadMedicine.Dicom.Tests/StudyTests.cs @@ -2,54 +2,53 @@ using NUnit.Framework; using System; -namespace BadMedicine.Dicom.Tests +namespace BadMedicine.Dicom.Tests; + +internal class StudyTests { - class StudyTests + [Test] + public void Test_CreatingNewStudy_HasSomeImages() { - [Test] - public void Test_CreatingNewStudy_HasSomeImages() - { - var r = new Random(100); + var r = new Random(100); - using var generator = new DicomDataGenerator(r,null) {NoPixels = true}; + using var generator = new DicomDataGenerator(r,null) {NoPixels = true}; - var p = new Person(r); + var p = new Person(r); - Study study = new(generator,p,new("MR",2,0,50,0,r),r); + Study study = new(generator,p,new ModalityStats("MR",2,0,50,0,r),r); - Assert.AreEqual(2,study.Series.Count); - Assert.AreEqual(50,study.Series[0].Datasets.Count); + Assert.AreEqual(2,study.Series.Count); + Assert.AreEqual(50,study.Series[0].Datasets.Count); - foreach(DicomDataset ds in study.Series[0]) - { - Assert.AreEqual("MR",ds.GetValues(DicomTag.Modality)[0]); - Assert.AreEqual(study.StudyTime,ds.GetSingleValue(DicomTag.StudyTime).TimeOfDay); - } + foreach(var ds in study.Series[0]) + { + Assert.AreEqual("MR",ds.GetValues(DicomTag.Modality)[0]); + Assert.AreEqual(study.StudyTime,ds.GetSingleValue(DicomTag.StudyTime).TimeOfDay); } + } - [Test] - public void Test_UsingExplicitUIDs() - { - UIDAllocator.StudyUIDs.Enqueue("999"); - UIDAllocator.SeriesUIDs.Enqueue("888"); - UIDAllocator.SOPUIDs.Enqueue("777"); + [Test] + public void Test_UsingExplicitUIDs() + { + UIDAllocator.StudyUIDs.Enqueue("999"); + UIDAllocator.SeriesUIDs.Enqueue("888"); + UIDAllocator.SOPUIDs.Enqueue("777"); - var r = new Random(100); + var r = new Random(100); - using var generator = new DicomDataGenerator(r, null) {NoPixels = true}; + using var generator = new DicomDataGenerator(r, null) {NoPixels = true}; - var p = new Person(r); + var p = new Person(r); - Study study = new(generator, p, new("MR", 2, 0, 50, 0, r), r); + Study study = new(generator, p, new ModalityStats("MR", 2, 0, 50, 0, r), r); - Assert.AreEqual("999", study.StudyUID.UID); - Assert.AreEqual("888", study.Series[0].SeriesUID.UID); + Assert.AreEqual("999", study.StudyUID.UID); + Assert.AreEqual("888", study.Series[0].SeriesUID.UID); - var image1 = study.Series[0].Datasets[0]; - Assert.AreEqual("999", image1.GetSingleValue(DicomTag.StudyInstanceUID).UID); - Assert.AreEqual("888", image1.GetSingleValue(DicomTag.SeriesInstanceUID).UID); - Assert.AreEqual("777", image1.GetSingleValue(DicomTag.SOPInstanceUID).UID); - } + var image1 = study.Series[0].Datasets[0]; + Assert.AreEqual("999", image1.GetSingleValue(DicomTag.StudyInstanceUID).UID); + Assert.AreEqual("888", image1.GetSingleValue(DicomTag.SeriesInstanceUID).UID); + Assert.AreEqual("777", image1.GetSingleValue(DicomTag.SOPInstanceUID).UID); } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom.sln b/BadMedicine.Dicom.sln index 00149ad..2fd8979 100644 --- a/BadMedicine.Dicom.sln +++ b/BadMedicine.Dicom.sln @@ -11,7 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BadDicom", "BadDicom\BadDic EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9C7F03FC-AB21-48FB-B9B1-F8E84EA32E4D}" ProjectSection(SolutionItems) = preProject - BadMedicine.Dicom\BadMedicine.Dicom.nuspec = BadMedicine.Dicom\BadMedicine.Dicom.nuspec CHANGELOG.md = CHANGELOG.md Packages.md = Packages.md README.md = README.md diff --git a/BadMedicine.Dicom/BadMedicine.Dicom.csproj b/BadMedicine.Dicom/BadMedicine.Dicom.csproj index c1878c4..ca744e8 100644 --- a/BadMedicine.Dicom/BadMedicine.Dicom.csproj +++ b/BadMedicine.Dicom/BadMedicine.Dicom.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -10,15 +10,15 @@ HIC.BadMedicine.Dicom Health Informatics Centre - University of Dundee Generate large volumes of complex (in terms of tags) DICOM images for integration/stress testing ETL and image management tools. BadMedicine.Dicom generates DICOM images on demand based on an anonymous aggregate model of tag data found in Scottish medical imaging with a small memory footprint. - https://github.com/HicServices/BadMedicine.Dicom + https://github.com/SMI/BadMedicine.Dicom GPL-3.0-or-later Copyright 2019 DICOM,Test Data,Random,Synthetic Data,Health + enable - - + @@ -40,9 +40,9 @@ - - - + + + diff --git a/BadMedicine.Dicom/DescBodyPart.cs b/BadMedicine.Dicom/DescBodyPart.cs index 8ef2d6d..ef82e5f 100644 --- a/BadMedicine.Dicom/DescBodyPart.cs +++ b/BadMedicine.Dicom/DescBodyPart.cs @@ -1,39 +1,38 @@ using FellowOakDicom; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// +/// Describes a commonly seen occurrence of a given triplet of values +/// , and +/// in scottish medical imaging data. +/// +/// +/// This class (and its corresponding DicomDataGeneratorDescBodyPart.csv) allow +/// synthetic data in the description tags to make sense when comparing to the other +/// 2 tags listed. It prevents for example a study being generated called CT Head with +/// a Series Description of 'Foot Scan' +/// +/// +/// +public class DescBodyPart { /// - /// - /// Describes a commonly seen occurance of a given triplet of values - /// , and - /// in scottish medical imaging data. - /// - /// - /// This class (and its corresponding DicomDataGeneratorDescBodyPart.csv) allow - /// synthetic data in the description tags to make sense when comparing to the other - /// 2 tags listed. It prevents for example a study being generated called CT Head with - /// a Series Description of 'Foot Scan' - /// - /// + /// A known value of which is consistent with + /// and (of this class) /// - public class DescBodyPart - { - /// - /// A known value of which is consistent with - /// and (of this class) - /// - public string StudyDescription { get; set; } + public string? StudyDescription { get; init; } - /// - /// A known value of which is consistent with - /// and (of this class) - /// - public string BodyPartExamined { get; set; } + /// + /// A known value of which is consistent with + /// and (of this class) + /// + public string? BodyPartExamined { get; init; } - /// - /// A known value of which is consistent with - /// and (of this class) - /// - public string SeriesDescription { get; set; } - } + /// + /// A known value of which is consistent with + /// and (of this class) + /// + public string? SeriesDescription { get; init; } } \ No newline at end of file diff --git a/BadMedicine.Dicom/DicomDataGenerator.cs b/BadMedicine.Dicom/DicomDataGenerator.cs index 3efc766..7e9e3f2 100644 --- a/BadMedicine.Dicom/DicomDataGenerator.cs +++ b/BadMedicine.Dicom/DicomDataGenerator.cs @@ -8,445 +8,426 @@ using System.Runtime.InteropServices; using CsvHelper; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// which produces dicom files on disk and accompanying metadata +/// +public class DicomDataGenerator : DataGenerator,IDisposable { /// - /// which produces dicom files on disk and accompanying metadata + /// Location on disk to output dicom files to /// - public class DicomDataGenerator : DataGenerator,IDisposable - { - /// - /// Location on disk to output dicom files to - /// - public DirectoryInfo OutputDir { get; } - - /// - /// Set to true to generate without any pixel data. - /// - public bool NoPixels { get; set; } - - /// - /// Set to true to discard the generated DICOM files, usually for testing. - /// - private bool DevNull { get; } - - private static readonly string DevNullPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)?"NUL":"/dev/null"; - - /// - /// Set to true to run on the generated before writting to disk. - /// - public bool Anonymise {get;set;} - - /// - /// True to output Study / Series / Image level CSV files containing all the tag data. Setting this option - /// disables image file output - /// - public bool Csv { get; set; } - - /// - /// The subdirectories layout to put dicom files into when writting to disk - /// - public FileSystemLayout Layout{ - get => _pathProvider.Layout; - set => _pathProvider = new(value); - } + public DirectoryInfo? OutputDir { get; } + + /// + /// Set to true to generate without any pixel data. + /// + public bool NoPixels { get; set; } + + /// + /// Set to true to discard the generated DICOM files, usually for testing. + /// + private bool DevNull { get; } + + private static readonly string DevNullPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)?"NUL":"/dev/null"; + + /// + /// Set to true to run on the generated before writing to disk. + /// + public bool Anonymise {get;set;} + + /// + /// True to output Study / Series / Image level CSV files containing all the tag data. Setting this option + /// disables image file output + /// + public bool Csv { get; set; } + + /// + /// The subdirectories layout to put dicom files into when writing to disk + /// + public FileSystemLayout Layout{ + get => _pathProvider.Layout; + set => _pathProvider = new FileSystemLayoutProvider(value); + } - /// - /// The maximum number of images to generate regardless of how many calls to , Defaults to int.MaxValue - /// - public int MaximumImages { get; set; } = int.MaxValue; - - private FileSystemLayoutProvider _pathProvider = new(FileSystemLayout.StudyYearMonthDay); - - readonly PixelDrawer drawing = new(); - - private readonly int[] _modalities; - - private List _studyTags; - private List _seriesTags; - private List _imageTags; - private string _lastStudyUID = ""; - private string _lastSeriesUID = ""; - private CsvWriter studyWriter, seriesWriter, imageWriter; - private DicomAnonymizer _anonymizer = new(); - - /// - /// Name of the file that contains distinct Study level records for all images when is true - /// - public const string StudyCsvFilename = "study.csv"; - - /// - /// Name of the file that contains distinct Series level records for all images when is true - /// - public const string SeriesCsvFilename = "series.csv"; - - /// - /// Name of the file that contains distinct Image level records for all images when is true - /// - public const string ImageCsvFilename = "image.csv"; - - private bool csvInitialized = false; - - /// - /// - /// - /// - /// - /// List of modalities to generate from e.g. CT,MR. The frequency of images generated is based on - /// the popularity of that modality in a clinical PACS. Passing nothing results in all supported modalities being generated - public DicomDataGenerator(Random r, string outputDir, params string[] modalities):base(r) + /// + /// The maximum number of images to generate regardless of how many calls to , Defaults to int.MaxValue + /// + public int MaximumImages { get; set; } = int.MaxValue; + + private FileSystemLayoutProvider _pathProvider = new(FileSystemLayout.StudyYearMonthDay); + + private readonly int[] _modalities; + + private static readonly List StudyTags = new() + { + DicomTag.PatientID, + DicomTag.StudyInstanceUID, + DicomTag.StudyDate, + DicomTag.StudyTime, + DicomTag.ModalitiesInStudy, + DicomTag.StudyDescription, + DicomTag.PatientAge, + DicomTag.NumberOfStudyRelatedInstances, + DicomTag.PatientBirthDate + }; + private static readonly List SeriesTags = new() + { + DicomTag.StudyInstanceUID, + DicomTag.SeriesInstanceUID, + DicomTag.SeriesDate, + DicomTag.SeriesTime, + DicomTag.Modality, + DicomTag.ImageType, + DicomTag.SourceApplicationEntityTitle, + DicomTag.InstitutionName, + DicomTag.ProcedureCodeSequence, + DicomTag.ProtocolName, + DicomTag.PerformedProcedureStepID, + DicomTag.PerformedProcedureStepDescription, + DicomTag.SeriesDescription, + DicomTag.BodyPartExamined, + DicomTag.DeviceSerialNumber, + DicomTag.NumberOfSeriesRelatedInstances, + DicomTag.SeriesNumber + }; + private static readonly List ImageTags= new() { - DevNull = outputDir?.Equals("/dev/null", StringComparison.InvariantCulture)??true; - OutputDir = DevNull ? null : Directory.CreateDirectory(outputDir); - - var stats = DicomDataGeneratorStats.GetInstance(r); + DicomTag.SeriesInstanceUID, + DicomTag.SOPInstanceUID, + DicomTag.BurnedInAnnotation, + DicomTag.SliceLocation, + DicomTag.SliceThickness, + DicomTag.SpacingBetweenSlices, + DicomTag.SpiralPitchFactor, + DicomTag.KVP, + DicomTag.ExposureTime, + DicomTag.Exposure, + DicomTag.ManufacturerModelName, + DicomTag.Manufacturer, + DicomTag.XRayTubeCurrent, + DicomTag.PhotometricInterpretation, + DicomTag.ContrastBolusRoute, + DicomTag.ContrastBolusAgent, + DicomTag.AcquisitionNumber, + DicomTag.AcquisitionDate, + DicomTag.AcquisitionTime, + DicomTag.ImagePositionPatient, + DicomTag.PixelSpacing, + DicomTag.FieldOfViewDimensions, + DicomTag.FieldOfViewDimensionsInFloat, + DicomTag.DerivationDescription, + DicomTag.TransferSyntaxUID, + DicomTag.LossyImageCompression, + DicomTag.LossyImageCompressionMethod, + DicomTag.LossyImageCompressionRatio, + DicomTag.ScanOptions + }; + private string _lastStudyUID = ""; + private string _lastSeriesUID = ""; + private CsvWriter? _studyWriter, _seriesWriter, _imageWriter; + private readonly DicomAnonymizer _anonymiser = new(); - if(modalities.Length == 0) - { - _modalities = stats.ModalityIndexes.Values.ToArray(); - } - else - { - foreach(var m in modalities) - { - if(!stats.ModalityIndexes.ContainsKey(m)) - throw new ArgumentException( - $"Modality '{m}' was not supported, supported modalities are:{string.Join(",", stats.ModalityIndexes.Select(kvp => kvp.Key))}"); - } + /// + /// Name of the file that contains distinct Study level records for all images when is true + /// + public const string StudyCsvFilename = "study.csv"; - _modalities = modalities.Select(m=>stats.ModalityIndexes[m]).ToArray(); - } + /// + /// Name of the file that contains distinct Series level records for all images when is true + /// + public const string SeriesCsvFilename = "series.csv"; + + /// + /// Name of the file that contains distinct Image level records for all images when is true + /// + public const string ImageCsvFilename = "image.csv"; + + private bool csvInitialized; + + /// + /// + /// + /// + /// + /// List of modalities to generate from e.g. CT,MR. The frequency of images generated is based on + /// the popularity of that modality in a clinical PACS. Passing nothing results in all supported modalities being generated + public DicomDataGenerator(Random r, string? outputDir, params string[] modalities):base(r) + { + DevNull = outputDir?.Equals("/dev/null", StringComparison.InvariantCulture)!=false; + OutputDir = DevNull ? null : Directory.CreateDirectory(outputDir!); + + var stats = DicomDataGeneratorStats.GetInstance(); + + if(modalities.Length == 0) + { + _modalities = stats.ModalityIndexes.Values.ToArray(); } + else + { + if (modalities.Any(m => !stats.ModalityIndexes.ContainsKey(m))) + throw new ArgumentException( + $"Modality '{string.Join(',',modalities.Except(stats.ModalityIndexes.Keys))}' not supported, supported modalities are:{string.Join(",", stats.ModalityIndexes.Keys)}"); + _modalities = modalities.Select(m=>stats.ModalityIndexes[m]).ToArray(); + } + } - /// - /// Creates a new dicom dataset - /// - /// - /// - public override object[] GenerateTestDataRow(Person p) + /// + /// Creates a new dicom dataset + /// + /// + /// + public override object?[] GenerateTestDataRow(Person p) + { + if(!csvInitialized && Csv) + InitialiseCSVOutput(); + + //The currently extracting study + string? studyUID = null; + + foreach (var ds in GenerateStudyImages(p, out var study)) { - if(!csvInitialized && Csv) - InitialiseCSVOutput(); + //don't generate more than the maximum number of images + if (MaximumImages-- <= 0) + { + break; + } - //The currently extracting study - string studyUID = null; + studyUID = study.StudyUID.UID; //all images will have the same study - foreach (var ds in GenerateStudyImages(p, out var study)) + // ACH : additions to produce some CSV data + if(Csv) + AddDicomDatasetToCSV( + ds, + _studyWriter ?? throw new InvalidOperationException(), + _seriesWriter ?? throw new InvalidOperationException(), + _imageWriter ?? throw new InvalidOperationException()); + else { - //don't generate more than the maximum number of images - if (MaximumImages-- <= 0) - { - break; - } - else - studyUID = study.StudyUID.UID; //all images will have the same study + var f = new DicomFile(ds); - // ACH : additions to produce some CSV data - if(Csv) - AddDicomDatasetToCSV(ds); - else + FileInfo? fi=null; + if (!DevNull) { - var f = new DicomFile(ds); - - FileInfo fi=null; - if (!DevNull) - { - fi = _pathProvider.GetPath(OutputDir, f.Dataset); - if (fi.Directory is { Exists: false }) - fi.Directory.Create(); - } - - using var outFile = new FileStream(fi?.FullName ?? DevNullPath, FileMode.Create); - f.Save(outFile); + fi = _pathProvider.GetPath(OutputDir!, f.Dataset); + if (fi.Directory is { Exists: false }) + fi.Directory.Create(); } - } - //in the CSV write only the StudyUID - return new object[]{studyUID }; + using var outFile = new FileStream(fi?.FullName ?? DevNullPath, FileMode.Create); + f.Save(outFile); + } } - /// - /// Returns headers for the inventory file produced during - /// - /// - protected override string[] GetHeaders() - { - return new[]{ "Studies Generated" }; - } + //in the CSV write only the StudyUID + return new object?[]{studyUID }; + } - /// - /// Creates a dicom study for the with tag values that make sense for that person. This call - /// will generate an entire with a (sensible) random number of series and a random number of images per series - /// (e.g. for CT studies you might get 2 series of ~100 images each). - /// - /// - /// - /// - public DicomDataset[] GenerateStudyImages(Person p, out Study study) - { - //generate a study - study = new(this,p,GetRandomModality(r),r); - - return study.SelectMany(series=>series).ToArray(); - } + /// + /// Returns headers for the inventory file produced during + /// + /// + protected override string[] GetHeaders() + { + return new[]{ "Studies Generated" }; + } - /// - /// Generates a new for the given . This will be a single image single series study - /// - /// - /// - /// - public DicomDataset GenerateTestDataset(Person p,Random _r) - { - //get a random modality - var modality = GetRandomModality(_r); - return GenerateTestDataset(p,new Study(this,p,modality,_r).Series[0]); - } + /// + /// Creates a dicom study for the with tag values that make sense for that person. This call + /// will generate an entire with a (sensible) random number of series and a random number of images per series + /// (e.g. for CT studies you might get 2 series of ~100 images each). + /// + /// + /// + /// + public DicomDataset[] GenerateStudyImages(Person p, out Study study) + { + //generate a study + study = new Study(this,p,GetRandomModality(r),r); + + return study.SelectMany(series=>series).ToArray(); + } - private ModalityStats GetRandomModality(Random _r) - { - return DicomDataGeneratorStats.GetInstance(_r).ModalityFrequency.GetRandom(_modalities,_r); - } + /// + /// Generates a new for the given . This will be a single image single series study + /// + /// + /// + /// + public DicomDataset GenerateTestDataset(Person p,Random _r) + { + //get a random modality + var modality = GetRandomModality(_r); + return GenerateTestDataset(p,new Study(this,p,modality,_r).Series[0]); + } - /// - /// Returns a new random dicom image for the with tag values that make sense for that person - /// - /// - /// - /// - public DicomDataset GenerateTestDataset(Person p,Series series) - { - var ds = new DicomDataset(); + private ModalityStats GetRandomModality(Random _r) + { + return DicomDataGeneratorStats.GetInstance().ModalityFrequency.GetRandom(_modalities,_r); + } + + /// + /// Returns a new random dicom image for the with tag values that make sense for that person + /// + /// + /// + /// + public DicomDataset GenerateTestDataset(Person p,Series series) + { + var ds = new DicomDataset(); - ds.AddOrUpdate(DicomTag.StudyInstanceUID,series.Study.StudyUID); - ds.AddOrUpdate(DicomTag.SeriesInstanceUID,series.SeriesUID); + ds.AddOrUpdate(DicomTag.StudyInstanceUID,series.Study.StudyUID); + ds.AddOrUpdate(DicomTag.SeriesInstanceUID,series.SeriesUID); - DicomUID sopInstanceUID = UIDAllocator.GenerateSOPInstanceUID(); - ds.AddOrUpdate(DicomTag.SOPInstanceUID,sopInstanceUID); - ds.AddOrUpdate(DicomTag.SOPClassUID , DicomUID.SecondaryCaptureImageStorage); + var sopInstanceUID = UIDAllocator.GenerateSOPInstanceUID(); + ds.AddOrUpdate(DicomTag.SOPInstanceUID,sopInstanceUID); + ds.AddOrUpdate(DicomTag.SOPClassUID , DicomUID.SecondaryCaptureImageStorage); - //patient details - ds.AddOrUpdate(DicomTag.PatientID, p.CHI); - ds.AddOrUpdate(DicomTag.PatientName, $"{p.Forename} {p.Surname}"); - ds.AddOrUpdate(DicomTag.PatientBirthDate, p.DateOfBirth); + //patient details + ds.AddOrUpdate(DicomTag.PatientID, p.CHI); + ds.AddOrUpdate(DicomTag.PatientName, $"{p.Forename} {p.Surname}"); + ds.AddOrUpdate(DicomTag.PatientBirthDate, p.DateOfBirth); - if (p.Address != null) - { - string s = - $"{p.Address.Line1} {p.Address.Line2} {p.Address.Line3} {p.Address.Line4} {p.Address.Postcode.Value}"; + if (p.Address != null) + { + var s = + $"{p.Address.Line1} {p.Address.Line2} {p.Address.Line3} {p.Address.Line4} {p.Address.Postcode.Value}"; - ds.AddOrUpdate(DicomTag.PatientAddress, - s[..Math.Min(s.Length,64)] //LO only allows 64 characters - ); - } + ds.AddOrUpdate(DicomTag.PatientAddress, + s[..Math.Min(s.Length,64)] //LO only allows 64 characters + ); + } - ds.AddOrUpdate(new DicomDate(DicomTag.StudyDate, series.Study.StudyDate)); - ds.AddOrUpdate(new DicomTime(DicomTag.StudyTime, DateTime.Today + series.Study.StudyTime)); + ds.AddOrUpdate(new DicomDate(DicomTag.StudyDate, series.Study.StudyDate)); + ds.AddOrUpdate(new DicomTime(DicomTag.StudyTime, DateTime.Today + series.Study.StudyTime)); - ds.AddOrUpdate(new DicomDate(DicomTag.SeriesDate, series.SeriesDate)); - ds.AddOrUpdate(new DicomTime(DicomTag.SeriesTime, DateTime.Today + series.SeriesTime)); + ds.AddOrUpdate(new DicomDate(DicomTag.SeriesDate, series.SeriesDate)); + ds.AddOrUpdate(new DicomTime(DicomTag.SeriesTime, DateTime.Today + series.SeriesTime)); - ds.AddOrUpdate(DicomTag.Modality,series.Modality); - ds.AddOrUpdate(DicomTag.AccessionNumber, series.Study.AccessionNumber?? ""); + ds.AddOrUpdate(DicomTag.Modality,series.Modality); + ds.AddOrUpdate(DicomTag.AccessionNumber, series.Study.AccessionNumber?? ""); - if(series.Study.StudyDescription != null) - ds.AddOrUpdate(DicomTag.StudyDescription,series.Study.StudyDescription); + if(series.Study.StudyDescription != null) + ds.AddOrUpdate(DicomTag.StudyDescription,series.Study.StudyDescription); - if(series.SeriesDescription != null) - ds.AddOrUpdate(DicomTag.SeriesDescription, series.SeriesDescription); + if(series.SeriesDescription != null) + ds.AddOrUpdate(DicomTag.SeriesDescription, series.SeriesDescription); - if (series.BodyPartExamined != null) - ds.AddOrUpdate(DicomTag.BodyPartExamined, series.BodyPartExamined); + if (series.BodyPartExamined != null) + ds.AddOrUpdate(DicomTag.BodyPartExamined, series.BodyPartExamined); - // Calculate the age of the patient at the time the series was taken - var age = series.SeriesDate.Year - p.DateOfBirth.Year; - // Go back to the year the person was born in case of a leap year - if (p.DateOfBirth.Date > series.SeriesDate.AddYears(-age)) age--; - ds.AddOrUpdate(new DicomAgeString(DicomTag.PatientAge, $"{age:000}Y")); + // Calculate the age of the patient at the time the series was taken + var age = series.SeriesDate.Year - p.DateOfBirth.Year; + // Go back to the year the person was born in case of a leap year + if (p.DateOfBirth.Date > series.SeriesDate.AddYears(-age)) age--; + ds.AddOrUpdate(new DicomAgeString(DicomTag.PatientAge, $"{age:000}Y")); - if(!NoPixels) - drawing.DrawBlackBoxWithWhiteText(ds,500,500,sopInstanceUID.UID); - - // Additional DICOM tags added for the generation of CSV files - ds.AddOrUpdate(DicomTag.ModalitiesInStudy, series.Modality); - ds.AddOrUpdate(DicomTag.NumberOfStudyRelatedInstances, series.Study.NumberOfStudyRelatedInstances); - //// Series DICOM tags - ds.AddOrUpdate(DicomTag.ImageType, series.ImageType); - //ds.AddOrUpdate(DicomTag.ProcedureCodeSequence, "0"); //TODO - ds.AddOrUpdate(DicomTag.PerformedProcedureStepID, "0"); - ds.AddOrUpdate(DicomTag.NumberOfSeriesRelatedInstances, series.NumberOfSeriesRelatedInstances); - ds.AddOrUpdate(DicomTag.SeriesNumber, "0"); - //// Image DICOM tags - ds.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO"); - ds.AddOrUpdate(DicomTag.SliceLocation, ""); - ds.AddOrUpdate(DicomTag.SliceThickness, ""); - ds.AddOrUpdate(DicomTag.SpacingBetweenSlices, ""); - ds.AddOrUpdate(DicomTag.SpiralPitchFactor, "0"); - ds.AddOrUpdate(DicomTag.KVP, "0"); - ds.AddOrUpdate(DicomTag.ExposureTime, "0"); - ds.AddOrUpdate(DicomTag.Exposure, "0"); - ds.AddOrUpdate(DicomTag.XRayTubeCurrent, "0"); - ds.AddOrUpdate(DicomTag.PhotometricInterpretation, ""); - ds.AddOrUpdate(DicomTag.AcquisitionNumber, "0"); - ds.AddOrUpdate(DicomTag.AcquisitionDate, series.SeriesDate); - ds.AddOrUpdate(new DicomTime(DicomTag.AcquisitionTime, DateTime.Today + series.SeriesTime)); - ds.AddOrUpdate(DicomTag.ImagePositionPatient, "0","0","0"); - ds.AddOrUpdate(new DicomDecimalString(DicomTag.PixelSpacing,"0.3","0.25")); - ds.AddOrUpdate(DicomTag.FieldOfViewDimensions, "0"); - ds.AddOrUpdate(DicomTag.FieldOfViewDimensionsInFloat, "0"); - //ds.AddOrUpdate(DicomTag.TransferSyntaxUID, "1.2.840.10008.1.2"); this seems to break saving of files lets not set it - ds.AddOrUpdate(DicomTag.LossyImageCompression, "00"); - ds.AddOrUpdate(DicomTag.LossyImageCompressionMethod, "ISO_10918_1"); - ds.AddOrUpdate(DicomTag.LossyImageCompressionRatio, "1"); - - if(Anonymise) - { - _anonymizer.AnonymizeInPlace(ds); - ds.AddOrUpdate(DicomTag.StudyInstanceUID,series.Study.StudyUID); - ds.AddOrUpdate(DicomTag.SeriesInstanceUID,series.SeriesUID); - } - - return ds; - } - - // ACH - Methods for CSV output added below + if(!NoPixels) + PixelDrawer.DrawBlackBoxWithWhiteText(ds,500,500,sopInstanceUID.UID); + + // Additional DICOM tags added for the generation of CSV files + ds.AddOrUpdate(DicomTag.ModalitiesInStudy, series.Modality); + ds.AddOrUpdate(DicomTag.NumberOfStudyRelatedInstances, series.Study.NumberOfStudyRelatedInstances); + //// Series DICOM tags + ds.AddOrUpdate(DicomTag.ImageType, series.ImageType); + //ds.AddOrUpdate(DicomTag.ProcedureCodeSequence, "0"); //TODO + ds.AddOrUpdate(DicomTag.PerformedProcedureStepID, "0"); + ds.AddOrUpdate(DicomTag.NumberOfSeriesRelatedInstances, series.NumberOfSeriesRelatedInstances); + ds.AddOrUpdate(DicomTag.SeriesNumber, "0"); + //// Image DICOM tags + ds.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO"); + ds.AddOrUpdate(DicomTag.SliceLocation, ""); + ds.AddOrUpdate(DicomTag.SliceThickness, ""); + ds.AddOrUpdate(DicomTag.SpacingBetweenSlices, ""); + ds.AddOrUpdate(DicomTag.SpiralPitchFactor, "0"); + ds.AddOrUpdate(DicomTag.KVP, "0"); + ds.AddOrUpdate(DicomTag.ExposureTime, "0"); + ds.AddOrUpdate(DicomTag.Exposure, "0"); + ds.AddOrUpdate(DicomTag.XRayTubeCurrent, "0"); + ds.AddOrUpdate(DicomTag.PhotometricInterpretation, ""); + ds.AddOrUpdate(DicomTag.AcquisitionNumber, "0"); + ds.AddOrUpdate(DicomTag.AcquisitionDate, series.SeriesDate); + ds.AddOrUpdate(new DicomTime(DicomTag.AcquisitionTime, DateTime.Today + series.SeriesTime)); + ds.AddOrUpdate(DicomTag.ImagePositionPatient, "0","0","0"); + ds.AddOrUpdate(new DicomDecimalString(DicomTag.PixelSpacing,"0.3","0.25")); + ds.AddOrUpdate(DicomTag.FieldOfViewDimensions, "0"); + ds.AddOrUpdate(DicomTag.FieldOfViewDimensionsInFloat, "0"); + //ds.AddOrUpdate(DicomTag.TransferSyntaxUID, "1.2.840.10008.1.2"); this seems to break saving of files lets not set it + ds.AddOrUpdate(DicomTag.LossyImageCompression, "00"); + ds.AddOrUpdate(DicomTag.LossyImageCompressionMethod, "ISO_10918_1"); + ds.AddOrUpdate(DicomTag.LossyImageCompressionRatio, "1"); + + if (!Anonymise) return ds; + _anonymiser.AnonymizeInPlace(ds); + ds.AddOrUpdate(DicomTag.StudyInstanceUID,series.Study.StudyUID); + ds.AddOrUpdate(DicomTag.SeriesInstanceUID, series.SeriesUID); + return ds; + } - private void InitialiseCSVOutput() - { - // Write the headers - if(csvInitialized) - return; - csvInitialized = true; + // ACH - Methods for CSV output added below - _studyTags = new() - { - DicomTag.PatientID, - DicomTag.StudyInstanceUID, - DicomTag.StudyDate, - DicomTag.StudyTime, - DicomTag.ModalitiesInStudy, - DicomTag.StudyDescription, - DicomTag.PatientAge, - DicomTag.NumberOfStudyRelatedInstances, - DicomTag.PatientBirthDate - }; - - _seriesTags = new() - { - DicomTag.StudyInstanceUID, - DicomTag.SeriesInstanceUID, - DicomTag.SeriesDate, - DicomTag.SeriesTime, - DicomTag.Modality, - DicomTag.ImageType, - DicomTag.SourceApplicationEntityTitle, - DicomTag.InstitutionName, - DicomTag.ProcedureCodeSequence, - DicomTag.ProtocolName, - DicomTag.PerformedProcedureStepID, - DicomTag.PerformedProcedureStepDescription, - DicomTag.SeriesDescription, - DicomTag.BodyPartExamined, - DicomTag.DeviceSerialNumber, - DicomTag.NumberOfSeriesRelatedInstances, - DicomTag.SeriesNumber - }; - - - _imageTags = new() - { - DicomTag.SeriesInstanceUID, - DicomTag.SOPInstanceUID, - DicomTag.BurnedInAnnotation, - DicomTag.SliceLocation, - DicomTag.SliceThickness, - DicomTag.SpacingBetweenSlices, - DicomTag.SpiralPitchFactor, - DicomTag.KVP, - DicomTag.ExposureTime, - DicomTag.Exposure, - DicomTag.ManufacturerModelName, - DicomTag.Manufacturer, - DicomTag.XRayTubeCurrent, - DicomTag.PhotometricInterpretation, - DicomTag.ContrastBolusRoute, - DicomTag.ContrastBolusAgent, - DicomTag.AcquisitionNumber, - DicomTag.AcquisitionDate, - DicomTag.AcquisitionTime, - DicomTag.ImagePositionPatient, - DicomTag.PixelSpacing, - DicomTag.FieldOfViewDimensions, - DicomTag.FieldOfViewDimensionsInFloat, - DicomTag.DerivationDescription, - DicomTag.TransferSyntaxUID, - DicomTag.LossyImageCompression, - DicomTag.LossyImageCompressionMethod, - DicomTag.LossyImageCompressionRatio, - DicomTag.ScanOptions - }; - - if (OutputDir != null) - { - // Create/open CSV files - studyWriter = new(new StreamWriter(Path.Combine(OutputDir.FullName, StudyCsvFilename)),CultureInfo.CurrentCulture); - seriesWriter = new(new StreamWriter(Path.Combine(OutputDir.FullName, SeriesCsvFilename)),CultureInfo.CurrentCulture); - imageWriter = new(new StreamWriter(Path.Combine(OutputDir.FullName, ImageCsvFilename)),CultureInfo.CurrentCulture); + private void InitialiseCSVOutput() + { + // Write the headers + if(csvInitialized) + return; + csvInitialized = true; + + if (OutputDir == null) return; + // Create/open CSV files + _studyWriter = new CsvWriter(new StreamWriter(Path.Combine(OutputDir.FullName, StudyCsvFilename)),CultureInfo.CurrentCulture); + _seriesWriter = new CsvWriter(new StreamWriter(Path.Combine(OutputDir.FullName, SeriesCsvFilename)),CultureInfo.CurrentCulture); + _imageWriter = new CsvWriter(new StreamWriter(Path.Combine(OutputDir.FullName, ImageCsvFilename)),CultureInfo.CurrentCulture); - // Write header - WriteData("STUDY>>", studyWriter, _studyTags.Select(i => i.DictionaryEntry.Keyword)); - WriteData("SERIES>>", seriesWriter, _seriesTags.Select(i => i.DictionaryEntry.Keyword)); - WriteData("IMAGES>>", imageWriter, _imageTags.Select(i => i.DictionaryEntry.Keyword)); - } - } + // Write header + WriteData(_studyWriter, StudyTags.Select(i => i.DictionaryEntry.Keyword)); + WriteData(_seriesWriter, SeriesTags.Select(i => i.DictionaryEntry.Keyword)); + WriteData(_imageWriter, ImageTags.Select(i => i.DictionaryEntry.Keyword)); + } - private void WriteData(string fileId, CsvWriter sw, IEnumerable data) - { - foreach (string s in data) - sw.WriteField(s); + private static void WriteData(CsvWriter sw, IEnumerable data) + { + foreach (var s in data) + sw.WriteField(s); - sw.NextRecord(); - } + sw.NextRecord(); + } - private void AddDicomDatasetToCSV(DicomDataset ds) + private void AddDicomDatasetToCSV(DicomDataset ds,CsvWriter studies,CsvWriter series,CsvWriter images) + { + if (_lastStudyUID != ds.GetString(DicomTag.StudyInstanceUID)) { - if (_lastStudyUID != ds.GetString(DicomTag.StudyInstanceUID)) - { - _lastStudyUID = ds.GetString(DicomTag.StudyInstanceUID); - - WriteTags("STUDY>>", studyWriter, _studyTags, ds); - } + _lastStudyUID = ds.GetString(DicomTag.StudyInstanceUID); - if (_lastSeriesUID != ds.GetString(DicomTag.SeriesInstanceUID)) - { - _lastSeriesUID = ds.GetString(DicomTag.SeriesInstanceUID); - - WriteTags("SERIES>>", seriesWriter, _seriesTags, ds); - } - - WriteTags("IMAGE>>", imageWriter, _imageTags, ds); + WriteTags(studies, StudyTags, ds); } - private void WriteTags(string fileId, CsvWriter sw, List tags, DicomDataset ds) + if (_lastSeriesUID != ds.GetString(DicomTag.SeriesInstanceUID)) { - var columnData = new List(); - foreach (DicomTag tag in tags) - { - columnData.Add(ds.Contains(tag) ? ds.GetString(tag) : "NULL"); - } + _lastSeriesUID = ds.GetString(DicomTag.SeriesInstanceUID); - WriteData(fileId, sw, columnData); - sw.Flush(); + WriteTags(series, SeriesTags, ds); } - /// - /// Closes all writers and flushes to disk - /// - public void Dispose() - { - studyWriter?.Dispose(); - seriesWriter?.Dispose(); - imageWriter?.Dispose(); - } + WriteTags(images, ImageTags, ds); + } + + private static void WriteTags(CsvWriter sw, IEnumerable tags, DicomDataset ds) + { + var columnData = tags.Select(tag => ds.Contains(tag) ? ds.GetString(tag) : "NULL"); + WriteData(sw, columnData); + sw.Flush(); + } + + /// + /// Closes all writers and flushes to disk + /// + public void Dispose() + { + GC.SuppressFinalize(this); + _studyWriter?.Dispose(); + _seriesWriter?.Dispose(); + _imageWriter?.Dispose(); } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom/DicomDataGeneratorStats.cs b/BadMedicine.Dicom/DicomDataGeneratorStats.cs index 4ecd920..4a85b15 100644 --- a/BadMedicine.Dicom/DicomDataGeneratorStats.cs +++ b/BadMedicine.Dicom/DicomDataGeneratorStats.cs @@ -4,213 +4,197 @@ using System.Data; using BadMedicine.Datasets; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +internal class DicomDataGeneratorStats { - internal class DicomDataGeneratorStats - { - private static DicomDataGeneratorStats _instance; - private static readonly object InstanceLock = new(); + public static readonly DicomDataGeneratorStats Instance=new(); - /// - /// Dictionary of Modality=>Tag=>FrequencyOfEachValue - /// - public readonly Dictionary>> TagValuesByModalityAndTag = new(); - public BucketList ModalityFrequency; - public Dictionary ModalityIndexes = new(); + /// + /// Dictionary of Modality=>Tag=>FrequencyOfEachValue + /// + public readonly Dictionary>> TagValuesByModalityAndTag = new(); + public readonly BucketList ModalityFrequency=new(); + public readonly Dictionary ModalityIndexes = new(); - public readonly Dictionary> DescBodyPartsByModality = new (); - /// - /// Distribution of time of day (in hours only) that tests were taken - /// - public static BucketList HourOfDay; + public readonly Dictionary> DescBodyPartsByModality = new (); + /// + /// Distribution of time of day (in hours only) that tests were taken + /// + private readonly BucketList _hourOfDay=new(); - /// - /// CT Image Type - /// - public static BucketList ImageType; + /// + /// CT Image Type + /// + private readonly BucketList _imageType=new(); - private DicomDataGeneratorStats(Random r) - { - InitializeTagValuesByModalityAndTag(); - InitializeModalityFrequency(r); - InitializeImageType(); + private DicomDataGeneratorStats() + { + InitializeTagValuesByModalityAndTag(); + InitializeModalityFrequency(new Random()); + InitializeImageType(); - InitializeDescBodyPart(); + InitializeDescBodyPart(); - InitializeHourOfDay(); - } + InitializeHourOfDay(); + } - private static void InitializeHourOfDay() - { - //Provenance: - //select DATEPART(HOUR,StudyTime),work.dbo.get_aggregate_value(count(*)) from CT_Godarts_StudyTable group by DATEPART(HOUR,StudyTime) + private void InitializeHourOfDay() + { + //Provenance: + //select DATEPART(HOUR,StudyTime),work.dbo.get_aggregate_value(count(*)) from CT_GoDARTS_StudyTable group by DATEPART(HOUR,StudyTime) + _hourOfDay.Add(1,1); + _hourOfDay.Add(4,1); + _hourOfDay.Add(5,1); + _hourOfDay.Add(6,1); + _hourOfDay.Add(8,15); + _hourOfDay.Add(9,57); + _hourOfDay.Add(10,36); + _hourOfDay.Add(11,41); + _hourOfDay.Add(12,51); + _hourOfDay.Add(13,55); + _hourOfDay.Add(14,54); + _hourOfDay.Add(15,42); + _hourOfDay.Add(16,44); + _hourOfDay.Add(17,42); + _hourOfDay.Add(18,33); + _hourOfDay.Add(19,1); + _hourOfDay.Add(20,7); + _hourOfDay.Add(21,5); + _hourOfDay.Add(22,8); + } - HourOfDay = new(); + /// + /// Generates a random time of day with a frequency that matches the times when the most images are captured (e.g. more images are + /// taken at 1pm than at 8pm + /// + /// + /// + public TimeSpan GetRandomTimeOfDay(Random r) + { + var ts = new TimeSpan(0,_hourOfDay.GetRandom(r),r.Next(60),r.Next(60),0); - HourOfDay.Add(1,1); - HourOfDay.Add(4,1); - HourOfDay.Add(5,1); - HourOfDay.Add(6,1); - HourOfDay.Add(8,15); - HourOfDay.Add(9,57); - HourOfDay.Add(10,36); - HourOfDay.Add(11,41); - HourOfDay.Add(12,51); - HourOfDay.Add(13,55); - HourOfDay.Add(14,54); - HourOfDay.Add(15,42); - HourOfDay.Add(16,44); - HourOfDay.Add(17,42); - HourOfDay.Add(18,33); - HourOfDay.Add(19,1); - HourOfDay.Add(20,7); - HourOfDay.Add(21,5); - HourOfDay.Add(22,8); - } + ts = ts.Subtract(new TimeSpan(ts.Days,0,0,0)); - /// - /// Generates a random time of day with a frequency that matches the times when the most images are captured (e.g. more images are - /// taken at 1pm than at 8pm - /// - /// - /// - public TimeSpan GetRandomTimeOfDay(Random r) - { - var ts = new TimeSpan(0,HourOfDay.GetRandom(r),r.Next(60),r.Next(60),0); - - ts = ts.Subtract(new(ts.Days,0,0,0)); + if(ts.Days != 0) + throw new Exception("What!"); - if(ts.Days != 0) - throw new("What!"); + return ts; + } - return ts; - } + public string GetRandomImageType(Random r) => _imageType.GetRandom(r); - public string GetRandomImageType(Random r) - { - return ImageType.GetRandom(r); - } + /// + /// returns a random string e.g. T101H12451352 where the first letter indicates Tayside and 5th letter indicates Hospital + /// + /// + /// + public static string GetRandomAccessionNumber(Random r) => $"T{r.Next(4)}{r.Next(2)}{r.Next(5)}H{r.Next(9999999)}"; - /// - /// returns a random string e.g. T101H12451352 where the first letter indicates Tayside and 5th letter indicates Hospital - /// - /// - /// - public string GetRandomAccessionNumber(Random r) + private void InitializeModalityFrequency(Random r) + { + using DataTable dt = new(); + dt.BeginLoadData(); + dt.Columns.Add("Frequency", typeof(int)); + dt.Columns.Add("AverageSeriesPerStudy", typeof(double)); + dt.Columns.Add("StandardDeviationSeriesPerStudy", typeof(double)); + dt.Columns.Add("AverageImagesPerSeries", typeof(double)); + dt.Columns.Add("StandardDeviationImagesPerSeries", typeof(double)); + + DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorModalities.csv", dt); + dt.EndLoadData(); + + var idx = 0; + foreach (DataRow dr in dt.Rows) { - return $"T{r.Next(4)}{r.Next(2)}{r.Next(5)}H{r.Next(9999999)}"; + var modality = (string)dr["Modality"]; + ModalityFrequency.Add((int)dr["Frequency"], + new ModalityStats( + modality, + (double)dr["AverageSeriesPerStudy"], + (double)dr["StandardDeviationSeriesPerStudy"], + (double)dr["AverageImagesPerSeries"], + (double)dr["StandardDeviationImagesPerSeries"], + r + )); + + ModalityIndexes.Add(modality, idx++); } + } - private void InitializeModalityFrequency(Random r) + private void InitializeDescBodyPart() + { + using DataTable dt = new(); + dt.BeginLoadData(); + dt.Columns.Add("Modality", typeof(string)); + dt.Columns.Add("StudyDescription", typeof(string)); + dt.Columns.Add("BodyPartExamined", typeof(string)); + dt.Columns.Add("SeriesDescription", typeof(string)); + dt.Columns.Add("series_count", typeof(int)); + + DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorDescBodyPart.csv", dt); + dt.EndLoadData(); + foreach (DataRow dr in dt.Rows) { - using DataTable dt = new(); - dt.Columns.Add("Frequency", typeof(int)); - dt.Columns.Add("AverageSeriesPerStudy", typeof(double)); - dt.Columns.Add("StandardDeviationSeriesPerStudy", typeof(double)); - dt.Columns.Add("AverageImagesPerSeries", typeof(double)); - dt.Columns.Add("StandardDeviationImagesPerSeries", typeof(double)); - - DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorModalities.csv", dt); + var modality = (string)dr["Modality"]; - ModalityFrequency = new(); - - int idx = 0; - foreach (DataRow dr in dt.Rows) + // first time we have seen this modality + if(!DescBodyPartsByModality.ContainsKey(modality)) { - string modality = (string)dr["Modality"]; - ModalityFrequency.Add((int)dr["Frequency"], - new( - modality, - (double)dr["AverageSeriesPerStudy"], - (double)dr["StandardDeviationSeriesPerStudy"], - (double)dr["AverageImagesPerSeries"], - (double)dr["StandardDeviationImagesPerSeries"], - r - )); - - ModalityIndexes.Add(modality, idx++); + DescBodyPartsByModality.Add(modality, new BucketList()); } - } - private void InitializeDescBodyPart() - { - using DataTable dt = new(); - dt.Columns.Add("Modality", typeof(string)); - dt.Columns.Add("StudyDescription", typeof(string)); - dt.Columns.Add("BodyPartExamined", typeof(string)); - dt.Columns.Add("SeriesDescription", typeof(string)); - dt.Columns.Add("series_count", typeof(int)); - - DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorDescBodyPart.csv", dt); - - foreach (DataRow dr in dt.Rows) + var part = new DescBodyPart { - var modality = (string)dr["Modality"]; - - // first time we have seen this modality - if(!DescBodyPartsByModality.ContainsKey(modality)) - { - DescBodyPartsByModality.Add(modality, new ()); - } - - var part = new DescBodyPart - { - StudyDescription = dr["StudyDescription"] as string, - BodyPartExamined = dr["BodyPartExamined"] as string, // as string deals with DBNull.value - SeriesDescription = dr["SeriesDescription"] as string, - }; - - // record how often we see this part - DescBodyPartsByModality[modality].Add((int)dr["series_count"], part); - } + StudyDescription = dr["StudyDescription"] as string, + BodyPartExamined = dr["BodyPartExamined"] as string, // as string deals with DBNull.value + SeriesDescription = dr["SeriesDescription"] as string + }; + + // record how often we see this part + DescBodyPartsByModality[modality].Add((int)dr["series_count"], part); } - private void InitializeTagValuesByModalityAndTag() - { - using DataTable dt = new(); - dt.Columns.Add("Frequency", typeof(int)); + } + private void InitializeTagValuesByModalityAndTag() + { + using DataTable dt = new(); + dt.BeginLoadData(); + dt.Columns.Add("Frequency", typeof(int)); - DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorTags.csv", dt); + DataGenerator.EmbeddedCsvToDataTable(typeof(DicomDataGenerator), "DicomDataGeneratorTags.csv", dt); + dt.EndLoadData(); - foreach (DataRow dr in dt.Rows) - { - var modality = (string)dr["Modality"]; - var tag = DicomDictionary.Default[(string)dr["Tag"]]; + foreach (DataRow dr in dt.Rows) + { + var modality = (string)dr["Modality"]; + var tag = DicomDictionary.Default[(string)dr["Tag"]]; - if (!TagValuesByModalityAndTag.ContainsKey(modality)) - TagValuesByModalityAndTag.Add(modality, new()); + if (!TagValuesByModalityAndTag.ContainsKey(modality)) + TagValuesByModalityAndTag.Add(modality, new Dictionary>()); - if (!TagValuesByModalityAndTag[modality].ContainsKey(tag)) - TagValuesByModalityAndTag[modality].Add(tag, new()); + if (!TagValuesByModalityAndTag[modality].ContainsKey(tag)) + TagValuesByModalityAndTag[modality].Add(tag, new BucketList()); - int frequency = (int)dr["Frequency"]; - TagValuesByModalityAndTag[modality][tag].Add(frequency, (string)dr["Value"]); - } + var frequency = (int)dr["Frequency"]; + TagValuesByModalityAndTag[modality][tag].Add(frequency, (string)dr["Value"]); } + } - private static void InitializeImageType() - { - ImageType = new(); - - ImageType.Add(96,"ORIGINAL\\PRIMARY\\AXIAL"); - ImageType.Add(1,"ORIGINAL\\PRIMARY\\LOCALIZER"); - ImageType.Add(3,"DERIVED\\SECONDARY"); - } + private void InitializeImageType() + { + _imageType.Add(96,"ORIGINAL\\PRIMARY\\AXIAL"); + _imageType.Add(1,"ORIGINAL\\PRIMARY\\LOCALIZER"); + _imageType.Add(3,"DERIVED\\SECONDARY"); + } - /// - /// Returns the existing stats for tag popularity, modality frequencies etc. If stats have not been loaded they are loaded - /// and primed with the Random (otherwise is ignored). - /// - /// - /// - public static DicomDataGeneratorStats GetInstance(Random r) - { - lock(InstanceLock) - { - return _instance ??= new(r); - } - - } + /// + /// Returns the existing stats for tag popularity, modality frequencies etc. + /// + /// + public static DicomDataGeneratorStats GetInstance() + { + return Instance; } } \ No newline at end of file diff --git a/BadMedicine.Dicom/FileSystemLayout.cs b/BadMedicine.Dicom/FileSystemLayout.cs index 2e5d4a2..5dde8ab 100644 --- a/BadMedicine.Dicom/FileSystemLayout.cs +++ b/BadMedicine.Dicom/FileSystemLayout.cs @@ -1,29 +1,28 @@ -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// How and whether to group the generated files into subdirectories +/// +public enum FileSystemLayout { /// - /// How and whether to group the generated files into subdirectories + /// Files are created in the target directory without subdirectories /// - public enum FileSystemLayout - { - /// - /// Files are created in the target directory without subdirectories - /// - Flat, + Flat, - /// - /// Files are created in a subdirectory by Study year/month/day e.g. /2001/12/1/my.dcm - /// - StudyYearMonthDay, + /// + /// Files are created in a subdirectory by Study year/month/day e.g. /2001/12/1/my.dcm + /// + StudyYearMonthDay, - /// - /// Files are created in a subdirectory by Study year then AccessionNumber e.g. /2001/12/1/N123/my.dcm - /// - StudyYearMonthDayAccession, + /// + /// Files are created in a subdirectory by Study year then AccessionNumber e.g. /2001/12/1/N123/my.dcm + /// + StudyYearMonthDayAccession, - /// - /// Files are created in subdirectories by Study UID - /// - StudyUID + /// + /// Files are created in subdirectories by Study UID + /// + StudyUID - } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom/FileSystemLayoutProvider.cs b/BadMedicine.Dicom/FileSystemLayoutProvider.cs index 1e94b4c..3f3cb1d 100644 --- a/BadMedicine.Dicom/FileSystemLayoutProvider.cs +++ b/BadMedicine.Dicom/FileSystemLayoutProvider.cs @@ -2,70 +2,67 @@ using System; using System.IO; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +internal class FileSystemLayoutProvider { - class FileSystemLayoutProvider + public FileSystemLayout Layout { get; } + + public FileSystemLayoutProvider(FileSystemLayout layout) { - public FileSystemLayout Layout { get; } + Layout = layout; + } - public FileSystemLayoutProvider(FileSystemLayout layout) - { - Layout = layout; - } + public FileInfo GetPath(DirectoryInfo root,DicomDataset ds) + { + var filename = $"{ds.GetSingleValue(DicomTag.SOPInstanceUID).UID}.dcm"; + var date = ds.GetValues(DicomTag.StudyDate); - public FileInfo GetPath(DirectoryInfo root,DicomDataset ds) + switch(Layout) { - var filename = $"{ds.GetSingleValue(DicomTag.SOPInstanceUID).UID}.dcm"; - var date = ds.GetValues(DicomTag.StudyDate); + case FileSystemLayout.Flat: + return new FileInfo(Path.Combine(root.FullName,filename)); - switch(Layout) - { - case FileSystemLayout.Flat: - return new(Path.Combine(root.FullName,filename)); + case FileSystemLayout.StudyYearMonthDay: - case FileSystemLayout.StudyYearMonthDay: - - if(date.Length > 0) - { - return new(Path.Combine( + if(date.Length > 0) + { + return new FileInfo(Path.Combine( root.FullName, date[0].Year.ToString(), date[0].Month.ToString(), date[0].Day.ToString(), filename)); - } - else - break; + } + break; - case FileSystemLayout.StudyYearMonthDayAccession: - - var acc = ds.GetSingleValue(DicomTag.AccessionNumber); - - if(date.Length > 0 && !string.IsNullOrWhiteSpace(acc)) - { - return new(Path.Combine( + case FileSystemLayout.StudyYearMonthDayAccession: + + var acc = ds.GetSingleValue(DicomTag.AccessionNumber); + + if(date.Length > 0 && !string.IsNullOrWhiteSpace(acc)) + { + return new FileInfo(Path.Combine( root.FullName, date[0].Year.ToString(), date[0].Month.ToString(), date[0].Day.ToString(), acc, filename)); - } - else - break; + } + break; - case FileSystemLayout.StudyUID: + case FileSystemLayout.StudyUID: - return new(Path.Combine( - root.FullName, - ds.GetSingleValue(DicomTag.StudyInstanceUID).UID, - filename)); + return new FileInfo(Path.Combine( + root.FullName, + ds.GetSingleValue(DicomTag.StudyInstanceUID).UID, + filename)); - default: throw new ArgumentOutOfRangeException(); - } - - return new(Path.Combine(root.FullName,filename)); + default: throw new ArgumentOutOfRangeException(nameof(Layout)); } + return new FileInfo(Path.Combine(root.FullName,filename)); } -} + +} \ No newline at end of file diff --git a/BadMedicine.Dicom/ModalityStats.cs b/BadMedicine.Dicom/ModalityStats.cs index a30a36b..49b730e 100644 --- a/BadMedicine.Dicom/ModalityStats.cs +++ b/BadMedicine.Dicom/ModalityStats.cs @@ -1,69 +1,68 @@ using MathNet.Numerics.Distributions; using System; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// A set of statistical distribution parameters for a specific Modality +/// +public class ModalityStats { + /// - /// A set of statistical distribution parameters for a specific Modality + /// Which Modality this relates to, for example 'MR' /// - public class ModalityStats - { - - /// - /// Which Modality this relates to, for example 'MR' - /// - public string Modality{ get; } + public string Modality{ get; } - /// - /// The mean number of Series in a Study of this Modality - /// - public double SeriesPerStudyAverage => SeriesPerStudyNormal.Mean; + /// + /// The mean number of Series in a Study of this Modality + /// + public double SeriesPerStudyAverage => SeriesPerStudyNormal.Mean; - /// - /// The standard deviation of the number of Series in a Study of this Modality - /// - public double SeriesPerStudyStandardDeviation => SeriesPerStudyNormal.StdDev; + /// + /// The standard deviation of the number of Series in a Study of this Modality + /// + public double SeriesPerStudyStandardDeviation => SeriesPerStudyNormal.StdDev; - /// - /// The parameterised Normal distribution used for the number of series per study - /// - public Normal SeriesPerStudyNormal { get; } + /// + /// The parameterised Normal distribution used for the number of series per study + /// + public Normal SeriesPerStudyNormal { get; } - /// - /// The mean number of Images in a Series of this Modality - /// - public double ImagesPerSeriesAverage { get => ImagesPerSeriesNormal.Mean; set => ImagesPerSeriesNormal=new(value,ImagesPerSeriesNormal.StdDev, Rng); } + /// + /// The mean number of Images in a Series of this Modality + /// + public double ImagesPerSeriesAverage { get => ImagesPerSeriesNormal.Mean; set => ImagesPerSeriesNormal=new Normal(value,ImagesPerSeriesNormal.StdDev, Rng); } - /// - /// The standard deviation of the number of Images in a Series of this Modality - /// - public double ImagesPerSeriesStandardDeviation{ get => ImagesPerSeriesNormal.StdDev; set => ImagesPerSeriesNormal=new(ImagesPerSeriesNormal.Mean,value,Rng); } + /// + /// The standard deviation of the number of Images in a Series of this Modality + /// + public double ImagesPerSeriesStandardDeviation{ get => ImagesPerSeriesNormal.StdDev; set => ImagesPerSeriesNormal=new Normal(ImagesPerSeriesNormal.Mean,value,Rng); } - /// - /// The Normal distribution of the number of Images per Series for this Modality - /// - public Normal ImagesPerSeriesNormal {get; private set; } + /// + /// The Normal distribution of the number of Images per Series for this Modality + /// + public Normal ImagesPerSeriesNormal {get; private set; } - /// - /// The Random pseudo-random number generator to be used - /// - private Random Rng { get; } + /// + /// The Random pseudo-random number generator to be used + /// + private Random Rng { get; } - /// - /// Construct a new set of distributions for use with the specified Modality - /// - /// - /// - /// - /// - /// - /// - public ModalityStats(string modality, double averageSeriesPerStudy,double standardDeviationSeriesPerStudy,double averageImagesPerSeries,double standardDeviationImagesPerSeries, Random r) - { - Rng = r; - Modality = modality; - SeriesPerStudyNormal = new(averageSeriesPerStudy, standardDeviationSeriesPerStudy, r); - ImagesPerSeriesNormal = new(averageImagesPerSeries, standardDeviationImagesPerSeries, r); - } + /// + /// Construct a new set of distributions for use with the specified Modality + /// + /// + /// + /// + /// + /// + /// + public ModalityStats(string modality, double averageSeriesPerStudy,double standardDeviationSeriesPerStudy,double averageImagesPerSeries,double standardDeviationImagesPerSeries, Random r) + { + Rng = r; + Modality = modality; + SeriesPerStudyNormal = new Normal(averageSeriesPerStudy, standardDeviationSeriesPerStudy, r); + ImagesPerSeriesNormal = new Normal(averageImagesPerSeries, standardDeviationImagesPerSeries, r); } } \ No newline at end of file diff --git a/BadMedicine.Dicom/PixelDrawer.cs b/BadMedicine.Dicom/PixelDrawer.cs index 0db4484..f8a3121 100644 --- a/BadMedicine.Dicom/PixelDrawer.cs +++ b/BadMedicine.Dicom/PixelDrawer.cs @@ -19,27 +19,25 @@ internal class PixelDrawer // https://cannotintospacefonts.blogspot.com/ // Converted via https://cloudconvert.com/ private static readonly Font Font = new FontCollection().Add(new MemoryStream(Convert.FromBase64String("AAEAAAANAIAAAwBQRkZUTXkt2uUAASlIAAAAHEdERUYAJQAAAAEpMAAAABhPUy8yZ2JuRQAAAVgAAABgY21hcIkBdt4AAAe4AAADdmdhc3D//wADAAEpKAAAAAhnbHlmT53c8wAADjQAANn4aGVhZAMwg60AAADcAAAANmhoZWEH8AZtAAABFAAAACRobXR4jR9nOAAAAbgAAAX+bG9jYcoxkiwAAAswAAADAm1heHAByABvAAABOAAAACBuYW1lgOlUVQAA6CwAADjicG9zdIZMM+YAASEQAAAIFQABAAAAAQAAZx15AV8PPPUACwPoAAAAAM0dv6AAAAAA0byBVP8W/mcGIQNtAAAACAACAAAAAAAAAAEAAAK2/sYAAAcG/xb/IQYhAAEAAAAAAAAAAAAAAAAAAAF/AAEAAAGAAGwABgAAAAAAAgAAAAEAAQAAAEAAAAAAAAAAAwJZAZAABQAAArwCigAAAI8CvAKKAAABxQA/AQMAAAIABAkAAAAAAACgAAAvUAAASwAAAAAAAAAAUVVRQQAAAAD7AgK2/sYAAANtAZogAACTAAAAAAOcBKMAAAAgAAECVwAAAAAAAAJXAAAAAAAAAlcAAAJXAOACVwCQAlcAEwJXAFYCVwAYAlcASAJXAO0CVwCRAlcArQJXAFECVwBTAlcAswJXAFMCVwDZAlcAXgJXAFwCVwBuAlcAWwJXAFICVwBIAlcATwJXAGICVwBwAlcAZQJXAF4CVwDZAlcAswJXAD0CVwBTAlcASwJXAHsCVwAXAlcABQJXACcCVwA6AlcAJwJXACcCVwAnAlcAMAJXACcCVwBiAlcANAJXACcCVwAnAlcADwJXACICVwArAlcAJwJXACoCVwAnAlcATQJXADwCVwAiAlcABQJX//wCVwAYAlcAHQJXAFICVwChAlcAXAJXAK8CVwBxAlf/8gJXAIACVwBCAlcAEQJXAD4CVwA9AlcARwJXAFsCVwA9AlcAJgJXAFsCVwBTAlcAIQJXAFYCV//+AlcAJgJXADsCVwANAlcAPQJXAEgCVwBXAlcAOQJXACICVwAYAlf/+wJXACICVwAYAlcAVwJXAGUCVwEHAlcAmwJXAEsCVwAAAlcA4QJXAD0CVwBhAlcAHQJXAQcCVwBWAlcAgwJXABECVwB1AlcAcwJXAEQCVwASAlcAhAJXAHgCVwBTAlcAjQJXAJICVwC8AlcAQAJXANkCVwCsAlcAkwJXAHMCVwBvAlf//AJX//wCVwAHAlcAfgJXAAUCVwAFAlcABQJXAAUCVwAFAlcABQJXAAACVwA6AlcAJwJXACcCVwAnAlcAJwJXAGICVwBiAlcAYgJXAGICVwAYAlcAIgJXACsCVwArAlcAKwJXACsCVwArAlcAcAJXACsCVwAiAlcAIgJXACICVwAiAlcAHQJXADYCVwAUAlcAQgJXAEICVwBCAlcAQgJXAEICVwBCAlcAJQJXAD0CVwBHAlcARwJXAEcCVwBHAlcAWwJXAFsCVwBbAlcAWwJXAEQCVwAmAlcAOwJXADsCVwA7AlcAOwJXADsCVwBTAlcANgJXACMCVwAjAlcAIwJXACMCVwAYAlcADQJXABgCVwAFAlcAQgJXAAUCVwBCAlcABQJXAEICVwA5AlcAPgJXADkCVwA+AlcAOQJXAD4CVwA5AlcAPgJXACcCVwA9AlcAJwJXAD0CVwAnAlcARwJXACcCVwBHAlcAJwJXAEcCVwAnAlcARwJXACcCVwBHAlcAMAJXAD0CVwAwAlcAPQJXADACVwA9AlcAMAJXAD0CVwAnAlcAJgJXAA8CVwAKAlcAYgJXAFsCVwBiAlcAWwJXAGICVwBbAlcAYgJXAFsCVwBiAlcAWwJXAAoCVwAcAlcANAJXAFMCVwAnAlcAIQJXACcCVwBWAlcAJwJXAFYCVwAnAlcARwJXACcCVwBHAlcAGwJXAFYCVwAiAlcAJgJXACICVwAmAlcAIgJXACYC6QAwAlcAKwJXADsCVwArAlcAOwJXACsCVwA7AlcAHgJXACICVwAnAlcASAJXACcCVwBIAlcAJwJXAEgCVwBNAlcAVwJXAE0CVwBXAlcATQJXAFcCVwBNAlcAVwJXADwCVwA5AlcAPAJXADkCVwA8AlcAOQJXACICVwAjAlcAIgJXACMCVwAiAlcAIwJXACICVwAjAlcAIgJXACMCVwAiAlcAIgJX//sCV//7AlcAHQJXABgCVwAdAlcAUgJXAFcCVwBSAlcAVwJXAFICVwBXAlcAJwJXAAoCVwBNAlcAVwJXAFMCVwCCAlcAggJXAHICVwDnAlcAtQJXAMkCVwCBAlcAogJX//sCV//7Alf/+wJX//sCV//7Alf/+wJXAB0CVwAYAlcAUwJX//ECVwDM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AHMAaQBvAG4AIAAxAC4AMAAwAABWZXJzaW9uIDEuMDAAAE0AZQBjAGgAYQBuAGkAYwBhAGwAAE1lY2hhbmljYWwAAFEAdQBvAHQAZQAtAFUAbgBxAHUAbwB0AGUAIABBAHAAcABzAABRdW90ZS1VbnF1b3RlIEFwcHMAAEEAbABhAG4AIABEAGEAZwB1AGUALQBHAHIAZQBlAG4AZQAAQWxhbiBEYWd1ZS1HcmVlbmUAAGgAdAB0AHAAOgAvAC8AcQB1AG8AdABlAHUAbgBxAHUAbwB0AGUAYQBwAHAAcwAuAGMAbwBtAABodHRwOi8vcXVvdGV1bnF1b3RlYXBwcy5jb20AAGgAdAB0AHAAOgAvAC8AYgBhAHMAaQBjAHIAZQBjAGkAcABlAC4AYwBvAG0AAGh0dHA6Ly9iYXNpY3JlY2lwZS5jb20AAEMAbwBwAHkAcgBpAGcAaAB0ACAAKABjACkAIAAyADAAMQAzACwAIABRAHUAbwB0AGUALQBVAG4AcQB1AG8AdABlACAAQQBwAHAAcwAgACgAaAB0AHQAcAA6AC8ALwBxAHUAbwB0AGUAdQBuAHEAdQBvAHQAZQBhAHAAcABzAC4AYwBvAG0AKQAsACAAdwBpAHQAaAAgAFIAZQBzAGUAcgB2AGUAZAAgAEYAbwBuAHQAIABOAGEAbQBlACAAQwBvAHUAcgBpAGUAcgAgAFAAcgBpAG0AZQAuAA0ADQBUAGgAaQBzACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAGkAcwAgAGwAaQBjAGUAbgBzAGUAZAAgAHUAbgBkAGUAcgAgAHQAaABlACAAUwBJAEwAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUALAAgAFYAZQByAHMAaQBvAG4AIAAxAC4AMQAuACAAVABoAGkAcwAgAGwAaQBjAGUAbgBzAGUAIABpAHMAIABjAG8AcABpAGUAZAAgAGIAZQBsAG8AdwAsACAAYQBuAGQAIABpAHMAIABhAGwAcwBvACAAYQB2AGEAaQBsAGEAYgBsAGUAIAB3AGkAdABoACAAYQAgAEYAQQBRACAAYQB0ADoAIABoAHQAdABwADoALwAvAHMAYwByAGkAcAB0AHMALgBzAGkAbAAuAG8AcgBnAC8ATwBGAEwADQANAA0ACgAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ADQBTAEkATAAgAE8AUABFAE4AIABGAE8ATgBUACAATABJAEMARQBOAFMARQAgAFYAZQByAHMAaQBvAG4AIAAxAC4AMQAgAC0AIAAyADYAIABGAGUAYgByAHUAYQByAHkAIAAyADAAMAA3AA0ACgAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ALQAtAC0ADQANAFAAUgBFAEEATQBCAEwARQANAFQAaABlACAAZwBvAGEAbABzACAAbwBmACAAdABoAGUAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUAIAAoAE8ARgBMACkAIABhAHIAZQAgAHQAbwAgAHMAdABpAG0AdQBsAGEAdABlACAAdwBvAHIAbABkAHcAaQBkAGUAIABkAGUAdgBlAGwAbwBwAG0AZQBuAHQAIABvAGYAIABjAG8AbABsAGEAYgBvAHIAYQB0AGkAdgBlACAAZgBvAG4AdAAgAHAAcgBvAGoAZQBjAHQAcwAsACAAdABvACAAcwB1AHAAcABvAHIAdAAgAHQAaABlACAAZgBvAG4AdAAgAGMAcgBlAGEAdABpAG8AbgAgAGUAZgBmAG8AcgB0AHMAIABvAGYAIABhAGMAYQBkAGUAbQBpAGMAIABhAG4AZAAgAGwAaQBuAGcAdQBpAHMAdABpAGMAIABjAG8AbQBtAHUAbgBpAHQAaQBlAHMALAAgAGEAbgBkACAAdABvACAAcAByAG8AdgBpAGQAZQAgAGEAIABmAHIAZQBlACAAYQBuAGQAIABvAHAAZQBuACAAZgByAGEAbQBlAHcAbwByAGsAIABpAG4AIAB3AGgAaQBjAGgAIABmAG8AbgB0AHMAIABtAGEAeQAgAGIAZQAgAHMAaABhAHIAZQBkACAAYQBuAGQAIABpAG0AcAByAG8AdgBlAGQAIABpAG4AIABwAGEAcgB0AG4AZQByAHMAaABpAHAAIAB3AGkAdABoACAAbwB0AGgAZQByAHMALgANAA0AVABoAGUAIABPAEYATAAgAGEAbABsAG8AdwBzACAAdABoAGUAIABsAGkAYwBlAG4AcwBlAGQAIABmAG8AbgB0AHMAIAB0AG8AIABiAGUAIAB1AHMAZQBkACwAIABzAHQAdQBkAGkAZQBkACwAIABtAG8AZABpAGYAaQBlAGQAIABhAG4AZAAgAHIAZQBkAGkAcwB0AHIAaQBiAHUAdABlAGQAIABmAHIAZQBlAGwAeQAgAGEAcwAgAGwAbwBuAGcAIABhAHMAIAB0AGgAZQB5ACAAYQByAGUAIABuAG8AdAAgAHMAbwBsAGQAIABiAHkAIAB0AGgAZQBtAHMAZQBsAHYAZQBzAC4AIABUAGgAZQAgAGYAbwBuAHQAcwAsACAAaQBuAGMAbAB1AGQAaQBuAGcAIABhAG4AeQAgAGQAZQByAGkAdgBhAHQAaQB2AGUAIAB3AG8AcgBrAHMALAAgAGMAYQBuACAAYgBlACAAYgB1AG4AZABsAGUAZAAsACAAZQBtAGIAZQBkAGQAZQBkACwAIAByAGUAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAYQBuAGQALwBvAHIAIABzAG8AbABkACAAdwBpAHQAaAAgAGEAbgB5ACAAcwBvAGYAdAB3AGEAcgBlACAAcAByAG8AdgBpAGQAZQBkACAAdABoAGEAdAAgAGEAbgB5ACAAcgBlAHMAZQByAHYAZQBkACAAbgBhAG0AZQBzACAAYQByAGUAIABuAG8AdAAgAHUAcwBlAGQAIABiAHkAIABkAGUAcgBpAHYAYQB0AGkAdgBlACAAdwBvAHIAawBzAC4AIABUAGgAZQAgAGYAbwBuAHQAcwAgAGEAbgBkACAAZABlAHIAaQB2AGEAdABpAHYAZQBzACwAIABoAG8AdwBlAHYAZQByACwAIABjAGEAbgBuAG8AdAAgAGIAZQAgAHIAZQBsAGUAYQBzAGUAZAAgAHUAbgBkAGUAcgAgAGEAbgB5ACAAbwB0AGgAZQByACAAdAB5AHAAZQAgAG8AZgAgAGwAaQBjAGUAbgBzAGUALgAgAFQAaABlACAAcgBlAHEAdQBpAHIAZQBtAGUAbgB0ACAAZgBvAHIAIABmAG8AbgB0AHMAIAB0AG8AIAByAGUAbQBhAGkAbgAgAHUAbgBkAGUAcgAgAHQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAZABvAGUAcwAgAG4AbwB0ACAAYQBwAHAAbAB5ACAAdABvACAAYQBuAHkAIABkAG8AYwB1AG0AZQBuAHQAIABjAHIAZQBhAHQAZQBkACAAdQBzAGkAbgBnACAAdABoAGUAIABmAG8AbgB0AHMAIABvAHIAIAB0AGgAZQBpAHIAIABkAGUAcgBpAHYAYQB0AGkAdgBlAHMALgANAA0ARABFAEYASQBOAEkAVABJAE8ATgBTAA0AIgBGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACIAIAByAGUAZgBlAHIAcwAgAHQAbwAgAHQAaABlACAAcwBlAHQAIABvAGYAIABmAGkAbABlAHMAIAByAGUAbABlAGEAcwBlAGQAIABiAHkAIAB0AGgAZQAgAEMAbwBwAHkAcgBpAGcAaAB0ACAASABvAGwAZABlAHIAKABzACkAIAB1AG4AZABlAHIAIAB0AGgAaQBzACAAbABpAGMAZQBuAHMAZQAgAGEAbgBkACAAYwBsAGUAYQByAGwAeQAgAG0AYQByAGsAZQBkACAAYQBzACAAcwB1AGMAaAAuACAAVABoAGkAcwAgAG0AYQB5ACAAaQBuAGMAbAB1AGQAZQAgAHMAbwB1AHIAYwBlACAAZgBpAGwAZQBzACwAIABiAHUAaQBsAGQAIABzAGMAcgBpAHAAdABzACAAYQBuAGQAIABkAG8AYwB1AG0AZQBuAHQAYQB0AGkAbwBuAC4ADQANACIAUgBlAHMAZQByAHYAZQBkACAARgBvAG4AdAAgAE4AYQBtAGUAIgAgAHIAZQBmAGUAcgBzACAAdABvACAAYQBuAHkAIABuAGEAbQBlAHMAIABzAHAAZQBjAGkAZgBpAGUAZAAgAGEAcwAgAHMAdQBjAGgAIABhAGYAdABlAHIAIAB0AGgAZQAgAGMAbwBwAHkAcgBpAGcAaAB0ACAAcwB0AGEAdABlAG0AZQBuAHQAKABzACkALgANAA0AIgBPAHIAaQBnAGkAbgBhAGwAIABWAGUAcgBzAGkAbwBuACIAIAByAGUAZgBlAHIAcwAgAHQAbwAgAHQAaABlACAAYwBvAGwAbABlAGMAdABpAG8AbgAgAG8AZgAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABjAG8AbQBwAG8AbgBlAG4AdABzACAAYQBzACAAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAYgB5ACAAdABoAGUAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByACgAcwApAC4ADQANACIATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgAiACAAcgBlAGYAZQByAHMAIAB0AG8AIABhAG4AeQAgAGQAZQByAGkAdgBhAHQAaQB2AGUAIABtAGEAZABlACAAYgB5ACAAYQBkAGQAaQBuAGcAIAB0AG8ALAAgAGQAZQBsAGUAdABpAG4AZwAsACAAbwByACAAcwB1AGIAcwB0AGkAdAB1AHQAaQBuAGcAIAAtAC0AIABpAG4AIABwAGEAcgB0ACAAbwByACAAaQBuACAAdwBoAG8AbABlACAALQAtACAAYQBuAHkAIABvAGYAIAB0AGgAZQAgAGMAbwBtAHAAbwBuAGUAbgB0AHMAIABvAGYAIAB0AGgAZQAgAE8AcgBpAGcAaQBuAGEAbAAgAFYAZQByAHMAaQBvAG4ALAAgAGIAeQAgAGMAaABhAG4AZwBpAG4AZwAgAGYAbwByAG0AYQB0AHMAIABvAHIAIABiAHkAIABwAG8AcgB0AGkAbgBnACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAdABvACAAYQAgAG4AZQB3ACAAZQBuAHYAaQByAG8AbgBtAGUAbgB0AC4ADQANACIAQQB1AHQAaABvAHIAIgAgAHIAZQBmAGUAcgBzACAAdABvACAAYQBuAHkAIABkAGUAcwBpAGcAbgBlAHIALAAgAGUAbgBnAGkAbgBlAGUAcgAsACAAcAByAG8AZwByAGEAbQBtAGUAcgAsACAAdABlAGMAaABuAGkAYwBhAGwAIAB3AHIAaQB0AGUAcgAgAG8AcgAgAG8AdABoAGUAcgAgAHAAZQByAHMAbwBuACAAdwBoAG8AIABjAG8AbgB0AHIAaQBiAHUAdABlAGQAIAB0AG8AIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALgANAA0AUABFAFIATQBJAFMAUwBJAE8ATgAgACYAIABDAE8ATgBEAEkAVABJAE8ATgBTAA0AUABlAHIAbQBpAHMAcwBpAG8AbgAgAGkAcwAgAGgAZQByAGUAYgB5ACAAZwByAGEAbgB0AGUAZAAsACAAZgByAGUAZQAgAG8AZgAgAGMAaABhAHIAZwBlACwAIAB0AG8AIABhAG4AeQAgAHAAZQByAHMAbwBuACAAbwBiAHQAYQBpAG4AaQBuAGcAIABhACAAYwBvAHAAeQAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAsACAAdABvACAAdQBzAGUALAAgAHMAdAB1AGQAeQAsACAAYwBvAHAAeQAsACAAbQBlAHIAZwBlACwAIABlAG0AYgBlAGQALAAgAG0AbwBkAGkAZgB5ACwAIAByAGUAZABpAHMAdAByAGkAYgB1AHQAZQAsACAAYQBuAGQAIABzAGUAbABsACAAbQBvAGQAaQBmAGkAZQBkACAAYQBuAGQAIAB1AG4AbQBvAGQAaQBmAGkAZQBkACAAYwBvAHAAaQBlAHMAIABvAGYAIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALAAgAHMAdQBiAGoAZQBjAHQAIAB0AG8AIAB0AGgAZQAgAGYAbwBsAGwAbwB3AGkAbgBnACAAYwBvAG4AZABpAHQAaQBvAG4AcwA6AA0ADQAKADEAKQAgAE4AZQBpAHQAaABlAHIAIAB0AGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABuAG8AcgAgAGEAbgB5ACAAbwBmACAAaQB0AHMAIABpAG4AZABpAHYAaQBkAHUAYQBsACAAYwBvAG0AcABvAG4AZQBuAHQAcwAsACAAaQBuACAATwByAGkAZwBpAG4AYQBsACAAbwByACAATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgBzACwAIABtAGEAeQAgAGIAZQAgAHMAbwBsAGQAIABiAHkAIABpAHQAcwBlAGwAZgAuAA0ADQAKADIAKQAgAE8AcgBpAGcAaQBuAGEAbAAgAG8AcgAgAE0AbwBkAGkAZgBpAGUAZAAgAFYAZQByAHMAaQBvAG4AcwAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAG0AYQB5ACAAYgBlACAAYgB1AG4AZABsAGUAZAAsACAAcgBlAGQAaQBzAHQAcgBpAGIAdQB0AGUAZAAgAGEAbgBkAC8AbwByACAAcwBvAGwAZAAgAHcAaQB0AGgAIABhAG4AeQAgAHMAbwBmAHQAdwBhAHIAZQAsACAAcAByAG8AdgBpAGQAZQBkACAAdABoAGEAdAAgAGUAYQBjAGgAIABjAG8AcAB5ACAAYwBvAG4AdABhAGkAbgBzACAAdABoAGUAIABhAGIAbwB2AGUAIABjAG8AcAB5AHIAaQBnAGgAdAAgAG4AbwB0AGkAYwBlACAAYQBuAGQAIAB0AGgAaQBzACAAbABpAGMAZQBuAHMAZQAuACAAVABoAGUAcwBlACAAYwBhAG4AIABiAGUAIABpAG4AYwBsAHUAZABlAGQAIABlAGkAdABoAGUAcgAgAGEAcwAgAHMAdABhAG4AZAAtAGEAbABvAG4AZQAgAHQAZQB4AHQAIABmAGkAbABlAHMALAAgAGgAdQBtAGEAbgAtAHIAZQBhAGQAYQBiAGwAZQAgAGgAZQBhAGQAZQByAHMAIABvAHIAIABpAG4AIAB0AGgAZQAgAGEAcABwAHIAbwBwAHIAaQBhAHQAZQAgAG0AYQBjAGgAaQBuAGUALQByAGUAYQBkAGEAYgBsAGUAIABtAGUAdABhAGQAYQB0AGEAIABmAGkAZQBsAGQAcwAgAHcAaQB0AGgAaQBuACAAdABlAHgAdAAgAG8AcgAgAGIAaQBuAGEAcgB5ACAAZgBpAGwAZQBzACAAYQBzACAAbABvAG4AZwAgAGEAcwAgAHQAaABvAHMAZQAgAGYAaQBlAGwAZABzACAAYwBhAG4AIABiAGUAIABlAGEAcwBpAGwAeQAgAHYAaQBlAHcAZQBkACAAYgB5ACAAdABoAGUAIAB1AHMAZQByAC4ADQANAAoAMwApACAATgBvACAATQBvAGQAaQBmAGkAZQBkACAAVgBlAHIAcwBpAG8AbgAgAG8AZgAgAHQAaABlACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAG0AYQB5ACAAdQBzAGUAIAB0AGgAZQAgAFIAZQBzAGUAcgB2AGUAZAAgAEYAbwBuAHQAIABOAGEAbQBlACgAcwApACAAdQBuAGwAZQBzAHMAIABlAHgAcABsAGkAYwBpAHQAIAB3AHIAaQB0AHQAZQBuACAAcABlAHIAbQBpAHMAcwBpAG8AbgAgAGkAcwAgAGcAcgBhAG4AdABlAGQAIABiAHkAIAB0AGgAZQAgAGMAbwByAHIAZQBzAHAAbwBuAGQAaQBuAGcAIABDAG8AcAB5AHIAaQBnAGgAdAAgAEgAbwBsAGQAZQByAC4AIABUAGgAaQBzACAAcgBlAHMAdAByAGkAYwB0AGkAbwBuACAAbwBuAGwAeQAgAGEAcABwAGwAaQBlAHMAIAB0AG8AIAB0AGgAZQAgAHAAcgBpAG0AYQByAHkAIABmAG8AbgB0ACAAbgBhAG0AZQAgAGEAcwAgAHAAcgBlAHMAZQBuAHQAZQBkACAAdABvACAAdABoAGUAIAB1AHMAZQByAHMALgANAA0ACgA0ACkAIABUAGgAZQAgAG4AYQBtAGUAKABzACkAIABvAGYAIAB0AGgAZQAgAEMAbwBwAHkAcgBpAGcAaAB0ACAASABvAGwAZABlAHIAKABzACkAIABvAHIAIAB0AGgAZQAgAEEAdQB0AGgAbwByACgAcwApACAAbwBmACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAcwBoAGEAbABsACAAbgBvAHQAIABiAGUAIAB1AHMAZQBkACAAdABvACAAcAByAG8AbQBvAHQAZQAsACAAZQBuAGQAbwByAHMAZQAgAG8AcgAgAGEAZAB2AGUAcgB0AGkAcwBlACAAYQBuAHkAIABNAG8AZABpAGYAaQBlAGQAIABWAGUAcgBzAGkAbwBuACwAIABlAHgAYwBlAHAAdAAgAHQAbwAgAGEAYwBrAG4AbwB3AGwAZQBkAGcAZQAgAHQAaABlACAAYwBvAG4AdAByAGkAYgB1AHQAaQBvAG4AKABzACkAIABvAGYAIAB0AGgAZQAgAEMAbwBwAHkAcgBpAGcAaAB0ACAASABvAGwAZABlAHIAKABzACkAIABhAG4AZAAgAHQAaABlACAAQQB1AHQAaABvAHIAKABzACkAIABvAHIAIAB3AGkAdABoACAAdABoAGUAaQByACAAZQB4AHAAbABpAGMAaQB0ACAAdwByAGkAdAB0AGUAbgAgAHAAZQByAG0AaQBzAHMAaQBvAG4ALgANAA0ACgA1ACkAIABUAGgAZQAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALAAgAG0AbwBkAGkAZgBpAGUAZAAgAG8AcgAgAHUAbgBtAG8AZABpAGYAaQBlAGQALAAgAGkAbgAgAHAAYQByAHQAIABvAHIAIABpAG4AIAB3AGgAbwBsAGUALAAgAG0AdQBzAHQAIABiAGUAIABkAGkAcwB0AHIAaQBiAHUAdABlAGQAIABlAG4AdABpAHIAZQBsAHkAIAB1AG4AZABlAHIAIAB0AGgAaQBzACAAbABpAGMAZQBuAHMAZQAsACAAYQBuAGQAIABtAHUAcwB0ACAAbgBvAHQAIABiAGUAIABkAGkAcwB0AHIAaQBiAHUAdABlAGQAIAB1AG4AZABlAHIAIABhAG4AeQAgAG8AdABoAGUAcgAgAGwAaQBjAGUAbgBzAGUALgAgAFQAaABlACAAcgBlAHEAdQBpAHIAZQBtAGUAbgB0ACAAZgBvAHIAIABmAG8AbgB0AHMAIAB0AG8AIAByAGUAbQBhAGkAbgAgAHUAbgBkAGUAcgAgAHQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAZABvAGUAcwAgAG4AbwB0ACAAYQBwAHAAbAB5ACAAdABvACAAYQBuAHkAIABkAG8AYwB1AG0AZQBuAHQAIABjAHIAZQBhAHQAZQBkACAAdQBzAGkAbgBnACAAdABoAGUAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlAC4ADQANAFQARQBSAE0ASQBOAEEAVABJAE8ATgANAFQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAYgBlAGMAbwBtAGUAcwAgAG4AdQBsAGwAIABhAG4AZAAgAHYAbwBpAGQAIABpAGYAIABhAG4AeQAgAG8AZgAgAHQAaABlACAAYQBiAG8AdgBlACAAYwBvAG4AZABpAHQAaQBvAG4AcwAgAGEAcgBlACAAbgBvAHQAIABtAGUAdAAuAA0ADQBEAEkAUwBDAEwAQQBJAE0ARQBSAA0AVABIAEUAIABGAE8ATgBUACAAUwBPAEYAVABXAEEAUgBFACAASQBTACAAUABSAE8AVgBJAEQARQBEACAAIgBBAFMAIABJAFMAIgAsACAAVwBJAFQASABPAFUAVAAgAFcAQQBSAFIAQQBOAFQAWQAgAE8ARgAgAEEATgBZACAASwBJAE4ARAAsACAARQBYAFAAUgBFAFMAUwAgAE8AUgAgAEkATQBQAEwASQBFAEQALAAgAEkATgBDAEwAVQBEAEkATgBHACAAQgBVAFQAIABOAE8AVAAgAEwASQBNAEkAVABFAEQAIABUAE8AIABBAE4AWQAgAFcAQQBSAFIAQQBOAFQASQBFAFMAIABPAEYAIABNAEUAUgBDAEgAQQBOAFQAQQBCAEkATABJAFQAWQAsACAARgBJAFQATgBFAFMAUwAgAEYATwBSACAAQQAgAFAAQQBSAFQASQBDAFUATABBAFIAIABQAFUAUgBQAE8AUwBFACAAQQBOAEQAIABOAE8ATgBJAE4ARgBSAEkATgBHAEUATQBFAE4AVAAgAE8ARgAgAEMATwBQAFkAUgBJAEcASABUACwAIABQAEEAVABFAE4AVAAsACAAVABSAEEARABFAE0AQQBSAEsALAAgAE8AUgAgAE8AVABIAEUAUgAgAFIASQBHAEgAVAAuACAASQBOACAATgBPACAARQBWAEUATgBUACAAUwBIAEEATABMACAAVABIAEUAIABDAE8AUABZAFIASQBHAEgAVAAgAEgATwBMAEQARQBSACAAQgBFACAATABJAEEAQgBMAEUAIABGAE8AUgAgAEEATgBZACAAQwBMAEEASQBNACwAIABEAEEATQBBAEcARQBTACAATwBSACAATwBUAEgARQBSACAATABJAEEAQgBJAEwASQBUAFkALAAgAEkATgBDAEwAVQBEAEkATgBHACAAQQBOAFkAIABHAEUATgBFAFIAQQBMACwAIABTAFAARQBDAEkAQQBMACwAIABJAE4ARABJAFIARQBDAFQALAAgAEkATgBDAEkARABFAE4AVABBAEwALAAgAE8AUgAgAEMATwBOAFMARQBRAFUARQBOAFQASQBBAEwAIABEAEEATQBBAEcARQBTACwAIABXAEgARQBUAEgARQBSACAASQBOACAAQQBOACAAQQBDAFQASQBPAE4AIABPAEYAIABDAE8ATgBUAFIAQQBDAFQALAAgAFQATwBSAFQAIABPAFIAIABPAFQASABFAFIAVwBJAFMARQAsACAAQQBSAEkAUwBJAE4ARwAgAEYAUgBPAE0ALAAgAE8AVQBUACAATwBGACAAVABIAEUAIABVAFMARQAgAE8AUgAgAEkATgBBAEIASQBMAEkAVABZACAAVABPACAAVQBTAEUAIABUAEgARQAgAEYATwBOAFQAIABTAE8ARgBUAFcAQQBSAEUAIABPAFIAIABGAFIATwBNACAATwBUAEgARQBSACAARABFAEEATABJAE4ARwBTACAASQBOACAAVABIAEUAIABGAE8ATgBUACAAUwBPAEYAVABXAEEAUgBFAC4AAENvcHlyaWdodCAoYykgMjAxMywgUXVvdGUtVW5xdW90ZSBBcHBzIChodHRwOi8vcXVvdGV1bnF1b3RlYXBwcy5jb20pLCB3aXRoIFJlc2VydmVkIEZvbnQgTmFtZSBDb3VyaWVyIFByaW1lLg0NVGhpcyBGb250IFNvZnR3YXJlIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBTSUwgT3BlbiBGb250IExpY2Vuc2UsIFZlcnNpb24gMS4xLiBUaGlzIGxpY2Vuc2UgaXMgY29waWVkIGJlbG93LCBhbmQgaXMgYWxzbyBhdmFpbGFibGUgd2l0aCBhIEZBUSBhdDogaHR0cDovL3NjcmlwdHMuc2lsLm9yZy9PRkwNDQ0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NU0lMIE9QRU4gRk9OVCBMSUNFTlNFIFZlcnNpb24gMS4xIC0gMjYgRmVicnVhcnkgMjAwNw0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NDVBSRUFNQkxFDVRoZSBnb2FscyBvZiB0aGUgT3BlbiBGb250IExpY2Vuc2UgKE9GTCkgYXJlIHRvIHN0aW11bGF0ZSB3b3JsZHdpZGUgZGV2ZWxvcG1lbnQgb2YgY29sbGFib3JhdGl2ZSBmb250IHByb2plY3RzLCB0byBzdXBwb3J0IHRoZSBmb250IGNyZWF0aW9uIGVmZm9ydHMgb2YgYWNhZGVtaWMgYW5kIGxpbmd1aXN0aWMgY29tbXVuaXRpZXMsIGFuZCB0byBwcm92aWRlIGEgZnJlZSBhbmQgb3BlbiBmcmFtZXdvcmsgaW4gd2hpY2ggZm9udHMgbWF5IGJlIHNoYXJlZCBhbmQgaW1wcm92ZWQgaW4gcGFydG5lcnNoaXAgd2l0aCBvdGhlcnMuDQ1UaGUgT0ZMIGFsbG93cyB0aGUgbGljZW5zZWQgZm9udHMgdG8gYmUgdXNlZCwgc3R1ZGllZCwgbW9kaWZpZWQgYW5kIHJlZGlzdHJpYnV0ZWQgZnJlZWx5IGFzIGxvbmcgYXMgdGhleSBhcmUgbm90IHNvbGQgYnkgdGhlbXNlbHZlcy4gVGhlIGZvbnRzLCBpbmNsdWRpbmcgYW55IGRlcml2YXRpdmUgd29ya3MsIGNhbiBiZSBidW5kbGVkLCBlbWJlZGRlZCwgcmVkaXN0cmlidXRlZCBhbmQvb3Igc29sZCB3aXRoIGFueSBzb2Z0d2FyZSBwcm92aWRlZCB0aGF0IGFueSByZXNlcnZlZCBuYW1lcyBhcmUgbm90IHVzZWQgYnkgZGVyaXZhdGl2ZSB3b3Jrcy4gVGhlIGZvbnRzIGFuZCBkZXJpdmF0aXZlcywgaG93ZXZlciwgY2Fubm90IGJlIHJlbGVhc2VkIHVuZGVyIGFueSBvdGhlciB0eXBlIG9mIGxpY2Vuc2UuIFRoZSByZXF1aXJlbWVudCBmb3IgZm9udHMgdG8gcmVtYWluIHVuZGVyIHRoaXMgbGljZW5zZSBkb2VzIG5vdCBhcHBseSB0byBhbnkgZG9jdW1lbnQgY3JlYXRlZCB1c2luZyB0aGUgZm9udHMgb3IgdGhlaXIgZGVyaXZhdGl2ZXMuDQ1ERUZJTklUSU9OUw0iRm9udCBTb2Z0d2FyZSIgcmVmZXJzIHRvIHRoZSBzZXQgb2YgZmlsZXMgcmVsZWFzZWQgYnkgdGhlIENvcHlyaWdodCBIb2xkZXIocykgdW5kZXIgdGhpcyBsaWNlbnNlIGFuZCBjbGVhcmx5IG1hcmtlZCBhcyBzdWNoLiBUaGlzIG1heSBpbmNsdWRlIHNvdXJjZSBmaWxlcywgYnVpbGQgc2NyaXB0cyBhbmQgZG9jdW1lbnRhdGlvbi4NDSJSZXNlcnZlZCBGb250IE5hbWUiIHJlZmVycyB0byBhbnkgbmFtZXMgc3BlY2lmaWVkIGFzIHN1Y2ggYWZ0ZXIgdGhlIGNvcHlyaWdodCBzdGF0ZW1lbnQocykuDQ0iT3JpZ2luYWwgVmVyc2lvbiIgcmVmZXJzIHRvIHRoZSBjb2xsZWN0aW9uIG9mIEZvbnQgU29mdHdhcmUgY29tcG9uZW50cyBhcyBkaXN0cmlidXRlZCBieSB0aGUgQ29weXJpZ2h0IEhvbGRlcihzKS4NDSJNb2RpZmllZCBWZXJzaW9uIiByZWZlcnMgdG8gYW55IGRlcml2YXRpdmUgbWFkZSBieSBhZGRpbmcgdG8sIGRlbGV0aW5nLCBvciBzdWJzdGl0dXRpbmcgLS0gaW4gcGFydCBvciBpbiB3aG9sZSAtLSBhbnkgb2YgdGhlIGNvbXBvbmVudHMgb2YgdGhlIE9yaWdpbmFsIFZlcnNpb24sIGJ5IGNoYW5naW5nIGZvcm1hdHMgb3IgYnkgcG9ydGluZyB0aGUgRm9udCBTb2Z0d2FyZSB0byBhIG5ldyBlbnZpcm9ubWVudC4NDSJBdXRob3IiIHJlZmVycyB0byBhbnkgZGVzaWduZXIsIGVuZ2luZWVyLCBwcm9ncmFtbWVyLCB0ZWNobmljYWwgd3JpdGVyIG9yIG90aGVyIHBlcnNvbiB3aG8gY29udHJpYnV0ZWQgdG8gdGhlIEZvbnQgU29mdHdhcmUuDQ1QRVJNSVNTSU9OICYgQ09ORElUSU9OUw1QZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5IG9mIHRoZSBGb250IFNvZnR3YXJlLCB0byB1c2UsIHN0dWR5LCBjb3B5LCBtZXJnZSwgZW1iZWQsIG1vZGlmeSwgcmVkaXN0cmlidXRlLCBhbmQgc2VsbCBtb2RpZmllZCBhbmQgdW5tb2RpZmllZCBjb3BpZXMgb2YgdGhlIEZvbnQgU29mdHdhcmUsIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOg0NCjEpIE5laXRoZXIgdGhlIEZvbnQgU29mdHdhcmUgbm9yIGFueSBvZiBpdHMgaW5kaXZpZHVhbCBjb21wb25lbnRzLCBpbiBPcmlnaW5hbCBvciBNb2RpZmllZCBWZXJzaW9ucywgbWF5IGJlIHNvbGQgYnkgaXRzZWxmLg0NCjIpIE9yaWdpbmFsIG9yIE1vZGlmaWVkIFZlcnNpb25zIG9mIHRoZSBGb250IFNvZnR3YXJlIG1heSBiZSBidW5kbGVkLCByZWRpc3RyaWJ1dGVkIGFuZC9vciBzb2xkIHdpdGggYW55IHNvZnR3YXJlLCBwcm92aWRlZCB0aGF0IGVhY2ggY29weSBjb250YWlucyB0aGUgYWJvdmUgY29weXJpZ2h0IG5vdGljZSBhbmQgdGhpcyBsaWNlbnNlLiBUaGVzZSBjYW4gYmUgaW5jbHVkZWQgZWl0aGVyIGFzIHN0YW5kLWFsb25lIHRleHQgZmlsZXMsIGh1bWFuLXJlYWRhYmxlIGhlYWRlcnMgb3IgaW4gdGhlIGFwcHJvcHJpYXRlIG1hY2hpbmUtcmVhZGFibGUgbWV0YWRhdGEgZmllbGRzIHdpdGhpbiB0ZXh0IG9yIGJpbmFyeSBmaWxlcyBhcyBsb25nIGFzIHRob3NlIGZpZWxkcyBjYW4gYmUgZWFzaWx5IHZpZXdlZCBieSB0aGUgdXNlci4NDQozKSBObyBNb2RpZmllZCBWZXJzaW9uIG9mIHRoZSBGb250IFNvZnR3YXJlIG1heSB1c2UgdGhlIFJlc2VydmVkIEZvbnQgTmFtZShzKSB1bmxlc3MgZXhwbGljaXQgd3JpdHRlbiBwZXJtaXNzaW9uIGlzIGdyYW50ZWQgYnkgdGhlIGNvcnJlc3BvbmRpbmcgQ29weXJpZ2h0IEhvbGRlci4gVGhpcyByZXN0cmljdGlvbiBvbmx5IGFwcGxpZXMgdG8gdGhlIHByaW1hcnkgZm9udCBuYW1lIGFzIHByZXNlbnRlZCB0byB0aGUgdXNlcnMuDQ0KNCkgVGhlIG5hbWUocykgb2YgdGhlIENvcHlyaWdodCBIb2xkZXIocykgb3IgdGhlIEF1dGhvcihzKSBvZiB0aGUgRm9udCBTb2Z0d2FyZSBzaGFsbCBub3QgYmUgdXNlZCB0byBwcm9tb3RlLCBlbmRvcnNlIG9yIGFkdmVydGlzZSBhbnkgTW9kaWZpZWQgVmVyc2lvbiwgZXhjZXB0IHRvIGFja25vd2xlZGdlIHRoZSBjb250cmlidXRpb24ocykgb2YgdGhlIENvcHlyaWdodCBIb2xkZXIocykgYW5kIHRoZSBBdXRob3Iocykgb3Igd2l0aCB0aGVpciBleHBsaWNpdCB3cml0dGVuIHBlcm1pc3Npb24uDQ0KNSkgVGhlIEZvbnQgU29mdHdhcmUsIG1vZGlmaWVkIG9yIHVubW9kaWZpZWQsIGluIHBhcnQgb3IgaW4gd2hvbGUsIG11c3QgYmUgZGlzdHJpYnV0ZWQgZW50aXJlbHkgdW5kZXIgdGhpcyBsaWNlbnNlLCBhbmQgbXVzdCBub3QgYmUgZGlzdHJpYnV0ZWQgdW5kZXIgYW55IG90aGVyIGxpY2Vuc2UuIFRoZSByZXF1aXJlbWVudCBmb3IgZm9udHMgdG8gcmVtYWluIHVuZGVyIHRoaXMgbGljZW5zZSBkb2VzIG5vdCBhcHBseSB0byBhbnkgZG9jdW1lbnQgY3JlYXRlZCB1c2luZyB0aGUgRm9udCBTb2Z0d2FyZS4NDVRFUk1JTkFUSU9ODVRoaXMgbGljZW5zZSBiZWNvbWVzIG51bGwgYW5kIHZvaWQgaWYgYW55IG9mIHRoZSBhYm92ZSBjb25kaXRpb25zIGFyZSBub3QgbWV0Lg0NRElTQ0xBSU1FUg1USEUgRk9OVCBTT0ZUV0FSRSBJUyBQUk9WSURFRCAiQVMgSVMiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTIE9SIElNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gQU5ZIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZLCBGSVRORVNTIEZPUiBBIFBBUlRJQ1VMQVIgUFVSUE9TRSBBTkQgTk9OSU5GUklOR0VNRU5UIE9GIENPUFlSSUdIVCwgUEFURU5ULCBUUkFERU1BUkssIE9SIE9USEVSIFJJR0hULiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUgQ09QWVJJR0hUIEhPTERFUiBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSwgREFNQUdFUyBPUiBPVEhFUiBMSUFCSUxJVFksIElOQ0xVRElORyBBTlkgR0VORVJBTCwgU1BFQ0lBTCwgSU5ESVJFQ1QsIElOQ0lERU5UQUwsIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUywgV0hFVEhFUiBJTiBBTiBBQ1RJT04gT0YgQ09OVFJBQ1QsIFRPUlQgT1IgT1RIRVJXSVNFLCBBUklTSU5HIEZST00sIE9VVCBPRiBUSEUgVVNFIE9SIElOQUJJTElUWSBUTyBVU0UgVEhFIEZPTlQgU09GVFdBUkUgT1IgRlJPTSBPVEhFUiBERUFMSU5HUyBJTiBUSEUgRk9OVCBTT0ZUV0FSRS4AAGgAdAB0AHAAOgAvAC8AcwBjAHIAaQBwAHQAcwAuAHMAaQBsAC4AbwByAGcALwBPAEYATAAAaHR0cDovL3NjcmlwdHMuc2lsLm9yZy9PRkwAAE0AZQBjAGgAYQBuAGkAYwBhAGwAAE1lY2hhbmljYWwAAAAAAgAAAAAAAP9oAD8AAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAQACAQIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAAxADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0ATgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAQMAowCEAIUAlgDoAIYAjgCLAJ0AqQCkAIoA2gCDAJMA8gDzAI0AiADDAN4A8QCeAKoA9QD0APYAogCtAMkAxwCuAGIAYwCQAGQAywBlAMgAygDPAMwAzQDOAOkAZgDTANAA0QCvAGcA8ACRANYA1ADVAGgA6wDtAIkAagBpAGsAbQBsAG4AoABvAHEAcAByAHMAdQB0AHYAdwDqAHgAegB5AHsAfQB8ALgAoQB/AH4AgACBAOwA7gC6AQQBBQEGAQcBCAEJAP0A/gEKAQsBDAENAP8BAAEOAQ8BEAEBAREBEgETARQBFQEWARcBGAEZARoBGwEcAPgA+QEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAPoA1wEtAS4BLwEwATEBMgEzATQBNQE2ATcBOAE5AToA4gDjATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcAsACxAUgBSQFKAUsBTAFNAU4BTwFQAVEA+wD8AOQA5QFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnALsBaAFpAWoBawDmAOcBbACmAW0BbgFvANgA4QDbANwA3QDgANkA3wFwAXEBcgFzAXQBdQF2AXcAsgCzALYAtwDEALQAtQDFAIIAwgCHAKsAxgC+AL8AvAF4APcBeQF6AXsAjACoAJkA7wCcAKcAjwCUAJUBfADAAMEBfQF+AX8BgAGBAYIBgwGEAYUBhgGHAYgBiQGKBE5VTEwHdW5pMDBBMAdBbWFjcm9uB2FtYWNyb24GQWJyZXZlBmFicmV2ZQdBb2dvbmVrB2FvZ29uZWsLQ2NpcmN1bWZsZXgLY2NpcmN1bWZsZXgKQ2RvdGFjY2VudApjZG90YWNjZW50BkRjYXJvbgZkY2Fyb24GRGNyb2F0B0VtYWNyb24HZW1hY3JvbgZFYnJldmUGZWJyZXZlCkVkb3RhY2NlbnQKZWRvdGFjY2VudAdFb2dvbmVrB2VvZ29uZWsGRWNhcm9uBmVjYXJvbgtHY2lyY3VtZmxleAtnY2lyY3VtZmxleApHZG90YWNjZW50Cmdkb3RhY2NlbnQMR2NvbW1hYWNjZW50DGdjb21tYWFjY2VudAtIY2lyY3VtZmxleAtoY2lyY3VtZmxleARIYmFyBGhiYXIGSXRpbGRlBml0aWxkZQdJbWFjcm9uB2ltYWNyb24GSWJyZXZlBmlicmV2ZQdJb2dvbmVrB2lvZ29uZWsCSUoCaWoLSmNpcmN1bWZsZXgLamNpcmN1bWZsZXgMS2NvbW1hYWNjZW50DGtjb21tYWFjY2VudAZMYWN1dGUGbGFjdXRlDExjb21tYWFjY2VudAxsY29tbWFhY2NlbnQGTGNhcm9uBmxjYXJvbgRMZG90BGxkb3QGTmFjdXRlBm5hY3V0ZQxOY29tbWFhY2NlbnQMbmNvbW1hYWNjZW50Bk5jYXJvbgZuY2Fyb24LbmFwb3N0cm9waGUHT21hY3JvbgdvbWFjcm9uBk9icmV2ZQZvYnJldmUNT2h1bmdhcnVtbGF1dA1vaHVuZ2FydW1sYXV0BlJhY3V0ZQZyYWN1dGUMUmNvbW1hYWNjZW50DHJjb21tYWFjY2VudAZSY2Fyb24GcmNhcm9uBlNhY3V0ZQZzYWN1dGULU2NpcmN1bWZsZXgLc2NpcmN1bWZsZXgMVGNvbW1hYWNjZW50DHRjb21tYWFjY2VudAZUY2Fyb24GdGNhcm9uBFRiYXIEdGJhcgZVdGlsZGUGdXRpbGRlB1VtYWNyb24HdW1hY3JvbgZVYnJldmUGdWJyZXZlBVVyaW5nBXVyaW5nDVVodW5nYXJ1bWxhdXQNdWh1bmdhcnVtbGF1dAdVb2dvbmVrB3VvZ29uZWsLV2NpcmN1bWZsZXgLd2NpcmN1bWZsZXgLWWNpcmN1bWZsZXgLeWNpcmN1bWZsZXgGWmFjdXRlBnphY3V0ZQpaZG90YWNjZW50Cnpkb3RhY2NlbnQFbG9uZ3MMU2NvbW1hYWNjZW50DHNjb21tYWFjY2VudAhkb3RsZXNzagZXZ3JhdmUGd2dyYXZlBldhY3V0ZQZ3YWN1dGUJV2RpZXJlc2lzCXdkaWVyZXNpcwZZZ3JhdmUGeWdyYXZlDGZvdXJzdXBlcmlvcgRFdXJvB3VuaTIwQkQHdW5pMjEyMQtjb21tYWFjY2VudApwZXJpb2QuYWx0CWNvbW1hLmFsdAljb2xvbi5hbHQNc2VtaWNvbG9uLmFsdA1lbGxpcHNpcy5hbHQxDWVsbGlwc2lzLmFsdDINZWxsaXBzaXMuYWx0Mw1lbGxpcHNpcy5hbHQ0DWVsbGlwc2lzLmFsdDULZW1kYXNoLmFsdDELZW1kYXNoLmFsdDILZW1kYXNoLmFsdDMLZW1kYXNoLmFsdDQKaHlwaGVuLmFsdAAAAAAAAAH//wACAAEAAAAMAAAAEAAAAAIAAAAEAAAAAgAAAAAAAQAAAADaU5nwAAAAAM0dv6AAAAAA0byBVA=="))).CreateFont(12,FontStyle.Regular); - internal void DrawBlackBoxWithWhiteText(DicomDataset ds, int width, int height, string msg) + internal static void DrawBlackBoxWithWhiteText(DicomDataset ds, int width, int height, string msg) { - using ImageSharpImage img=new(500,500); - img.Render(0,false,false,0); - img.AsSharpImage().Mutate(x=>x.Fill(Color.Black)); - img.AsSharpImage().Mutate(x=>x.DrawText(msg,Font,Color.White,new PointF(250,100))); - var memory = new Span(new byte[img.Pixels.ByteSize]); - img.RenderedImage.CloneAs().CopyPixelDataTo(memory); - MemoryByteBuffer buffer = new(memory.ToArray()); + var buffer=new byte[width * height * 3]; + using var img=new Image(width, height); + img.Mutate(x => x.Fill(Color.Black)); + img.Mutate(x=>x.DrawText(msg,Font,Color.White,new PointF(width/2f,height/2f))); + img.CopyPixelDataTo(buffer); ds.Add(DicomTag.PhotometricInterpretation, PhotometricInterpretation.Rgb.Value); ds.Add(DicomTag.Rows, (ushort)img.Height); ds.Add(DicomTag.Columns, (ushort)img.Width); ds.Add(DicomTag.BitsAllocated, (ushort)8); - DicomPixelData pixelData = DicomPixelData.Create(ds, true); + var pixelData = DicomPixelData.Create(ds, true); pixelData.BitsStored = 8; pixelData.SamplesPerPixel = 3; pixelData.HighBit = 7; pixelData.PixelRepresentation = 0; pixelData.PlanarConfiguration = 0; - pixelData.AddFrame(buffer); + pixelData.AddFrame(new MemoryByteBuffer(buffer)); } } \ No newline at end of file diff --git a/BadMedicine.Dicom/Series.cs b/BadMedicine.Dicom/Series.cs index 34a24c1..51c811e 100644 --- a/BadMedicine.Dicom/Series.cs +++ b/BadMedicine.Dicom/Series.cs @@ -4,130 +4,125 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// Data class representing a single dicom series. Stores +/// the DICOM tags that fit at the series level hierarchy +/// (and are modelled by BadMedicine.Dicom). +/// +public class Series : IEnumerable { /// - /// Data class representing a single dicom series. Stores - /// the DICOM tags that fit at the series level hierarchy - /// (and are modelled by BadMedicine.Dicom). + /// The unique identifier for this series /// - public class Series : IEnumerable - { - /// - /// The unique identifier for this series - /// - public DicomUID SeriesUID {get; } + public DicomUID SeriesUID {get; } - /// - /// The Dicom Study this series is a part of - /// - public Study Study{get; } - - /// - /// All dicom images generated for this series. These can be - /// written out to file by other processes and do not yet exist - /// on disk. - /// - public IReadOnlyList Datasets{get; } - - private readonly List _datasets = new(); + /// + /// The Dicom Study this series is a part of + /// + public Study Study{get; } + + /// + /// All dicom images generated for this series. These can be + /// written out to file by other processes and do not yet exist + /// on disk. + /// + public IReadOnlyList Datasets{get; } + + private readonly List _datasets = new(); - /// - /// Patient level information generated by BadMedicine.Dicom for whom - /// the study exists. - /// - public Person person; - - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public string Modality {get; } - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public string ImageType {get; } - - /// - /// Date to use for the when writting - /// out to dicom datasets - /// - public DateTime SeriesDate { get; internal set; } - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public TimeSpan SeriesTime { get; internal set; } - - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public int NumberOfSeriesRelatedInstances { get; } - - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public string SeriesDescription { get; } - - - /// - /// Value to use for the when writting - /// out to dicom datasets - /// - public string BodyPartExamined { get; } - - internal Series(Study study, Person person, string modality, string imageType, int imageCount, DescBodyPart part = null) - { - SeriesUID = UIDAllocator.GenerateSeriesInstanceUID(); - - this.Study = study; - this.person = person; - this.Modality = modality; - this.ImageType = imageType; - this.NumberOfSeriesRelatedInstances = imageCount; - - //todo: for now just use the Study date, in theory secondary capture images could be generated later - SeriesDate = study.StudyDate; - SeriesTime = study.StudyTime; - - if(part != null) - { - SeriesDescription = part.SeriesDescription; - BodyPartExamined = part.BodyPartExamined; - } + /// + /// Patient level information generated by BadMedicine.Dicom for whom + /// the study exists. + /// + public Person person; + + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public string Modality {get; } + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public string ImageType {get; } + + /// + /// Date to use for the when writing + /// out to dicom datasets + /// + public DateTime SeriesDate { get; } + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public TimeSpan SeriesTime { get; } + + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public int NumberOfSeriesRelatedInstances { get; } + + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public string? SeriesDescription { get; } + + + /// + /// Value to use for the when writing + /// out to dicom datasets + /// + public string? BodyPartExamined { get; } + + internal Series(Study study, Person person, string modality, string imageType, int imageCount, DescBodyPart? part = null) + { + SeriesUID = UIDAllocator.GenerateSeriesInstanceUID(); + + Study = study; + this.person = person; + Modality = modality; + ImageType = imageType; + NumberOfSeriesRelatedInstances = imageCount; + //todo: for now just use the Study date, in theory secondary capture images could be generated later + SeriesDate = study.StudyDate; + SeriesTime = study.StudyTime; - for (int i =0 ; i(_datasets); - } + Datasets = new ReadOnlyCollection(_datasets); + } - /// - /// Returns as IEnumerable - /// - /// - public IEnumerator GetEnumerator() - { - return _datasets.GetEnumerator(); - } - - /// - /// Returns as IEnumerator - /// - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _datasets.GetEnumerator(); - } + /// + /// Returns as IEnumerable + /// + /// + public IEnumerator GetEnumerator() + { + return _datasets.GetEnumerator(); + } + + /// + /// Returns as IEnumerator + /// + /// + IEnumerator IEnumerable.GetEnumerator() + { + return _datasets.GetEnumerator(); } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom/Study.cs b/BadMedicine.Dicom/Study.cs index 737a725..41ecd9c 100644 --- a/BadMedicine.Dicom/Study.cs +++ b/BadMedicine.Dicom/Study.cs @@ -3,141 +3,140 @@ using System.Collections; using System.Collections.Generic; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// Represents a whole DICOM Study (a collection of Series objects). +/// Stores the DICOM tags that fit at the study/patient level hierarchy +/// (and are modelled by BadMedicine.Dicom). +/// +public class Study : IEnumerable { /// - /// Represents a whole DICOM Study (a collection of Series objects). - /// Stores the DICOM tags that fit at the study/patient level hierarchy - /// (and are modelled by BadMedicine.Dicom). + /// The Series objects within this Study /// - public class Study : IEnumerable - { - /// - /// The Series objects within this Study - /// - public IReadOnlyList Series => _series.AsReadOnly(); - - /// - /// The DicomDataGenerator which created this Study - /// - public DicomDataGenerator Parent; + public IReadOnlyList Series => _series.AsReadOnly(); + + /// + /// The DicomDataGenerator which created this Study + /// + public DicomDataGenerator Parent; - /// - /// The DICOM UID of this Study - /// - public DicomUID StudyUID{get; } - - /// - /// The timestamp on this Study - /// - public DateTime StudyDate { get; internal set; } - - /// - /// Free-text description of this Study - /// - public string StudyDescription {get; } - /// - /// The Accession Number for this Study, usually used to associate the study with clinical data in the RIS - /// - public string AccessionNumber { get; } - /// - /// Starting time of the Study, empty if unknown - /// - public TimeSpan StudyTime { get; } - - /// - /// Count of Instances within this Study - /// - public int NumberOfStudyRelatedInstances { get; } - - private readonly List _series = new(); - - /// - /// Constructor for a new Study on a specified Person - /// - /// The DicomDataGenerator creating this Study - /// The Person representing this patient - /// Statistical distributions to use - /// Seeded PRNG to use - public Study(DicomDataGenerator parent, Person person, ModalityStats modalityStats, Random r) - { - /////////////////////// Generate all the Study Values //////////////////// - Parent = parent; - StudyUID = UIDAllocator.GenerateStudyInstanceUID(); - StudyDate = person.GetRandomDateDuringLifetime(r).Date; + /// + /// The DICOM UID of this Study + /// + public DicomUID StudyUID{get; } - var stats = DicomDataGeneratorStats.GetInstance(r); + /// + /// The timestamp on this Study + /// + public DateTime StudyDate { get; internal set; } - string imageType; - NumberOfStudyRelatedInstances = 1; - int imageCount = 2; + /// + /// Free-text description of this Study + /// + public string? StudyDescription {get; } + /// + /// The Accession Number for this Study, usually used to associate the study with clinical data in the RIS + /// + public string AccessionNumber { get; } + /// + /// Starting time of the Study, empty if unknown + /// + public TimeSpan StudyTime { get; } - //if we know about the frequency of tag values for this modality? - if(stats.TagValuesByModalityAndTag.ContainsKey(modalityStats.Modality)) - foreach(KeyValuePair> dict in stats.TagValuesByModalityAndTag[modalityStats.Modality]) - { - //for each tag we know about + /// + /// Count of Instances within this Study + /// + public int NumberOfStudyRelatedInstances { get; } - //if it's a study level one record it here - if(dict.Key == DicomTag.StudyDescription) - StudyDescription = dict.Value.GetRandom(r); - } + private readonly List _series = new(); - AccessionNumber = stats.GetRandomAccessionNumber(r); - StudyTime = stats.GetRandomTimeOfDay(r); + /// + /// Constructor for a new Study on a specified Person + /// + /// The DicomDataGenerator creating this Study + /// The Person representing this patient + /// Statistical distributions to use + /// Seeded PRNG to use + public Study(DicomDataGenerator parent, Person person, ModalityStats modalityStats, Random r) + { + /////////////////////// Generate all the Study Values //////////////////// + Parent = parent; + StudyUID = UIDAllocator.GenerateStudyInstanceUID(); + StudyDate = person.GetRandomDateDuringLifetime(r).Date; - ///////////////////// Generate all the Series (will also generate images) ///////////////////// - - //have a random number of series (based on average and standard deviation for that modality) - //but have at least 1 series! + var stats = DicomDataGeneratorStats.GetInstance(); - if(modalityStats.Modality == "CT") - { - // Set ImageType - imageType = stats.GetRandomImageType(r); - if(imageType == "ORIGINAL\\PRIMARY\\AXIAL") - { - NumberOfStudyRelatedInstances = Math.Max(1,(int)modalityStats.SeriesPerStudyNormal.Sample()); - imageCount = Math.Max(1,(int)modalityStats.ImagesPerSeriesNormal.Sample()); - } - } - else + string imageType; + NumberOfStudyRelatedInstances = 1; + var imageCount = 2; + + //if we know about the frequency of tag values for this modality? + if(stats.TagValuesByModalityAndTag.TryGetValue(modalityStats.Modality, out var tag)) + foreach(var (key, value) in tag) { - imageType = "ORIGINAL\\PRIMARY"; - NumberOfStudyRelatedInstances = Math.Max(1,(int)modalityStats.SeriesPerStudyNormal.Sample()); - imageCount = Math.Max(1,(int)modalityStats.ImagesPerSeriesNormal.Sample()); + //for each tag we know about + + //if it's a study level one record it here + if(key == DicomTag.StudyDescription) + StudyDescription = value.GetRandom(r); } - // see if we have a better StudyDescription / SeriesDescription / BodyPart value set for - // this modality - DescBodyPart part = null; + AccessionNumber = DicomDataGeneratorStats.GetRandomAccessionNumber(r); + StudyTime = DicomDataGeneratorStats.Instance.GetRandomTimeOfDay(r); + + ///////////////////// Generate all the Series (will also generate images) ///////////////////// + + //have a random number of series (based on average and standard deviation for that modality) + //but have at least 1 series! - if (stats.DescBodyPartsByModality.ContainsKey(modalityStats.Modality)) + if(modalityStats.Modality == "CT") + { + // Set ImageType + imageType = DicomDataGeneratorStats.Instance.GetRandomImageType(r); + if(imageType == "ORIGINAL\\PRIMARY\\AXIAL") { - part = stats.DescBodyPartsByModality[modalityStats.Modality].GetRandom(r); - StudyDescription = part.StudyDescription; + NumberOfStudyRelatedInstances = Math.Max(1,(int)modalityStats.SeriesPerStudyNormal.Sample()); + imageCount = Math.Max(1,(int)modalityStats.ImagesPerSeriesNormal.Sample()); } - - for (int i=0;i - /// Returns enumeration of - /// - /// - public IEnumerator GetEnumerator() + else { - return _series.GetEnumerator(); + imageType = "ORIGINAL\\PRIMARY"; + NumberOfStudyRelatedInstances = Math.Max(1,(int)modalityStats.SeriesPerStudyNormal.Sample()); + imageCount = Math.Max(1,(int)modalityStats.ImagesPerSeriesNormal.Sample()); } - /// - /// Returns IEnumerable of - /// - /// - IEnumerator IEnumerable.GetEnumerator() + // see if we have a better StudyDescription / SeriesDescription / BodyPart value set for + // this modality + DescBodyPart? part = null; + + if (stats.DescBodyPartsByModality.TryGetValue(modalityStats.Modality, out var stat)) { - return _series.GetEnumerator(); + part = stat.GetRandom(r); + StudyDescription = part?.StudyDescription; } + + for (var i=0;i + /// Returns enumeration of + /// + /// + public IEnumerator GetEnumerator() + { + return _series.GetEnumerator(); + } + /// + /// Returns IEnumerable of + /// + /// + + IEnumerator IEnumerable.GetEnumerator() + { + return _series.GetEnumerator(); } -} +} \ No newline at end of file diff --git a/BadMedicine.Dicom/UIDAllocator.cs b/BadMedicine.Dicom/UIDAllocator.cs index f4382a7..6d03b82 100644 --- a/BadMedicine.Dicom/UIDAllocator.cs +++ b/BadMedicine.Dicom/UIDAllocator.cs @@ -1,67 +1,55 @@ using FellowOakDicom; using System.Collections.Concurrent; -namespace BadMedicine.Dicom +namespace BadMedicine.Dicom; + +/// +/// Allocates values from an explicit list(s) or +/// by calling . +/// +public class UIDAllocator { /// - /// Allocates values from an explicit list(s) or - /// by calling . + /// Explicit string values to use when allocating uids for studies /// - public class UIDAllocator - { - /// - /// Explicit string values to use when allocating uids for studies - /// - public static ConcurrentQueue StudyUIDs = new (); - - - /// - /// Explicit string values to use when allocating uids for series - /// - public static ConcurrentQueue SeriesUIDs = new(); - - /// - /// Explicit string values to use when allocating uids for images - /// - public static ConcurrentQueue SOPUIDs = new(); + public static readonly ConcurrentQueue StudyUIDs = new (); - /// - /// Returns a new from or allocated - /// with - /// - /// - public static DicomUID GenerateStudyInstanceUID() - { - if (StudyUIDs.TryDequeue(out string result)) - return new DicomUID(result, "Local UID", DicomUidType.Unknown); - return DicomUID.Generate(); - } + /// + /// Explicit string values to use when allocating uids for series + /// + public static readonly ConcurrentQueue SeriesUIDs = new(); - /// - /// Returns a new from or allocated - /// with - /// - /// - public static DicomUID GenerateSeriesInstanceUID() - { - if (SeriesUIDs.TryDequeue(out string result)) - return new DicomUID(result, "Local UID", DicomUidType.Unknown); + /// + /// Explicit string values to use when allocating uids for images + /// + public static readonly ConcurrentQueue SOPUIDs = new(); - return DicomUID.Generate(); - } + /// + /// Returns a new from or allocated + /// with + /// + /// + public static DicomUID GenerateStudyInstanceUID() => + StudyUIDs.TryDequeue(out var result) + ? new DicomUID(result, "Local UID", DicomUidType.Unknown) + : DicomUID.Generate(); - /// - /// Returns a new from or allocated - /// with - /// - /// - public static DicomUID GenerateSOPInstanceUID() - { - if (SOPUIDs.TryDequeue(out string result)) - return new DicomUID(result, "Local UID", DicomUidType.Unknown); + /// + /// Returns a new from or allocated + /// with + /// + /// + public static DicomUID GenerateSeriesInstanceUID() => SeriesUIDs.TryDequeue(out var result) + ? new DicomUID(result, "Local UID", DicomUidType.Unknown) + : DicomUID.Generate(); - return DicomUID.Generate(); - } - } -} + /// + /// Returns a new from or allocated + /// with + /// + /// + public static DicomUID GenerateSOPInstanceUID() => SOPUIDs.TryDequeue(out var result) + ? new DicomUID(result, "Local UID", DicomUidType.Unknown) + : DicomUID.Generate(); +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6a47b..1725afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ... +## [0.0.16] - 2023-10-04 + +### Dependencies + +- Bump HIC.BadMedicine from 1.1.0 to 1.1.1 +- Bump SixLabors.ImageSharp.Drawing from 1.0.0-beta15 to 2.0.0 +- Bump SixLabors.ImageSharp from 2.1.3 to 3.0.2 + ## [0.0.15] - 2022-10-31 ### Dependencies @@ -27,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.13] - 2022-06-02 -- Fixed SpiralPitchFactor illegal value of 0.0 [#107](https://github.com/HicServices/BadMedicine.Dicom/issues/107) +- Fixed SpiralPitchFactor illegal value of 0.0 [#107](https://github.com/SMI/BadMedicine.Dicom/issues/107) - Added support for specifying explicit UIDs to use when generating images - Added linked statistics for frequency of StudyDescription, SeriesDescription and BodyPartExamined for CT - Adds SeriesDescription and BodyPartExamined as new tags now modelled @@ -144,19 +152,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for PatientAge, Modality, Address, UIDs, StudyDate/Time - Support for pixel data / NoPixels flag -[Unreleased]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.15...develop -[0.0.15]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.14...v0.0.15 -[0.0.14]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.13...v0.0.14 -[0.0.13]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.12...v0.0.13 -[0.0.12]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.11...v0.0.12 -[0.0.11]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.10...v0.0.11 -[0.0.10]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.9...v0.0.10 -[0.0.9]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.8...v0.0.9 -[0.0.8]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.7...v0.0.8 -[0.0.7]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.6...v0.0.7 -[0.0.6]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.5...v0.0.6 -[0.0.5]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.4...v0.0.5 -[0.0.4]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.3...v0.0.4 -[0.0.3]: https://github.com/HicServices/BadMedicine.Dicom/compare/5517d7e29aaf3742e91b86288b85f692a063dba4...v0.0.3 -[0.0.2]: https://github.com/HicServices/BadMedicine.Dicom/compare/v0.0.1...5517d7e29aaf3742e91b86288b85f692a063dba4 -[0.0.1]: https://github.com/HicServices/BadMedicine.Dicom/compare/bdea963df0337e47434c3e72bde7a16a111b99a8...v0.0.1 +[Unreleased]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.15...develop +[0.0.15]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.14...v0.0.15 +[0.0.14]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.13...v0.0.14 +[0.0.13]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.12...v0.0.13 +[0.0.12]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.11...v0.0.12 +[0.0.11]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.10...v0.0.11 +[0.0.10]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.9...v0.0.10 +[0.0.9]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.8...v0.0.9 +[0.0.8]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.7...v0.0.8 +[0.0.7]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.6...v0.0.7 +[0.0.6]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.5...v0.0.6 +[0.0.5]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.4...v0.0.5 +[0.0.4]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.3...v0.0.4 +[0.0.3]: https://github.com/SMI/BadMedicine.Dicom/compare/5517d7e29aaf3742e91b86288b85f692a063dba4...v0.0.3 +[0.0.2]: https://github.com/SMI/BadMedicine.Dicom/compare/v0.0.1...5517d7e29aaf3742e91b86288b85f692a063dba4 +[0.0.1]: https://github.com/SMI/BadMedicine.Dicom/compare/bdea963df0337e47434c3e72bde7a16a111b99a8...v0.0.1 diff --git a/Packages.md b/Packages.md index 235f401..d69b8c2 100644 --- a/Packages.md +++ b/Packages.md @@ -7,20 +7,21 @@ 2. This package is widely used and is actively maintained. 3. It is open source. -| Package | Source Code | Version | License | Purpose | Additional Risk Assessment | -| ------- | ------------| --------| ------- | ------- | -------------------------- | -| HIC.BadMedicine | [GitHub](https://github.com/HicServices/BadMedicine) | [1.1.1](https://www.nuget.org/packages/HIC.BadMedicine/1.1.1) | [GPL 3.0](https://www.gnu.org/licenses/gpl-3.0.html) | Handles generating baseline random data (patient date of birth, CHI numberts etc)| | -| fo-dicom | [GitHub](https://github.com/fo-dicom/fo-dicom) |[5.0.3](https://www.nuget.org/packages/fo-dicom/5.0.3)|[MS-PL](https://opensource.org/licenses/MS-PL) | Handles reading/writing dicom tags from dicom datasets | | -| fo-dicom.Imaging.ImageSharp | [GitHub](https://github.com/fo-dicom/fo-dicom) |[5.0.3](https://www.nuget.org/packages/fo-dicom/5.0.3)|[MS-PL](https://opensource.org/licenses/MS-PL) | Handles imaging aspects with fo-dicom | | -| SixLabors.ImageSharp | [GitHub](https://github.com/SixLabors/ImageSharp) | [2.1.3](https://www.nuget.org/packages/SixLabors.ImageSharp/2.1.3) | [Apache 2.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) | Platform-independent replacement for legacy Windows-only System.Drawing.Common | | -| SixLabors.ImageSharp.Drawing | [GitHub](https://github.com/SixLabors/ImageSharp.Drawing) | [1.0.0-beta15](https://www.nuget.org/packages/SixLabors.ImageSharp.Drawing/1.0.0-beta15) | [Apache 2.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) | Font handling for ImageSharp | | -| Microsoft.SourceLink.GitHub | [GitHub](https://github.com/dotnet/sourcelink) | [1.1.1](https://www.nuget.org/packages/Microsoft.SourceLink.GitHub/1.1.1) | [Apache License 2.0](https://github.com/dotnet/sourcelink/blob/master/License.txt) | Enables source debugging of project nuget package| | -| Microsoft.NETCore.App | [GitHub](https://github.com/dotnet/runtime) | [2.2.8](https://www.nuget.org/packages/Microsoft.NETCore.App/2.2.8) |[MIT](https://opensource.org/licenses/MIT) | | .Net Core API| -| HIC.DicomTypeTranslation | [GitHub](https://github.com/HicServices/DicomTypeTranslation) | [4.0.1](https://www.nuget.org/packages/HIC.DicomTypeTranslation/4.0.1) | [GPL 3.0](https://github.com/HicServices/DicomTypeTranslation/blob/master/LICENSE) | | | -| YamlDotNet | [GitHub](https://github.com/aaubry/YamlDotNet) | [12.0.2](https://www.nuget.org/packages/YamlDotNet/12.0.2) | [MIT](https://opensource.org/licenses/MIT) |Loading configuration files| -| CommandLineParser | [GitHub](https://github.com/commandlineparser/commandline) | [2.9.1](https://www.nuget.org/packages/CommandLineParser/2.9.1) | [MIT](https://opensource.org/licenses/MIT) | Allows command line arguments for main client application and CLI executables | -| [Nunit](https://nunit.org/) |[GitHub](https://github.com/nunit/nunit) | [3.13.3](https://www.nuget.org/packages/NUnit/3.13.3) | [MIT](https://opensource.org/licenses/MIT) | Unit testing | -| NUnit3TestAdapter | [GitHub](https://github.com/nunit/nunit3-vs-adapter)| [4.3.0](https://www.nuget.org/packages/NUnit3TestAdapter/4.3.0) | [MIT](https://opensource.org/licenses/MIT) | Run unit tests from within Visual Studio | -| Microsoft.NET.Test.Sdk | [GitHub](https://github.com/microsoft/vstest/) | [17.3.2](https://www.nuget.org/packages/Microsoft.NET.Test.Sdk/17.3.2) | [MIT](https://opensource.org/licenses/MIT) | Run unit tests | | -| NunitXml.TestLogger | [GitHub](https://github.com/spekt/nunit.testlogger) | [3.0.127](https://www.nuget.org/packages/NunitXml.TestLogger/3.0.127) | [MIT](https://opensource.org/licenses/MIT) | Report test results in XML syntax | | -| XunitXml.TestLogger | [GitHub](https://github.com/spekt/xunit.testlogger) | [3.0.70](https://www.nuget.org/packages/XunitXml.TestLogger/3.0.70) | [MIT](https://opensource.org/licenses/MIT) | Report test results in XML syntax | | +| Package | Source Code | License | Purpose | Additional Risk Assessment | +| ------- | ------------| ------- | ------- | -------------------------- | +| HIC.BadMedicine | [GitHub](https://github.com/SMI/BadMedicine) | [GPL 3.0](https://www.gnu.org/licenses/gpl-3.0.html) | Handles generating baseline random data (patient date of birth, CHI numberts etc)| | +| fo-dicom | [GitHub](https://github.com/fo-dicom/fo-dicom) |[MS-PL](https://opensource.org/licenses/MS-PL) | Handles reading/writing dicom tags from dicom datasets | | +| fo-dicom.Imaging.ImageSharp | [GitHub](https://github.com/fo-dicom/fo-dicom) |[MS-PL](https://opensource.org/licenses/MS-PL) | Handles imaging aspects with fo-dicom | | +| SixLabors.ImageSharp | [GitHub](https://github.com/SixLabors/ImageSharp) | [Apache 2.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) | Platform-independent replacement for legacy Windows-only System.Drawing.Common | | +| SixLabors.ImageSharp.Drawing | [GitHub](https://github.com/SixLabors/ImageSharp.Drawing) | [Apache 2.0](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE) | Font handling for ImageSharp | | +| Microsoft.SourceLink.GitHub | [GitHub](https://github.com/dotnet/sourcelink) | [Apache License 2.0](https://github.com/dotnet/sourcelink/blob/master/License.txt) | Enables source debugging of project nuget package| | +| Microsoft.NETCore.App | [GitHub](https://github.com/dotnet/runtime) |[MIT](https://opensource.org/licenses/MIT) | | .Net Core API| +| HIC.DicomTypeTranslation | [GitHub](https://github.com/SMI/DicomTypeTranslation) | [GPL 3.0](https://github.com/SMI/DicomTypeTranslation/blob/master/LICENSE) | | | +| YamlDotNet | [GitHub](https://github.com/aaubry/YamlDotNet) | [MIT](https://opensource.org/licenses/MIT) |Loading configuration files| +| Vecc.YamlDotNet.Analyzers.StaticGenerator | [GitHub](https://github.com/aaubry/YamlDotNet) | [MIT](https://opensource.org/licenses/MIT) |Extension to YamlDotNet for static processing| +| CommandLineParser | [GitHub](https://github.com/commandlineparser/commandline) | [MIT](https://opensource.org/licenses/MIT) | Allows command line arguments for main client application and CLI executables | +| [Nunit](https://nunit.org/) |[GitHub](https://github.com/nunit/nunit) | [MIT](https://opensource.org/licenses/MIT) | Unit testing | +| NUnit3TestAdapter | [GitHub](https://github.com/nunit/nunit3-vs-adapter)| [MIT](https://opensource.org/licenses/MIT) | Run unit tests from within Visual Studio | +| Microsoft.NET.Test.Sdk | [GitHub](https://github.com/microsoft/vstest/) | [MIT](https://opensource.org/licenses/MIT) | Run unit tests | | +| NunitXml.TestLogger | [GitHub](https://github.com/spekt/nunit.testlogger) | [MIT](https://opensource.org/licenses/MIT) | Report test results in XML syntax | | +| XunitXml.TestLogger | [GitHub](https://github.com/spekt/xunit.testlogger) | [MIT](https://opensource.org/licenses/MIT) | Report test results in XML syntax | | diff --git a/README.md b/README.md index 68b1fdd..9b45932 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # BadMedicine.Dicom -[![Build Status](https://travis-ci.org/HicServices/BadMedicine.Dicom.svg?branch=master)](https://travis-ci.org/HicServices/BadMedicine.Dicom) [![NuGet Badge](https://buildstats.info/nuget/HIC.BadMedicine.Dicom)](https://www.nuget.org/packages/HIC.BadMedicine.Dicom/) [![Total alerts](https://img.shields.io/lgtm/alerts/g/HicServices/BadMedicine.Dicom.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HicServices/BadMedicine.Dicom/alerts/) + +[![NuGet Badge](https://buildstats.info/nuget/HIC.BadMedicine.Dicom)](https://www.nuget.org/packages/HIC.BadMedicine.Dicom/) [![Build, test and package](https://github.com/SMI/BadMedicine.Dicom/actions/workflows/testpack.yml/badge.svg)](https://github.com/SMI/BadMedicine.Dicom/actions/workflows/testpack.yml) [![CodeQL](https://github.com/SMI/BadMedicine.Dicom/actions/workflows/codeql.yml/badge.svg)](https://github.com/SMI/BadMedicine.Dicom/actions/workflows/codeql.yml) The purpose of BadMedicine.Dicom is to generate large volumes of complex (in terms of tags) dicom images for integration/stress testing ETL and image management tools. @@ -9,13 +10,13 @@ There are a number of public sources of Dicom clinical images e.g. [TCIA ](https - Do not represent the breadth of Modalities/Tags found in a live clinical [PACS](https://en.wikipedia.org/wiki/Picture_archiving_and_communication_system). - Take up a lot of space -BadMedicine.Dicom generates dicom images on demand based on an anonymous aggregate model of tag data found in scottish medical imaging. It is an extension of [BadMedicine](https://github.com/HicServices/BadMedicine) which generates traditional EHR records. +BadMedicine.Dicom generates dicom images on demand based on an anonymous aggregate model of tag data found in scottish medical imaging. It is an extension of [BadMedicine](https://github.com/SMI/BadMedicine) which generates traditional EHR records. ## Usage BadDicom is available as a [nuget package](https://www.nuget.org/packages/HIC.BadMedicine.Dicom/) for linking as a library -The standalone CLI (BadDicom.exe) is available in the [releases section of Github](https://github.com/HicServices/BadMedicine.Dicom/releases) +The standalone CLI (BadDicom.exe) is available in the [releases section of Github](https://github.com/SMI/BadMedicine.Dicom/releases) Usage is as follows: @@ -37,7 +38,7 @@ BadDicom.exe c:\temp\testdicoms 5 10 --NoPixels -s 100 ## Direct to Database -You can generate DICOM metadata directly into a relational database (instead of onto disk). This can be done by downloading an [image template](https://github.com/HicServices/DicomTypeTranslation/tree/master/Templates) or by [creating one yourself](https://github.com/HicServices/DicomTemplateBuilder). +You can generate DICOM metadata directly into a relational database (instead of onto disk). This can be done by downloading an [image template](https://github.com/SMI/DicomTypeTranslation/tree/master/Templates) or by [creating one yourself](https://github.com/SMI/DicomTemplateBuilder). To turn this mode on rename the file `BadDicom.template.yaml` to `BadDicom.yaml` and provide the connection strings to your database e.g.: @@ -57,7 +58,7 @@ Database: ## EHR Datasets -If you want to generate EHR datasets with a shared patient pool with the dicom data (e.g. for doing linkage) you can provided the -s (seed) and use the main [BadMedicine](https://github.com/HicServices/BadMedicine) application. +If you want to generate EHR datasets with a shared patient pool with the dicom data (e.g. for doing linkage) you can provided the -s (seed) and use the main [BadMedicine](https://github.com/SMI/BadMedicine) application. ``` BadDicom.exe c:\temp\testdicoms 12 10 -s 100 @@ -119,7 +120,7 @@ $ sudo yum install libc6-devel libgdiplus # Tag Data -Basic random patient information (age, CHI etc) are generated by [BadMedicine](https://github.com/HicServices/BadMedicine) +Basic random patient information (age, CHI etc) are generated by [BadMedicine](https://github.com/SMI/BadMedicine) The following tags are populated in dicom files generated: diff --git a/SharedAssemblyInfo.cs b/SharedAssemblyInfo.cs index e28eb3a..c5fadee 100644 --- a/SharedAssemblyInfo.cs +++ b/SharedAssemblyInfo.cs @@ -2,11 +2,11 @@ [assembly: AssemblyCompany("Health Informatics Centre, University of Dundee")] [assembly: AssemblyProduct("Bad Dicom")] -[assembly: AssemblyCopyright("Copyright (c) 2018 - 2022")] +[assembly: AssemblyCopyright("Copyright (c) 2018 - 2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // These should be replaced with correct values by the release process -[assembly: AssemblyVersion("0.0.15")] -[assembly: AssemblyFileVersion("0.0.15")] -[assembly: AssemblyInformationalVersion("0.0.15")] +[assembly: AssemblyVersion("0.0.16")] +[assembly: AssemblyFileVersion("0.0.16")] +[assembly: AssemblyInformationalVersion("0.0.16")]