影像阅片排序换到后端做
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
3f9e506d09
commit
52f49994ab
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -362,7 +363,7 @@ namespace IRaCIS.Core.Application.Services
|
|||
}
|
||||
else
|
||||
{
|
||||
series = await _taskSeriesRepository .Where(s => s.Id == inDto.SeriesId).ProjectTo<DicomSeriesDTO>(_mapper.ConfigurationProvider).FirstNotNullAsync();
|
||||
series = await _taskSeriesRepository.Where(s => s.Id == inDto.SeriesId).ProjectTo<DicomSeriesDTO>(_mapper.ConfigurationProvider).FirstNotNullAsync();
|
||||
|
||||
var instanceList = await _taskInstanceRepository.Where(t => t.SeriesId == inDto.SeriesId)
|
||||
.Select(t => new { t.SeriesId, t.StudyId, t.Id, t.InstanceNumber, t.Path, t.NumberOfFrames, t.WindowCenter, t.WindowWidth, t.HtmlPath, t.SliceLocation, t.FileSize }).ToListAsync();
|
||||
|
|
@ -556,7 +557,7 @@ namespace IRaCIS.Core.Application.Services
|
|||
.WhereIf(isManualGenerate, t => t.SubjectCriteriaEvaluationVisitStudyFilterList.Any(t => t.TrialReadingCriterionId == taskInfo.TrialReadingCriterionId && t.IsConfirmed && t.IsReading))
|
||||
//影像后处理 上传了新的影像 还要原始dsa
|
||||
.WhereIf(taskInfo.IsHaveTaskStudy && taskInfo.CriterionType == CriterionType.OCT,
|
||||
t => t.ModalityForEdit == "XA" || t.ModalityForEdit == "DSA" )
|
||||
t => t.ModalityForEdit == "XA" || t.ModalityForEdit == "DSA")
|
||||
|
||||
.WhereIf(taskInfo.CriterionType == CriterionType.IVUS,
|
||||
t => t.ModalityForEdit == "XA" || t.ModalityForEdit == "DSA" || t.ModalityForEdit == "IVUS")
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -823,7 +850,7 @@ namespace IRaCIS.Core.Application.Services
|
|||
|
||||
var instanceCount = await _noneDicomStudyFileRepository.Where(t => t.IsReading)
|
||||
.WhereIf(taskInfo.IsHaveTaskNoneDicomStudyFile == false && taskInfo.IsMarkNoneDicomStudy == true, x => x.ImageLabelNoneDicomStudyId == item.StudyId)
|
||||
.WhereIf(taskInfo.IsHaveTaskNoneDicomStudyFile && taskInfo.IsMarkNoneDicomStudy, x => x.OriginNoneDicomStudyId == item.StudyId && x.VisitTaskId==indto.VisitTaskId)
|
||||
.WhereIf(taskInfo.IsHaveTaskNoneDicomStudyFile && taskInfo.IsMarkNoneDicomStudy, x => x.OriginNoneDicomStudyId == item.StudyId && x.VisitTaskId == indto.VisitTaskId)
|
||||
.WhereIf(taskInfo.IsHaveTaskNoneDicomStudyFile == false && taskInfo.IsMarkNoneDicomStudy == false, x => x.NoneDicomStudyId == item.StudyId)
|
||||
.Where(t => !t.FileType.Contains(StaticData.FileType.Zip))
|
||||
.CountAsync();
|
||||
|
|
|
|||
Loading…
Reference in New Issue