Skip to content

Commit

Permalink
suppport skipping safe pixel data
Browse files Browse the repository at this point in the history
  • Loading branch information
rkm committed Jan 6, 2024
1 parent 720e179 commit a875a09
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 23 deletions.
7 changes: 7 additions & 0 deletions IsIdentifiable/Options/IsIdentifiableDicomFileOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ public class IsIdentifiableDicomFileOptions : IsIdentifiableOptions
[Option(HelpText = "Optional. If non-zero, will ignore any reported pixel data text less than (but not equal to) the specified number of characters")]
public uint IgnoreTextLessThan { get; set; } = 0;

/// <summary>
/// Optional. If true, skip pixel data validation for "safe" combinations of Modality and ImageType e.g., 'ORIGINAL/PRIMARY' CT and MR
/// </summary>
[Option(HelpText = "Optional. If true, skip pixel data validation for \"safe\" combinations of Modality and ImageType e.g., 'ORIGINAL/PRIMARY' CT and MR")]
public bool SkipSafePixelValidation { get; set; } = false;


/// <summary>
/// Usage examples for running IsIdentifiable on dicom files
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion IsIdentifiable/Reporting/Reports/PixelTextFailureReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ protected override void CloseReportBase()
}

//TODO Replace argument list with object
public void FoundPixelData(IFileInfo fi, string sopID, string studyID, string seriesID, string modality, string[] imageType, float meanConfidence, int textLength, string pixelText, int rotation, int frame, int overlay)
public void FoundPixelData(IFileInfo fi, string sopID, string studyID, string seriesID, string modality, string?[] imageType, float meanConfidence, int textLength, string pixelText, int rotation, int frame, int overlay)
{
var dr = _dt.Rows.Add();

Expand Down
72 changes: 50 additions & 22 deletions IsIdentifiable/Runners/DicomFileRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public class DicomFileRunner : IsIdentifiableAbstractRunner
/// </summary>
public bool ThrowOnError { get; set; } = true;

/// <summary>
/// Total number of files validated
/// </summary>
public int FilesValidated { get; private set; } = 0;

/// <summary>
/// Total number of files with pixel data validated
/// </summary>
public int PixelFilesValidated { get; private set; } = 0;


/// <summary>
/// Creates a new instance based on the <paramref name="opts"/>. Options include what
/// reports to write out, wether to perform OCR etc.
Expand Down Expand Up @@ -182,17 +193,39 @@ public void ValidateDicomFile(IFileInfo fi)
}

var dicomFile = DicomFile.Open(fi.FullName);
var dataSet = dicomFile.Dataset;
var ds = dicomFile.Dataset;
var modality = GetTagOrUnknown(ds, DicomTag.Modality);
var imageTypeArr = GetImageType(ds);

if (_tesseractEngine != null)
ValidateDicomPixelData(fi, dicomFile, dataSet);
if (_tesseractEngine != null && ShouldValidatePixelData(modality, imageTypeArr))
{
ValidateDicomPixelData(fi, dicomFile, ds, modality, imageTypeArr);
PixelFilesValidated += 1;
}

foreach (var dicomItem in dataSet)
ValidateDicomItem(fi, dicomFile, dataSet, dicomItem);
foreach (var dicomItem in ds)
ValidateDicomItem(fi, dicomFile, ds, dicomItem);

FilesValidated += 1;
DoneRows(1);
}

private bool ShouldValidatePixelData(string? modality, string?[] imageTypeArr)
{
if (modality == "SR")
return false;

if (
_opts.SkipSafePixelValidation &&
(modality == "CT" || modality == "MR") &&
imageTypeArr[0] == "ORIGINAL" &&
imageTypeArr[1] == "PRIMARY"
)
return false;

return true;
}

