Task 658: .SFH File Format

Task 658: .SFH File Format

1. Properties of the .SFH File Format Intrinsic to Its File System

The .SFH file format is an ASCII text-based format primarily used in EEG and MEG analysis software such as BrainVoyager QX and BESA Research for storing head surface points and coregistration information. Based on the specifications from BrainVoyager documentation, the intrinsic properties of the format, as defined by its structure and content organization, are as follows:

  • File Type: ASCII text file, line-delimited (typically LF or CRLF line endings).
  • Encoding: Plain ASCII or UTF-8 compatible, with numeric values represented as floating-point strings.
  • Header: The first line contains a single integer (N), representing the total number of surface points (including 3 fixed fiducials + additional surface points such as electrodes or coils).
  • Fiducial Points Section: The next 3 lines correspond to fixed fiducials in order: Nasion, Left Auricular Point, Right Auricular Point. Each line contains at least 3 floating-point values (X, Y, Z coordinates in millimeters).
  • Surface Points Section: The subsequent (N - 3) lines contain coordinates for surface points (e.g., EEG electrodes or MEG positioning coils). Each line contains at least 3 floating-point values (X, Y, Z in millimeters), optionally followed by additional floating-point values for display attributes (e.g., radius, color components).
  • Coregistration Transformation Section (optional, appended post-processing): A section with affine transformation parameters, typically 12 floating-point values representing a 3D affine matrix (3x3 rotation/scaling matrix + 3 translation vectors).
  • Coordinate System: Coordinates are initially in the head or device coordinate system (millimeters); post-coregistration, they are transformed via the appended affine matrix.
  • File Size Constraints: No strict limits, but designed for small files (hundreds of lines for typical EEG setups with 64-256 channels).
  • Validation Rules: The first 3 points must be fiducials in fixed order; total points must match the header integer N.

These properties ensure compatibility for head-MRI coregistration in distributed source modeling.

Direct public download links for .SFH files are limited due to their specialized use in proprietary EEG/MEG software and the proprietary nature of sample datasets. Extensive searches across repositories, academic sites, and software documentation did not yield freely accessible direct links to standalone .SFH files. However, sample datasets from relevant software include .SFH files and can be downloaded as archives. The following are two such resources containing .SFH files:

These archives can be extracted to obtain the .SFH files.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .SFH File Parsing

The following is a self-contained HTML snippet with embedded JavaScript, suitable for embedding in a Ghost blog post (use the HTML card in the editor). It enables drag-and-drop of a .SFH file, parses it according to the format, and dumps all properties to the screen in a formatted div. It assumes the simple structure (header N, N lines of X Y Z floats) for core properties; additional display params or transformation are noted if present.

Drag and drop a .SFH file here to parse its properties.

This code handles parsing and display; for writing, a save button could be added, but the task focuses on dumping properties.

4. Python Class for .SFH File Handling

The following Python class opens a .SFH file, decodes/reads its properties, prints them to console, and supports writing the properties back to a file.

import sys

class SFHFile:
    def __init__(self, filename):
        self.filename = filename
        self.n_points = 0
        self.fiducials = []  # List of [X, Y, Z]
        self.surface_points = []  # List of [X, Y, Z, ...]
        self.transformation = []  # List of 12 floats if present
        self.read()

    def read(self):
        try:
            with open(self.filename, 'r') as f:
                lines = f.readlines()
            if not lines:
                print("Error: Empty file.", file=sys.stderr)
                return
            self.n_points = int(lines[0].strip())
            if len(lines) < self.n_points + 1:
                print("Error: Insufficient lines for N points.", file=sys.stderr)
                return
            # Parse fiducials and surface points
            for i in range(self.n_points):
                line = lines[i + 1].strip()
                parts = [float(p) for p in line.split()]
                if len(parts) < 3:
                    print(f"Error: Invalid point line {i+1}.", file=sys.stderr)
                    return
                xyz = parts[:3]
                if i < 3:
                    self.fiducials.append(xyz)
                else:
                    self.surface_points.append(parts)  # Include extra for display
            # Parse transformation if present
            extra = ' '.join([l.strip() for l in lines[self.n_points + 1:]]).split()
            if extra and all(not e.isalpha() for e in extra):  # Assume numeric
                self.transformation = [float(e) for e in extra[:12]]
            self.print_properties()
        except Exception as e:
            print(f"Error reading file: {e}", file=sys.stderr)

    def print_properties(self):
        print("SFH File Properties:")
        print(f"Total Surface Points (N): {self.n_points}")
        fid_names = ["Nasion", "Left Auricular", "Right Auricular"]
        for i, fid in enumerate(self.fiducials):
            print(f"{fid_names[i]}: X={fid[0]:.2f} mm, Y={fid[1]:.2f} mm, Z={fid[2]:.2f} mm")
        print(f"\nSurface Points ({len(self.surface_points)} points):")
        for idx, point in enumerate(self.surface_points, 1):
            print(f"Point {idx}: X={point[0]:.2f} mm, Y={point[1]:.2f} mm, Z={point[2]:.2f} mm" +
                  (f" (Extra: {', '.join(f'{p:.2f}' for p in point[3:])})" if len(point) > 3 else ""))
        if self.transformation:
            print("\nCoregistration Affine Transformation (12 params):")
            for i, param in enumerate(self.transformation):
                print(f"Param {i+1}: {param:.4f}")

    def write(self, output_filename):
        try:
            with open(output_filename, 'w') as f:
                f.write(f"{self.n_points}\n")
                for fid in self.fiducials:
                    f.write(f"{fid[0]} {fid[1]} {fid[2]}\n")
                for point in self.surface_points:
                    f.write(' '.join(f"{p}" for p in point) + '\n')
                if self.transformation:
                    f.write(' '.join(f"{p}" for p in self.transformation) + '\n')
            print(f"Properties written to {output_filename}")
        except Exception as e:
            print(f"Error writing file: {e}", file=sys.stderr)

# Example usage: SFHFile('example.sfh').write('output.sfh')

To use, instantiate with a filename; it reads and prints automatically. Call write('new.sfh') to save.

5. Java Class for .SFH File Handling

The following Java class opens a .SFH file, decodes/reads its properties, prints them to console, and supports writing the properties back to a file. Compile with javac SFHFile.java and run with java SFHFile example.sfh.

import java.io.*;
import java.util.*;

public class SFHFile {
    private String filename;
    private int nPoints;
    private List<double[]> fiducials = new ArrayList<>(); // 3 points
    private List<double[]> surfacePoints = new ArrayList<>(); // N-3 points
    private List<Double> transformation = new ArrayList<>(); // Up to 12 params

    public SFHFile(String filename) {
        this.filename = filename;
        read();
    }

    private void read() {
        try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
            String line = br.readLine();
            if (line == null) {
                System.err.println("Error: Empty file.");
                return;
            }
            nPoints = Integer.parseInt(line.trim());
            for (int i = 0; i < nPoints; i++) {
                line = br.readLine();
                if (line == null) {
                    System.err.println("Error: Insufficient lines.");
                    return;
                }
                String[] parts = line.trim().split("\\s+");
                double[] point = new double[parts.length];
                for (int j = 0; j < parts.length; j++) {
                    point[j] = Double.parseDouble(parts[j]);
                }
                if (i < 3) {
                    fiducials.add(point);
                } else {
                    surfacePoints.add(point);
                }
            }
            // Parse transformation from remaining lines
            StringBuilder extra = new StringBuilder();
            while ((line = br.readLine()) != null) {
                extra.append(line).append(" ");
            }
            if (extra.length() > 0) {
                String[] extras = extra.toString().trim().split("\\s+");
                for (int i = 0; i < Math.min(12, extras.length); i++) {
                    transformation.add(Double.parseDouble(extras[i]));
                }
            }
            printProperties();
        } catch (Exception e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }

