Основы DICOM с использованием .NET и C#: операции запроса и извлечения (C-GET)

Введение

В этом материале мы продолжим изучение того, как в DICOM реализуются операции запросов извлечения данных. Мы уже рассмотрели две другие составные операции в этой группе. А именно, это операции C-FIND и C-MOVE . C-FIND, как вы помните, позволяет запрашивать информацию о данных DICOM (изображения, структурированные отчеты и т. д.), а операция C-MOVE позволяет затем получить эти данные (а также переслать эти данные в указанное место назначения). Теперь мы рассмотрим еще один способ получения данных DICOM с использованием операции с более подходящим названием, называемой составной операцией C-GET, которая работает немного иначе, чем операция C-MOVE. C-GET не так популярен, как C-MOVE, но, тем не менее, очень полезен. Большинству пользователей, не имеющих опыта работы в сфере здравоохранения, также будет гораздо проще с ним справиться, поскольку он работает во многом так же, как запрос GET работает при использовании протокола HTTP.

Прежде чем мы начнем…

Как и в предыдущих примерах программирования, я буду использовать минимальный код и подход, чтобы проиллюстрировать концепции, которые раскрываются в этом руководстве. Это означает, что код, который я привожу здесь,  подходит для демонстрации концепции, но не является наиболее эффективным кодом для развертывания в реальной жизни и в вашем рабочем приложении.

Чтобы начать работу, вам нужно будет настроить несколько вещей на вашем компьютере, включая среду разработки Visual Studio, а также набор инструментов fo-dicom, прежде чем вы сможете запустить приведенный пример, если вы хотите попробовать это самостоятельно.

Кроме того, вам также понадобится сервер DICOM для выполнения некоторых операций, описанных в этом руководстве. Для примеров, показанных ниже, я буду использовать сервер dcm4chee-arc, который установлен и работает в виртуалбоксе на моем компьютере. В Интернете доступно множество других серверов PACS с открытым исходным кодом. Однако мне нравится использовать dcm4chee-arc, поскольку его очень легко установить и начать работу. Убедитесь, что у вас уже загружены некоторые исследования и вы можете искать исследования пациентов в этих данных.

Этот материал, как и другие материалы этой серии, основан на моем переводе и адаптации материалов с сайта https://saravanansubramanian.com/. Так как на авторском сайте нет статьи с примером иллюстрирующем C-GET для C#, то за теоретическую основу была взята часть статьи "DICOM Basics using Java - Query and Retrieve Operations (C-GET)", а иллюстрации кода были разработаны мною (прим. - А.К.)

Получение данных DICOM с помощью C-GET

C-GET — это еще один (и более «современный») способ получения данных DICOM, который может вас заинтересовать. В отличие от операции C-MOVE (со слегка нелогичным названием), которая позволяет не только извлекать данные, но и отправлять данные в другой пункт назначения, операция C-GET делает именно то, что должна, а именно - получить интересующие вас данные DICOM. Она использует одну ассоциацию DICOM (вместо двух, как C-MOVE) для выполнения операции извлечения данных, что упрощает использование с точки зрения конфигурации, поскольку требуется меньше портов.

Несмотря на более интуитивное название и даже несмотря на то, что данные могут быть отправлены обратно вызывающему абоненту с использованием той же ассоциации DICOM, может быть немного удивительно, что операция C-MOVE по-прежнему остается более популярным методом, используемым для извлечения данных. Для этого есть прежде всего две причины. Первая причина заключается в том, что C-MOVE будет отправлять данные только вызывающим абонентам, которые уже зарегистрированы в таблицах поиска AE, поэтому администраторы PACS обычно более доверяют этому подходу. Вторая причина популярности C-MOVE заключается в том, что его проще реализовать, чем C-GET, который требует гораздо большего количества согласований и переключения ролей между одними и теми же устройствами, поскольку все эти действия должны происходить через одно соединение. По этим причинам огромное количество производителей PACS исторически не поддерживали работу C-GET. Тем не менее C-GET имеет ряд применений в диагностической визуализации, например, при получении изображений через Интернет из системы PACS, развернутой в больничной системе (что C-MOVE может сделать лишь с большими трудностями, поскольку нужны дополнительно настроенные порты и заголовки AE), а также используется в операциях рабочего списка модальности DICOM. Надеюсь, что диаграмма ниже должна проиллюстрировать, как работает операция C-GET.

