Merge branch 'Test_IRC_Net8' of https://gitea.frp.extimaging.com/XCKJ/irc-netcore-api into Test_IRC_Net8
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
commit
1e509c0ec7
|
|
@ -129,7 +129,7 @@ public sealed class DicomMaskOptions
|
||||||
/// 灰度图像的遮挡像素值,默认 0。
|
/// 灰度图像的遮挡像素值,默认 0。
|
||||||
/// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
/// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ushort GrayscaleMaskValue { get; init; } = 0;
|
//public ushort GrayscaleMaskValue { get; init; } = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 彩色图像的遮挡值,默认 [0,0,0]。
|
/// 彩色图像的遮挡值,默认 [0,0,0]。
|
||||||
|
|
@ -147,6 +147,19 @@ public sealed class DicomMaskOptions
|
||||||
/// 是否更新 BurnedInAnnotation 为 NO。
|
/// 是否更新 BurnedInAnnotation 为 NO。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UpdateBurnedInAnnotationToNo { get; init; } = false;
|
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);
|
ValidateDataset(originalDataset);
|
||||||
|
|
||||||
var originalTransferSyntax = originalFile.FileMetaInfo.TransferSyntax;
|
var originalTs = originalFile.FileMetaInfo.TransferSyntax;
|
||||||
var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
||||||
|
|
||||||
var rows = originalDataset.GetSingleValue<int>(DicomTag.Rows);
|
var rows = originalDataset.GetSingleValue<int>(DicomTag.Rows);
|
||||||
|
|
@ -198,36 +211,32 @@ public static class DicomPixelMasker
|
||||||
var samplesPerPixel = originalDataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
var samplesPerPixel = originalDataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
||||||
var pixelRepresentation = originalDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
var pixelRepresentation = originalDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
||||||
var planarConfiguration = originalDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (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
|
// 1. 转为工作用的未压缩 DICOM
|
||||||
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
||||||
var workingDataset = workingFile.Dataset;
|
|
||||||
|
|
||||||
// 2. 修改像素
|
// 2. 修改像素
|
||||||
MaskPixelDataInPlace(workingDataset, regionList, options);
|
MaskPixelDataInPlace(workingFile.Dataset, regionList, options);
|
||||||
|
|
||||||
// 3. 保持原 PhotometricInterpretation
|
// 3. 保持原 PhotometricInterpretation
|
||||||
if (!string.IsNullOrWhiteSpace(originalPhotometric))
|
if (!string.IsNullOrWhiteSpace(originalPhotometric))
|
||||||
{
|
{
|
||||||
workingDataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
workingFile.Dataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 可选更新 BurnedInAnnotation
|
// 4. 可选更新 BurnedInAnnotation
|
||||||
if (options.UpdateBurnedInAnnotationToNo)
|
if (options.UpdateBurnedInAnnotationToNo)
|
||||||
{
|
{
|
||||||
workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
workingFile.Dataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
||||||
}
|
}
|
||||||
|
// 5. 编码回原始传输语法
|
||||||
// 5. 转回原始 TransferSyntax
|
|
||||||
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
||||||
workingFile,
|
workingFile,
|
||||||
originalTransferSyntax,
|
originalTs,
|
||||||
options.StrictKeepTransferSyntax,
|
options.StrictKeepTransferSyntax,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
finalFile.FileMetaInfo.TransferSyntax = originalTransferSyntax;
|
finalFile.FileMetaInfo.TransferSyntax = originalTs;
|
||||||
|
|
||||||
if (output.CanSeek)
|
if (output.CanSeek)
|
||||||
output.SetLength(0);
|
output.SetLength(0);
|
||||||
|
|
@ -236,6 +245,7 @@ public static class DicomPixelMasker
|
||||||
|
|
||||||
if (output.CanSeek)
|
if (output.CanSeek)
|
||||||
output.Position = 0;
|
output.Position = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -254,6 +264,37 @@ public static class DicomPixelMasker
|
||||||
_ = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
_ = 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>
|
/// <summary>
|
||||||
/// 转为工作用的未压缩 DICOM
|
/// 转为工作用的未压缩 DICOM
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -288,59 +329,35 @@ public static class DicomPixelMasker
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 转回原始 TransferSyntax
|
/// 转回原始 TransferSyntax
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uncompressedFile"></param>
|
/// <param name="workingFile"></param>
|
||||||
/// <param name="originalTransferSyntax"></param>
|
/// <param name="originalTransferSyntax"></param>
|
||||||
/// <param name="strictKeepTransferSyntax"></param>
|
/// <param name="strictKeepTransferSyntax"></param>
|
||||||
/// <param name="cancellationToken"></param>
|
/// <param name="cancellationToken"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
private static async Task<DicomFile> ReEncodeToOriginalTransferSyntaxAsync(
|
private static async Task<DicomFile> ReEncodeToOriginalTransferSyntaxAsync(
|
||||||
DicomFile uncompressedFile,
|
DicomFile workingFile,
|
||||||
DicomTransferSyntax originalTransferSyntax,
|
DicomTransferSyntax originalTransferSyntax,
|
||||||
bool strictKeepTransferSyntax,
|
bool strictKeepTransferSyntax,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!originalTransferSyntax.IsEncapsulated)
|
if (workingFile.FileMetaInfo.TransferSyntax == originalTransferSyntax)
|
||||||
{
|
return workingFile;
|
||||||
if (uncompressedFile.FileMetaInfo.TransferSyntax == originalTransferSyntax)
|
|
||||||
{
|
|
||||||
return uncompressedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var transcoder = new DicomTranscoder(
|
var currentTs = workingFile.FileMetaInfo.TransferSyntax;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var currentTs = uncompressedFile.FileMetaInfo.TransferSyntax;
|
|
||||||
var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax);
|
var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax);
|
||||||
return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken)
|
return await Task.Run(() => transcoder.Transcode(workingFile), cancellationToken).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (strictKeepTransferSyntax)
|
if (strictKeepTransferSyntax)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
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);
|
ex);
|
||||||
}
|
}
|
||||||
|
return workingFile;
|
||||||
return uncompressedFile;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,70 +375,59 @@ public static class DicomPixelMasker
|
||||||
var rows = dataset.GetSingleValue<int>(DicomTag.Rows);
|
var rows = dataset.GetSingleValue<int>(DicomTag.Rows);
|
||||||
var cols = dataset.GetSingleValue<int>(DicomTag.Columns);
|
var cols = dataset.GetSingleValue<int>(DicomTag.Columns);
|
||||||
var bitsAllocated = dataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
var bitsAllocated = dataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
||||||
|
var bitsStored = dataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
|
||||||
var samplesPerPixel = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
var samplesPerPixel = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
||||||
var pixelRepresentation = dataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
var pixelRepresentation = dataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
||||||
var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
||||||
var planarConfiguration = dataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
var planarConfiguration = dataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
||||||
|
|
||||||
var pixelData = DicomPixelData.Create(dataset, false);
|
var pixelData = DicomPixelData.Create(dataset, false);
|
||||||
var frameCount = pixelData.NumberOfFrames;
|
var frameCount = pixelData.NumberOfFrames;
|
||||||
|
|
||||||
var framesToProcess = ResolveFrames(frameCount, options.FrameIndices);
|
var framesToProcess = ResolveFrames(frameCount, options.FrameIndices);
|
||||||
|
var replacementFrames = new List<IByteBuffer>(frameCount);
|
||||||
var newFrames = new List<IByteBuffer>(frameCount);
|
|
||||||
|
|
||||||
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
||||||
{
|
{
|
||||||
var frame = pixelData.GetFrame(frameIndex);
|
var frame = pixelData.GetFrame(frameIndex);
|
||||||
var data = frame.Data.ToArray();
|
var buffer = frame.Data;
|
||||||
|
var bytes = buffer.ToArray();
|
||||||
if (framesToProcess.Contains(frameIndex))
|
if (framesToProcess.Contains(frameIndex))
|
||||||
{
|
{
|
||||||
ApplyMaskToFrame(
|
ApplyMaskToFrame(
|
||||||
data,
|
frameData: bytes,
|
||||||
rows,
|
rows: rows,
|
||||||
cols,
|
cols: cols,
|
||||||
bitsAllocated,
|
bitsAllocated: bitsAllocated,
|
||||||
samplesPerPixel,
|
bitsStored: bitsStored,
|
||||||
pixelRepresentation,
|
samplesPerPixel: samplesPerPixel,
|
||||||
photometric,
|
pixelRepresentation: pixelRepresentation,
|
||||||
planarConfiguration,
|
photometric: photometric,
|
||||||
regions,
|
planarConfiguration: planarConfiguration,
|
||||||
options);
|
regions: regions,
|
||||||
|
options: options);
|
||||||
}
|
}
|
||||||
|
replacementFrames.Add(new MemoryByteBuffer(bytes));
|
||||||
newFrames.Add(new MemoryByteBuffer(data));
|
|
||||||
}
|
}
|
||||||
|
ReplacePixelDataFrames(dataset, pixelData, replacementFrames);
|
||||||
ReplacePixelDataFrames(dataset, pixelData, newFrames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<int> ResolveFrames(int frameCount, IReadOnlyCollection<int>? frameIndices)
|
private static HashSet<int> ResolveFrames(int frameCount, IReadOnlyCollection<int>? frameIndices)
|
||||||
{
|
{
|
||||||
if (frameIndices == null || frameIndices.Count == 0)
|
if (frameIndices == null || frameIndices.Count == 0)
|
||||||
{
|
|
||||||
return Enumerable.Range(0, frameCount).ToHashSet();
|
return Enumerable.Range(0, frameCount).ToHashSet();
|
||||||
}
|
|
||||||
|
|
||||||
var result = new HashSet<int>();
|
var result = new HashSet<int>();
|
||||||
foreach (var i in frameIndices)
|
foreach (var index in frameIndices)
|
||||||
{
|
{
|
||||||
if (i < 0 || i >= frameCount)
|
if (index < 0 || index >= frameCount)
|
||||||
throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {i} out of range [0, {frameCount - 1}]");
|
throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {index} out of range [0, {frameCount - 1}]");
|
||||||
|
result.Add(index);
|
||||||
result.Add(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ReplacePixelDataFrames(
|
private static void ReplacePixelDataFrames(
|
||||||
DicomDataset dataset,
|
DicomDataset dataset,
|
||||||
DicomPixelData sourcePixelData,
|
DicomPixelData sourcePixelData,
|
||||||
IReadOnlyList<IByteBuffer> frames)
|
IReadOnlyList<IByteBuffer> frames)
|
||||||
{
|
{
|
||||||
dataset.Remove(DicomTag.PixelData);
|
dataset.Remove(DicomTag.PixelData);
|
||||||
|
|
||||||
var newPixelData = DicomPixelData.Create(dataset, true);
|
var newPixelData = DicomPixelData.Create(dataset, true);
|
||||||
//newPixelData.BitsAllocated = sourcePixelData.BitsAllocated;
|
//newPixelData.BitsAllocated = sourcePixelData.BitsAllocated;
|
||||||
newPixelData.BitsStored = sourcePixelData.BitsStored;
|
newPixelData.BitsStored = sourcePixelData.BitsStored;
|
||||||
|
|
@ -432,7 +438,6 @@ public static class DicomPixelMasker
|
||||||
newPixelData.Height = sourcePixelData.Height;
|
newPixelData.Height = sourcePixelData.Height;
|
||||||
newPixelData.Width = sourcePixelData.Width;
|
newPixelData.Width = sourcePixelData.Width;
|
||||||
newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation;
|
newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation;
|
||||||
|
|
||||||
foreach (var frame in frames)
|
foreach (var frame in frames)
|
||||||
{
|
{
|
||||||
newPixelData.AddFrame(frame);
|
newPixelData.AddFrame(frame);
|
||||||
|
|
@ -444,6 +449,7 @@ public static class DicomPixelMasker
|
||||||
int rows,
|
int rows,
|
||||||
int cols,
|
int cols,
|
||||||
int bitsAllocated,
|
int bitsAllocated,
|
||||||
|
int bitsStored,
|
||||||
int samplesPerPixel,
|
int samplesPerPixel,
|
||||||
ushort pixelRepresentation,
|
ushort pixelRepresentation,
|
||||||
string photometric,
|
string photometric,
|
||||||
|
|
@ -455,48 +461,90 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
if (bitsAllocated == 8)
|
if (bitsAllocated == 8)
|
||||||
{
|
{
|
||||||
//8 位灰度
|
byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options);
|
||||||
ApplyMask_Grayscale8(frameData, rows, cols, regions, (byte)Math.Min(options.GrayscaleMaskValue, byte.MaxValue));
|
ApplyMask_Grayscale8(frameData, rows, cols, regions, maskValue8);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16 位灰度
|
|
||||||
if (bitsAllocated == 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;
|
return;
|
||||||
}
|
}
|
||||||
|
throw new NotSupportedException(
|
||||||
throw new NotSupportedException($"Unsupported grayscale BitsAllocated={bitsAllocated}, Photometric={photometric}.");
|
$"Unsupported grayscale image: BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
||||||
}
|
}
|
||||||
|
|
||||||
//RGB 彩图
|
|
||||||
if (samplesPerPixel == 3)
|
if (samplesPerPixel == 3)
|
||||||
{
|
{
|
||||||
if (bitsAllocated != 8)
|
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)
|
if (planarConfiguration == 0)
|
||||||
{
|
{
|
||||||
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, options.ColorMaskValue);
|
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, options.ColorMaskValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planarConfiguration == 1)
|
if (planarConfiguration == 1)
|
||||||
{
|
{
|
||||||
ApplyMask_Color8_Planar(frameData, rows, cols, regions, options.ColorMaskValue);
|
ApplyMask_Color8_Planar(frameData, rows, cols, regions, options.ColorMaskValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotSupportedException($"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
$"Unsupported image format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, PixelRepresentation={pixelRepresentation}, Photometric={photometric}.");
|
$"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}");
|
||||||
|
}
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"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(
|
private static void ApplyMask_Grayscale8(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
|
|
@ -508,18 +556,16 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
if (left >= right || top >= bottom) continue;
|
if (left >= right || top >= bottom) continue;
|
||||||
|
|
||||||
for (int y = top; y < bottom; y++)
|
for (int y = top; y < bottom; y++)
|
||||||
{
|
{
|
||||||
int rowOffset = y * cols;
|
int offset = y * cols;
|
||||||
for (int x = left; x < right; x++)
|
for (int x = left; x < right; x++)
|
||||||
{
|
{
|
||||||
data[rowOffset + x] = maskValue;
|
data[offset + x] = maskValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyMask_Grayscale16(
|
private static void ApplyMask_Grayscale16(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
|
|
@ -531,22 +577,19 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
if (left >= right || top >= bottom) continue;
|
if (left >= right || top >= bottom) continue;
|
||||||
|
|
||||||
for (int y = top; y < bottom; y++)
|
for (int y = top; y < bottom; y++)
|
||||||
{
|
{
|
||||||
int rowOffset = y * cols;
|
int offset = y * cols;
|
||||||
for (int x = left; x < right; x++)
|
for (int x = left; x < right; x++)
|
||||||
{
|
{
|
||||||
int pixelIndex = rowOffset + x;
|
int pixelIndex = offset + x;
|
||||||
int byteIndex = pixelIndex * 2;
|
int byteIndex = pixelIndex * 2;
|
||||||
|
|
||||||
data[byteIndex] = (byte)(maskValue & 0xFF);
|
data[byteIndex] = (byte)(maskValue & 0xFF);
|
||||||
data[byteIndex + 1] = (byte)((maskValue >> 8) & 0xFF);
|
data[byteIndex + 1] = (byte)((maskValue >> 8) & 0xFF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyMask_Color8_Interleaved(
|
private static void ApplyMask_Color8_Interleaved(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
|
|
@ -556,23 +599,19 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
if (color == null || color.Length < 3)
|
if (color == null || color.Length < 3)
|
||||||
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
||||||
|
|
||||||
byte c0 = color[0];
|
byte c0 = color[0];
|
||||||
byte c1 = color[1];
|
byte c1 = color[1];
|
||||||
byte c2 = color[2];
|
byte c2 = color[2];
|
||||||
|
|
||||||
foreach (var region in regions)
|
foreach (var region in regions)
|
||||||
{
|
{
|
||||||
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
if (left >= right || top >= bottom) continue;
|
if (left >= right || top >= bottom) continue;
|
||||||
|
|
||||||
for (int y = top; y < bottom; y++)
|
for (int y = top; y < bottom; y++)
|
||||||
{
|
{
|
||||||
for (int x = left; x < right; x++)
|
for (int x = left; x < right; x++)
|
||||||
{
|
{
|
||||||
int pixelIndex = y * cols + x;
|
int pixelIndex = y * cols + x;
|
||||||
int byteIndex = pixelIndex * 3;
|
int byteIndex = pixelIndex * 3;
|
||||||
|
|
||||||
data[byteIndex] = c0;
|
data[byteIndex] = c0;
|
||||||
data[byteIndex + 1] = c1;
|
data[byteIndex + 1] = c1;
|
||||||
data[byteIndex + 2] = c2;
|
data[byteIndex + 2] = c2;
|
||||||
|
|
@ -580,7 +619,6 @@ public static class DicomPixelMasker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyMask_Color8_Planar(
|
private static void ApplyMask_Color8_Planar(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
|
|
@ -590,18 +628,14 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
if (color == null || color.Length < 3)
|
if (color == null || color.Length < 3)
|
||||||
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
|
||||||
|
|
||||||
int planeSize = rows * cols;
|
int planeSize = rows * cols;
|
||||||
|
|
||||||
byte c0 = color[0];
|
byte c0 = color[0];
|
||||||
byte c1 = color[1];
|
byte c1 = color[1];
|
||||||
byte c2 = color[2];
|
byte c2 = color[2];
|
||||||
|
|
||||||
foreach (var region in regions)
|
foreach (var region in regions)
|
||||||
{
|
{
|
||||||
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
if (left >= right || top >= bottom) continue;
|
if (left >= right || top >= bottom) continue;
|
||||||
|
|
||||||
for (int y = top; y < bottom; y++)
|
for (int y = top; y < bottom; y++)
|
||||||
{
|
{
|
||||||
int rowOffset = y * cols;
|
int rowOffset = y * cols;
|
||||||
|
|
@ -615,8 +649,10 @@ public static class DicomPixelMasker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private static (int left, int top, int right, int bottom) ClipRegion(
|
||||||
private static (int left, int top, int right, int bottom) ClipRegion(MaskRegion region, int imageWidth, int imageHeight)
|
MaskRegion region,
|
||||||
|
int imageWidth,
|
||||||
|
int imageHeight)
|
||||||
{
|
{
|
||||||
int left = Math.Max(0, region.X);
|
int left = Math.Max(0, region.X);
|
||||||
int top = Math.Max(0, region.Y);
|
int top = Math.Max(0, region.Y);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
using System;
|
using IdentityModel;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using IdentityModel;
|
|
||||||
|
|
||||||
namespace IRaCIS.Core.Application.Helper.OtherTool;
|
namespace IRaCIS.Core.Application.Helper.OtherTool;
|
||||||
|
|
||||||
|
|
@ -31,6 +32,11 @@ public static class WeComNotifier
|
||||||
|
|
||||||
public static async Task SendAlertAsync(string webhook, WeComAlert alert)
|
public static async Task SendAlertAsync(string webhook, WeComAlert alert)
|
||||||
{
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = new RestClient();
|
var client = new RestClient();
|
||||||
|
|
|
||||||
|
|
@ -16157,12 +16157,6 @@
|
||||||
要处理的帧,null 或空表示全部帧。
|
要处理的帧,null 或空表示全部帧。
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.GrayscaleMaskValue">
|
|
||||||
<summary>
|
|
||||||
灰度图像的遮挡像素值,默认 0。
|
|
||||||
注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.ColorMaskValue">
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.ColorMaskValue">
|
||||||
<summary>
|
<summary>
|
||||||
彩色图像的遮挡值,默认 [0,0,0]。
|
彩色图像的遮挡值,默认 [0,0,0]。
|
||||||
|
|
@ -16180,6 +16174,18 @@
|
||||||
是否更新 BurnedInAnnotation 为 NO。
|
是否更新 BurnedInAnnotation 为 NO。
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.AutoSelectGrayscaleMaskValue">
|
||||||
|
<summary>
|
||||||
|
是否启用自动灰度遮挡值。
|
||||||
|
true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.GrayscaleMaskValue">
|
||||||
|
<summary>
|
||||||
|
当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
|
||||||
|
当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.ValidateDataset(FellowOakDicom.DicomDataset)">
|
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.ValidateDataset(FellowOakDicom.DicomDataset)">
|
||||||
<summary>
|
<summary>
|
||||||
验证dicom tag
|
验证dicom tag
|
||||||
|
|
@ -16200,7 +16206,7 @@
|
||||||
<summary>
|
<summary>
|
||||||
转回原始 TransferSyntax
|
转回原始 TransferSyntax
|
||||||
</summary>
|
</summary>
|
||||||
<param name="uncompressedFile"></param>
|
<param name="workingFile"></param>
|
||||||
<param name="originalTransferSyntax"></param>
|
<param name="originalTransferSyntax"></param>
|
||||||
<param name="strictKeepTransferSyntax"></param>
|
<param name="strictKeepTransferSyntax"></param>
|
||||||
<param name="cancellationToken"></param>
|
<param name="cancellationToken"></param>
|
||||||
|
|
@ -17206,17 +17212,17 @@
|
||||||
</member>
|
</member>
|
||||||
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Question">
|
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Question">
|
||||||
<summary>
|
<summary>
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
质疑
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Consistency">
|
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Consistency">
|
||||||
<summary>
|
<summary>
|
||||||
һ<EFBFBD><EFBFBD><EFBFBD>Ժ˲<EFBFBD>
|
一致性核查
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:IRaCIS.Core.Application.ViewModel.CopyFrontAuditConfigItemDto">
|
<member name="T:IRaCIS.Core.Application.ViewModel.CopyFrontAuditConfigItemDto">
|
||||||
<summary>
|
<summary>
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
复制
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:IRaCIS.Core.Application.ViewModel.SystemNoticeView">
|
<member name="T:IRaCIS.Core.Application.ViewModel.SystemNoticeView">
|
||||||
|
|
|
||||||
|
|
@ -137,10 +137,7 @@ namespace IRaCIS.Core.Application.Service
|
||||||
{
|
{
|
||||||
new MaskRegion(0, 0, 200, 200)
|
new MaskRegion(0, 0, 200, 200)
|
||||||
};
|
};
|
||||||
var options = new DicomMaskOptions
|
|
||||||
{
|
|
||||||
StrictKeepTransferSyntax = true
|
|
||||||
};
|
|
||||||
foreach (var inputPath in Directory.EnumerateFiles(sourceDir, "*.dcm", SearchOption.TopDirectoryOnly))
|
foreach (var inputPath in Directory.EnumerateFiles(sourceDir, "*.dcm", SearchOption.TopDirectoryOnly))
|
||||||
{
|
{
|
||||||
var relativePath = Path.GetRelativePath(sourceDir, inputPath);
|
var relativePath = Path.GetRelativePath(sourceDir, inputPath);
|
||||||
|
|
@ -152,7 +149,19 @@ namespace IRaCIS.Core.Application.Service
|
||||||
}
|
}
|
||||||
await using var input = File.OpenRead(inputPath);
|
await using var input = File.OpenRead(inputPath);
|
||||||
await using var output = File.Create(outputPath);
|
await using var output = File.Create(outputPath);
|
||||||
await DicomPixelMasker.MaskAsync(input, output, regions, options);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DicomPixelMasker.MaskAsync(input, output, regions);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
// 跳过该文件
|
||||||
|
|
||||||
|
Console.WriteLine($"error: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Done: {relativePath}");
|
Console.WriteLine($"Done: {relativePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,6 @@ public enum ReadingOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[ComplexType]
|
|
||||||
public class DefaultSegmentNameDto
|
public class DefaultSegmentNameDto
|
||||||
{
|
{
|
||||||
public string SegmentationName { get; set; } =string.Empty;
|
public string SegmentationName { get; set; } =string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,10 @@ public class IRaCISDBContext : DbContext
|
||||||
modelBuilder.Entity<ReadingQuestionCriterionTrial>(entity =>
|
modelBuilder.Entity<ReadingQuestionCriterionTrial>(entity =>
|
||||||
{
|
{
|
||||||
//默认SegmentName
|
//默认SegmentName
|
||||||
entity.OwnsOne(x => x.DefaultSegmentName, ownedNavigationBuilder =>
|
entity.Property(e => e.DefaultSegmentName).HasConversion(
|
||||||
{
|
v => v == null ? null : JsonConvert.SerializeObject(v),
|
||||||
ownedNavigationBuilder.ToJson();
|
v => string.IsNullOrEmpty(v) ? new DefaultSegmentNameDto() : JsonConvert.DeserializeObject<DefaultSegmentNameDto>(v)
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
#region pgsql codefirst 配置 暂时屏蔽
|
#region pgsql codefirst 配置 暂时屏蔽
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue