Skip to content

Commit 7fbb725

Browse files
committed
v1.0.0: two-layer fNIRS toolbox - group analysis, DOT, aux signals, first-party DSP, and visualization (#7)
* Refactor plotting helpers and add modular utilities Refactored +pf2/+data/+plot/oxy.m, raw.m, and roi.m to use new helper functions for probe info loading, marker processing, baseline correction, and optode positioning. Added new utility functions in +pf2_base for modular handling of probe info (plot/loadProbeInfo), marker processing (plot/processMarkers), baseline correction (plot/processBaseline), optode subplot positioning (plot/getOptodePosition), and GUI helpers (gui/filterOptodeTable, gui/getTimeIndices, gui/getWavelengthColors). Added pf2.data.crop for simple time window extraction. Improved error handling and reduced code duplication across plotting and import functions. Minor bugfixes in importSNIRF and processStageFilterHb. Added processStageOD2Hb for Beer-Lambert conversion. Updated tests and documentation accordingly. * Refactor plotting functions to use flexible argument parsing Updated oxy, raw, and roi plotting functions to use a unified input signature with flexible positional and name-value argument parsing. This improves usability, supports legacy options, and allows for more robust handling of channels/ROIs and plotting options. Documentation and input validation were also improved for clarity and consistency. * Add unit tests for synthetic data processing Introduces SyntheticDataTest.m with comprehensive unit tests for pf2.data functions using synthetic data. Tests cover resampling, splitting, cropping, time shifting, concatenation, and edge cases to ensure correctness and numerical stability of core data manipulation routines. * Rename gui module to GUI for consistency * Add Experiment API, connectivity tools, and tests Introduce a scriptable Experiment class for group fNIRS analysis and a set of connectivity tools: computeMatrix, plotMatrix, and plotBlockComparison (including coupling method resolution and ROI support). Add hyperscanning helpers, coupling methods, ROI utilities, plotting helpers, and a number of pf2/pf2_base processing methods (resampling, GLM helpers, short-channel regression, method create/delete/edit stubs). Include many unit/integration/golden tests and synthetic generators, benchmark scripts and example data, updated import/export/resample utilities, miscellaneous helper functions, and bundled Wavelab binaries and golden files. These changes enable non-GUI batch/group workflows, connectivity analysis, and provide test coverage and example pipelines. * Fix white mode styling in report figures - use full package path The applyReportStyle function was calling forceWhiteMode without the full package path, causing "Undefined function" errors during report generation. Updated to use exploreFNIRS.report.forceWhiteMode(fig) to properly reference the function and ensure white background styling is applied to all report figures (temporal plots, bar charts, LME plots). Verified: Sample report now generates successfully with correct white styling. * Add connectivity analysis and plotting modules Introduce a comprehensive connectivity feature set and related plotting/reporting utilities for exploreFNIRS. Highlights: - New connectivity analysis functions: computeDynamicFC, computeInterROI, computeIntraROI, detectStates (dynamic FC, ROI/within-ROI analyses, state clustering). - Extended computeMatrix to support directed methods (Granger, transfer entropy) and updated coupling function resolution. - New visualization routines: plotChord, plotDirected, plotDynamicFC, plus new/interfaced plotInterROI/plotIntraROI/plotMatrix and improvements to plotBlockComparison (now uses pf2_base.plot utilities and PlotStyle). - Added coupling implementations and wrappers (granger, transferEntropy references) and hyperscanning helpers (computeDyad, dyad plotting, group/hyperscan visualizations). - Reporting tools: Pipeline, report generation helpers and a suite of report table/formatting functions. - New plotting/core utilities and tests: PlotStyle, createFigure, handleSave, many unit tests, and numerous example notebooks / sample report assets. - Several utility functions added (motion correction, SCI rejection, IIR bandpass) and README / CHANGELOG updates. Purpose: expand exploreFNIRS with time-varying and directed connectivity analyses, richer visualizations, reporting, and tests to support development and reproducible examples. * Add connectivity analysis and plotting modules Introduce a comprehensive connectivity feature set and related plotting/reporting utilities for exploreFNIRS. Highlights: - New connectivity analysis functions: computeDynamicFC, computeInterROI, computeIntraROI, detectStates (dynamic FC, ROI/within-ROI analyses, state clustering). - Extended computeMatrix to support directed methods (Granger, transfer entropy) and updated coupling function resolution. - New visualization routines: plotChord, plotDirected, plotDynamicFC, plus new/interfaced plotInterROI/plotIntraROI/plotMatrix and improvements to plotBlockComparison (now uses pf2_base.plot utilities and PlotStyle). - Added coupling implementations and wrappers (granger, transferEntropy references) and hyperscanning helpers (computeDyad, dyad plotting, group/hyperscan visualizations). - Reporting tools: Pipeline, report generation helpers and a suite of report table/formatting functions. - New plotting/core utilities and tests: PlotStyle, createFigure, handleSave, many unit tests, and numerous example notebooks / sample report assets. - Several utility functions added (motion correction, SCI rejection, IIR bandpass) and README / CHANGELOG updates. Purpose: expand exploreFNIRS with time-varying and directed connectivity analyses, richer visualizations, reporting, and tests to support development and reproducible examples. * Add GLM diagnostics and pipeline improvements Introduce a comprehensive GLM diagnostic tool and refactor pipeline processing. Adds pf2_base.fnirs.diagnoseGLM with detailed checks (VIF, condition number, partial R2, residual ACF, predicted amplitudes, flags) and unit tests to validate diagnostics. Fix fitGLM/AR-IRLS contrast computation by returning/using prewhitened design and residuals (Xw/residuals_w) so contrast SEs are correct under AR-IRLS; update fitARIRLS signature and GPU gather. Refactor processStageFilterHb and processStageRaw2OD to use the new PipelineFunction API (robust legacy-struct conversion, context-based execution, improved ROI handling, mask/timeMask propagation, error handling), add pipeline classes/examples/tests, and enhance SNIRF import to store source/detector labels and per-channel S_D labels. Misc: GUI method wiring, prefs and benchmark updates, and many supporting unit tests and example scripts. * Add GUI plotting helpers and baseline option Introduce reusable GUI plotting helpers and centralize device/time handling; add support for pre-computed baseline samples. - Added pf2_base.gui.plotArranged, plotStageRaw, plotStageHb: shared plotting helpers to remove duplicated GUI plotting code and standardize color/axis handling. - Added pf2_base.gui.updateCurrentDevice: merges probe tables, resolves time vector/sample indices and sampling rate, and returns a consolidated device/context result for GUI and headless paths. - Extended pf2_base.fnirs.processStageOD2Hb to accept an options argument (BaselineSamples) via an arguments block so callers (GUI) can pass pre-computed baseline sample indices for view-relative/windowed baseline modes. - Refactored GUI (GUI/processFNIRS2_GUI.m): sync GUI context, delegate raw/OD/oxy processing to pf2_base functions, compute view-relative baseline samples in the GUI and pass them into processStageOD2Hb, use new plotting helpers, and wire updateCurrentDevice to populate PF2 state and time/fs. - Minor glue: added syncContextFromGUI helper and logic to compute valid rows for window-only processing. These changes reduce duplication, make baseline/view-relative behavior explicit, and centralize probe/time resolution for more robust GUI/headless integration. Tests and small filter helpers were added/updated accordingly. * Fix unpack output and pass flags to processing Updates across PF2 processing code: - +pf2_base/+gui/plotArranged.m: Removed the @islogical validator from the LightColorAuto addParameter call to avoid validation errors for non-logical truthy values. - +pf2_base/pf2_unpackMethod.m: Consolidates handling of the 'output' field when unpacking struct arrays. Instead of trying to build a cell per element, take the scalar output from the first struct element (or default to 'x') and assign it to Fidx. Adds a comment explaining MATLAB struct scalar distribution. - GUI/processFNIRS2_GUI.m: Pass explicit boolean flags into pf2_base.fnirs.processStageRaw2OD and pf2_base.fnirs.processStageFilterHb calls (added trailing true). Also ensure stageData includes markers, Aux, and time before calling the filter stage so the function has required context. These changes ensure consistent handling of the 'output' field from struct arrays and make processing function calls explicit about the new flag/behavior, preventing downstream errors. * Add graph metrics, HB-ICA, and pf2 updates Introduce graph-theory tooling and hyperscanning HB-ICA support across the codebase. Added a graph module with computeMetrics and metric implementations (degree, betweenness, clusteringCoefficient, charPathLength, efficiency, modularity, smallWorld, detectHubs, threshold, plotNetwork, plotMetrics, metricsToTable, etc.), plus a coupling adapter exploreFNIRS.coupling.hbica and Experiment methods (graphMetrics, hbica, statsClusterPermutation) with block support and computeHBICAcore. Integrated hbica into connectivity dispatch and added unit tests and utilities (pf2_base.signal.tdsep, several pf2 imports/method updates, QC ChannelCheck, probe computeAdjacency and many pf2 adjustments). Updated CHANGELOG and multiple preprocessing/function scripts to support the new features and analyses. * Add literature references; refactor calcCBSI Add citation comments to various exploreFNIRS functions (connectivity, coupling, hyperscanning, stats) to document key methods and relevant literature. Refactor pf2_base/+external/calcCBSI.m: replace legacy header, add inputs/outputs and reference, add input validation and clear error/warning messages, use NaN-safe vectorized std and computation of CBSI (cOxy = (oxy - alpha .* deoxy) / 2), and remove old commented online code. These changes improve documentation, robustness, and numerical handling of the CBSI routine. * Fix scanFiles init and canonicalize rootPath Initialize scanFiles' matches as an empty array to avoid initialization errors across MATLAB versions, and resolve rootPath to its canonical absolute path in analyzeDirectoryLevels (using java.io.File#getCanonicalPath) so strrep matches dir() output. These changes improve cross-version compatibility and ensure correct relative path extraction. * Update quickSetup.m * Preserve select() order and add bar error color Store and apply user-specified ordering from Experiment.select(): add selectOrder_ property, initialize it, capture orders for string/numeric selections, and reorder groupRows/gbyIdx when grouping by a single variable so plot order matches select() calls. Unify bar error/edge coloring: thread a new 'ErrorColor' argument (sty.ForegroundColor) into plotBar, plotAuxBar, and renderBar calls to pf2_base.external.barweb, and extend barweb.m to accept an ErrorColor parameter and prefer it over the automatic dark-mode detection. This ensures consistent/errorbar visibility (especially in dark backgrounds). * Add FFT-based CWT and batch wcoherence Introduce a fast, FFT-based Morlet CWT and wavelet-coherence implementation and wire it into batch connectivity/hyperscanning paths. Added pf2_base.wavelet.cwt and pf2_base.wavelet.wcoherence (GPU/single-precision aware, supports pre-computed CWT and pre-smoothed autospectra) plus local smoothing helpers. computeMatrix and computeDyad gain batch CWT code paths that precompute CWTs and smoothed auto-spectra to avoid redundant work when computing wcoherence across many channel pairs. Updated exploreFNIRS.coupling.wcoherence to delegate to the new pf2_base implementation and refined resolveWavelet docs. Added a suite of wavelet benchmarks (benchmarks/wavelet/*) to evaluate alternatives and optimizations. Small related adjustments to pf2_MotionCorrectWavelet to align with the new wavelet utilities. * Add neural efficiency plots; enable parfor Introduce a neural-efficiency visualization suite and performance improvements. Adds three new APIs: plotNeuralEfficiency (Experiment wrapper), plotNeuralEfficiencyCore (renderer) and plotNeuralEfficiencyFromTable (table-driven wrapper) which support z-scoring (pooled or per-group), ROI/channel selection, hierarchical averaging, color schemes, centroids/error bars, regression lines, arrows, quadrant labels and per-group statistics. Also parallelize several expensive loops (using pf2_base.accel.canParfor()) in computeGroup, permutationTest, smallWorld and connectivity matrix/intra-ROI computations, including pre-extracting indices for parfor compatibility and improved logging. Includes input validation and helper utilities for unmatched args and color shading. * Add NE outputs and per-point neTable; fix INI writer Compute and expose neural-efficiency (NE) values across the plotting stack and normalize INI writer behavior. - exploreFNIRS core: plotNeuralEfficiency, plotNeuralEfficiencyFromTable, and Experiment.plotNeuralEfficiency now return a third output neTable containing per-point Group, zX, zY, NE. Wrapper functions build/append the table; docs updated. - plotNeuralEfficiencyCore: compute NE = zY - zX, store per-subject NE and meanNE in stats, and include NE in stats struct. Documentation and outputs updated accordingly. - +pf2_base/+external/INI.m: switch to LF newlines, convert string inputs to char before writing, refine quoting/format logic, print logicals as 0/1, and refactor the format helper (Format -> getFormatStr). Misc whitespace/cleanup fixes. * Add partialCorr and mutualInfo coupling methods Introduce two new coupling measures and integrate them into connectivity/hyperscanning pipelines. Added exploreFNIRS.coupling.partialCorr (residualized and windowed partial correlation) and exploreFNIRS.coupling.mutualInfo (histogram-based MI with optional normalization, sliding windows and surrogate p-values). Implemented a batch precision-matrix path computeBatchPartialCorr in computeMatrix to compute partial correlations for connectivity matrices (with simple shrinkage regularization and p-value estimation). Registered the new methods in computeMatrix and computeDyad (getCouplingFn) and extended error messages. Added comprehensive unit tests in ConnectivityTest.m covering basic behavior, windowing, argument validation, and matrix-level checks for both partialCorr and mutualInfo. * Fix resample, import, QC, INI, ROI signature Multiple fixes across modules: - +pf2/+data/resample.m: Tightened varNames length check (use ==) and added guards to regenerate variable names when resampled column count mismatches, preventing table VariableNames errors. - +pf2/+import/importNIR.m: When channel-check is skipped and no saved mask exists, default fchMask to all-ones to mark all channels valid. - +pf2/+qc/+pipeline/assess.m: Pre-initialize all check fields in the report (pass/skipped) so downstream isfield() lookups are safe. - +pf2_base/+external/INI.m: Support function_handle values by converting to func2str and handle empty string values when determining quoting symbols. - functions/pf2_build_nanmean_ROI.m: Updated function signature to accept an unused second argument for compatibility with callers passing extra params. These changes fix runtime errors and improve robustness when inputs are missing or mismatched. * Add VLines option to plotTemporal Introduce a new 'VLines' plotting option to exploreFNIRS.core.plotTemporal to draw vertical annotation lines on all subplots. The option accepts a numeric vector (simple dashed gray lines) or a struct array with fields: time (required), label, color, and style. Implements a drawVLines helper that normalizes inputs and calls pf2_base.external.vline for each axis, and wires the option into input parsing and the plotting flow. Also updates examples and help text in example_plot_options.m to demonstrate numeric and labeled VLines usage. * Resolve probe layout using MNI/Pos2D, fallback grid Update resolveProbeLayout to prefer MNI 3D positions (projected X,Z) first, then 2D optode positions (Pos2D_x/Pos2D_y) with Y flipped so the top of the head is at the top of the plot, and finally fall back to the subplot layout grid. chNums is computed earlier from the standard-channel mask and returned; empty layout now clears chMask and chNums. Minor comment and formatting cleanup. * Add stable signal filters and qualify lpf call Introduce pf2_base.signal lpf, hpf, and bpf implementations and update assess.m to call the namespaced lpf. New filters: lpf supports FIR, equiripple and Butterworth (with ZPK->SOS for numeric stability), hpf and bpf use Butterworth via ZPK->SOS. All filters handle per-channel NaN-padded regions and use pf2_base.external.filtfilt_classic for zero-phase filtering. The change fixes numerical instability at low normalized frequencies and makes the lpf reference explicit in the QC pipeline. * Refactor Takizawa QC into pf2.qc.takizawa Move and centralize the Takizawa channel-quality logic into a new pf2.qc.takizawa() function with configurable options and a richer per-rule report (pass/values/rules/ruleNames/strict/fs/units and skipped info). Update +pf2/+qc/+pipeline/assess.m to build a minimal qcData struct and call the new API, wire the returned tkReport fields into the report and passMatrix, and remove the local extractTakizawaRules helper. Replace the legacy functions/pf2_TakizawaRejection.m implementation with a thin backwards-compatible wrapper that delegates to pf2.qc.takizawa. This refactor centralizes rules, improves diagnostics and configurability, and keeps backward compatibility with existing pipeline callers. * Standardize pf2 warnings and add suppression API Replace ad-hoc fprintf/printf informational and error prints with warning(...) calls using consistent pf2-prefixed warning IDs across the codebase, and add a new pf2.settings.warnings(state) helper to toggle or restore those warnings. Changes touch multiple modules (exploreFNIRS hyperscanning/stats, pf2 data/import/probe, pf2_base fnirs/plot/initialize, GUI/processFNIRS2_GUI, and processFNIRS2.m) to unify messaging, improve programmatic control, and allow silencing of noisy output during batch processing. The new warnings function saves/restores prior states and accepts 'on'/'off' or a saved struct for precise control. * Add statWindow, fromConfig, and stats utilities Introduce time-windowed stats and configuration-driven experiment creation. Adds a new Experiment.settings.statWindow and plumbing to auto-inject it into plotting (plotBar/plotLME) and stats (fitLME) via PlotProxy and Experiment.injectStatWindow. Adds Experiment.fromConfig(cfg) for declarative import/process/blocks -> Experiment pipelines with validation, and clears prior aggregation state when regrouping. Implements new statistical utilities: exploreFNIRS.stats.permTest (paired sign-flip permutation testing), exploreFNIRS.stats.effectSize (bootstrap effect sizes with CIs), and exploreFNIRS.stats.buildContrasts (standard contrast matrix builders). fitLME now accepts StatWindow and reports it in results. Adds helper getShortSeparationIdx and config validation, updates example script and includes new unit tests to cover contrasts, effect size, fromConfig and permutation test flows. * Add caching, hierarchicalAverageMulti, and tests Add persistent in-memory caches to pf2_base.fnirs.bvoxy::estimateAbsorb and pf2_base.fitProbe2D (with a '__clear__' sentinel) to avoid redundant recomputation. Introduce pf2_base.hierarchicalAverageMulti to compute multiple aggregation functions in a single pass and replace repeated hierarchicalAverage calls in base_functions/grandAvgFNIRS; also refactor grandAvgFNIRS for robustness (vectorized removals, improved Aux/time handling, safer field checks, and performance fixes). Add new unit tests (pf2_base.tests.unit.CacheTest) and extend HierarchicalAverageTest to validate the new multi-function averaging behavior and caching semantics. * Refactor group preprocessing and parallel grand-avg Rework Experiment group processing into two stages: Stage A collects preprocessing results (ppData/barData), hierarchy vars, and bar bins for all groups up front, marking and skipping empty groups. Stage B performs grand-averaging using the collected inputs and writes results back to obj.groups; grand-averaging is parallelized with parfor when pf2_base.accel.canParfor() indicates a running pool. Caching behavior preserved (ppData saved to group.cache), logging updated, and redundant per-group key/hierarchy computation removed to reduce repeated work and improve performance for multi-group runs. * Parallel resampling and metadata collection Introduce optional parallel resampling using parfor when a parallel pool is available (pf2_base.accel.canParfor()) and there are enough segments (numfSeg>2). Move resampling out of the metadata collection loop so data is resampled once before collecting segmentTimes and Aux info. Minor micro-optimization: cache Aux field in curField before measuring size to avoid repeated indexing. Adds comments and keeps existing resample options/behavior (centerOnT0/timeOutMode/averageAux/flattenAux/trimAux). Overall this reduces redundant work and improves performance for large datasets. * Optimize segment table builder; parallel import Refactor exploreFNIRS.dataset.buildSegmentInfoTable to a two-pass approach: discover full field schema, pre-allocate a table with appropriate VariableTypes, initialize sensible defaults (NaN, "", NaT), and fill rows with direct assignments. Adds classifyForTable helper, unwraps 1x1 tables, and performs type-aware coercion to avoid dynamic column growth and expensive ismember calls, improving performance and robustness. Update pf2.import.importDirectory to support parallel imports: pre-compute full paths and relative parts for parfor, use parfor when a pool is available (pf2_base.accel.canParfor), capture per-file errors while honoring ContinueOnError, and preserve verbose progress in the sequential fallback. After import, failed entries are filtered out and failures are reported when verbose. * Add per-segment pp cache, optimize resample & GUI Multiple improvements across the codebase: - Experiment: added per-segment preprocessing cache (ppCache_/ppCacheKey_) and dataIdx tracking for groups; invalidate/update cache when reprocessing or when preprocessing settings change; use cached per-segment pp/bar data when available. - processMethods: refactored segment processing to batch full-processing via processFNIRS2 and to loop for oxy-only; filter invalid segments; apply channel masking and resampling in parallel when a pool is available. - +pf2/+data/resample.m: replaced uses of pf2_base.isnestedfield with robust isfield/isstruct/istable checks; fixed time-field detection using ismember(...,1); simplified auxiliary table creation; vectorized accumulation for resampling (avoid per-bin nanmean calls) and improved NaN handling and sizing for accumarray calls. - +pf2_base/+fnirs/processStageFilterHb.m and processFNIRS2.m: removed isnestedfield usage and replaced with safer isfield/isstruct checks; added final ROI state check and consistent hasROI handling. - +pf2_base/isnestedfield.m: fixed logic to handle structs/tables properly and use isprop instead of bitwise ops; improved detection when traversing nested field strings. - exploreFNIRS.m: added File menu with Import File/Directory callbacks; parallelized resampling/baselining and group grand-averaging (uses parfor when available); created/synced an Experiment object for settings persistence and CLI round-trip; updated status text and data structures to support the refactor. These changes improve robustness (avoid brittle nested-field checks), performance (vectorized resample and parallel processing), and usability (import menu and settings sync). * Plotting tweaks: styles, vline visibility, and warnings Small updates to plotting utilities: - +exploreFNIRS/+core/plotHeatmap.m: add a 'Colors' input parameter (accepted for API consistency; heatmaps still use Colormap). - +exploreFNIRS/+core/plotScatter.m: apply theme/style values (sty.LegendTextColor, sty.LegendBgColor, sty.LegendEdgeColor) to legend appearance instead of hardcoded colors. - +exploreFNIRS/+core/plotTemporal.m: compute hasLabel and pass 'handleVisibility' to pf2_base.external.vline so labeled vertical lines are handled correctly for legend visibility. - +pf2_base/+external/barweb.m: suppress HISTOGRAM:notEnoughDataPoints warnings during plotting and restore warning state via onCleanup to avoid noisy output. * Fix warning state restore and query saving Restore warning states using the stored identifier and state fields, and save the full query result when capturing current warning settings. Previously the code passed the loop id to warning(...) instead of the stored identifier and attempted to use a nonexistent second output from warning('query', id), which could lose or misapply saved settings. This change ensures warning states are restored correctly and savedStates holds the complete query output. * Use style for legend and add Colors option Hook legend styling in Experiment to the style object (use sty.LegendTextColor, sty.LegendBgColor, sty.LegendEdgeColor) instead of hard-coded colors so themes control legend appearance. Also add an accepted-but-unused 'Colors' parameter to plotInfoLME, plotTopo, and plotTopoLME for API consistency (plotTopo continues to use Colormap). No functional changes beyond the style hookup and API compatibility. * Improve processing, resampling and optode handling Refactor processing and resampling flow and make optode/device handling more robust across the UI and data pipeline. - +exploreFNIRS/processMethods.m: Use explicit index lists for valid segments, adjust loops to iterate by index (avoid passing cell arrays into oxy-only path), remove parallel parfor branch and run applyChannelMask + resample in a simple loop. Add update of ExFNIRS.currentOpt from processed segments' channels. - +pf2/+data/resample.m: Skip auxiliary fields that end with "_unit" in addition to existing metadata names when recursively resampling. - exploreFNIRS.m: Refresh optode list in the UI when available; attempt to resolve and attach a pf2.Device to segments that lack one and print a device summary (with a warning on failure); fix a loop bug iterating over numEx; initialize/normalize ExFNIRS.UpdateNeeded; guard Experiment creation with try/catch to avoid hard failures; enhance logic to build the optode list from channels, device channel numbers, HbO matrices, or raw/fchMask data. These changes improve robustness when device metadata is missing, ensure the UI optode list is updated from processed data, simplify resampling code to avoid fragile parallel paths, and avoid including unit metadata in resampling operations. * Dark-mode colors and improved timeline labels Add dark-mode color adaptation and replace hardcoded styles in plotExTimeline. Introduces an isDark check that sets block/plot/baseline/dim colors and dash/style arguments, and uses those variables in plotHorizViewBar, vline, and plotPeriodicSample calls. Also updates y-axis labels to show start/end ranges and resample rates for each row, improving clarity in the timeline view. * Enhance plotting: escape TeX, TightLayout, VLines Add several plotting improvements and features across the codebase: - Introduce a 'TightLayout' parameter to many plot functions and thread it through PlotProxy so callers can opt into tighter figure layout control. - Sanitize user-provided text by applying pf2_base.plot.escapeTeX to titles, labels, legends and other text to avoid TeX/Interpreter issues. - Add VLines support to plotHeatmap (numeric or struct input) and a drawVLines helper to annotate vertical events (task boundaries, etc.). - Enhance plotTopo: support custom ChannelLabels, change default colormap to a blue-white-red diverging map, fix label colors/placement, and apply the plot style to the entire figure. - Major updates to plotTopoLME: add 'Projection' (2D/3D) option, interpolation and ROI/label options, refactor rendering into separate paths (render3D / render2D) and reorganize color/colormap handling for clarity. - Apply various UI/text/legend fixes in scatter/chord/directed/block/composite and other plotting helpers for consistent escaping and theming. - Add small utilities and examples: pf2_base/+plot/applyTightLayout and examples/scripts/example_gca_timemodel.m; add new stats helpers autoModelLME and behavioralTable. These changes improve robustness to special characters in text, provide better layout control, richer heatmap annotations, and broaden visualization options (2D probe views) for LME topographies. * Fix up grand average resampling and add padding support * Fix IO/processing bugs and add ForcedTerms Multiple fixes and enhancements across import/export/processing routines: - +exploreFNIRS/+export/mergeGbyTablesLong.m: fix variable name/initialization (numCh) and minor whitespace. - +exploreFNIRS/+stats/autoModelLME.m: add 'ForcedTerms' input, prepend forced terms to base model, and improve mergedTable initialization condition so table is only set once when empty. - +pf2/+data/resample.m: use size(...,1) instead of length(...) when computing baseline NaN fraction to avoid incorrect rates for non-column inputs. - +pf2/+export/asSNIRF.m: ensure measurement.wavelengthActual is set correctly for valid and invalid wavelengths. - +pf2/+import/importHitachiMES.m: improve filename type checking, add onCleanup-based file handle management, fix typo in error message, validate reopen of file, and add helper fcloseIfOpen to avoid leaks. - +pf2/+import/importNIRX.m: restore working directory using onCleanup instead of manual cd. - +pf2_base/+fnirs/processStageOD2Hb.m: guard against empty start/end sample lookups by defaulting to full range. These changes fix resource leaks, incorrect indexing/NaN calculations, and add a model option to force inclusion of covariates, improving robustness and correctness. * Fix table merging, BH FDR, LME dummy coding Several bug fixes and correctness improvements: - export/mergeGbyTablesLong.m: Corrected type checks when adding missing table columns by inspecting the source column values (char/string/cellstr -> strings(), otherwise -> NaN) instead of testing the variable name. This prevents incorrect column initialization and preserves appropriate types. - fx/autoContrast.m: Ensure unique name indices are sorted before updating isV0 to maintain consistent ordering when iterating rows. - fx/performFDR.m: Replaced incorrect q-value calculation with a standard Benjamini–Hochberg implementation: exclude NaNs, sort valid p-values, compute candidate q_i = p_i * m / i, enforce monotonicity via running minimum, cap at 1, and unsort back to original positions. This yields proper FDR-adjusted q-values and correct passed flags. - stats/clusterPermutation.m: Added 'DummyVarCoding','reference' to fitlme calls to enforce consistent dummy variable coding across permutations. Overall these changes fix type handling, statistical corrections, and ensure deterministic model behavior. * Refactor contrast SE calc and term matching Compute contrast effect sizes and SEs using coefficient covariance so arbitrary (including fractional) contrast weights are supported; derive Glass's delta from positive-side weights. Remove prior DF-weighted pooling logic. Update clusterPermutation term lookup to match dummy-coded categorical levels using startsWith on coefficient names. Adjust unit test to set fitlme DummyVarCoding to 'reference' for consistent dummy coding. * Add geodesic support and refactor color projection Introduce geodesic-based interpolation and centralize color projection logic. Key changes: - New pf2_base.plot.interpolateChannelColors helper: unifies nearest/IDW projection, fade-to-background calculation, and masking for both surface and voxel projections. - Add UseGeodesic option to interpolateValues3D and implement iLocalGeodesicDistSq to compute graph-geodesic distances on the cortical mesh (with caching and Euclidean fallback). - Refactor interpolateValues3D to use the new helper, cache distance arrays keyed by control points and geodesic flag, and blend colors with brain background via returned fadeAlpha. - Theme-aware label font color default (uses pf2_base.plot.PlotStyle when available) and several robustness improvements (use mean(...,'omitnan'), min/max with 'omitnan', clim instead of caxis, improved auto camera center fallback). - SNIRF export: map Det/Src positions by DetIdx/SrcIdx, pad missing indices with NaN rows so measurementList indexing remains correct. - Bugfixes in resample.m: use size(...,1) for baseline NaN fraction, use sum(numericIdx) for numeric column counts, and correct reshape ordering for fB_nanCheck. These changes improve interpolation accuracy across sulci, reduce duplicated code, fix indexing/nan-handling bugs, and make plotting more robust to sparse/non-contiguous probe indexing. * Add stat projection wrappers & transparent overlay Introduce project wrappers for statistical visualizations (pf2.probe.project.pvalues, fstats, correlation, biomarker, counts) and tests. Extend interpolateValues3D with ChannelAlpha/AlphaMode (transparent|blend), ForceLightMode, and default UseGeodesic=true; implement per-vertex alpha interpolation (iLocalInterpScalar) and render overlays (BrainOverlay, BrainVoxelOverlay) so non-significant areas are truly transparent. Fix colorbar/axes parenting, add mesh-graph caching, and forward unmatched args. Small import change to use pf2_base.channelCheckVersion(), update loadExistingMaskOrCheck, add channelCheckVersion helper and an example script. Tests cover new features and wrappers. * INI: split only on first '=' and recover structs Fix INI parsing so the field/value split uses the first '=' only (preserving '=' characters inside quoted strings or struct(...) values) and simplify string detection. Also add recovery in pf2_unpackMethod: if a value looks like 'struct(...)' (from older/truncated INI reads), attempt to eval it back to a struct inside a try/catch and restore the field when possible; otherwise leave it for downstream checks. This prevents accidental truncation of values and handles legacy corrupted cfg entries more robustly. * Enhance ChannelCheck UI, QC, and multi-file support Add multi-file mask snapshot/revert and improved dataset labels so edits are tracked per-file and cancel restores original sidecar/in-memory masks. Load sidecar *_CH.mat masks when in-memory mask is absent. Make QC more configurable: use QCSettings.CoVThreshold in stats, add confirmation when accepting QC recommendations if manual edits exist, and recompute stats after settings change. Improve UI/UX: add tooltips for many controls, keyboard shortcuts (Ctrl/Cmd+S to save, Esc to cancel), highlight/disable active state buttons, and update undo/redo tooltip text. Add wavelength-aware coloring for raw signals and ambient handling (mini and detail plots), plus helper functions miniSignalColors and wavelengthColor. Keep probe aspect ratio when arranging mini-axes by disabling automatic child resizing and adding relayoutMiniAxes called on SizeChanged; Q C map and mini-grid layout use consistent aspect handling. Misc: tighten x-limits for plotted segments, expose small plotting style tweaks, and include a tryLoadSidecarMask helper to find numeric mask fields in sidecar files. * Add methods editor, seeds, reset & tests Introduce a full AppDesigner-based pf2.methods.Editor for creating/editing/saving raw and oxy processing methods. Add repository seed factory functions (OD_TDDR, OD_TDDR_lpf, LPF, LPF_ROI) and a seeds.list helper plus resetDefaults to reinstall repo defaults. Add integration/unit tests exercising the editor, seeding workflow, and pipeline/model functionality. Fix INI serializer to convert MATLAB objects with toStruct (e.g. PipelineFunction) to legacy struct form before writing so saved method entries round-trip cleanly. Misc updates to pipeline classes and test fixtures to support the editor and seeding workflow. * Support fNIR3000 probe and RGB vline options Detect fNIR 3000 data sources in importNIR and set probe info/raw columns (HD/HDS -> fNIR_Devices_fNIR3000_18ch, raw(:,1:55)); preserve existing probe handling for other models. Extend vline to accept bare RGB/RGBA numeric color vectors (translate to {'Color', rgb}) in addition to linespecs and option cells, and update the validator. Add a unit test to verify vline accepts RGB and RGBA inputs without warnings. Add new devices/fNIR_Devices_fNIR3000.cfg device configuration. * Robust probe detection and add LD device cfg Refactor importNIR to robustly parse COBI Studio DataSource strings and map them to probe configs. Introduces resolveProbeFromDataSource and lookupProbeCfg helpers, uses pf2.Device.load to infer expected raw column width and trims trailing unused columns (with warnings on mismatch or load failure) instead of brittle positional parsing. Handles unidentified probes more gracefully. Adds a new device config fNIR_Devices_fNIR1000_LD.cfg for the Model 1000 LD probe. Also updates fNIR_Devices_fNIR3000.cfg to label the device as 'Model 3000C'. These changes improve compatibility with varied DataSource formats and support the LD probe layout. * Remove redundant fclose calls Delete two explicit fclose(fid) calls in importHitachiMES.m. The file handle is already managed via fidCleanup and the code sets fid = -1 afterward, so removing the redundant fclose avoids potential double-close or invalid-handle errors while preserving the existing cleanup behavior. * Update barchart_infogroup.m * Headless QC/topo, Octave checks, pipeline tweaks Add headless QC and topographic helpers, runtime detection for Octave, and several pipeline/editor fixes. Key changes: - New utilities: pf2.import.sampleData.group (one-call synthetic group), pf2.probe.plot.topo (2D/3D topo wrapper), pf2.qc.snapshot (headless QC snapshot writer). - Runtime helpers: pf2_base.env.isOctave and pf2_base.env.runtime to detect Octave/MATLAB and report missing Octave packages. - GUI guards: Editor and ChannelCheck now error gracefully under Octave (App Designer not supported). exploreFNIRS GUI also guarded. - 3D rendering/headless fixes: interpolateValues3D gained savePath/save options and logic to ensure off-screen figures are briefly visible so headless rasterization and imgOut captures work correctly. - Pipeline improvements: accept a struct for name-value overrides in add(), added move() to reorder steps, clarified swapStep error on numeric input, and expanded PipelineFunction docs for writing custom steps. - Robustness/UI: raw/oxy describeMethod and methods listing handle an empty current-method selection gracefully; importDirectory resolves canonical absolute paths without erroneously prepending pwd. - Docs and examples: README expanded with pipeline & group/stat examples; new example script examples/scripts/example_group_stats_bridge.m; QC example updated to pass proper result structs. Also includes minor touches (sampleData docs, prefs file update, added sample data file). These changes enable scriptable/headless workflows, clearer runtime diagnostics, and easier programmatic pipeline manipulation. * Add Filename option and duplicate-ID diagnostics Introduce a 'Filename' parameter to pf2.import.importDirectory to populate an info field from each file's basename (useful for flat folders). Refactor info population into applyInfoFields (parfor-safe) and add warnDuplicateSubjectIDs to warn when imported files share SubjectID values and may break grouping; include guidance on using Filename/DirN mappings. In pf2.data.importInfo, include a hint in no-match errors that reports how many imported structs share the provided key values (adds countStructsWithKeyVals helper). Also adjust qc pipeline assess to only pre-initialize the selected checks in the report (use checks instead of validChecks) so reports contain exactly the checks that were run. * Block averaging, QC tweaks, and WaveLab fixes Add pf2.data.blockAverage and pf2.data.grandAverage (alias) to resample epoched segments to a shared grid before averaging. Improve pf2.data.extractBlocks with guards for embedded block structs and a warning when default PreTime/PostTime would produce overly large segments. Optimize pf2.data.getMarkers with a fast path for column-vector OR queries. Tweak importNIR to report saved channel masks more clearly and silently ignore missing optional COBI logs. Raise default CoV thresholds (assess and ChannelCheck) from 0.1 to 0.2 and refine takizawa movement detection: increase BodyMovementThreshold to 0.5, count discrete movement events (rising edges) and scale allowed events by recording length. Make pf2_base.resolveWavelet ensure WaveLab is on the path. Suppress noisy first-run warnings in pf2_initialize in favor of a concise fprintf. Replace numerous WaveLab calls to reverse(...) with reverse_wl(...) to avoid name conflicts. Misc: minor UI/formatting and sample data updates. * Add marker dictionary subsystem and headless channel-check gating * Add support for artinis3 * Fixes for addFunction and plotIntraROI * Statistics Toolbox compatibility & Device optode coordiantes / provenance * Harmonize errors and library usage * Add montage export, sliding-window epoching, and cross-device anatomical canonicalization * Isolate ProcessingContext-based processing from global state * Update gather.m * Add spatial visualizations: activation movies, parcel projection, connectome, and dual-brain synchrony * Add bids export controls * Add typed auxiliary signals, high-quality 3D rendering, and export refinements Auxiliary signals (first-class, typed handling) - Canonical aux representation via normalizeAux/auxSignalType/resolveAux: every signal carries {data,time,unit,varNames,type,kind}; type inferred for HR/EKG/PPG/ACCEL/GSR/RESP/TEMP/EEG - Type-aware feature extractors: heartRateFrom, hrvFeatures, respFeatures, edaDecompose, eegBandPower, accelFeatures, addFeature - auxOnGrid aligns nested/flattened signals onto the fNIRS time base (anti-aliased, NaN-gap aware); findAuxByType resolves signals by type - Accelerometer-informed motion handling: accelMotionDetect, accelRegress, and extractBlocks 'RejectByAux' epoch rejection - Adaptive (RETROICOR-style) cardiac regression: physioRegress - oxy3Aux exports Artinis AD channels as typed aux on import - Aux as GLM nuisance (buildDesignMatrix, GLMExperiment), LME covariate (autoModelLME), and event-related overlay (plotTemporal) Hyperscanning physiology controls - partialCoherence to control shared physiology between brains - physioConfoundQC flags shared LFO/VLFO confounds; computeDyad updates High-quality 3D rendering - RenderStyle presets ('showcase' default, 'publication'): procedural matcap shading, sulcal ambient occlusion, brain colormaps, hero view - Supporting primitives: matcapShade/matcapTexture, meshCurvature, vertexNormals, brainColormap - Explore3D interactive explorer; interpolateValues3D and project.regions adopt the Style system; example_brain_render_styles script BIDS & tensor export - asBIDS writes optodes/coordsystem at montage level (BIDS inheritance), strips dark/ambient channels, spec-ordered channels.tsv columns, faithful coordinate-system tagging, ordered participants columns - asTensor exports aligned auxiliary signals and windowed tensors - importSNIRF stores source-detector distances in cm (Beer-Lambert and short-separation detection now consistent across importers) Correctness fixes - betasToSegments excludes nuisance/aux regressors from stimulus detection - accel/physioRegress recompute HbTotal/HbDiff/CBSI after correcting HbO/HbR - autoModelLME joins aux covariates by identifier key, not row position - interpolateValues3D honors renderStyle.lighting at the axes level * Add diffuse optical tomography, cross-brain PPI, and tidy-table import * Adjust docs * Add first-party signal-processing functions (butter, fir1, sgolay, windows) + Update documents * Add parcelation projectsion and other fixes * Add public ProcessingContext and arg validation Introduces a new public `pf2.ProcessingContext` wrapper that is usable from construction, supports alias-based configuration, `process()` convenience, and recipe restore via `fromRecipe`. The base context now uses copyable semantics, reloads method libraries on deserialize, and shifts global write-back behavior to `GUIContext` only; `processFNIRS2` also now respects `ctx.processRejected` unless explicitly overridden. Across data/aux/import/export/probe/qc APIs, many functions were migrated from `inputParser` to `arguments` blocks with clearer defaults and stronger validation while preserving existing call patterns (including cell-array forwarding and positional compatibility where needed). Tests and docs were updated to reflect the new context workflow, and MCP onboarding artifacts were added (`mcp/mcp.example.json`, README pointer). * Improve plot theming and bar alignment Updated ExploreFNIRS temporal, scatter, and barchart plots to apply `PlotStyle` colors to legends and annotation text so dark/light theme behavior is consistent. In `barweb`, foreground styling now prefers `PlotStyle` (with a fallback), legend background matches theme under ForceLightMode, and bar center calculation now uses `XEndPoints` when available to avoid misplaced overlays/error elements before graphics flush. Also adjusted `suptitle` top-band spacing/positioning to reduce overlap with subplot titles.
1 parent 5154017 commit 7fbb725

1,998 files changed

Lines changed: 141152 additions & 221335 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
function [aligned, masterChannels, masterLabels, nValid] = alignMatrices(results, mode)
2+
% ALIGNMATRICES Align connectivity results from subjects with different channels
3+
%
4+
% Maps each subject's/dyad's connectivity result into a common channel-
5+
% indexed grid so that matrix entry (i,j) always represents the same
6+
% channel pair across subjects, regardless of per-subject channel rejection.
7+
%
8+
% Syntax:
9+
% [aligned, masterCh, masterLabels, nValid] = ...
10+
% exploreFNIRS.connectivity.alignMatrices(results, 'union')
11+
% [aligned, masterCh, masterLabels, nValid] = ...
12+
% exploreFNIRS.connectivity.alignMatrices(results, 'intersection')
13+
% [aligned, masterCh, masterLabels, nValid] = ...
14+
% exploreFNIRS.connectivity.alignMatrices(results, 0.75)
15+
%
16+
% Inputs:
17+
% results - Cell array of result structs. Each must have one of:
18+
% Connectivity: .matrix [N x N] and .channels [1 x N]
19+
% Hyperscanning 'same': .values [N x 1] and .channelsA [1 x N]
20+
% Hyperscanning 'all': .values [Na x Nb] and .channelsA, .channelsB
21+
% mode - Alignment mode:
22+
% 'union' - All channels present in any subject (default)
23+
% 'intersection' - Only channels present in every subject
24+
% numeric 0-1 - Channels present in >= mode fraction of subjects
25+
%
26+
% Outputs:
27+
% aligned - 3D array with aligned values. NaN where a subject lacks
28+
% data for a channel. Shape: [M x M x K] for connectivity,
29+
% [M x 1 x K] for hyperscanning 'same', [Ma x Mb x K] for 'all'.
30+
% masterChannels - Master channel vector (or {masterA, masterB} for 'all' pairing)
31+
% masterLabels - Cell array of labels (or {labelsA, labelsB} for 'all' pairing)
32+
% nValid - Per-cell count of subjects contributing a non-NaN value
33+
%
34+
% See also: exploreFNIRS.connectivity.computeMatrix,
35+
% exploreFNIRS.hyperscanning.computeGroup
36+
37+
if nargin < 2
38+
mode = 'union';
39+
end
40+
41+
K = length(results);
42+
if K == 0
43+
error('exploreFNIRS:connectivity:alignMatrices', 'Empty results cell array');
44+
end
45+
46+
% Detect result shape
47+
isConnectivity = isfield(results{1}, 'matrix');
48+
isHyperAll = ~isConnectivity && isfield(results{1}, 'channelsB') && ...
49+
isfield(results{1}, 'values') && ~isvector(results{1}.values);
50+
51+
if isHyperAll
52+
[aligned, masterChannels, masterLabels, nValid] = ...
53+
alignHyperscanningAll(results, K, mode);
54+
elseif isConnectivity
55+
[aligned, masterChannels, masterLabels, nValid] = ...
56+
alignConnectivity(results, K, mode);
57+
else
58+
% Hyperscanning 'same' pairing (vector values)
59+
[aligned, masterChannels, masterLabels, nValid] = ...
60+
alignHyperscanningSame(results, K, mode);
61+
end
62+
end
63+
64+
65+
function [aligned, masterCh, masterLabels, nValid] = alignConnectivity(results, K, mode)
66+
% Align NxN connectivity matrices
67+
68+
% Collect all channel vectors
69+
allChannels = cell(K, 1);
70+
for k = 1:K
71+
allChannels{k} = results{k}.channels(:)';
72+
end
73+
74+
masterCh = computeMasterChannels(allChannels, K, mode);
75+
M = length(masterCh);
76+
77+
% Build aligned 3D array
78+
aligned = nan(M, M, K);
79+
for k = 1:K
80+
[~, masterIdx, subIdx] = intersect(masterCh, allChannels{k});
81+
aligned(masterIdx, masterIdx, k) = results{k}.matrix(subIdx, subIdx);
82+
end
83+
84+
nValid = sum(~isnan(aligned), 3);
85+
86+
% Build labels
87+
masterLabels = buildLabels(results, masterCh, 'labels', 'channels');
88+
end
89+
90+
91+
function [aligned, masterCh, masterLabels, nValid] = alignHyperscanningSame(results, K, mode)
92+
% Align Nx1 hyperscanning 'same' pairing vectors
93+
94+
allChannels = cell(K, 1);
95+
for k = 1:K
96+
allChannels{k} = results{k}.channelsA(:)';
97+
end
98+
99+
masterCh = computeMasterChannels(allChannels, K, mode);
100+
M = length(masterCh);
101+
102+
aligned = nan(M, 1, K);
103+
for k = 1:K
104+
[~, masterIdx, subIdx] = intersect(masterCh, allChannels{k});
105+
aligned(masterIdx, 1, k) = results{k}.values(subIdx);
106+
end
107+
108+
nValid = sum(~isnan(aligned), 3);
109+
110+
masterLabels = buildLabels(results, masterCh, 'labelsA', 'channelsA');
111+
end
112+
113+
114+
function [aligned, masterChannels, masterLabels, nValid] = alignHyperscanningAll(results, K, mode)
115+
% Align Na x Nb hyperscanning 'all' pairing matrices
116+
117+
allChA = cell(K, 1);
118+
allChB = cell(K, 1);
119+
for k = 1:K
120+
allChA{k} = results{k}.channelsA(:)';
121+
allChB{k} = results{k}.channelsB(:)';
122+
end
123+
124+
masterA = computeMasterChannels(allChA, K, mode);
125+
masterB = computeMasterChannels(allChB, K, mode);
126+
Ma = length(masterA);
127+
Mb = length(masterB);
128+
129+
aligned = nan(Ma, Mb, K);
130+
for k = 1:K
131+
[~, mIdxA, sIdxA] = intersect(masterA, allChA{k});
132+
[~, mIdxB, sIdxB] = intersect(masterB, allChB{k});
133+
aligned(mIdxA, mIdxB, k) = results{k}.values(sIdxA, sIdxB);
134+
end
135+
136+
nValid = sum(~isnan(aligned), 3);
137+
138+
labelsA = buildLabels(results, masterA, 'labelsA', 'channelsA');
139+
labelsB = buildLabels(results, masterB, 'labelsB', 'channelsB');
140+
141+
masterChannels = {masterA, masterB};
142+
masterLabels = {labelsA, labelsB};
143+
end
144+
145+
146+
function master = computeMasterChannels(allChannels, K, mode)
147+
% Compute the master channel set based on alignment mode
148+
149+
if ischar(mode) || isstring(mode)
150+
switch lower(char(mode))
151+
case 'union'
152+
master = allChannels{1};
153+
for k = 2:K
154+
master = union(master, allChannels{k});
155+
end
156+
case 'intersection'
157+
master = allChannels{1};
158+
for k = 2:K
159+
master = intersect(master, allChannels{k});
160+
end
161+
otherwise
162+
error('exploreFNIRS:connectivity:alignMatrices', ...
163+
'Unknown alignment mode "%s". Use ''union'', ''intersection'', or a numeric threshold.', char(mode));
164+
end
165+
elseif isnumeric(mode) && isscalar(mode) && mode > 0 && mode <= 1
166+
% Threshold mode: channels in >= mode fraction of subjects
167+
all = [];
168+
for k = 1:K
169+
all = union(all, allChannels{k});
170+
end
171+
counts = zeros(size(all));
172+
for k = 1:K
173+
counts = counts + ismember(all, allChannels{k});
174+
end
175+
master = all(counts >= mode * K);
176+
else
177+
error('exploreFNIRS:connectivity:alignMatrices', ...
178+
'mode must be ''union'', ''intersection'', or a numeric threshold in (0, 1].');
179+
end
180+
181+
master = sort(master(:)');
182+
end
183+
184+
185+
function labels = buildLabels(results, masterCh, labelField, chField)
186+
% Build labels for master channel set from the first result that has them
187+
188+
labels = arrayfun(@(c) sprintf('Ch%d', c), masterCh, 'UniformOutput', false);
189+
190+
for k = 1:length(results)
191+
if isfield(results{k}, labelField) && ~isempty(results{k}.(labelField))
192+
subCh = results{k}.(chField)(:)';
193+
subLabels = results{k}.(labelField);
194+
if iscell(subLabels)
195+
[~, mIdx, sIdx] = intersect(masterCh, subCh);
196+
labels(mIdx) = subLabels(sIdx);
197+
end
198+
break;
199+
end
200+
end
201+
end

0 commit comments

Comments
 (0)