File size: 29,220 Bytes
d49f7bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# Copyright (c) Meta Platforms, Inc. and affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from __future__ import annotations
import logging
from collections import defaultdict
from pathlib import Path
from typing import Union, List, Tuple, Dict, TypedDict, Optional
import yaml
from pkg_resources import resource_filename
from animated_drawings.utils import resolve_ad_filepath


class Config():

    def __init__(self, user_mvc_cfg_fn: str) -> None:
        # get the base mvc config
        with open(resource_filename(__name__, "mvc_base_cfg.yaml"), 'r') as f:
            base_cfg = defaultdict(dict, yaml.load(f, Loader=yaml.FullLoader) or {})  # pyright: ignore[reportUnknownMemberType])

        # search for the user-specified mvc config
        user_mvc_cfg_p: Path = resolve_ad_filepath(user_mvc_cfg_fn, 'user mvc config')
        logging.info(f'Using user-specified mvc config file located at {user_mvc_cfg_p.resolve()}')
        with open(str(user_mvc_cfg_p), 'r') as f:
            user_cfg = defaultdict(dict, yaml.load(f, Loader=yaml.FullLoader) or {})  # pyright: ignore[reportUnknownMemberType]

        # overlay user specified mvc options onto base mvc, use to generate subconfig classes
        self.view: ViewConfig = ViewConfig({**base_cfg['view'], **user_cfg['view']})
        self.scene: SceneConfig = SceneConfig({**base_cfg['scene'], **user_cfg['scene']})
        self.controller: ControllerConfig = ControllerConfig({**base_cfg['controller'], **user_cfg['controller']})

        # cannot use an interactive controller with a headless mesa viewer
        if self.controller.mode == 'interact':
            try:
                assert self.view.use_mesa is False, 'cannot use interactive controller when USE_MESA is True'
            except AssertionError as e:
                msg = f'Config error: {e}'
                logging.critical(msg)
                assert False, msg

        # output video path must be set for render controller
        if self.controller.mode == 'video_render':
            try:
                assert self.controller.output_video_path is not None, 'output_video_path must be set when using video_render controller'
            except AssertionError as e:
                msg = f'Config error: {e}'
                logging.critical(msg)
                assert False, msg

        # output video codec must be set for render controller with .mp4 output filetype
        if self.controller.mode == 'video_render' and self.controller.output_video_path is not None and self.controller.output_video_path.endswith('.mp4'):
            try:
                assert self.controller.output_video_codec is not None, 'output_video_codec must be set when using video_render controller'
            except AssertionError as e:
                msg = f'Config error: {e}'
                logging.critical(msg)
                assert False, msg


class SceneConfig():

    def __init__(self, scene_cfg: dict) -> None:

        # show or hide the floor
        try:
            self.add_floor: bool = scene_cfg['ADD_FLOOR']
            assert isinstance(self.add_floor, bool), 'is not bool'
        except (AssertionError, ValueError) as e:
            msg = f'Error in ADD_FLOOR config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # show or hide visualization of BVH motion driving characters
        try:
            self.add_ad_retarget_bvh: bool = scene_cfg['ADD_AD_RETARGET_BVH']
            assert isinstance(self.add_ad_retarget_bvh, bool), 'is not bool'
        except (AssertionError, ValueError) as e:
            msg = f'Error in ADD_AD_RETARGET_BVH config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # config files for characters, driving motions, and retargeting
        self.animated_characters: List[Tuple[CharacterConfig, RetargetConfig, MotionConfig]] = []

        each: Dict[str, str]
        for each in scene_cfg['ANIMATED_CHARACTERS']:
            char_cfg_fn: str = each['character_cfg']
            motion_cfg_fn: str = each['motion_cfg']
            retarget_cfg_fn: str = each['retarget_cfg']
            self.animated_characters.append((
                CharacterConfig(char_cfg_fn),
                RetargetConfig(retarget_cfg_fn),
                MotionConfig(motion_cfg_fn)
            ))


class ViewConfig():

    def __init__(self, view_cfg: dict) -> None:  # noqa: C901

        # set color used to clear render buffer
        try:
            self.clear_color: list[Union[float, int]] = view_cfg["CLEAR_COLOR"]
            assert len(self.clear_color) == 4, 'length not four'
            for val in self.clear_color:
                assert isinstance(val, (float, int)), f'{val} not float or int'
                assert val <= 1.0, 'values must be <= 1.0'
                assert val >= 0.0, 'values must be >= 0.0'
        except (AssertionError, ValueError) as e:
            msg = f'Error in CLEAR_COLOR config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set an image to use for the background, if desired
        try:
            self.background_image: Union[None, str] = view_cfg["BACKGROUND_IMAGE"]
            assert isinstance(self.background_image, (NoneType, str)), 'type not NoneType or str'
        except (AssertionError, ValueError) as e:
            msg = f'Error in BACKGROUND_IMAGE config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set the dimensions of the window or output video
        try:
            self.window_dimensions: tuple[int, int] = view_cfg["WINDOW_DIMENSIONS"]
            assert len(self.window_dimensions) == 2, 'length is not 2'
            for val in self.window_dimensions:
                assert val > 0, f'{val} must be > 0'
                assert isinstance(val, int), 'type not int'
        except (AssertionError, ValueError) as e:
            msg = f'Error in WINDOW_DIMENSIONS config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set whether we want the character rigs to be visible
        try:
            self.draw_ad_rig: bool = view_cfg['DRAW_AD_RIG']
            assert isinstance(self.draw_ad_rig, bool), 'value is not bool type'
        except (AssertionError, ValueError) as e:
            msg = f'Error in DRAW_AD_RIG config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set whether we want the character textures to be visible
        try:
            self.draw_ad_txtr: bool = view_cfg['DRAW_AD_TXTR']
            assert isinstance(self.draw_ad_txtr, bool), 'value is not bool type'
        except (AssertionError, ValueError) as e:
            msg = f'Error in DRAW_AD_TXTR config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set whether we want the character triangle->bone assignment colors to be visible
        try:
            self.draw_ad_color: bool = view_cfg['DRAW_AD_COLOR']
            assert isinstance(self.draw_ad_color, bool), 'value is not bool type'
        except (AssertionError, ValueError) as e:
            msg = f'Error in DRAW_AD_COLOR config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set whether we want the character mesh lines to be visible
        try:
            self.draw_ad_mesh_lines: bool = view_cfg['DRAW_AD_MESH_LINES']
            assert isinstance(self.draw_ad_mesh_lines, bool), 'value is not bool type'
        except (AssertionError, ValueError) as e:
            msg = f'Error in DRAW_AD_MESH_LINES config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set whether we want to use mesa on the back end (necessary for headless rendering)
        try:
            self.use_mesa: bool = view_cfg['USE_MESA']
            assert isinstance(self.use_mesa, bool), 'value is not bool type'
        except (AssertionError, ValueError) as e:
            msg = f'Error in USE_MESA config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set the position of the view camera
        try:
            self.camera_pos: list[Union[float, int]] = view_cfg['CAMERA_POS']
            assert len(self.camera_pos) == 3, 'length != 3'
            for val in self.camera_pos:
                assert isinstance(val, (float, int)), f' {val} is not float or int'
        except (AssertionError, ValueError) as e:
            msg = f'Error in CAMERA_POS config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set the forward vector of the view camera (but it renders out of it's rear)
        try:
            self.camera_fwd: list[Union[float, int]] = view_cfg['CAMERA_FWD']
            assert len(self.camera_fwd) == 3, 'length != 3'
            for val in self.camera_fwd:
                assert isinstance(val, (float, int)), f' {val} is not float or int'
        except (AssertionError, ValueError) as e:
            msg = f'Error in CAMERA_FWD config parameter: {e}'
            logging.critical(msg)
            assert False, msg


class ControllerConfig():

    def __init__(self, controller_cfg: dict) -> None:

        # set controller mode
        try:
            self.mode: str = controller_cfg["MODE"]
            assert isinstance(self.mode, str), 'is not str'
            assert self.mode in ('interactive', 'video_render'), 'mode not interactive or video_render'
        except (AssertionError, ValueError) as e:
            msg = f'Error in MODE config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set timestep for user interactions in interactive mode
        try:
            self.keyboard_timestep: Union[float, int] = controller_cfg["KEYBOARD_TIMESTEP"]
            assert isinstance(self.keyboard_timestep, (float, int)), 'is not floar or int'
            assert self.keyboard_timestep > 0, 'timestep val must be > 0'
        except (AssertionError, ValueError) as e:
            msg = f'Error in KEYBOARD_TIMESTEP config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set output video path (only use in video_render mode)
        try:
            self.output_video_path: Union[None, str] = controller_cfg['OUTPUT_VIDEO_PATH']
            assert isinstance(self.output_video_path, (NoneType, str)), 'type is not None or str'
            if isinstance(self.output_video_path, str):
                assert Path(self.output_video_path).suffix in ('.gif', '.mp4'), 'output video extension not .gif or .mp4 '
        except (AssertionError, ValueError) as e:
            msg = f'Error in OUTPUT_VIDEO_PATH config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # set output video codec (only use in video_render mode with .mp4)
        try:
            self.output_video_codec: Union[None, str] = controller_cfg['OUTPUT_VIDEO_CODEC']
            assert isinstance(self.output_video_codec, (NoneType, str)), 'type is not None or str'
        except (AssertionError, ValueError) as e:
            msg = f'Error in OUTPUT_VIDEO_CODEC config parameter: {e}'
            logging.critical(msg)
            assert False, msg


class CharacterConfig():

    class JointDict(TypedDict):
        loc: List[float]
        name: str
        parent: Union[None, str]

    def __init__(self, char_cfg_fn: str) -> None:  # noqa: C901
        character_cfg_p = resolve_ad_filepath(char_cfg_fn, 'character cfg')
        with open(str(character_cfg_p), 'r') as f:
            char_cfg = yaml.load(f, Loader=yaml.FullLoader)

        # validate image height
        try:
            self.img_height: int = char_cfg['height']
            assert isinstance(self.img_height, int), 'type not int'
            assert self.img_height > 0, 'must be > 0'
        except (AssertionError, ValueError) as e:
            msg = f'Error in character height config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # validate image width
        try:
            self.img_width: int = char_cfg['width']
            assert isinstance(self.img_width, int), 'type not int'
            assert self.img_width > 0, 'must be > 0'
        except (AssertionError, ValueError) as e:
            msg = f'Error in character width config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # based on height and width, determine what final img dimension will be (post padding)
        self.img_dim: int = max(self.img_height, self.img_width)

        # validate skeleton
        try:
            self.skeleton: List[CharacterConfig.JointDict] = []
            for joint in char_cfg['skeleton']:

                # ensure loc input is valid...
                loc: List[int] = joint['loc']
                assert len(loc) == 2, 'joint loc must be of length 2'
                assert loc[0] >= 0, 'x val must be >= 0'
                assert loc[0] < self.img_width, 'x val must be < image width'
                assert loc[1] >= 0, 'y val must be >= 0'
                assert loc[1] < self.img_height, 'y val must be < image height'

                # ... then scale to between 0-1 based on img dim
                loc_x: float = loc[0] / self.img_dim  # width
                loc_y: float = loc[1] / self.img_dim + (1 - self.img_height / self.img_dim)  # height

                # validate joint name
                name: str = joint['name']
                assert isinstance(name, str), 'name must be str'

                # validate joint parent
                parent: Union[None, str] = joint['parent']
                assert isinstance(parent, (NoneType, str)), 'parent must be str or NoneType'

                self.skeleton.append({'loc': [loc_x, loc_y], 'name': name, 'parent': parent})
        except AssertionError as e:
            msg = f'Error in character skeleton: {e}'
            logging.critical(msg)
            assert False, msg

        # validate skeleton joint parents
        try:
            names: List[str] = [joint['name'] for joint in self.skeleton]
            for joint in self.skeleton:
                assert isinstance(joint['parent'], NoneType) or joint['parent'] in names, f'joint.parent not None and not valid joint name: {joint}'
        except AssertionError as e:
            msg = f'Error in character skeleton: {e}'
            logging.critical(msg)
            assert False, msg

        # validate mask and texture files
        try:
            self.mask_p: Path = character_cfg_p.parent / 'mask.png'
            self.txtr_p: Path = character_cfg_p.parent / 'texture.png'
            assert self.mask_p.exists(), f'cannot find character mask: {self.mask_p}'
            assert self.txtr_p.exists(), f'cannot find character texture: {self.txtr_p}'
        except AssertionError as e:
            msg = f'Error validating character files: {e}'
            logging.critical(msg)
            assert False, msg


