339 lines
15 KiB
C#
339 lines
15 KiB
C#
// Copyright (c) 2012-2021 fo-dicom contributors.
|
|
// Licensed under the Microsoft Public License (MS-PL).
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FellowOakDicom;
|
|
using FellowOakDicom.Log;
|
|
using FellowOakDicom.Network;
|
|
using QueryRetrieve_SCP.Model;
|
|
using FellowOakDicom.Network.Client;
|
|
using FellowOakDicom.Imaging.Codec;
|
|
|
|
namespace QueryRetrieve_SCP
|
|
{
|
|
|
|
public class QRService : DicomService, IDicomServiceProvider, IDicomCFindProvider, IDicomCEchoProvider, IDicomCMoveProvider, IDicomCGetProvider
|
|
{
|
|
|
|
|
|
private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[]
|
|
{
|
|
DicomTransferSyntax.ExplicitVRLittleEndian,
|
|
DicomTransferSyntax.ExplicitVRBigEndian,
|
|
DicomTransferSyntax.ImplicitVRLittleEndian
|
|
};
|
|
|
|
|
|
private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[]
|
|
{
|
|
// Lossless
|
|
DicomTransferSyntax.JPEGLSLossless,
|
|
DicomTransferSyntax.JPEG2000Lossless,
|
|
DicomTransferSyntax.JPEGProcess14SV1,
|
|
DicomTransferSyntax.JPEGProcess14,
|
|
DicomTransferSyntax.RLELossless,
|
|
|
|
// Lossy
|
|
DicomTransferSyntax.JPEGLSNearLossless,
|
|
DicomTransferSyntax.JPEG2000Lossy,
|
|
DicomTransferSyntax.JPEGProcess1,
|
|
DicomTransferSyntax.JPEGProcess2_4,
|
|
|
|
// Uncompressed
|
|
DicomTransferSyntax.ExplicitVRLittleEndian,
|
|
DicomTransferSyntax.ExplicitVRBigEndian,
|
|
DicomTransferSyntax.ImplicitVRLittleEndian
|
|
};
|
|
|
|
|
|
public string CallingAE { get; protected set; }
|
|
public string CalledAE { get; protected set; }
|
|
|
|
|
|
public QRService(INetworkStream stream, Encoding fallbackEncoding, ILogger log, ILogManager logmanager, INetworkManager network, ITranscoderManager transcoder) : base(stream, fallbackEncoding, log, logmanager, network, transcoder)
|
|
{
|
|
/* initialization per association can be done here */
|
|
}
|
|
|
|
|
|
public async Task<DicomCEchoResponse> OnCEchoRequestAsync(DicomCEchoRequest request)
|
|
{
|
|
Logger.Info($"Received verification request from AE {CallingAE} with IP: {Association.RemoteHost}");
|
|
return new DicomCEchoResponse(request, DicomStatus.Success);
|
|
}
|
|
|
|
|
|
public void OnConnectionClosed(Exception exception)
|
|
{
|
|
Clean();
|
|
}
|
|
|
|
|
|
public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
|
|
{
|
|
//log the abort reason
|
|
Logger.Error($"Received abort from {source}, reason is {reason}");
|
|
}
|
|
|
|
|
|
public Task OnReceiveAssociationReleaseRequestAsync()
|
|
{
|
|
Clean();
|
|
return SendAssociationReleaseResponseAsync();
|
|
}
|
|
|
|
|
|
public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
|
|
{
|
|
CallingAE = association.CallingAE;
|
|
CalledAE = association.CalledAE;
|
|
|
|
Logger.Info($"Received association request from AE: {CallingAE} with IP: {association.RemoteHost} ");
|
|
|
|
if (QRServer.AETitle != CalledAE)
|
|
{
|
|
Logger.Error($"Association with {CallingAE} rejected since called aet {CalledAE} is unknown");
|
|
return SendAssociationRejectAsync(DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized);
|
|
}
|
|
|
|
foreach (var pc in association.PresentationContexts)
|
|
{
|
|
if (pc.AbstractSyntax == DicomUID.Verification
|
|
|| pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelFind
|
|
|| pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelMove
|
|
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelFind
|
|
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelMove)
|
|
{
|
|
pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes);
|
|
}
|
|
else if (pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelGet
|
|
|| pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelGet)
|
|
{
|
|
pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
|
|
}
|
|
else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
|
|
{
|
|
pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
|
|
}
|
|
else
|
|
{
|
|
Logger.Warn($"Requested abstract syntax {pc.AbstractSyntax} from {CallingAE} not supported");
|
|
pc.SetResult(DicomPresentationContextResult.RejectAbstractSyntaxNotSupported);
|
|
}
|
|
}
|
|
|
|
Logger.Info($"Accepted association request from {CallingAE}");
|
|
return SendAssociationAcceptAsync(association);
|
|
}
|
|
|
|
|
|
public async IAsyncEnumerable<DicomCFindResponse> OnCFindRequestAsync(DicomCFindRequest request)
|
|
{
|
|
var queryLevel = request.Level;
|
|
|
|
var matchingFiles = new List<string>();
|
|
IDicomImageFinderService finderService = QRServer.CreateFinderService;
|
|
|
|
// a QR SCP has to define in a DICOM Conformance Statement for which dicom tags it can query
|
|
// depending on the level of the query. Below there are only very few parameters evaluated.
|
|
|
|
switch (queryLevel)
|
|
{
|
|
case DicomQueryRetrieveLevel.Patient:
|
|
{
|
|
var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
|
|
var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
|
|
|
|
matchingFiles = finderService.FindPatientFiles(patname, patid);
|
|
}
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Study:
|
|
{
|
|
var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
|
|
var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
|
|
var accNr = request.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty);
|
|
var studyUID = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty);
|
|
|
|
matchingFiles = finderService.FindStudyFiles(patname, patid, accNr, studyUID);
|
|
}
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Series:
|
|
{
|
|
var patname = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
|
|
var patid = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
|
|
var accNr = request.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty);
|
|
var studyUID = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty);
|
|
var seriesUID = request.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty);
|
|
var modality = request.Dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty);
|
|
|
|
matchingFiles = finderService.FindSeriesFiles(patname, patid, accNr, studyUID, seriesUID, modality);
|
|
}
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Image:
|
|
yield return new DicomCFindResponse(request, DicomStatus.QueryRetrieveUnableToProcess);
|
|
yield break;
|
|
}
|
|
|
|
// now read the required dicomtags from the matching files and return as results
|
|
foreach (var matchingFile in matchingFiles)
|
|
{
|
|
var dicomFile = DicomFile.Open(matchingFile);
|
|
var result = new DicomDataset();
|
|
foreach (var requestedTag in request.Dataset)
|
|
{
|
|
// most of the requested DICOM tags are stored in the DICOM files and therefore saved into a database.
|
|
// you can fill the responses by selecting the values from the database.
|
|
// also be aware that there are some requested DicomTags like "ModalitiesInStudy" or "NumberOfStudyRelatedInstances"
|
|
// or "NumberOfPatientRelatedInstances" and so on which have to be calculated and cannot be read from a DICOM file.
|
|
if (dicomFile.Dataset.Contains(requestedTag.Tag))
|
|
{
|
|
dicomFile.Dataset.CopyTo(result, requestedTag.Tag);
|
|
}
|
|
// else if (requestedTag == DicomTag.NumberOfStudyRelatedInstances)
|
|
// {
|
|
// ... somehow calculate how many instances are stored within the study
|
|
// result.Add(DicomTag.NumberOfStudyRelatedInstances, number);
|
|
// } ....
|
|
else
|
|
{
|
|
result.Add(requestedTag);
|
|
}
|
|
}
|
|
yield return new DicomCFindResponse(request, DicomStatus.Pending) { Dataset = result };
|
|
}
|
|
|
|
yield return new DicomCFindResponse(request, DicomStatus.Success);
|
|
}
|
|
|
|
|
|
public void Clean()
|
|
{
|
|
// cleanup, like cancel outstanding move- or get-jobs
|
|
}
|
|
|
|
|
|
public async IAsyncEnumerable<DicomCMoveResponse> OnCMoveRequestAsync(DicomCMoveRequest request)
|
|
{
|
|
// the c-move request contains the DestinationAE. the data of this AE should be configured somewhere.
|
|
if (request.DestinationAE != "STORESCP")
|
|
{
|
|
yield return new DicomCMoveResponse(request, DicomStatus.QueryRetrieveMoveDestinationUnknown);
|
|
yield return new DicomCMoveResponse(request, DicomStatus.ProcessingFailure);
|
|
yield break;
|
|
}
|
|
|
|
// this data should come from some data storage!
|
|
var destinationPort = 11112;
|
|
var destinationIP = "localhost";
|
|
|
|
IDicomImageFinderService finderService = QRServer.CreateFinderService;
|
|
IEnumerable<string> matchingFiles = Enumerable.Empty<string>();
|
|
|
|
switch (request.Level)
|
|
{
|
|
case DicomQueryRetrieveLevel.Patient:
|
|
matchingFiles = finderService.FindFilesByUID(request.Dataset.GetString(DicomTag.PatientID), string.Empty, string.Empty);
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Study:
|
|
matchingFiles = finderService.FindFilesByUID(string.Empty, request.Dataset.GetString(DicomTag.StudyInstanceUID), string.Empty);
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Series:
|
|
matchingFiles = finderService.FindFilesByUID(string.Empty, string.Empty, request.Dataset.GetString(DicomTag.SeriesInstanceUID));
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Image:
|
|
yield return new DicomCMoveResponse(request, DicomStatus.QueryRetrieveUnableToPerformSuboperations);
|
|
yield break;
|
|
}
|
|
|
|
var client = DicomClientFactory.Create(destinationIP, destinationPort, false, QRServer.AETitle, request.DestinationAE);
|
|
client.NegotiateAsyncOps();
|
|
int storeTotal = matchingFiles.Count();
|
|
int storeDone = 0; // this variable stores the number of instances that have already been sent
|
|
int storeFailure = 0; // this variable stores the number of faulues returned in a OnResponseReceived
|
|
foreach (string file in matchingFiles)
|
|
{
|
|
var storeRequest = new DicomCStoreRequest(file);
|
|
// !!! there is a Bug in fo-dicom 3.0.2 that the OnResponseReceived handlers are invoked not until the DicomClient has already
|
|
// sent all the instances. So the counters are not increased image by image sent but only once in a bulk after all storage
|
|
// has been finished. This bug will be fixed hopefully soon.
|
|
storeRequest.OnResponseReceived += (req, resp) =>
|
|
{
|
|
if (resp.Status == DicomStatus.Success)
|
|
{
|
|
Logger.Info("Storage of image successfull");
|
|
storeDone++;
|
|
}
|
|
else
|
|
{
|
|
Logger.Error("Storage of image failed");
|
|
storeFailure++;
|
|
}
|
|
};
|
|
client.AddRequestAsync(storeRequest).Wait();
|
|
}
|
|
|
|
var sendTask = client.SendAsync();
|
|
|
|
while (!sendTask.IsCompleted)
|
|
{
|
|
// while the send-task is runnin we inform the QR SCU every 2 seconds about the status and how many instances are remaining to send.
|
|
yield return new DicomCMoveResponse(request, DicomStatus.Pending) { Remaining = storeTotal - storeDone - storeFailure, Completed = storeDone };
|
|
Thread.Sleep(TimeSpan.FromSeconds(2));
|
|
}
|
|
|
|
Logger.Info("..finished");
|
|
yield return new DicomCMoveResponse(request, DicomStatus.Success);
|
|
}
|
|
|
|
|
|
public async IAsyncEnumerable<DicomCGetResponse> OnCGetRequestAsync(DicomCGetRequest request)
|
|
{
|
|
IDicomImageFinderService finderService = QRServer.CreateFinderService;
|
|
IEnumerable<string> matchingFiles = Enumerable.Empty<string>();
|
|
|
|
switch (request.Level)
|
|
{
|
|
case DicomQueryRetrieveLevel.Patient:
|
|
matchingFiles = finderService.FindFilesByUID(request.Dataset.GetString(DicomTag.PatientID), string.Empty, string.Empty);
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Study:
|
|
matchingFiles = finderService.FindFilesByUID(string.Empty, request.Dataset.GetString(DicomTag.StudyInstanceUID), string.Empty);
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Series:
|
|
matchingFiles = finderService.FindFilesByUID(string.Empty, string.Empty, request.Dataset.GetString(DicomTag.SeriesInstanceUID));
|
|
break;
|
|
|
|
case DicomQueryRetrieveLevel.Image:
|
|
yield return new DicomCGetResponse(request, DicomStatus.QueryRetrieveUnableToPerformSuboperations);
|
|
yield break;
|
|
}
|
|
|
|
foreach (var matchingFile in matchingFiles)
|
|
{
|
|
var storeRequest = new DicomCStoreRequest(matchingFile);
|
|
SendRequestAsync(storeRequest).Wait();
|
|
}
|
|
|
|
yield return new DicomCGetResponse(request, DicomStatus.Success);
|
|
}
|
|
|
|
|
|
}
|
|
}
|