    private void printProperties() {
        System.out.println("SFH File Properties:");
        System.out.println("Total Surface Points (N): " + nPoints);
        String[] fidNames = {"Nasion", "Left Auricular", "Right Auricular"};
        for (int i = 0; i < fiducials.size(); i++) {
            double[] fid = fiducials.get(i);
            System.out.printf("%s: X=%.2f mm, Y=%.2f mm, Z=%.2f mm", fidNames[i], fid[0], fid[1], fid[2]);
            if (fid.length > 3) {
                System.out.print(" (Extra: " + Arrays.toString(Arrays.copyOfRange(fid, 3, fid.length)) + ")");
            }
            System.out.println();
        }
        System.out.println("\nSurface Points (" + surfacePoints.size() + " points):");
        for (int i = 0; i < surfacePoints.size(); i++) {
            double[] point = surfacePoints.get(i);
            System.out.printf("Point %d: X=%.2f mm, Y=%.2f mm, Z=%.2f mm", i + 1, point[0], point[1], point[2]);
            if (point.length > 3) {
                System.out.print(" (Extra: " + Arrays.toString(Arrays.copyOfRange(point, 3, point.length)) + ")");
            }
            System.out.println();
        }
        if (!transformation.isEmpty()) {
            System.out.println("\nCoregistration Affine Transformation (12 params):");
            for (int i = 0; i < transformation.size(); i++) {
                System.out.printf("Param %d: %.4f%n", i + 1, transformation.get(i));
            }
        }
    }

    public void write(String outputFilename) {
        try (PrintWriter pw = new PrintWriter(new FileWriter(outputFilename))) {
            pw.println(nPoints);
            for (double[] fid : fiducials) {
                for (double val : fid) pw.print(val + " ");
                pw.println();
            }
            for (double[] point : surfacePoints) {
                for (double val : point) pw.print(val + " ");
                pw.println();
            }
            if (!transformation.isEmpty()) {
                for (double val : transformation) pw.print(val + " ");
                pw.println();
            }
            System.out.println("Properties written to " + outputFilename);
        } catch (Exception e) {
            System.err.println("Error writing file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java SFHFile <filename.sfh>");
            return;
        }
        SFHFile sfh = new SFHFile(args[0]);
        // Example: sfh.write("output.sfh");
    }
}

6. JavaScript Class for .SFH File Handling

The following Node.js-compatible JavaScript class opens a .SFH file, decodes/reads its properties, prints them to console, and supports writing the properties back to a file. Run with Node.js (e.g., node sfh.js example.sfh).

const fs = require('fs');

class SFHFile {
  constructor(filename) {
    this.filename = filename;
    this.nPoints = 0;
    this.fiducials = []; // Array of [X, Y, Z]
    this.surfacePoints = []; // Array of [X, Y, Z, ...]
    this.transformation = []; // Array of 12 numbers if present
    this.read();
  }

  read() {
    try {
      const text = fs.readFileSync(this.filename, 'utf8');
      const lines = text.trim().split('\n');
      if (lines.length === 0) {
        console.error('Error: Empty file.');
        return;
      }
      this.nPoints = parseInt(lines[0]);
      if (isNaN(this.nPoints) || lines.length < this.nPoints + 1) {
        console.error('Error: Invalid header or insufficient lines.');
        return;
      }
      // Parse points
      for (let i = 0; i < this.nPoints; i++) {
        const parts = lines[i + 1].trim().split(/\s+/).map(p => parseFloat(p));
        if (parts.length < 3) {
          console.error(`Error: Invalid point line ${i + 1}.`);
          return;
        }
        if (i < 3) {
          this.fiducials.push(parts.slice(0, 3)); // Only XYZ for fiducials
        } else {
          this.surfacePoints.push(parts);
        }
      }
      // Parse transformation
      const extraText = lines.slice(this.nPoints + 1).join(' ').trim();
      if (extraText) {
        const extras = extraText.split(/\s+/).map(p => parseFloat(p)).filter(n => !isNaN(n));
        this.transformation = extras.slice(0, 12);
      }
      this.printProperties();
    } catch (e) {
      console.error('Error reading file:', e.message);
    }
  }