С точки зрения связи DICOM, от клиента поставщику услуг (C-GET SCP) передается команда DIMSE («C-GET-RQ») в сочетании с объектом DICOM IOD, содержащим критерии для данных, которые необходимо переместить в указанное место назначения (C-GET-SCU). Критерии задаются с помощью атрибута «соответствующие ключи», который мы видели при рассмотрении C-FIND и C-MOVE. Кроме того, передается информация об уровне запроса/извлечения (query/retrieve level), которая указывает, что мы хотим получить (все исследование, серию, изображение и т. д.). Иногда также может быть указан набор символов для управления кодировкой символов представлений значений возвращаемых данных DICOM. Затем C-GET SCP переключает роли на C-STORE SCU и отправляет команду C-STORE-RQ и любые данные, соответствующие критериям, пересылаются в указанное место назначения. C-GET SCP продолжает сообщать о состоянии операции C-STORE, например о количестве переданных файлов, количестве файлов, ожидающих передачи, любых предупреждениях и ошибках, возникающих во время операции и т. д. C-GET SCU также может инициировать операцию отмены (используя C-CANCEL-GET-RQ) в любое время, при этом любые текущие операции C-STORE также будут отменены, а статус «отменен» будет передан обратно в C-GET SCU. Структуры запроса и ответа C-GET показаны ниже для справки. Более подробную информацию можно найти в официальной документации DICOM, поскольку это практически вся теория, которую я могу осветить в этой статье.

Пример операции C-GET

Давайте перейдем к рассмотрению быстрого примера использования набора инструментов fo-dicom для выполнения операции C-GET SCU. Я собираюсь подключиться к серверу dcm-4chee-arc, работающему в virtualbox на моем компьютере. Я настроил его для прослушивания соединений через порт 11112 с IP 192.168.88.26. АЕТ сервера - XRAYARCHIVE. Наша программа, выполняющая роль C-GET SCU будет иметь АЕТ - LOCALAET. Мой сервер настроен на прием запросов от любых устройств с любыми заголовками AE. Поэтому АЕТ нашей программы вообще не обязательно указывать на сервере в отличие от случая, когда используется запрос C-MOVE. В общем случае это неправильный подход, и серверы должны проверять валидность АЕТ  вызывающих устройств в целях безопасности. Выше уже обсуждалось различие между C-GET и C-MOVE. На мой взгляд более простым может быть такое объяснение с точки зрения пользователя. При запросе C-MOVE мы "поручаем" серверу отправить данные нам или другому пользователю. Поэтому сервер должен точно "знать" адрес, куда их посылать. К тому же "приёмник" должен иметь неизменный статический IP и порт и быть готовым получить эти данные, т.е. иметь запущенный сервис C-STORE SCP. Если "приёмник" и сервер находятся в разных сетях, то неизбежно возникают проблемы маршрутизации. А в случае с C-GET мы только "спрашиваем" у сервера разрешения и сами "забираем" нужные нам данные, а сервер их просто отдает. В этом случае нам не нужен статический адрес и порт, все вопросы маршрутизации решаются программно при установлении ассоциации. Т.о. с точки зрения пользователя это намного проще. Единственное, что нужно предусмотреть в программе - куда и с каким именем сохранять принимаемые файлы.

Я собираюсь отправить на свой тестовый сервер сначала запрос C-FIND, чтобы найти исследования DICOM, относящиеся к конкретному пациенту "Anonymous" (который уже заведомо есть на моем сервере 🙂 ), далее (чтобы немного усложнить пример) в этих исследованиях найти серию по определенному описанию (тег Series Description) "KNEE LEFT*", затем получить изображения этой серии и сохранить их в файловой системе моего компьютера. В ходе всей операции мой C#Net-клиент действует в роли C-GET SCU и при необходимости переключает роли на C-STORE SCP, а удаленный DICOM-сервер действует как C-GET SCP, так и C-GET SCU. Код должен быть понятен, если вы понимаете теорию этой операции, которая была объяснена в предыдущих разделах.

У меня есть один пациент, у которого одно исследование, в котором три серии по одному изображению. По заданному выше условию мы должны в итоге "найти" и загрузить всего одну серию, т.е. одно изображение:

Код разделил на куски для лучшей читабельности. Если Вы сравните его с кодом из примера в материале "Основы DICOM с использованием .NET и C#: операции запроса и извлечения (C-MOVE)", то увидите, что он отличается буквально несколькими строками. Процедуры поиска исследований и серий не отличаются вообще, поэтому я не буду их здесь приводить. Весь код программы можно будет скачать по ссылке ниже.

Итак, в процедуре Main задаем вышеизложенную логику.

if (await GetDicomStudyUidsAsync(patientName)) 
                {
                    foreach (string studyUID in studyUIDs)
                    {
                        if (await GetDicomSeriesUidsAsync(studyUID))
                        {
                            foreach (string seriesUID in seriesUIDs)
                            {
                                GetSeriesAsync(studyUID, seriesUID);
                            }
                        }
                        else
                        {
                            LogToDebugConsole("No series found");
                        }
                    }
                }
                else
                {
                    LogToDebugConsole("No studies found");
                }

