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 failing
Details
continuous-integration/drone/push Build is failing
Details
commit
e430c40b99
|
|
@ -150,7 +150,7 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger<FileSync
|
||||||
{
|
{
|
||||||
log.JobState = jobState.FAILED;
|
log.JobState = jobState.FAILED;
|
||||||
|
|
||||||
log.Msg = ex.Message[..300];
|
log.Msg = ex.Message.Length > 300 ? ex.Message.Substring(0, 300) : ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.EndTime = DateTime.Now;
|
log.EndTime = DateTime.Now;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class SyncFileRecoveryService(IServiceScopeFactory _scopeFactory, FileSyn
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService<IRepository<FileUploadRecord>>();
|
var fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService<IRepository<FileUploadRecord>>();
|
||||||
|
|
||||||
// 延迟启动,保证主机快速启动
|
// 延迟启动,保证主机快速启动
|
||||||
|
|
@ -143,7 +143,7 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger<FileSync
|
||||||
{
|
{
|
||||||
log.JobState = jobState.FAILED;
|
log.JobState = jobState.FAILED;
|
||||||
|
|
||||||
log.Msg = ex.Message[..300];
|
log.Msg = ex.Message.Length > 300 ? ex.Message.Substring(0, 300) : ex.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.EndTime = DateTime.Now;
|
log.EndTime = DateTime.Now;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
|
||||||
|
using FellowOakDicom;
|
||||||
using FellowOakDicom.Imaging;
|
using FellowOakDicom.Imaging;
|
||||||
|
using FellowOakDicom.Imaging.Codec;
|
||||||
|
using FellowOakDicom.IO.Buffer;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace IRaCIS.Core.Application.Helper;
|
namespace IRaCIS.Core.Application.Helper;
|
||||||
|
|
||||||
|
#region 废弃
|
||||||
|
|
||||||
public static class DicomSliceSorterFast
|
public static class DicomSliceSorterFast
|
||||||
{
|
{
|
||||||
|
|
@ -94,6 +98,536 @@ public static class DicomSliceSorterFast
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public sealed class MaskRegion
|
||||||
|
{
|
||||||
|
public int X { get; }
|
||||||
|
public int Y { get; }
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
public MaskRegion(int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width));
|
||||||
|
if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height));
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class DicomMaskOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 要处理的帧,null 或空表示全部帧。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<int>? FrameIndices { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 灰度图像的遮挡像素值,默认 0。
|
||||||
|
/// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
|
||||||
|
/// </summary>
|
||||||
|
public ushort GrayscaleMaskValue { get; init; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 彩色图像的遮挡值,默认 [0,0,0]。
|
||||||
|
/// 对 RGB 表示黑色;对 YBR 则表示三个分量都写 0。
|
||||||
|
/// </summary>
|
||||||
|
public byte[] ColorMaskValue { get; init; } = new byte[] { 0, 0, 0 };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 严格保持原始 TransferSyntax。
|
||||||
|
/// 如果无法重新编码到原语法,则抛异常。
|
||||||
|
/// </summary>
|
||||||
|
public bool StrictKeepTransferSyntax { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否更新 BurnedInAnnotation 为 NO。
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdateBurnedInAnnotationToNo { get; init; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static class DicomPixelMasker
|
||||||
|
{
|
||||||
|
public static async Task<MemoryStream> MaskAsync(
|
||||||
|
Stream input,
|
||||||
|
IEnumerable<MaskRegion> regions,
|
||||||
|
DicomMaskOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var output = new MemoryStream();
|
||||||
|
await MaskAsync(input, output, regions, options, cancellationToken).ConfigureAwait(false);
|
||||||
|
output.Position = 0;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task MaskAsync(
|
||||||
|
Stream input,
|
||||||
|
Stream output,
|
||||||
|
IEnumerable<MaskRegion> regions,
|
||||||
|
DicomMaskOptions? options = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||||
|
if (output == null) throw new ArgumentNullException(nameof(output));
|
||||||
|
|
||||||
|
var regionList = regions?.ToList() ?? throw new ArgumentNullException(nameof(regions));
|
||||||
|
if (regionList.Count == 0)
|
||||||
|
throw new ArgumentException("At least one mask region is required.", nameof(regions));
|
||||||
|
options ??= new DicomMaskOptions();
|
||||||
|
|
||||||
|
if (input.CanSeek)
|
||||||
|
input.Position = 0;
|
||||||
|
|
||||||
|
var originalFile = await DicomFile.OpenAsync(input, FileReadOption.ReadAll).ConfigureAwait(false);
|
||||||
|
var originalDataset = originalFile.Dataset;
|
||||||
|
|
||||||
|
ValidateDataset(originalDataset);
|
||||||
|
|
||||||
|
var originalTransferSyntax = 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);
|
||||||
|
//log?.Invoke($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}");
|
||||||
|
|
||||||
|
|
||||||
|
// 1. 转为工作用的未压缩 DICOM
|
||||||
|
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
|
||||||
|
var workingDataset = workingFile.Dataset;
|
||||||
|
|
||||||
|
// 2. 修改像素
|
||||||
|
MaskPixelDataInPlace(workingDataset, regionList, options);
|
||||||
|
|
||||||
|
// 3. 保持原 PhotometricInterpretation
|
||||||
|
if (!string.IsNullOrWhiteSpace(originalPhotometric))
|
||||||
|
{
|
||||||
|
workingDataset.AddOrUpdate(DicomTag.PhotometricInterpretation, originalPhotometric);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 可选更新 BurnedInAnnotation
|
||||||
|
if (options.UpdateBurnedInAnnotationToNo)
|
||||||
|
{
|
||||||
|
workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 转回原始 TransferSyntax
|
||||||
|
var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
|
||||||
|
workingFile,
|
||||||
|
originalTransferSyntax,
|
||||||
|
options.StrictKeepTransferSyntax,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
finalFile.FileMetaInfo.TransferSyntax = originalTransferSyntax;
|
||||||
|
|
||||||
|
if (output.CanSeek)
|
||||||
|
output.SetLength(0);
|
||||||
|
|
||||||
|
await finalFile.SaveAsync(output).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (output.CanSeek)
|
||||||
|
output.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证dicom tag
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataset"></param>
|
||||||
|
/// <exception cref="NotSupportedException"></exception>
|
||||||
|
private static void ValidateDataset(DicomDataset dataset)
|
||||||
|
{
|
||||||
|
if (!dataset.Contains(DicomTag.PixelData))
|
||||||
|
throw new NotSupportedException("DICOM dataset does not contain Pixel Data.");
|
||||||
|
|
||||||
|
_ = dataset.GetSingleValue<int>(DicomTag.Rows);
|
||||||
|
_ = dataset.GetSingleValue<int>(DicomTag.Columns);
|
||||||
|
_ = dataset.GetSingleValue<int>(DicomTag.BitsAllocated);
|
||||||
|
_ = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转为工作用的未压缩 DICOM
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceFile"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
private static async Task<DicomFile> EnsureUncompressedAsync(DicomFile sourceFile, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var ts = sourceFile.FileMetaInfo.TransferSyntax;
|
||||||
|
|
||||||
|
if (!ts.IsEncapsulated)
|
||||||
|
{
|
||||||
|
return new DicomFile(sourceFile.Dataset.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var transcoder = new DicomTranscoder(ts, DicomTransferSyntax.ExplicitVRLittleEndian);
|
||||||
|
var transcoded = await Task.Run(() => transcoder.Transcode(sourceFile), cancellationToken).ConfigureAwait(false);
|
||||||
|
return transcoded;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to decode compressed DICOM from TransferSyntax {ts.UID.UID}. " +
|
||||||
|
"Please ensure fo-dicom codecs are available in the current Linux runtime.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转回原始 TransferSyntax
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uncompressedFile"></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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentTs = uncompressedFile.FileMetaInfo.TransferSyntax;
|
||||||
|
var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax);
|
||||||
|
return await Task.Run(() => transcoder.Transcode(uncompressedFile), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (strictKeepTransferSyntax)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to re-encode DICOM back to original compressed TransferSyntax {originalTransferSyntax.UID.UID}.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uncompressedFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改像素
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataset"></param>
|
||||||
|
/// <param name="regions"></param>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
private static void MaskPixelDataInPlace(
|
||||||
|
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 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);
|
||||||
|
|
||||||
|
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
|
||||||
|
{
|
||||||
|
var frame = pixelData.GetFrame(frameIndex);
|
||||||
|
var data = frame.Data.ToArray();
|
||||||
|
|
||||||
|
if (framesToProcess.Contains(frameIndex))
|
||||||
|
{
|
||||||
|
ApplyMaskToFrame(
|
||||||
|
data,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
bitsAllocated,
|
||||||
|
samplesPerPixel,
|
||||||
|
pixelRepresentation,
|
||||||
|
photometric,
|
||||||
|
planarConfiguration,
|
||||||
|
regions,
|
||||||
|
options);
|
||||||
|
}
|
||||||
|
|
||||||
|
newFrames.Add(new MemoryByteBuffer(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplacePixelDataFrames(dataset, pixelData, newFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (i < 0 || i >= frameCount)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {i} out of range [0, {frameCount - 1}]");
|
||||||
|
|
||||||
|
result.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
newPixelData.HighBit = sourcePixelData.HighBit;
|
||||||
|
newPixelData.SamplesPerPixel = sourcePixelData.SamplesPerPixel;
|
||||||
|
newPixelData.PixelRepresentation = sourcePixelData.PixelRepresentation;
|
||||||
|
newPixelData.PlanarConfiguration = sourcePixelData.PlanarConfiguration;
|
||||||
|
newPixelData.Height = sourcePixelData.Height;
|
||||||
|
newPixelData.Width = sourcePixelData.Width;
|
||||||
|
newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation;
|
||||||
|
|
||||||
|
foreach (var frame in frames)
|
||||||
|
{
|
||||||
|
newPixelData.AddFrame(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (samplesPerPixel == 1)
|
||||||
|
{
|
||||||
|
if (bitsAllocated == 8)
|
||||||
|
{
|
||||||
|
//8 位灰度
|
||||||
|
ApplyMask_Grayscale8(frameData, rows, cols, regions, (byte)Math.Min(options.GrayscaleMaskValue, byte.MaxValue));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 位灰度
|
||||||
|
if (bitsAllocated == 16)
|
||||||
|
{
|
||||||
|
ApplyMask_Grayscale16(frameData, rows, cols, regions, options.GrayscaleMaskValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotSupportedException($"Unsupported grayscale BitsAllocated={bitsAllocated}, Photometric={photometric}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//RGB 彩图
|
||||||
|
if (samplesPerPixel == 3)
|
||||||
|
{
|
||||||
|
if (bitsAllocated != 8)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Unsupported color image: SamplesPerPixel=3 but BitsAllocated={bitsAllocated}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 image format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, PixelRepresentation={pixelRepresentation}, Photometric={photometric}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMask_Grayscale8(
|
||||||
|
byte[] data,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions,
|
||||||
|
byte maskValue)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
for (int x = left; x < right; x++)
|
||||||
|
{
|
||||||
|
data[rowOffset + x] = maskValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMask_Grayscale16(
|
||||||
|
byte[] data,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions,
|
||||||
|
ushort maskValue)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
for (int x = left; x < right; x++)
|
||||||
|
{
|
||||||
|
int pixelIndex = rowOffset + 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,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions,
|
||||||
|
byte[] color)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyMask_Color8_Planar(
|
||||||
|
byte[] data,
|
||||||
|
int rows,
|
||||||
|
int cols,
|
||||||
|
IReadOnlyList<MaskRegion> regions,
|
||||||
|
byte[] color)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
for (int x = left; x < right; x++)
|
||||||
|
{
|
||||||
|
int idx = rowOffset + x;
|
||||||
|
data[idx] = c0;
|
||||||
|
data[planeSize + idx] = c1;
|
||||||
|
data[2 * planeSize + idx] = c2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
int right = Math.Min(imageWidth, region.X + region.Width);
|
||||||
|
int bottom = Math.Min(imageHeight, region.Y + region.Height);
|
||||||
|
return (left, top, right, bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static class DicomSortHelper
|
public static class DicomSortHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -15904,7 +15904,13 @@
|
||||||
<param name="outEnrollTime"></param>
|
<param name="outEnrollTime"></param>
|
||||||
<returns></returns>
|
<returns></returns>
|
||||||
</member>
|
</member>
|
||||||
<member name="M:IRaCIS.Core.Application.Service.TestService.SetTrialLifeCycel(IRaCIS.Core.Application.Service.TestService.ModelVerifyCommand)">
|
<member name="M:IRaCIS.Core.Application.Service.TestService.MaskImage">
|
||||||
|
<summary>
|
||||||
|
遮挡影像
|
||||||
|
</summary>
|
||||||
|
<returns></returns>
|
||||||
|
</member>
|
||||||
|
<member name="M:IRaCIS.Core.Application.Service.TestService.SetTrialLifeCycel">
|
||||||
<summary>
|
<summary>
|
||||||
设置生命周期
|
设置生命周期
|
||||||
</summary>
|
</summary>
|
||||||
|
|
@ -16146,6 +16152,69 @@
|
||||||
<param name="prefix"></param>
|
<param name="prefix"></param>
|
||||||
<returns></returns>
|
<returns></returns>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.FrameIndices">
|
||||||
|
<summary>
|
||||||
|
要处理的帧,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]。
|
||||||
|
对 RGB 表示黑色;对 YBR 则表示三个分量都写 0。
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.StrictKeepTransferSyntax">
|
||||||
|
<summary>
|
||||||
|
严格保持原始 TransferSyntax。
|
||||||
|
如果无法重新编码到原语法,则抛异常。
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.UpdateBurnedInAnnotationToNo">
|
||||||
|
<summary>
|
||||||
|
是否更新 BurnedInAnnotation 为 NO。
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.ValidateDataset(FellowOakDicom.DicomDataset)">
|
||||||
|
<summary>
|
||||||
|
验证dicom tag
|
||||||
|
</summary>
|
||||||
|
<param name="dataset"></param>
|
||||||
|
<exception cref="T:System.NotSupportedException"></exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.EnsureUncompressedAsync(FellowOakDicom.DicomFile,System.Threading.CancellationToken)">
|
||||||
|
<summary>
|
||||||
|
转为工作用的未压缩 DICOM
|
||||||
|
</summary>
|
||||||
|
<param name="sourceFile"></param>
|
||||||
|
<param name="cancellationToken"></param>
|
||||||
|
<returns></returns>
|
||||||
|
<exception cref="T:System.InvalidOperationException"></exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.ReEncodeToOriginalTransferSyntaxAsync(FellowOakDicom.DicomFile,FellowOakDicom.DicomTransferSyntax,System.Boolean,System.Threading.CancellationToken)">
|
||||||
|
<summary>
|
||||||
|
转回原始 TransferSyntax
|
||||||
|
</summary>
|
||||||
|
<param name="uncompressedFile"></param>
|
||||||
|
<param name="originalTransferSyntax"></param>
|
||||||
|
<param name="strictKeepTransferSyntax"></param>
|
||||||
|
<param name="cancellationToken"></param>
|
||||||
|
<returns></returns>
|
||||||
|
<exception cref="T:System.InvalidOperationException"></exception>
|
||||||
|
</member>
|
||||||
|
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.MaskPixelDataInPlace(FellowOakDicom.DicomDataset,System.Collections.Generic.IReadOnlyList{IRaCIS.Core.Application.Helper.MaskRegion},IRaCIS.Core.Application.Helper.DicomMaskOptions)">
|
||||||
|
<summary>
|
||||||
|
修改像素
|
||||||
|
</summary>
|
||||||
|
<param name="dataset"></param>
|
||||||
|
<param name="regions"></param>
|
||||||
|
<param name="options"></param>
|
||||||
|
</member>
|
||||||
<member name="M:IRaCIS.Core.Application.Helper.DicomSortHelper.SortSlices``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.String},System.Func{``0,System.String},System.Func{``0,System.Nullable{System.Int32}})">
|
<member name="M:IRaCIS.Core.Application.Helper.DicomSortHelper.SortSlices``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.String},System.Func{``0,System.String},System.Func{``0,System.Nullable{System.Int32}})">
|
||||||
<summary>
|
<summary>
|
||||||
DICOM Slice 排序(IPP + IOP)
|
DICOM Slice 排序(IPP + IOP)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,43 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 遮挡影像
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IResponseOutput> MaskImage()
|
||||||
|
{
|
||||||
|
|
||||||
|
var sourceDir = @"D:\images";
|
||||||
|
var targetDir = @"D:\images\after";
|
||||||
|
|
||||||
|
Directory.CreateDirectory(targetDir);
|
||||||
|
var regions = new[]
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
var outputPath = Path.Combine(targetDir, relativePath);
|
||||||
|
var outputFolder = Path.GetDirectoryName(outputPath);
|
||||||
|
if (!string.IsNullOrEmpty(outputFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(outputFolder);
|
||||||
|
}
|
||||||
|
await using var input = File.OpenRead(inputPath);
|
||||||
|
await using var output = File.Create(outputPath);
|
||||||
|
await DicomPixelMasker.MaskAsync(input, output, regions, options);
|
||||||
|
Console.WriteLine($"Done: {relativePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseOutput.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置生命周期
|
/// 设置生命周期
|
||||||
|
|
@ -128,7 +165,7 @@ namespace IRaCIS.Core.Application.Service
|
||||||
/// <param name="modelVerify"></param>
|
/// <param name="modelVerify"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IResponseOutput> SetTrialLifeCycel(ModelVerifyCommand modelVerify)
|
public async Task<IResponseOutput> SetTrialLifeCycel()
|
||||||
{
|
{
|
||||||
var trialIdList = _trialRepository.Where(t => t.TrialStatusStr == StaticData.TrialState.TrialCompleted).Select(t => t.Id).ToList();
|
var trialIdList = _trialRepository.Where(t => t.TrialStatusStr == StaticData.TrialState.TrialCompleted).Select(t => t.Id).ToList();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue