From 9d0dc7acadb634fe54f22333179d7f725123fbd7 Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Fri, 22 May 2026 17:50:39 +0200 Subject: [PATCH 01/15] move back to dev version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8bc13b6..4a2b8063 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'fastsurfer' -version = '2.5.2' +version = '2.6.0-dev' description = 'A fast and accurate deep-learning based neuroimaging pipeline' readme = 'README.md' license = {file = 'LICENSE'} From 9cbc3db34db252304aeb5de8190f8def4021ff84 Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Sat, 23 May 2026 09:56:22 +0200 Subject: [PATCH 02/15] add dependabot for workflow action updates --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5ace4600 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From ed9dcbd421156096ad0c1485c9fd39ea53ce8f6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 07:57:05 +0000 Subject: [PATCH 03/15] Bump actions/setup-python from 5 to 6 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/deploy.yml | 2 +- .github/workflows/unittest.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c7dcd77..e4bb0346 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: with: fetch-depth: 0 - name: Set up python environment - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: install dependencies diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 68ab6290..e1a4ad82 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -41,7 +41,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' architecture: 'x64' From eecc5e5136f6c4d2e32ff2d4b6f9b92bccd01a29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 07:57:08 +0000 Subject: [PATCH 04/15] Bump actions/upload-artifact from 4 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/doc.yml | 2 +- .github/workflows/unittest.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index d62dbb8f..f686b60a 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -33,7 +33,7 @@ jobs: run: | uv run --project src --extra doc sphinx-build -WT --keep-going -j auto src/doc doc-build - name: Upload documentation - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: doc path: | diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 68ab6290..8a5779b5 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -71,7 +71,7 @@ jobs: python -m pytest "${flags[@]}" ${{ matrix.pytest-flags }} test/${{ matrix.tests }} echo "::endgroup::" - name: Upload the JUnit XML file as an artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: fastsurfer-${{ github.sha }}-${{ matrix.tests }}-junit path: /tmp/fastsurfer-unittest-${{ matrix.tests }}.junit.xml From e61fb91e4f8f14f43243a99fb0eae9a74fd66207 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 07:57:12 +0000 Subject: [PATCH 05/15] Bump astral-sh/setup-uv from 5 to 7 Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 7. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code-style.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/quicktest.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index b31dac38..1a6ed5dc 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -18,7 +18,7 @@ jobs: with: path: 'src' - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: python-version: '3.10' - name: Run Ruff diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index d62dbb8f..94e717f6 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -23,7 +23,7 @@ jobs: with: path: src - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: python-version: '3.12' - name: Build doc diff --git a/.github/workflows/quicktest.yaml b/.github/workflows/quicktest.yaml index 38b3d463..d26ab2f7 100644 --- a/.github/workflows/quicktest.yaml +++ b/.github/workflows/quicktest.yaml @@ -140,7 +140,7 @@ jobs: with: ref: ${{ steps.parse.outputs.DOCKER_IMAGE }} - name: Setup uv and Python 3.11 - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 with: python-version: '3.11' # python 3.11 is needed to read pyproject.toml (tomllib) From b2dc55bb027d344fbcb6254ad26da04f8b25b2ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 07:57:16 +0000 Subject: [PATCH 06/15] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/code-style.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/doc.yml | 2 +- .github/workflows/quicktest.yaml | 6 +++--- .github/workflows/unittest.yaml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index b31dac38..aa7f7c3a 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: 'src' - name: Install uv diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c7dcd77..4e544d66 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,7 +20,7 @@ jobs: - name: Get repository name. run: echo "FASTSURFER_DIR=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up python environment diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index d62dbb8f..3c5a3755 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -19,7 +19,7 @@ jobs: shell: bash steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: src - name: Install uv diff --git a/.github/workflows/quicktest.yaml b/.github/workflows/quicktest.yaml index 38b3d463..8cc73992 100644 --- a/.github/workflows/quicktest.yaml +++ b/.github/workflows/quicktest.yaml @@ -132,11 +132,11 @@ jobs: - name: Checkout repository # DOCKER_IMAGE is build-cached => we want to build, check this repository out to build the docker image if: steps.parse.outputs.CONTINUE == 'true' && steps.parse.outputs.DOCKER_IMAGE == 'build-cached' - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Checkout repository # DOCKER_IMAGE starts with pr/*** => we want to build, check the PR out to build the docker image if: steps.parse.outputs.CONTINUE == 'true' && startsWith(steps.parse.outputs.DOCKER_IMAGE, 'pr/') - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ steps.parse.outputs.DOCKER_IMAGE }} - name: Setup uv and Python 3.11 @@ -195,7 +195,7 @@ jobs: extra-args: "--3T" steps: - name: Check out the repository that is to be tested - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run FastSurfer for ${{ matrix.subject-id }} with the previously created docker container uses: Deep-MI/FastSurfer/.github/actions/run-fastsurfer@dev with: diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 68ab6290..916da263 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -39,7 +39,7 @@ jobs: pytest-flags: [""] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python 3.10 uses: actions/setup-python@v5 with: From c4f06f9131b7c34ea1e4691ab894a846381a1dd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 07:57:19 +0000 Subject: [PATCH 07/15] Bump actions/download-artifact from 4 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/quicktest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/quicktest.yaml b/.github/workflows/quicktest.yaml index 38b3d463..b4b974c9 100644 --- a/.github/workflows/quicktest.yaml +++ b/.github/workflows/quicktest.yaml @@ -221,7 +221,7 @@ jobs: if: always() && needs.build-docker.outputs.continue == 'true' steps: - name: Retrieve test JUnit files - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: fastsurfer-${{ github.sha }}-junit-* merge-multiple: 'true' From d1b32f04ac99bca9bd1b92e21dcbaba604976f8c Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Wed, 27 May 2026 15:13:17 +0200 Subject: [PATCH 08/15] push neuroreg version to guard getuser in docker for lta write --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a2b8063..7b914611 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ 'matplotlib>=3.7.1', 'meshpy>=2025.1.1', # needed for FastSurfer-CC 'monai>=1.4.0', # needed for FastSurfer-CC - 'neuroreg>=0.6.1', # needed for registration / etiv + 'neuroreg>=0.6.2', # needed for registration / etiv 'nibabel>=5.4.0', # needed to fix a bug in nibabel 'numpy>=1.25', 'packaging', diff --git a/requirements.txt b/requirements.txt index e59e917c..cfea3ea1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ multipledispatch==1.0.0 narwhals==2.21.0 networkx==3.6.1 neurolit==0.6.1 -neuroreg==0.6.1 +neuroreg==0.6.2 nibabel==5.4.2 numpy==2.4.4 packaging==26.2 From bb23b79283a207f8bd007d1fbe9ab96df4560d1e Mon Sep 17 00:00:00 2001 From: ClePol Date: Thu, 21 May 2026 12:05:57 +0200 Subject: [PATCH 09/15] Fix surfaceRAS header check --- recon_surf/recon-surf.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recon_surf/recon-surf.sh b/recon_surf/recon-surf.sh index e0e48049..8bfca5a8 100755 --- a/recon_surf/recon-surf.sh +++ b/recon_surf/recon-surf.sh @@ -714,7 +714,7 @@ for hemi in lh rh ; do # Check if the surfaceRAS was correctly set and exit otherwise (sanity check in case nibabel changes their default header behaviour) { - cmd="mris_info $outmesh | tr -s ' ' | grep -q 'vertex locs : surfaceRAS'" + cmd="mris_info $outmesh | awk '\$1 == \"vertex\" && \$2 == \"locs\" && \$3 == \":\" && \$4 == \"surfaceRAS\" { found = 1 } END { exit !found }'" echo "echo \"$cmd\"" echo "$timecmd $cmd" } | tee -a "$CMDF" From e49bc9d4bb522b6d53d0f6d6ea8eeb69da14881c Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Tue, 26 May 2026 12:48:16 +0200 Subject: [PATCH 10/15] remove randomness from lapy.fem.eigs call --- recon_surf/spherically_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recon_surf/spherically_project.py b/recon_surf/spherically_project.py index c0706d47..0de5c464 100644 --- a/recon_surf/spherically_project.py +++ b/recon_surf/spherically_project.py @@ -145,7 +145,7 @@ def get_flipped_area(tria): return area fem = Solver(tria, lump=False, use_cholmod=use_cholmod) - evals, evecs = fem.eigs(k=4) + evals, evecs = fem.eigs(k=4, rng=0) if debug: data = dict() From 732f71b550324f031a632ec47ccd2e5fb3c339ba Mon Sep 17 00:00:00 2001 From: ClePol Date: Fri, 15 May 2026 22:06:44 +0200 Subject: [PATCH 11/15] Speed up aparc smoothing mode filter Replace per-row scipy.stats.mode calls in sample_parc smoothing with np.bincount().argmax(), preserving scipy's smallest-label tie behavior for non-negative labels. Validation on 114823_MR1 against surf_speed_no_fs_t1_threads8_run1: mapped annotations byte-identical; final MRI volumes have zero voxel changes; white/pial/sphere.reg surfaces and morphometry have zero changes. sample_parc.py dropped from 61.57s total to 8.59s total; full wall time changed from 40:22.01 to 39:58.11. --- recon_surf/smooth_aparc.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/recon_surf/smooth_aparc.py b/recon_surf/smooth_aparc.py index b1f1437e..ca77bafb 100644 --- a/recon_surf/smooth_aparc.py +++ b/recon_surf/smooth_aparc.py @@ -204,8 +204,6 @@ def mode_filter( # create sparse matrix with labels at neighbors nlabels = sparse.csr_matrix((labels[JJ], (II, JJ))) # print("nlabels: {}".format(nlabels)) - from scipy.stats import mode - if not isinstance(nlabels, sparse.csr_matrix): raise ValueError("Matrix must be CSR format.") # novote = [-1,0,fillonlylabel] @@ -227,19 +225,16 @@ def mode_filter( rr = np.isin(nlabels.data, novote) nlabels.data[rr] = 0 nlabels.eliminate_zeros() - # run over all rows and compute mode (maybe vectorize later) + # Run over all rows and compute mode. The labels are non-negative at + # this point; bincount().argmax() matches scipy.stats.mode's smallest-value + # tie behavior without the heavy per-row SciPy dispatch. rempty = 0 for row in rows: rvals = nlabels.data[nlabels.indptr[row] : nlabels.indptr[row + 1]] if rvals.size == 0: rempty += 1 continue - # print(str(rvals)) - mvals = mode(rvals, keepdims=True)[0] - # print(str(mvals)) - if mvals.size != 0: - # print(str(row)+' '+str(ids[row])+' '+str(mvals[0])) - labels_new[ids[row]] = mvals[0] + labels_new[ids[row]] = np.bincount(rvals).argmax() if rempty > 0: # should not happen print("WARNING: row empty: " + str(rempty)) From 8160aaef7ef399489b3ba5cd34e91af6bd0b7862 Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Tue, 26 May 2026 13:29:13 +0200 Subject: [PATCH 12/15] copy labels to avoid silent shadow --- recon_surf/smooth_aparc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recon_surf/smooth_aparc.py b/recon_surf/smooth_aparc.py index ca77bafb..65e99735 100644 --- a/recon_surf/smooth_aparc.py +++ b/recon_surf/smooth_aparc.py @@ -177,7 +177,7 @@ def mode_filter( # for num rings exponentiate adjM and add adjM from step before # we currently do this outside of mode_filter # new labels will be the same as old almost everywhere - labels_new = labels + labels_new = labels.copy() # find vertices to fill # if fillonlylabels empty, fill all if not fillonlylabel: From 21407ba5a8153d05cafa52f6ef9aef3a6af71de2 Mon Sep 17 00:00:00 2001 From: Clemens Pollak Date: Wed, 27 May 2026 17:12:26 +0200 Subject: [PATCH 13/15] CC Visualization fix duplicate point handling (#829) --- CorpusCallosum/shape/contour.py | 5 ++++ CorpusCallosum/shape/thickness.py | 46 +++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CorpusCallosum/shape/contour.py b/CorpusCallosum/shape/contour.py index f5a1583f..0f23e9f1 100644 --- a/CorpusCallosum/shape/contour.py +++ b/CorpusCallosum/shape/contour.py @@ -320,6 +320,11 @@ def fill_thickness_values(self) -> None: closest_indices = known_idx[np.argsort(distances)[:2]] closest_distances = np.sort(distances)[:2] + zero_distance = closest_distances <= 1e-10 + if np.any(zero_distance): + thickness[j] = np.mean(thickness[closest_indices[zero_distance]]) + continue + # Calculate weights based on inverse distance weights = 1.0 / closest_distances weights = weights / np.sum(weights) diff --git a/CorpusCallosum/shape/thickness.py b/CorpusCallosum/shape/thickness.py index 8dc16ca4..79dd8cd9 100644 --- a/CorpusCallosum/shape/thickness.py +++ b/CorpusCallosum/shape/thickness.py @@ -113,7 +113,7 @@ def insert_point_with_thickness( point: np.ndarray, thickness_value: float, return_index: Literal[True], -) -> tuple[np.ndarray, np.ndarray, int] | list[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, int, bool]: ... @@ -123,7 +123,7 @@ def insert_point_with_thickness( point: np.ndarray, thickness_value: float, return_index: bool = False -) -> tuple[np.ndarray, np.ndarray, int] | tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, int, bool] | tuple[np.ndarray, np.ndarray]: """Inserts a point and its thickness value into the contour. Parameters @@ -142,12 +142,26 @@ def insert_point_with_thickness( Returns ------- contour_in_as_space : np.ndarray - Updated contour of shape (N+1, 2). + Updated contour. Shape is unchanged if the point already exists, otherwise (N+1, 2). contour_thickness : np.ndarray - Updated thickness values of shape (N+1,). + Updated thickness values. Shape is unchanged if the point already exists, otherwise (N+1,). insertion_index : int The index, where the point was inserted (only if return_index is True). + inserted : bool + Whether a new contour point was inserted (only if return_index is True). """ + existing_distances = np.linalg.norm(contour_in_as_space[:, :2] - point[:2], axis=1) + existing_matches = np.where(existing_distances <= 1e-10)[0] + if len(existing_matches) > 0: + point_idx = existing_matches[0] + if np.isnan(contour_thickness[point_idx]): + contour_thickness[point_idx] = thickness_value + + if return_index: + return contour_in_as_space, contour_thickness, point_idx, False + else: + return contour_in_as_space, contour_thickness + # Find closest edge for the point edge_idx = find_closest_edge(point, contour_in_as_space) @@ -156,7 +170,7 @@ def insert_point_with_thickness( contour_thickness = np.insert(contour_thickness, edge_idx + 1, thickness_value) if return_index: - return contour_in_as_space, contour_thickness, edge_idx + 1 + return contour_in_as_space, contour_thickness, edge_idx + 1, True else: return contour_in_as_space, contour_thickness @@ -321,23 +335,25 @@ def cc_thickness( levelpath_start = levelpath_asz[0, :2] levelpath_end = levelpath_asz[-1, :2] - contour_2d, contour_thickness, inserted_idx_start = insert_point_with_thickness( + contour_2d, contour_thickness, inserted_idx_start, inserted_start = insert_point_with_thickness( contour_2d, contour_thickness, levelpath_start, lvlpath_length, return_index=True, ) # keep track of start index - if inserted_idx_start <= anterior_endpoint_idx: - anterior_endpoint_idx += 1 - if inserted_idx_start <= posterior_endpoint_idx: - posterior_endpoint_idx += 1 + if inserted_start: + if inserted_idx_start <= anterior_endpoint_idx: + anterior_endpoint_idx += 1 + if inserted_idx_start <= posterior_endpoint_idx: + posterior_endpoint_idx += 1 - contour_2d, contour_thickness, inserted_idx_end = insert_point_with_thickness( + contour_2d, contour_thickness, inserted_idx_end, inserted_end = insert_point_with_thickness( contour_2d, contour_thickness, levelpath_end, lvlpath_length, return_index=True, ) # keep track of end index - if inserted_idx_end <= anterior_endpoint_idx: - anterior_endpoint_idx += 1 - if inserted_idx_end <= posterior_endpoint_idx: - posterior_endpoint_idx += 1 + if inserted_end: + if inserted_idx_end <= anterior_endpoint_idx: + anterior_endpoint_idx += 1 + if inserted_idx_end <= posterior_endpoint_idx: + posterior_endpoint_idx += 1 contour_2d_with_thickness = np.concatenate([contour_2d, contour_thickness[:, None]], axis=1) From 328a1e2a79765cec692a07d5afcba5497efb87d4 Mon Sep 17 00:00:00 2001 From: Clemens Pollak Date: Thu, 28 May 2026 18:49:13 +0200 Subject: [PATCH 14/15] avoid unexpected number of contour points when levelpath endpoints and contour points match --- CorpusCallosum/shape/thickness.py | 69 +++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/CorpusCallosum/shape/thickness.py b/CorpusCallosum/shape/thickness.py index 79dd8cd9..e86eb74e 100644 --- a/CorpusCallosum/shape/thickness.py +++ b/CorpusCallosum/shape/thickness.py @@ -113,7 +113,7 @@ def insert_point_with_thickness( point: np.ndarray, thickness_value: float, return_index: Literal[True], -) -> tuple[np.ndarray, np.ndarray, int, bool]: +) -> tuple[np.ndarray, np.ndarray, int]: ... @@ -123,7 +123,7 @@ def insert_point_with_thickness( point: np.ndarray, thickness_value: float, return_index: bool = False -) -> tuple[np.ndarray, np.ndarray, int, bool] | tuple[np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, int] | tuple[np.ndarray, np.ndarray]: """Inserts a point and its thickness value into the contour. Parameters @@ -142,23 +142,52 @@ def insert_point_with_thickness( Returns ------- contour_in_as_space : np.ndarray - Updated contour. Shape is unchanged if the point already exists, otherwise (N+1, 2). + Updated contour of shape (N+1, 2). contour_thickness : np.ndarray - Updated thickness values. Shape is unchanged if the point already exists, otherwise (N+1,). + Updated thickness values of shape (N+1,). insertion_index : int The index, where the point was inserted (only if return_index is True). - inserted : bool - Whether a new contour point was inserted (only if return_index is True). """ existing_distances = np.linalg.norm(contour_in_as_space[:, :2] - point[:2], axis=1) existing_matches = np.where(existing_distances <= 1e-10)[0] if len(existing_matches) > 0: - point_idx = existing_matches[0] + point_idx = int(existing_matches[0]) if np.isnan(contour_thickness[point_idx]): contour_thickness[point_idx] = thickness_value + # Keep the contour cardinality stable across slices, but avoid inserting + # an exact duplicate vertex. Exact duplicates create zero-length edges in + # visualization/mesh code, while skipping the point breaks the equal + # point-count invariant expected by CCMesh.from_contours(). + before_idx = (point_idx - 1) % len(contour_in_as_space) + after_idx = (point_idx + 1) % len(contour_in_as_space) + before_vector = contour_in_as_space[before_idx, :2] - contour_in_as_space[point_idx, :2] + after_vector = contour_in_as_space[after_idx, :2] - contour_in_as_space[point_idx, :2] + before_length = np.linalg.norm(before_vector) + after_length = np.linalg.norm(after_vector) + + if after_length >= before_length and after_length > 0: + insert_idx = point_idx + 1 + direction = after_vector / after_length + edge_length = after_length + elif before_length > 0: + insert_idx = point_idx + direction = before_vector / before_length + edge_length = before_length + else: + insert_idx = point_idx + 1 + direction = np.zeros_like(point[:2]) + edge_length = 0.0 + + if edge_length > 0: + step = min(max(edge_length * 1e-6, 1e-8), edge_length * 0.5) + point = contour_in_as_space[point_idx, :2] + direction * step + + contour_in_as_space = np.insert(contour_in_as_space, insert_idx, point, axis=0) + contour_thickness = np.insert(contour_thickness, insert_idx, thickness_value) + if return_index: - return contour_in_as_space, contour_thickness, point_idx, False + return contour_in_as_space, contour_thickness, insert_idx else: return contour_in_as_space, contour_thickness @@ -170,7 +199,7 @@ def insert_point_with_thickness( contour_thickness = np.insert(contour_thickness, edge_idx + 1, thickness_value) if return_index: - return contour_in_as_space, contour_thickness, edge_idx + 1, True + return contour_in_as_space, contour_thickness, edge_idx + 1 else: return contour_in_as_space, contour_thickness @@ -335,25 +364,23 @@ def cc_thickness( levelpath_start = levelpath_asz[0, :2] levelpath_end = levelpath_asz[-1, :2] - contour_2d, contour_thickness, inserted_idx_start, inserted_start = insert_point_with_thickness( + contour_2d, contour_thickness, inserted_idx_start = insert_point_with_thickness( contour_2d, contour_thickness, levelpath_start, lvlpath_length, return_index=True, ) # keep track of start index - if inserted_start: - if inserted_idx_start <= anterior_endpoint_idx: - anterior_endpoint_idx += 1 - if inserted_idx_start <= posterior_endpoint_idx: - posterior_endpoint_idx += 1 + if inserted_idx_start <= anterior_endpoint_idx: + anterior_endpoint_idx += 1 + if inserted_idx_start <= posterior_endpoint_idx: + posterior_endpoint_idx += 1 - contour_2d, contour_thickness, inserted_idx_end, inserted_end = insert_point_with_thickness( + contour_2d, contour_thickness, inserted_idx_end = insert_point_with_thickness( contour_2d, contour_thickness, levelpath_end, lvlpath_length, return_index=True, ) # keep track of end index - if inserted_end: - if inserted_idx_end <= anterior_endpoint_idx: - anterior_endpoint_idx += 1 - if inserted_idx_end <= posterior_endpoint_idx: - posterior_endpoint_idx += 1 + if inserted_idx_end <= anterior_endpoint_idx: + anterior_endpoint_idx += 1 + if inserted_idx_end <= posterior_endpoint_idx: + posterior_endpoint_idx += 1 contour_2d_with_thickness = np.concatenate([contour_2d, contour_thickness[:, None]], axis=1) From 12bb9f9c79129274f036c1b5eca723a39146e29d Mon Sep 17 00:00:00 2001 From: Martin Reuter Date: Fri, 29 May 2026 07:52:04 +0200 Subject: [PATCH 15/15] Update version from 2.6.0-dev to 2.5.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7b914611..36657850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'fastsurfer' -version = '2.6.0-dev' +version = '2.5.3' description = 'A fast and accurate deep-learning based neuroimaging pipeline' readme = 'README.md' license = {file = 'LICENSE'}