Code covered by the BSD License  

Highlights from
Reading Philips MR Physiolgy logfiles

image thumbnail
from Reading Philips MR Physiolgy logfiles by Paul Groot
ReadPhilipsScanPhysLog.m reads Philips scanphyslog files, which are created by Philips MRI scanners.

ReadPhilipsScanPhysLog(filename, channels, skipprep)
function [ DATA HDR ] = ReadPhilipsScanPhysLog(filename, channels, skipprep)
%READPHILIPSSCANPHYSLOG load Philips MRI physiolog file
%
%   [ DATA HDR ] = READPHILIPSSCANPHYSLOG(FILENAME)
%   Reads the sample information (all 10 channels) from FILENAME. The 
%   samples are loaded as signed 32-bit integer values (int32). Sampling
%   rate is 500Hz. (i.e. 500 lines in the logfile per second scanning time)
%
%   [ DATA HDR ] = READPHILIPSSCANPHYSLOG(FILENAME, CHANNELS)
%   Also read the log data, but only loads the signals specified by cell
%   structure CHANNELS. {v1raw, v2raw, v1, v2, ppu, resp, gx, gy, gz, mark}
%   Use [] or 'all' to load all channels (default), or an arbitrary invalid
%   name (i.e. 'none') to ignore the channel samples and only load markers.
%
%   [ DATA HDR ] = READPHILIPSSCANPHYSLOG(FILENAME, CHANNELS, SKIPPREP)
%   Also read the log data and specified channels, but skip preperation
%   fase (marked by empty comment (#) line)
%
%   DATA    is a structure with elements for the channel matrix C, 
%           marker table M and event index vectors I. 
%   DATA.C  32-bit integer matrix containing column vectors for
%           all resp. channels values. (i.e. each signal channel is stored 
%           as column vector). Note that the mark column contains
%           hexadecimal values (this will always be the last column).
%   DATA.M  is a 2-column table containing {marker,index} pairs of the
%           the markers detected in the marker channel. The first
%           column of M contains the mark values >0, and the second column 
%           contains the sample indices of the corresponding mark-value 
%           in the first column. Markers are bit-encoded values (i.e. each
%           event type corresponds to a single bit position). Currently the
%           following bits are known:
%                mark      bit pattern         description
%               0x0001 = 0000.0000.0000.0001 = VCG wave onset (R-peak)
%               0x0002 = 0000.0000.0000.0010 = PPU wave onset
%               0x0004 = 0000.0000.0000.0100 = ?Respiration?
%               0x0008 = 0000.0000.0000.1000 = slice onset
%               0x0010 = 0000.0001.0000.0000 = start of scan sequence (decimal 16)
%               0x0020 = 0000.0010.0000.0000 = end of scan sequence (decimal 32)
%           Note that events can occur at the same sample, so bits can be
%           combined and give marker values like 3 (=1+2). The events in
%           DATA.I are extracted from DATA.M by bit masking, so all event
%           index vectors include overlapping events properly.
%
%   DATA.I  a structure holding index vectors for all known event types.
%           The event indices are derived by scanning the bit-pattern
%           in the marker column.
%   DATA.I.ScannerStart vector with sample indices of start of MRI sequence (incl. preparation)
%   DATA.I.ScannerStop  vector with sample indices of end of MRI sequence
%   DATA.I.VcgOnset     vector with sample indices of gated events
%   DATA.I.PpuOnset     vector with sample indices of ... events
%   DATA.I.SliceOnset   vector with sample indices of slice onset events
%   DATA.I.Mark4        vector with sample indices of mark4 (Respiration related?)
%
%   HDR     Optional return value for a structure containing all
%           information in the file header. 
%   HDR.ID
%   HDR.ID.Site         site name (from first line)
%   HDR.ID.Release      software release name
%   HDR.ID.SWID         software number
%   HDR.DATETIME        year,month,date,hour,min,sec
%   HDR.STATS           statistics from line 3
%   HDR.DockableTable   true/false
%   HDR.COLUMN_NAMES    resp. signal names: {v1raw, v2raw, v1, v2, ppu, 
%                                           resp, gx, gy, gz, mark}
%
%   Examples:
%
%   D = ReadPhilipsScanPhysLog('SCANPHYSLOG20110420142406.log');
%   Reads all physiological channels and marker flags. The marker index
%   information will automatically put into D.M and D.I.
%
%   D = ReadPhilipsScanPhysLog('SCANPHYSLOG20110420142406.log',{'v1','v2','resp'});
%   Reads both artefact corrected VCG and respiration channels.  The marker index
%   information will automatically put into D.M and D.I.
%
%   D = ReadPhilipsScanPhysLog('SCANPHYSLOG20110420142406.log','none');
%   Ignore channels, and only load markers (i.e. DATA.C==[])
%
%   [D H] = ReadPhilipsScanPhysLog('SCANPHYSLOG20110420142406.log');
%   Also parses the header and stores info in H.
%
%   Using start/stop markers to extract epochs:
%
%       beginindex = D.M(find(D.M(:,1)==16),2);
%       endindex   = D.M(find(D.M(:,1)==32),2);
%       epoch = D.C(beginindex:endindex,:);
%
%   However, the start marker is not properly synchronized with the onset of the 
%   first volume. As a workaround you could use the end-marker and real duration of 
%   the scan and calculate the start index yourself:
%
%         freq=500;
%         TR=2.3;
%         nrvolumes=200;
%         endindex = D.M(find(D.M(:,1)==16),2);
%         beginindex = endindex  - nrvolumes * TR * freq;
%         epoch = logdata.C(beginindex:endindex,:);
%
%   Compatability
%   Developed and tested with Matlab R2009a (7.8.0) - Windows 64-bit.
%   bitand behaviour was changed in Version 7 (R14), so you might have
%   compatibility issues with release before R14.
%
% Copyright 2011 Academical Medical Center Amsterdam
% Created by Paul F.C. Groot
% $Rev:: 172        $:  Revision of last commit 
% $Author:: paul   $:  Author of last commit 
% $Date:: 2012-09-27 #$:  Date of last commit 

%%
% Additonal info from:
% http://dbic.dartmouth.edu/wiki/index.php/Recording_Physiology
% The onset of the file corresponds to 15 seconds prior to the start of the
% scan. There is a single line containing a hash mark (#) and no numbers
% that serves as a time-stamp of the onset of the actual scan. The last
% line of the file corresponds to the exact end of the scan. Note: For
% functional scans, the # mark corresponds to the onset of the template,
% not the onset of the functional run itself, and recording continues
% during any amount of time that passes between the end of the template
% scan and the onset of the actual functional run (which is variable).
% Therefore, it is advisable to time-lock physiological response data
% during functional scans based on when the scan ENDS.
 
    %% Initialize
    if nargin<2
        channels = {};
    elseif ~iscell(channels)
        channels = { channels };
    end

    if nargin<3 || isempty(skipprep)
        skipprep = false;
    end
    
    bParseHeader = nargout>=2;
       
    % open philips log file, and parse all lines
    fid=fopen(filename,'rt');
    if fid==-1
        error('Couldn''t open %s',filename);
    end
    
    %% fill HDR (read header)
    % the file starts with a header, like this one:
    % ## AMC Amsterdam, Release hestia (SWID 118)
    % ## Wed 20-04-2011 14:24:06
    % ## 179 -1351 306 171 -567 266 -30 375 0
    % ## Dockable table = FALSE
    % # v1raw v2raw  v1 v2  ppu resp  gx gy gz mark
    
    % define regular expressions for the resp. header lines
    if bParseHeader
        p{1} = '##\s*([\s\w]*),\sRelease\s(\w+)\s\(SWID (\d+)\)$';
        p{2} = '##\s*[\s\w]*(\d\d)-(\d\d)-(\d\d\d\d)\s(\d\d):(\d\d):(\d\d)$';
        p{3} = '##\s*([\s\-\d])+$';
        p{4} = '##\s*Dockable table\s*=\s*(\w+)$';
    %   p{5} = '#\s*(\w+)...'; See note below '#'
        iPattern = 1;
    end
    
    H = cell(4,1);      % this will hold the results of regexp parsing
    COLUMN_NAMES = [];  % signal name from last header line

    % read all header lines (starting with ## or #) until single #
    while ~feof(fid)
        str = fgetl(fid);
        if strncmp(str,'##',2)
            if bParseHeader
                T = regexp(str, p{iPattern}, 'tokens');
                if length(T)~=1 
                    warning('SCANPHYSLOG:invalidHeader','Invalid header at line 1: %s', str); 
                elseif iPattern<=length(p) 
                    H{iPattern}=T{1}; 
                    iPattern=iPattern+1; % continue with next pattern if current one matched
                else
                    warning('SCANPHYSLOG:invalidHeader','Ignoring additional header line: %s', str);
                end;
            end
        elseif strncmp(str,'#',1)
            % this last header line is parsed a bit differently: this way we can
            % continue without error if the file has a different number of
            % channels (butthe marker should always be the last...)
            COLUMN_NAMES = regexp(str(2:end),'\w+','match'); 
            break;
        else
            error('Invalid header');
        end
    end

    % assemble the returned HDR structure when requested
    if bParseHeader
        % translate file identification info from first header line into struct
        if length(H)>=1 && ~isempty(H{1})
            T = H{1};
            ID = struct('Site', T{1}, 'Release', T{2}, 'SWID', str2double(T{3}) );
        else
            ID = struct('Site', 'Unknown site', 'Release', 'unknown release', 'SWID', 0 );
        end

        % translate scan data/time info from second header line into struct
        if length(H)>=2 && ~isempty(H{2})
            T = H{2};
            DATETIME = struct(  'year',     uint16(str2double(T{3})), ... 
                                'month',    uint16(str2double(T{2})), ...
                                'day',      uint16(str2double(T{1})), ...
                                'hour',     uint16(str2double(T{4})), ... 
                                'min',      uint16(str2double(T{5})), ...
                                'sec',      uint16(str2double(T{6})) ...                                 
                            );
        else
            DATETIME = struct(  'year',     uint16(1900), ... 
                                'month',    uint16(1), ...
                                'day',      uint16(1), ...
                                'hour',     uint16(0), ... 
                                'min',      uint16(0), ...
                                'sec',      uint16(0) ...                                 
                            );
        end

        % translate signals statistics from thirth header line into struct
        if length(H)>=3 && ~isempty(H{3})
            T = H{3};
            STATS = sscanf(T{1},'%d')';
        else
            STATS = NaN .* zeros(1,9);
        end

        % translate DockableTable flag from fourth header line into struct
        if length(H)>=4 && ~isempty(H{4})
            T = H{4};
            DockableTable = strcmpi(T,'TRUE');
        else
            DockableTable = false;
        end

        HDR = struct('ID', ID, 'DATETIME', DATETIME, 'STATS', STATS, ...
            'DockableTable', DockableTable, 'COLUMN_NAMES', COLUMN_NAMES);
    end

    %% skip preparation?
    if skipprep
        startpos = ftell(fid); % remember start position in ase we didn't find a #
        while ~feof(fid)
            str = fgetl(fid);
            if strcmp(strtrim(str),'#')
                break;
            end
        end
        if feof(fid)
            warning('SCANPHYSLOG:prepNotFound','Preparation fase not found: reading all samples');
            fseek(fid,startpos,'bof'); % go back to where we came from
        end
    end
 
    %% fill DATA by loading requested columns
    % Create a logical vector S which is true for selected channels. This
    % vector is needed below with textscan to skip unrequired columns.
    % First make sure that the channel name cell array is properly filled.
    nColumns = length(COLUMN_NAMES);
    if isempty(channels) || sum(strcmpi(channels,'all'))>0
        channels = COLUMN_NAMES; % just include all channels
    end
    % Also create an ordering vector to reshuffle the columns back into the
    % order the caller requested (used below to reshuffle C)
    channel_ordering = []; 
    S = logical(zeros(1,nColumns)); %#ok<LOGL>
    for iChannel=1:length(channels)
        which_channel_vector = strcmpi( channels{iChannel}, COLUMN_NAMES );
        S = S | which_channel_vector;
        iChannelPos = find(which_channel_vector);
        if (iChannelPos>0)
            channel_ordering(end+1) = iChannelPos; %#ok<AGROW>
        end
    end
    % The channel_ordering vector should be renumbered because columns
    % might have been skipped (i.e. [2 5 3] should become [1 3 2].
    % So, loop and replace the lowest by incrementing index.
    column_ordering = zeros(1,length(channel_ordering));
    temp_channel_ordering = channel_ordering;
    for iChannel=1:length(temp_channel_ordering)
        iMin = find(temp_channel_ordering==min(temp_channel_ordering));
        column_ordering(iMin) = iChannel;
        % set to huge value so it can't be the lowest anymore in next
        % iteration:
        temp_channel_ordering(iMin) = Inf; 
    end
 
    % prepare a format string for parsing lines:
    % - use %* to skip columns
    % - use sprintf generate the format string: %c is placeholder for * or null, %% is just %
    % - the last column is a hexadecimal value (which is not supported yet by textscan with %x),
    %   so we first read it as plain string and then convert explicitly.
    %   The bit pattern must be preserved because different event types can occur at the same sample.
    format = [ sprintf('%%%cd ',(S(1:end-1)==0).*'*') '%4s']; % e.g. '%*d %*d %d %*d %d %d %d %d %d %4s
    format = regexprep(format,'%[^*]d','%d'); % remove null characters
    
    % read all sample rows, skipping stray '#' lines along the way
    C=textscan(fid,format','MultipleDelimsAsOne',1,'CommentStyle','#');
    fclose(fid);
    
    %% translate marker column to event indices (sample indices)
    % get last (mark) column and translate 4 hex digits to unsigned number
    markers = uint16(hex2dec(C{end}));
%   fprintf('markers: '); fprintf(' %d',unique(markers)); fprintf('\n');
    % then mask all known event types (marked by single bit) and create
    % index vectors for each type; create a struct along the way
    n = length(markers);
    onevec = ones(n,1,'uint16');
    I.ScannerStart  = uint32(find(bitand(markers,16*onevec)));
    I.ScannerStop   = uint32(find(bitand(markers,32*onevec)));
    I.VcgOnset      = uint32(find(bitand(markers,1*onevec))); % VCG wave onset
    I.PpuOnset      = uint32(find(bitand(markers,2*onevec))); % PPU wave onset
    I.Mark4         = uint32(find(bitand(markers,4*onevec))); % Inspration wave onset ?
    I.SliceOnset    = uint32(find(bitand(markers,8*onevec)));

    % convert marker channel to numerical values if this column should be
    % included in the output 
    if S(end)
        C{end} = int32(markers); % all channels should be the same int32
    else
        C = C(1:end-1); % just remove markers from output
    end
    % create matrix from columns in cell's and reshuffle columns in order caller requested
    C = cell2mat(C(column_ordering));

    % instead of simply returning the marker vector, a table is created
    % which has marker values in the first column and indices (sample
    % numbers) in the second column
    iMarkerIndices = uint32(find(markers>0));
    M = [uint32(markers(iMarkerIndices)), iMarkerIndices];
    
    %% and finally assemble DATA struct to return
    DATA = struct('C', C, 'M', M, 'I', I);
end

Contact us