diff --git a/integrateCTAGGER.m b/integrateCTAGGER.m new file mode 100644 index 000000000..2ad2e2564 --- /dev/null +++ b/integrateCTAGGER.m @@ -0,0 +1,86 @@ +function [dataStruct, tags] = integrateCTAGGER(dataStruct, varargin) + + jarFile = fullfile(pwd, 'CTagger.jar'); + if ~ismember(jarFile, javaclasspath('-dynamic')) + javaaddpath(jarFile); + end + % Parse input arguments + p = parseArguments(dataStruct, varargin{:}); + + if ~isempty(p.sidecar) && exist(p.sidecar, 'file') + tags = fileread(p.sidecar); % Read directly from the file + else + % Hardcoded default JSON + tags = ['{' ... + '"onset":{"Description":"Time at which the event occurred, in seconds.","Units":"seconds"},' ... + '"duration":{"Description":"Duration of the event, in seconds.","Units":"seconds"},' ... + '"trial_type":{"Description":"Type of trial.","Levels":{' ... + '"stimulus":"Event indicating stimulus",' ... + '"response":"Event indicating response"' ... + '}}' ... + '}' + ]; + end + + try + [tags, canceled] = useCTAGGER(tags); + + if canceled + disp('Tagging process canceled.'); + return; + end + catch ME + error('CTAGGER Error: %s', ME.message); + end + + dataStruct.tags = tags; + disp('Tagging complete.'); +end + +function [tags, canceled] = useCTAGGER(tags) + % Wrapper for launching CTAGGER + canceled = false; + [newTags, canceled] = loadCTAGGER(tags); + if ~canceled + tags = newTags; + end +end + +function [result, canceled] = loadCTAGGER(json) + % Launch CTAGGER + canceled = false; + notified = false; + + try + loader = javaObject('TaggerLoader', json); + catch ME + error('Error initializing CTAGGER: %s', ME.message); + end + + timeout = 300; % seconds + tStart = tic; + while ~notified && toc(tStart) < timeout + pause(0.5); + notified = loader.isNotified(); + end + + if ~notified + error('CTAGGER did not respond within the timeout period.'); + end + + % Check if tagging was canceled + if loader.isCanceled() + canceled = true; + result = ''; + else + result = char(loader.getHEDJson()); + end +end + +function p = parseArguments(dataStruct, varargin) + parser = inputParser; + parser.addRequired('dataStruct', @(x) isstruct(x)); + parser.addParameter('sidecar', '', @ischar); + parser.parse(dataStruct, varargin{:}); + p = parser.Results; +end diff --git a/process_grouphed.m b/process_grouphed.m new file mode 100644 index 000000000..2a5b1d022 --- /dev/null +++ b/process_grouphed.m @@ -0,0 +1,117 @@ +function varargout = process_group_by_hed(varargin) + + eval(macro_method); +end + +%% ===== GET DESCRIPTION ===== +function sProcess = GetDescription() + % Description of the process + sProcess.Comment = 'Group Events by HED Tags'; + sProcess.Category = 'Custom'; + sProcess.SubGroup = 'User'; + sProcess.Index = 1002; + sProcess.isSeparator = 1; + + % Define the input and output types + sProcess.InputTypes = {'data', 'raw'}; + sProcess.OutputTypes = {'data', 'raw'}; + sProcess.nInputs = 1; + sProcess.nMinFiles = 1; + + % Add options for user configuration + sProcess.options.showOutput.Comment = 'Display grouped events in a message box'; + sProcess.options.showOutput.Type = 'checkbox'; + sProcess.options.showOutput.Value = 1; +end + +%% ===== FORMAT COMMENT ===== +function Comment = FormatComment(sProcess) + Comment = sProcess.Comment; +end + +%% ===== RUN ===== +function OutputFiles = Run(sProcess, sInput) + % Initialize output file list + OutputFiles = {}; + + % Check for exactly one input file + if length(sInput) ~= 1 + bst_report('Error', sProcess, sInput, 'This process requires exactly one input file.'); + return; + end + + % Load the input data structure + DataStruct = in_bst_data(sInput.FileName); + if ~isfield(DataStruct, 'Events') || isempty(DataStruct.Events) + bst_report('Error', sProcess, sInput, 'No events found in the data.'); + return; + end + + % Extract events + events = DataStruct.Events; + + % Group events by HED tags + try + groupedEvents = groupEventsByHEDTags(events); + catch ME + bst_report('Error', sProcess, sInput, ['Error grouping events: ' ME.message]); + return; + end + + % Optionally display grouped events + if sProcess.options.showOutput.Value + DisplayGroupedEvents(groupedEvents); + end + + % No output modification needed; return input file + OutputFiles{1} = sInput.FileName; +end + +function groupedEvents = groupEventsByHEDTags(events) + % Groups events based on their HED tags + % + % Input: + % events - Array of event structures with HED tags + % Output: + % groupedEvents - Struct where each field is a unique HED tag, and its + % value is an array of event indices + + tagMap = containers.Map('KeyType', 'char', 'ValueType', 'any'); + + for i = 1:length(events) + % Extract the HED tags for this event + if ~isfield(events(i), 'hedTags') || isempty(events(i).hedTags) + continue; + end + + % Assume hedTags is a comma-separated string; split into individual tags + hedTags = strsplit(events(i).hedTags, ','); + hedTags = strtrim(hedTags); + + % Group by each tag + for j = 1:length(hedTags) + tag = hedTags{j}; + if ~isKey(tagMap, tag) + tagMap(tag) = []; % Initialize empty group + end + tagMap(tag) = [tagMap(tag), i]; % Add event index to group + end + end + + % Convert map to struct for easier access + groupedEvents = struct(); + tagKeys = keys(tagMap); + for k = 1:length(tagKeys) + groupedEvents.(matlab.lang.makeValidName(tagKeys{k})) = tagMap(tagKeys{k}); + end +end + +function DisplayGroupedEvents(groupedEvents) + % Display grouped events in a message box + msg = 'Grouped Events by HED Tags:\n'; + tagNames = fieldnames(groupedEvents); + for i = 1:length(tagNames) + msg = sprintf('%s\n%s: [%s]', msg, tagNames{i}, num2str(groupedEvents.(tagNames{i}))); + end + msgbox(msg, 'Grouped Events', 'help'); +end diff --git a/testCTagger.m b/testCTagger.m new file mode 100644 index 000000000..602cae080 --- /dev/null +++ b/testCTagger.m @@ -0,0 +1,20 @@ +% Load the events.json file +eventsJsonFile = 'task-FacePerception_events.json'; + +if exist(eventsJsonFile, 'file') + disp('Using events.json as input...'); + sidecar = eventsJsonFile; +else + sidecar = ''; +end + +%example data structure +dataStruct.events = struct('onset', {0.5, 1.0, 1.5}, ... + 'duration', {0.5, 0.5, 0.5}, ... + 'trial_type', {'stimulus', 'response', 'stimulus'}); + +% Integrate CTAGGER +[dataStruct, tags] = integrateCTAGGER(dataStruct, 'sidecar', sidecar); + +disp('Updated JSON:'); +disp(tags); diff --git a/toolbox/process/functions/process_evt_hed.m b/toolbox/process/functions/process_evt_hed.m new file mode 100644 index 000000000..3fb9a2e19 --- /dev/null +++ b/toolbox/process/functions/process_evt_hed.m @@ -0,0 +1,90 @@ +function varargout = process_evt_hed(varargin) +% PROCESS_EVT_HED: Attach HED tags from sidecar JSON to imported events +% USAGE: OutputFiles = process_evt_hed('Run', sProcess, sInputs) +% This process reads the HED sidecar (.json) and populates S.Events(i).hedTags +% for each raw/data file in the protocol. + +eval(macro_method); +end + +%% ===== GET DESCRIPTION ===== +function sProcess = GetDescription() + sProcess.Comment = 'Import HED sidecar'; + sProcess.Category = 'File'; + sProcess.SubGroup = 'Events'; + sProcess.Index = 42.5; + sProcess.InputTypes = {'data','raw', 'matrix'}; + sProcess.OutputTypes = {'data','raw', 'matrix'}; + sProcess.nInputs = 1; + sProcess.nMinFiles = 1; + + SelectOptions = {... + '', '', 'open', ... + 'Select HED sidecar JSON...', ... + 'HED_JSON', 'single', 'files', ... + {{'.json'}, 'JSON sidecar files (*.json)', ''}, ... + {}}; + + sProcess.options.sidecar.Comment = 'HED sidecar JSON file:'; + sProcess.options.sidecar.Type = 'filename'; + sProcess.options.sidecar.Value = SelectOptions; +end + +%% ===== FORMAT COMMENT ===== +function Comment = FormatComment(sProcess) + Comment = sProcess.Comment; +end + +%% ===== RUN ===== +function OutputFile = Run(sProcess, sInput) + % Return all the input files + OutputFile = {sInput.FileName}; + + jsonFile = sProcess.options.sidecar.Value{1}; + if ~file_exist(jsonFile) + bst_report('Error', sProcess, sInput, 'Sidecar JSON not found.'); + return; + end + + % Read HEDs + hedInfo = bst_jsondecode(jsonFile); + + % LOAD FILE + % Get file descriptor + isRaw = strcmpi(sInput.FileType, 'raw'); + % Load the raw file descriptor + if isRaw + DataMat = in_bst_data(sInput.FileName, 'F', 'History'); + sEvents = DataMat.F.events; + else + DataMat = in_bst_data(sInput.FileName, 'Events', 'History'); + sEvents = DataMat.Events; + end + + % Add HED tags to each event + isUpdated = 0; + for iEvt = 1:numel(sEvents) + key = sEvents(iEvt).label; + if isfield(hedInfo.trial_type, 'Levels') && ... + isfield(hedInfo.trial_type.Levels, key) && ... + isfield(hedInfo.trial_type.Levels.(key), 'HED') + sEvents(iEvt).hedTags = hedInfo.trial_type.Levels.(key).HED; + isUpdated = 1; + else + sEvents(iEvt).hedTags = ''; + end + end + + % ===== SAVE RESULT ===== + if isUpdated + if isRaw + DataMat.F.events = sEvents; + else + DataMat.Events = sEvents; + end + % Add history entry + DataMat = bst_history('add', DataMat, 'HED', sprintf('Added HED to events %s', jsonFile)); + % Only save changes if something was change + bst_save(file_fullpath(sInput.FileName), DataMat, 'v6', 1); + end +end \ No newline at end of file