private void ValidateDicomItem(IFileInfo fi, DicomFile dicomFile, DicomDataset dataset, DicomItem dicomItem)
{
//if it is a sequence get the Sequences dataset and then start processing that
Expand Down Expand Up @@ -253,25 +286,20 @@ private void Validate(IFileInfo fi, DicomFile dicomFile, DicomItem dicomItem, st
AddToReports(FailureFrom(fi, dicomFile, fieldValue, tagName, parts));
}

void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds)
void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds, string? modality, string?[] imageTypeArr)
{
var modality = GetTagOrUnknown(ds, DicomTag.Modality);
var imageType = GetImageType(ds);
var studyID = GetTagOrUnknown(ds, DicomTag.StudyInstanceUID);
var seriesID = GetTagOrUnknown(ds, DicomTag.SeriesInstanceUID);
var sopID = GetTagOrUnknown(ds, DicomTag.SOPInstanceUID);

// Don't go looking for images in structured reports
if (modality == "SR") return;

_logger.Info($"Processing '{fi.FullName}'");
try
{
var dicomImageObj = new DicomImage(fi.FullName);
var numFrames = dicomImageObj.NumberOfFrames;
for (var frameNum = 0; frameNum < numFrames; frameNum++)
{
_logger.Info($" Frame {frameNum} in '{fi.FullName}'");
_logger.Info($"Frame {frameNum} in '{fi.FullName}'");
dicomImageObj.OverlayColor = 0xffffff; // white, as default magenta not good for tesseract
var dicomImage = dicomImageObj.RenderImage(frameNum).AsSharpImage();
using var memStreamOut = new System.IO.MemoryStream();
Expand All @@ -281,16 +309,16 @@ void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds)
dicomImage.SaveAsBmp(ms);
ms.Position = 0;
mi.Read(ms);
ProcessBitmapMemStream(ms.ToArray(), fi, dicomFile, sopID, studyID, seriesID, modality, imageType, 0, frameNum);
ProcessBitmapMemStream(ms.ToArray(), fi, dicomFile, sopID, studyID, seriesID, modality, imageTypeArr, 0, frameNum);
}

// Threshold the image to monochrome using a window size of 25 square
// The size 25 was determined empirically based on real images (could be larger, less effective if smaller)
mi.AdaptiveThreshold(25, 25);
ProcessBitmapMemStream(mi, false, fi, dicomFile, sopID, studyID, seriesID, modality, imageType, 0, frameNum);
ProcessBitmapMemStream(mi, false, fi, dicomFile, sopID, studyID, seriesID, modality, imageTypeArr, 0, frameNum);
// Tesseract only works with black text on white background so run again negated
mi.Negate();
ProcessBitmapMemStream(mi, false, fi, dicomFile, sopID, studyID, seriesID, modality, imageType, 0, frameNum);
ProcessBitmapMemStream(mi, false, fi, dicomFile, sopID, studyID, seriesID, modality, imageTypeArr, 0, frameNum);

// Need to threshold and possibly negate the image for best results
// Magick.NET won't read from Bitmap directly in .net core so go via MemoryStream
Expand All @@ -305,7 +333,7 @@ void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds)
using var ms = new System.IO.MemoryStream();
dicomImage.Mutate(x => x.Rotate(RotateMode.Rotate90));
dicomImage.SaveAsBmp(ms);
ProcessBitmapMemStream(ms.ToArray(), fi, dicomFile, sopID, studyID, seriesID, modality, imageType,
ProcessBitmapMemStream(ms.ToArray(), fi, dicomFile, sopID, studyID, seriesID, modality, imageTypeArr,
(i + 1) * 90, frameNum);
}
}
Expand Down Expand Up @@ -364,7 +392,7 @@ void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds)
//magick_image.Write($"{fi.FullName}.ov{group-0x6000}.frame{ovframenum}.png", MagickFormat.Png);
// Tesseract only works with black text on white background so run again negated
magick_image.Negate();
ProcessBitmapMemStream(magick_image, true, fi, dicomFile, sopID, studyID, seriesID, modality, imageType, 0, group, ovframenum);
ProcessBitmapMemStream(magick_image, true, fi, dicomFile, sopID, studyID, seriesID, modality, imageTypeArr, 0, group, ovframenum);
}

}
Expand All @@ -386,7 +414,7 @@ void ValidateDicomPixelData(IFileInfo fi, DicomFile dicomFile, DicomDataset ds)
}

