diff --git a/src/pspm_cfg/pspm_cfg_pupil_preprocess.m b/src/pspm_cfg/pspm_cfg_pupil_preprocess.m index 452242f5b..2d7afcc2d 100644 --- a/src/pspm_cfg/pspm_cfg_pupil_preprocess.m +++ b/src/pspm_cfg/pspm_cfg_pupil_preprocess.m @@ -133,9 +133,9 @@ % Residual filter for lowpass cut-off ResdFiltLPCF = cfg_entry; ResdFiltLPCF.name = 'Butterworth cutoff frequency (Hz)'; -ResdFiltLPCF.tag = 'residualsFilter_interpFs'; +ResdFiltLPCF.tag = 'residualsFilter_lowpassCF'; ResdFiltLPCF.num = [1 1]; -ResdFiltLPCF.val = {100}; +ResdFiltLPCF.val = {16};% ResdFiltLPCF.help = pspm_cfg_help_format('pspm_pupil_pp_options', 'raw.residualsFilter_lowpassCF'); % Keep filter data diff --git a/src/pspm_cfg/pspm_cfg_selector_channel.m b/src/pspm_cfg/pspm_cfg_selector_channel.m index bf6ba8ac6..e32a0fe80 100644 --- a/src/pspm_cfg/pspm_cfg_selector_channel.m +++ b/src/pspm_cfg/pspm_cfg_selector_channel.m @@ -182,7 +182,7 @@ function out = pupil_chan(menu_set, menu_default) labels = {'Combined pupil channel', 'Both pupil channels', 'Left pupil', 'Right pupil', 'Default', 'None'}; - values = {'pupil_c', 'both', 'pupil_l', 'pupil_r', 'pupil', ''}; + values = {'pupil_c', 'both', 'pupil_l', 'pupil_r', 'pupil', 'none'}; out = cfg_menu; out.name = 'Channel specification'; diff --git a/src/pspm_pupil_pp.m b/src/pspm_pupil_pp.m index 71603e3c7..294b05539 100644 --- a/src/pspm_pupil_pp.m +++ b/src/pspm_pupil_pp.m @@ -85,7 +85,7 @@ % ├─────.end : [decimal][Unit: second] Ending time of the segment. % ├────.name : [string] Name of the segment. Segment will be stored by this name. % ├.plot_data: [Boolean][Default: false or 0] Plot the preprocessing steps. -% ├─.chan_valid_cutoff : [optional][Default: 0.01] +% ├─.chan_valid_cutoff : [optional][Default: 0.2] % │ A cut-off value for checking whether there are too many missing values % │ in a data channel for combination. If the difference in % │ missing data percentage between the two channels exceeds @@ -116,18 +116,25 @@ if nargin == 1 options = struct(); end + + options = pspm_options(options, 'pupil_pp'); if options.invalid return end -[lsts, default_settings] = pspm_pupil_pp_options(); + +% when batch editor has default settings +if ~isfield(options,'custom_settings') + [lsts, default_settings] = pspm_pupil_pp_options(); % so no warnings are displayed +else + [lsts, default_settings] = pspm_pupil_pp_options(options.custom_settings); +end + + if lsts ~= 1 return end -if isfield(options, 'custom_settings') - default_settings = pspm_assign_fields_recursively(... - default_settings, options.custom_settings); -end + options.custom_settings = default_settings; %% 3 Input checks @@ -352,16 +359,3 @@ end end end -function out_struct = pspm_assign_fields_recursively(out_struct, in_struct) -% Definition -% pspm_assign_fields_recursively assign all fields of in_struct to -% out_struct recursively, overwriting when necessary. -fnames = fieldnames(in_struct); -for i = 1:numel(fnames) - name = fnames{i}; - if isstruct(in_struct.(name)) && isfield(out_struct, name) - out_struct.(name) = pspm_assign_fields_recursively(out_struct.(name), in_struct.(name)); - else - out_struct.(name) = in_struct.(name); - end -end diff --git a/src/pspm_pupil_pp_options.m b/src/pspm_pupil_pp_options.m index 4e4713ef8..ee4670be7 100644 --- a/src/pspm_pupil_pp_options.m +++ b/src/pspm_pupil_pp_options.m @@ -1,12 +1,12 @@ -function [sts, default_settings] = pspm_pupil_pp_options() +function [sts, default_settings] = pspm_pupil_pp_options(custom_settings) % ● Description % pspm_pupil_pp_options is a helper function that can be used to modify the % behaviour of pspm_pupil_pp function. This function returns the settings % structure used by pspm_pupil_pp for pupil preprocessing. You can modify the % returned structure and then pass it to pspm_pupil_pp. See below for % explanation of the parameters. Adapted from: -% - pspm/pupil-size/code/helperFunctions/rawDataFilter.m lines 63 to 149, -% - pspm/pupil-size/code/dataModels/ValidSamplesModel.m lines 357 to 373. +% - src/ext/pupil-size/code/helperFunctions/rawDataFilter.m lines 63 to 149, +% - src/ext/pupil-size/code/dataModels/ValidSamplesModel.m lines 357 to 373. % ● Format % [sts, default_settings] = pspm_pupil_pp_options() % ● Outputs @@ -90,39 +90,124 @@ % │ Cutoff frequency for first order Butterworth filter. % │ (Default: 16 Hz) % │ +% ├─residualsFilter_lowpassB: +% │ Numerator (B) coefficients of the first-order +% │ Butterworth filter used in the residuals filter passes. +% │ Automatically computed from residualsFilter_lowpassCF +% │ and residualsFilter_interpFs. +% ├─residualsFilter_lowpassA: +% │ Denominator (A) coefficients of the first-order +% │ Butterworth filter used in the residuals filter +% │ passes. Automatically computed. +% │ % │ // Keep filter data % │ -% └─keepFilterData: If true, intermediate filter data will be stored. +% └───────keepFilterData:If true, intermediate filter data will be stored. % Set to false to save memory and improve plotting % performance. (Default: true) -% -% // Final data smoothing -% +% % ┌───────────────valid % ├interp_upsamplingFreq:The upsampling frequency used to generate the smooth % │ signal. (Default: 1000 Hz) -% ├───LpFilt_cutoffFreq: Cutoff frequency of the lowpass filter used during -% │ final smoothing. (Default: 4 Hz) -% ├────────LpFilt_order: Filter order of the lowpass filter used during final -% │ smoothing. (Default: 4) -% └───────interp_maxGap: Maximum gap in the used (valid) raw samples to -% interpolate over. Sections that were interpolated -% over distances larger than this value will be set -% to NaN. (Default: 250 ms) +% ├───────interp_maxGap: Maximum gap in the used (valid) raw samples to +% │ interpolate over. Sections that were interpolated +% │ over distances larger than this value will be set +% │ to NaN. (Default: 250 ms) +% │ +% │ // For final data smoothing, the below computation is performed: +% │ // [LpFilt_B, LpFilt_A] = butter(LpFilt_order, ... +% │ // 2*LpFilt_cutoffFreq/interp_upsamplingFreq ); +% │ +% ├────LpFilt_cutoffFreq:The cutoff frequency (in Hz) for the low-pass Butterworth filter +% │ that is applied to the upsampled signal. (Default: 4 Hz) +% ├─────────LpFilt_order:The order of the Butterworth filter used on the +% │ upsampled signal. (Default: 4) +% ├─────────────LpFilt_B:The numerator coefficients of the digital Butterworth +% │ low-pass filter. Automatically computed. +% └─────────────LpFilt_A:The denominator coefficients of the digital +% Butterworth low-pass filter. Automatically computed. +% +% % ● History % Introduced In PsPM version ?. % Written in 2019 by Eshref Yozdemir (University of Zurich) % Maintained in 2022 by Teddy +% Maintained in 2024 by Bernhard von Raußendorf global settings if isempty(settings) pspm_init; end + sts = -1; +default_settings = struct(); + +if nargin < 1 + flag = 0; +elseif ~isstruct(custom_settings) + warning('Input must be a struct. Returning default settings.'); + flag = 0; +else + flag = 1; +end + + + +if nargin == 1 && flag == 1 + + reqFieldsRaw = {'residualsFilter_interpFs','residualsFilter_lowpassCF'}; + if isfield(custom_settings,'raw') && all(isfield(custom_settings.raw, reqFieldsRaw)) + % from src/ext/pupil-size/code/helperFunctions/rawDataFilter.m + + [custom_settings.raw.residualsFilter_lowpassB , custom_settings.raw.residualsFilter_lowpassA] ... + = butter(1 , custom_settings.raw.residualsFilter_lowpassCF/(custom_settings.raw.residualsFilter_interpFs/2) ); + else + warning('Missing required fields in custom_settings.raw: Default filter coefficients will be used.'); % change + end + + reqFieldsValid = {'LpFilt_cutoffFreq','interp_upsamplingFreq','LpFilt_order'}; + if isfield(custom_settings,'valid') && all(isfield(custom_settings.valid, reqFieldsValid)) + % settingsOut = getDefaultSettings() from ValidSamplesModel + [custom_settings.valid.LpFilt_B, custom_settings.valid.LpFilt_A] = butter(custom_settings.valid.LpFilt_order, ... + 2*custom_settings.valid.LpFilt_cutoffFreq/custom_settings.valid.interp_upsamplingFreq ); + else + warning('Missing required fields in custom_settings.valid: Default filter coefficients will be used.'); % change + end + +end + + libbase_path = pspm_path('ext','pupil-size', 'code'); libpath = {fullfile(libbase_path, 'dataModels'), fullfile(libbase_path, 'helperFunctions')}; addpath(libpath{:}); default_settings = PupilDataModel.getDefaultSettings(); + +% gets +default_settings.valid.LpFilt_cutoffFreq = 4; % could also be added to the default_settings in ValidSamplesModel +default_settings.valid.LpFilt_order = 4; % could also be added to the default_settings in ValidSamplesModel + +if nargin == 1 && flag == 1 + default_settings = pspm_assign_fields_recursively(default_settings, custom_settings); +end + + rmpath(libpath{:}); sts = 1; return +end + +function out_struct = pspm_assign_fields_recursively(out_struct, in_struct) +% Definition +% pspm_assign_fields_recursively assign all fields of in_struct to +% out_struct recursively, overwriting when necessary. +fnames = fieldnames(in_struct); +for i = 1:numel(fnames) + name = fnames{i}; + if isstruct(in_struct.(name)) && isfield(out_struct, name) + out_struct.(name) = pspm_assign_fields_recursively(out_struct.(name), in_struct.(name)); + else + out_struct.(name) = in_struct.(name); + end +end + +end \ No newline at end of file diff --git a/test/pspm_pupil_pp_test.m b/test/pspm_pupil_pp_test.m index 271cffb11..bd9592c56 100644 --- a/test/pspm_pupil_pp_test.m +++ b/test/pspm_pupil_pp_test.m @@ -22,8 +22,9 @@ function backup(this) import{end + 1}.type = 'gaze_x_l'; import{end + 1}.type = 'gaze_y_l'; import{end + 1}.type = 'marker'; + options.overwrite = 1; [sts, this.pspm_input_filename] = pspm_import(... - this.raw_input_filename, 'eyelink', import, struct()); + this.raw_input_filename, 'eyelink', import, options); this.pspm_input_filename = this.pspm_input_filename; end end