Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

PACS-сервер своими руками

Anna | 24.06.2014 | нет комментариев
Не так давным-давно наша компания завершила работу над внедрением PACS-сервера (Picture Archiving and Communication System) в один из медицинских диагностических центров нашего города. До этого там стоял PACS-сервер с открытым начальным кодом — dcm4chee, тот, что не блистал высокой скоростью работы, от того что написан на Java. К тому же одним из требований клиента было иметь доступ к внутренней структуре сервера. Следственно было решено написать свой. К тому же в компании имелся навык сходственных разработок как клиентских, так и серверных частей PACS-систем, следственно компромиссным решением было сделать личный PACS-архив, удовлетворяющий требования клиента. Большей частью реализации ядра сервера пришлось заниматься мне и за это время был приобретён особенный навык в этой области, чем и хочу поделиться с програ-сообществом. Но обо всём по порядку.

Преамбула

Для всеобщего понимания разглядим роль PACS-системы в диагностическом центре. В любом диагностическом центре есть диагностическое оборудование: МРТ-, КТ-томографы, УЗИ-станции либо ЭКГ-агрегаты (всякое из этих устройств в терминах протокола DICOM именуется Modality) и ПО для диагностики (у наших докторов применялся OsiriX). Получив изображения на томографе, нужно отправить их на станцию диагностики. Видимо, что для этого нужно некое интегрирующее звено, которое собирает изображения с томографов, УЗИ-станций, ЭКГ-агрегатов, может изготавливать по ним поиск и передавать изображения по сети. Таким звеном и являются PACS-сервера:

Видимо, что для взаимодействия разнокачественного медицинского оборудования нужен цельный протокол. И как раз таким протоколом выступает DICOM (Digital Imaging and Communications in Medicine), тот, что за последние 20 лет был серьёзно улучшен, что дозволило легко интегрировать медицинское оборудование в всеобщую информационную систему. Фактически все изготовители медицинского оборудования следуют этому протоколу. Следственно помощь протокола DICOM было обычным требованием к PACS-серверу. Было решено реализовать многопоточный высоконагруженный PACS, способный трудиться в кластере. Сервер разрабатывался на языке С и использовалась самая адекватная на сегодняшний день библиотека для работы с протоколом DICOM, написанная на С — DCMTK. Именно вследствие этой библиотеке стало допустимымстремительно реализовывать высоконагруженные PACS-системы.

Проектируем БД

БД в PACS-системе разрешает беречь информацию о сохранённых изображениях и изготавливать по ним поиск. Изображения также необходимо уметь передавать по сети, а совместно с ним метаинформацию о изображении (кто на снимке, в какой больнице произведены, кто проводил изыскание и другое). Для этих целей протоколом DICOM предусмотрена особая 4х уровневая модель данных, о которой дозволено лаконично узнать тут. Полный список всевозможных признаков файла дозволено узнать на официальном сайте протокола[1]. В изображениях, полученных из различных агрегатов, список этих признаков будет отличаться, что является абсолютно типичным. Впрочем часть признаков остаётся непременной для поддержания универсального поиска по изображениям. Их немножко — каждого штук десять, среди них Patient Name, Patient ID, Patient Birthday, Modality Type (КТ, МРТ, УЗИ и др), Study Date (дата изыскания) и др. Помимо непременных параметров существуют необязательные, их достаточно много и поддерживать их в БД как показывает практика — является лишним.

Присутствие большого числа необязательных параметров в метаинформации изображений — один из недостатков протокола DICOM. Некоторые агрегаты выставляют одни параметры, некоторые — другие. Следственно поддерживать их в БД для поиска — безрезультатно. В результате проработав несколько вариантов, остановились на таком варианте БД:

Как видно схема БД соответствует многоуровневой структуре DICOM файлов. У одного пациента может быть много стадий (читай изысканий). Изыскание представляет собой уйма серий, определяемых протоколом изыскания. Серия хранит уйма изображений.

Основные функции PACS-системы

Разглядим лаконично основные функции (сервисы) стандартной PACS-системы, примерно все из которых я теснее упомянул. От того что всякое взаимодействие рабочих станций с PACS-системой является заказчик-серверным, то и все операции также реализованы в 2-х вариантах — клиентском и серверном. В DCMTK присутствуют реализации обоих вариантов. PACS реализует серверную часть.
Префикс ‘C-’ у операций обозначает Composite, что подразумевает, что операция — целостная и самодостаточная и выполняется без привязки к иным операциям. Существуют ещё операции с префиксом ‘N-’(N-CREATE, N-SET, N-GET и др.), которые выполняются в рамках какой-то больше всеобщей операции (выставляют ранги, сообщают о начале изыскания и др.). Эти операции не относятся к теме данной статьи.

