Skip to content

sEEG contacts

The sEEG contacts extension is a contact editor for stereo-EEG depth electrodes: open a registered CT and the BIDS electrodes.tsv that was localised on it, fix what the localiser got wrong, and write the table back — reversibly, with a backup and a provenance sidecar. It reproduces the 3D Slicer SEEG Contact Editor workflow (seegprep's slicer/SEEGContactEditor) in Tetravox's own panes, and reads and writes the same files, so the two can be used on the same subject interchangeably.

It is an extension like any other (tetravox.seeg): nothing ships inside Tetravox, so you download it once from File ▸ Extensions…, read the permission sheet, and enable it — after which it is in the toolbar's switcher (, right of the panes), and opening one of its files is enough to bring it up. Its own source, and the deeper reference for what it does, live at github.com/idossha/tetravox-seeg.

Opening a subject

Drop, or Open…, either of these and the extension finds the other beside it:

FileWhere
the registered CTderivatives/seegprep/sub-<id>/ct/sub-<id>_acq-bone_space-T1w_ct.nii.gz
the electrodes tablederivatives/seegprep/sub-<id>/ieeg/sub-<id>_space-T1w_electrodes.tsv

From the CT it also looks for the _coordsystem.json, an existing _editlog.json, and the subject's T1 at derivatives/SimNIBS/sub-<id>/m2m_<id>/T1.nii.gz. Nothing is searched for: the extension knows those four names and asks whether each one exists.

Opening the table first is fine — it is read and held until a volume arrives, and the panel says so. The CT has to be open for anything that needs image intensities (that is Snap), because an extension reads the volume through the app rather than opening files itself.

The reader is deliberately forgiving. It detects tab, comma, semicolon or whitespace; strips a UTF-8 BOM; matches column names case-insensitively (name/label, x/pos_x/x_mm, or R/A/S); takes the electrode from electrode, group, shaft or lead, or infers it by stripping the trailing digits off the contact name (LHIP8LHIP); and truncates a ragged row rather than refusing the file. A 3D Slicer .fcsv markups file works too, LPS coordinates and all. A missing required column is the one thing it refuses, and the message names the delimiter it detected and the columns it found.

On load the CT is set the way the Slicer editor sets it — grey, fully opaque, and everything below 150 HU hidden, so soft tissue drops away and bone and metal are what is left. Colormap, opacity and the intensity floor stay in the ordinary volume-layer editor on the left; the extension sets them once and then leaves them to you. If a T1 is loaded above the CT in the layer list, raise the CT above it — the floor only reveals what is underneath.

If an _editlog.json already sits beside the table, the panel shows a banner saying when it was hand-edited: somebody has been here before you.

Editing

The contacts are one points layer named Contacts · <table stem>, one dot per contact, with the shaft drawn as a line between consecutive contacts and each contact's name beside it. The dot, its shaft line and its name are all the electrode's own colour, so on a fifteen-shaft implant you can tell at a glance which line belongs to which contact. Contacts that are not on the current slice are drawn as ghosts at 0.6 opacity so a shaft reads as a shaft while you scroll; g turns that off and on.

Three switches decide how much of that is drawn, and none of them touches the table:

SwitchDoes
Ghost (g)draw the contacts that are not on this slice, faintly
Wire (d)draw the shaft lines. Off is for a figure about one slice's contacts
size − / +how big a contact is drawn, 2–12 px. The bigger dot is also a bigger click target

All three are saved with the scene, so a figure reopens looking the way you left it, and all three are job-file operations (ghost, wire, size) — which of them are on is part of what a figure is.

Do thisWith
select a contactclick it in a pane — ghosts included — or click its row in the list
move onedrag it in a 2D pane, once the slice is on it
add contactsAdd (a) — then every click in a pane drops a new contact on the chosen electrode
walk the electroden / p, or the list — the crosshair follows, so every pane slices through the contact
snap to the metals for the selected contact, ⇧S for the whole electrode, Snap all… for every one
re-fit the shaftf
renumber from the tipRenumber tip-first
flip which end is the tipt
deleteDelete or
undo / redoz / ⇧Z

Clicking a contact selects it, and everything follows: the electrode dropdown switches to that contact's shaft, the crosshair moves onto it so every pane slices through it, and a ring is drawn round the one you have. You do not arm anything first — while the panel is open, clicking contacts is what a click does, and Esc puts you back into selecting rather than turning the tool off. A click that is not on a contact still moves the crosshair, exactly as it does with no extension open.

Clicking a ghosted contact jumps the slice to it. A ghost is a contact that lives on another slice, so there is no sensible way to drag one — it would move in a plane it is not in. Clicking one therefore does the useful half instead: it selects that contact and takes the crosshair there, so every pane re-cuts through it. The contact you clicked is now on the slice, and a second click grabs it in the ordinary way. In practice you click the marker you can see, the view comes to it, and you drag from there — you never have to scroll onto a contact first to be able to pick it.

Snap moves a contact to the intensity-weighted peak of a small box around it — the metal it is inside — at the radius the panel's field sets (0.5–5 mm, 1.5 mm by default). A contact with nothing bright near it does not move and is not counted. Snap all asks first, because it touches every electrode at once; one snap of any scope is a single undo step.

Re-fit shaft fits a line through the electrode's contacts, projects them onto it, re-spaces them evenly at the median observed gap — median, so one missing contact does not stretch the rest — and relabels them from the tip. It reports the line RMS and the spacing CV, which are the two numbers that say whether the shaft is straight and evenly spaced.

Numbering only ever changes when you ask. Loading, placing, dragging, snapping and deleting all leave every contact's number and name exactly as they were — a clinical table's numbering is wired to the recording system through its csc column, and nothing should renumber it behind your back. Only Re-fit and Renumber tip-first relabel, and both say so on the button. New names keep the zero-padding the file used (LINS01, not LINS1).

Which end is the tip is a heuristic, and the panel shows the answer: contact 1 is the end of the shaft nearer the centre of the volume, and the other end is the entry. That is right for nearly every depth electrode and wrong for some — a shaft entering near the midline can defeat it — so the tip contact is marked in the list and t flips it. A flip is remembered per electrode and saved with the scene.

Saving

Save writes the table back over the file it came from; Save as… picks a new one. Either way three things happen, in this order:

  1. the previous table is copied to <name>.<YYYYMMDD-HHMMSS>.bak;
  2. the table is written — tab-separated, LF, your original columns in their original order, with electrode, contact and status appended if they were not already there. status is kept, edited (moved by more than 0.001 mm) or added; a row that has not moved keeps whatever status the localiser gave it, so located and gapfilled survive;
  3. <stem>_editlog.json is written beside it, recording what changed — counts, and one entry per contact added, moved, renamed or deleted, with where it was and where it is now. Renumber and Re-fit relabel contacts that may not have moved at all, and those entries carry the name the table had (renamed_from) beside the name it has now: relabelling is the one edit that changes how the csc column maps onto your recording system, so an editlog silent about it would be lying.

That editlog name matters: seegprep looks for *_electrodes_editlog.json in the subject's ieeg/ directory and refuses to re-run over a hand-edited subject unless you pass --force. If you save under a name whose stem does not end in _electrodes, or outside an ieeg/ directory, the extension warns you that the guard will not see it.

Revert to loaded positions puts every contact back where the file had it and forgets the additions, which is the in-session undo of everything; the .bak is the on-disk one.

⌘S saves the scene, not the table. When contacts are unsaved the extension says so, the window title carries a , and closing the window, starting a new scene, opening another one or closing the CT all ask first.

Scenes, and a build without the extension

The contacts are ordinary scene layers, so a *.tetravox.json written here opens anywhere — including in a build that has no sEEG extension, which still draws every contact with its name, its electrode and its number. What that build cannot carry is the extension's own record: which table the contacts came from, where that table put each one, and its other columns. Re-open such a scene here and the extension rebuilds the electrodes from the layer, tells you the provenance is gone, and turns Save into Save as… rather than writing a table in which everything looks new.

From a job file

Every button is also a job-file operation, so a batch can do what the panel does — load, snap, refit, renumber, flip-tip, revert, delete, ghost, wire, size, stats and save. flip-tip matters more than it looks: which end of a shaft is contact 1 comes from a heuristic, renumber applies whatever the tip currently is, and this is how a batch corrects the shaft the heuristic read backwards — the same thing t does in the panel. See Automation.

MIT licensed. macOS and Linux first; a Windows build is published too.