Процедура запроса C-GET для получения найденных серий. Обратите внимание на строки var pcs = DicomPresentationContext.GetScpRolePresentationContextsFromStorageUids(... и clientGET.OnCStoreRequest += (DicomCStoreRequest req) =>.... Здесь мы указываем, какие синтаксисы передачи поддерживает наша программа и задаем обработчик для роли C-CTORE-SCP, когда программа в процессе ассоциации переключится на приём данных.

  private static async void GetSeriesAsync(string studyUid, string seriesUid)
        {
            try
            {
                int numberInstances = 0; // Будем считать сколько файлов сохранили
                // Создаем клиент для установления ассоциации
                var clientGET = DicomClientFactory.Create(QRServerHost, QRServerPort, false, localAET, QRServerAET);
                clientGET.AssociationRejected += ClientOnAssociationRejected;
                clientGET.AssociationAccepted += ClientOnAssociationAccepted;
                clientGET.AssociationReleased += ClientOnAssociationReleased;

                // Создание запроса C-GET на получение серии по ее УИДам
                var cGetRequest = new DicomCGetRequest(studyUid, seriesUid);                              
                // Добавляем контексты представлений, которые можем принимать 
                // На самом деле их намного больше, но здесь и этого достаточно, для примера
                var pcs = DicomPresentationContext.GetScpRolePresentationContextsFromStorageUids(
                             DicomStorageCategory.Image,
                             DicomTransferSyntax.ExplicitVRLittleEndian,
                             DicomTransferSyntax.ImplicitVRLittleEndian);
                
                // Обработчик для роли C-CTORE-SCP для получения файла             
                clientGET.OnCStoreRequest += (DicomCStoreRequest req) =>
                {
                    if (SaveImage(req.Dataset)) // Если изображение сохранено возвращаем статус успеха 
                    {
                        LogToDebugConsole($"Save dicom file: {req.Dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, "Unknown").Trim()}/" +
                            $"{req.Dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, "Unknown").Trim()}");
                        numberInstances++;
                        return Task.FromResult(new DicomCStoreResponse(req, DicomStatus.Success));
                    }
                    else
                    {
                        return Task.FromResult(new DicomCStoreResponse(req, DicomStatus.ProcessingFailure));
                    }
                    
                };
                clientGET.AdditionalPresentationContexts.AddRange(pcs);                

                await clientGET.AddRequestAsync(cGetRequest);
                await clientGET.SendAsync();
                LogToDebugConsole($"Get {numberInstances} dicom files in series");
            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error move series: {e.StackTrace}");
            }
        }

Процедура сохранения dicom-файла. Файл будет сохранен в папке программы. Для этого сначала создается папка для серии с названием по УИДу серии (Series Instance UID), а в ней сохраняется файл изображения с именем = УИД изображения (SOP Instance UID). На что ещё обратить внимание? Пришлось использовать устаревшую функцию dataset.AutoValidate = false, чтобы отключить автоматическую проверку валидности dicom-тегов из-за того, что выбранные мной для примера файлы в UID содержат ведущие 0 в группах. А по последним изменениям к стандарту это не допускается и без отключения проверки процедура будет давать ошибку. см материал "Типы данных в DICOM"

[Obsolete]
        private static bool SaveImage(DicomDataset dataset)
        {
            try
            {   // Отключение проверки тегов
                dataset.AutoValidate = false;
                var seriesUid = dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, "Unknown").Trim();
                var sopUid = dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, "Unknown").Trim();                              
                if (!Directory.Exists(seriesUid))
                {
                    Directory.CreateDirectory(seriesUid);
                }
                string pathDicomFileName = Path.Combine(seriesUid, sopUid) + ".dcm"; 
                // Записываем файл на диск
                DicomFile dcmFile = new DicomFile(dataset);
                dcmFile.Save(pathDicomFileName);
                if (File.Exists(pathDicomFileName))
                { return true; }
                else { return false; }                               
            }
            catch (Exception e)
            {
                LogToDebugConsole($"Error save dicom file: {e.StackTrace}");
                return false;
            }
        }

Библиотека fo-dicom позволяет нам создать сам запрос C-GET всего одной строчкой - var cGetRequest = new DicomCGetRequest(studyUid, seriesUid);
Причем DicomCGetRequest DicomCMoveRequest имеет три перегрузки: можно было как углубиться ещё на один уровень - запросить для получения определенное изображение из серии, так и вообще-то остановиться на уровне исследований. Другими словами можно создавать запросы на получение всего исследования, всей серии или конкретного изображения. Но в отличии от C-MOVE необходимо определять также обработчик для роли C-STORE-SCP.
Результат выполнения программы для приведенного примера:

Так как вывод очень длинный из-за перечисления поддерживаемых сервером синтаксисов передачи, то здесь я его "укоротил", вырезав часть и оставив только существенные сведения, из которых наглядно видно установление ассоциаций для двух запросов C-FIND и одного C-GET, а также результат - имя сохраненного файла.

Заключение

На этом завершается статья о том, как работает составная операция C-GET. Составные службы C-FIND, C-MOVE и C-GET составляют основу для всех операций запроса и поиска в любых рабочих процессах, связанных с DICOM. В следующем статье этой серии я расскажу еще об одной составной операции под названием «C-STORE». Эта услуга обеспечивает отправку данных другим DICOM-устройствам, например для сохранения в архиве PACS.

 

Scroll to top