  printProperties() {
    console.log('SFH File Properties:');
    console.log(`Total Surface Points (N): ${this.nPoints}`);
    const fidNames = ['Nasion', 'Left Auricular', 'Right Auricular'];
    for (let i = 0; i < this.fiducials.length; i++) {
      const fid = this.fiducials[i];
      let line = `${fidNames[i]}: X=${fid[0].toFixed(2)} mm, Y=${fid[1].toFixed(2)} mm, Z=${fid[2].toFixed(2)} mm`;
      console.log(line);
    }
    console.log(`\nSurface Points (${this.surfacePoints.length} points):`);
    for (let i = 0; i < this.surfacePoints.length; i++) {
      const point = this.surfacePoints[i];
      let line = `Point ${i + 1}: X=${point[0].toFixed(2)} mm, Y=${point[1].toFixed(2)} mm, Z=${point[2].toFixed(2)} mm`;
      if (point.length > 3) line += ` (Extra: ${point.slice(3).map(p => p.toFixed(2)).join(', ')})`;
      console.log(line);
    }
    if (this.transformation.length > 0) {
      console.log('\nCoregistration Affine Transformation (12 params):');
      this.transformation.forEach((param, i) => console.log(`Param ${i + 1}: ${param.toFixed(4)}`));
    }
  }

  write(outputFilename) {
    try {
      let content = `${this.nPoints}\n`;
      this.fiducials.forEach(fid => {
        content += `${fid[0]} ${fid[1]} ${fid[2]}\n`;
      });
      this.surfacePoints.forEach(point => {
        content += point.map(p => p.toString()).join(' ') + '\n';
      });
      if (this.transformation.length > 0) {
        content += this.transformation.join(' ') + '\n';
      }
      fs.writeFileSync(outputFilename, content);
      console.log(`Properties written to ${outputFilename}`);
    } catch (e) {
      console.error('Error writing file:', e.message);
    }
  }
}

// Example usage
if (require.main === module) {
  if (process.argv.length < 3) {
    console.error('Usage: node sfh.js <filename.sfh>');
    process.exit(1);
  }
  const sfh = new SFHFile(process.argv[2]);
  // sfh.write('output.sfh');
}

7. C Class for .SFH File Handling

The following C implementation (struct-based "class") opens a .SFH file, decodes/reads its properties, prints them to console, and supports writing the properties back to a file. Compile with gcc sfh.c -o sfh and run with ./sfh example.sfh.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_POINTS 1024
#define MAX_LINE 256
#define FIDUCIAL_COUNT 3
#define TRANSFORM_COUNT 12

typedef struct {
    char filename[256];
    int n_points;
    double fiducials[FIDUCIAL_COUNT][3];
    double surface_points[MAX_POINTS - FIDUCIAL_COUNT][MAX_LINE / 10]; // Flexible for extra
    int surface_extra[MAX_POINTS - FIDUCIAL_COUNT]; // Number of extra per point
    double transformation[TRANSFORM_COUNT];
    int has_transform;
} SFHFile;

void read_sfh(SFHFile *sfh) {
    FILE *fp = fopen(sfh->filename, "r");
    if (!fp) {
        fprintf(stderr, "Error opening file %s\n", sfh->filename);
        return;
    }
    char line[MAX_LINE];
    if (!fgets(line, sizeof(line), fp)) {
        fprintf(stderr, "Error: Empty file.\n");
        fclose(fp);
        return;
    }
    sfh->n_points = atoi(line);
    if (sfh->n_points > MAX_POINTS || sfh->n_points < FIDUCIAL_COUNT) {
        fprintf(stderr, "Error: Invalid N (%d).\n", sfh->n_points);
        fclose(fp);
        return;
    }
    for (int i = 0; i < sfh->n_points; i++) {
        if (!fgets(line, sizeof(line), fp)) {
            fprintf(stderr, "Error: Insufficient lines.\n");
            fclose(fp);
            return;
        }
        char *token = strtok(line, " \t\n");
        int col = 0;
        double vals[32]; // Max cols per line
        while (token && col < 32) {
            vals[col++] = atof(token);
            token = strtok(NULL, " \t\n");
        }
        if (col < 3) {
            fprintf(stderr, "Error: Invalid point line %d.\n", i + 1);
            fclose(fp);
            return;
        }
        if (i < FIDUCIAL_COUNT) {
            for (int j = 0; j < 3; j++) {
                sfh->fiducials[i][j] = vals[j];
            }
        } else {
            int idx = i - FIDUCIAL_COUNT;
            for (int j = 0; j < 3; j++) {
                sfh->surface_points[idx][j] = vals[j];
            }
            sfh->surface_extra[idx] = col - 3;
            for (int j = 3; j < col; j++) {
                sfh->surface_points[idx][j] = vals[j];
            }
        }
    }
    // Parse transformation
    sfh->has_transform = 0;
    double temp[TRANSFORM_COUNT];
    int count = 0;
    while (fgets(line, sizeof(line), fp) && count < TRANSFORM_COUNT) {
        char *token = strtok(line, " \t\n");
        while (token && count < TRANSFORM_COUNT) {
            temp[count++] = atof(token);
            token = strtok(NULL, " \t\n");
        }
    }
    if (count >= TRANSFORM_COUNT) {
        sfh->has_transform = 1;
        for (int j = 0; j < TRANSFORM_COUNT; j++) {
            sfh->transformation[j] = temp[j];
        }
    }
    fclose(fp);
    print_properties(sfh);
}

