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 running
Details
continuous-integration/drone/push Build is running
Details
commit
be5c25ba5f
|
|
@ -196,71 +196,59 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||||
if (output == null) throw new ArgumentNullException(nameof(output));
|
if (output == null) throw new ArgumentNullException(nameof(output));
|
||||||
|
|
||||||
var regionList = regions?.ToList() ?? throw new ArgumentNullException(nameof(regions));
|
var regionList = regions?.ToList() ?? throw new ArgumentNullException(nameof(regions));
|
||||||
if (regionList.Count == 0)
|
if (regionList.Count == 0)
|
||||||
throw new ArgumentException("At least one mask region is required.", nameof(regions));
|
throw new ArgumentException("At least one mask region is required.", nameof(regions));
|
||||||
options ??= new DicomMaskOptions();
|
options ??= new DicomMaskOptions();
|
||||||
|
|
||||||
if (input.CanSeek)
|
if (input.CanSeek)
|
||||||
input.Position = 0;
|
input.Position = 0;
|
||||||
|
|
||||||
var originalFile = await DicomFile.OpenAsync(input, FileReadOption.ReadAll).ConfigureAwait(false);
|
var originalFile = await DicomFile.OpenAsync(input, FileReadOption.ReadAll).ConfigureAwait(false);
|
||||||
var originalDataset = originalFile.Dataset;
|
var originalDataset = originalFile.Dataset;
|
||||||
|
|
||||||
ValidateDataset(originalDataset);
|
ValidateDataset(originalDataset);
|
||||||
|
|
||||||
var originalTs = originalFile.FileMetaInfo.TransferSyntax;
|
var originalTs = originalFile.FileMetaInfo.TransferSyntax;
|
||||||
var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
// 先解压到工作格式
|
||||||
|
|
||||||
var rows = originalDataset.GetSingleValue<int>(DicomTag.Rows);
|
|
||||||
var cols = originalDataset.GetSingleValue<int>(DicomTag.Columns);
|
|
||||||
var bitsAllocated = originalDataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
|
||||||
var bitsStored = originalDataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
|
|
||||||
var samplesPerPixel = originalDataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
|
||||||
var pixelRepresentation = originalDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
|
||||||
var planarConfiguration = originalDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
|
||||||
Console.WriteLine($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}");
|
|
||||||
|
|
||||||
var isSupport = IsSupportedPhotometric(originalPhotometric, samplesPerPixel);
|
|
||||||
|
|
||||||
if (isSupport)
|
|
||||||
{
|
|
||||||
// 1. 转为工作用的未压缩 DICOM
|
|
||||||
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
||||||
// 2. 修改像素
|
var workingDataset = workingFile.Dataset;
|
||||||
MaskPixelDataInPlace(workingFile.Dataset, regionList, options);
|
|
||||||
// 3. 保持原 PhotometricInterpretation
|
var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
||||||
if (!string.IsNullOrWhiteSpace(originalPhotometric))
|
Console.WriteLine($"Original Photometric={originalPhotometric}, Original TS={originalTs.UID.UID}");
|
||||||
|
|
||||||
|
var rows = workingDataset.GetSingleValue<int>(DicomTag.Rows);
|
||||||
|
var cols = workingDataset.GetSingleValue<int>(DicomTag.Columns);
|
||||||
|
var bitsAllocated = workingDataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
||||||
|
var bitsStored = workingDataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
|
||||||
|
var samplesPerPixel = workingDataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
||||||
|
var pixelRepresentation = workingDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
|
||||||
|
var planarConfiguration = workingDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
|
||||||
|
var workingPhotometric = workingDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
|
||||||
|
|
||||||
|
Console.WriteLine($"Working Photometric={workingPhotometric}, Working TS={workingFile.FileMetaInfo.TransferSyntax.UID.UID}");
|
||||||
|
|
||||||
|
Console.WriteLine($"Working Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}, Photometric={workingPhotometric}, TransferSyntax={workingFile.FileMetaInfo.TransferSyntax.UID.UID}");
|
||||||
|
var isSupport = IsSupportedPhotometric(workingPhotometric, samplesPerPixel);
|
||||||
|
if (!isSupport)
|
||||||
{
|
{
|
||||||
workingFile.Dataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
throw new NotSupportedException(
|
||||||
|
$"Unsupported PhotometricInterpretation after decode: {workingPhotometric}, SamplesPerPixel={samplesPerPixel}");
|
||||||
}
|
}
|
||||||
// 4. 可选更新 BurnedInAnnotation
|
// 修改 working dataset 的像素
|
||||||
|
MaskPixelDataInPlace(workingDataset, regionList, options);
|
||||||
if (options.UpdateBurnedInAnnotationToNo)
|
if (options.UpdateBurnedInAnnotationToNo)
|
||||||
{
|
{
|
||||||
workingFile.Dataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
||||||
}
|
}
|
||||||
// 5. 编码回原始传输语法
|
// 不要把 original photometric 强行写回
|
||||||
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
||||||
workingFile,
|
workingFile,
|
||||||
originalTs,
|
originalTs,
|
||||||
options.StrictKeepTransferSyntax,
|
options.StrictKeepTransferSyntax,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
finalFile.FileMetaInfo.TransferSyntax = originalTs;
|
finalFile.FileMetaInfo.TransferSyntax = originalTs;
|
||||||
|
|
||||||
if (output.CanSeek)
|
if (output.CanSeek)
|
||||||
output.SetLength(0);
|
output.SetLength(0);
|
||||||
|
|
||||||
await finalFile.SaveAsync(output).ConfigureAwait(false);
|
await finalFile.SaveAsync(output).ConfigureAwait(false);
|
||||||
|
|
||||||
if (output.CanSeek)
|
if (output.CanSeek)
|
||||||
output.Position = 0;
|
output.Position = 0;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -285,35 +273,21 @@ public static class DicomPixelMasker
|
||||||
{
|
{
|
||||||
if (samplesPerPixel == 1)
|
if (samplesPerPixel == 1)
|
||||||
{
|
{
|
||||||
if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase) ||
|
return string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase)
|
||||||
string.Equals(photometric, "MONOCHROME2", StringComparison.OrdinalIgnoreCase) ||
|
|| string.Equals(photometric, "MONOCHROME2", StringComparison.OrdinalIgnoreCase)
|
||||||
string.Equals(photometric, "PALETTE COLOR", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(photometric, "PALETTE COLOR", StringComparison.OrdinalIgnoreCase);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (samplesPerPixel == 3)
|
if (samplesPerPixel == 3)
|
||||||
{
|
{
|
||||||
if (string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase) ||
|
return string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase)
|
||||||
string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase)
|
||||||
{
|
|| string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase)
|
||||||
return true;
|
|| string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase)
|
||||||
}
|
|| string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase);
|
||||||
// 对 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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
//throw new NotSupportedException($"Unsupported color PhotometricInterpretation: {photometric}");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
//throw new NotSupportedException($"Unsupported SamplesPerPixel={samplesPerPixel}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -491,13 +465,13 @@ public static class DicomPixelMasker
|
||||||
if (bitsAllocated == 8)
|
if (bitsAllocated == 8)
|
||||||
{
|
{
|
||||||
byte maskIndex = (byte)ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
|
byte maskIndex = (byte)ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
|
||||||
ApplyMask_Grayscale8(frameData, rows, cols, regions, maskIndex);
|
ApplyMask_SingleSample8(frameData, rows, cols, regions, maskIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bitsAllocated == 16)
|
if (bitsAllocated == 16)
|
||||||
{
|
{
|
||||||
ushort maskIndex = ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
|
ushort maskIndex = ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
|
||||||
ApplyMask_Grayscale16(frameData, rows, cols, regions, maskIndex);
|
ApplyMask_SingleSample16(frameData, rows, cols, regions, maskIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
|
|
@ -508,7 +482,7 @@ public static class DicomPixelMasker
|
||||||
if (bitsAllocated == 8)
|
if (bitsAllocated == 8)
|
||||||
{
|
{
|
||||||
byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options);
|
byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options);
|
||||||
ApplyMask_Grayscale8(frameData, rows, cols, regions, maskValue8);
|
ApplyMask_SingleSample8(frameData, rows, cols, regions, maskValue8);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bitsAllocated == 16)
|
if (bitsAllocated == 16)
|
||||||
|
|
@ -518,7 +492,7 @@ public static class DicomPixelMasker
|
||||||
bitsStored,
|
bitsStored,
|
||||||
pixelRepresentation,
|
pixelRepresentation,
|
||||||
options);
|
options);
|
||||||
ApplyMask_Grayscale16(frameData, rows, cols, regions, maskValue16);
|
ApplyMask_SingleSample16(frameData, rows, cols, regions, maskValue16);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
|
|
@ -534,22 +508,268 @@ public static class DicomPixelMasker
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
$"Unsupported color image: SamplesPerPixel=3, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
$"Unsupported color image: SamplesPerPixel=3, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
||||||
}
|
}
|
||||||
if (planarConfiguration == 0)
|
int expectedFull = rows * cols * 3;
|
||||||
|
int expected422 = rows * cols * 2;
|
||||||
|
int chromaRows = (rows + 1) / 2;
|
||||||
|
int chromaCols = (cols + 1) / 2;
|
||||||
|
int expected420 = rows * cols + 2 * chromaRows * chromaCols;
|
||||||
|
// 根据 photometric 自动解析默认颜色
|
||||||
|
var colorMask = ResolveColorMaskValue(photometric, options);
|
||||||
|
// 1) 先处理能通过长度明确识别的 subsampled YBR
|
||||||
|
if (frameData.Length == expected422)
|
||||||
{
|
{
|
||||||
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, options.ColorMaskValue);
|
if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ApplyMask_YbrFull422(frameData, rows, cols, regions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (planarConfiguration == 1)
|
if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
ApplyMask_Color8_Planar(frameData, rows, cols, regions, options.ColorMaskValue);
|
ApplyMask_YbrPartial422(frameData, rows, cols, regions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
$"Unsupported PlanarConfiguration={planarConfiguration}, Photometric={photometric}");
|
$"Frame length matches 4:2:2 layout, but Photometric={photometric} is not supported for 422 masking.");
|
||||||
|
}
|
||||||
|
if (frameData.Length == expected420)
|
||||||
|
{
|
||||||
|
if (string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ApplyMask_YbrPartial420(frameData, rows, cols, regions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"Frame length matches 4:2:0 layout, but Photometric={photometric} is not supported for 420 masking.");
|
||||||
|
}
|
||||||
|
// 2) full-resolution 三通道数据,长度无法区分 planar / interleaved
|
||||||
|
// 必须依赖 PlanarConfiguration
|
||||||
|
if (frameData.Length == expectedFull)
|
||||||
|
{
|
||||||
|
if (planarConfiguration == 1)
|
||||||
|
{
|
||||||
|
ApplyMask_Color8_Planar(frameData, rows, cols, regions, colorMask);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 包括 planarConfiguration == 0 以及很多解码后默认输出
|
||||||
|
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, colorMask);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 3) 最后兜底:如果长度异常,尝试按 PlanarConfiguration 处理
|
||||||
|
if (planarConfiguration == 1)
|
||||||
|
{
|
||||||
|
ApplyMask_Color8_Planar(frameData, rows, cols, regions, colorMask);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (planarConfiguration == 0)
|
||||||
|
{
|
||||||
|
ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, colorMask);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"Unsupported color frame layout: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, " +
|
||||||
|
$"Photometric={photometric}, PlanarConfiguration={planarConfiguration}, FrameLength={frameData.Length}");
|
||||||
}
|
}
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
$"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
$"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] ResolveColorMaskValue(string photometric, DicomMaskOptions options)
|
||||||
|
{
|
||||||
|
if (options.ColorMaskValue != null && options.ColorMaskValue.Length >= 3)
|
||||||
|
return options.ColorMaskValue;
|
||||||
|
|
||||||
|
if (string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 黑色 in YCbCr full range
|
||||||
|
return new byte[] { 0, 128, 128 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 黑色 in YCbCr video range
|
||||||
|
return new byte[] { 16, 128, 128 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGB 默认黑
|
||||||
|
return new byte[] { 0, 0, 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ybr 422...
|
||||||
|
private static void ApplyMask_YbrFull422(
|
||||||
|
byte[] frameData,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions)
|
||||||
|
{
|
||||||
|
const byte maskY = 0;
|
||||||
|
const byte maskCb = 128;
|
||||||
|
const byte maskCr = 128;
|
||||||
|
|
||||||
|
int bytesPerRow = cols * 2; // 2 pixels -> 4 bytes
|
||||||
|
|
||||||
|
foreach (var region in regions)
|
||||||
|
{
|
||||||
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
|
if (left >= right || top >= bottom)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int alignedLeft = left & ~1;
|
||||||
|
int alignedRight = (right + 1) & ~1;
|
||||||
|
if (alignedRight > cols) alignedRight = cols;
|
||||||
|
|
||||||
|
for (int y = top; y < bottom; y++)
|
||||||
|
{
|
||||||
|
int rowOffset = y * bytesPerRow;
|
||||||
|
|
||||||
|
for (int x = alignedLeft; x < alignedRight; x += 2)
|
||||||
|
{
|
||||||
|
int offset = rowOffset + (x / 2) * 4;
|
||||||
|
|
||||||
|
// Y0 Cb Y1 Cr
|
||||||
|
frameData[offset + 0] = maskY;
|
||||||
|
frameData[offset + 1] = maskCb;
|
||||||
|
frameData[offset + 2] = maskY;
|
||||||
|
frameData[offset + 3] = maskCr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMask_YbrPartial422(
|
||||||
|
byte[] frameData,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions)
|
||||||
|
{
|
||||||
|
const byte maskY = 16;
|
||||||
|
const byte maskCb = 128;
|
||||||
|
const byte maskCr = 128;
|
||||||
|
|
||||||
|
int bytesPerRow = cols * 2; // 2 pixels -> 4 bytes
|
||||||
|
|
||||||
|
foreach (var region in regions)
|
||||||
|
{
|
||||||
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
|
if (left >= right || top >= bottom)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int alignedLeft = left & ~1;
|
||||||
|
int alignedRight = (right + 1) & ~1;
|
||||||
|
if (alignedRight > cols) alignedRight = cols;
|
||||||
|
|
||||||
|
for (int y = top; y < bottom; y++)
|
||||||
|
{
|
||||||
|
int rowOffset = y * bytesPerRow;
|
||||||
|
|
||||||
|
for (int x = alignedLeft; x < alignedRight; x += 2)
|
||||||
|
{
|
||||||
|
int offset = rowOffset + (x / 2) * 4;
|
||||||
|
|
||||||
|
// Y0 Cb Y1 Cr
|
||||||
|
frameData[offset + 0] = maskY;
|
||||||
|
frameData[offset + 1] = maskCb;
|
||||||
|
frameData[offset + 2] = maskY;
|
||||||
|
frameData[offset + 3] = maskCr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMask_YbrPartial420(
|
||||||
|
byte[] frameData,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions)
|
||||||
|
{
|
||||||
|
const byte maskY = 16;
|
||||||
|
const byte maskCb = 128;
|
||||||
|
const byte maskCr = 128;
|
||||||
|
|
||||||
|
int yPlaneSize = rows * cols;
|
||||||
|
int chromaRows = (rows + 1) / 2;
|
||||||
|
int chromaCols = (cols + 1) / 2;
|
||||||
|
int chromaPlaneSize = chromaRows * chromaCols;
|
||||||
|
|
||||||
|
int cbBase = yPlaneSize;
|
||||||
|
int crBase = yPlaneSize + chromaPlaneSize;
|
||||||
|
|
||||||
|
foreach (var region in regions)
|
||||||
|
{
|
||||||
|
var (left, top, right, bottom) = ClipRegion(region, cols, rows);
|
||||||
|
if (left >= right || top >= bottom)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int alignedLeft = left & ~1;
|
||||||
|
int alignedTop = top & ~1;
|
||||||
|
int alignedRight = (right + 1) & ~1;
|
||||||
|
int alignedBottom = (bottom + 1) & ~1;
|
||||||
|
|
||||||
|
if (alignedRight > cols) alignedRight = cols;
|
||||||
|
if (alignedBottom > rows) alignedBottom = rows;
|
||||||
|
|
||||||
|
// Y plane
|
||||||
|
for (int y = top; y < bottom; y++)
|
||||||
|
{
|
||||||
|
int rowOffset = y * cols;
|
||||||
|
for (int x = left; x < right; x++)
|
||||||
|
{
|
||||||
|
frameData[rowOffset + x] = maskY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cb / Cr plane, one chroma sample for each 2x2 block
|
||||||
|
for (int y = alignedTop; y < alignedBottom; y += 2)
|
||||||
|
{
|
||||||
|
int chromaY = y / 2;
|
||||||
|
|
||||||
|
for (int x = alignedLeft; x < alignedRight; x += 2)
|
||||||
|
{
|
||||||
|
int chromaX = x / 2;
|
||||||
|
int chromaIndex = chromaY * chromaCols + chromaX;
|
||||||
|
|
||||||
|
frameData[cbBase + chromaIndex] = maskCb;
|
||||||
|
frameData[crBase + chromaIndex] = maskCr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static bool TryApplyMaskForSubsampledYbr(
|
||||||
|
byte[] frameData,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
string photometric,
|
||||||
|
IReadOnlyList<MaskRegion> regions)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(photometric))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ApplyMask_YbrFull422(frameData, rows, cols, regions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ApplyMask_YbrPartial422(frameData, rows, cols, regions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ApplyMask_YbrPartial420(frameData, rows, cols, regions);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private static ushort ResolvePaletteColorMaskIndex(
|
private static ushort ResolvePaletteColorMaskIndex(
|
||||||
DicomDataset dataset,
|
DicomDataset dataset,
|
||||||
int bitsStored,
|
int bitsStored,
|
||||||
|
|
@ -664,162 +884,6 @@ public static class DicomPixelMasker
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region 尾彩
|
|
||||||
|
|
||||||
//private static ushort ResolvePaletteColorMaskIndex16(
|
|
||||||
//DicomDataset dataset,
|
|
||||||
//int bitsStored,
|
|
||||||
//DicomMaskOptions options)
|
|
||||||
//{
|
|
||||||
// int maxValue = bitsStored >= 16 ? 65535 : (1 << bitsStored) - 1;
|
|
||||||
|
|
||||||
// if (options.PaletteColorMaskIndex.HasValue)
|
|
||||||
// {
|
|
||||||
// int v = options.PaletteColorMaskIndex.Value;
|
|
||||||
// if (v < 0 || v > maxValue)
|
|
||||||
// {
|
|
||||||
// throw new ArgumentOutOfRangeException(
|
|
||||||
// nameof(options.PaletteColorMaskIndex),
|
|
||||||
// $"PaletteColorMaskIndex must be in range [0, {maxValue}] for BitsStored={bitsStored}");
|
|
||||||
// }
|
|
||||||
// return (ushort)v;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// int autoIndex = FindDarkestPaletteIndex(dataset, maxValue);
|
|
||||||
// return (ushort)autoIndex;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private static int FindDarkestPaletteIndex(DicomDataset dataset, int maxPixelValue)
|
|
||||||
//{
|
|
||||||
// var redDesc = GetPaletteDescriptor(dataset, DicomTag.RedPaletteColorLookupTableDescriptor);
|
|
||||||
// var greenDesc = GetPaletteDescriptor(dataset, DicomTag.GreenPaletteColorLookupTableDescriptor);
|
|
||||||
// var blueDesc = GetPaletteDescriptor(dataset, DicomTag.BluePaletteColorLookupTableDescriptor);
|
|
||||||
|
|
||||||
// if (redDesc.EntryCount != greenDesc.EntryCount || redDesc.EntryCount != blueDesc.EntryCount ||
|
|
||||||
// redDesc.FirstMappedValue != greenDesc.FirstMappedValue || redDesc.FirstMappedValue != blueDesc.FirstMappedValue ||
|
|
||||||
// redDesc.BitsPerEntry != greenDesc.BitsPerEntry || redDesc.BitsPerEntry != blueDesc.BitsPerEntry)
|
|
||||||
// {
|
|
||||||
// throw new NotSupportedException("RGB palette LUT descriptors are inconsistent.");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ushort[] red = GetPaletteLutDataAsUShortArray(dataset, DicomTag.RedPaletteColorLookupTableData, redDesc);
|
|
||||||
// ushort[] green = GetPaletteLutDataAsUShortArray(dataset, DicomTag.GreenPaletteColorLookupTableData, greenDesc);
|
|
||||||
// ushort[] blue = GetPaletteLutDataAsUShortArray(dataset, DicomTag.BluePaletteColorLookupTableData, blueDesc);
|
|
||||||
|
|
||||||
// if (red.Length != redDesc.EntryCount || green.Length != greenDesc.EntryCount || blue.Length != blueDesc.EntryCount)
|
|
||||||
// {
|
|
||||||
// throw new NotSupportedException("Palette LUT data length does not match LUT descriptor.");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// long bestScore = long.MaxValue;
|
|
||||||
// int bestLutIndex = 0;
|
|
||||||
|
|
||||||
// for (int i = 0; i < redDesc.EntryCount; i++)
|
|
||||||
// {
|
|
||||||
// long score = (long)red[i] + green[i] + blue[i];
|
|
||||||
// if (score < bestScore)
|
|
||||||
// {
|
|
||||||
// bestScore = score;
|
|
||||||
// bestLutIndex = i;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// int pixelValue = redDesc.FirstMappedValue + bestLutIndex;
|
|
||||||
|
|
||||||
// if (pixelValue < 0)
|
|
||||||
// pixelValue = 0;
|
|
||||||
// if (pixelValue > maxPixelValue)
|
|
||||||
// pixelValue = maxPixelValue;
|
|
||||||
|
|
||||||
// return pixelValue;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private static ushort[] GetPaletteLutDataAsUShortArray(
|
|
||||||
//DicomDataset dataset,
|
|
||||||
//DicomTag dataTag,
|
|
||||||
//PaletteLutDescriptor descriptor)
|
|
||||||
//{
|
|
||||||
// if (!dataset.TryGetSingleValue<byte[]>(dataTag, out var rawBytes) || rawBytes == null || rawBytes.Length == 0)
|
|
||||||
// {
|
|
||||||
// throw new NotSupportedException($"Missing palette LUT data: {dataTag}");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// int entryCount = descriptor.EntryCount;
|
|
||||||
// int bitsPerEntry = descriptor.BitsPerEntry;
|
|
||||||
|
|
||||||
// if (bitsPerEntry <= 8)
|
|
||||||
// {
|
|
||||||
// if (rawBytes.Length < entryCount)
|
|
||||||
// {
|
|
||||||
// throw new NotSupportedException(
|
|
||||||
// $"Palette LUT data too short for {dataTag}. Expected at least {entryCount} bytes, actual {rawBytes.Length}.");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var result = new ushort[entryCount];
|
|
||||||
// for (int i = 0; i < entryCount; i++)
|
|
||||||
// {
|
|
||||||
// result[i] = rawBytes[i];
|
|
||||||
// }
|
|
||||||
// return result;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (bitsPerEntry <= 16)
|
|
||||||
// {
|
|
||||||
// int requiredBytes = entryCount * 2;
|
|
||||||
// if (rawBytes.Length < requiredBytes)
|
|
||||||
// {
|
|
||||||
// throw new NotSupportedException(
|
|
||||||
// $"Palette LUT data too short for {dataTag}. Expected at least {requiredBytes} bytes, actual {rawBytes.Length}.");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var result = new ushort[entryCount];
|
|
||||||
// for (int i = 0; i < entryCount; i++)
|
|
||||||
// {
|
|
||||||
// int byteIndex = i * 2;
|
|
||||||
// result[i] = (ushort)(rawBytes[byteIndex] | (rawBytes[byteIndex + 1] << 8));
|
|
||||||
// }
|
|
||||||
// return result;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// throw new NotSupportedException(
|
|
||||||
// $"Unsupported palette LUT bits per entry {bitsPerEntry} for {dataTag}.");
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private readonly struct PaletteLutDescriptor
|
|
||||||
//{
|
|
||||||
// public int EntryCount { get; }
|
|
||||||
// public int FirstMappedValue { get; }
|
|
||||||
// public int BitsPerEntry { get; }
|
|
||||||
|
|
||||||
// public PaletteLutDescriptor(int entryCount, int firstMappedValue, int bitsPerEntry)
|
|
||||||
// {
|
|
||||||
// EntryCount = entryCount;
|
|
||||||
// FirstMappedValue = firstMappedValue;
|
|
||||||
// BitsPerEntry = bitsPerEntry;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private static PaletteLutDescriptor GetPaletteDescriptor(DicomDataset dataset, DicomTag tag)
|
|
||||||
//{
|
|
||||||
// var values = dataset.GetValues<ushort>(tag);
|
|
||||||
// if (values == null || values.Length < 3)
|
|
||||||
// throw new NotSupportedException($"Missing or invalid palette LUT descriptor: {tag}");
|
|
||||||
|
|
||||||
// int entryCount = values[0] == 0 ? 65536 : values[0];
|
|
||||||
|
|
||||||
// // 第二个值在 DICOM 标准中是 signed short
|
|
||||||
// int firstMappedValue = (short)values[1];
|
|
||||||
|
|
||||||
// int bitsPerEntry = values[2];
|
|
||||||
// if (bitsPerEntry <= 0)
|
|
||||||
// throw new NotSupportedException($"Invalid palette LUT descriptor bits per entry: {bitsPerEntry}");
|
|
||||||
|
|
||||||
// return new PaletteLutDescriptor(entryCount, firstMappedValue, bitsPerEntry);
|
|
||||||
//}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private static byte ResolveGrayscaleMaskValue8(string photometric, DicomMaskOptions options)
|
private static byte ResolveGrayscaleMaskValue8(string photometric, DicomMaskOptions options)
|
||||||
{
|
{
|
||||||
if (!options.AutoSelectGrayscaleMaskValue)
|
if (!options.AutoSelectGrayscaleMaskValue)
|
||||||
|
|
@ -864,7 +928,7 @@ public static class DicomPixelMasker
|
||||||
return unchecked((ushort)selected);
|
return unchecked((ushort)selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static void ApplyMask_Grayscale8(
|
private static void ApplyMask_SingleSample8(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
int cols,
|
int cols,
|
||||||
|
|
@ -885,7 +949,7 @@ public static class DicomPixelMasker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static void ApplyMask_Grayscale16(
|
private static void ApplyMask_SingleSample16(
|
||||||
byte[] data,
|
byte[] data,
|
||||||
int rows,
|
int rows,
|
||||||
int cols,
|
int cols,
|
||||||
|
|
|
||||||
|
|
@ -17242,17 +17242,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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue