|
13 | 13 |
|
14 | 14 | import warnings |
15 | 15 | from pathlib import Path |
16 | | -from typing import Callable |
| 16 | +from typing import TYPE_CHECKING, Callable |
| 17 | + |
| 18 | +if TYPE_CHECKING: |
| 19 | + from deeplabcut.core.config.project_config import ProjectConfig, ProjectConfig3D |
17 | 20 |
|
18 | 21 | import yaml |
19 | 22 | import ruamel.yaml.representer |
20 | 23 | from ruamel.yaml import YAML |
| 24 | +from omegaconf import DictConfig |
| 25 | +from pydantic import ValidationError |
21 | 26 |
|
22 | 27 | from deeplabcut.core.engine import Engine |
23 | 28 |
|
@@ -97,126 +102,10 @@ def create_config_template(multianimal: bool = False) -> tuple: |
97 | 102 | Returns: |
98 | 103 | (cfg_file, ruamelFile) for further editing and dumping. |
99 | 104 | """ |
100 | | - if multianimal: |
101 | | - yaml_str = """\ |
102 | | -# Project definitions (do not edit) |
103 | | -Task: |
104 | | -scorer: |
105 | | -date: |
106 | | -multianimalproject: |
107 | | -identity: |
108 | | -\n |
109 | | -# Project path (change when moving around) |
110 | | -project_path: |
111 | | -\n |
112 | | -# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) |
113 | | -engine: pytorch |
114 | | -\n |
115 | | -# Annotation data set configuration (and individual video cropping parameters) |
116 | | -video_sets: |
117 | | -individuals: |
118 | | -uniquebodyparts: |
119 | | -multianimalbodyparts: |
120 | | -bodyparts: |
121 | | -\n |
122 | | -# Fraction of video to start/stop when extracting frames for labeling/refinement |
123 | | -start: |
124 | | -stop: |
125 | | -numframes2pick: |
126 | | -\n |
127 | | -# Plotting configuration |
128 | | -skeleton: |
129 | | -skeleton_color: |
130 | | -pcutoff: |
131 | | -dotsize: |
132 | | -alphavalue: |
133 | | -colormap: |
134 | | -\n |
135 | | -# Training,Evaluation and Analysis configuration |
136 | | -TrainingFraction: |
137 | | -iteration: |
138 | | -default_net_type: |
139 | | -default_augmenter: |
140 | | -default_track_method: |
141 | | -snapshotindex: |
142 | | -detector_snapshotindex: |
143 | | -batch_size: |
144 | | -\n |
145 | | -# Cropping Parameters (for analysis and outlier frame detection) |
146 | | -cropping: |
147 | | -#if cropping is true for analysis, then set the values here: |
148 | | -x1: |
149 | | -x2: |
150 | | -y1: |
151 | | -y2: |
152 | | -\n |
153 | | -# Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) |
154 | | -corner2move2: |
155 | | -move2corner: |
156 | | -\n |
157 | | -# Conversion tables to fine-tune SuperAnimal weights |
158 | | -SuperAnimalConversionTables: |
159 | | - """ |
160 | | - else: |
161 | | - yaml_str = """\ |
162 | | -# Project definitions (do not edit) |
163 | | -Task: |
164 | | -scorer: |
165 | | -date: |
166 | | -multianimalproject: |
167 | | -identity: |
168 | | -\n |
169 | | -# Project path (change when moving around) |
170 | | -project_path: |
171 | | -\n |
172 | | -# Default DeepLabCut engine to use for shuffle creation (either pytorch or tensorflow) |
173 | | -engine: pytorch |
174 | | -\n |
175 | | -# Annotation data set configuration (and individual video cropping parameters) |
176 | | -video_sets: |
177 | | -bodyparts: |
178 | | -\n |
179 | | -# Fraction of video to start/stop when extracting frames for labeling/refinement |
180 | | -start: |
181 | | -stop: |
182 | | -numframes2pick: |
183 | | -\n |
184 | | -# Plotting configuration |
185 | | -skeleton: |
186 | | -skeleton_color: |
187 | | -pcutoff: |
188 | | -dotsize: |
189 | | -alphavalue: |
190 | | -colormap: |
191 | | -\n |
192 | | -# Training,Evaluation and Analysis configuration |
193 | | -TrainingFraction: |
194 | | -iteration: |
195 | | -default_net_type: |
196 | | -default_augmenter: |
197 | | -snapshotindex: |
198 | | -detector_snapshotindex: |
199 | | -batch_size: |
200 | | -detector_batch_size: |
201 | | -\n |
202 | | -# Cropping Parameters (for analysis and outlier frame detection) |
203 | | -cropping: |
204 | | -#if cropping is true for analysis, then set the values here: |
205 | | -x1: |
206 | | -x2: |
207 | | -y1: |
208 | | -y2: |
209 | | -\n |
210 | | -# Refinement configuration (parameters from annotation dataset configuration also relevant in this stage) |
211 | | -corner2move2: |
212 | | -move2corner: |
213 | | -\n |
214 | | -# Conversion tables to fine-tune SuperAnimal weights |
215 | | -SuperAnimalConversionTables: |
216 | | - """ |
217 | | - |
| 105 | + warnings.warn("This function is deprecated. Use deeplabcut.core.config.ProjectConfig instead.") |
| 106 | + from deeplabcut.core.config.project_config import ProjectConfig |
218 | 107 | ruamelFile = YAML() |
219 | | - cfg_file = ruamelFile.load(yaml_str) |
| 108 | + cfg_file = ProjectConfig(multianimalproject=multianimal).to_dict() |
220 | 109 | return cfg_file, ruamelFile |
221 | 110 |
|
222 | 111 |
|
@@ -256,52 +145,54 @@ def create_config_template_3d() -> tuple: |
256 | 145 | return cfg_file_3d, ruamelFile_3d |
257 | 146 |
|
258 | 147 |
|
259 | | -def read_config(configname: str | Path) -> dict: |
| 148 | +def read_config(configname: str | Path, ignore_empty: bool = True) -> DictConfig: |
260 | 149 | """ |
261 | 150 | Reads structured config file defining a project. |
262 | | - Applies default values and repairs (engine, detector_snapshotindex, project_path) and writes back if needed. |
263 | | - """ |
264 | | - ruamelFile = YAML() |
265 | | - path = Path(configname) |
266 | | - if path.exists(): |
267 | | - try: |
268 | | - with open(path, "r") as f: |
269 | | - cfg = ruamelFile.load(f) |
270 | | - curr_dir = str(Path(configname).parent.resolve()) |
271 | | - |
272 | | - if cfg.get("engine") is None: |
273 | | - cfg["engine"] = Engine.TF.aliases[0] |
274 | | - write_project_config(configname, cfg) |
275 | | - |
276 | | - if cfg.get("detector_snapshotindex") is None: |
277 | | - cfg["detector_snapshotindex"] = -1 |
278 | | - |
279 | | - if cfg.get("detector_batch_size") is None: |
280 | | - cfg["detector_batch_size"] = 1 |
281 | | - |
282 | | - if cfg["project_path"] != curr_dir: |
283 | | - cfg["project_path"] = curr_dir |
284 | | - write_project_config(configname, cfg) |
285 | | - except Exception as err: |
286 | | - if len(err.args) > 2: |
287 | | - if ( |
288 | | - err.args[2] |
289 | | - == "could not determine a constructor for the tag '!!python/tuple'" |
290 | | - ): |
291 | | - with open(path, "r") as ymlfile: |
292 | | - cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) |
293 | | - write_project_config(configname, cfg) |
294 | | - else: |
295 | | - raise |
296 | | - else: |
297 | | - raise FileNotFoundError( |
298 | | - f"Config file at {path} not found. Please make sure that the file exists and/or that you passed the path of the config file correctly!" |
299 | | - ) |
300 | | - return cfg |
301 | 151 |
|
| 152 | + Applies default values and repairs (engine, detector_snapshotindex, project_path) |
| 153 | + and writes back if needed. |
302 | 154 |
|
303 | | -def write_project_config(configname: str | Path, cfg: dict) -> None: |
| 155 | + Args: |
| 156 | + configname: Path to the project configuration file (config.yaml). |
| 157 | + ignore_empty: If True, empty/None values in the YAML are ignored and |
| 158 | + dataclass defaults are used instead. If False, empty values represent None. |
| 159 | + Defaults to True. |
| 160 | +
|
| 161 | + Returns: |
| 162 | + The project configuration as a DictConfig. |
| 163 | + """ |
| 164 | + # NOTE @deruyter92 2026-02-05: Default ignore_empty is now set to True to match |
| 165 | + # the prior behaviour of read_config. We should consider changing this to False |
| 166 | + # for stricter validation. |
| 167 | + from deeplabcut.core.config.project_config import ProjectConfig |
| 168 | + path = Path(configname) |
| 169 | + project_config = ProjectConfig.from_yaml(path, ignore_empty=ignore_empty) |
| 170 | + |
| 171 | + # NOTE @deruyter92 2026-02-02: copied old behaviour of writing the config back to the file. |
| 172 | + # We should consider separating the writing and reading instead of having inplace edits during reading. |
| 173 | + curr_dir = str(Path(configname).parent.resolve()) |
| 174 | + if project_config.project_path != curr_dir: |
| 175 | + project_config.project_path = curr_dir |
| 176 | + project_config.to_yaml(configname) |
| 177 | + return project_config.to_dictconfig() |
| 178 | + |
| 179 | + |
| 180 | +def write_project_config( |
| 181 | + configname: str | Path, |
| 182 | + cfg: dict | ProjectConfig | DictConfig, |
| 183 | +) -> None: |
304 | 184 | """Write structured project config file (config.yaml) preserving template order.""" |
| 185 | + from deeplabcut.core.config.project_config import ProjectConfig |
| 186 | + |
| 187 | + try: |
| 188 | + project_config: ProjectConfig = ProjectConfig.from_any(cfg) |
| 189 | + project_config.to_yaml(configname) |
| 190 | + return |
| 191 | + except ValidationError as e: |
| 192 | + warnings.warn( |
| 193 | + f"Invalid configuration! Validation error in config file {cfg}. Error: {e}" |
| 194 | + "Reverting to legacy config file writing." |
| 195 | + ) |
305 | 196 | with open(configname, "w") as cf: |
306 | 197 | cfg_file, ruamelFile = create_config_template( |
307 | 198 | cfg.get("multianimalproject", False) |
|
0 commit comments