User Tools

Site Tools


neuroimagen:pipeline2xnat_api

Usando XNATACE.pm

Qué es?

XNATACE.pm es un modulo Perl que añade funciones auxiliares para simplificar la interaccion con la API de XNAT desde el pipeline de neuroimagen de ACE

Porqué?

Aunque la API de XNAT es extremadamente versátil, require que cada vez que se use, se han elaborar ordenes complejas. Escribiendo estas interacciones con la API es, 1.- muy sencillo cometer un error, 2.- muy dificil encontrar los errores que se cometen.

Asi que con la integracion cada vez mayor del pipeline con XNAT, es extremadamente util encapsular las llamadas a la API de XNAT dentro de funciones Perl que acepten como argumento, variables mas o menos sencillas.

Ya tenemos algo asi para la interaccion en el prompt del sistema (xnatapic). Porque no usarlo? Pues porque añade otra capa de software que necesita un mantenimiento independiente y que puede afectar el funcionamiento del pipeline con una sencilla actualizacion de herramientas basicas como curl o jq. xnatapic es una herramienta ideal para una interaccion rapida o el prototipado de acciones en XNAT pero añadiria una capa de complejidad innecesaria al pipeline.

Configuracion de XNAT

Al igual que cualquier cliente de XNAT, XNATACE necesita la informacion basica para acceder a XNAT. De momento, la funcion xconf lee por defecto el archivo

 $ENV{'HOME'}.'/.xnatapic/xnat.conf' 

y carga los valores de HOST, USER y PASSWORD. lo que he hecho aqui es reutilizar el config de xnatapic.

Es posible sobreescribir el path del archivo que usa XNATACE pasando como argumento el path deseado cuando se crea una sesion.

Usando las funciones

Las funciones de XNATACE se importan del modo usual en un script Perl,

use XNATACE qw(xget_session xget_subjects xget_mri);

La lista de funciones esta medianamente documentada.

Abriendo sesion

Siempre lo primero que se necesita es abrir un sesion en XNAT,

my %conn = xget_session();

Esto guarda en el hash %conn todos los datos necesarios. Si en lugar de esto se hace,

my %conn = xget_session('/other/than/default/path/xnat.conf');

entonces se sobreescribe la direccion del archivo de configuracion y se cargan los datos de este nuevo archivo.

Leyendo sujetos

Tipicamente, lo primero que hacemos despues de abrir una sesion es leer la lista de sujetos dentro del proyecto.

my %subjects = xget_subjects($conn{'HOST'}, $conn{'JSESSION'}, $prj_data{'XNAME'});

El hash devuelto por la funcion contiene el XNAT_SUBJECT_ID como key y el campo label con el identificador del proyecto. Es decir es de tipo, { XNAT_SUBJECT_ID ⇒ {'label' ⇒ LABEL} } . Esto permite añadir otros campos al hash a medida que sea necesario.

Por ejemplo, para saber el experimento MRI asociado a cada sujeto podemos hacer ahora,

foreach my $sbj (sort keys %subjects){
    $subjects{$sbj}{'experiment'} = xget_mri($conn{'HOST'}, $conn{'JSESSION'}, $prj_data{'XNAME'}, $sbj);
}

o, con la misma sintaxis, podemos usar xget_pet() si queremos saber el PET asociado al sujeto.

Almacenando datos externos

Una de las grandes ventajas de XNAT es que, ademas de las imagenes, podemos almacenar datos asociados al sujeto y al experimento. Asi como se guardan los resultados de la ejecucion del pipeline, podemos utilizar la REST API de XNAT para almacenar datos que hayan sido obtenidos fuera de la plataforma. Un ejemplo tipico es como almacenamos, importando un CSV, la fecha de nacimiento y genero del sujeto.

Otro ejemplo son las funciones especificas

xput_report();
xput_rvr();

que almacenan el informe radiologico en PDF y los datos extraidos, respectivamente, dentro de cada experimento.

Mas interesantes son las funciones,

xcreate_res();
xput_res_file();
xput_res_data();

que son una generalizacion de las anteriores y deberian sustituirlas. Si tuviera tiempo para deprecar cosas :-/.

Aqui se genera un recurso especifico asociado a un experimento con nombre data,

xcreate_res($conn{'HOST'}, $conn{'JSESSION'}, $experiment, 'data');

Claramente este nombre puede ser cualquier cosa. Por ejemplo RVR o fsqc.

Ahora podemos guardar distintos elementos dentro de este recurso.

Para guardar cualquier archivo hacemos algo como,

xput_res_file($conn{'HOST'}, $conn{'JSESSION'}, $experiment, 'data', 'filename.whatever', $actual_file_path);

y dentro del resource data se guardara el archivo guardado en la variable $actual_file_path con el nombre filename.whatever.

Pero esto implica que si queremos guardar datos, tendremos que construir el archivo json antes de guardarlo. Otra manera de proceder sería invocar,

xput_res_data($conn{'HOST'}, $conn{'JSESSION'}, $experiment, 'data', 'my_new_data.json', \%hash_of_data);

que toma los datos dentro de %hash_of_data, los pone en formato json y los almacena en el recurso data, dentro del archivo my_new_data.json.

Modificando datos del sujeto

La funcion

xput_sbj_data();

permite modificar datos relativos al sujeto como dob, gender, education, etc. Normalmente lo usaremos para edad y genero y no mucho mas, pero en caso necesario ahi lo tenemos.

La llamada es normalmente del tipo,

my  $xdata = xput_sbj_data($conn{'HOST'}, $conn{'JSESSION'}, $xbj, 'gender', 'female');

o

my  $xdata = xput_sbj_data($conn{'HOST'}, $conn{'JSESSION'}, $xbj, 'dob', '1947-06-07');

pero tambien puede escribirse,

my  $xdata = xput_sbj_data($conn{'HOST'}, $conn{'JSESSION'}, $xbj, 'gender,dob', 'female,1947-06-07');

