|
|
|
@@ -129,7 +129,7 @@ public sealed class DicomMaskOptions
|
|
|
|
|
/// 灰度图像的遮挡像素值,默认 0。
|
|
|
|
|
/// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ushort GrayscaleMaskValue { get; init; } = 0;
|
|
|
|
|
//public ushort GrayscaleMaskValue { get; init; } = 0;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 彩色图像的遮挡值,默认 [0,0,0]。
|
|
|
|
@@ -147,6 +147,19 @@ public sealed class DicomMaskOptions
|
|
|
|
|
/// 是否更新 BurnedInAnnotation 为 NO。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool UpdateBurnedInAnnotationToNo { get; init; } = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 是否启用自动灰度遮挡值。
|
|
|
|
|
/// true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public bool AutoSelectGrayscaleMaskValue { get; init; } = true;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
|
|
|
|
|
/// 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int? GrayscaleMaskValue { get; init; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -188,7 +201,7 @@ public static class DicomPixelMasker
|
|
|
|
|
|
|
|
|
|
ValidateDataset(originalDataset);
|
|
|
|
|
|
|
|
|
|
var originalTransferSyntax = originalFile.FileMetaInfo.TransferSyntax;
|
|
|
|
|
var originalTs = originalFile.FileMetaInfo.TransferSyntax;
|
|
|
|
|
var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
|
|
|
|
|
|
|
|
|
var rows = originalDataset.GetSingleValue<int>(DicomTag.Rows);
|
|
|
|
@@ -198,36 +211,32 @@ public static class DicomPixelMasker
|
|
|
|
|
var samplesPerPixel = originalDataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
|
|
|
|
var pixelRepresentation = originalDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
|
|
|
|
var planarConfiguration = originalDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
|
|
|
|
//log?.Invoke($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}");
|
|
|
|
|
Console.WriteLine($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}");
|
|
|
|
|
|
|
|
|
|
EnsureSupportedPhotometric(originalPhotometric, samplesPerPixel);
|
|
|
|
|
|
|
|
|
|
// 1. 转为工作用的未压缩 DICOM
|
|
|
|
|
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
var workingDataset = workingFile.Dataset;
|
|
|
|
|
|
|
|
|
|
// 2. 修改像素
|
|
|
|
|
MaskPixelDataInPlace(workingDataset, regionList, options);
|
|
|
|
|
|
|
|
|
|
MaskPixelDataInPlace(workingFile.Dataset, regionList, options);
|
|
|
|
|
// 3. 保持原 PhotometricInterpretation
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(originalPhotometric))
|
|
|
|
|
{
|
|
|
|
|
workingDataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
|
|
|
|
workingFile.Dataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 可选更新 BurnedInAnnotation
|
|
|
|
|
if (options.UpdateBurnedInAnnotationToNo)
|
|
|
|
|
{
|
|
|
|
|
workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
|
|
|
|
workingFile.Dataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. 转回原始 TransferSyntax
|
|
|
|
|
// 5. 编码回原始传输语法
|
|
|
|
|
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
|
|
|
|
workingFile,
|
|
|
|
|
originalTransferSyntax,
|
|
|
|
|
originalTs,
|
|
|
|
|
options.StrictKeepTransferSyntax,
|
|
|
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
finalFile.FileMetaInfo.TransferSyntax = originalTransferSyntax;
|
|
|
|
|
finalFile.FileMetaInfo.TransferSyntax = originalTs;
|
|
|
|
|
|
|
|
|
|
if (output.CanSeek)
|
|
|
|
|
output.SetLength(0);
|
|
|
|
@@ -236,6 +245,7 @@ public static class DicomPixelMasker
|
|
|
|
|
|
|
|
|
|
if (output.CanSeek)
|
|
|
|
|
output.Position = 0;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
@@ -254,6 +264,37 @@ public static class DicomPixelMasker
|
|
|
|
|
_ = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureSupportedPhotometric(string photometric, int samplesPerPixel, Action<string>? log = null)
|
|
|
|
|
{
|
|
|
|
|
if (samplesPerPixel == 1)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(photometric, "MONOCHROME2", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw new NotSupportedException($"Unsupported grayscale PhotometricInterpretation: {photometric}");
|
|
|
|
|
}
|
|
|
|
|
if (samplesPerPixel == 3)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 对 YBR_FULL_422 不建议直接按当前布局改
|
|
|
|
|
if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
throw new NotSupportedException(
|
|
|
|
|
$"PhotometricInterpretation={photometric} uses subsampling layout and is not safely supported by this pixel masking implementation.");
|
|
|
|
|
}
|
|
|
|
|
throw new NotSupportedException($"Unsupported color PhotometricInterpretation: {photometric}");
|
|
|
|
|
}
|
|
|
|
|
throw new NotSupportedException($"Unsupported SamplesPerPixel={samplesPerPixel}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 转为工作用的未压缩 DICOM
|
|
|
|
|
/// </summary>
|
|
|
|
@@ -288,59 +329,35 @@ public static class DicomPixelMasker
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 转回原始 TransferSyntax
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="uncompressedFile"></param>
|
|
|
|
|
/// <param name="workingFile"></param>
|
|
|
|
|
/// <param name="originalTransferSyntax"></param>
|
|
|
|
|
/// <param name="strictKeepTransferSyntax"></param>
|
|
|
|
|
/// <param name="cancellationToken"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
|
|
|
private static async Task<DicomFile> ReEncodeToOriginalTransferSyntaxAsync(
|
|
|
|
|
DicomFile uncompressedFile,
|
|
|
|
|
DicomTransferSyntax originalTransferSyntax,
|
|
|
|
|
bool strictKeepTransferSyntax,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
DicomFile workingFile,
|
|
|
|
|
DicomTransferSyntax originalTransferSyntax,
|
|
|
|
|
bool strictKeepTransferSyntax,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (!originalTransferSyntax.IsEncapsulated)
|
|
|
|
|
{
|
|
|
|
|
if (uncompressedFile.FileMetaInfo.TransferSyntax == originalTransferSyntax)
|
|
|
|
|
{
|
|
|
|
|
return uncompressedFile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var transcoder = new DicomTranscoder(
|
|
|
|
|
uncompressedFile.FileMetaInfo.TransferSyntax,
|
|
|
|
|
originalTransferSyntax);
|
|
|
|
|
|
|
|
|
|
return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Failed to convert dataset back to original uncompressed TransferSyntax {originalTransferSyntax.UID.UID}.",
|
|
|
|
|
ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (workingFile.FileMetaInfo.TransferSyntax == originalTransferSyntax)
|
|
|
|
|
return workingFile;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var currentTs = uncompressedFile.FileMetaInfo.TransferSyntax;
|
|
|
|
|
var currentTs = workingFile.FileMetaInfo.TransferSyntax;
|
|
|
|
|
var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax);
|
|
|
|
|
return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken)
|
|
|
|
|
.ConfigureAwait(false);
|
|
|
|
|
return await Task.Run(() => transcoder.Transcode(workingFile), cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
if (strictKeepTransferSyntax)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Failed to re-encode DICOM back to original compressed TransferSyntax {originalTransferSyntax.UID.UID}.",
|
|
|
|
|
$"Failed to re-encode DICOM back to original TransferSyntax {originalTransferSyntax.UID.UID}.",
|
|
|
|
|
ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uncompressedFile;
|
|
|
|
|
return workingFile;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -351,77 +368,66 @@ public static class DicomPixelMasker
|
|
|
|
|
/// <param name="regions"></param>
|
|
|
|
|
/// <param name="options"></param>
|
|
|
|
|
private static void MaskPixelDataInPlace(
|
|
|
|
|
DicomDataset dataset,
|
|
|
|
|
IReadOnlyList<MaskRegion> regions,
|
|
|
|
|
DicomMaskOptions options)
|
|
|
|
|
DicomDataset dataset,
|
|
|
|
|
IReadOnlyList<MaskRegion> regions,
|
|
|
|
|
DicomMaskOptions options)
|
|
|
|
|
{
|
|
|
|
|
var rows = dataset.GetSingleValue<int>(DicomTag.Rows);
|
|
|
|
|
var cols = dataset.GetSingleValue<int>(DicomTag.Columns);
|
|
|
|
|
var bitsAllocated = dataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
|
|
|
|
var bitsStored = dataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
|
|
|
|
|
var samplesPerPixel = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
|
|
|
|
var pixelRepresentation = dataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
|
|
|
|
var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
|
|
|
|
var planarConfiguration = dataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
|
|
|
|
|
|
|
|
|
var pixelData = DicomPixelData.Create(dataset, false);
|
|
|
|
|
var frameCount = pixelData.NumberOfFrames;
|
|
|
|
|
|
|
|
|
|
var framesToProcess = ResolveFrames(frameCount, options.FrameIndices);
|
|
|
|
|
|
|
|
|
|
var newFrames = new List<IByteBuffer>(frameCount);
|
|
|
|
|
|
|
|
|
|
var replacementFrames = new List<IByteBuffer>(frameCount);
|
|
|
|
|
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
|
|
|
|
{
|
|
|
|
|
var frame = pixelData.GetFrame(frameIndex);
|
|
|
|
|
var data = frame.Data.ToArray();
|
|
|
|
|
|
|
|
|
|
var buffer = frame.Data;
|
|
|
|
|
var bytes = buffer.ToArray();
|
|
|
|
|
if (framesToProcess.Contains(frameIndex))
|
|
|
|
|
{
|
|
|
|
|
ApplyMaskToFrame(
|
|
|
|
|
data,
|
|
|
|
|
rows,
|
|
|
|
|
cols,
|
|
|
|
|
bitsAllocated,
|
|
|
|
|
samplesPerPixel,
|
|
|
|
|
pixelRepresentation,
|
|
|
|
|
photometric,
|
|
|
|
|
planarConfiguration,
|
|
|
|
|
regions,
|
|
|
|
|
options);
|
|
|
|
|
frameData: bytes,
|
|
|
|
|
rows: rows,
|
|
|
|
|
cols: cols,
|
|
|
|
|
bitsAllocated: bitsAllocated,
|
|
|
|
|
bitsStored: bitsStored,
|
|
|
|
|
samplesPerPixel: samplesPerPixel,
|
|
|
|
|
pixelRepresentation: pixelRepresentation,
|
|
|
|
|
photometric: photometric,
|
|
|
|
|
planarConfiguration: planarConfiguration,
|
|
|
|
|
regions: regions,
|
|
|
|
|
options: options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newFrames.Add(new MemoryByteBuffer(data));
|
|
|
|
|
replacementFrames.Add(new MemoryByteBuffer(bytes));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ReplacePixelDataFrames(dataset, pixelData, newFrames);
|
|
|
|
|
ReplacePixelDataFrames(dataset, pixelData, replacementFrames);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static HashSet<int> ResolveFrames(int frameCount, IReadOnlyCollection<int>? frameIndices)
|
|
|
|
|
{
|
|
|
|
|
if (frameIndices == null || frameIndices.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return Enumerable.Range(0, frameCount).ToHashSet();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result = new HashSet<int>();
|
|
|
|
|
foreach (var i in frameIndices)
|
|
|
|
|
foreach (var index in frameIndices)
|
|
|
|
|
{
|
|
|
|
|
if (i < 0 || i >= frameCount)
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {i} out of range [0, {frameCount - 1}]");
|
|
|
|
|
|
|
|
|
|
result.Add(i);
|
|
|
|
|
if (index < 0 || index >= frameCount)
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {index} out of range [0, {frameCount - 1}]");
|
|
|
|
|
result.Add(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ReplacePixelDataFrames(
|
|
|
|
|
DicomDataset dataset,
|
|
|
|
|
DicomPixelData sourcePixelData,
|
|
|
|
|
IReadOnlyList<IByteBuffer> frames)
|
|
|
|
|
{
|
|
|
|
|
dataset.Remove(DicomTag.PixelData);
|
|
|
|
|
|
|
|
|
|
var newPixelData = DicomPixelData.Create(dataset, true);
|
|
|
|
|
//newPixelData.BitsAllocated = sourcePixelData.BitsAllocated;
|
|
|
|
|
newPixelData.BitsStored = sourcePixelData.BitsStored;
|
|
|
|
@@ -432,7 +438,6 @@ public static class DicomPixelMasker
|
|
|
|
|
newPixelData.Height = sourcePixelData.Height;
|
|
|
|
|
newPixelData.Width = sourcePixelData.Width;
|
|
|
|
|
newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation;
|
|
|
|
|
|
|
|
|
|
foreach (var frame in frames)
|
|
|
|
|
{
|
|
|
|
|
newPixelData.AddFrame(frame);
|
|
|
|
@@ -440,63 +445,106 @@ public static class DicomPixelMasker
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyMaskToFrame(
|
|
|
|
|
byte[] frameData,
|
|
|
|
|
int rows,
|
|
|
|
|
int cols,
|
|
|
|
|
int bitsAllocated,
|
|
|
|
|
int samplesPerPixel,
|
|
|
|
|
ushort pixelRepresentation,
|
|
|
|
|
string photometric,
|
|
|
|
|
ushort planarConfiguration,
|
|
|
|
|
IReadOnlyList<MaskRegion> regions,
|
|
|
|
|
DicomMaskOptions options)
|
|
|
|
|
byte[] frameData,
|
|
|
|
|
int rows,
|
|
|
|
|
int cols,
|
|
|
|
|
int bitsAllocated,
|
|
|
|
|
int bitsStored,
|
|
|
|
|
int samplesPerPixel,
|
|
|
|
|
ushort pixelRepresentation,
|
|
|
|
|
string photometric,
|
|
|
|
|
ushort planarConfiguration,
|
|
|
|
|
IReadOnlyList<MaskRegion> regions,
|
|
|
|
|
DicomMaskOptions options)
|
|
|
|
|
{
|
|
|
|
|
if (samplesPerPixel == 1)
|
|
|
|
|
{
|
|
|
|
|
if (bitsAllocated == 8)
|
|
|
|
|
{
|
|
|
|
|
//8 位灰度
|
|
|
|
|
ApplyMask_Grayscale8(frameData, rows, cols, regions, (byte)Math.Min(options.GrayscaleMaskValue, byte.MaxValue));
|
|
|
|
|
byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options);
|
|
|
|
|
ApplyMask_Grayscale8(frameData, rows, cols, regions, maskValue8);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 16 位灰度
|
|
|
|
|
if (bitsAllocated == 16)
|
|
|
|
|
{
|
|
|
|
|
ApplyMask_Grayscale16(frameData, rows, cols, regions, options.GrayscaleMaskValue);
|
|
|
|
|
ushort maskValue16 = ResolveGrayscaleMaskValue16(
|
|
|
|
|
photometric,
|
|
|
|
|
bitsStored,
|
|
|
|
|
pixelRepresentation,
|
|
|
|
|
options);
|
|
|
|
|
ApplyMask_Grayscale16(frameData, rows, cols, regions, maskValue16);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new NotSupportedException($"Unsupported grayscale BitsAllocated={bitsAllocated}, Photometric={photometric}.");
|
|
|
|
|
throw new NotSupportedException(
|
|
|
|
|
$"Unsupported grayscale image: BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//RGB 彩图
|
|
|
|
|
if (samplesPerPixel == 3)
|
|
|
|
|
{
|
|
|
|
|
if (bitsAllocated != 8)
|
|
|
|
|
{
|
|
|
|
|
throw new NotSupportedException($"Unsupported color image: SamplesPerPixel=3 but BitsAllocated={bitsAllocated}.");
|
|
|
|
|
throw new NotSupportedException(
|
|
|
|
|
$"Unsupported color image: SamplesPerPixel=3, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (planarConfiguration == 0)
|
|
|
|
|
{
|
|
|
|
|
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, options.ColorMaskValue);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (planarConfiguration == 1)
|
|
|
|
|
{
|
|
|
|
|
ApplyMask_Color8_Planar(frameData, rows, cols, regions, options.ColorMaskValue);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new NotSupportedException($"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}.");
|
|
|
|
|
throw new NotSupportedException(
|
|
|
|
|
$"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new NotSupportedException(
|
|
|
|
|
$"Unsupported image format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, PixelRepresentation={pixelRepresentation}, Photometric={photometric}.");
|
|
|
|
|
$"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
|
|
|
|
}
|
|
|
|
|
private static byte ResolveGrayscaleMaskValue8(string photometric, DicomMaskOptions options)
|
|
|
|
|
{
|
|
|
|
|
if (!options.AutoSelectGrayscaleMaskValue)
|
|
|
|
|
{
|
|
|
|
|
var value = options.GrayscaleMaskValue ?? 0;
|
|
|
|
|
return (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue);
|
|
|
|
|
}
|
|
|
|
|
if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
return byte.MaxValue;
|
|
|
|
|
return byte.MinValue;
|
|
|
|
|
}
|
|
|
|
|
private static ushort ResolveGrayscaleMaskValue16(
|
|
|
|
|
string photometric,
|
|
|
|
|
int bitsStored,
|
|
|
|
|
ushort pixelRepresentation,
|
|
|
|
|
DicomMaskOptions options)
|
|
|
|
|
{
|
|
|
|
|
if (!options.AutoSelectGrayscaleMaskValue)
|
|
|
|
|
{
|
|
|
|
|
var value = options.GrayscaleMaskValue ?? 0;
|
|
|
|
|
return unchecked((ushort)value);
|
|
|
|
|
}
|
|
|
|
|
bool isSigned = pixelRepresentation == 1;
|
|
|
|
|
bool mono1 = string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
bitsStored = Math.Clamp(bitsStored, 1, 16);
|
|
|
|
|
if (!isSigned)
|
|
|
|
|
{
|
|
|
|
|
ushort min = 0;
|
|
|
|
|
ushort max = bitsStored == 16
|
|
|
|
|
? ushort.MaxValue
|
|
|
|
|
: (ushort)((1 << bitsStored) - 1);
|
|
|
|
|
return mono1 ? max : min;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// signed range based on BitsStored
|
|
|
|
|
// minSigned = -(1 << (bitsStored - 1))
|
|
|
|
|
// maxSigned = (1 << (bitsStored - 1)) - 1
|
|
|
|
|
int minSigned = -(1 << (bitsStored - 1));
|
|
|
|
|
int maxSigned = (1 << (bitsStored - 1)) - 1;
|
|
|
|
|
short selected = mono1 ? (short)maxSigned : (short)minSigned;
|
|
|
|
|
return unchecked((ushort)selected);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyMask_Grayscale8(
|
|
|
|
|
byte[] data,
|
|
|
|
|
int rows,
|
|
|
|
@@ -508,18 +556,16 @@ public static class DicomPixelMasker
|
|
|
|
|
{
|
|
|
|
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
|
|
|
|
if (left >= right || top >= bottom) continue;
|
|
|
|
|
|
|
|
|
|
for (int y = top; y < bottom; y++)
|
|
|
|
|
{
|
|
|
|
|
int rowOffset = y * cols;
|
|
|
|
|
int offset = y * cols;
|
|
|
|
|
for (int x = left; x < right; x++)
|
|
|
|
|
{
|
|
|
|
|
data[rowOffset + x] = maskValue;
|
|
|
|
|
data[offset + x] = maskValue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyMask_Grayscale16(
|
|
|
|
|
byte[] data,
|
|
|
|
|
int rows,
|
|
|
|
@@ -531,22 +577,19 @@ public static class DicomPixelMasker
|
|
|
|
|
{
|
|
|
|
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
|
|
|
|
if (left >= right || top >= bottom) continue;
|
|
|
|
|
|
|
|
|
|
for (int y = top; y < bottom; y++)
|
|
|
|
|
{
|
|
|
|
|
int rowOffset = y * cols;
|
|
|
|
|
int offset = y * cols;
|
|
|
|
|
for (int x = left; x < right; x++)
|
|
|
|
|
{
|
|
|
|
|
int pixelIndex = rowOffset + x;
|
|
|
|
|
int pixelIndex = offset + x;
|
|
|
|
|
int byteIndex = pixelIndex * 2;
|
|
|
|
|
|
|
|
|
|
data[byteIndex] = (byte)(maskValue & 0xFF);
|
|
|
|
|
data[byteIndex + 1] = (byte)((maskValue >> 8) & 0xFF);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyMask_Color8_Interleaved(
|
|
|
|
|
byte[] data,
|
|
|
|
|
int rows,
|
|
|
|
@@ -556,23 +599,19 @@ public static class DicomPixelMasker
|
|
|
|
|
{
|
|
|
|
|
if (color == null || color.Length < 3)
|
|
|
|
|
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
|
|
|
|
|
|
|
|
|
byte c0 = color[0];
|
|
|
|
|
byte c1 = color[1];
|
|
|
|
|
byte c2 = color[2];
|
|
|
|
|
|
|
|
|
|
foreach (var region in regions)
|
|
|
|
|
{
|
|
|
|
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
|
|
|
|
if (left >= right || top >= bottom) continue;
|
|
|
|
|
|
|
|
|
|
for (int y = top; y < bottom; y++)
|
|
|
|
|
{
|
|
|
|
|
for (int x = left; x < right; x++)
|
|
|
|
|
{
|
|
|
|
|
int pixelIndex = y * cols + x;
|
|
|
|
|
int byteIndex = pixelIndex * 3;
|
|
|
|
|
|
|
|
|
|
data[byteIndex] = c0;
|
|
|
|
|
data[byteIndex + 1] = c1;
|
|
|
|
|
data[byteIndex + 2] = c2;
|
|
|
|
@@ -580,7 +619,6 @@ public static class DicomPixelMasker
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ApplyMask_Color8_Planar(
|
|
|
|
|
byte[] data,
|
|
|
|
|
int rows,
|
|
|
|
@@ -590,18 +628,14 @@ public static class DicomPixelMasker
|
|
|
|
|
{
|
|
|
|
|
if (color == null || color.Length < 3)
|
|
|
|
|
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
|
|
|
|
|
|
|
|
|
int planeSize = rows * cols;
|
|
|
|
|
|
|
|
|
|
byte c0 = color[0];
|
|
|
|
|
byte c1 = color[1];
|
|
|
|
|
byte c2 = color[2];
|
|
|
|
|
|
|
|
|
|
foreach (var region in regions)
|
|
|
|
|
{
|
|
|
|
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
|
|
|
|
if (left >= right || top >= bottom) continue;
|
|
|
|
|
|
|
|
|
|
for (int y = top; y < bottom; y++)
|
|
|
|
|
{
|
|
|
|
|
int rowOffset = y * cols;
|
|
|
|
@@ -615,8 +649,10 @@ public static class DicomPixelMasker
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (int left, int top, int right, int bottom) ClipRegion(MaskRegion region, int imageWidth, int imageHeight)
|
|
|
|
|
private static (int left, int top, int right, int bottom) ClipRegion(
|
|
|
|
|
MaskRegion region,
|
|
|
|
|
int imageWidth,
|
|
|
|
|
int imageHeight)
|
|
|
|
|
{
|
|
|
|
|
int left = Math.Max(0, region.X);
|
|
|
|
|
int top = Math.Max(0, region.Y);
|
|
|
|
|