private void ProcessBitmapMemStream(byte[] bytes, IFileInfo fi, DicomFile dicomFile, string sopID,
string studyID, string seriesID, string modality, string[] imageType, int rotationIfAny = 0,
string studyID, string seriesID, string modality, string?[] imageType, int rotationIfAny = 0,
int frame = -1, int overlay = -1)
{
float meanConfidence;
Expand Down Expand Up @@ -430,7 +458,7 @@ private void ProcessBitmapMemStream(byte[] bytes, IFileInfo fi, DicomFile dicomF
/// <param name="rotationIfAny"></param>
/// <param name="frame"></param>
/// <param name="overlay"></param>
private void ProcessBitmapMemStream(MagickImage mi, bool forcePgm, IFileInfo fi, DicomFile dicomFile, string sopID, string studyID, string seriesID, string modality, string[] imageType, int rotationIfAny = 0, int frame = -1, int overlay = -1)
private void ProcessBitmapMemStream(MagickImage mi, bool forcePgm, IFileInfo fi, DicomFile dicomFile, string sopID, string studyID, string seriesID, string modality, string?[] imageType, int rotationIfAny = 0, int frame = -1, int overlay = -1)
{
byte[] bytes;
using (System.IO.MemoryStream ms = new())
Expand All @@ -450,9 +478,9 @@ private void ProcessBitmapMemStream(MagickImage mi, bool forcePgm, IFileInfo fi,
/// </summary>
/// <param name="ds"></param>
/// <returns></returns>
static string[] GetImageType(DicomDataset ds)
static string?[] GetImageType(DicomDataset ds)
{
var result = new string[3];
var result = new string?[3];

if (ds.Contains(DicomTag.ImageType))
{
Expand Down Expand Up @@ -484,7 +512,7 @@ static string[] GetImageType(DicomDataset ds)
/// <param name="ds"></param>
/// <param name="dt"></param>
/// <returns></returns>
private static string GetTagOrUnknown(DicomDataset ds, DicomTag dt)
private static string? GetTagOrUnknown(DicomDataset ds, DicomTag dt)
{
return ds.Contains(dt) ? ds.GetValue<string>(dt, 0) : null;
}
Expand Down
36 changes: 36 additions & 0 deletions Tests/IsIdentifiableTests/RunnerTests/DicomFileRunnerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,41 @@ public void IgnorePixelDataLessThan(bool ignoreShortText)
Assert.That(failures, Has.Count.EqualTo(ignoreShortText ? 1 : 3));
}

[TestCase(true)]
[TestCase(false)]
public void SkipPixelSafeTags(bool skipSafePixelValidation)
{
// Arrange

var opts = new IsIdentifiableDicomFileOptions
{
ColumnReport = true,
TessDirectory = _tessDir.FullName,
SkipSafePixelValidation = skipSafePixelValidation,
};

var fileSystem = new System.IO.Abstractions.FileSystem();

var fileName = Path.Combine(TestContext.CurrentContext.TestDirectory, nameof(DicomFileRunnerTest), "f1.dcm");
TestData.Create(fileSystem.FileInfo.New(fileName), TestData.IMG_013);

var runner = new DicomFileRunner(opts, fileSystem);

var fileInfo = fileSystem.FileInfo.New(fileName);
Assert.That(fileInfo.Exists, Is.True);

// Act

runner.ValidateDicomFile(fileInfo);

// Assert

Assert.Multiple(() =>
{
Assert.That(runner.FilesValidated, Is.EqualTo(1));
Assert.That(runner.PixelFilesValidated, Is.EqualTo(skipSafePixelValidation ? 0 : 1));
});
}

#endregion
}

0 comments on commit a875a09

Please sign in to comment.