using FellowOakDicom.Network; using FellowOakDicom; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using IRaCIS.Core.SCP.Service; using IRaCIS.Core.Domain.Models; using IRaCIS.Core.Infra.EFCore; using Medallion.Threading; using IRaCIS.Core.Domain.Share; using Serilog; using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; using Microsoft.Extensions.Options; using System.Data; using FellowOakDicom.Imaging; using SharpCompress.Common; using SixLabors.ImageSharp.Formats.Jpeg; using IRaCIS.Core.Infrastructure; using IRaCIS.Core.Infrastructure.Extention; using FellowOakDicom.IO.Buffer; namespace IRaCIS.Core.SCP.Service { public class DicomSCPServiceOption { public List CalledAEList { get; set; } public string ServerPort { get; set; } } public class CStoreSCPService : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider { private IServiceProvider _serviceProvider { get; set; } private List _SCPStudyIdList => _ImageUploadList.Where(t => t.SCPStudyId != Guid.Empty).Select(t => t.SCPStudyId).ToList(); private List _ImageUploadList { get; set; } = new List(); private SCPImageUpload _upload { get; set; } private Guid _trialId { get; set; } private Guid _trialSiteId { get; set; } private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[] { DicomTransferSyntax.ExplicitVRLittleEndian, DicomTransferSyntax.ExplicitVRBigEndian, DicomTransferSyntax.ImplicitVRLittleEndian }; private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[] { // Lossless DicomTransferSyntax.JPEGLSLossless, //1.2.840.10008.1.2.4.80 DicomTransferSyntax.JPEG2000Lossless, //1.2.840.10008.1.2.4.90 DicomTransferSyntax.JPEGProcess14SV1, //1.2.840.10008.1.2.4.70 DicomTransferSyntax.JPEGProcess14, //1.2.840.10008.1.2.4.57 JPEG Lossless, Non-Hierarchical (Process 14) DicomTransferSyntax.RLELossless, //1.2.840.10008.1.2.5 // Lossy DicomTransferSyntax.JPEGLSNearLossless,//1.2.840.10008.1.2.4.81" DicomTransferSyntax.JPEG2000Lossy, //1.2.840.10008.1.2.4.91 DicomTransferSyntax.JPEGProcess1, //1.2.840.10008.1.2.4.50 DicomTransferSyntax.JPEGProcess2_4, //1.2.840.10008.1.2.4.51 // Uncompressed DicomTransferSyntax.ExplicitVRLittleEndian, //1.2.840.10008.1.2.1 DicomTransferSyntax.ExplicitVRBigEndian, //1.2.840.10008.1.2.2 DicomTransferSyntax.ImplicitVRLittleEndian //1.2.840.10008.1.2 }; public CStoreSCPService(INetworkStream stream, Encoding fallbackEncoding, Microsoft.Extensions.Logging.ILogger log, DicomServiceDependencies dependencies, IServiceProvider injectServiceProvider) : base(stream, fallbackEncoding, log, dependencies) { _serviceProvider = injectServiceProvider.CreateScope().ServiceProvider; } public Task OnReceiveAssociationRequestAsync(DicomAssociation association) { _upload = new SCPImageUpload() { StartTime = DateTime.Now, CallingAE = association.CallingAE, CalledAE = association.CalledAE, CallingAEIP = association.RemoteHost }; Log.Logger.Warning($"接收到来自{association.CallingAE}的连接"); //_serviceProvider = (IServiceProvider)this.UserState; var _trialDicomAERepository = _serviceProvider.GetService>(); var trialDicomAEList = _trialDicomAERepository.Select(t => new { t.CalledAE, t.TrialId }).ToList(); var trialCalledAEList = trialDicomAEList.Select(t => t.CalledAE).ToList(); Log.Logger.Information("当前系统配置:", string.Join('|', trialDicomAEList)); var findCalledAE = trialDicomAEList.Where(t => t.CalledAE == association.CalledAE).FirstOrDefault(); var isCanReceiveIamge = false; if (findCalledAE != null) { _trialId = findCalledAE.TrialId; var _trialSiteDicomAERepository = _serviceProvider.GetService>(); var findTrialSiteAE = _trialSiteDicomAERepository.Where(t => t.CallingAE == association.CallingAE && t.TrialId==_trialId).FirstOrDefault(); if (findTrialSiteAE != null) { _trialSiteId = findTrialSiteAE.TrialSiteId; isCanReceiveIamge = true; } } if (association.CallingAE == "test-callingAE") { isCanReceiveIamge = true; } if (!trialCalledAEList.Contains(association.CalledAE) || isCanReceiveIamge == false) { Log.Logger.Warning($"拒绝CallingAE:{association.CallingAE} CalledAE:{association.CalledAE}的连接"); return SendAssociationRejectAsync( DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized); } foreach (var pc in association.PresentationContexts) { if (pc.AbstractSyntax == DicomUID.Verification) { pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes); } else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None) { pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes); } } return SendAssociationAcceptAsync(association); } public async Task OnReceiveAssociationReleaseRequestAsync() { var _distributedLockProvider = _serviceProvider.GetService(); var @lock = _distributedLockProvider.CreateLock($"{_upload.CallingAE}"); using (await @lock.AcquireAsync()) { await DataMaintenanceAsaync(); //记录监控 await AddUploadLogAsync(); var _studyRepository = _serviceProvider.GetService>(); //将检查设置为传输结束 await _studyRepository.BatchUpdateNoTrackingAsync(t => _SCPStudyIdList.Contains(t.Id), u => new SCPStudy() { IsUploadFinished = true }); await _studyRepository.SaveChangesAndClearAllTrackingAsync(); } await SendAssociationReleaseResponseAsync(); } private async Task AddUploadLogAsync() { //记录监控 var _SCPImageUploadRepository = _serviceProvider.GetService>(); _upload.EndTime = DateTime.Now; _upload.StudyCount = _ImageUploadList.Count; _upload.TrialId = _trialId; _upload.TrialSiteId = _trialSiteId; _upload.UploadJsonStr = (new SCPImageLog() { UploadList = _ImageUploadList }).ToJsonStr(); //可能是测试echo 导致记录了 await _SCPImageUploadRepository.AddAsync(_upload, _upload.FileCount > 0 ? true : false); } private async Task DataMaintenanceAsaync() { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}传输结束:开始维护数据,处理检查Modality"); //处理检查Modality var _dictionaryRepository = _serviceProvider.GetService>(); var _seriesRepository = _serviceProvider.GetService>(); var _studyRepository = _serviceProvider.GetService>(); var dicModalityList = _dictionaryRepository.Where(t => t.Code == "Modality").SelectMany(t => t.ChildList.Select(c => c.Value)).ToList(); var seriesModalityList = _seriesRepository.Where(t => _SCPStudyIdList.Contains(t.StudyId)).Select(t => new { SCPStudyId = t.StudyId, t.Modality }).ToList(); foreach (var g in seriesModalityList.GroupBy(t => t.SCPStudyId)) { var modality = string.Join('、', g.Select(t => t.Modality).Distinct().ToList()); //特殊逻辑 var modalityForEdit = dicModalityList.Contains(modality) ? modality : String.Empty; if (modality == "MR") { modalityForEdit = "MRI"; } if (modality == "PT") { modalityForEdit = "PET"; } if (modality == "PT、CT" || modality == "CT、PT") { modalityForEdit = "PET-CT"; } await _studyRepository.BatchUpdateNoTrackingAsync(t => t.Id == g.Key, u => new SCPStudy() { Modalities = modality, ModalityForEdit = modalityForEdit }); } Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}维护数据结束"); } public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}接收中断,中断原因:{source.ToString() + reason.ToString()}"); /* nothing to do here */ } public async void OnConnectionClosed(Exception exception) { /* nothing to do here */ //奇怪的bug 上传的时候,用王捷修改的影像,会关闭,重新连接,导致检查id 丢失,然后状态不一致 if (exception == null) { //var _studyRepository = _serviceProvider.GetService>(); ////将检查设置为传输结束 //await _studyRepository.BatchUpdateNoTrackingAsync(t => _SCPStudyIdList.Contains(t.Id), u => new SCPStudy() { IsUploadFinished = true }); //await _studyRepository.SaveChangesAndClearAllTrackingAsync(); } else { //记录日志 await AddUploadLogAsync(); } Log.Logger.Warning($"连接关闭 {exception?.Message} {exception?.InnerException?.Message}"); } public async Task OnCStoreRequestAsync(DicomCStoreRequest request) { string studyInstanceUid = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); string seriesInstanceUid = request.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty); string sopInstanceUid = request.Dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty); string patientIdStr = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); if (studyInstanceUid.IsNullOrEmpty() || seriesInstanceUid.IsNullOrEmpty() || sopInstanceUid.IsNullOrEmpty()) { Log.Logger.Error($"接收数据读取StudyInstanceUID:{studyInstanceUid}、SeriesInstanceUID:{seriesInstanceUid}、SOPInstanceUID:{sopInstanceUid}有空 "); return new DicomCStoreResponse(request, DicomStatus.Success); } //确保来了影像集合存在 if (!_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid)) { _ImageUploadList.Add(new ImageUploadInfo() { StudyInstanceUid = studyInstanceUid }); } //Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString()); Guid seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, _trialId.ToString()); Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid, _trialId.ToString()); var ossService = _serviceProvider.GetService(); var dicomArchiveService = _serviceProvider.GetService(); var _seriesRepository = _serviceProvider.GetService>(); var _distributedLockProvider = _serviceProvider.GetService(); var storeRelativePath = string.Empty; var ossFolderPath = $"{_trialId}/Image/PACS/{_trialSiteId}/{studyInstanceUid}"; long fileSize = 0; try { using (MemoryStream ms = new MemoryStream()) { await request.File.SaveAsync(ms); #region 1帧拆成多个固定大小的,方便移动端浏览 // 回到开头,读取 dicom ms.Position = 0; var dicomFile = DicomFile.Open(ms); var numberOfFrames = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1); //多帧处理逻辑 if (numberOfFrames > 1) { //一定要有像素数据才处理 var pixelData = DicomPixelData.Create(dicomFile.Dataset); if (pixelData != null) { try { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 开始处理多帧instanceId:{instanceId}"); var syntax = pixelData.Syntax; // 每个 fragment 固定大小 (64KB 示例,可以自己调整) int fragmentSize = 20 * 1024; var frag = dicomFile.Dataset.GetDicomItem(DicomTag.PixelData); int fragmentCount = frag?.Fragments?.Count() ?? 0; var originOffsetTable = frag?.OffsetTable; //有可能没有表,需要自己重建 var bot = new List(); uint botOffset = 0; //需要拆成固定片段的 if (syntax.IsEncapsulated && fragmentCount == pixelData.NumberOfFrames && numberOfFrames > 1) { var newFragments = new DicomOtherByteFragment(DicomTag.PixelData); #region test //var newDicomFile = dicomFile.Clone(); //var newDataset = newDicomFile.Dataset; //var dstPd = DicomPixelData.Create(newDataset, true); //for (int i = 0; i < pixelData.NumberOfFrames; i++) //{ // var frame = pixelData.GetFrame(i); // dstPd.AddFrame(frame); // var data = frame.Data; // int offset = 0; // while (offset < data.Length) // { // int size = Math.Min(fragmentSize, data.Length - offset); // var buffer = new byte[size]; // Buffer.BlockCopy(data, offset, buffer, 0, size); // newFragments.Fragments.Add(new MemoryByteBuffer(buffer)); // offset += size; // } //} //var newOffsetTable = newDataset.GetDicomItem(DicomTag.PixelData).OffsetTable; //newFragments.OffsetTable.AddRange(newOffsetTable.ToArray()); #endregion #region test fo-dicom auto bot //var newDicomFile = dicomFile.Clone(); //var newDataset = newDicomFile.Dataset; //var dstPd = DicomPixelData.Create(newDataset, true); //for (int i = 0; i < pixelData.NumberOfFrames; i++) //{ // var frame = pixelData.GetFrame(i); // dstPd.AddFrame(frame); //} //var newOffsetTable = newDataset.GetDicomItem(DicomTag.PixelData).OffsetTable; //Console.WriteLine(newOffsetTable.ToJsonStr()); #endregion #region 最终使用 for (int n = 0; n < pixelData.NumberOfFrames; n++) { var frameData = pixelData.GetFrame(n); // 获取完整一帧 var data = frameData.Data; int offset = 0; bot.Add(botOffset); botOffset += (uint)data.Length; while (offset < data.Length) { botOffset += 8; int size = Math.Min(fragmentSize, data.Length - offset); var buffer = new byte[size]; Buffer.BlockCopy(data, offset, buffer, 0, size); newFragments.Fragments.Add(new MemoryByteBuffer(buffer)); offset += size; } } //保留原始偏移表 if (originOffsetTable.Count == pixelData.NumberOfFrames) { newFragments.OffsetTable.AddRange(originOffsetTable.ToArray()); } else { newFragments.OffsetTable.AddRange(bot.ToArray()); //Console.WriteLine(bot.ToJsonStr()); } #endregion dicomFile.Dataset.AddOrUpdate(newFragments); // 重新保存 dicom 到流 ms.SetLength(0); dicomFile.Save(ms); } //传递过来的就是拆分的,但是是没有偏移表的,我需要自己创建偏移表,不然生成缩略图失败 else if (syntax.IsEncapsulated && fragmentCount > pixelData.NumberOfFrames && originOffsetTable.Count == 0) { uint offset = 0; bot.Add(offset); var fistSize = frag.Fragments.FirstOrDefault()?.Size ?? 0; //和上一个大小不一样 var isDiffrentBefore = false; uint count = 0; // 假设你知道每帧对应的 fragment 数量 foreach (var frameFragments in frag.Fragments) { count++; if (frameFragments.Size == fistSize) { isDiffrentBefore = false; // 累加这一帧所有 fragment 的大小 offset += (uint)frameFragments.Size; continue; } else { offset += (uint)frameFragments.Size; isDiffrentBefore = true; } if (isDiffrentBefore) { //每个Fragment 也占用字节 offset += 8 * count; bot.Add(offset); count = 0; } } bot.RemoveAt(bot.Count - 1); // 设置到新的 PixelData frag.OffsetTable.AddRange(bot.ToArray()); // 重新保存 DICOM 到流 ms.SetLength(0); dicomFile.Save(ms); } } catch (Exception mutiEx) { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 处理多帧失败,上传原始文件:{mutiEx.ToString()}"); } } } #endregion #region 本地测试 //// --- 保存到本地文件测试 --- //var localPath = @"D:\TestDicom.dcm"; //using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write)) //{ // ms.CopyTo(fs); //} //return new DicomCStoreResponse(request, DicomStatus.Success); #endregion ms.Position = 0; //irc 从路径最后一截取Guid storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false); fileSize = ms.Length; } Log.Logger.Information($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} {request.SOPInstanceUID} 上传完成 "); } catch (Exception ec) { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 上传异常 {ec.Message}"); } var @lock = _distributedLockProvider.CreateLock($"{studyInstanceUid}"); using (await @lock.AcquireAsync()) { try { var scpStudyId = await dicomArchiveService.ArchiveDicomFileAsync(request.File, _trialId, _trialSiteId, storeRelativePath, Association.CallingAE, Association.CalledAE,fileSize); var series = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId); //没有缩略图 if (series != null && string.IsNullOrEmpty(series.ImageResizePath)) { // 生成缩略图 using (var memoryStream = new MemoryStream()) { DicomImage image = new DicomImage(request.Dataset); var sharpimage = image.RenderImage().AsSharpImage(); sharpimage.Save(memoryStream, new JpegEncoder()); // 上传缩略图到 OSS var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, $"{seriesId.ToString()}_{instanceId.ToString()}.preview.jpg", false); series.ImageResizePath = seriesPath; } } await _seriesRepository.SaveChangesAsync(); if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid)) { var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid)); find.SuccessImageCount++; if (!find.PatientNameList.Any(t => t == patientIdStr) && patientIdStr.IsNotNullOrEmpty()) { find.PatientNameList.Add(patientIdStr); } //首次 (默认是Guid 空,数据库归档出了Id) if (find.SCPStudyId != scpStudyId) { find.SCPStudyId = scpStudyId; } } } catch (Exception ex) { Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 传输处理异常:{ex.ToString()}"); if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid)) { var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid)); find.FailedImageCount++; } } } //监控信息设置 _upload.FileCount++; _upload.FileSize = _upload.FileSize + fileSize; return new DicomCStoreResponse(request, DicomStatus.Success); } public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e) { // let library handle logging and error response return Task.CompletedTask; } public Task OnCEchoRequestAsync(DicomCEchoRequest request) { return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success)); } } }