C-ECHO – команда, дозволяющая узнать доступность заказчика в сети. Аналогична команде ping в винде. В реализации команда дюже примитивна — необходимо каждого лишь отправить результат со рангом STATUS_Success:

DIMSE_sendEchoResponse(assoc, presID, request, STATUS_Success, NULL)

где assoc – соединение, установленное заказчиком, request — входящий запрос.

С-STORE — команда, дозволяющая сберегать изображения на PACS-сервере в формате DCM.
Вот кусок кода, тот, что это делает:

OFCondition storeSCP()
{
  T_DIMSE_C_StoreRQ* req = &m_msg->msg.CStoreRQ;
  DcmDataset* dset = 0x0;
  OFCondition cond = DIMSE_storeProvider(m_assoc, m_presID, req, NULL, OFTrue,  &dset,storeSCPCallback, 0x0, DIMSE_BLOCKING, 0);
  if (cond.bad())  
    Log::error("C-STORE provider failed. Text: %s", cond.text());  
  return cond;
}

void storeSCPCallback( void* /*callbackData*/,
                       T_DIMSE_StoreProgress *progress,
                       T_DIMSE_C_StoreRQ* /*request*/,
                       char * /*imageFileName*/,
                       DcmDataset **imageDataSet,
                       T_DIMSE_C_StoreRSP* response,
                       DcmDataset **statusDetail)
{
  if (progress->state == DIMSE_StoreEnd)
  {
    if ((imageDataSet != NULL) && (*imageDataSet != NULL))
    {
      DcmFileFormat dcmff(*imageDataSet);
      // some error
      if (!commandStore(&dcmff))     
        response->DimseStatus = STATUS_STORE_Refused_OutOfResources;

      delete *imageDataSet;
      *imageDataSet = 0x0;
    }
  }
  delete *statusDetail;
  *statusDetail = 0x0;
}

bool ServerCoreImpl::commandStore(DcmFileFormat* file)
{  
  // тут 
  // 1. парсим файл, составляем объекты(бины) для таблиц PATIENT, STUDY, SERIES, OBJECT. 
  // 2. Если такого файла нет в базе, то сберегаем его на диск
  // 3. Если пришлось сберегать файл, то оставляем на него ссылку в БД (делаем
  //    вставку бинов)  
  // Функция возвращает true, если всё прошло благополучно, напротив false
}

Колбэк storeSCPCallback срабатывает на всякий пакет, а не на всякий файл. О заключении скачивания файла свидетельствует условие progress->state == DIMSE_StoreEnd, тогда мы можем сберечь файл. Исключительной трудностью при реализации этой команды является выбор конструкции каталогов при сохранении файла. Дабы не беречь в таблице OBJECTS путь к файлу, мы вычисляем его из остальных данных. Мы остановились на такой структуре каталогов: ПУТЬ_К_ХРАНИЛИЩУ/STUDY.DATE(YEAR)/STUDY.DATE(MONTH)/STUDY.DATE(DAY)/STUDY.TIME(HOUR)/PATIENT.PID(первая буква)/ PATIENT.PID/STUDY.UID/{изображения}. Такая иерархическая конструкция разрешает минимизировать число вложенных папок, что разрешает трудиться с данной конструкцией каталогов без временных лагов.

Также хочется сказать, что таблица OBJECT заполняется дюже насыщенно. Одно изыскание на МРТ-томографе длится в среднем 20 минут, за это время томограф изготавливает 100-300 изображений, КТ-томограф 500-700 иображений. Итого изображений за день может добиваться 1440/20 * 500 = 36000 изображений за сутки. В нашем диагностическом центре перерывов в работе томографов фактически нет ни днём, ни ночью. Следственно таблица OBJECT должна беречь минимально допустимое число данных.

С-MOVE — команда, разрешающая передать изображения из PACS на рабочую либо диагностическую станцию. Команда передаётся вызывающей станцией (source) на PACS и в ней указывается, на какую станцию (destination) нужно загрузить изображения. В частном случае, если source=destination, то происходит легко скачивание файлов.

Команда C-MOVE является больше многофункциональной по сопоставлению с командой С-GET, дозволяющей только скачивать изображения. С-MOVE может скачивать изображения не только на свою, но и на всякую иную. В команде указывается AETitle станции, на которую требуется загрузить изображения. AETitle – это имя заказчика, обыкновенно крупными буквами (скажем, CLIENT_SCU). Оно устанавливается при запуске dicom-listener’а (сервера).

То есть заказчик, инициализирующий команду С-MOVE на PACS-сервер, должен у себя запустить мини-PACS, дозволяющий принимать только команду С-STORE. А PACS-сервер в свою очередь должен при команде С-MOVE установить новое соединение с заказчиком, поднять изображения из хранилища и исполнить для всякого из низ клиентскую версию команды С-STORE обратно на заказчик. Кстати, только команда С-MOVE разрешает передавать как сжатые изображения (JPEG), так и несжатые за счёт установления нового соединения.
Команда С-GET, впрочем, может загружать изображения без установления нового соединения и, следственно, без необходимости поднимать сервер на клиентской стороне. В этом случае PACS также исполняет клиентскую версию команды С-STORE, только через соединение установленное командой С-GET.