La funcion retorna el sujeto modificado (subject's XNAT accession ID) en caso de tener exito. Esto puede servir para controlar el proceso haciendo algo como,

whatever unless xput_sbj_data($conn{'HOST'}, $conn{'JSESSION'}, $xbj, 'dob', '1947-06-07');

Bajando datos

Ahora que ya tenemos la lista de sujetos y experimentos asociados podemos obtener varios datos almacenados en XNAT.

Para empezar los datos demograficos basicos, edad y genero, deberian poder obtenerse con la funcion,

xget_sbj_demog();

Nota: No se almacena la edad del sujeto por motivos obvios, lo que se almacena es la fecha de nacimiento (como dob).

Pro ejemplo, si queremos saber el genero de un sujeto hacemos,

my $gender = xget_sbj_demog($conn{'HOST'}, $conn{'JSESSION'}, $subject, 'gender');

En cambio para averiguar la edad habria que tambien saber la fecha en que se hizo el experimento.

Para sacar la fecha de un experimento dado usamos la funcion

xget_exp_data();

Esta funcion es capaz de extraer varios datos pertenecientes al experimento. Ejemplo, para averiguar que datos podemos extraer del experimento XNAT_E00550, hago,

$  curl -X GET -b JSESSIONID=F92CF5668348A12A0ADBA47576748923 "http://detritus.fundacioace.com:8088/data/experiments/XNAT_E00550?format=json" 2>/dev/null | jq '.items[0].data_fields'
{
  "dcmPatientId": "D22183969",
  "subject_ID": "XNAT_S00449",
  "date": "2022-04-10",
  "dcmAccessionNumber": "2",
  "modality": "MR",
  "prearchivePath": "/old_nas/xnat/prearchive/unidad/20220601_112511233/D22183969",
  "project": "unidad",
  "scanner/model": "MAGNETOM Vida",
  "study_id": "2",
  "label": "D22183969",
  "scanner/manufacturer": "Siemens",
  "dcmPatientName": "20071018",
  "UID": "0.0.0.0.1.0.0.0.1654096824.46895.447490818.1",
  "fieldStrength": "3.0",
  "session_type": "unidad",
  "time": "18:55:12",
  "ID": "XNAT_E00550",
  "id": "XNAT_E00550"
}

Si lo que queremos es la fecha, entonces utilizamos la funcion como,

my $mri_date = xget_exp_data($conn{'HOST'}, $conn{'JSESSION'}, $subjects{$subject}{'experiment'}, 'date');

y ahora, para saber la edad del sujeto en la fecha de la MRI basta hacer,

my $dob = xget_sbj_demog($conn{'HOST'}, $conn{'JSESSION'}, $subject, 'dob');
my $ddif = Delta_Format(DateCalc(ParseDate($dob),ParseDate($mri_date)),2,"%hh")/(24*365.2425);
$subjects{$subject}{'age'} = nearest(0.1, $ddif);

A partir de saber el experimento adecuado hay mucha otra información que es posible extraer de XNAT. Para empezar, la ejecución de los pipelines deja almacenada todos los resultados de la ejecucion. Las funciones

xget_fs_data();
xget_fs_stats();

permiten extraer el resultado completo de la ejecucion de Freesurfer, o en el caso de la ultima, solo los archivos de stats. Si lo que quiero es dejar el resultado de Freesurfer en un directorio con el nombre del sujeto, puedo hacer algo como,

my %subjects = xget_subjects($conn{'HOST'}, $conn{'JSESSION'}, $prj_data{'XNAME'});
foreach my $sbj (sort keys %subjects){
    $subjects{$sbj}{'experiment'} = xget_mri($conn{'HOST'}, $conn{'JSESSION'}, $prj_data{'XNAME'}, $sbj);
    my $data_dir = 'fsresult/'.$subjects{$sbj}{'label'};
    mkdir $data_dir;
    my $tmp_tgz = $ENV{'TMPDIR'}.'/'.$sbj.'.tgz';
    xget_fs_data($conn{'HOST'}, $conn{'JSESSION'},$prj_data{'XNAME'},  $subjects{$sbj}{'experiment'}, $tmp_tgz);
    my $order =  "tar xzf ".$tmp_tgz." -C ".$data_dir."/ --transform=\'s/".$sbj."//\' --exclude=\'fsaverage\' 2>/dev/null";
    system($order);
    unlink $tmp_tgz;
}

O, en su lugar podemos bajar solo los archivos stats y dejarlos en el mismo sitio, ejecutando algo como,

xget_fs_stats($conn{'HOST'}, $conn{'JSESSION'}, $subjects{$sbj}{'experiment'}, 'aseg.stats', $data_dir.'/stats/.aseg.stats');

Nota: Para bajar las aparc habria que hacer lo tanto para lh como para rh. Es decir,

xget_fs_stats($conn{'HOST'}, $conn{'JSESSION'}, $subjects{$sbj}{'experiment'}, 'lh.aparc.stats', $data_dir.'/stats/.lh.aparc.stats');
xget_fs_stats($conn{'HOST'}, $conn{'JSESSION'}, $subjects{$sbj}{'experiment'}, 'rh.aparc.stats', $data_dir.'/stats/.rh.aparc.stats');

De aqui la conversion a una tabla es trivial y esta implementada por completo en xnat_pullfs.pl

Incluso se puede bajar el experimento completo. La llamada

xget_dicom($conn{'HOST'}, $conn{'JSESSION'}, $subjects{$sbj}{'experiment'},$src_dir);

deja el DICOM original en el path $src_dir. Esto permite bajar el proyecto original en DICOM para ejecutar tareas que no se ejecuten automaticamente.

También podemos bajar los datos ajenos a XNAT que hemos almacenado como recursos. En este caso, estos datos van asociados (generalmente) al experimento. Asi que para obtener un hash con los datos del recurso my_res y guardados en el archivo my_ext_data.json, invocamos la funcion

my %ext_data = xget_res_data($conn{'HOST'}, $conn{'JSESSION'}, $experiment, 'my_res', 'my_ext_data.json');

Ejemplo completo de uso: Evaluando y guardando neurodegeneracion

Vamos a poner un ejemplo de como construir dos scripts desde cero utiliazndo las funciones de la biblioteca. el primero va a ser para guardar datos en XNAT y el segundo para leerlos. Pero antes vamos a definir y calcular los datos. En este caso voy a guardar la N, es decir, el indice de neurodegeneracion calculado para cada experimento.

Obteniendo y organizando datos

Aqui vamos a utilizar lo que ya tenemos hecho e integrado en el pipeline para obtener los datos necesarios. Esto es, edad y resultados del procesamiento de FS.

[osotolongo@brick03 mri_face]$ xnat_get_age.pl -x mriface
[osotolongo@brick03 mri_face]$ head mriface_age_data.csv
Subject_ID,AGE
20120884,68.3
20220716,86.1
20221062,80.8
20161142,75.3
20220933,73.5
20200214,81
20210031,75.2
20220785,70.6
20220512,74.5
[osotolongo@brick03 mri_face]$ xnat_pullfs.pl -s aseg -x mriface -o mriface_base_aseg.csv
[osotolongo@brick03 mri_face]$ xnat_pullfs.pl -s aparc -x mriface -o mriface_base_aparc.csv
[osotolongo@brick03 mri_face]$ join -t, mriface_base_aseg.csv mriface_base_aparc.csv > mriface_base.csv
[osotolongo@brick03 mri_face]$ (head -n 1 mriface_base.csv && tail -n +2 mriface_base.csv | sort -t,) > mriface_base_sorted.csv
[osotolongo@brick03 mri_face]$ (head -n 1 mriface_age_data.csv && tail -n +2 mriface_age_data.csv | sort -t,) > mriface_age_sorted.csv
[osotolongo@brick03 mri_face]$ join -t, mriface_base_sorted.csv mriface_age_sorted.csv > input_data.csv

Ahora me bajo el script que me hace falta y lo ejecuto,

[osotolongo@brick03 mri_face]$ wget https://raw.githubusercontent.com/asqwerty666/acenip/main/neuroass/nplus.r
--2022-11-25 11:17:06--  https://raw.githubusercontent.com/asqwerty666/acenip/main/neuroass/nplus.r
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2194 (2.1K) [text/plain]
Saving to: ‘nplus.r’
 
nplus.r                       100%[================================================>]   2.14K  --.-KB/s    in 0s
 
2022-11-25 11:17:06 (42.7 MB/s) - ‘nplus.r’ saved [2194/2194]
 
[osotolongo@brick03 mri_face]$ Rscript nplus.r
Loading required package: ggplot2
Loading required package: lattice
Loading required package: Hmisc
Loading required package: survival
 
Attaching package: ‘survival’
 
The following object is masked from ‘package:caret’:
 
    cluster
 
Loading required package: Formula
 
Attaching package: ‘Hmisc’
 
The following object is masked from ‘package:e1071’:
 
    impute
 
The following objects are masked from ‘package:base’:
 
    format.pval, units
 
null device
          1
null device
          1
null device
          1

El output es algo como,

[osotolongo@brick03 mri_face]$ head classifier_output.csv
Subject_ID,ND,Nprob
19990095,1,0.999306569453432
20070391,1,0.547690203532351
20080792,1,0.999997047769151
20090586,1,0.977614710937273
20110558,1,0.991139933061357
20120884,1,0.747207698029093
20140487,1,0.985199951162192
20141212,1,0.999863709247326
20150204,1,0.85089918263054

Guardando resultados

TL;DR: xnat_up_nd.pl

Para empezar. Supongamos que ya he calculado las probabilidades de neurodegeneracion y tengo ela rchivo con resultados. Lo siguiente que necesito es saber a que proyecto de XNAT corresponden. Esto lo puedo saber de dos maneras,

  • Directamente, como input del script, usando -x
  • Si los datos calculados corresponden a un projecto analizado con el pipeline, en la configuracion de este debe estar el proyecto XNAT asociado y se introduce con -p

Asi que el inicio es el de siempre,

use strict; use warnings;
my $prj;
my $xprj;
my $ifile;
@ARGV = ("-h") unless @ARGV;
while (@ARGV and $ARGV[0] =~ /^-/) {
        $_ = shift;
        last if /^--$/;
        if (/^-i/) { $ifile = shift; chomp($ifile);}
        if (/^-x/) { $xprj = shift; chomp($xprj);} #nombre del proyecto en XNAT
        if (/^-p/) { $prj = shift; chomp($prj);} #nombre local del proyecto
}

Y ahora compruebo que tengo los datos necesarios,

if ($prj and not $xprj) {
        my %pdata = load_project($prj);
        $xprj = $pdata{'XNAME'};
}
 
die "Should supply XNAT project name or define it at local project config!\n" unless $xprj;
die "No input data file\n" unless $ifile and -f $ifile;

Nota: La funcion load_project() es parte de la biblioteca NEURO4.pm y debe ser importada antes

Ahora, leemos el archivo de datos y lo almacenamos en un hash, hasta aqui todo es normalillo,

open IDF, "<$ifile";
while (<IDF>){
        if (/.*,\d,\d+\.\d+/){
                my ($sbj, $n, $p) = /(.*),(.*),(.*)/;
                $nass{$sbj}{'N'} = $n;
                $nass{$sbj}{'Nprob'} = $p;
        }
}
close IDF;

Y ahora viene la parte nueva,

Abrimos una conexion con XNAT y para cada sujeto de la lista, obtenemos el epxerimento correspondiente, creamos el resource, rellenamos un hash con los valores a escribir y lo subimos a su sitio.

my %xconf = xget_session();
foreach my $sbj (sort keys %nass){
        my $experiment = xget_mri($xconf{'HOST'}, $xconf{'JSESSION'}, $xprj, $sbj);
        my %ass_data = ('N' => $nass{$sbj}{'N'}, 'Nprob' => $nass{$sbj}{'Nprob'});
        xcreate_res($xconf{'HOST'}, $xconf{'JSESSION'}, $experiment, 'data');
        xput_res_data($xconf{'HOST'}, $xconf{'JSESSION'}, $experiment, 'data', 'neuroass.json', \%ass_data);
}

Ya esta, todos los resultados estan guardados. Vamos a comprobarlo, me bajo el JSON de un experimento cualquiera, digamos el 20200391

[osotolongo@brick03 mri_face]$ grep 20200391 classifier_output.csv
20200391,0,0.176295099461233

y hago,

[osotolongo@brick03 mri_face]$ cat neuroass.json | jq '.'
{
  "ResultSet": {
    "Result": [
      {
        "N": "0",
        "Nprob": "0.176295099461233"
      }
    ]
  }
}

8-)

