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-15 11:51:12 +08:00
commit ebf6c225f3
4 changed files with 498 additions and 57 deletions

View File

@ -204,6 +204,8 @@ public interface IOSSService
public Task<long> GetObjectSizeAsync(string sourcePath);
public Task SyncFileAsync(string objectKey, ObjectStoreUse source, ObjectStoreUse destination, CancellationToken ct = default);
public void ConvertPrefixToStandard(string prefix);
}
@ -215,6 +217,7 @@ public class OSSService(IOptionsMonitor<ObjectStoreServiceOptions> options,
private AliyunOSSTempToken AliyunOSSTempToken { get; set; }
private AWSTempToken AWSTempToken { get; set; }
public object result { get; private set; }
@ -652,6 +655,90 @@ public class OSSService(IOptionsMonitor<ObjectStoreServiceOptions> options,
}
/// <summary>
/// 将某个路径下的归档的文件 转为标准存储
/// </summary>
/// <param name="prefix"></param>
public void ConvertPrefixToStandard(string prefix)
{
BackBatchGetToken();
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
{
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
var _ossClient = new OssClient(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
AliyunOSSTempToken.AccessKeyId,
AliyunOSSTempToken.AccessKeySecret,
AliyunOSSTempToken.SecurityToken
);
var bucketName = aliConfig.BucketName;
try
{
ObjectListing objectListing = null;
string nextMarker = null;
do
{
// 使用 prefix 模拟目录结构,设置 MaxKeys 和 NextMarker
objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(bucketName)
{
Prefix = prefix,
MaxKeys = 1000,
Marker = nextMarker
});
foreach (var obj in objectListing.ObjectSummaries)
{
try
{
// 👇 跳过已经是标准存储的(优化)
if (obj.StorageClass == StorageClass.Standard.ToString())
continue;
var metadata = new ObjectMetadata();
// 👇 关键:手动加 Header
metadata.AddHeader("x-oss-storage-class", "Standard");
var copyRequest = new Aliyun.OSS.CopyObjectRequest(bucketName, obj.Key, bucketName, obj.Key) { NewObjectMetadata = metadata };
_ossClient.CopyObject(copyRequest);
}
catch (Exception ex)
{
Log.Logger.Error($"❌ 失败: {obj.Key}, 错误: {ex.Message}");
}
}
// 设置 NextMarker 以获取下一页的数据
nextMarker = objectListing.NextMarker;
} while (objectListing.IsTruncated);
}
catch (Exception ex)
{
Log.Logger.Error($"Error: {ex.Message}");
}
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
/// <summary>
/// 坑方法,会清空之前的规则
/// </summary>
@ -1507,20 +1594,20 @@ public class OSSService(IOptionsMonitor<ObjectStoreServiceOptions> options,
GetObjectStoreTempToken(objectUse: config.Primary);
await DeleteFromPrefixInternal(config.Primary,prefix, isCache);
await DeleteFromPrefixInternal(config.Primary, prefix, isCache);
}
}
else
{
GetObjectStoreTempToken();
await DeleteFromPrefixInternal(ObjectStoreServiceOptions.ObjectStoreUse,prefix, isCache);
await DeleteFromPrefixInternal(ObjectStoreServiceOptions.ObjectStoreUse, prefix, isCache);
}
}
private async Task DeleteFromPrefixInternal(string objectUse ,string prefix, bool isCache = false)
private async Task DeleteFromPrefixInternal(string objectUse, string prefix, bool isCache = false)
{
if (objectUse == "AliyunOSS")
{

View File

@ -6,6 +6,7 @@ using FellowOakDicom.IO.Buffer;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using System.Data;
namespace IRaCIS.Core.Application.Helper;
@ -158,8 +159,16 @@ public sealed class DicomMaskOptions
/// <summary>
/// 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
/// 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
/// 灰度图像的遮挡像素值,默认 0。
/// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
/// </summary>
public int? GrayscaleMaskValue { get; init; }
/// <summary>
/// PALETTE COLOR 图像使用的遮盖索引。
/// null 表示自动从调色板中选择最暗的一个索引。
/// </summary>
public int? PaletteColorMaskIndex { get; init; }
}
@ -213,8 +222,10 @@ public static class DicomPixelMasker
var planarConfiguration = originalDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
Console.WriteLine($"Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}");
EnsureSupportedPhotometric(originalPhotometric, samplesPerPixel);
var isSupport = IsSupportedPhotometric(originalPhotometric, samplesPerPixel);
if (isSupport)
{
// 1. 转为工作用的未压缩 DICOM
var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
// 2. 修改像素
@ -245,6 +256,12 @@ public static class DicomPixelMasker
if (output.CanSeek)
output.Position = 0;
}
else
{
}
}
@ -264,23 +281,24 @@ public static class DicomPixelMasker
_ = dataset.GetSingleValue<int>(DicomTag.SamplesPerPixel);
}
private static void EnsureSupportedPhotometric(string photometric, int samplesPerPixel, Action<string>? log = null)
private static bool IsSupportedPhotometric(string photometric, int samplesPerPixel)
{
if (samplesPerPixel == 1)
{
if (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))
{
return;
return true;
}
throw new NotSupportedException($"Unsupported grayscale PhotometricInterpretation: {photometric}");
return false;
}
if (samplesPerPixel == 3)
{
if (string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase) ||
string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase))
{
return;
return true;
}
// 对 YBR_FULL_422 不建议直接按当前布局改
if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase) ||
@ -290,9 +308,12 @@ public static class DicomPixelMasker
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}");
return false;
//throw new NotSupportedException($"Unsupported color PhotometricInterpretation: {photometric}");
}
throw new NotSupportedException($"Unsupported SamplesPerPixel={samplesPerPixel}");
return false;
//throw new NotSupportedException($"Unsupported SamplesPerPixel={samplesPerPixel}");
}
/// <summary>
@ -402,7 +423,7 @@ public static class DicomPixelMasker
photometric: photometric,
planarConfiguration: planarConfiguration,
regions: regions,
options: options);
options: options, dataset);
}
replacementFrames.Add(new MemoryByteBuffer(bytes));
}
@ -455,9 +476,34 @@ public static class DicomPixelMasker
string photometric,
ushort planarConfiguration,
IReadOnlyList<MaskRegion> regions,
DicomMaskOptions options)
DicomMaskOptions options,
DicomDataset dataset)
{
if (samplesPerPixel == 1)
{
if (string.Equals(photometric, "PALETTE COLOR", StringComparison.OrdinalIgnoreCase))
{
if (pixelRepresentation != 0)
{
throw new NotSupportedException("PALETTE COLOR with signed pixel representation is not supported.");
}
if (bitsAllocated == 8)
{
byte maskIndex = (byte)ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
ApplyMask_Grayscale8(frameData, rows, cols, regions, maskIndex);
return;
}
if (bitsAllocated == 16)
{
ushort maskIndex = ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
ApplyMask_Grayscale16(frameData, rows, cols, regions, maskIndex);
return;
}
throw new NotSupportedException(
$"Unsupported PALETTE COLOR image: BitsAllocated={bitsAllocated}");
}
else
{
if (bitsAllocated == 8)
{
@ -478,6 +524,9 @@ public static class DicomPixelMasker
throw new NotSupportedException(
$"Unsupported grayscale image: BitsAllocated={bitsAllocated}, Photometric={photometric}");
}
}
if (samplesPerPixel == 3)
{
if (bitsAllocated != 8)
@ -501,6 +550,276 @@ public static class DicomPixelMasker
throw new NotSupportedException(
$"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}");
}
private static ushort ResolvePaletteColorMaskIndex(
DicomDataset dataset,
int bitsStored,
DicomMaskOptions options)
{
int pixelMin = 0;
int pixelMax = bitsStored == 16 ? ushort.MaxValue : ((1 << bitsStored) - 1);
if (options.PaletteColorMaskIndex.HasValue)
{
int manual = options.PaletteColorMaskIndex.Value;
if (manual < pixelMin || manual > pixelMax)
{
throw new ArgumentOutOfRangeException(
nameof(options.PaletteColorMaskIndex),
$"PaletteColorMaskIndex must be in range [{pixelMin}, {pixelMax}] for BitsStored={bitsStored}.");
}
return (ushort)manual;
}
// 2. 尝试从 Descriptor 读取 FirstMappedPixelValue
if (TryGetPaletteFirstMappedPixelValue(dataset, out int firstMapped))
{
if (firstMapped < pixelMin || firstMapped > pixelMax)
{
throw new InvalidDataException(
$"Palette LUT FirstMappedPixelValue={firstMapped} is out of pixel range [{pixelMin}, {pixelMax}].");
}
return (ushort)firstMapped;
}
// 3. 实在没有就退化为 0可改成直接抛异常
if (0 >= pixelMin && 0 <= pixelMax)
{
return 0;
}
throw new InvalidDataException(
"Cannot resolve palette color mask index because palette LUT descriptor is missing.");
//int autoIndex = FindDarkestPaletteIndex(dataset, maxValue);
//return (byte)autoIndex;
}
private static bool TryGetPaletteFirstMappedPixelValue(DicomDataset dataset, out int firstMappedPixelValue)
{
firstMappedPixelValue = 0;
if (TryGetPaletteDescriptor(dataset, DicomTag.RedPaletteColorLookupTableDescriptor, out var redDesc))
{
firstMappedPixelValue = redDesc.FirstMappedPixelValue;
return true;
}
if (TryGetPaletteDescriptor(dataset, DicomTag.GreenPaletteColorLookupTableDescriptor, out var greenDesc))
{
firstMappedPixelValue = greenDesc.FirstMappedPixelValue;
return true;
}
if (TryGetPaletteDescriptor(dataset, DicomTag.BluePaletteColorLookupTableDescriptor, out var blueDesc))
{
firstMappedPixelValue = blueDesc.FirstMappedPixelValue;
return true;
}
return false;
}
private sealed class PaletteLutDescriptor
{
public int NumberOfEntries { get; init; }
public int FirstMappedPixelValue { get; init; }
public int BitsPerEntry { get; init; }
}
private static bool TryGetPaletteDescriptor(
DicomDataset dataset,
DicomTag tag,
out PaletteLutDescriptor descriptor)
{
descriptor = null!;
if (!dataset.Contains(tag))
return false;
// LUT Descriptor 可能是 US 或 SSfo-dicom 通常可直接按 short/ushort 读
if (dataset.TryGetValues<short>(tag, out short[]? ssValues) && ssValues != null && ssValues.Length >= 3)
{
descriptor = new PaletteLutDescriptor
{
NumberOfEntries = ssValues[0] == 0 ? 65536 : ssValues[0],
FirstMappedPixelValue = ssValues[1],
BitsPerEntry = ssValues[2]
};
return true;
}
if (dataset.TryGetValues<ushort>(tag, out ushort[]? usValues) && usValues != null && usValues.Length >= 3)
{
descriptor = new PaletteLutDescriptor
{
NumberOfEntries = usValues[0] == 0 ? 65536 : usValues[0],
FirstMappedPixelValue = usValues[1],
BitsPerEntry = usValues[2]
};
return true;
}
return false;
}
#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)
{
if (!options.AutoSelectGrayscaleMaskValue)

View File

@ -15927,6 +15927,12 @@
<param name="modelVerify"></param>
<returns></returns>
</member>
<member name="M:IRaCIS.Core.Application.Service.TestService.ConvertPrefixToStandard">
<summary>
归档直读
</summary>
<returns></returns>
</member>
<member name="M:IRaCIS.Core.Application.Service.TestService.TestTrialEfficacyEvaluationStat(System.Collections.Generic.List{IRaCIS.Core.Application.Service.TestService.TestEfficacyEvaluation})">
<summary>
测试疗效评估
@ -16110,6 +16116,12 @@
<param name="isDelete">默认是添加/更新 </param>
</member>
<!-- Badly formed XML comment ignored for member "M:IRaCIS.Core.Application.Helper.OSSService.RestoreFilesByPrefixAsync(System.String,System.Int32,System.Int32)" -->
<member name="M:IRaCIS.Core.Application.Helper.OSSService.ConvertPrefixToStandard(System.String)">
<summary>
将某个路径下的归档的文件 转为标准存储
</summary>
<param name="prefix"></param>
</member>
<member name="M:IRaCIS.Core.Application.Helper.OSSService.SetLifecycle(System.String,System.String)">
<summary>
坑方法,会清空之前的规则
@ -16194,6 +16206,14 @@
<summary>
当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
灰度图像的遮挡像素值,默认 0。
注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
</summary>
</member>
<member name="P:IRaCIS.Core.Application.Helper.DicomMaskOptions.PaletteColorMaskIndex">
<summary>
PALETTE COLOR 图像使用的遮盖索引。
null 表示自动从调色板中选择最暗的一个索引。
</summary>
</member>
<member name="M:IRaCIS.Core.Application.Helper.DicomPixelMasker.ValidateDataset(FellowOakDicom.DicomDataset)">

View File

@ -152,17 +152,20 @@ namespace IRaCIS.Core.Application.Service
try
{
Console.WriteLine($"开始处理: {relativePath}");
await DicomPixelMasker.MaskAsync(input, output, regions);
}
catch(Exception ex)
catch (Exception ex)
{
// 跳过该文件
Console.WriteLine($"error: {ex.Message}");
Console.WriteLine();
Console.WriteLine($"error: {relativePath} —————— {ex.Message}");
Console.WriteLine();
}
Console.WriteLine($"Done: {relativePath}");
Console.WriteLine($"处理结束: {relativePath}");
}
return ResponseOutput.Ok();
@ -195,6 +198,18 @@ namespace IRaCIS.Core.Application.Service
return ResponseOutput.Ok();
}
/// <summary>
/// 归档直读
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public async Task<IResponseOutput> ConvertPrefixToStandard()
{
_IOSSService.ConvertPrefixToStandard("601a0000-3e2c-0016-56de-08dae983124b/Image");
return ResponseOutput.Ok();
}
[AllowAnonymous]