影像阅片排序换到后端做
continuous-integration/drone/push Build is passing Details

Test_IRC_Net8
hang 2026-03-26 15:21:46 +08:00
parent 3f9e506d09
commit 52f49994ab
4 changed files with 253 additions and 8 deletions

View File

@ -0,0 +1,193 @@

using FellowOakDicom.Imaging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace IRaCIS.Core.Application.Helper;
public static class DicomSliceSorterFast
{
public static List<T> Sort<T>(
IEnumerable<T> source,
Func<T, string?> ippSelector,
Func<T, string?> iopSelector,
Func<T, int?> instanceSelector)
{
var items = source.ToList();
if (items.Count < 2)
return items;
var refIPP = Parse(ippSelector(items[0]));
var refIOP = Parse(iopSelector(items[0]));
if (refIPP == null || refIOP == null || refIOP.Length != 6)
return items.OrderBy(instanceSelector).ToList();
// normal
var nx = refIOP[1] * refIOP[5] - refIOP[2] * refIOP[4];
var ny = refIOP[2] * refIOP[3] - refIOP[0] * refIOP[5];
var nz = refIOP[0] * refIOP[4] - refIOP[1] * refIOP[3];
var projections = new (T item, double dist)[items.Count];
double min = double.MaxValue;
double max = double.MinValue;
// ---------- projection pass ----------
for (int i = 0; i < items.Count; i++)
{
var ipp = Parse(ippSelector(items[i]));
if (ipp == null)
return items.OrderBy(instanceSelector).ToList();
var dx = refIPP[0] - ipp[0];
var dy = refIPP[1] - ipp[1];
var dz = refIPP[2] - ipp[2];
var dist = dx * nx + dy * ny + dz * nz;
projections[i] = (items[i], dist);
if (dist < min) min = dist;
if (dist > max) max = dist;
}
// ---------- estimate spacing ----------
var spacing = (max - min) / (items.Count - 1);
if (Math.Abs(spacing) < 1e-6)
return items.OrderBy(instanceSelector).ToList();
var result = new T[items.Count];
// ---------- O(n) placement ----------
foreach (var p in projections)
{
var index = (int)Math.Round((p.dist - min) / spacing);
index = Math.Clamp(index, 0, items.Count - 1);
// collision fallback极少发生
while (result[index] != null!)
index = Math.Min(index + 1, items.Count - 1);
result[index] = p.item;
}
return result.ToList();
}
private static double[]? Parse(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
var s = value.Split('\\');
var r = new double[s.Length];
for (int i = 0; i < s.Length; i++)
if (!double.TryParse(s[i], out r[i]))
return null;
return r;
}
}
public static class DicomSortHelper
{
/// <summary>
/// DICOM Slice 排序IPP + IOP
/// 自动 fallback InstanceNumber
/// </summary>
public static List<T> SortSlices<T>(
IEnumerable<T> source,
Func<T, string?> ippSelector,
Func<T, string?> iopSelector,
Func<T, int?> instanceNumberSelector)
{
var list = source.ToList();
if (list.Count < 2)
return list;
var first = list[0];
var reference = ParseVector(ippSelector(first));
var iop = ParseVector(iopSelector(first));
// ===== fallback 条件 =====
if (reference == null || iop == null || iop.Length != 6)
return list.OrderBy(instanceNumberSelector).ToList();
// row / column direction
var row = new[] { iop[0], iop[1], iop[2] };
var col = new[] { iop[3], iop[4], iop[5] };
// normal = row × col
var normal = new[]
{
row[1]*col[2] - row[2]*col[1],
row[2]*col[0] - row[0]*col[2],
row[0]*col[1] - row[1]*col[0]
};
// 如果法向量异常 → fallback
if (IsZeroVector(normal))
return list.OrderBy(instanceNumberSelector).ToList();
// ===== 计算距离 =====
var sorted = list
.Select(item =>
{
var ipp = ParseVector(ippSelector(item));
if (ipp == null)
return (item, distance: double.MinValue);
var vec0 = reference[0] - ipp[0];
var vec1 = reference[1] - ipp[1];
var vec2 = reference[2] - ipp[2];
var distance =
vec0 * normal[0] +
vec1 * normal[1] +
vec2 * normal[2];
return (item, distance);
})
.OrderByDescending(x => x.distance)
.Select(x => x.item)
.ToList();
return sorted;
}
// ---------------- helpers ----------------
private static double[]? ParseVector(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
var parts = value.Split('\\');
var result = new double[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!double.TryParse(parts[i], out result[i]))
return null;
}
return result;
}
private static bool IsZeroVector(double[] v)
{
const double eps = 1e-6;
return Math.Abs(v[0]) < eps &&
Math.Abs(v[1]) < eps &&
Math.Abs(v[2]) < eps;
}
}

