# Bruker::Utils package; parsing for Bruker data format
#
# $Id: Utils.pm,v 1.4 2004/11/10 19:39:58 matthewbrett Exp $
package Bruker::Utils;
use Carp;
use File::Basename;
use Math::Matrix 0.4;
require Exporter;
@ISA = qw( Exporter );
@EXPORT = qw( );
@EXPORT_OK = qw(bruker_series bruker_find_dir parse_bruker_params
bruker2generic bruker_transposition
bruker_text_headers best_of);
use strict;
use vars qw($VERSION );
$VERSION = 0.18;
# Bruker text files to load, with error messages
my($subjfile) = [["subject"],
"no subject info - minor problem"];
# Format; file names to try, message if missing, flag=1 if fatal
my(@paramfiles) = (
[["%d/imnd", "%d/method"],
"input params - transformation matrix will fail", 0],
[["%d/acqp"],
"output params - expect problems with size, timing etc", 0],
[["%d/pdata/%d/reco"],
"reco params - cannot do conversion", 1],
[["%d/pdata/%d/d3proc"],
"image display params - used if other files are missing", 0],
);
# option defaults
my(%text_opt_defs) = (
'anon', 1,
);
sub bruker_series{
# gets series nos from Bruker directory
my $pv_dir = shift;
my(@flist, $f, @seriesnos);
@flist = <${pv_dir}/[0-9]*>;
foreach $f(@flist) {
if (-d $f) {
$f =~ /.*?(\d+)$/;
@seriesnos = (@seriesnos, $1);
}
}
return @seriesnos;
}
sub bruker_find_dir{
# does check / find for Bruker directory
my ($pv_start, $one_multf, $options, @rest) = @_;
my @searchpath = @rest ? @rest :
defined($options->{dbpath}) ? @{$options->{dbpath}} :
();
my ($pv_name, $pv_path, $pv_ext, $spath,
@flist, $s_ext, $s_targ, @dirlist);
$pv_start =~ s/\/$//; # strip trailing path sep
($pv_name, $pv_path, $pv_ext) = fileparse($pv_start,'\..*');
if ($pv_path eq './' && $pv_start !~ "^./") { # path not given
# check each search directory, last first
@searchpath = ('.', @searchpath);
} else {
@searchpath = $pv_path;
}
@searchpath = reverse(@searchpath);
# add asterisk for search
$s_ext = $pv_ext eq "" ? "*" : $pv_ext;
SEARCHLOOP: foreach $spath(@searchpath) {
$s_targ = "$spath/$pv_name$s_ext";
print "Searching for $s_targ...\n" if $options->{verbose};
if ($pv_ext ne "") { # we know what we are looking for
if (-d $s_targ) {
push(@dirlist, $s_targ);
last SEARCHLOOP if $one_multf;
}
} else { # use input as root file name
@flist = ();
foreach (glob($s_targ)) {
push(@flist, $_) if (-d);
}
if (@flist) {
@dirlist = (@dirlist, @flist);
last SEARCHLOOP if $one_multf;
}
}
}
return (\@dirlist, \@searchpath);
}
# get header files info
sub bruker_text_headers{
my($pv_dir, $seriesno, $recono, $in_opts) = @_;
my(%options) = %text_opt_defs;
foreach (keys(%$in_opts)) {
$options{$_} = $in_opts->{$_};
}
my($pfile, $file_loc, $bhstr, $warning, $hdrdata, $ptry, $foundf);
local $/;
# add subject info if required
@paramfiles = ($subjfile, @paramfiles) if (!$options{anon});
# initialize file string
$hdrdata = "";
$warning = "";
# cycle through the files passed
PFILE: foreach $pfile(@paramfiles) {
$foundf = 0;
PFILETRY: foreach $ptry(@{$pfile->[0]}) {
# do variable substitution
$file_loc = sprintf($ptry, $seriesno, $recono);
$bhstr = $pv_dir . "/" . $file_loc;
if (open(TD,"<$bhstr") ) {
$foundf = 1;
last PFILETRY;
}
}
unless ($foundf) {
if ($pfile->[2]) { # abort
$warning .= "No $file_loc file; $pfile->[1]; aborting...\n";
$hdrdata = undef;
last PFILE;
} else {
$warning .= "No $file_loc file; $pfile->[1]; continuing...\n";
next PFILE;
}
}
# Read input files as one long record.
{
local $/;
$hdrdata=$hdrdata .
;
}
close TD;
}
# dos2unix strip
$hdrdata =~ s/\r//sg if $hdrdata;
return($hdrdata, $warning);
}
# Parse Bruker parameter file (passed as string) and return as hash
sub parse_bruker_params{
my($hdrdata) = shift;
my($i, $j, $k, $l, $record, $fname, $fval, @arr, @arels, %fields);
# strip out vis sections
$hdrdata =~ s/(^\$\$ \@vis=(.|\n)*?)(^\#\#)/$3/mg;
# to records
foreach $record (split(/\#\#/, $hdrdata)){
($fname, $fval) = split(/=/,$record);
if (defined($fname)) {
if ($fname =~ s/^\$// ) {
chomp($fval);
if ($fval ne "") {
if ($fval =~ s/\((.*)\)\n//) {
@arels = split(/,/, $1);
if ($fval =~ /^<(.*)>$/) {
$fields{$fname} = $1;
}
else {
# deals with 1D or 2D or 3D arrays
@arr = split(/\s+/, $fval);
if (@arels==3){ # 3D
# fills rowwise - is this correct?
$l=0;
for ($i=0;$i<$arels[0];$i++){
for ($j=0;$j<$arels[1];$j++){
for ($k=0;$k<$arels[2];$k++){
$fields{$fname}[$i][$j][$k]=$arr[$l];
$l++;
}
}
}
} elsif (@arels==2) { # 2D
# fills rowwise - is this correct?
$k=0;
for ($i=0;$i<$arels[0];$i++){
for ($j=0;$j<$arels[1];$j++){
$fields{$fname}[$i][$j]=$arr[$k];
$k++;
}
}
} else {
$fields{$fname} = [@arr];
}
}
} else { # not an array
$fields{$fname}=$fval;
}
}
}
}
}
return %fields;
}
# parses Bruker header into a generic format. When adding a format,
# why not add it here, in case your calculations are useful to the
# rest of us? The Bruker header is a bit confusing. Here's some
# terms to start. Each time a subject gets into the scanner and gets
# scanned, is a 'session'. Each individual scan, or time series, is a
# 'series'. A series may be 3d (volume acquisition) or 2d (slice
# acquisition). For each volume (3d) or slice (2d), there may be more
# than one scan acquired. For example, the T2/PD scans are two images
# (for each slice) acquired during the same scan (=series) I will term
# this as a 'layer', so that a normal strucutural has one 'layer',
# whereas the T2/PD scan has two layers. These layers are combined in
# the reconstructed data file (i.e interleaved - volume by volume in
# the 3D case, and slice by slice in the 2D case). The total number
# of images for one volume (slices * layers for 2D, layers for 3D), I
# will term the number of 'images' for that scan. The reconstruction
# can output images with the read/phase or read/slice, phase/slices
# dimensions transposed (this transposition is _not_ reflected in the
# image dimensions from the RECO file, but it _is_ reflected in the
# dimensions in the d3proc file). Each image can be differentially
# transposed. See the paravision documents for more details, and some
# confusing parameter names.
# conversion between Bruker and other datatypes
my(%dataconv) = (
'_8BIT_UNSGN_INT', => {'bytes' => 1,
'd3no' => 2, #?
'min' => 0,
'max', => 255,
'anatype' => 2,
'mnctype' => '-unsigned -byte'},
'_16BIT_SGN_INT' , => {'bytes' => 2,
'd3no' => 3,
'min' => -(2**15),
'max', => (2**15-1),
'anatype' => 4,
'mnctype' => '-signed -short'},
'_32BIT_SGN_INT' , => {'bytes' => 4,
'd3no' => 5,
'min' => -(2**31),
'max', => (2**31-1),
'anatype' => 8,
'mnctype' => '-signed -long'}
);
# RECO transpose coding - see Image_Re.pdf
#1. If (RECO_transposition[i] = 0), no transposition is to be done.
#
#2. If (0 < RECO_transposition[i] < ACQ_dim), the two consecutive data
#directions given by RECO_transposition[i] and RECO_transposition[i] +
#1 will be transposed.
#
#3. If (RECO_transposition[i] = ACQ_dim), the first and final data
#directions will be transposed.
#
#Please note that for ACQ_dim = 2 the two values 1 and 2 are both legal
#for RECO_transposition[i] and will have the same effect; the first and
#second dimensions will be transposed.
sub bruker_transpose {
my($bhdr, $layer) = @_;
my $code = $bhdr->{'RECO_transposition'}[$layer];
return [0, 0, 'none'] if ($code == 0);
my $is2d = ($bhdr->{ACQ_dim} == 2);
return [0, 1, 'read/phase'] if ($is2d || $code == 1);
return [1, 2, 'phase/slice'] if ($code == 2);
return [0, 2, 'read/slice'];
}
# option defaults
my(%opt_defs) = (
'quiet', 1,
'verbose', 0,
'frecofix',1,
'layers', 1
);
sub bruker2generic{
my ($bhdr, $inopts) = @_;
my %options = %opt_defs;
foreach (keys(%$inopts)) {
$options{$_} = $inopts->{$_};
}
my $i4 = Math::Matrix->eye(4);
my $swaps = $i4->new([-1, 0, 0, 0],
[0, -1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]);
my ($key, $code, @t, $tmp, $bruk_trans, @acq_dim);
my ($start, $ft2mm, $trans, $recoff, $so_far, $rotn, $vx2mm, $radio);
my (@ftdim, $is2d, @fov, @vox, $vox, $off, $i, $j, $gradmat, @vector);
my %ghdr = ();
my $i1 = 0;
# get the dimensionality of the acquisition
$ghdr{dimensions} = @{$bhdr->{'RECO_size'}};
if ($ghdr{dimensions} < 2) {
unless ($options{quiet}) {
warn "Do not know how to handle data of less than two dimensions";
}
}
# fill 2D or 3D worth of dimensions etc
$ghdr{dim} = [@{$bhdr->{'RECO_size'}}];
$ghdr{fov} = [@{$bhdr->{'RECO_fov'}}];
# voxel dimensions in mm
foreach $i (0..($ghdr{dimensions}-1)) {
$ghdr{vox}[$i] = $ghdr{fov}[$i] * 10 / $ghdr{dim}[$i];
}
# slice separation mode, see below
$ghdr{sl_mode} = $bhdr->{'ACQ_slice_sepn_mode'};
# test to see if we have an ACQP file
if (defined($ghdr{sl_mode})) {
# We have an ACQ file
if ($ghdr{dimensions} < 3) {
# This is a 2D image
# Therefore we still need slice information
$ghdr{dim}[2] = $bhdr->{'NSLICES'};
$ghdr{sl_thick} = $bhdr->{'ACQ_slice_thick'};
$ghdr{vox}[2]=$bhdr->{'ACQ_slice_thick'};
# slices can be equidistant, contiguous, var parallel
# or var angle. The slice separation as a single no
# only makes sense in the non Var case
if ((($ghdr{sl_mode} eq "Equidistant") ||
($ghdr{sl_mode} eq "Contiguous"))
&& defined($bhdr->{'ACQ_slice_sepn'})) {
$ghdr{sl_sepn} = $bhdr->{'ACQ_slice_sepn'}[0];
$ghdr{vox}[2] = $ghdr{sl_sepn} if ($ghdr{sl_sepn} > 0);
}
} else { # 3D
$ghdr{sl_thick}= $ghdr{vox}[2];
$ghdr{sl_sepn} = $ghdr{sl_thick};
}
if ($ghdr{sl_sepn}) {
$ghdr{sl_gap} = $ghdr{sl_sepn} > 0 ?
$ghdr{sl_sepn} - $ghdr{sl_thick} : 0;
} else {
$ghdr{sl_gap} = $ghdr{sl_thick};
}
# NI is the total number of images (=images) acquired in a
# repetition so, for a 2D volume, and one layer (e.g. EPI),
# this will be equal to the number of slices. If there are
# two layers, NI will be equal to the NSLICES *2. For a
# 3D volume, NSLICES should I think be 1.
$ghdr{image_nr}= $bhdr->{'NI'};
$ghdr{layer_nr} = $ghdr{image_nr} / $bhdr->{'NSLICES'};
# no of volumes
$ghdr{dim}[3]=best_of( $bhdr->{'ACQ_nr_completed'}, 1);
# the TR. This seems to be an array, with one element
# for each multiplex step. We'll take the first I guess.
$ghdr{vox}[3]=$bhdr->{'ACQ_repetition_time'}[0];
# the slice acquisition order. Well in fact the order of
# acquisition of each 2D or 3D image, ie for a 2D scan, with two
# layers and 10 slices, this array will have 20 elements
$ghdr{objorder} = [@{$bhdr->{'ACQ_obj_order'}}];
} else {
# no ACQ, let's try our best, maybe using the d3proc file
# guess no of layers
$ghdr{layer_nr} = 1;
# no of images (as for NI from acqp). These could be different layers,
# could be slices, could be both, we don't know without the ACQP file
$ghdr{image_nr}= @{$bhdr->{'RECO_transposition'}};
if ($ghdr{dimensions} < 3) {
# we don't know how many slices, let's guess that the NI-like parameter
# above actually refers to no of slices
$ghdr{dim}[2] = $ghdr{image_nr};
}
# maybe the total number of volumes can be calculated from the d3proc file
if (!defined($bhdr->{IM_SIZ})) {
# we do seem to have a d3proc
# the following apparently overcomplex expression is to
# get round the situation where the dimensions have been
# transposed, which will be reflected in the d3proc file
my($nslices)=$bhdr->{IM_SIX} * $bhdr->{IM_SIY}* $bhdr->{IM_SIZ}
/ $ghdr{dim}[0] / $ghdr{dim}[1]
* $bhdr->{IM_SIT};
$ghdr{dim}[3]=$nslices / $ghdr{dim}[2];
} else {
# we have to guess at no of volumes
$ghdr{dim}[3]=1;
}
}
# set slice FOV if not already set
if (!defined($ghdr{fov}[2])) {
if (!defined($ghdr{vox}[2])) {
$ghdr{fov}[2] = 1; # no slice volume info
} else {
$ghdr{fov}[2] = $ghdr{vox}[2] * $ghdr{dim}[2] / 10;
}
}
# set use of layers from command line option
if (!$options{layers}) {
if ($ghdr{layer_nr} > 1) {
unless ($options{quiet}) {
warn "Reset layer number to 1 from $ghdr{layer_nr}\n";
}
$ghdr{dim}[$ghdr{dimensions}] *= $ghdr{layer_nr};
$ghdr{layer_nr} = 1;
}
$ghdr{dim}[3] = $options{timelength};
}
# set no of time points from command line option
if ($options{timelength}) {
$ghdr{dim}[3] = $options{timelength};
}
# transposition. For each image, and therefore for each slice
# for a 2D experiment, the image can have its dimensions
# transposed in the output reconstructed image. This is dictated
# by the REC0_transposition parameter, which has one value for
# each layer for a 3D series, and one value for each layer for
# each slice for a 2D series. This transposition is not yet
# reflected in the FOV and image dimensions we have recorded
# above. Calculate the transposed dimensions for each layer and store.
# The paravision parameter is called RECO_transpose_dim in the docs, but not
# in the actual reco files ? version change
my($l, $r, $oimgno);
foreach $l (0..$ghdr{layer_nr}-1) {
$ghdr{transpose}[$l]{code}=$bhdr->{'RECO_transposition'}[$l];
for ($r=$l+$ghdr{layer_nr};$r<$ghdr{image_nr}-1;$r+=$ghdr{layer_nr}){
if ($bhdr->{'RECO_transposition'}[$r] && # maybe we have not enough tranposes
($ghdr{transpose}[$l]{code}!=$bhdr->{'RECO_transposition'}[$r])) {
unless ($options{quiet}) {
warn ("Different transpositions for the same image in ".
"the same volume: confusing, expect ugliness");
}
}
}
# work out the transposed dimensions
$ghdr{transpose}[$l]{dim} = [@{$ghdr{dim}}];
$ghdr{transpose}[$l]{vox} = [@{$ghdr{vox}}];
$ghdr{transpose}[$l]{fov} = [@{$ghdr{fov}}];
$bruk_trans = bruker_transpose($bhdr, $l);
# may need to transpose dimensions
swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{dim});
swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{vox});
swapels(@{$bruk_trans}[0,1],$ghdr{transpose}[$l]{fov});
$ghdr{transpose}[$l]{descrip} = $bruk_trans->[2];
# now set the 4x4 transformation matrix
# check that position parameters are defined
# this code needs generalizing
if (best_of(
$bhdr->{IMND_read_offset},
$bhdr->{PVM_ReadOffset},
$bhdr->{PVM_SPackArrReadOffset}) &&
$bhdr->{ACQ_fov}) {
# Compile matrix
$is2d = (@{$bhdr->{ACQ_fov}} == 2);
$start = $i4->clone();
# transposition
$tmp = $start->[$bruk_trans->[0]];
$start->[$bruk_trans->[0]] = $start->[$bruk_trans->[1]];
$start->[$bruk_trans->[1]] = $tmp;
# reco offset
$recoff = $i4->clone();
$recoff->[0][3] = $bhdr->{RECO_offset}[0][$l];
$recoff->[1][3] = $bhdr->{RECO_offset}[1][$l];
if ($is2d) {
$recoff->[2][3] = 0;
} else {
$recoff->[2][3] = $bhdr->{RECO_offset}[2][$l];
}
# ft voxels -> mm
$ft2mm = $i4->clone();
@ftdim = @{$bhdr->{RECO_ft_size}};
@acq_dim = @{$bhdr->{ACQ_fov}};
if ($is2d) {
push(@ftdim, $bhdr->{NSLICES});
push(@acq_dim, $ghdr{fov}[2]);
}
foreach $i(0..2) {
$vox = $acq_dim[$i] / $ftdim[$i] * 10;
$off = ($ftdim[$i]+1)/2 * -$vox;
$ft2mm->[$i][$i] = $vox;
$ft2mm->[$i][3] = $off;
}
# translation offset in read/phase/slice
$trans = $i4->clone();
if (defined($bhdr->{IMND_read_offset})) {
$trans->[0][3] = $bhdr->{IMND_read_offset}[0];
$trans->[1][3] = best_of(
$bhdr->{IMND_phase1_offset}[0],
$bhdr->{ACQ_phase1_offset}[0],
0);
$trans->[2][3] = $bhdr->{IMND_slice_offset};
} else {
$trans->[0][3] = mean(@{
best_of($bhdr->{PVM_ReadOffset},
$bhdr->{PVM_SPackArrReadOffset})
});
$trans->[1][3] = mean(@{
best_of($bhdr->{PVM_Phase1Offset},
$bhdr->{PVM_SPackArrPhase1Offset})
});
$trans->[2][3] = mean(@{
best_of($bhdr->{PVM_SliceOffset},
$bhdr->{PVM_SPackArrSliceOffset})
});
}
# gradient matrix - is transposed
# There is one (3x3) gradient matrix for each slice - see Para_Cla.pdf in the Paravision documentation
# Quoting:
# ACQ_grad_matrix - is a three dimensional array of doubles to describe the
# orientation of the images to be measured.
# � The fast dimension has three items to describe gradient vectors for the Gx/
# Gy/Gz coordinate system. The gradient vectors should also describe a
# unit sphere with a radius of 1.0.
# � The medium dimension has also three items to describe the read-, phase-
# and slice gradient.
# � The slow dimension is used to describe all slices to be measured.
$rotn = $i4->clone();
$gradmat = $bhdr->{ACQ_grad_matrix}[0];
for $i(0..2){
for $j(0..2){
$rotn->[$i][$j] = $gradmat->[$j][$i];
}
}
# whole thing
$vx2mm = $rotn * $trans * $swaps * $ft2mm * $recoff * $start;
# do report
if ($options{showmat}) { # very verbose
$start->print("Tranposition\n");
$so_far = mat_so_far("Reco offset",$recoff, $start);
$so_far = mat_so_far("fft to mm",$ft2mm, $so_far);
$so_far = mat_so_far("Swap xy",$swaps, $so_far);
$so_far = mat_so_far("Translations",$trans, $so_far);
$so_far = mat_so_far("Rotations",$rotn, $so_far);
}
# do radiological conversion if nec
if ($options{radio}) {
$radio = $vx2mm->eye(4);
$radio->[0][0] *= -1;
$vx2mm = $radio * $vx2mm;
if ($options{verbose}) {
print "Converting matrix to radiological orientation\n";
}
}
# and into generic header
$ghdr{mat}[$l] = $vx2mm;
} else {
if ($options{verbose}) {
print "Can't find the data for orientation matrix\n"
}
}
}
# data type
my($datatype);
if ($bhdr->{'RECO_wordtype'}){
$datatype = $bhdr->{'RECO_wordtype'};
} else {
# try and get datatype from d3proc file
if ($bhdr->{DATTYPE}) {
$datatype = $dataconv{$bhdr->{DATTYPE}}{d3no};
}
}
$ghdr{datatype} = {%{$dataconv{$datatype}}};
$ghdr{datatype}{name} = $datatype;
# the size in bytes of the images (potentially) interleaved
$ghdr{layersize} = 1;
foreach $i (0..$ghdr{dimensions}-1) {
$ghdr{layersize} *= $ghdr{dim}[$i];
}
$ghdr{layersize} *= $ghdr{datatype}{bytes};
# endianness of reconstructed data
$ghdr{"endian"} = ($bhdr->{RECO_byte_order} eq 'littleEndian') ?
'le':'be';
# there's an endian problem with FRECO reconstructions
# They report littleEndian, when in fact they are bigEndian
# So, if the raw data is bigEndian, and the reco data is littleEndian,
# then assume this is a FRECO reconstruction, and fix
if ($options{frecofix} &&
($bhdr->{'BYTORDA'} =~ /big/) &&
($ghdr{"endian"} eq "le")) {
if (!$options{quiet}) {
print "Fixing FRECO endian -> bigEndian\n";
}
$ghdr{"endian"} = "be";
}
# some details
if ($bhdr->{'ACQ_time'}){
$bhdr->{'ACQ_time'} =~ /(..:..:..)\s*(\w*\s*\w*\s*\w*)/;
$ghdr{time_of_scan} = $1;
$ghdr{date_of_scan} = $2;
}
# image type
$ghdr{iscomplex}=0;
if ($bhdr->{RECO_image_type}) {
$ghdr{image_type}=$bhdr->{RECO_image_type};
$ghdr{image_type}=~s/_IMAGE//;
if ($ghdr{image_type}=~ /COMPLEX/) {
$ghdr{iscomplex}=1;
}
}
# entire data block max/min values
if ($bhdr->{RECO_maxima}) {
$ghdr{blockmax} = $ghdr{datatype}{min};
foreach $i (@{$bhdr->{RECO_maxima}}) {
$ghdr{blockmax} = $i if ($i > $ghdr{blockmax});
}
}
if ($bhdr->{RECO_minima}) {
$ghdr{blockmin} = $ghdr{datatype}{max};
foreach $i (@{$bhdr->{RECO_minima}}) {
$ghdr{blockmin} = $i if ($i < $ghdr{blockmin});
}
}
return %ghdr;
}
sub swapels {
my($elno1, $elno2, $rarr) = @_;
my($tmp) = $rarr->[$elno1];
$rarr->[$elno1] = $rarr->[$elno2];
$rarr->[$elno2] = $tmp;
}
sub mean {
my($i, $s);
$s = 0;
foreach $i(@_) {
$s += $i;
}
return ($s / @_);
}
sub best_of {
my $a;
A: while (@_) {
$a = shift;
last A if (defined($a));
};
return $a;
}
sub mat_so_far {
my($label, $in, $sofar) = @_;
$in->print("$label\n");
$sofar = $in * $sofar;
$sofar->print("Result so far\n");
return $sofar;
}
1; |