Введение
В этом материале мы продолжим изучение того, как в 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.