Skip to content

Fix: leftover Points layer leaking save info into newly loaded image layers - #246

Open
C-Achard wants to merge 29 commits into
mainfrom
cy/fix-points-data-drift
Open

C-Achard wants to merge 29 commits into
mainfrom
cy/fix-points-data-drift

Conversation

@C-Achard

@C-Achard C-Achard commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a warning avoiding edge cases related to leftover Points layer carrying saving information over to unrelated, newly loaded Image layers:

  • Different folder in the same project (videoA layer, videoB opened), no matter the frame names
  • Same folder name in a different project (project-A/labeled-data/mouse1 vs project-B/…)
  • Two folders whose frame names collide completely

Some cases were already covered but not clearly reported:

  • No frame overlap at any depth: same folder, but frames re-extracted or renamed
  • An ambiguous depth-1 match: the layer's paths span folders with duplicate basenames, giving a non-bijective mapping

All these now clearly warn the user and give feedback.

Closes #245.

Main changes

  • Added dataset_key to PointsMetadata, the resolved absolute folder a layer was read from, written once at read time on both image and annotation layers
  • _remap_frame_indices compares keys before attempting any frame matching, layers with differing keys are left untouched
  • root, shape and name are adopted only alongside a paths update, or when the layer had no paths at all, so the two can no longer name different datasets
  • A rejected adoption raises a modal through a new layer_dataset_mismatch signal, deduplicated per layer and target folder, and emitted out of the insert callback, opening a dialog
  • A layer that does not match the opened folder keeps its own root and paths, so a subsequent save writes back to its original dataset instead of the newly opened one.

@C-Achard C-Achard self-assigned this Sep 15, 2026
@C-Achard C-Achard added bug fix Fixes an issue or a bug I/O Related to reading/writing in the plugin: h5, csv, videos, etc labels Sep 15, 2026
@C-Achard
C-Achard requested a balanced review from Copilot September 15, 2026 11:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Pre-remap synchronization and permanently unbound config-first layers can still leak metadata across datasets.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds dataset identity checks to prevent surviving annotation layers from adopting unrelated image paths.

Changes:

  • Adds persistent dataset-folder keys and mismatch detection.
  • Warns users when layers cannot safely follow a new folder.
  • Adds unit and end-to-end folder-switch coverage.
File summaries
File Description
core/project_paths.py Adds dataset identity helpers.
core/layer_lifecycle/manager.py Gates remapping and emits mismatch warnings.
core/io.py Records dataset keys during reads.
config/models.py Adds Points dataset metadata.
_widgets.py Displays mismatch dialogs.
_tests/e2e/utils.py Adds dataset test fixtures.
_tests/e2e/test_folder_switch_integrity.py Tests folder-switch save integrity.
_tests/core/test_remap.py Tests remap rejection behavior.
_tests/core/test_project_paths.py Tests dataset identity helpers.
_tests/core/layer_manager/test_manager.py Tests lifecycle mismatch handling.
Review details

Suppressed comments (1)

src/napari_deeplabcut/core/layer_lifecycle/manager.py:948

  • An unbound layer adopts this context without recording _image_dataset_key. Config-first layers therefore remain permanently unbound; after labeling folder A, removing its image, and opening folder B with the same frame names, _belongs_to_current_dataset(None) allows the layer to remap and migrate to B—the leak this PR is intended to prevent. Bind a missing key on the first successful adoption.
                try:
                    safe_image_meta = self._image_meta.model_dump(exclude_none=True)
                    safe_image_meta.pop("paths", None)
                    layer.metadata.update(safe_image_meta)
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py Outdated
Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py Outdated
Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py
@C-Achard
C-Achard marked this pull request as ready for review September 17, 2026 13:28
@C-Achard C-Achard added 0.3.2 and removed 0.3.2 labels Sep 17, 2026
@C-Achard C-Achard added this to the 0.4.0 milestone Sep 17, 2026
Extract shared test utilities to write DLC `config.yaml` files and labeled frames, then reuse them across existing fixture builders to remove duplicated setup code. Also add a new helper that creates a project with two `labeled-data` folders (including optional basename-collision frame names) for multi-folder e2e scenarios.
Add end-to-end coverage for reopening a different labeled folder while a Points layer survives. The new tests guard against partially rebinding layer metadata on unmappable switches, verify saves do not write foreign-indexed annotations into the new dataset, and include a positive control where matching frames correctly move root and paths together.
Only adopt the current image context for a layer when it has no existing paths or when a path remap is explicitly accepted. If remapping is rejected because the new folder does not match cleanly, show a deduplicated warning so users know the layer still saves back to its original dataset instead of silently following the newly opened folder.
Add coverage for remap cases that must leave layer metadata unchanged when frame matching fails or is ambiguous, and for successful remaps that must update `root` and `paths` together. The new tests also lock in suppression of duplicate dataset-mismatch warnings and cover remap results that reject path updates on ambiguous depth-1 or no-overlap matches.
Connect `LayerLifecycleManager.layer_dataset_mismatch` to `KeypointControls` so dataset-folder mismatch warnings are shown from the widget layer. Tests were updated to assert the signal-based behavior (including deduping once per target folder) instead of monkeypatching the old direct warning path.

Route dataset mismatch warnings via signal

Replace direct `show_warning` calls when a layer stays on its previous dataset with `_report_layer_left_on_previous_dataset`, which records mismatches per layer/new-root and reports only the first occurrence per folder switch. The message is now sent through `viewer.status` and a new `layer_dataset_mismatch` signal, while logging still captures every remap pass.
Prevented layer remapping from matching unrelated files when switching labeled-data folders. The lifecycle manager now checks whether a layer root and current image root refer to the same dataset folder, using full ordered-depth matching only in that case. For cross-dataset cases, remapping uses a new `DATASET_SCOPED` policy (depths 3 and 2 only) to avoid bare-filename collisions from DLC’s fixed frame names.
Add a dataset-scoped path matching policy that refuses ambiguous basename-only overlaps when frame names repeat across folders. This prevents layers from silently migrating annotations onto another dataset when the only commonality is matching filenames after a project or folder move.

Updated remap and folder-switch tests to cover the safer behavior and guard against false-positive rebinds.
Add a regression test covering two DeepLabCut projects that share the same video-derived dataset path (`labeled-data/mouse1/...`). The new helper builds that fixture setup, and the test asserts an existing points layer keeps its original project/root metadata and does not save annotations into the second project when folders are switched.
Add an immutable `dataset_key` to image and points metadata based on the source folder. The lifecycle manager now uses that key to decide whether a layer should adopt the current image context, leaving layers from other datasets untouched and warning instead of remapping them. Since dataset identity is checked up front, path remapping can always use the ordered depth fallback safely, and the old dataset-scoped policy is removed.
Updates remap-related tests to reflect the new identity model: dataset ownership is determined by `dataset_key` (absolute dataset folder), not by basename/path-match policy alone. The layer manager tests now cover cross-dataset rejection, cross-project same-folder-name rejection, same-dataset remap adoption, and adoption of unbound placeholder layers. Related path/remap tests were simplified by removing `DATASET_SCOPED` policy assertions and adding checks that dataset keys round-trip and remain distinct across projects.
Use the image dataset key when tracking repeated dataset-mismatch warnings so duplicate notifications are suppressed consistently across folder changes. The commit also tightens the status message shown to users and clarifies the filename-matching comment around remapping frame paths.
Add `is_same_dataset` in `project_paths` to compare dataset folders robustly using normalized case and `os.path.samefile`, with fail-closed behavior when on-disk comparison is unavailable. Update layer lifecycle matching to use this helper instead of raw string equality, so equivalent paths (e.g., symlink/UNC aliases and Windows case variants) are treated as the same dataset. Expand tests to cover identical/distinct paths, missing folders, alternate routes to one folder, and Windows case-insensitive matching.
Treat config placeholders as dataset-bound when they adopt image context, so layers that start without a `dataset_key` keep the dataset they first take over instead of being remapped to a later folder with matching frame names.
The override rewrote `root` when the points root equalled the project root or was not a dataset root, so `sync_points_from_image` had two jobs: seeding missing image-derived fields, and second-guessing a root that was already set. Only the first was reachable.
Centralize dataset-follow checks for layer metadata seeding and bind layers to the dataset they actually adopt. This prevents points and image layers from inheriting paths or context from the wrong folder, especially when a layer is already tied to another dataset or starts unbound.
Clarify and tighten how points layers inherit image context from the currently open folder. Layers now consistently check whether they may follow the current dataset before inheriting `root` or `paths`, and the helper that records the adopted dataset key is renamed to better reflect that it only persists the folder identity after paths are inherited. This helps prevent layers from being accidentally associated with the wrong dataset when folders share frame names.
Cover points-layer syncing and wiring when metadata inherits paths and dataset keys from the open folder. The new tests ensure layers keep their original dataset root, adopt matching paths, and reject paths from a different dataset without spuriously warning when no image is open.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

DLC videos lack dataset keys, mismatch notifications are not fully deduplicated, and same-named dataset warnings are ambiguous.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py Outdated
Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py Outdated
Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py Outdated
When a video/image is opened, store its dataset key in metadata so layer lifecycle logic can compare the correct dataset identity against the active folder.

This fixes dataset-mismatch detection for layers that have been reopened or moved across folders. The warning logic now tracks mismatches per layer more accurately and shows both the original save location and the active folder, making it clear where data will still be written.
This change fixes dataset identity checks when a video/image context lacks an explicit dataset key. The lifecycle manager now resolves the key from the containing labeled-data folder, avoids re-reporting the same mismatch, and includes both dataset folders in the mismatch message.

read_video now stores the resolved dataset key in metadata so videos and annotations are treated as the same dataset when they belong to the same labeled-data folder. Added regression tests covering mismatch deduplication, folder names in warnings, and the video metadata key.
Update the layer manager test to use a real temporary folder path for the keyless image context. The test now reflects the actual resolved folder behavior and no longer stubs header validation.
When frame indices are remapped within the same opened dataset, the layer lifecycle manager no longer tells the user to clear the layer. This fixes the misleading guidance that appeared when re-extracting frames in place, where the annotations should stay with the existing folder and continue saving there.

The fix distinguishes same-dataset remaps from true cross-folder mismatches and emits a clearer message in the same-folder case. A regression test covers the scenario where the folder name remains unchanged but the frame set is replaced.
@C-Achard
C-Achard requested review from deruyter92 and a balanced review from Copilot September 18, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Dataset provenance is consistently enforced across reading, remapping, saving, user feedback, and regression tests.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Expand the `is_same_dataset` docstring to capture the Windows path aliasing bug from DeepLabCut#3348. This explains why the `samefile` fallback is required when the same project directory can appear under mapped-drive, UNC, or volume GUID paths, and warns against reducing the check to string equality.

@deruyter92 deruyter92 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix, looks in good state.

one comment with a minor suggestion. Please judge for yourself if needed

Comment thread src/napari_deeplabcut/core/layer_lifecycle/manager.py
Use the active dataset binding instead of a layer's stale root when deciding whether a points layer still belongs to the currently opened folder. This updates the mismatch message to reference the open folder and avoids telling unbound layers to clear annotations when they can still save to the current dataset. Tests cover unbound layers and the revised warning text.
Ensure point layers record their dataset key whenever they inherit image context from the currently open folder, whether that happens during layer setup or image metadata sync. This prevents adopted layers from gaining root/paths without being bound to the active dataset, and adds regression coverage for both entry points.
Add detailed comment explaining the historical context of the 'keypoints' fallback key. The 'keypoints' key was used in this package from May 2022 until April 2026, when the canonical 'df_with_missing' key was adopted. Projects labelled during that period may legitimately contain either key, so this fallback is necessary for backward compatibility with older labelling data.
Replace `dataset_key` with `dataset_folder` across layer metadata, lifecycle management, IO, and tests to make dataset identity terminology match the actual absolute folder being tracked. This also renames the folder resolver helper to `resolve_dataset_folder` and updates the related assertions and comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two adoption paths can still pair existing paths with a new root before remapping validates the dataset.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Comment on lines +694 to +696
inherits_context = (not ly.metadata.get("paths") and bool(self._image_meta.paths)) or (
not ly.metadata.get("root") and bool(self._image_meta.root)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug fix Fixes an issue or a bug I/O Related to reading/writing in the plugin: h5, csv, videos, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect file paths saved if re-using a partially annotated Points layer to save on new, unrelated images

3 participants