class MotionConfig():

    def __init__(self, motion_cfg_fn: str) -> None:  # noqa: C901
        motion_cfg_p = resolve_ad_filepath(motion_cfg_fn, 'motion cfg')
        with open(str(motion_cfg_p), 'r') as f:
            motion_cfg = yaml.load(f, Loader=yaml.FullLoader)

        # validate start_frame_idx
        try:
            self.start_frame_idx: int = motion_cfg.get('start_frame_idx', 0)
            assert isinstance(self.start_frame_idx, int), 'type not int'
            assert self.start_frame_idx >= 0, 'start_frame_idx must be > 0'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating start_frame_idx: {e}'
            logging.critical(msg)
            assert False, msg

        # validate end_frame_idx
        try:
            self.end_frame_idx: Optional[int] = motion_cfg.get('end_frame_idx', None)
            assert isinstance(self.end_frame_idx, (NoneType, int)), 'type not NoneType or int'
            if isinstance(self.end_frame_idx, int):
                assert self.end_frame_idx >= self.start_frame_idx, 'end_frame_idx must be > start_frame_idx'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating end_frame_idx: {e}'
            logging.critical(msg)
            assert False, msg

        # validate frame time override
        try:
            self.frame_time: Optional[float] = motion_cfg.get('frame_time', None)
            assert isinstance(self.frame_time, (NoneType, float)), 'is not None or float'
        except (AssertionError, ValueError) as e:
            msg = f'Error in frame_time config parameter: {e}'
            logging.critical(msg)
            assert False, msg

        # validate groundplane joint
        try:
            self.groundplane_joint: str = motion_cfg['groundplane_joint']
            assert isinstance(self.groundplane_joint, str), 'groundplane joint must be str'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating groundplane joint: {e}'
            logging.critical(msg)
            assert False, msg

        # validate forward_perp_joint_vectors
        try:
            self.forward_perp_joint_vectors: List[Tuple[str, str]] = motion_cfg['forward_perp_joint_vectors']
            assert len(self.forward_perp_joint_vectors) > 0, 'forward_perp_joint_vectors len must be > 0'
            for each in self.forward_perp_joint_vectors:
                assert len(each) == 2, 'each list in forrward_perp_joint_vectors must have len = 2'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating forward_perp_joint_vectors: {e}'
            logging.critical(msg)
            assert False, msg

        # validate scale
        try:
            self.scale: float = motion_cfg['scale']
            assert isinstance(self.scale, (int, float)), 'scale must be float or int'
            assert self.scale > 0, 'scale must be > 0'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating scale: {e}'
            logging.critical(msg)
            assert False, msg

        # validate up
        try:
            self.up: str = motion_cfg['up']
            assert self.up in ['+y', '+z'], 'up must be "+y" or "+z'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating up: {e}'
            logging.critical(msg)
            assert False, msg

        # validate bvh_p
        try:
            self.bvh_p: Path = resolve_ad_filepath(motion_cfg['filepath'], 'bvh filepath')
        except (AssertionError, ValueError) as e:
            msg = f'Error validating bvh_p: {e}'
            logging.critical(msg)
            assert False, msg

    def validate_bvh(self, bvh_joint_names: List[str]) -> None:
        """ Performs all the validation steps that depend upon knowing the BVH joint names. This should be called once the BVH had been loaded."""
        try:
            for prox_joint_name, dist_joint_name in self.forward_perp_joint_vectors:
                assert prox_joint_name in bvh_joint_names, f'invalid prox_joint name in motion_cfg.forward_perp_joint_vectors: {prox_joint_name}'
                assert dist_joint_name in bvh_joint_names, f'invalid dist_joint name in motion_cfg.forward_perp_joint_vectors: {dist_joint_name}'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating forward_perp_joint_vector joints: {e}'
            logging.critical(msg)
            assert False, msg


class RetargetConfig():

    class BvhProjectionBodypartGroup(TypedDict):
        bvh_joint_names: List[str]
        method: str
        name: str

    class CharBodypartGroup(TypedDict):
        bvh_depth_drivers: List[str]
        char_joints: List[str]

    class CharBvhRootOffset(TypedDict):
        bvh_projection_bodypart_group_for_offset: str
        bvh_joints: List[List[str]]
        char_joints: List[List[str]]

    def __init__(self, retarget_cfg_fn: str) -> None:  # noqa: C901
        retarget_cfg_p = resolve_ad_filepath(retarget_cfg_fn, 'retarget cfg')
        with open(str(retarget_cfg_p), 'r') as f:
            retarget_cfg = yaml.load(f, Loader=yaml.FullLoader)

        # validate character starting location
        try:
            self.char_start_loc = retarget_cfg['char_starting_location']
            assert len(self.char_start_loc) == 3, 'char start loc must be of len 3'
            for val in self.char_start_loc:
                assert isinstance(val, (float, int)), 'type must be float or int'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating char start location: {e}'
            logging.critical(msg)
            assert False, msg

        # validate bvh project bodypart groups
        self.bvh_projection_bodypart_groups: List[RetargetConfig.BvhProjectionBodypartGroup]
        try:
            self.bvh_projection_bodypart_groups = retarget_cfg['bvh_projection_bodypart_groups']

            for group in self.bvh_projection_bodypart_groups:
                assert group['method'] in ['pca', 'sagittal', 'frontal'], 'group method must be "pca", "sagittal", or "frontal"'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating bvh_projection_bodypart_groups: {e}'
            logging.critical(msg)
            assert False, msg

        # Check that group names are unique
        try:
            group_names = [group['name'] for group in self.bvh_projection_bodypart_groups]
            assert len(group_names) == len(set(group_names)), 'group names are not unique'
        except AssertionError as e:
            msg = f'Error validating bvh_projection_bodypart_groups: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char bodypart groups
        self.char_bodypart_groups: List[RetargetConfig.CharBodypartGroup]
        try:
            self.char_bodypart_groups = retarget_cfg['char_bodypart_groups']
            for group in self.char_bodypart_groups:
                assert len(group['bvh_depth_drivers']) > 0, 'bvh_depth_drivers must have at least one joint specified'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating char_bodypart_groups: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char bvh root offset
        self.char_bvh_root_offset: RetargetConfig.CharBvhRootOffset
        try:
            self.char_bvh_root_offset = retarget_cfg['char_bvh_root_offset']
            assert len(self.char_bvh_root_offset['bvh_joints']) > 0, 'bvh_joints list must be greater than zero'
            for each in self.char_bvh_root_offset['bvh_joints']:
                assert len(each) > 0, 'each list in bvh_joints must have len > 0'

            assert len(self.char_bvh_root_offset['char_joints']) > 0, 'char_joints list must be greater than zero'
            for each in self.char_bvh_root_offset['char_joints']:
                assert len(each) > 0, 'each list in char_joints must have len > 0'

            assert isinstance(self.char_bvh_root_offset['bvh_projection_bodypart_group_for_offset'], str), 'bvh_projection_bodypart_group_for_offset must be str'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating char_bvh_root_offset: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char joint bvh joints mapping
        self.char_joint_bvh_joints_mapping: Dict[str, Tuple[str, str]]
        try:
            self.char_joint_bvh_joints_mapping = retarget_cfg['char_joint_bvh_joints_mapping']
            for key, val in self.char_joint_bvh_joints_mapping.items():
                assert isinstance(key, str), 'key must be str'
                assert isinstance(val, tuple), 'val must be tuple'
                assert len(val) == 2, 'val must be of len 2'
                assert isinstance(val[0], str) and isinstance(val[1], str), 'values must be str'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating char_bvh_root_offset: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char runtime checks
        self.char_runtime_checks: List[str]
        try:
            self.char_runtime_checks = retarget_cfg['char_runtime_checks']
            for check in self.char_runtime_checks:
                assert check[0] in ['above'], 'currently only above check is supported'
                if check[0] == 'above':
                    assert len(check) == 4, 'above check needs 3 additional parameters'
        except (AssertionError, ValueError) as e:
            msg = f'Error validating char_runtime_checks: {e}'
            logging.critical(msg)
            assert False, msg

    def validate_char_and_bvh_joint_names(self, char_joint_names: List[str], bvh_joint_names: List[str]) -> None:  # noqa: C901

        # validate bvh_projection_bodypart_groups
        try:
            for group in self.bvh_projection_bodypart_groups:
                for bvh_joint_name in group['bvh_joint_names']:
                    assert bvh_joint_name in bvh_joint_names, f'bvh_joint_name not valid: {bvh_joint_name}'
        except AssertionError as e:
            msg = f'Error validating bvh_projection_bodypart_groups: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char_bodypart_groups
        try:
            for group in self.char_bodypart_groups:
                # check that bvh joint drivers are valid bvh joints
                for bvh_joint_name in group['bvh_depth_drivers']:
                    assert bvh_joint_name in bvh_joint_names, f'bvh_depth_driver joint name invalid: {bvh_joint_name}'

                # check that all char_joints are valid character joints
                for char_joint_name in group['char_joints']:
                    assert char_joint_name in char_joint_names, f'char_joints joint name invalid: {char_joint_name}'
        except AssertionError as e:
            msg = f'Error validating char_bodypart_groups: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char_bvh_root_offset
        try:
            # check that bvh_projection_bodypart_group_for_offset matches a bvh_projection_bodypart_group name
            group_names = [group['name'] for group in self.bvh_projection_bodypart_groups]
            assert self.char_bvh_root_offset['bvh_projection_bodypart_group_for_offset'] in group_names, 'invalid bvh_projection_bodypart_group_for_offset'

            # check bvh_joints contains valid joints
            for bvh_joint_name_group in self.char_bvh_root_offset['bvh_joints']:
                for joint_name in bvh_joint_name_group:
                    assert joint_name in bvh_joint_names, f'invalid joint name in bvh_joints: {joint_name}'

            # check char_joints are valid joints
            for char_joint_name_group in self.char_bvh_root_offset['char_joints']:
                for joint_name in char_joint_name_group:
                    assert joint_name in char_joint_names, f'invalid joint name in char_joints: {joint_name}'
        except AssertionError as e:
            msg = f'Error validating char_bvh_root_offset: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char_joint_bvh_joints_mapping
        try:
            # check that dict keys correspond to valid character joints
            for char_joint_name in self.char_joint_bvh_joints_mapping.keys():
                assert char_joint_name in char_joint_names, f'invalid char_joint_name: {char_joint_name}'

            # check that dict values correspond to valid bvh joints
            for bvh_prox_joint_name, bvh_dist_joint_name in self.char_joint_bvh_joints_mapping.values():
                assert bvh_prox_joint_name in bvh_joint_names, f'invalid bvh_prox_joint_name: {bvh_prox_joint_name}'
                assert bvh_dist_joint_name in bvh_joint_names, f'invalid bvh_dist_joint_name: {bvh_dist_joint_name}'
        except AssertionError as e:
            msg = f'Error validating char_joint_bvh_joints_mapping: {e}'
            logging.critical(msg)
            assert False, msg

        # validate char runtime checks
        try:
            for check in self.char_runtime_checks:
                if check[0] == 'above':
                    # check that, if above test, following 3 params are valid character joint names
                    _, target_joint_name, joint1_name, joint2_name = check
                    assert target_joint_name in char_joint_names, f'above test target_joint_name invalid {target_joint_name}'
                    assert joint1_name in char_joint_names, f'above test joint1_name invalid {joint1_name}'
                    assert joint2_name in char_joint_names, f'above test joint2_name invalid {joint2_name}'
        except AssertionError as e:
            msg = f'Error validating char_runtime_checks: {e}'
            logging.critical(msg)
            assert False, msg


NoneType = type(None)  # needed for type checking