Stars2Cells logo

Documentation

Release • v1.0.0
Early release. This page reflects an early release of S2C. APIs, file formats, and outputs may change. There is currently no bundled example dataset; documentation focuses on concepts, expected I/O, and parameter tuning.

In this guide

The core idea

Stars2Cells matches neurons across imaging sessions the way astronomers plate-solve the night sky — by encoding the relative geometry of small constellations of points into descriptors that don't change when the field rotates, translates, or shifts. Identifying any single neuron is hopeless; identifying four of them at a time is robust.

The unit of information is a quad: four neurons, with one pair forming the longest edge (the diagonal) and the other two as third-points sitting off to either side. Project the third-points into a coordinate frame defined by the diagonal, normalize by the diagonal's length, and you get a four-number signature that is invariant to translation, rotation, and uniform scale.

QUAD IN SESSION A A B C D L = ‖B − A‖ PROJECT & NORMALIZE q ∈ ℝ⁴ QUAD IN SESSION B (ROTATED) A B C D same q ∈ ℝ⁴
Figure 1.The quad descriptor. A and B are the diagonal endpoints (longest edge); C and D are the third-points. Their coordinates after projection into the diagonal-aligned frame and division by L = ‖B − A‖ give a four-number vector that is identical in both panels despite the rotation.

Two sessions' quads are then directly comparable in ℝ⁴ without ever solving for the unknown transform between them. Once enough quads agree, the transform falls out of them — recovered by RANSAC, used to convert quad-level agreement into a one-to-one neuron assignment, and finally chained transitively across sessions to produce stable global identities.

S2C is robust to local non-linearities because it does not rely on a single global transform. Quads are sampled across spatial scales — local neighbourhoods stabilize fine structure, long-range diagonals span the field of view — so the consensus emerges from many small, independent geometric witnesses.

The pipeline at a glance

Five steps. Each step writes a self-contained file that the next step consumes; nothing is kept in memory across steps, so any stage can be re-run in isolation by deleting its outputs.

STEP 1 Quad generation centroids → quads STEP 1.5 Calibration τ = C·√N STEP 2 Descriptor match FAISS NN search STEP 2.5 RANSAC geometric filter STEP 3 Hungarian + tracking _centroids_quads.npz τ per animal _matches_light.npz _filtered_matches.npz _sweep.npz _consolidated_tracking.npz in: .npy centroids out: cross-session identities
Figure 2.The five pipeline stages and the file each one writes. Steps run end-to-end via the GUI or programmatically; each stage caches its results so re-runs only redo what changed.

Step 1 → 1.5

Per-session quad descriptors are generated once. Step 1.5 then samples a fixed number of quads per pair, sweeps a similarity threshold τ ∈ [0, 1], and fits the scaling law τ = C · √N across an animal's session pairs. C is the per-animal calibration constant; it propagates into Step 2.

Step 2 → 2.5 → 3

Step 2 runs FAISS top-1 nearest neighbour search in descriptor space, gated by τ and a geometric consistency filter. Step 2.5 runs vectorised RANSAC over the surviving matches (1,000 candidate transforms estimated in one batched SVD) to find the rigid transform and reject outliers. Step 3 turns the inlier quad matches into a one-to-one neuron assignment via padded Hungarian, with a confidence score per pair and per chained track.

Inputs

S2C operates on centroid-based neuron representations. One .npy file per session, all in a single input directory. The pipeline accepts two formats: a Python dict (recommended) or a raw 3D footprint matrix (the A matrix from CNMF / Suite2p / EXTRACT). Footprints, traces, and other ROI metadata are allowed alongside the centroids, but the pipeline ignores them.

File naming

Filenames must match the pattern {animal_id}_{session_id}{optional_suffix}.npy. The default regex is:

^([A-Za-z0-9_]+?)_(\d+)(.*?)\.npy$

where animal_id is alphanumeric plus underscores (non-greedy), session_id is one or more digits, and the optional suffix is anything else before .npy. The suffix is preserved as part of the session name and is available to the Step 1.5 group regex for FOV grouping.

FilenameMatch?AnimalSession
408021_758519303.npy✓408021758519303
mouse_a_3__fov1.npy✓mouse_a3
Mouse42_3.npy✓Mouse423
mouse1_session1.npy✗session id is not all digits
408021-758519303.npy✗wrong separator (hyphen not underscore)

Heads-up: if your animal id ends in a digit (e.g. mouse5_3.npy), the non-greedy +? can split at the wrong underscore. Prefer animal ids that end in a letter or underscore.

Format 1 — dictionary (recommended)

A Python dict saved with np.save(..., allow_pickle=True).

Required keys

  • centroids_x — array/list of N floats (X coords, pixels)
  • centroids_y — array/list of N floats (Y coords, pixels)

Optional & ignored

  • roi_ids — N unique IDs; defaults to np.arange(N) if absent
  • Anything else — subject_id, session_id, footprints, traces, timestamps. Allowed but unread; animal and session names come from the filename, not the dict.
Reference snippet — write a session file
import numpy as np data = { 'centroids_x': np.array([123.4, 456.7, 789.0, 234.1]), 'centroids_y': np.array([234.5, 567.8, 890.1, 345.2]), 'roi_ids': np.array([0, 1, 2, 3]), # optional } # filename: animal_id ends before the first _<digits> segment np.save('408021_758519303.npy', data, allow_pickle=True)

Format 2 — raw 3D footprint matrix

A 3D numpy array of shape (H, W, N) — the spatial footprints for N neurons. The pipeline auto-extracts centroids and assigns sequential roi_ids via np.arange(N). Use this only if you already have an A matrix; the dict format is smaller and avoids re-running centroid extraction.

Sanity checks

Outputs

Each step writes a .npz file (or several) into a step-specific subdirectory of the output root. The most useful files for downstream analysis are the Step 3 sweep files (per-pair match results with confidence) and the consolidated tracking file (global neuron identities across sessions).

Step 1 — step_1_results/{session_name}_centroids_quads.npz

  • centroids — (N, 2) stacked as (y, x)
  • neuron_ids — from input roi_ids or auto-generated
  • quad_desc — (Q, 4) the ℝ⁴ descriptors
  • quad_idx — (Q, 4) vertex indices into centroids
  • n_neurons, n_quads, generation_time, generation_method = "diagonal_first"

Step 2 — step_2_results/{pair_name}_matches_light.npz

  • match_indices — (M, 8) int32, concatenated [ref_quad_verts, tgt_quad_verts]
  • distances — (M,) float32, descriptor distance for each match
  • ref_centroids, target_centroids, n_ref_neurons, n_target_neurons
  • threshold_used, n_raw_matches, n_filtered_matches

Step 2.5 — step_2_5_results/{pair_name}_filtered_matches.npz

  • match_indices, ref_centroids, tgt_centroids — inliers only
  • transform_matrix — (2, 2) refined rigid transform
  • transform_translation — (2,)
  • max_residual_threshold — persisted from Step 2.5 config; consumed by Step 3
  • Inlier count, rotation, translation magnitude, etc.

Step 3 — step_3_results/{pair_name}_sweep.npz

Per-pair match results. (Note: the "sweep" naming is legacy — V3.1 produces a single best assignment, not a true threshold sweep. match_counts and match_rates are constant arrays kept for backward compatibility.)

  • matched_ref_indices, matched_tgt_indices — the assignment
  • matched_costs — (M,) float32, combined Hungarian cost
  • match_confidence — (M,) float32, [0, 1] score; the right field to threshold on
  • match_pass — (M,) int32, 1 = pass-1 Hungarian, 2 = pass-2 recovery
  • ref_centroids, tgt_centroids, n_inlier_quads, etc.

Step 3 — step_3_results/{animal_id}_consolidated_tracking.npz

Global neuron identities for one animal, chained transitively across all its sessions. neuron_tracks is a Python dict — load with allow_pickle=True and use .item().

  • sessions — canonical session order
  • neuron_tracks — dict[global_id, dict[session_idx, local_idx]]
  • track_lengths — sessions each global neuron appears in
  • track_confidence — minimum link confidence (weakest-link reliability)
  • track_mean_confidence — mean link confidence
  • n_total_tracks, full_length_tracks, avg_track_length, max_track_length
Use match_confidence, not raw cost. Cost values depend on whether quad voting was enabled and which dummy regime ran, so they aren't comparable across runs. match_confidence is normalised to [0, 1] from four signals (vote, distance, assignment margin, pass), making it the natural threshold for downstream analyses.

Parameter tuning — a Goldilocks guide

Calcium imaging data is not uniform. A 100-neuron lateral habenula recording from a head-fixed mouse and a 900-neuron cortical recording from a freely-moving rat produce fundamentally different geometric landscapes. Every parameter exposed below exists because the obvious automated default failed silently somewhere. The cost of a wrong default a user never questions is far higher than the cost of asking you to think for ten minutes.

For each knob below, three states: too low, just right, too high. The middle column is the default and what most datasets want.

Debug from Step 1. Bad inputs propagate forward; no amount of Step 3 tuning fixes bad quads.

Step 1 — Quad generation

Step 1 enumerates diagonals (KNN local + random long-range), computes perpendicular heights for all other neurons per diagonal, keeps the top-K by height, and pairs those into quads. No triangle enumeration, no C(N,3) blow-up. Each quad is emitted exactly once via an ownership guard — only from the diagonal that is its longest edge.

knn_k = 5 sparse coverage knn_k = 15 balanced density knn_k = 40 saturated & slow
Figure 3.knn_k controls how many local neighbours each neuron forms diagonals with. Long-range diagonals (k_random ≈ knn_k / 2) are added on top.

knn_k

Local neighbours per neuron used for diagonal generation.
Default 15 Range 5–100
Too low — k = 5
sparse coverage
Neurons in low-density regions of the FOV end up with zero quads. Step 2 can't match them. Coverage remediation fires constantly.
Just right — k = 15
100–400 neuron recordings
Most neurons participate in hundreds of quads. Coverage remediation barely needs to do anything. Total diagonals scale as N · 1.5k / 2.
Too high — k = 40
descriptor space crowds
Quad counts approach the saturation cap. Compute scales linearly in k. Similar-looking quads multiply, descriptor crowding inflates false matches.

max_triangles_per_diagonal (K-cap)

Top-K third-points per diagonal, ranked by perpendicular height. Quads per diagonal = K(K−1)/2.
Default 25 Range 2–500 Quads/diagonal 10, 300, 4 950
Too low — K = 5
10 quads/diagonal
Only the tallest third-points survive. Selective but you miss subtle geometric relationships. Works for FOVs with < 80 neurons.
Just right — K = 25
300 quads/diagonal
Captures both prominent and moderate geometric features. Solid tradeoff for 100–500 neuron recordings.
Too high — K = 100
4 950 quads/diagonal
Captures everything including near-degenerate flat quads close to the diagonal line. Massive counts, descriptor noise, slow matching.

quad_keep_fraction

After all quads are generated, rank by descriptor quality |y_C| + |y_D| (third-point distance from diagonal) and keep this fraction.
Default 1.0 Range 0.0–1.0
Too low — 0.1
top 10% only
Discriminative descriptors but sparse. Some neuron pairs that should match can't because the connecting quads were pruned. Low recall.
Just right — 1.0
keep everything
Let downstream steps do selection. More candidates → higher chance of finding valid matches. Reduce only if you're hitting the saturation cap.
Too high — n/a
capped at 1.0
Can't go above 1.0. The "wrong" direction here is artificial constraint at moderate values like 0.3–0.5 when not warranted.

min_pairwise_distance

Minimum Euclidean distance (px) between any two of the four quad vertices. Quads with closer pairs are dropped.
Default 0.0 (disabled) Range 0–100 px
Too low — 0.0
no filter
Near-coincident centroids form numerically unstable descriptors that pollute matching. Fine if your centroid extraction is clean.
Just right — 2–5 px
cleans jitter
Removes quads where two neurons are within a few pixels — the regime where descriptors are genuinely numerically unstable. No collateral damage.
Too high — 15 px
aggressive
Removes many valid quads in dense regions. Only justifiable if your extraction has known jitter > 10 px.

min_coverage_fraction

Coverage remediation. Neurons whose quad count falls below this fraction of the field median get re-processed with the ownership guard disabled.
Default 0.4 Range 0.0–1.0
Too low — 0.0
disabled
Neurons starved by the ownership guard stay at zero. Visible as cold spots on the Step 1 viewer's coverage heatmap.
Just right — 0.4
below 40% of median
Catches the worst gaps without inflating quad counts for neurons that are merely below average.
Too high — 0.8
below 80% of median
More uniform coverage but adds many quads. Can push you over the saturation cap on dense recordings.

Other Step 1 knobs

Step 1.5 — Threshold calibration

The most commonly misunderstood step. The "quality" metric here is not a measure of how good your neuron matches are. It is a measure of how well the descriptor distance threshold separates signal from noise in descriptor space. The pipeline sweeps τ ∈ [0, 1], finds the per-pair quality peak, then fits the scaling law:

τ(N) = C · √N

across all session pairs of one animal, yielding the per-animal constant C, its standard deviation C_std, and goodness of fit R². The √N form is justified because the typical nearest-neighbour distance in descriptor space scales as Q⁻¹ᐟ⁴ ~ N⁻¹ᐟ², so τ must track that to maintain a constant signal-to-noise ratio.

The calibration trap. For high-N recordings (> 400) with suspiciously high match rates (> 80%): your threshold is probably too loose. Lower target_quality to 0.85 or 0.90 to push τ tighter. Counter-intuitive, but a lower target produces a tighter threshold because it commits to τ at an earlier, more conservative point on the quality curve.

target_quality

The quality level at which τ is selected. Lower values pick τ earlier on the rising curve → tighter threshold.
Default 0.95 Range 0.5–1.0
Too low — 0.85
conservative
Tighter τ. Fewer matches, higher precision. Use this for > 400 neuron recordings where descriptor crowding inflates false matches at higher quality settings.
Just right — 0.95
150–400 neurons
Balanced. Picks τ near the quality plateau. Default for typical mid-N recordings.
Too high — 0.99
permissive
Waits for quality to be near-perfect before committing. Looser τ. More matches including borderline ones. Works for < 150 neurons.

sample_size

Quads sampled from each session for calibration. Speed/accuracy tradeoff for calibration only — does not affect the actual matching in Step 2.
Default 10 000 Range 10–1 000 000
Too low — 100
noisy fit
C_std blows up. The √N law can't be fit reliably. Threshold ends up animal-specific in name only.
Just right — 10 000
stable C estimate
C values stabilise within a few percent of full-data estimates. Calibration runs fast.
Too high — 100 000+
slow, no payoff
Near-exact but slow. Only useful if you need sub-1% C precision for some methodological reason.

session_pair_strategy

How session pairs are generated within each FOV group.
Default consecutive Options consecutive · all_vs_all
Default — consecutive
N − 1 pairs
Session 1↔2, 2↔3, 3↔4 ... Fast. Best for longitudinal recordings where adjacent sessions are most similar.
Heavier — all_vs_all
C(N, 2) pairs
All within-group pairs. Maximises calibration data and downstream cross-session linking. Slow for > 30 sessions/group.
Capped — max_pairs_per_animal
subsample groups (headless)
Exposed only in s2c_api.py, not the GUI. If max_pairs_per_animal is set, large groups are randomly subsampled (seed = 42) to fit the cap. Use when all_vs_all is computationally infeasible.

Other Step 1.5 knobs

Step 2 — Descriptor matching

Step 2 runs FAISS top-1 nearest-neighbour search on L2-normalised descriptors (cosine via IndexFlatIP), keeps matches with descriptor distance below the per-animal calibrated τ, and applies a geometric consistency filter. The result is a set of quad-level correspondences for each session pair — still containing chance collisions that Step 2.5 will reject.

consistency_threshold

Geometric consistency filter. Rejects matches whose implied transform deviates from the bulk of other matches in the same pair.
Default 0.8 Range 0.0–1.0
Too low — 0.5
permissive
Allows matches with substantial geometric disagreement. More survive but more are wrong. Loads garbage into Step 2.5.
Just right — 0.8
moderate
Removes the worst outliers while keeping borderline matches that RANSAC can sort out properly.
Too high — 0.95
strict
Only matches in tight geometric agreement survive. Discards valid matches when there's any non-rigid deformation (tissue drift, lens distortion).

Other Step 2 knobs

Step 2.5 — RANSAC verification

RANSAC estimates the best rigid transform between each session pair from the Step 2 matches. 1,000 candidate transforms are drawn (m = 4 point pairs each), all estimated simultaneously via batched SVD on the cross-covariance matrices, residuals evaluated in float32 chunks sized to ~200 MB working memory, and the best transform is re-estimated on its full inlier set in float64 for precision. The max_residual_threshold used here is persisted into the output NPZ and consumed by Step 3 to set its distance cutoffs.

2 px — too tight excludes valid matches 5 px — just right captures real signal 15 px — too loose contaminated by noise
Figure 4.The RANSAC inlier ring. The dot at the centre is a target neuron after applying the candidate transform; white dots inside the ring are inliers; grey dots are outliers. The ring is the residual threshold.

ransac_max_residual

Pixel residual threshold for RANSAC inliers. The single most important Step 2.5 parameter — and it propagates into Step 3 via dist_cutoff_multiplier and postfilter_residual_multiplier.
Default 5.0 px Range 0.1–100 px
Too low — 2 px
very tight
Only matches whose transformed centroid lands within 2 px of the target survive. High precision but misses neurons with even moderate centroid jitter. Reasonable only for head-fixed, perfectly stable FOV.
Just right — 5 px
standard
Accommodates typical centroid uncertainty (2–3 px) plus minor session-to-session drift. Good balance for most datasets.
Too high — 15 px
permissive
Many false inliers; the consensus transform itself becomes contaminated. Only use if your sessions have large non-rigid deformations.

ransac_iterations

Number of random subsets to try. P(find good subset) = 1 − (1 − wᵐ)ⁿ where w is true inlier ratio.
Default 1 000 Range 100–10 000
Too low — 100
may miss the answer
At w = 30% inliers, P(find good subset) ≈ 93%. At 10% inliers, only ≈ 9%. With low inlier ratios, more iterations are essential.
Just right — 1 000
covers most cases
P ≈ 1.0 at w = 30%; P ≈ 63% at w = 10%. With batched SVD this is essentially free.
Too high — 10 000
diminishing returns
10× cost. Only worth it when you've measured w < 5% and still expect a real signal in there.

ransac_min_inlier_ratio

Minimum fraction of matches that must agree with the best transform for the result to be accepted.
Default 0.05 Range 0.0–1.0
Too low — 0.01
accepts noise
Will accept transforms supported by 1% of matches. Risks confidently spurious transforms surviving on heavily contaminated input.
Just right — 0.05
5% agreement
Good default for typical match sets where the true inlier ratio is 10–50%. Decrease to 0.02 if Step 2.5 is rejecting pairs you know should match.
Too high — 0.3
strict
Only accepts very clean match sets. Fails on sessions with many coincidental Step-2 descriptor matches even when the underlying signal is fine.

Other Step 2.5 knobs

Step 3 — Hungarian assignment + tracking

Step 3 turns the inlier quad-level correspondences from Step 2.5 into a one-to-one neuron mapping. Each surviving inlier quad votes for its 16 implied neuron pairings (4 ref × 4 tgt). Vote counts are degree-normalised, combined with spatial residual under the inferred transform, padded with rectangular dummy slots so any neuron can opt out of matching, and solved via scipy.optimize.linear_sum_assignment. Pass-1 matches with too much spatial residual are rejected; pass-2 always runs on the remaining unmatched neurons under a relaxed cutoff. Every surviving match gets a confidence score in [0, 1].

Use match_confidence, not raw cost. The score combines four equally-weighted signals — degree-normalised votes, spatial proximity, assignment margin (1st vs 2nd best cost), and pass origin (1.0 for pass-1, 0.5 for pass-2). It's normalised to [0, 1] and comparable across runs in a way that costs aren't.

use_quad_voting

Cost matrix strategy. True = degree-normalised quad voting + spatial residual. False = pure spatial distance after transform.
Default True
Just right — True
recommended
If neurons i and j co-appear in many inlier quads, cost is low. √(degree_i · degree_j) normalisation prevents hub neurons from dominating. Strictly more information than distance alone. When to set False: debugging, or when very few inlier quads survive Step 2.5 and the voting signal is unreliable.

dist_cutoff_multiplier

Hard distance cutoff. Pairs farther than dist_cutoff_multiplier × ransac_max_residual are assigned infinite cost.
Default 3.0 Range 1.0–20.0 At default residual 7.5 / 15 / 25 px
Too low — 1.5
7.5 px cutoff
Only very close pairs can match. Misses neurons with moderate centroid uncertainty. Most cost-matrix entries become infinite.
Just right — 3.0
15 px cutoff
Three RANSAC residuals — captures virtually all true matches while excluding distant noise. Standard.
Too high — 5.0
25 px cutoff
Allows matches between moderately distant pairs. Risk of false matches in dense regions; the Hungarian may force-pair a peripheral neuron to a distant partner.

postfilter_residual_multiplier

Pass-1 post-filter. Matches with transformed distance > this × ransac_max_residual are removed after the first Hungarian solve, sent to pass-2 for re-evaluation.
Default 1.0 Range 0.5–10.0
Too low — 0.5
2.5 px (at default)
Very tight. Many valid matches removed and need pass-2 recovery. Pass-2 handles them but with reduced confidence (pass_score = 0.5).
Just right — 1.0
5 px (at default)
Same cutoff as RANSAC. Matches within one residual of the consensus survive pass-1.
Too high — 2.0
10 px (at default)
Permissive. Less load on pass-2, but more false matches survive pass-1 (and they carry pass-1's higher confidence weight).

pass2_cutoff_multiplier

Distance cutoff for second-pass recovery. Intentionally more relaxed than pass-1 because pass-2 runs on neurons that pass-1 rejected.
Default 2.0 Range 1.0–10.0
Too low — 1.5
marginal recovery
Only slightly looser than pass-1. Recovers a few borderline neurons; the rest go unmatched.
Just right — 2.0
2× residual
Doubles the recovery radius. Catches neurons with moderate centroid drift while keeping the pool small enough that the sub-Hungarian stays fast.
Too high — 4.0
very relaxed
Recovers neurons up to 20 px away (at default residual). Appropriate for non-rigid deformation, risky in dense FOVs.

pass2_dummy_percentile

Pass-2 sub-Hungarian's dummy cost is set to the Nth percentile of finite costs among unmatched neurons. Higher = harder to leave a neuron unmatched = more aggressive recovery.
Default 75.0 Range 10.0–99.0
Too low — 50
conservative
Median dummy cost. Only neurons with below-median cost to their best partner get recovered. Many unmatched neurons stay unmatched.
Just right — 75
balanced
Recovers most neurons with a reasonable partner. Default.
Too high — 95
aggressive
Forces matching for almost all unmatched neurons. May produce false matches for genuinely-novel neurons (those that legitimately appear/disappear between sessions).

Other Step 3 knobs

Environment

Targets Python 3.10+. Conda is recommended for the numerics stack so numpy, scipy, and faiss-cpu come from conda-forge with consistent BLAS bindings.

Conda (numerics)

numpy==1.26.4 scipy==1.13.0 faiss-cpu

Pip (everything else)

matplotlib==3.10.8 PyQt5==5.15.11 PyQt5-Qt5==5.15.2 PyQt5_sip==12.18.0 pyqtgraph==0.14.0 scikit-image==0.26.0 scikit-learn==1.8.0 tqdm==4.67.3 Pillow==12.1.1 ImageIO==2.37.3 tifffile==2026.3.3 psutil==7.0.0 networkx==3.6.1

No example data ships with the repo; the GUI and pipeline are intended to be run on your own session exports.

Diagnostic checklist

When results don't look right, work through these in order. Step 1 problems propagate forward; Step 3 tuning won't fix bad quads.

SymptomLikely cause & fix
No sessions found in Step 1 session_filename_regex doesn't match your filenames. Check the logs — every failed match is logged with NO MATCH:.
Quad counts are zero or very low Files aren't in either expected format. Should be a dict with centroids_x / centroids_y, or a 3D A matrix. Logs say Unrecognised file format.
Quad counts exceed the saturation cap Reduce max_triangles_per_diagonal or quad_keep_fraction. The GUI estimate panel suggests specific values.
Step 1.5 C ≈ 0 or R² very low Either too few session pairs (need ≥ 3–4), or the threshold sweep range doesn't cover the optimum. Widen threshold_min/max. Check that session_group_regex is grouping FOV-matched sessions correctly.
Step 2 match rates > 95% Threshold too loose. Lower target_quality (e.g. 0.85). The most common calibration error for high-N recordings.
Step 2 match rates near zero Threshold too tight, or sessions don't share neurons (different FOVs grouped together). Verify your group regex.
Step 2.5 inlier ratio < 5% Most quad matches are false. Step 2 threshold issue — go back and tighten. Or increase ransac_iterations if you believe the signal is there.
Step 3 match rates low despite good RANSAC inlier ratios Cost matrix too restrictive. Increase dist_cutoff_multiplier or enable use_asymmetric_dummy_costs. Check the cost distribution — if mostly infinite, the cutoff is too tight.
Step 3 produces obvious false matches Look at the match_confidence distribution first — false matches usually cluster at low confidence. Then: enable block_zero_vote_pairs, reduce pass2_dummy_percentile, tighten postfilter_residual_multiplier. If concentrated at FOV edges, enable use_asymmetric_dummy_costs. If mostly match_pass == 2, reduce pass2_cutoff_multiplier.
Tracking shows neurons "appearing" that should have been tracked Pass-2 not reaching far enough. Increase pass2_cutoff_multiplier or pass2_dummy_percentile.

Citing Stars2Cells

A CITATION.cff file in the repository gives GitHub a "Cite this repository" button and standardises how downstream tools format references to S2C.

If you use Stars2Cells, please cite the preprint:

Peden-Asarch AM, Honan LE, Bai JZ, Asarch EM, Quinn JA, Coffey KR, Neumaier JF. Stars2Cells: Astrometric Tracking of Neurons Across Imaging Sessions. bioRxiv, 2026. doi:10.64898/2026.07.03.736144