View File

@ -12091,6 +12091,11 @@
影像标记
</summary>
</member>
<member name="P:IRaCIS.Core.Application.Service.Reading.Dto.ReadingQuestionTrialView.ImageMarkTypeEnum">
<summary>
影像标记类型
</summary>
</member>
<member name="P:IRaCIS.Core.Application.Service.Reading.Dto.ReadingQuestionTrialView.ImageTool">
<summary>
影像工具
@ -16054,6 +16059,12 @@
<param name="prefix"></param>
<returns></returns>
</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}})">
<summary>
DICOM Slice 排序IPP + IOP
自动 fallback InstanceNumber
</summary>
</member>
<member name="P:IRaCIS.Core.Application.Helper.HolidayHelper._client">
<summary>
github 链接https://github.com/lanceliao/china-holiday-calender?tab=readme-ov-file 
@ -17827,6 +17838,16 @@
任务类型
</summary>
</member>
<member name="P:IRaCIS.Core.Application.ViewModel.SegmentBindingView.SegmentationName">
<summary>
分割分组名称
</summary>
</member>
<member name="P:IRaCIS.Core.Application.ViewModel.SegmentBindingView.SegmentName">
<summary>
SegmentName
</summary>
</member>
<member name="P:IRaCIS.Core.Application.ViewModel.SegmentAddOrEdit.SegmentJson">
<summary>
分割的Json

View File

@ -108,6 +108,10 @@ namespace IRaCIS.Core.Application.Contracts.Dicom.DTO
public string WindowCenter { get; set; }
[JsonIgnore]
public string WindowWidth { get; set; }
[JsonIgnore]
public string ImagePositionPatient { get; set; }
[JsonIgnore]
public string ImageOrientationPatient { get; set; }
public DateTime? RowDate { get; set; }
}

View File

@ -2,6 +2,7 @@
using IRaCIS.Core.Application.Contracts;
using IRaCIS.Core.Application.Contracts.Dicom.DTO;
using IRaCIS.Core.Application.Filter;
using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.Interfaces;
using IRaCIS.Core.Application.Service.Reading.Dto;
using IRaCIS.Core.Domain.Models;
@ -588,7 +589,7 @@ namespace IRaCIS.Core.Application.Services
if (studyIds.Count > 0)
{
var instanceList = await _dicomInstanceRepository.Where(t => studyIds.Contains(t.StudyId) && t.IsReading)
.Select(t => new { t.SeriesId, t.Id, t.InstanceNumber, t.Path, t.NumberOfFrames, t.WindowCenter, t.WindowWidth, t.HtmlPath, t.IsReading, t.FileSize }).ToListAsync();
.Select(t => new { t.SeriesId, t.Id, t.InstanceNumber, t.Path, t.NumberOfFrames, t.WindowCenter, t.WindowWidth, t.HtmlPath, t.IsReading, t.FileSize, t.ImagePositionPatient, t.ImageOrientationPatient }).ToListAsync();
List<DicomSeriesDTO> seriesLists = await _dicomSeriesRepository.Where(s => studyIds.Contains(s.StudyId))
.WhereIf(isManualGenerate == false, t => t.IsReading)
@ -597,11 +598,23 @@ namespace IRaCIS.Core.Application.Services
foreach (var t in dicomStudyList)
{
t.SeriesList = seriesLists.Where(s => s.StudyId == t.StudyId).OrderBy(s => s.SeriesNumber).ThenBy(s => s.SeriesTime).ToList();
t.SeriesList.ForEach(series =>
{
series.InstanceInfoList = instanceList.Where(t => t.SeriesId == series.Id).OrderBy(t => t.InstanceNumber).Select(k =>
var instances = instanceList.Where(x => x.SeriesId == series.Id);
// ⭐ DICOM 空间排序(带兜底)
var sorted = DicomSortHelper.SortSlices(
instances,
x => x.ImagePositionPatient,
x => x.ImageOrientationPatient,
x => x.InstanceNumber
);
series.InstanceInfoList = sorted.Select(k =>
new InstanceBasicInfo()
{
Id = k.Id,
@ -642,6 +655,20 @@ namespace IRaCIS.Core.Application.Services
{
study.SeriesList = study.SeriesList.OrderBy(s => s.SeriesNumber).ThenBy(s => s.SeriesTime).ToList();
study.SeriesList.ForEach(series =>
{
// ⭐ DICOM 空间排序(带兜底)
var sorted = DicomSortHelper.SortSlices(
series.InstanceInfoList,
x => x.ImagePositionPatient,
x => x.ImageOrientationPatient,
x => x.InstanceNumber
);
series.InstanceInfoList = sorted;
});
study.InstanceCount = study.SeriesList.SelectMany(t => t.InstanceInfoList).Count();
}