Skip to content

Commit 02c78da

Browse files
authored
Add additional drop_likelihood_columns guards (#3333)
* Expose drop_likelihood_columns and apply it Rename the private _drop_likelihood_columns to drop_likelihood_columns and use it across the codebase to consistently sanitize annotation DataFrames (drop any coord level named 'likelihood'). Updated imports/usages in modelzoo (ma_dlc, ma_dlc_dataframe), pose_estimation_pytorch (dlcloader), and utils (skeleton). Added a docstring note about centralizing project I/O and maintenance concerns. Added a unit test (tests/pose_estimation_pytorch/data/test_dlc_dataloader.py) to ensure DLCLoader.to_coco ignores likelihood columns. * Warn and drop likelihood columns; normalize data var Change logging from info to warning (with stacklevel=2) when likelihood columns are detected and drop them. Call drop_likelihood_columns immediately after reading the HDF file. Normalize variable naming by replacing Data with data throughout mergeandsplit and update subsequent references (index ranges, scorer extraction, and enumeration) to use the lower-case variable.
1 parent e7762a6 commit 02c78da

6 files changed

Lines changed: 96 additions & 13 deletions

File tree

deeplabcut/generate_training_dataset/trainingsetmanipulation.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,16 @@ def parse_video_filenames(videos: list[str]) -> list[str]:
493493
return filenames
494494

495495

496-
def _drop_likelihood_columns(df: pd.DataFrame) -> pd.DataFrame:
496+
def drop_likelihood_columns(df: pd.DataFrame) -> pd.DataFrame:
497497
"""Drop any columns whose coord level is named 'likelihood'.
498498
499499
This sanitizes annotation DataFrames coming from h5/csv files before they are
500500
used for training dataset generation.
501+
502+
# NOTE @C-Achard 2026-05-18: This is used in several places as a guard
503+
Most call sites using this should instead go through a canonical, validated project loading function
504+
AND THEN do any custom local processing they require. The current design is hard to maintain and error prone,
505+
and lacks a clearly documented, centralized project I/O interface.
501506
"""
502507
if not isinstance(df.columns, pd.MultiIndex):
503508
return df
@@ -507,7 +512,7 @@ def _drop_likelihood_columns(df: pd.DataFrame) -> pd.DataFrame:
507512

508513
likelihood_mask = coord_values == "likelihood"
509514
if likelihood_mask.any():
510-
logging.info("Detected likelihood columns in annotation data; dropping them.")
515+
logging.warning("Detected likelihood columns in annotation data; dropping them.", stacklevel=2)
511516
df = df.drop(columns=df.columns[likelihood_mask])
512517

513518
return df
@@ -569,7 +574,7 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full):
569574
AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts"))
570575
# Filter out any stray likelihood columns that may have been concatenated in
571576
# see napari-deeplabcut #204 and DeepLabCut #3319
572-
AnnotationData = _drop_likelihood_columns(AnnotationData)
577+
AnnotationData = drop_likelihood_columns(AnnotationData)
573578

574579
if AnnotationData.empty:
575580
logging.warning(
@@ -701,23 +706,24 @@ def mergeandsplit(config, trainindex=0, uniform=True):
701706
fn = os.path.join(project_path, trainingsetfolder, "CollectedData_" + cfg["scorer"])
702707

703708
try:
704-
Data = pd.read_hdf(fn + ".h5")
709+
data = pd.read_hdf(fn + ".h5")
710+
data = drop_likelihood_columns(data)
705711
except FileNotFoundError:
706-
Data = merge_annotateddatasets(
712+
data = merge_annotateddatasets(
707713
cfg,
708714
Path(os.path.join(project_path, trainingsetfolder)),
709715
)
710-
if Data is None:
716+
if data is None:
711717
return [], []
712718

713-
conversioncode.guarantee_multiindex_rows(Data)
714-
Data = Data[scorer] # extract labeled data
719+
conversioncode.guarantee_multiindex_rows(data)
720+
data = data[scorer] # extract labeled data
715721

716722
if uniform:
717723
TrainingFraction = cfg["TrainingFraction"]
718724
trainFraction = TrainingFraction[trainindex]
719725
trainIndices, testIndices = SplitTrials(
720-
range(len(Data.index)),
726+
range(len(data.index)),
721727
trainFraction,
722728
True,
723729
)
@@ -726,7 +732,7 @@ def mergeandsplit(config, trainindex=0, uniform=True):
726732
test_video_name = [Path(i).stem for i in videos][trainindex]
727733
print("Excluding the following folder (from training):", test_video_name)
728734
trainIndices, testIndices = [], []
729-
for index, name in enumerate(Data.index):
735+
for index, name in enumerate(data.index):
730736
if test_video_name == name[1]: # this is the video name
731737
# print(name,test_video_name)
732738
testIndices.append(index)
@@ -754,7 +760,7 @@ def to_matlab_cell(array):
754760
return outer
755761

756762
# Again, remove likelihood if present
757-
df = _drop_likelihood_columns(df)
763+
df = drop_likelihood_columns(df)
758764

759765
if isinstance(df.columns, pd.MultiIndex):
760766
coord_level = "coords" if "coords" in df.columns.names else df.columns.names[-1]

deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import numpy as np
1414
import pandas as pd
1515

16+
from deeplabcut.generate_training_dataset.trainingsetmanipulation import drop_likelihood_columns
1617
from deeplabcut.modelzoo.generalized_data_converter.datasets.base_dlc import (
1718
BaseDLCPoseDataset,
1819
)
@@ -27,7 +28,7 @@ def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""):
2728
super().__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix)
2829

2930
def _df2generic(self, df, image_id_offset=0):
30-
31+
df = drop_likelihood_columns(df)
3132
individuals = df.columns.get_level_values("individuals").unique().tolist()
3233

3334
unique_bpts = []

deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import pandas as pd
1616

1717
from deeplabcut.generate_training_dataset.trainingsetmanipulation import (
18+
drop_likelihood_columns,
1819
parse_video_filenames,
1920
)
2021
from deeplabcut.modelzoo.generalized_data_converter.datasets.base import BasePoseDataset
@@ -79,6 +80,7 @@ def merge_annotateddatasets(cfg):
7980
else:
8081
bodyparts = cfg["bodyparts"]
8182
AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts"))
83+
AnnotationData = drop_likelihood_columns(AnnotationData)
8284

8385
return AnnotationData
8486

@@ -140,7 +142,7 @@ def populate_generic(self):
140142
self.whether_anno_image_match(self.generic_test_images, self.generic_test_annotations)
141143

142144
def _df2generic(self, df, image_id_offset=0):
143-
145+
df = drop_likelihood_columns(df)
144146
individuals = df.columns.get_level_values("individuals").unique().tolist()
145147

146148
unique_bpts = []

deeplabcut/pose_estimation_pytorch/data/dlcloader.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import deeplabcut.utils.auxiliaryfunctions as af
2525
from deeplabcut.core.engine import Engine
26+
from deeplabcut.generate_training_dataset.trainingsetmanipulation import drop_likelihood_columns
2627
from deeplabcut.pose_estimation_pytorch.data.base import Loader
2728
from deeplabcut.pose_estimation_pytorch.data.dataset import PoseDatasetParameters
2829
from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot
@@ -373,6 +374,8 @@ def to_coco(
373374
Returns:
374375
the coco format data
375376
"""
377+
df = drop_likelihood_columns(df)
378+
376379
with_individuals = "individuals" in df.columns.names
377380
if not with_individuals and (len(parameters.individuals) > 1 or len(parameters.unique_bpts) > 0):
378381
raise ValueError(

deeplabcut/utils/skeleton.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from scipy.spatial import KDTree
3232
from skimage import io
3333

34+
from deeplabcut.generate_training_dataset.trainingsetmanipulation import drop_likelihood_columns
35+
3436

3537
# NOTE @C-Achard 2026-03-26 duplicate config read/write functions
3638
# should be addressed in config refactor
@@ -60,6 +62,7 @@ def __init__(self, config_path):
6062
folder = os.path.join(root, dir_)
6163
if os.path.isdir(folder) and not any(folder.endswith(s) for s in ("cropped", "labeled")):
6264
self.df = pd.read_hdf(os.path.join(folder, f"CollectedData_{self.cfg['scorer']}.h5"))
65+
self.df = drop_likelihood_columns(self.df)
6366
row, col = self.pick_labeled_frame()
6467
if "individuals" in self.df.columns.names:
6568
self.df = self.df.xs(col, axis=1, level="individuals")
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from types import SimpleNamespace
2+
3+
import numpy as np
4+
import pandas as pd
5+
6+
import deeplabcut.pose_estimation_pytorch.data.dlcloader as dlcloader_mod
7+
from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader
8+
9+
10+
def test_to_coco_ignores_likelihood_columns(monkeypatch, tmp_path):
11+
fake_shape = (3, 480, 640)
12+
monkeypatch.setattr(
13+
dlcloader_mod,
14+
"read_image_shape_fast",
15+
lambda _: fake_shape,
16+
)
17+
18+
scorer = "testscorer"
19+
bodyparts = ["nose", "tail"]
20+
21+
index = pd.MultiIndex.from_tuples(
22+
[("labeled-data", "video1", "img0001.png")],
23+
names=["set", "video", "image"],
24+
)
25+
26+
# Baseline dataframe: x/y only
27+
columns_xy = pd.MultiIndex.from_product(
28+
[[scorer], bodyparts, ["x", "y"]],
29+
names=["scorer", "bodyparts", "coords"],
30+
)
31+
df_xy = pd.DataFrame(
32+
[[10.0, 20.0, 30.0, 40.0]],
33+
index=index,
34+
columns=columns_xy,
35+
)
36+
37+
# Same data, but with likelihood columns added
38+
columns_xyl = pd.MultiIndex.from_product(
39+
[[scorer], bodyparts, ["x", "y", "likelihood"]],
40+
names=["scorer", "bodyparts", "coords"],
41+
)
42+
df_xyl = pd.DataFrame(
43+
[[10.0, 20.0, 0.9, 30.0, 40.0, 0.8]],
44+
index=index,
45+
columns=columns_xyl,
46+
)
47+
48+
# to_coco only needs these attributes from parameters
49+
params = SimpleNamespace(
50+
bodyparts=bodyparts,
51+
unique_bpts=[],
52+
individuals=["animal"],
53+
)
54+
55+
baseline = DLCLoader.to_coco(tmp_path, df_xy, params)
56+
got = DLCLoader.to_coco(tmp_path, df_xyl, params)
57+
58+
assert len(got["images"]) == len(baseline["images"]) == 1
59+
assert len(got["annotations"]) == len(baseline["annotations"]) == 1
60+
61+
got_ann = got["annotations"][0]
62+
expected_ann = baseline["annotations"][0]
63+
64+
assert got_ann["image_id"] == expected_ann["image_id"]
65+
assert got_ann["category_id"] == expected_ann["category_id"]
66+
assert got_ann["num_keypoints"] == expected_ann["num_keypoints"] == 2
67+
assert np.array_equal(got_ann["keypoints"], expected_ann["keypoints"])
68+
assert np.allclose(got_ann["bbox"], expected_ann["bbox"])

0 commit comments

Comments
 (0)