void print_properties(SFHFile *sfh) {
    printf("SFH File Properties:\n");
    printf("Total Surface Points (N): %d\n", sfh->n_points);
    const char *fid_names[] = {"Nasion", "Left Auricular", "Right Auricular"};
    for (int i = 0; i < FIDUCIAL_COUNT; i++) {
        printf("%s: X=%.2f mm, Y=%.2f mm, Z=%.2f mm\n",
               fid_names[i], sfh->fiducials[i][0], sfh->fiducials[i][1], sfh->fiducials[i][2]);
    }
    printf("\nSurface Points (%d points):\n", sfh->n_points - FIDUCIAL_COUNT);
    for (int i = 0; i < sfh->n_points - FIDUCIAL_COUNT; i++) {
        printf("Point %d: X=%.2f mm, Y=%.2f mm, Z=%.2f mm",
               i + 1, sfh->surface_points[i][0], sfh->surface_points[i][1], sfh->surface_points[i][2]);
        if (sfh->surface_extra[i] > 0) {
            printf(" (Extra: ");
            for (int j = 0; j < sfh->surface_extra[i]; j++) {
                printf("%.2f ", sfh->surface_points[i][3 + j]);
            }
            printf(")");
        }
        printf("\n");
    }
    if (sfh->has_transform) {
        printf("\nCoregistration Affine Transformation (12 params):\n");
        for (int i = 0; i < TRANSFORM_COUNT; i++) {
            printf("Param %d: %.4f\n", i + 1, sfh->transformation[i]);
        }
    }
}

void write_sfh(SFHFile *sfh, const char *output_filename) {
    FILE *fp = fopen(output_filename, "w");
    if (!fp) {
        fprintf(stderr, "Error opening output file %s\n", output_filename);
        return;
    }
    fprintf(fp, "%d\n", sfh->n_points);
    for (int i = 0; i < FIDUCIAL_COUNT; i++) {
        for (int j = 0; j < 3; j++) {
            fprintf(fp, "%.6f ", sfh->fiducials[i][j]);
        }
        fprintf(fp, "\n");
    }
    for (int i = 0; i < sfh->n_points - FIDUCIAL_COUNT; i++) {
        for (int j = 0; j < 3 + sfh->surface_extra[i]; j++) {
            fprintf(fp, "%.6f ", sfh->surface_points[i][j]);
        }
        fprintf(fp, "\n");
    }
    if (sfh->has_transform) {
        for (int j = 0; j < TRANSFORM_COUNT; j++) {
            fprintf(fp, "%.6f ", sfh->transformation[j]);
        }
        fprintf(fp, "\n");
    }
    fclose(fp);
    printf("Properties written to %s\n", output_filename);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename.sfh>\n", argv[0]);
        return 1;
    }
    SFHFile sfh = {0};
    strcpy(sfh.filename, argv[1]);
    read_sfh(&sfh);
    // Example: write_sfh(&sfh, "output.sfh");
    return 0;
}

This implementation uses dynamic allocation limits suitable for typical files; extend MAX_POINTS if needed for larger datasets.