C-FIND — команда, разрешающая изготавливать поиск по изображениям на различных ярусах. То есть реально существует четыре вида команды С-FIND: C-FIND на ярусе PATIENT, на ярусе STUDY, на ярусе SERIES и на ярусе IMAGE.

void HandlerFind::findSCPCallback ( /* in */
                                    void* callbackData,
                                    OFBool cancelled, 
                                    T_DIMSE_C_FindRQ* request,
                                    DcmDataset* requestIdentifiers, 
                                    int responseCount,
                                    /* out */
                                    T_DIMSE_C_FindRSP *response,
                                    DcmDataset** responseDataSet,
                                    DcmDataset** statusDetail)
{  
  // запрос отменён
  if (cancelled)
  {
    strcpy(response->AffectedSOPClassUID, request->AffectedSOPClassUID);
    response->MessageIDBeingRespondedTo = request->MessageID;
    response->DimseStatus = STATUS_FIND_Cancel_MatchingTerminatedDueToCancelRequest;
    response->DataSetType = DIMSE_DATASET_NULL;
    return;
  }
  if (responseCount == 1)
  {
     // 
     // при первом запросе инициализируем запрос к БД.
     // О критериях поиска и о ярусе дозволено узнать в requestIdentifiers
     // 
  }
  /* 
  тут запрашиваем следующий объект из базы и заполняем responseDataSet в соответствии с ярусом
  */
  if (/*все данные отправили*/)
  {
    strcpy(response->AffectedSOPClassUID, request->AffectedSOPClassUID);
    response->MessageIDBeingRespondedTo = request->MessageID;
    response->DimseStatus = STATUS_Success;
    response->DataSetType = DIMSE_DATASET_NULL;
    return;
  }
}

OFCondition HandlerFind::find()
{
  OFCondition cond = EC_Normal;
  T_DIMSE_C_FindRQ *req = &m_msg->msg.CFindRQ;
  FindCallbackData cdata;
  cond = DIMSE_findProvider(m_assoc, m_presID, req, findSCPCallback, &cdata, DIMSE_BLOCKING, 0);
  if (cond.bad())
    Log::loggerDicom.error("C-FIND provider failed. Text: %s", cond.text());
  return cond;
}

То есть в коллбэке необходимо заполнить объекты response — параметры результата и responseDataSet — информация о пациенте/стадии/серии/изображении, которую необходимо было обнаружить. Об отправке их обратно на заказчик позаботится функция DIMSE_findProvider() из DCMTK.

Команда С-FIND опасна тем, что заказчик может указать слишком всеобщий критерий поиска и заказчику придётся отдавать огромный объём информации. Скажем, дозволено запросить все стадии за конечный год. Если попытаться вначале загрузить все данные на сервер, то сервер скорее каждого повиснет. Следственно делать крупные запросы невозможно, необходимо подгружать данные по мере срабатывания коллбэков. Для этого необходимо реализовать запрос к БД в виде итератора и по мере срабатывания коллбэков вызывать next() и таким образом брать дальнейший объект. К тому же отменить поиск дозволено только по приходу коллбэка, следственно если поиск на PACS’е повиснет на некоторое время на выборке из БД, а заказчик будет вызывать отмену запроса, то никакой реакции на заказчике не произойдёт. Это актуально для поиска на ярусе пациентов и стадий. Для поиска на ярусе серий это неактуально, от того что стадий, содержащих больше 15 серий, мы на практике не встречали. Подобно для поиска на ярусе изображений — серий с больше чем 1000 изображений мы на практике не встречали.

Обобщим

Выходит, мы разглядели основные функции PACS-системы и её роль в всеобщей структуре диагностического центра. Также освещены фактические моменты и разные аспекты реализации медицинских индустриальных PACS-систем. Впрочем этим функционалом PACS-системы как правило не ограничиваются. Существует также сервис WADO(Web Access to DICOM Objects) и сервис управления рабочими задачами (Modality worklist), также входящих в функции PACS-систем. Верю, для кого-то статья окажется пригодной и сэкономит кучу времени.

Ссылки

1. Список всех тегов DICOM (http://medical.nema.org/Dicom/2011/11_06pu.pdf, стр. 8).
2. Офицальная страница протокола DICOM – medical.nema.org/standard.html
3. Про PACS-системы на русском – ru.wikipedia.org/wiki/PACS
4. Про DICOM на русском — ru.wikipedia.org/wiki/DICOM

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB