49constexpr double CSD_PI = 3.14159265358979323846;
56MatrixXd SurfaceLaplacian::evaluateLegendre(
const MatrixXd& matX,
int iMaxOrder)
60 const Index nElements = matX.size();
61 MatrixXd result(iMaxOrder + 1, nElements);
64 result.row(0).setOnes();
68 for (Index i = 0; i < nElements; ++i)
69 result(1, i) = matX.data()[i];
73 for (
int n = 1; n < iMaxOrder; ++n) {
74 const double a =
static_cast<double>(2 * n + 1) /
static_cast<double>(n + 1);
75 const double b =
static_cast<double>(n) /
static_cast<double>(n + 1);
76 for (Index i = 0; i < nElements; ++i)
77 result(n + 1, i) = a * matX.data()[i] * result(n, i) - b * result(n - 1, i);
85MatrixXd SurfaceLaplacian::computeG(
const MatrixXd& matCosAng,
90 const Index nRows = matCosAng.rows();
91 const Index nCols = matCosAng.cols();
94 MatrixXd legP = evaluateLegendre(matCosAng, iNLegendreTerms);
97 MatrixXd G = MatrixXd::Zero(nRows, nCols);
99 for (
int n = 1; n <= iNLegendreTerms; ++n) {
100 const double factor =
static_cast<double>(2 * n + 1)
101 / (std::pow(
static_cast<double>(n), iStiffness)
102 * std::pow(
static_cast<double>(n + 1), iStiffness)
105 for (Index i = 0; i < nRows * nCols; ++i)
106 G.data()[i] += factor * legP(n, i);
114MatrixXd SurfaceLaplacian::computeH(
const MatrixXd& matCosAng,
119 const Index nRows = matCosAng.rows();
120 const Index nCols = matCosAng.cols();
122 MatrixXd legP = evaluateLegendre(matCosAng, iNLegendreTerms);
124 MatrixXd H = MatrixXd::Zero(nRows, nCols);
126 for (
int n = 1; n <= iNLegendreTerms; ++n) {
127 const double factor =
static_cast<double>(2 * n + 1)
128 / (std::pow(
static_cast<double>(n), iStiffness - 1)
129 * std::pow(
static_cast<double>(n + 1), iStiffness - 1)
132 for (Index i = 0; i < nRows * nCols; ++i)
133 H.data()[i] += factor * legP(n, i);
145 double dSphereRadius)
147 const int nCh =
static_cast<int>(matPositions.rows());
149 qWarning() <<
"[SurfaceLaplacian::computeTransform] Need at least 2 channels.";
154 Vector3d centre = matPositions.colwise().mean();
155 MatrixX3d posCentered = matPositions.rowwise() - centre.transpose();
158 if (dSphereRadius <= 0.0) {
160 for (
int i = 0; i < nCh; ++i)
161 dSphereRadius += posCentered.row(i).norm();
162 dSphereRadius /=
static_cast<double>(nCh);
166 MatrixX3d posNorm(nCh, 3);
167 for (
int i = 0; i < nCh; ++i) {
168 double norm = posCentered.row(i).norm();
170 posNorm.row(i) = posCentered.row(i) / norm;
172 posNorm.row(i).setZero();
176 MatrixXd cosAng = posNorm * posNorm.transpose();
179 cosAng = cosAng.cwiseMax(-1.0).cwiseMin(1.0);
182 MatrixXd G = computeG(cosAng, iStiffness, iNLegendreTerms);
183 MatrixXd H = computeH(cosAng, iStiffness, iNLegendreTerms);
186 for (
int i = 0; i < nCh; ++i)
190 MatrixXd Gi = G.inverse();
193 VectorXd TC = Gi.colwise().sum();
194 double sgi = TC.sum();
198 MatrixXd
Z = MatrixXd::Identity(nCh, nCh);
199 Z.array() -= 1.0 /
static_cast<double>(nCh);
202 MatrixXd Cp2 = Gi *
Z;
205 RowVectorXd c02 = Cp2.colwise().sum() / sgi;
208 MatrixXd C2 = Cp2 - TC * c02;
211 MatrixXd
X = (H.transpose() * C2) / (dSphereRadius * dSphereRadius);
219 const MatrixX3d& matPositions,
223 double dSphereRadius)
227 if (matData.rows() != matPositions.rows()) {
228 qWarning() <<
"[SurfaceLaplacian::compute] Data rows" << matData.rows()
229 <<
"!= position rows" << matPositions.rows();
234 iNLegendreTerms, dSphereRadius);
Spherical-spline surface Laplacian (Current Source Density) for EEG.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Result of a surface Laplacian (CSD) computation.
Eigen::MatrixXd matData
Transformed data (n_eeg_channels × n_times).
Eigen::MatrixXd matTransform
CSD transformation matrix (n_eeg × n_eeg).
static SurfaceLaplacianResult compute(const Eigen::MatrixXd &matData, const Eigen::MatrixX3d &matPositions, double dLambda2=1e-5, int iStiffness=4, int iNLegendreTerms=50, double dSphereRadius=-1.0)
static Eigen::MatrixXd computeTransform(const Eigen::MatrixX3d &matPositions, double dLambda2=1e-5, int iStiffness=4, int iNLegendreTerms=50, double dSphereRadius=-1.0)