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.
2. Two Direct Download Links for Files of Format .SFH
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:
- BrainVoyager Sample Data Set (includes EEG example with .SFH for 78-channel configuration): https://www.brainvoyager.com/bv/sampledata/EEG-MEG-Data.zip
- BESA Research Tutorial Dataset (includes .SFH for coregistration examples): https://www.besa.de/wp-content/uploads/2020/10/BESA-Research-7.1-Tutorial-Data.zip
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.