Bajando los resultados

TL;DR: xnat_get_nd.pl

Ya tengo como guardar los resultados en XNAT. Pues ahora me toca bajarmelos. La primera parte es mas o menos la misma, excepto que en lugar de pedir un archivo de input, debo pedir uno de output.

use strict; use warnings;
my $prj;
my $xprj;
my $ofile;
@ARGV = ("-h") unless @ARGV;
while (@ARGV and $ARGV[0] =~ /^-/) {
        $_ = shift;
        last if /^--$/;
        if (/^-o/) { $ofile = shift; chomp($ofile);}
        if (/^-x/) { $xprj = shift; chomp($xprj);} #nombre del proyecto en XNAT
        if (/^-p/) { $prj = shift; chomp($prj);} #nombre local del proyecto
}
if ($prj and not $xprj) {
        my %pdata = load_project($prj);
        $xprj = $pdata{'XNAME'};
}
 
die "Should supply XNAT project name or define it at local project config!\n" unless $xprj;

Ahora, tengo que sacar el listado de sujetos a los que voy a interrogar,

my %xconf = xget_session();
my %subjects = xget_subjects($xconf{'HOST'}, $xconf{'JSESSION'}, $xprj);

y ahora voy a bajar los archivos para cada uno y parsearlos. Pero antes voy a abrir el archivo de output. Lo que voy a hacer es redirigir el STDOUT a ese archivo, de manera que si no me das un nombre de archivo, imprimo los resultados en la pantalla. Sucio pero te servira para acordarte de poner el nombre de output para la proxima :-P

open STDOUT, ">$ofile" unless not $ofile;
print "Subject_ID,N,Nprob\n";

Ahora, para cada sujeto he de obtener el experimento y capturar los datos en el recurso asociado,

        my $experiment = xget_mri($xconf{'HOST'}, $xconf{'JSESSION'}, $xprj, $sbj);
        my %nass_data = xget_res_data($xconf{'HOST'}, $xconf{'JSESSION'}, $experiment, 'data', 'neuroass.json');

y antes de escribir, tambien necesitare, el label del sujeto,

        my $label = xget_sbj_data($xconf{'HOST'}, $xconf{'JSESSION'}, $sbj, 'label');
        if (exists($nass_data{'N'}) and exists($nass_data{'Nprob'})){
                print "$label,$nass_data{'N'},$nass_data{'Nprob'}\n";
        }else{
                print "$label,NA,NA\n";
        } 

Y ya esta. Hemos subido y bajado los datos necesarios.

neuroimagen/pipeline2xnat_api.txt · Last modified: 2022/12/06 15:45 by osotolongo