本地调试错误不发送企业微信+遮盖图像初步测试
parent
e430c40b99
commit
e197a889d4
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
using Newtonsoft.Json;
|
||||
using RestSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityModel;
|
||||
|
||||
namespace IRaCIS.Core.Application.Helper.OtherTool;
|
||||
|
||||
|
|
@ -31,6 +32,11 @@ public static class WeComNotifier
|
|||
|
||||
public static async Task SendAlertAsync(string webhook, WeComAlert alert)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = new RestClient();
|
||||
|
|
|
|||
|
|
@ -16157,12 +16157,6 @@
|
|||
要处理的帧,null 或空表示全部帧。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.GrayscaleMaskValue">
|
||||
<summary>
|
||||
灰度图像的遮挡像素值,默认 0。
|
||||
注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.ColorMaskValue">
|
||||
<summary>
|
||||
彩色图像的遮挡值,默认 [0,0,0]。
|
||||
|
|
@ -16180,6 +16174,18 @@
|
|||
是否更新 BurnedInAnnotation 为 NO。
|
||||
</summary>
|
||||
</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)">
|
||||
<summary>
|
||||
验证dicom tag
|
||||
|
|
@ -16200,7 +16206,7 @@
|
|||
<summary>
|
||||
转回原始 TransferSyntax
|
||||
</summary>
|
||||
<param name="uncompressedFile"></param>
|
||||
<param name="workingFile"></param>
|
||||
<param name="originalTransferSyntax"></param>
|
||||
<param name="strictKeepTransferSyntax"></param>
|
||||
<param name="cancellationToken"></param>
|
||||
|
|
@ -17206,17 +17212,17 @@
|
|||
</member>
|
||||
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Question">
|
||||
<summary>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
质疑
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:IRaCIS.Core.Application.ViewModel.AccessToDialogueEnum.Consistency">
|
||||
<summary>
|
||||
һ<EFBFBD><EFBFBD><EFBFBD>Ժ˲<EFBFBD>
|
||||
一致性核查
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:IRaCIS.Core.Application.ViewModel.CopyFrontAuditConfigItemDto">
|
||||
<summary>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
复制
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:IRaCIS.Core.Application.ViewModel.SystemNoticeView">
|
||||
|
|
|
|||
|
|
@ -137,10 +137,7 @@ namespace IRaCIS.Core.Application.Service
|
|||
{
|
||||
new MaskRegion(0, 0, 200, 200)
|
||||
};
|
||||
var options = new DicomMaskOptions
|
||||
{
|
||||
StrictKeepTransferSyntax = true
|
||||
};
|
||||
|
||||
foreach (var inputPath in Directory.EnumerateFiles(sourceDir, "*.dcm", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
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 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}");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue