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

Test_IRC_Net10
he 2026-04-14 09:13:48 +08:00
commit 1e509c0ec7
6 changed files with 214 additions and 158 deletions

View File

@ -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
{
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);
}
}
try try
{ {
var currentTs = uncompressedFile.FileMetaInfo.TransferSyntax; var currentTs = workingFile.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;
} }
} }
@ -351,77 +368,66 @@ public static class DicomPixelMasker
/// <param name="regions"></param> /// <param name="regions"></param>
/// <param name="options"></param> /// <param name="options"></param>
private static void MaskPixelDataInPlace( private static void MaskPixelDataInPlace(
DicomDataset dataset, DicomDataset dataset,
IReadOnlyList<MaskRegion> regions, IReadOnlyList<MaskRegion> regions,
DicomMaskOptions options) DicomMaskOptions options)
{ {
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);
@ -440,63 +445,106 @@ public static class DicomPixelMasker
} }
private static void ApplyMaskToFrame( private static void ApplyMaskToFrame(
byte[] frameData, byte[] frameData,
int rows, int rows,
int cols, int cols,
int bitsAllocated, int bitsAllocated,
int samplesPerPixel, int bitsStored,
ushort pixelRepresentation, int samplesPerPixel,
string photometric, ushort pixelRepresentation,
ushort planarConfiguration, string photometric,
IReadOnlyList<MaskRegion> regions, ushort planarConfiguration,
DicomMaskOptions options) IReadOnlyList<MaskRegion> regions,
DicomMaskOptions options)
{ {
if (samplesPerPixel == 1) if (samplesPerPixel == 1)
{ {
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(
throw new NotSupportedException($"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}."); $"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}");
} }
throw new NotSupportedException( 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( 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);

View File

@ -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();

View File

@ -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">

View File

@ -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}");
} }

View File

@ -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;

View File

@ -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 配置 暂时屏蔽