From b57d84958e72b68221ba2c6eef5aafe8ae58bd4b Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 2 Feb 2024 12:09:13 +0100 Subject: [PATCH 01/95] add code of hrnet + coam backbone --- .../models/backbones/hrnet_coam.py | 381 ++++++++++++++++++ .../models/modules/__init__.py | 4 + .../models/modules/coam_module.py | 313 ++++++++++++++ 3 files changed, 698 insertions(+) create mode 100644 deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py create mode 100644 deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py new file mode 100644 index 0000000000..81f57d9282 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -0,0 +1,381 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft +# Licensed under the MIT License. +# Written by Bin Xiao (Bin.Xiao@microsoft.com) +# Modified to Conditional Top Down by Mu Zhou, Lucas Stoffl et al. (ICCV 2023) +# ------------------------------------------------------------------------------ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import logging + +import torch +import torch.nn as nn + +from deeplabcut.pose_estimation_pytorch.models.backbones.base import ( + BACKBONES, + BaseBackbone, +) +from deeplabcut.pose_estimation_pytorch.models.modules import ( + BasicBlock, + Bottleneck, + HighResolutionModule, + CoAMBlock, + SelfAttentionModule_CoAM, +) + + +logger = logging.getLogger(__name__) + + +blocks_dict = { + 'BASIC': BasicBlock, + 'BOTTLENECK': Bottleneck +} + + +@BACKBONES.register_module +class HRNet_CoAM(BaseBackbone): + """HRNet backbone with Conditional Attention Module (CoAM). + + This version returns high-resolution feature maps of size 1/4 * original_image_size. + """ + + def __init__(self, cfg, **kwargs): + self.inplanes = 64 + extra = cfg['MODEL']['EXTRA'] + super(HRNet_CoAM, self).__init__() + + self.cfg = cfg + + self.bn_momentum = 0.1 + + # stem net + self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, + bias=False) + self.bn1 = nn.BatchNorm2d(64, momentum=self.bn_momentum) + self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1, + bias=False) + self.bn2 = nn.BatchNorm2d(64, momentum=self.bn_momentum) + self.relu = nn.ReLU(inplace=True) + self.layer1 = self._make_layer(Bottleneck, 64, 4) + + self.stage2_cfg = extra['STAGE2'] + num_channels = self.stage2_cfg['NUM_CHANNELS'] + block = blocks_dict[self.stage2_cfg['BLOCK']] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + self.transition1 = self._make_transition_layer([256], num_channels) + self.stage2, pre_stage_channels = self._make_stage( + self.stage2_cfg, num_channels) + + self.stage3_cfg = extra['STAGE3'] + num_channels = self.stage3_cfg['NUM_CHANNELS'] + block = blocks_dict[self.stage3_cfg['BLOCK']] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + self.transition2 = self._make_transition_layer( + pre_stage_channels, num_channels) + self.stage3, pre_stage_channels = self._make_stage( + self.stage3_cfg, num_channels) + + self.stage4_cfg = extra['STAGE4'] + num_channels = self.stage4_cfg['NUM_CHANNELS'] + block = blocks_dict[self.stage4_cfg['BLOCK']] + num_channels = [ + num_channels[i] * block.expansion for i in range(len(num_channels)) + ] + self.transition3 = self._make_transition_layer( + pre_stage_channels, num_channels) + self.stage4, pre_stage_channels = self._make_stage( + self.stage4_cfg, num_channels, multi_scale_output=False) + + self.final_layer = nn.Conv2d( + in_channels=pre_stage_channels[0], + out_channels=cfg['MODEL']['NUM_JOINTS'], + kernel_size=extra['FINAL_CONV_KERNEL'], + stride=1, + padding=1 if extra['FINAL_CONV_KERNEL'] == 3 else 0 + ) + + self.pretrained_layers = extra['PRETRAINED_LAYERS'] + + # ------------------------------------------------ + + att_heads = self.cfg['MODEL']['ATTENTION_HEADS'] + + self.stage1_att = None + self.stage2_att = None + self.stage3_att = None + self.stage4_att = None + self.att_config = cfg.MODEL.ATT_MODULES + self.selfatt_config = cfg.MODEL.SELFATT_MODULES + + spat_dims = [(int(cfg.MODEL.IMAGE_SIZE[0]/4),int(cfg.MODEL.IMAGE_SIZE[1]/4)), + (int(cfg.MODEL.IMAGE_SIZE[0]/8),int(cfg.MODEL.IMAGE_SIZE[1]/8)), + (int(cfg.MODEL.IMAGE_SIZE[0]/16),int(cfg.MODEL.IMAGE_SIZE[1]/16)), + (int(cfg.MODEL.IMAGE_SIZE[0]/32),int(cfg.MODEL.IMAGE_SIZE[1]/32))] + + assert not self.att_config[0] or not self.selfatt_config[0] + assert not self.att_config[1] or not self.selfatt_config[1] + assert not self.att_config[2] or not self.selfatt_config[2] + assert not self.att_config[3] or not self.selfatt_config[3] + + if self.att_config[0]: + self.stage1_att = CoAMBlock(spat_dims=spat_dims[:2], channel_list=self.stage2_cfg['NUM_CHANNELS'], + cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), + cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, + channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) + if self.att_config[1]: + self.stage2_att = CoAMBlock(spat_dims=spat_dims[:3], channel_list=self.stage3_cfg['NUM_CHANNELS'], + cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), + cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, + channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) + if self.att_config[2]: + self.stage3_att = CoAMBlock(spat_dims=spat_dims[:], channel_list=self.stage4_cfg['NUM_CHANNELS'], + cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), + cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, + channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) + if self.att_config[3]: + self.stage4_att = CoAMBlock(spat_dims=[spat_dims[0]], channel_list=[self.stage4_cfg['NUM_CHANNELS'][0]], + cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), + cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, + channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) + + if self.selfatt_config[0]: + self.stage1_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:2], channel_list=self.stage2_cfg['NUM_CHANNELS']) + if self.selfatt_config[1]: + self.stage2_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:3], channel_list=self.stage3_cfg['NUM_CHANNELS']) + if self.selfatt_config[2]: + self.stage3_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:], channel_list=self.stage4_cfg['NUM_CHANNELS']) + if self.selfatt_config[3]: + self.stage4_att = SelfAttentionModule_CoAM(spat_dims=[spat_dims[0]], channel_list=[self.stage4_cfg['NUM_CHANNELS'][0]]) + + # ------------------------------------------------ + + return + + def _make_transition_layer( + self, num_channels_pre_layer, num_channels_cur_layer): + num_branches_cur = len(num_channels_cur_layer) + num_branches_pre = len(num_channels_pre_layer) + + transition_layers = [] + for i in range(num_branches_cur): + if i < num_branches_pre: + if num_channels_cur_layer[i] != num_channels_pre_layer[i]: + transition_layers.append( + nn.Sequential( + nn.Conv2d( + num_channels_pre_layer[i], + num_channels_cur_layer[i], + 3, 1, 1, bias=False + ), + nn.BatchNorm2d(num_channels_cur_layer[i]), + nn.ReLU(inplace=True) + ) + ) + else: + transition_layers.append(None) + else: + conv3x3s = [] + for j in range(i+1-num_branches_pre): + inchannels = num_channels_pre_layer[-1] + outchannels = num_channels_cur_layer[i] \ + if j == i-num_branches_pre else inchannels + conv3x3s.append( + nn.Sequential( + nn.Conv2d( + inchannels, outchannels, 3, 2, 1, bias=False + ), + nn.BatchNorm2d(outchannels), + nn.ReLU(inplace=True) + ) + ) + transition_layers.append(nn.Sequential(*conv3x3s)) + + return nn.ModuleList(transition_layers) + + def _make_layer(self, block, planes, blocks, stride=1): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2d( + self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias=False + ), + nn.BatchNorm2d(planes * block.expansion, momentum=self.bn_momentum), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes)) + + return nn.Sequential(*layers) + + def _make_stage(self, layer_config, num_inchannels, + multi_scale_output=True): + num_modules = layer_config['NUM_MODULES'] + num_branches = layer_config['NUM_BRANCHES'] + num_blocks = layer_config['NUM_BLOCKS'] + num_channels = layer_config['NUM_CHANNELS'] + block = blocks_dict[layer_config['BLOCK']] + fuse_method = layer_config['FUSE_METHOD'] + + modules = [] + for i in range(num_modules): + # multi_scale_output is only used last module + if not multi_scale_output and i == num_modules - 1: + reset_multi_scale_output = False + else: + reset_multi_scale_output = True + + modules.append( + HighResolutionModule( + num_branches, + block, + num_blocks, + num_inchannels, + num_channels, + fuse_method, + reset_multi_scale_output + ) + ) + num_inchannels = modules[-1].get_num_inchannels() + + return nn.Sequential(*modules), num_inchannels + + + def forward(self, x): + + x = x.cuda() + + if self.cfg.MODEL.EXTRA.USE_ATTENTION: + if x[:,3:].shape[1] == 0: + raise Exception("condition is empty, please check your dataloader") + x_ = x[:,:3] + cond_hm = x[:,3:] + else: + x_ = x + + x = self.conv1(x_) + x = self.bn1(x) + x = self.relu(x) + x = self.conv2(x) + x = self.bn2(x) + x = self.relu(x) + x = self.layer1(x) + + x_list = [] + for i in range(self.stage2_cfg['NUM_BRANCHES']): + if self.transition1[i] is not None: + x_list.append(self.transition1[i](x)) + else: + x_list.append(x) + + # ------------------- + if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[0]: + x_list = self.stage1_att(x_list, cond_hm) + # ------------------- + + y_list = self.stage2(x_list) + + x_list = [] + for i in range(self.stage3_cfg['NUM_BRANCHES']): + if self.transition2[i] is not None: + x_list.append(self.transition2[i](y_list[-1])) + else: + x_list.append(y_list[i]) + + # ------------------- + if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[1]: + x_list = self.stage2_att(x_list, cond_hm) + # ------------------- + + y_list = self.stage3(x_list) + + x_list = [] + for i in range(self.stage4_cfg['NUM_BRANCHES']): + if self.transition3[i] is not None: + x_list.append(self.transition3[i](y_list[-1])) + else: + x_list.append(y_list[i]) + + # ------------------- + if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[2]: + x_list = self.stage3_att(x_list, cond_hm) + # ------------------- + + y_list = self.stage4(x_list) + + # ------------------- + if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[3]: + y_list = self.stage4_att(y_list, cond_hm) + # ------------------- + + x = self.final_layer(y_list[0]) + + return x + + def init_weights(self, pretrained=''): + logger.info('=> init weights from normal distribution') + for m in self.modules(): + if isinstance(m, nn.Conv2d): + # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + nn.init.normal_(m.weight, std=0.001) + for name, _ in m.named_parameters(): + if name in ['bias']: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.ConvTranspose2d): + nn.init.normal_(m.weight, std=0.001) + for name, _ in m.named_parameters(): + if name in ['bias']: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + nn.init.normal_(m.weight, std=0.001) + for name, _ in m.named_parameters(): + if name in ['bias']: + nn.init.constant_(m.bias, 0) + + if os.path.isfile(pretrained): + pretrained_state_dict = torch.load(pretrained) + logger.info('=> loading pretrained model {}'.format(pretrained)) + + need_init_state_dict = {} + for name, m in pretrained_state_dict.items(): + if name.split('.')[0] in self.pretrained_layers \ + or self.pretrained_layers[0] is '*': + need_init_state_dict[name] = m + self.load_state_dict(need_init_state_dict, strict=False) + elif pretrained: + logger.error('=> please download pre-trained models first!') + raise ValueError('{} is not exist!'.format(pretrained)) + + +def _load_hrnet_coam(cfg, is_train, **kwargs) -> nn.Module: + """ + Loads a HRNet with CoAM model. + + Args: + cfg: the configuration file + is_train: whether the model is in training mode + + Returns: + the HRNet + CoAM model + """ + model = HRNet_CoAM(cfg, **kwargs) + + if is_train and cfg['MODEL']['INIT_WEIGHTS']: + model.init_weights(cfg['MODEL']['PRETRAINED']) + + return model diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py index 9e571798fa..1ce83d79e3 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py @@ -16,3 +16,7 @@ from deeplabcut.pose_estimation_pytorch.models.modules.conv_module import ( HighResolutionModule, ) +from deeplabcut.pose_estimation_pytorch.models.modules.coam_module import ( + CoAMBlock, + SelfAttentionModule_CoAM +) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py new file mode 100644 index 0000000000..5602a3bb83 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -0,0 +1,313 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import numpy as np + +import torch +import torch.nn as nn +from torch.nn import init +import torchvision.transforms.functional as TF + + +class CoAMBlock(nn.Module): + def __init__(self, spat_dims, channel_list, cond_stacked, cond_colored, n_heads=1, channel_only=False): + super(CoAMBlock, self).__init__() + self.att_layers = [] + self.spat_dims = spat_dims + self.cond_color = cond_colored + self.cond_stacked = cond_stacked + if cond_stacked[0]: + d_cond = cond_stacked[1] + elif cond_colored: + d_cond = 3 + else: + d_cond = 1 + for i in range(len(spat_dims)): + att_layer = DAModule(d_model = channel_list[i], + d_cond = d_cond, kernel_size = 3, + H = spat_dims[i][1], W = spat_dims[i][0], + n_heads = n_heads, channel_only = channel_only) + self.att_layers.append(att_layer) + self.att_layers = nn.ModuleList(self.att_layers) + + def forward(self, y_list, cond_hm): + if not self.cond_color and not self.cond_stacked[0]: + cond_hm = cond_hm[:,0].unsqueeze(1) # we only want one channel of the heatmap + y_list_att = [] + for i in range(len(y_list)): + y_att = self.att_layers[i](y_list[i], TF.resize(cond_hm, (self.spat_dims[i][1],self.spat_dims[i][0]))) + y_list_att.append(y_att) + return y_list_att + + +# modified from https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/DANet.py +class PositionAttentionModule(nn.Module): + def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): + super().__init__() + self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, + padding=(kernel_size-1)//2) + self.pa = ScaledDotProductAttention(in_dim_q = d_model, in_dim_k = d_model, + d_k = d_model, d_v = d_model, h = n_heads) + self.self_att = self_att + if not self_att: + self.cnn_cond = nn.Conv2d(d_cond, d_cond, kernel_size=kernel_size, padding=(kernel_size-1)//2) + self.pa = ScaledDotProductAttention(in_dim_q = d_cond, in_dim_k = d_model, + d_k = d_model, d_v = d_model, h = n_heads) + + def forward(self,x,cond=None): + bs,c,h,w = x.shape + y = self.cnn(x) + y = y.view(bs,c,-1).permute(0,2,1) #bs,h*w,c + + if not self.self_att: + _,c_cond,_,_ = cond.shape + y_cond = self.cnn_cond(cond) + y_cond = y_cond.view(bs,c_cond,-1).permute(0,2,1) + y = self.pa(y_cond, y, y) # bs,h*w,c + + else: + y = self.pa(y,y,y) + + return y + +class ChannelAttentionModule(nn.Module): + def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): + super().__init__() + self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, padding=(kernel_size-1)//2) + self.self_att = self_att + if not self_att: + self.cnn_cond = nn.Conv2d(d_cond, d_model, kernel_size=kernel_size, padding=(kernel_size-1)//2) + self.pa = SimplifiedScaledDotProductAttention(H*W, h = n_heads) + + def forward(self,x,cond=None): + bs,c,h,w = x.shape + y = self.cnn(x) + y = y.view(bs,c,-1) # bs,c,h*w + + if not self.self_att: + y_cond = self.cnn_cond(cond) + y_cond = y_cond.view(bs,c,-1) + y = self.pa(y_cond, y, y) # bs,c_cond,h*w + else: + y = self.pa(y,y,y) # bs,c,h*w + + return y + +class DAModule(nn.Module): + def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, channel_only=False): + super().__init__() + self.channel_only = channel_only + if not channel_only: + self.position_attention_module=PositionAttentionModule(d_model=d_model, d_cond=d_cond, + kernel_size=kernel_size, H=H, W=W, + n_heads=n_heads) + self.channel_attention_module=ChannelAttentionModule(d_model=d_model, d_cond=d_cond, + kernel_size=kernel_size, H=H, W=W, + n_heads=n_heads) + + def forward(self,input,cond): + + bs,c,h,w = input.shape + + c_out = self.channel_attention_module(input, cond) + c_out = c_out.view(bs,c,h,w) + + if self.channel_only: + return input * c_out + + p_out = self.position_attention_module(input, cond) + p_out = p_out.permute(0,2,1).view(bs,c,h,w) + + return input + (p_out + c_out) + +class SelfDAModule(nn.Module): + def __init__(self, d_model=512, kernel_size=3, H=7, W=7): + super().__init__() + self.position_attention_module=PositionAttentionModule(d_model=d_model, d_cond=None, + kernel_size=kernel_size, H=H, W=W, + self_att=True) + self.channel_attention_module=ChannelAttentionModule(d_model=d_model, d_cond=None, + kernel_size=kernel_size, H=H, W=W, + self_att=True) + + def forward(self,input): + + bs,c,h,w = input.shape + + p_out = self.position_attention_module(input) + c_out = self.channel_attention_module(input) + + p_out = p_out.permute(0,2,1).view(bs,c,h,w) + c_out = c_out.view(bs,c,h,w) + + return p_out + c_out + +class SelfAttentionModule_CoAM(nn.Module): + def __init__(self, spat_dims, channel_list): + super(SelfAttentionModule_CoAM, self).__init__() + self.att_layers = [] + for i in range(len(spat_dims)): + att_layer = SelfDAModule(d_model = channel_list[i], kernel_size = 3, + H = spat_dims[i][0], W = spat_dims[i][1]) + self.att_layers.append(att_layer) + self.att_layers = nn.ModuleList(self.att_layers) + + def forward(self, y_list, *args): + y_list_att = [] + for i in range(len(y_list)): + y_att = self.att_layers[i](y_list[i]) + y_list_att.append(y_att) + return y_list_att + + + +# taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SelfAttention.py +class ScaledDotProductAttention(nn.Module): + ''' + Scaled dot-product attention + ''' + def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=.1, rev=False): + ''' + :param d_model: Output dimensionality of the model + :param d_k: Dimensionality of queries and keys + :param d_v: Dimensionality of values + :param h: Number of heads + ''' + super(ScaledDotProductAttention, self).__init__() + + # 'rev': condition is key/value and orig. feature map is query + if rev: + d_model = in_dim_q + else: + d_model = in_dim_k + self.fc_q = nn.Linear(in_dim_q, h * d_k) + self.fc_k = nn.Linear(in_dim_k, h * d_k) + self.fc_v = nn.Linear(in_dim_k, h * d_v) + self.fc_o = nn.Linear(h * d_v, d_model) + self.dropout = nn.Dropout(dropout) + + self.d_model = d_model + self.d_k = d_k + self.d_v = d_v + self.h = h + + self.init_weights() + + def init_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + init.kaiming_normal_(m.weight, mode='fan_out') + if m.bias is not None: + init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + init.constant_(m.weight, 1) + init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + init.normal_(m.weight, std=0.001) + if m.bias is not None: + init.constant_(m.bias, 0) + + def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): + ''' + Computes + :param queries: Queries (b_s, nq, d_model) + :param keys: Keys (b_s, nk, d_model) + :param values: Values (b_s, nk, d_model) + :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. + :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). + :return: + ''' + b_s, nq = queries.shape[:2] + nk = keys.shape[1] + + q = self.fc_q(queries).view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) + k = self.fc_k(keys).view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) + v = self.fc_v(values).view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) + + att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) + if attention_weights is not None: + att = att * attention_weights + if attention_mask is not None: + att = att.masked_fill(attention_mask, -np.inf) + att = torch.softmax(att, -1) + att=self.dropout(att) + + out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) + out = self.fc_o(out) # (b_s, nq, d_model) + return out + + +# taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SimplifiedSelfAttention.py +class SimplifiedScaledDotProductAttention(nn.Module): + ''' + Scaled dot-product attention + ''' + + def __init__(self, d_model, h, dropout=.1): + ''' + :param d_model: Output dimensionality of the model + :param d_k: Dimensionality of queries and keys + :param d_v: Dimensionality of values + :param h: Number of heads + ''' + super(SimplifiedScaledDotProductAttention, self).__init__() + + self.d_model = d_model + self.d_k = d_model//h + self.d_v = d_model//h + self.h = h + + self.fc_o = nn.Linear(h * self.d_v, d_model) + self.dropout=nn.Dropout(dropout) + + self.init_weights() + + def init_weights(self): + for m in self.modules(): + if isinstance(m, nn.Conv2d): + init.kaiming_normal_(m.weight, mode='fan_out') + if m.bias is not None: + init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + init.constant_(m.weight, 1) + init.constant_(m.bias, 0) + elif isinstance(m, nn.Linear): + init.normal_(m.weight, std=0.001) + if m.bias is not None: + init.constant_(m.bias, 0) + + def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): + ''' + Computes + :param queries: Queries (b_s, nq, d_model) + :param keys: Keys (b_s, nk, d_model) + :param values: Values (b_s, nk, d_model) + :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. + :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). + :return: + ''' + b_s, nq = queries.shape[:2] + nk = keys.shape[1] + + q = queries.view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) + k = keys.view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) + v = values.view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) + + att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) + if attention_weights is not None: + att = att * attention_weights + if attention_mask is not None: + att = att.masked_fill(attention_mask, -np.inf) + att = torch.softmax(att, -1) + att=self.dropout(att) + + out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) + out = self.fc_o(out) # (b_s, nq, d_model) + return out From 06908996f0fad6a7ea2e689df6aa19d342bd399d Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 2 Feb 2024 18:53:16 +0100 Subject: [PATCH 02/95] refactor and finish hrnet+coam integration --- .../config/ctd/hrnet_coam_w32.yaml | 11 + .../config/ctd/hrnet_coam_w48.yaml | 10 + .../models/backbones/__init__.py | 1 + .../models/backbones/hrnet_coam.py | 483 ++++++------------ .../models/modules/coam_module.py | 13 +- 5 files changed, 172 insertions(+), 346 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml new file mode 100644 index 0000000000..b582492a35 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml @@ -0,0 +1,11 @@ +data: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +model: + backbone: + type: HRNetCoAM + base_model_name: hrnet_w32 + pretrained: true + + backbone_output_channels: 480 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml new file mode 100644 index 0000000000..2d5cc23c81 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml @@ -0,0 +1,10 @@ +data: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +model: + backbone: + type: HRNetCoAM + base_model_name: hrnet_w48 + pretrained: true + backbone_output_channels: 720 diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py index d476de084d..cae2ce2827 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py @@ -14,3 +14,4 @@ ) from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet import HRNet from deeplabcut.pose_estimation_pytorch.models.backbones.resnet import ResNet, DLCRNet +from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet_coam import HRNetCoAM diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 81f57d9282..5cd545420f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -5,377 +5,182 @@ # Modified to Conditional Top Down by Mu Zhou, Lucas Stoffl et al. (ICCV 2023) # ------------------------------------------------------------------------------ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import logging +from __future__ import annotations import torch import torch.nn as nn from deeplabcut.pose_estimation_pytorch.models.backbones.base import ( BACKBONES, - BaseBackbone, +) +from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet import ( + HRNet, ) from deeplabcut.pose_estimation_pytorch.models.modules import ( - BasicBlock, - Bottleneck, - HighResolutionModule, CoAMBlock, SelfAttentionModule_CoAM, ) -logger = logging.getLogger(__name__) - - -blocks_dict = { - 'BASIC': BasicBlock, - 'BOTTLENECK': Bottleneck -} - - @BACKBONES.register_module -class HRNet_CoAM(BaseBackbone): +class HRNetCoAM(HRNet): """HRNet backbone with Conditional Attention Module (CoAM). This version returns high-resolution feature maps of size 1/4 * original_image_size. + + Attributes: + model: the HRNet model + coam_stages: CoAM blocks for each stage """ - def __init__(self, cfg, **kwargs): - self.inplanes = 64 - extra = cfg['MODEL']['EXTRA'] - super(HRNet_CoAM, self).__init__() - - self.cfg = cfg - - self.bn_momentum = 0.1 - - # stem net - self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1, - bias=False) - self.bn1 = nn.BatchNorm2d(64, momentum=self.bn_momentum) - self.conv2 = nn.Conv2d(64, 64, kernel_size=3, stride=2, padding=1, - bias=False) - self.bn2 = nn.BatchNorm2d(64, momentum=self.bn_momentum) - self.relu = nn.ReLU(inplace=True) - self.layer1 = self._make_layer(Bottleneck, 64, 4) - - self.stage2_cfg = extra['STAGE2'] - num_channels = self.stage2_cfg['NUM_CHANNELS'] - block = blocks_dict[self.stage2_cfg['BLOCK']] - num_channels = [ - num_channels[i] * block.expansion for i in range(len(num_channels)) - ] - self.transition1 = self._make_transition_layer([256], num_channels) - self.stage2, pre_stage_channels = self._make_stage( - self.stage2_cfg, num_channels) - - self.stage3_cfg = extra['STAGE3'] - num_channels = self.stage3_cfg['NUM_CHANNELS'] - block = blocks_dict[self.stage3_cfg['BLOCK']] - num_channels = [ - num_channels[i] * block.expansion for i in range(len(num_channels)) - ] - self.transition2 = self._make_transition_layer( - pre_stage_channels, num_channels) - self.stage3, pre_stage_channels = self._make_stage( - self.stage3_cfg, num_channels) - - self.stage4_cfg = extra['STAGE4'] - num_channels = self.stage4_cfg['NUM_CHANNELS'] - block = blocks_dict[self.stage4_cfg['BLOCK']] - num_channels = [ - num_channels[i] * block.expansion for i in range(len(num_channels)) - ] - self.transition3 = self._make_transition_layer( - pre_stage_channels, num_channels) - self.stage4, pre_stage_channels = self._make_stage( - self.stage4_cfg, num_channels, multi_scale_output=False) - - self.final_layer = nn.Conv2d( - in_channels=pre_stage_channels[0], - out_channels=cfg['MODEL']['NUM_JOINTS'], - kernel_size=extra['FINAL_CONV_KERNEL'], - stride=1, - padding=1 if extra['FINAL_CONV_KERNEL'] == 3 else 0 - ) - - self.pretrained_layers = extra['PRETRAINED_LAYERS'] - - # ------------------------------------------------ - - att_heads = self.cfg['MODEL']['ATTENTION_HEADS'] - - self.stage1_att = None - self.stage2_att = None - self.stage3_att = None - self.stage4_att = None - self.att_config = cfg.MODEL.ATT_MODULES - self.selfatt_config = cfg.MODEL.SELFATT_MODULES - - spat_dims = [(int(cfg.MODEL.IMAGE_SIZE[0]/4),int(cfg.MODEL.IMAGE_SIZE[1]/4)), - (int(cfg.MODEL.IMAGE_SIZE[0]/8),int(cfg.MODEL.IMAGE_SIZE[1]/8)), - (int(cfg.MODEL.IMAGE_SIZE[0]/16),int(cfg.MODEL.IMAGE_SIZE[1]/16)), - (int(cfg.MODEL.IMAGE_SIZE[0]/32),int(cfg.MODEL.IMAGE_SIZE[1]/32))] - - assert not self.att_config[0] or not self.selfatt_config[0] - assert not self.att_config[1] or not self.selfatt_config[1] - assert not self.att_config[2] or not self.selfatt_config[2] - assert not self.att_config[3] or not self.selfatt_config[3] - - if self.att_config[0]: - self.stage1_att = CoAMBlock(spat_dims=spat_dims[:2], channel_list=self.stage2_cfg['NUM_CHANNELS'], - cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), - cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, - channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) - if self.att_config[1]: - self.stage2_att = CoAMBlock(spat_dims=spat_dims[:3], channel_list=self.stage3_cfg['NUM_CHANNELS'], - cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), - cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, - channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) - if self.att_config[2]: - self.stage3_att = CoAMBlock(spat_dims=spat_dims[:], channel_list=self.stage4_cfg['NUM_CHANNELS'], - cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), - cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, - channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) - if self.att_config[3]: - self.stage4_att = CoAMBlock(spat_dims=[spat_dims[0]], channel_list=[self.stage4_cfg['NUM_CHANNELS'][0]], - cond_stacked=(self.cfg['DATASET']['STACKED_CONDITION'], self.cfg['MODEL']['NUM_JOINTS']), - cond_colored=self.cfg['DATASET']['COLORED'], n_heads=att_heads, - channel_only=self.cfg['MODEL']['ATT_CHANNEL_ONLY']) - - if self.selfatt_config[0]: - self.stage1_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:2], channel_list=self.stage2_cfg['NUM_CHANNELS']) - if self.selfatt_config[1]: - self.stage2_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:3], channel_list=self.stage3_cfg['NUM_CHANNELS']) - if self.selfatt_config[2]: - self.stage3_att = SelfAttentionModule_CoAM(spat_dims=spat_dims[:], channel_list=self.stage4_cfg['NUM_CHANNELS']) - if self.selfatt_config[3]: - self.stage4_att = SelfAttentionModule_CoAM(spat_dims=[spat_dims[0]], channel_list=[self.stage4_cfg['NUM_CHANNELS'][0]]) - - # ------------------------------------------------ - - return - - def _make_transition_layer( - self, num_channels_pre_layer, num_channels_cur_layer): - num_branches_cur = len(num_channels_cur_layer) - num_branches_pre = len(num_channels_pre_layer) - - transition_layers = [] - for i in range(num_branches_cur): - if i < num_branches_pre: - if num_channels_cur_layer[i] != num_channels_pre_layer[i]: - transition_layers.append( - nn.Sequential( - nn.Conv2d( - num_channels_pre_layer[i], - num_channels_cur_layer[i], - 3, 1, 1, bias=False - ), - nn.BatchNorm2d(num_channels_cur_layer[i]), - nn.ReLU(inplace=True) - ) - ) - else: - transition_layers.append(None) - else: - conv3x3s = [] - for j in range(i+1-num_branches_pre): - inchannels = num_channels_pre_layer[-1] - outchannels = num_channels_cur_layer[i] \ - if j == i-num_branches_pre else inchannels - conv3x3s.append( - nn.Sequential( - nn.Conv2d( - inchannels, outchannels, 3, 2, 1, bias=False - ), - nn.BatchNorm2d(outchannels), - nn.ReLU(inplace=True) - ) - ) - transition_layers.append(nn.Sequential(*conv3x3s)) - - return nn.ModuleList(transition_layers) - - def _make_layer(self, block, planes, blocks, stride=1): - downsample = None - if stride != 1 or self.inplanes != planes * block.expansion: - downsample = nn.Sequential( - nn.Conv2d( - self.inplanes, planes * block.expansion, - kernel_size=1, stride=stride, bias=False - ), - nn.BatchNorm2d(planes * block.expansion, momentum=self.bn_momentum), - ) - - layers = [] - layers.append(block(self.inplanes, planes, stride, downsample)) - self.inplanes = planes * block.expansion - for i in range(1, blocks): - layers.append(block(self.inplanes, planes)) - - return nn.Sequential(*layers) - - def _make_stage(self, layer_config, num_inchannels, - multi_scale_output=True): - num_modules = layer_config['NUM_MODULES'] - num_branches = layer_config['NUM_BRANCHES'] - num_blocks = layer_config['NUM_BLOCKS'] - num_channels = layer_config['NUM_CHANNELS'] - block = blocks_dict[layer_config['BLOCK']] - fuse_method = layer_config['FUSE_METHOD'] - - modules = [] - for i in range(num_modules): - # multi_scale_output is only used last module - if not multi_scale_output and i == num_modules - 1: - reset_multi_scale_output = False + def __init__( + self, + base_model_name: str = "hrnet_w32", + pretrained: bool = True, + + coam_modules: tuple[int,...] = (2,), + selfatt_coam_modules: tuple[int,...] | None = None, + channel_att_only: bool = False, + att_heads: int = 1, + cond_enc: str = 'colored', + + img_size: tuple[int,int] = (256, 256), + num_joints: int = 17, + ) -> None: + """Constructs an ImageNet pretrained HRNet from timm and creates CoAM blocks. + + Args: + base_model_name: Type of HRNet (e.g., 'hrnet_w32', 'hrnet_w48'). + pretrained: If True, loads the model with ImageNet pretrained weights. + coam_modules: List of stages to apply CoAM. + selfatt_coam_modules: List of stages to apply Self-Attention-CoAM. + channel_att_only: Whether to use only channel attention block in CoAM. + att_heads: Number of attention heads. + cond_enc: Type of conditional encoding ('stacked', 'colored', or greyscale). + img_size: Size of the input image. + num_joints: Number of joints in the dataset. + """ + + super().__init__( + model_name = base_model_name, + pretrained = pretrained, + only_high_res = True) + + self.coam_modules = coam_modules + self.selfatt_coam_modules = selfatt_coam_modules + self.channel_att_only = channel_att_only + self.cond_enc = cond_enc + + self.coam_stages = [None, None, None, None] + self.selfatt_coam_stages = [None, None, None, None] + + spat_dims = [(int(img_size[0]/4),int(img_size[1]/4)), + (int(img_size[0]/8),int(img_size[1]/8)), + (int(img_size[0]/16),int(img_size[1]/16)), + (int(img_size[0]/32),int(img_size[1]/32))] + + assert not(set(coam_modules) & set(selfatt_coam_modules) if selfatt_coam_modules else set()), \ + "CoAM and Self-Attention-CoAM cannot be used at the same time" + + all_output_channels = [self.model.stage2_cfg['num_channels'], + self.model.stage3_cfg['num_channels'], + self.model.stage4_cfg['num_channels']] + + for coam_pos in self.coam_modules: + if coam_pos == 4: + spat_dims_ = [spat_dims[0]] + channels = [all_output_channels[-1][0]] else: - reset_multi_scale_output = True + spat_dims_ = spat_dims[:coam_pos+1] + channels = all_output_channels[coam_pos-1] + + self.coam_stages[coam_pos-1] = CoAMBlock(spat_dims=spat_dims_, channel_list=channels, + cond_stacked=self.cond_enc, num_joints = num_joints, + n_heads=att_heads, channel_only=self.channel_att_only) + + if self.selfatt_coam_modules: + for selfatt_coam_pos in self.selfatt_coam_modules: + if selfatt_coam_pos == 4: + spat_dims_ = [spat_dims[0]] + channels = [all_output_channels[-1][0]] + else: + spat_dims_ = spat_dims[:selfatt_coam_pos+1] + channels = all_output_channels[coam_pos-1] + self.selfatt_coam_stages[selfatt_coam_pos-1] = SelfAttentionModule_CoAM(spat_dims=spat_dims_, channel_list=channels) - modules.append( - HighResolutionModule( - num_branches, - block, - num_blocks, - num_inchannels, - num_channels, - fuse_method, - reset_multi_scale_output - ) - ) - num_inchannels = modules[-1].get_num_inchannels() - return nn.Sequential(*modules), num_inchannels + def stages(self, x, cond_hm) -> list[torch.Tensor]: + x = self.model.layer1(x) + xl = [t(x) for i, t in enumerate(self.model.transition1)] - def forward(self, x): + if self.coam_stages[0]: + xl = self.coam_stages[0](xl, cond_hm) + elif self.selfatt_coam_modules[0]: + xl = self.selfatt_coam_stages[0](xl) - x = x.cuda() + yl = self.model.stage2(xl) + + xl = [t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] for i, t in enumerate(self.model.transition2)] - if self.cfg.MODEL.EXTRA.USE_ATTENTION: - if x[:,3:].shape[1] == 0: - raise Exception("condition is empty, please check your dataloader") - x_ = x[:,:3] - cond_hm = x[:,3:] - else: - x_ = x - - x = self.conv1(x_) - x = self.bn1(x) - x = self.relu(x) - x = self.conv2(x) - x = self.bn2(x) - x = self.relu(x) - x = self.layer1(x) - - x_list = [] - for i in range(self.stage2_cfg['NUM_BRANCHES']): - if self.transition1[i] is not None: - x_list.append(self.transition1[i](x)) - else: - x_list.append(x) + if self.coam_stages[1]: + xl = self.coam_stages[1](xl, cond_hm) + elif self.selfatt_coam_modules[1]: + xl = self.selfatt_coam_stages[1](xl) + + yl = self.model.stage3(xl) - # ------------------- - if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[0]: - x_list = self.stage1_att(x_list, cond_hm) - # ------------------- + xl = [t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] for i, t in enumerate(self.model.transition3)] - y_list = self.stage2(x_list) + if self.coam_stages[2]: + xl = self.coam_stages[2](xl, cond_hm) + elif self.selfatt_coam_modules[2]: + xl = self.selfatt_coam_stages[2](xl) - x_list = [] - for i in range(self.stage3_cfg['NUM_BRANCHES']): - if self.transition2[i] is not None: - x_list.append(self.transition2[i](y_list[-1])) - else: - x_list.append(y_list[i]) - - # ------------------- - if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[1]: - x_list = self.stage2_att(x_list, cond_hm) - # ------------------- + yl = self.model.stage4(xl) - y_list = self.stage3(x_list) + if self.coam_stages[3]: + yl = self.coam_stages[3](yl, cond_hm) + elif self.selfatt_coam_modules[3]: + yl = self.selfatt_coam_stages[3](yl) - x_list = [] - for i in range(self.stage4_cfg['NUM_BRANCHES']): - if self.transition3[i] is not None: - x_list.append(self.transition3[i](y_list[-1])) - else: - x_list.append(y_list[i]) - - # ------------------- - if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[2]: - x_list = self.stage3_att(x_list, cond_hm) - # ------------------- - - y_list = self.stage4(x_list) - - # ------------------- - if self.cfg.MODEL.EXTRA.USE_ATTENTION and self.att_config[3]: - y_list = self.stage4_att(y_list, cond_hm) - # ------------------- - - x = self.final_layer(y_list[0]) - - return x - - def init_weights(self, pretrained=''): - logger.info('=> init weights from normal distribution') - for m in self.modules(): - if isinstance(m, nn.Conv2d): - # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') - nn.init.normal_(m.weight, std=0.001) - for name, _ in m.named_parameters(): - if name in ['bias']: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.BatchNorm2d): - nn.init.constant_(m.weight, 1) - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.ConvTranspose2d): - nn.init.normal_(m.weight, std=0.001) - for name, _ in m.named_parameters(): - if name in ['bias']: - nn.init.constant_(m.bias, 0) - elif isinstance(m, nn.Linear): - nn.init.normal_(m.weight, std=0.001) - for name, _ in m.named_parameters(): - if name in ['bias']: - nn.init.constant_(m.bias, 0) - - if os.path.isfile(pretrained): - pretrained_state_dict = torch.load(pretrained) - logger.info('=> loading pretrained model {}'.format(pretrained)) - - need_init_state_dict = {} - for name, m in pretrained_state_dict.items(): - if name.split('.')[0] in self.pretrained_layers \ - or self.pretrained_layers[0] is '*': - need_init_state_dict[name] = m - self.load_state_dict(need_init_state_dict, strict=False) - elif pretrained: - logger.error('=> please download pre-trained models first!') - raise ValueError('{} is not exist!'.format(pretrained)) - - -def _load_hrnet_coam(cfg, is_train, **kwargs) -> nn.Module: - """ - Loads a HRNet with CoAM model. + return yl + - Args: - cfg: the configuration file - is_train: whether the model is in training mode + def forward(self, x): + """Forward pass through the HRNetCoAM backbone. - Returns: - the HRNet + CoAM model - """ - model = HRNet_CoAM(cfg, **kwargs) + Args: + x: Input tensor of shape (batch_size, channels, height, width, condition_channels). - if is_train and cfg['MODEL']['INIT_WEIGHTS']: - model.init_weights(cfg['MODEL']['PRETRAINED']) + Returns: + the feature map - return model + Example: + >>> import torch + >>> from deeplabcut.pose_estimation_pytorch.models.backbones import HRNetCoAM + >>> backbone = HRNetCoAM(model_name='hrnet_w32', pretrained=False) + >>> x = torch.randn(1, 6, 256, 256) + >>> y = backbone(x) + """ + + if x[:,3:].shape[1] == 0: + raise Exception("condition is empty, please check your dataloader") + x_ = x[:,:3] + cond_hm = x[:,3:] + + # Stem + x = self.model.conv1(x_) + x = self.model.bn1(x) + x = self.model.act1(x) + x = self.model.conv2(x) + x = self.model.bn2(x) + x = self.model.act2(x) + + # Stages + y = self.stages(x, cond_hm) + + # TODO: @niels, check if the final layer should be ran + #y = self.model.final_layer(y[0]) + + return y diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index 5602a3bb83..26fc6cb0c8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -17,15 +17,14 @@ class CoAMBlock(nn.Module): - def __init__(self, spat_dims, channel_list, cond_stacked, cond_colored, n_heads=1, channel_only=False): + def __init__(self, spat_dims, channel_list, cond_enc, num_joints, n_heads=1, channel_only=False): super(CoAMBlock, self).__init__() self.att_layers = [] self.spat_dims = spat_dims - self.cond_color = cond_colored - self.cond_stacked = cond_stacked - if cond_stacked[0]: - d_cond = cond_stacked[1] - elif cond_colored: + self.cond_enc = cond_enc + if cond_enc == 'stacked': + d_cond = num_joints + elif cond_enc == 'colored': d_cond = 3 else: d_cond = 1 @@ -38,7 +37,7 @@ def __init__(self, spat_dims, channel_list, cond_stacked, cond_colored, n_heads= self.att_layers = nn.ModuleList(self.att_layers) def forward(self, y_list, cond_hm): - if not self.cond_color and not self.cond_stacked[0]: + if not self.cond_enc == 'stacked' and not self.cond_enc == 'colored': cond_hm = cond_hm[:,0].unsqueeze(1) # we only want one channel of the heatmap y_list_att = [] for i in range(len(y_list)): From c6e66684e84caf0d5e3b6bda6b7b5402b61d342c Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 2 Feb 2024 18:56:06 +0100 Subject: [PATCH 03/95] add ctd configs --- .../pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml | 6 +++++- .../pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml | 5 +++++ .../pose_estimation_pytorch/models/backbones/hrnet_coam.py | 2 -- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml index b582492a35..9b90d9524f 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml @@ -7,5 +7,9 @@ model: type: HRNetCoAM base_model_name: hrnet_w32 pretrained: true - + coam_modules: (2,) + channel_att_only: false + att_heads: 1 + cond_enc: colored + num_joints: 17 backbone_output_channels: 480 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml index 2d5cc23c81..e597b1c1f5 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml @@ -7,4 +7,9 @@ model: type: HRNetCoAM base_model_name: hrnet_w48 pretrained: true + coam_modules: (2,) + channel_att_only: false + att_heads: 1 + cond_enc: colored + num_joints: 17 backbone_output_channels: 720 diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 5cd545420f..d1c6202a42 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -37,13 +37,11 @@ def __init__( self, base_model_name: str = "hrnet_w32", pretrained: bool = True, - coam_modules: tuple[int,...] = (2,), selfatt_coam_modules: tuple[int,...] | None = None, channel_att_only: bool = False, att_heads: int = 1, cond_enc: str = 'colored', - img_size: tuple[int,int] = (256, 256), num_joints: int = 17, ) -> None: From 78ebb57082a6a1b5da043e94b00f0bcaac1ea6c8 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 14 Feb 2024 10:55:02 +0100 Subject: [PATCH 04/95] initial work on generative sampling --- .../pose_estimation_pytorch/data/dataset.py | 19 +- .../data/generative_sampling.py | 499 ++++++++++++++++++ .../pose_estimation_pytorch/data/image.py | 108 ++++ .../data/preprocessor.py | 16 +- .../pose_estimation_pytorch/data/utils.py | 72 --- deeplabcut/pose_estimation_pytorch/task.py | 1 + 6 files changed, 622 insertions(+), 93 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/data/generative_sampling.py create mode 100644 deeplabcut/pose_estimation_pytorch/data/image.py diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index e7f14b829e..85aa6f65bd 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -17,8 +17,8 @@ import numpy as np from torch.utils.data import Dataset +from deeplabcut.pose_estimation_pytorch.data.image import load_image, _crop_and_pad_image_torch from deeplabcut.pose_estimation_pytorch.data.utils import ( - _crop_and_pad_image_torch, _crop_image_keypoints, _extract_keypoints_and_bboxes, apply_transform, @@ -137,15 +137,16 @@ def __getitem__(self, index: int) -> dict: } """ image_path, anns, image_id = self._get_data_based_on_task(index) - image, original_size = self._load_image(image_path) + image = load_image(image_path, color_mode=self.parameters.color_mode) + original_size = image.shape ( keypoints, keypoints_unique, bboxes, annotations_merged, ) = self.extract_keypoints_and_bboxes(anns, image.shape) - offsets = (0, 0) - scales = (1, 1) + scales, offsets = (1, 1), (0, 0) + if self.task == Task.TOP_DOWN: if self.parameters.cropped_image_size is None: raise ValueError( @@ -238,12 +239,6 @@ def _prepare_final_annotation_dict( ).astype(int), } - def _load_image(self, image_path): - image = cv2.imread(image_path) - if self.parameters.color_mode == "RGB": - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - return image, image.shape - def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: """ Retrieve data based on the specified task. @@ -262,9 +257,9 @@ def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: Returns: tuple: Tuple containing the image path, annotations, and image ID. """ - if self.task == Task.TOP_DOWN: + if self.task in (Task.TOP_DOWN, Task.CTD): return self._get_raw_item_crop(index) - elif self.task in [Task.BOTTOM_UP, Task.DETECT]: + elif self.task in (Task.BOTTOM_UP, Task.DETECT): return self._get_raw_item(index) raise ValueError(f"Unknown task: {self.task}") diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py new file mode 100644 index 0000000000..f2bb6640e0 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -0,0 +1,499 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""A file containing code to perform generative sampling of keypoints for CTD + +This code comes from PoseFix (see https://arxiv.org/pdf/1812.03595.pdf), and was then +adapted for BUCTD (github.com/amathislab/BUCTD/blob/main/lib/dataset/pose_synthesis.py, +see `synthesize_pose_fish(...)`). +They say: +> ... synthesized poses need to be diverse and realistic. To satisfy these properties, +> we generate synthesized poses randomly based on the error distributions of real poses +> as described in [24]. The distributions include the frequency of each pose error +> (i.e., jitter, inversion, swap, and miss) according to the joint type, number of +> visible keypoints, and overlap in the input image. +> ... +> Types of Keypoints: +> Good. Good status is defined as a very small displacement from the GT keypoint. +> Jitter. Jitter error is defined as a small displacement from the GT keypoint. +> Inversion. Inversion error occurs when a pose estimation model is confused between +> semantically similar parts that belong to the same instance. +> Swap. Swap error represents a confusion between the same or similar parts which belong +> to different persons. +> Miss. Miss error represents a large displacement from the GT keypoint position. + +In BUCTD and their adaptation to the maDLC fish dataset, they set: + if cfg.DATASET.DATASET == 'coco': + kps_symmetry = [(1, 2), (3, 4), (5, 6), ...] + kps_sigmas = np.array([.26, .25, .25, ...]) / 10.0 + elif cfg.DATASET.DATASET == 'crowdpose': + kps_sigmas = np.array([.79, .79, .72, ...])/10.0 + kps_symmetry= [(0, 1), (2, 3), (4, 5), ...] # l/r shoulder, l/r elbow, wrist, + else: + kps_symmetry = [] + kps_sigmas = np.array([1.] * num_kpts)/10.0 +""" +from __future__ import annotations + +import math +import random +from abc import ABC, abstractmethod + +import cv2 +import numpy as np + +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg + + +KEYPOINT_ENCODERS = Registry("detectors", build_func=build_from_cfg) + + +class BaseKeypointEncoder(ABC): + """Encodes keypoints into heatmaps + + Modified from BUCTD/data/JointsDataset + """ + + def __init__(self, kernel_size: tuple[int, int] = (15, 15)) -> None: + """ + Args: + kernel_size: the Gaussian kernel size to use when blurring a heatmap + """ + self.kernel_size = kernel_size + + @abstractmethod + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + raise NotImplementedError + + def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: + """Applies a Gaussian blur to a heatmap + + Taken from BUCTD/data/JointsDataset, generate_heatmap + + Args: + heatmap: the heatmap to blur (with values in [0, 1] or [0, 255]) + + Returns: + The heatmap with a Gaussian blur, such that max(heatmap) = 255 + """ + heatmap = cv2.GaussianBlur(heatmap, self.kernel_size, sigmaX=0) + am = np.amax(heatmap) + if am == 0: + return heatmap + heatmap /= (am / 255) + return heatmap + + +@KEYPOINT_ENCODERS.register_module +class StackedKeypointEncoder(BaseKeypointEncoder): + """Encodes keypoints into heatmaps, where each + + Modified from BUCTD/data/JointsDataset, get_stacked_condition + """ + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) + zero_matrix = np.zeros(size) + + def _get_condition_matrix(zero_matrix_, kpt_): + if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: + zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 + return zero_matrix_ + + condition_heatmap_list = [] + for i, kpt in enumerate(kpts): + condition = _get_condition_matrix(zero_matrix, kpt) + condition_heatmap = self.blur_heatmap(condition) + condition_heatmap_list.append(condition_heatmap) + zero_matrix = np.zeros(size) + + # ### debug: visualization -> check conditions + # condition_heatmap = np.expand_dims(condition_heatmap, axis=0) + # condition = np.repeat(condition_heatmap, 3, axis=0) + # print("condition", condition.shape) + # condition = np.transpose(condition, (1, 2, 0)) + # cv2.imwrite(f'/media/data/mu/test/cond_{i}.jpg', condition+image) + # cv2.imwrite(f'/media/data/mu/test/image.jpg', image) + + condition_heatmap_list = np.moveaxis(np.array(condition_heatmap_list), 0, -1) + return condition_heatmap_list + + +@KEYPOINT_ENCODERS.register_module +class ColoredKeypointEncoder(BaseKeypointEncoder): + """Encodes keypoints into a given number of color channels + + Modified from BUCTD/data/JointsDataset, get_condition_image_colored + """ + + def __init__(self, colors: list[float], **kwargs) -> None: + """ + Args: + colors: the color to use for each keypoint + """ + super().__init__(**kwargs) + self.colors = colors + + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + if not len(keypoints) == len(self.colors): + raise ValueError( + f"Cannot encode the keypoints. Initialized with {len(self.colors)} " + f"colors, but there are {len(keypoints)} to encode" + ) + + kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) + zero_matrix = np.zeros(size) + + def _get_condition_matrix(zero_matrix, kpts): + for color, kpt in zip(self.colors, kpts): + if 0 < kpt[0] < size[1] and 0 < kpt[1] < size[0]: + zero_matrix[kpt[1] - 1][kpt[0] - 1] = color + return zero_matrix + + condition = _get_condition_matrix(zero_matrix, kpts) + condition_heatmap = self.blur_heatmap(condition) + return condition_heatmap + + +class GenerativeSampler: + """Performs generative sampling of keypoints for CTD model training""" + + def __init__( + self, + num_keypoints: int, + keypoint_sigmas: float | list[float] = 0.1, + keypoints_symmetry: list[tuple[int, int]] | None = None + ): + """ + Args: + num_keypoints: the number of keypoints per individual + keypoint_sigmas: the sigma for each keypoint + keypoints_symmetry: indices of keypoints that are symmetric (e.g., left and + right eye) + """ + if isinstance(keypoint_sigmas, float): + keypoint_sigmas = num_keypoints * [keypoint_sigmas] + if keypoints_symmetry is None: + keypoints_symmetry = keypoints_symmetry + + self.keypoint_sigmas = np.array(keypoint_sigmas) + self.keypoints_symmetry = keypoints_symmetry + self.num_keypoints = num_keypoints + + def __call__( + self, + keypoints: np.ndarray, + near_keypoints: np.ndarray, + area: float, # ?? + num_overlap: int, # ?? + ) -> np.ndarray: + """Samples keypoints + + PoseFix uses conditional keypoints (estimated by a bottom-up model) when ground + truth keypoints are not available. For simplicity, we omit that. See + https://github.com/mks0601/PoseFix_RELEASE/blob/master/main/gen_batch.py#L76 + + Args: + keypoints: (num_keypoints, x-y-visibility) the ground truth keypoints + near_keypoints: (num_other_individuals, num_keypoints, x-y-visibility) joints + from other individuals near this one, for which keypoints might be swapped + area: the total area of the bounding box surrounding the keypoints + num_overlap: + + Returns: + the generative sampled keypoints, of shape (num_keypoints, x-y-visibility) + """ + if not keypoints.shape[0] == self.num_keypoints: + raise ValueError(f"Expected {self.num_keypoints} kpts, had {keypoints}") + + ks_10_dist = self.get_distance_wrt_keypoint_sim(0.10, area) + ks_50_dist = self.get_distance_wrt_keypoint_sim(0.50, area) + ks_85_dist = self.get_distance_wrt_keypoint_sim(0.85, area) + + synth_joints = keypoints.copy() + # FIXME: In the original codebase, if some keypoints are not annotated then they + # use the predictions made by a pose model. This is complex to integrate into + # the current codebase (where is the prediction file saved? how do we load + # predictions? which model?) so we ignore it for now + # for j in range(self.num_keypoints): + # # in case of not annotated joints, use other models`s result and add noise + # if joints[j, 2] == 0: + # synth_joints[j] = estimated_joints[j] + + num_valid_joint = np.sum(keypoints[:, 2] > 0) + + N = 500 # TODO: do not know how this is set + for j in range(self.num_keypoints): + + # source keypoint position candidates to generate error on that (gt, swap, inv, swap+inv) + coord_list = [] + # on top of gt + gt_coord = np.expand_dims(synth_joints[j, :2], 0) + coord_list.append(gt_coord) + # on top of swap gt + swap_coord = near_keypoints[near_keypoints[:, j, 2] > 0, j, :2] + coord_list.append(swap_coord) + + # on top of inv gt, swap inv gt + # FIXME: In the original codebase, they only swap symmetric keypoints. As + # we don't always have symmetries for keypoints in DeepLabCut, we swap any + # keypoints with any other keypoint by randomly selecting keypoints to swap + kps_symmetry = self.keypoints_symmetry + pair_exist = False + for (q, w) in kps_symmetry: + if j == q or j == w: + if j == q: + pair_idx = w + else: + pair_idx = q + pair_exist = True + if pair_exist and (keypoints[pair_idx, 2] > 0): + inv_coord = np.expand_dims(synth_joints[pair_idx, :2], 0) + coord_list.append(inv_coord) + else: + coord_list.append(np.empty([0, 2])) + + if pair_exist: + swap_inv_coord = near_keypoints[near_keypoints[:, pair_idx, 2] > 0, pair_idx, :2] + coord_list.append(swap_inv_coord) + else: + coord_list.append(np.empty([0, 2])) + + # shape (s, 2) + tot_coord_list = np.concatenate(coord_list) + + assert len(coord_list) == 4 + + # jitter error + synth_jitter = np.zeros(3) + if num_valid_joint <= 4: + jitter_prob = 0.20 + else: + jitter_prob = 0.15 + angle = np.random.uniform(0, 2 * math.pi, [N]) + r = np.random.uniform(ks_85_dist[j], ks_50_dist[j], [N]) + jitter_idx = 0 # gt + x = tot_coord_list[jitter_idx][0] + r * np.cos(angle) + y = tot_coord_list[jitter_idx][1] + r * np.sin(angle) + dist_mask = True + for i in range(len(tot_coord_list)): + if i == jitter_idx: + continue + dist_mask = np.logical_and( + dist_mask, np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r + ) + x = x[dist_mask].reshape(-1) + y = y[dist_mask].reshape(-1) + if len(x) > 0: + rand_idx = random.randrange(0, len(x)) + synth_jitter[0] = x[rand_idx] + synth_jitter[1] = y[rand_idx] + synth_jitter[2] = 1 + + # miss error + synth_miss = np.zeros(3) + if num_valid_joint <= 2: + miss_prob = 0.20 + elif num_valid_joint <= 4: + miss_prob = 0.13 + else: + miss_prob = 0.05 + + miss_pt_list = [] + for miss_idx in range(len(tot_coord_list)): + angle = np.random.uniform(0, 2 * math.pi, [4 * N]) + r = np.random.uniform(ks_50_dist[j], ks_10_dist[j], [4 * N]) + x = tot_coord_list[miss_idx][0] + r * np.cos(angle) + y = tot_coord_list[miss_idx][1] + r * np.sin(angle) + dist_mask = True + for i in range(len(tot_coord_list)): + if i == miss_idx: + continue + dist_mask = np.logical_and( + dist_mask, + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > ks_50_dist[ + j] + ) + x = x[dist_mask].reshape(-1) + y = y[dist_mask].reshape(-1) + if len(x) > 0: + if miss_idx == 0: + coord = np.transpose(np.vstack([x, y]), [1, 0]) + miss_pt_list.append(coord) + else: + rand_idx = np.random.choice(range(len(x)), size=len(x) // 4) + x = np.take(x, rand_idx) + y = np.take(y, rand_idx) + coord = np.transpose(np.vstack([x, y]), [1, 0]) + miss_pt_list.append(coord) + if len(miss_pt_list) > 0: + miss_pt_list = np.concatenate(miss_pt_list, axis=0).reshape(-1, 2) + rand_idx = random.randrange(0, len(miss_pt_list)) + synth_miss[0] = miss_pt_list[rand_idx][0] + synth_miss[1] = miss_pt_list[rand_idx][1] + synth_miss[2] = 1 + + # inversion prob + synth_inv = np.zeros(3) + inv_prob = 0.03 + if pair_exist and keypoints[pair_idx, 2] > 0: + angle = np.random.uniform(0, 2 * math.pi, [N]) + r = np.random.uniform(0, ks_50_dist[j], [N]) + inv_idx = (len(coord_list[0]) + len(coord_list[1])) + x = tot_coord_list[inv_idx][0] + r * np.cos(angle) + y = tot_coord_list[inv_idx][1] + r * np.sin(angle) + dist_mask = True + for i in range(len(tot_coord_list)): + if i == inv_idx: + continue + dist_mask = np.logical_and( + dist_mask, np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2 + ) > r + ) + x = x[dist_mask].reshape(-1) + y = y[dist_mask].reshape(-1) + if len(x) > 0: + rand_idx = random.randrange(0, len(x)) + synth_inv[0] = x[rand_idx] + synth_inv[1] = y[rand_idx] + synth_inv[2] = 1 + + # swap prob + synth_swap = np.zeros(3) + swap_exist = (len(coord_list[1]) > 0) or (len(coord_list[3]) > 0) + if (num_valid_joint <= 4 and num_overlap > 0) or (num_valid_joint <= 5 and num_overlap >= 1): + swap_prob = 0.10 + else: + swap_prob = 0.04 + if swap_exist: + swap_pt_list = [] + for swap_idx in range(len(tot_coord_list)): + if swap_idx == 0 or swap_idx == len(coord_list[0]) + len(coord_list[1]): + continue + angle = np.random.uniform(0, 2 * math.pi, [N]) + r = np.random.uniform(0, ks_50_dist[j], [N]) + x = tot_coord_list[swap_idx][0] + r * np.cos(angle) + y = tot_coord_list[swap_idx][1] + r * np.sin(angle) + dist_mask = True + for i in range(len(tot_coord_list)): + if i == 0 or i == len(coord_list[0]) + len(coord_list[1]): + dist_mask = np.logical_and( + dist_mask, np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2 + ) > r + ) + x = x[dist_mask].reshape(-1) + y = y[dist_mask].reshape(-1) + if len(x) > 0: + coord = np.transpose(np.vstack([x, y]), [1, 0]) + swap_pt_list.append(coord) + if len(swap_pt_list) > 0: + swap_pt_list = np.concatenate(swap_pt_list, axis=0).reshape(-1, 2) + rand_idx = random.randrange(0, len(swap_pt_list)) + synth_swap[0] = swap_pt_list[rand_idx][0] + synth_swap[1] = swap_pt_list[rand_idx][1] + synth_swap[2] = 1 + + # good prob + synth_good = np.zeros(3) + good_prob = 1 - (jitter_prob + miss_prob + inv_prob + swap_prob) + assert good_prob >= 0 + angle = np.random.uniform(0, 2 * math.pi, [N // 4]) + r = np.random.uniform(0, ks_85_dist[j], [N // 4]) + good_idx = 0 # gt + x = tot_coord_list[good_idx][0] + r * np.cos(angle) + y = tot_coord_list[good_idx][1] + r * np.sin(angle) + dist_mask = True + for i in range(len(tot_coord_list)): + if i == good_idx: + continue + dist_mask = np.logical_and( + dist_mask, np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r + ) + x = x[dist_mask].reshape(-1) + y = y[dist_mask].reshape(-1) + if len(x) > 0: + rand_idx = random.randrange(0, len(x)) + synth_good[0] = x[rand_idx] + synth_good[1] = y[rand_idx] + synth_good[2] = 1 + + if synth_jitter[2] == 0: + jitter_prob = 0 + if synth_inv[2] == 0: + inv_prob = 0 + if synth_swap[2] == 0: + swap_prob = 0 + if synth_miss[2] == 0: + miss_prob = 0 + if synth_good[2] == 0: + good_prob = 0 + + normalizer = jitter_prob + miss_prob + inv_prob + swap_prob + good_prob + if normalizer == 0: + synth_joints[j] = 0 + continue + + jitter_prob = jitter_prob / normalizer + miss_prob = miss_prob / normalizer + inv_prob = inv_prob / normalizer + swap_prob = swap_prob / normalizer + good_prob = good_prob / normalizer + + prob_list = [jitter_prob, miss_prob, inv_prob, swap_prob, good_prob] + synth_list = [synth_jitter, synth_miss, synth_inv, synth_swap, synth_good] + sampled_idx = np.random.choice(5, 1, p=prob_list)[0] + synth_joints[j] = synth_list[sampled_idx] + synth_joints[j, 2] = 0 + + return synth_joints + + def get_distance_wrt_keypoint_sim(self, ks: float, area: float) -> np.ndarray: + """ + Args: + ks: the desired keypoint similarity + area: the area of the bounding box for the individual + + Returns: + For each bodypart, the L2 distance for which the keypoint similarity is + equal to ks + """ + return np.sqrt(-2 * area * ((self.keypoint_sigmas * 2) ** 2) * np.log(ks)) diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py new file mode 100644 index 0000000000..c3e41eb51d --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -0,0 +1,108 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Classes and functions to manipulate images""" +from __future__ import annotations + +from pathlib import Path + +import cv2 +import numpy as np +import torch +from torchvision.ops import box_convert +from torchvision.transforms import functional as F + + +def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: + """Loads an image from a file using cv2 + + Args: + filepath: the path of the file containing the image to load + color_mode: {'RGB', 'BGR'} the color mode to load the image with + + Returns: + the image as a numpy array + """ + image = cv2.imread(str(filepath)) + if color_mode == "RGB": + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + elif not color_mode == "BGR": + raise ValueError(f"Unsupported `color_mode`: {color_mode}") + + return image + + +def _crop_and_pad_image_torch( + image: np.array, bbox: np.array, bbox_format: str, output_size: int +) -> tuple[np.array, tuple[int, int], tuple[int, int]]: + """TODO: Reimplement this function with numpy and for non-square resize :) + Only works for square cropped bounding boxes. Crops images around bounding boxes + for top-down pose estimation in a MMpose style. Computes offsets so that + coordinates in the original image can be mapped to the cropped one; + + x_cropped = (x - offset_x) / scale_x + x_cropped = (y - offset_y) / scale_y + + Args: + image: (h, w, c) the image to crop + bbox: (4,) the bounding box to crop around + bbox_format: {"xyxy", "xywh", "cxcywh"} the format of the bounding box + output_size: the size to resize the image to + + Returns: + cropped_image, (offset_x, offset_y), (scale_x, scale_y) + """ + image = torch.tensor(image).permute(2, 0, 1) + bbox = torch.tensor(bbox) + if bbox_format != "cxcywh": + bbox = box_convert(bbox.unsqueeze(0), bbox_format, "cxcywh").squeeze() + + c, h, w = image.shape + crop_size = torch.max(bbox[2:]) + + xmin = int(torch.clip(bbox[0] - (crop_size / 2), min=0, max=w - 1).cpu().item()) + xmax = int(torch.clip(bbox[0] + (crop_size / 2), min=1, max=w).cpu().item()) + ymin = int(torch.clip(bbox[1] - (crop_size / 2), min=0, max=h - 1).cpu().item()) + ymax = int(torch.clip(bbox[1] + (crop_size / 2), min=1, max=h).cpu().item()) + cropped_image = image[:, ymin:ymax, xmin:xmax] + + crop_h, crop_w = cropped_image.shape[1:3] + pad_size = max(crop_h, crop_w) + offset = (xmin, ymin) + + # Pad image if not square + if not crop_h == crop_w: + padded_cropped_image = torch.zeros((c, pad_size, pad_size), dtype=image.dtype) + # Try to center bbox in padding + w_start = 0 + if bbox[0] - (crop_size / 2) < 0: + # padding on the left + w_start = pad_size - crop_w + elif bbox[0] + (crop_size / 2) >= w: + # padding on the right + w_start = 0 + + h_start = 0 + if bbox[1] - (crop_size / 2) < 0: + # padding at the top + h_start = pad_size - crop_h + elif bbox[1] + (crop_size / 2) >= h: + # padding at the bottom + h_start = 0 + + h_end = h_start + crop_h + w_end = w_start + crop_w + offset = (offset[0] - w_start, offset[1] - h_start) + padded_cropped_image[:, h_start:h_end, w_start:w_end] = cropped_image + cropped_image = padded_cropped_image + + scale = pad_size / output_size + output = F.resize(cropped_image, [output_size, output_size], antialias=True) + return output.permute(1, 2, 0).numpy(), offset, (scale, scale) diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 2d2cd6f593..a7cdfa5092 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -16,11 +16,13 @@ from typing import Any, TypeVar import albumentations as A -import cv2 import numpy as np import torch -from deeplabcut.pose_estimation_pytorch.data.utils import _crop_and_pad_image_torch +from deeplabcut.pose_estimation_pytorch.data.image import ( + load_image, + _crop_and_pad_image_torch, +) Image = TypeVar("Image", torch.Tensor, np.ndarray, str, Path) Context = TypeVar("Context", dict[str, Any], None) @@ -126,18 +128,14 @@ def __call__(self, image: Image, context: Context) -> tuple[Image, Context]: class LoadImage(Preprocessor): """Loads an image from a file, if not yet loaded""" - def __init__(self, color_mode: str = "RBG") -> None: + def __init__(self, color_mode: str = "RGB") -> None: self.color_mode = color_mode def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context]: if isinstance(image, (str, Path)): - image_ = cv2.imread(str(image)) - if self.color_mode == "RGB": - image_ = cv2.cvtColor(image_, cv2.COLOR_BGR2RGB) - else: - image_ = image + image = load_image(image, color_mode=self.color_mode) - return image_, context + return image, context class AugmentImage(Preprocessor): diff --git a/deeplabcut/pose_estimation_pytorch/data/utils.py b/deeplabcut/pose_estimation_pytorch/data/utils.py index 065ac7be47..8d1339a8ed 100644 --- a/deeplabcut/pose_estimation_pytorch/data/utils.py +++ b/deeplabcut/pose_estimation_pytorch/data/utils.py @@ -16,10 +16,7 @@ import albumentations as A import numpy as np -import torch from PIL import Image -from torchvision.ops import box_convert -from torchvision.transforms import functional as F @lru_cache(maxsize=None) @@ -253,75 +250,6 @@ def _crop_image_keypoints( return cropped_resized_image, cropped_resized_keypoints, offsets, scales -def _crop_and_pad_image_torch( - image: np.array, bbox: np.array, bbox_format: str, output_size: int -) -> tuple[np.array, tuple[int, int], tuple[int, int]]: - """TODO: Reimplement this function with numpy and for non-square resize :) - Only works for square cropped bounding boxes. Crops images around bounding boxes - for top-down pose estimation in a MMpose style. Computes offsets so that - coordinates in the original image can be mapped to the cropped one; - - x_cropped = (x - offset_x) / scale_x - x_cropped = (y - offset_y) / scale_y - - Args: - image: (h, w, c) the image to crop - bbox: (4,) the bounding box to crop around - bbox_format: {"xyxy", "xywh", "cxcywh"} the format of the bounding box - output_size: the size to resize the image to - - Returns: - cropped_image, (offset_x, offset_y), (scale_x, scale_y) - """ - image = torch.tensor(image).permute(2, 0, 1) - bbox = torch.tensor(bbox) - if bbox_format != "cxcywh": - bbox = box_convert(bbox.unsqueeze(0), bbox_format, "cxcywh").squeeze() - - c, h, w = image.shape - crop_size = torch.max(bbox[2:]) - - xmin = int(torch.clip(bbox[0] - (crop_size / 2), min=0, max=w - 1).cpu().item()) - xmax = int(torch.clip(bbox[0] + (crop_size / 2), min=1, max=w).cpu().item()) - ymin = int(torch.clip(bbox[1] - (crop_size / 2), min=0, max=h - 1).cpu().item()) - ymax = int(torch.clip(bbox[1] + (crop_size / 2), min=1, max=h).cpu().item()) - cropped_image = image[:, ymin:ymax, xmin:xmax] - - crop_h, crop_w = cropped_image.shape[1:3] - pad_size = max(crop_h, crop_w) - offset = (xmin, ymin) - - # Pad image if not square - if not crop_h == crop_w: - padded_cropped_image = torch.zeros((c, pad_size, pad_size), dtype=image.dtype) - # Try to center bbox in padding - w_start = 0 - if bbox[0] - (crop_size / 2) < 0: - # padding on the left - w_start = pad_size - crop_w - elif bbox[0] + (crop_size / 2) >= w: - # padding on the right - w_start = 0 - - h_start = 0 - if bbox[1] - (crop_size / 2) < 0: - # padding at the top - h_start = pad_size - crop_h - elif bbox[1] + (crop_size / 2) >= h: - # padding at the bottom - h_start = 0 - - h_end = h_start + crop_h - w_end = w_start + crop_w - offset = (offset[0] - w_start, offset[1] - h_start) - padded_cropped_image[:, h_start:h_end, w_start:w_end] = cropped_image - cropped_image = padded_cropped_image - - scale = pad_size / output_size - output = F.resize(cropped_image, [output_size, output_size], antialias=True) - return output.permute(1, 2, 0).numpy(), offset, (scale, scale) - - def _compute_crop_bounds( bboxes: np.ndarray, image_shape: tuple[int, int, int] ) -> np.ndarray: diff --git a/deeplabcut/pose_estimation_pytorch/task.py b/deeplabcut/pose_estimation_pytorch/task.py index 944b9bc441..d31e1b9d0d 100644 --- a/deeplabcut/pose_estimation_pytorch/task.py +++ b/deeplabcut/pose_estimation_pytorch/task.py @@ -27,6 +27,7 @@ class Task(TaskDataMixin, Enum): BOTTOM_UP = ("BU", "BottomUp"), "snapshot" DETECT = ("DT", "Detect"), "snapshot-detector" TOP_DOWN = ("TD", "TopDown"), "snapshot" + CTD = ("CTD", "ConditionalTopDown"), "snapshot" @classmethod def _missing_(cls, value): From 4503b87c91e8b336cc34e3a01213cab93ac54740 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 21 Mar 2024 09:57:49 +0100 Subject: [PATCH 05/95] buctd cont I --- .../pose_estimation_pytorch/data/dataset.py | 17 ++++++++++++++--- .../data/generative_sampling.py | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 85aa6f65bd..b3af31cff7 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -27,6 +27,7 @@ pad_to_length, ) from deeplabcut.pose_estimation_pytorch.task import Task +from deeplabcut.pose_estimation_pytorch.data.generative_sampling import GenerativeSampler @dataclass(frozen=True) @@ -79,8 +80,11 @@ def __post_init__(self): img["id"]: index for index, img in enumerate(self.images) } + if self.task == Task.CTD: + self.generative_sampler = GenerativeSampler(self.parameters.num_joints()) + def __len__(self): - # TODO: TD should only return the number of annotations that aren't unique_bodyparts + # TODO: TD/CTD should only return the number of annotations that aren't unique_bodyparts if self.task in (Task.BOTTOM_UP, Task.DETECT): return len(self.images) @@ -146,8 +150,8 @@ def __getitem__(self, index: int) -> dict: annotations_merged, ) = self.extract_keypoints_and_bboxes(anns, image.shape) scales, offsets = (1, 1), (0, 0) - - if self.task == Task.TOP_DOWN: + + if self.task in (Task.TOP_DOWN, Task.CTD): if self.parameters.cropped_image_size is None: raise ValueError( "You must specify a cropped image size for top-down models" @@ -159,6 +163,13 @@ def __getitem__(self, index: int) -> dict: ) bboxes = bboxes.astype(int) + if self.task == Task.CTD: + synthesized_keypoints = self.generative_sampler( + keypoints=keypoints.reshape(-1, 3), near_keypoints=keypoints.reshape(-1, 3), + area=0, num_overlap=0 #TODO: add these arguments correctly + ) + # recompute bbox + # TODO: The following code should be replaced by a numpy version image, offsets, scales = _crop_and_pad_image_torch( image, bboxes[0], "xywh", self.parameters.cropped_image_size[0] diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index f2bb6640e0..e1eef89681 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -274,7 +274,7 @@ def __call__( # on top of inv gt, swap inv gt # FIXME: In the original codebase, they only swap symmetric keypoints. As # we don't always have symmetries for keypoints in DeepLabCut, we swap any - # keypoints with any other keypoint by randomly selecting keypoints to swap + # keypoint with any other keypoint by randomly selecting keypoints to swap kps_symmetry = self.keypoints_symmetry pair_exist = False for (q, w) in kps_symmetry: @@ -370,7 +370,7 @@ def __call__( synth_miss[1] = miss_pt_list[rand_idx][1] synth_miss[2] = 1 - # inversion prob + # inversion prob synth_inv = np.zeros(3) inv_prob = 0.03 if pair_exist and keypoints[pair_idx, 2] > 0: From ec4aa8735e74d283c72fa0c0ec9668011ad7df07 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 21 Mar 2024 11:34:00 +0100 Subject: [PATCH 06/95] prepare dataloader for loading cond pose with gen sampling --- .../pose_estimation_pytorch/data/dataset.py | 36 +++++++++++++++--- .../data/generative_sampling.py | 38 +++++++++++-------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index b3af31cff7..98c81ad20b 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -13,7 +13,6 @@ from dataclasses import dataclass import albumentations as A -import cv2 import numpy as np from torch.utils.data import Dataset @@ -22,9 +21,11 @@ _crop_image_keypoints, _extract_keypoints_and_bboxes, apply_transform, + bbox_from_keypoints, map_id_to_annotations, map_image_path_to_id, pad_to_length, + safe_stack, ) from deeplabcut.pose_estimation_pytorch.task import Task from deeplabcut.pose_estimation_pytorch.data.generative_sampling import GenerativeSampler @@ -113,6 +114,15 @@ def _get_raw_item_crop(self, index: int) -> tuple[str, list[dict], int]: ann = self.annotations[index] img = self.images[self.img_id_to_index[ann["image_id"]]] return img["file_name"], [ann], img["id"] + + def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: + """ + Includes keypoints from other individuals in the image ("context"). + """ + ann = self.annotations[index] + img = self.images[self.img_id_to_index[ann["image_id"]]] + near_anns = [self.annotations[idx] for idx in self.annotation_idx_map[img["id"]] if idx != index] + return img["file_name"], [ann] + near_anns, img["id"] def __getitem__(self, index: int) -> dict: """ @@ -164,11 +174,14 @@ def __getitem__(self, index: int) -> dict: bboxes = bboxes.astype(int) if self.task == Task.CTD: + keypoints = keypoints[0] + near_keypoints = keypoints[1:] synthesized_keypoints = self.generative_sampler( - keypoints=keypoints.reshape(-1, 3), near_keypoints=keypoints.reshape(-1, 3), - area=0, num_overlap=0 #TODO: add these arguments correctly + keypoints=keypoints.reshape(-1, 3), + near_keypoints=near_keypoints.reshape(-1, 3), + area=bboxes[0,2]*bboxes[0,3], ) - # recompute bbox + bboxes[0] = bbox_from_keypoints(synthesized_keypoints[:, :2], original_size[0], original_size[1], 10) # TODO: The following code should be replaced by a numpy version image, offsets, scales = _crop_and_pad_image_torch( @@ -176,6 +189,10 @@ def __getitem__(self, index: int) -> dict: ) keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] + if self.task == Task.CTD: + synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] + synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] + keypoints = safe_stack([keypoints, synthesized_keypoints[None,...]], (0, self.parameters.num_joints(), 3)) bboxes = np.zeros((0, 4)) # No more bboxes as we cropped around them transformed = self.apply_transform_all_keypoints( @@ -224,6 +241,9 @@ def _prepare_final_data_dict( "annotations": self._prepare_final_annotation_dict( keypoints, keypoints_unique, bboxes, annotations_merged ), + "context": { + "cond_keypoints": keypoints[1,:,:2].astype(np.single) if self.task == Task.CTD else None, + } } def _prepare_final_annotation_dict( @@ -234,8 +254,10 @@ def _prepare_final_annotation_dict( anns: dict, ) -> dict[str, np.ndarray]: num_animals = self.parameters.max_num_animals - if self.task == Task.TOP_DOWN: + if self.task in (Task.TOP_DOWN, Task.CTD): num_animals = 1 + if self.task == Task.CTD: + keypoints = keypoints[0] return { "keypoints": pad_to_length(keypoints[..., :2], num_animals, -1).astype(np.single), @@ -268,8 +290,10 @@ def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: Returns: tuple: Tuple containing the image path, annotations, and image ID. """ - if self.task in (Task.TOP_DOWN, Task.CTD): + if self.task == Task.TOP_DOWN: return self._get_raw_item_crop(index) + elif self.task == Task.CTD: + return self._get_raw_item_crop_context(index) elif self.task in (Task.BOTTOM_UP, Task.DETECT): return self._get_raw_item(index) diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index e1eef89681..a05202d1b7 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -222,7 +222,7 @@ def __call__( keypoints: np.ndarray, near_keypoints: np.ndarray, area: float, # ?? - num_overlap: int, # ?? + #num_overlap: int, # ?? ) -> np.ndarray: """Samples keypoints @@ -303,10 +303,13 @@ def __call__( # jitter error synth_jitter = np.zeros(3) - if num_valid_joint <= 4: - jitter_prob = 0.20 - else: - jitter_prob = 0.15 + + # if num_valid_joint <= 4: + # jitter_prob = 0.20 + # else: + # jitter_prob = 0.15 + jitter_prob = 0.16 + angle = np.random.uniform(0, 2 * math.pi, [N]) r = np.random.uniform(ks_85_dist[j], ks_50_dist[j], [N]) jitter_idx = 0 # gt @@ -329,12 +332,14 @@ def __call__( # miss error synth_miss = np.zeros(3) - if num_valid_joint <= 2: - miss_prob = 0.20 - elif num_valid_joint <= 4: - miss_prob = 0.13 - else: - miss_prob = 0.05 + + # if num_valid_joint <= 2: + # miss_prob = 0.20 + # elif num_valid_joint <= 4: + # miss_prob = 0.13 + # else: + # miss_prob = 0.05 + miss_prob = 0.10 miss_pt_list = [] for miss_idx in range(len(tot_coord_list)): @@ -399,10 +404,13 @@ def __call__( # swap prob synth_swap = np.zeros(3) swap_exist = (len(coord_list[1]) > 0) or (len(coord_list[3]) > 0) - if (num_valid_joint <= 4 and num_overlap > 0) or (num_valid_joint <= 5 and num_overlap >= 1): - swap_prob = 0.10 - else: - swap_prob = 0.04 + + # if (num_valid_joint <= 4 and num_overlap > 0) or (num_valid_joint <= 5 and num_overlap >= 1): + # swap_prob = 0.10 + # else: + # swap_prob = 0.04 + swap_prob = 0.08 + if swap_exist: swap_pt_list = [] for swap_idx in range(len(tot_coord_list)): From 6c0cf379b351f9b173aa26b6d93cfc0b1617d1c0 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 21 Mar 2024 15:23:36 +0100 Subject: [PATCH 07/95] adapt hrnet-coam output to hrnet code --- .../models/backbones/hrnet.py | 31 +++++++++++-------- .../models/backbones/hrnet_coam.py | 11 ++++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py index ae1e7902f9..02129906fb 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py @@ -58,6 +58,22 @@ def __init__( self.model = _load_hrnet(model_name, pretrained, increased_channel_count) self.interpolate_branches = interpolate_branches + def prepare_output(self, y_list: list) -> torch.Tensor: + if not self.interpolate_branches: + return y_list[0] + + x0_h, x0_w = y_list[0].size(2), y_list[0].size(3) + x = torch.cat( + [ + y_list[0], + F.interpolate(y_list[1], size=(x0_h, x0_w), mode="bilinear"), + F.interpolate(y_list[2], size=(x0_h, x0_w), mode="bilinear"), + F.interpolate(y_list[3], size=(x0_h, x0_w), mode="bilinear"), + ], + 1, + ) + return x + def forward(self, x: torch.Tensor) -> torch.Tensor: """Forward pass through the HRNet backbone. @@ -75,20 +91,9 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: >>> y = backbone(x) """ y_list = self.model(x) - if not self.interpolate_branches: - return y_list[0] - x0_h, x0_w = y_list[0].size(2), y_list[0].size(3) - x = torch.cat( - [ - y_list[0], - F.interpolate(y_list[1], size=(x0_h, x0_w), mode="bilinear"), - F.interpolate(y_list[2], size=(x0_h, x0_w), mode="bilinear"), - F.interpolate(y_list[3], size=(x0_h, x0_w), mode="bilinear"), - ], - 1, - ) - return x + return self.prepare_output(y_list) + def _load_hrnet( diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index d1c6202a42..c8c30edc5f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -44,6 +44,7 @@ def __init__( cond_enc: str = 'colored', img_size: tuple[int,int] = (256, 256), num_joints: int = 17, + **kwargs, ) -> None: """Constructs an ImageNet pretrained HRNet from timm and creates CoAM blocks. @@ -62,7 +63,8 @@ def __init__( super().__init__( model_name = base_model_name, pretrained = pretrained, - only_high_res = True) + only_high_res = True, + **kwargs) self.coam_modules = coam_modules self.selfatt_coam_modules = selfatt_coam_modules @@ -166,6 +168,7 @@ def forward(self, x): raise Exception("condition is empty, please check your dataloader") x_ = x[:,:3] cond_hm = x[:,3:] + # TODO: cond_hm = self.cond_encoder(cond_hm) # Stem x = self.model.conv1(x_) @@ -178,7 +181,7 @@ def forward(self, x): # Stages y = self.stages(x, cond_hm) - # TODO: @niels, check if the final layer should be ran - #y = self.model.final_layer(y[0]) + if self.model.incre_modules is not None: + x = [incre(f) for f, incre in zip(x, self.model.incre_modules)] - return y + return self.prepare_output(y) From 704a0c6e41bfd0f688e3fec94a1577e92e007ef0 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 21 Mar 2024 15:33:00 +0100 Subject: [PATCH 08/95] put kpt encoders into modules --- .../data/generative_sampling.py | 145 ---------------- .../models/modules/kpt_encoders.py | 158 ++++++++++++++++++ 2 files changed, 158 insertions(+), 145 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index a05202d1b7..1e47bb62d2 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -44,153 +44,8 @@ import math import random -from abc import ABC, abstractmethod - -import cv2 import numpy as np -from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg - - -KEYPOINT_ENCODERS = Registry("detectors", build_func=build_from_cfg) - - -class BaseKeypointEncoder(ABC): - """Encodes keypoints into heatmaps - - Modified from BUCTD/data/JointsDataset - """ - - def __init__(self, kernel_size: tuple[int, int] = (15, 15)) -> None: - """ - Args: - kernel_size: the Gaussian kernel size to use when blurring a heatmap - """ - self.kernel_size = kernel_size - - @abstractmethod - def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: - """ - Args: - keypoints: the keypoints to encode - size: the (height, width) of the heatmap in which the keypoints should - be encoded - - Returns: - the encoded keypoints - """ - raise NotImplementedError - - def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: - """Applies a Gaussian blur to a heatmap - - Taken from BUCTD/data/JointsDataset, generate_heatmap - - Args: - heatmap: the heatmap to blur (with values in [0, 1] or [0, 255]) - - Returns: - The heatmap with a Gaussian blur, such that max(heatmap) = 255 - """ - heatmap = cv2.GaussianBlur(heatmap, self.kernel_size, sigmaX=0) - am = np.amax(heatmap) - if am == 0: - return heatmap - heatmap /= (am / 255) - return heatmap - - -@KEYPOINT_ENCODERS.register_module -class StackedKeypointEncoder(BaseKeypointEncoder): - """Encodes keypoints into heatmaps, where each - - Modified from BUCTD/data/JointsDataset, get_stacked_condition - """ - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - - def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: - """ - Args: - keypoints: the keypoints to encode - size: the (height, width) of the heatmap in which the keypoints should - be encoded - - Returns: - the encoded keypoints - """ - kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) - zero_matrix = np.zeros(size) - - def _get_condition_matrix(zero_matrix_, kpt_): - if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: - zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 - return zero_matrix_ - - condition_heatmap_list = [] - for i, kpt in enumerate(kpts): - condition = _get_condition_matrix(zero_matrix, kpt) - condition_heatmap = self.blur_heatmap(condition) - condition_heatmap_list.append(condition_heatmap) - zero_matrix = np.zeros(size) - - # ### debug: visualization -> check conditions - # condition_heatmap = np.expand_dims(condition_heatmap, axis=0) - # condition = np.repeat(condition_heatmap, 3, axis=0) - # print("condition", condition.shape) - # condition = np.transpose(condition, (1, 2, 0)) - # cv2.imwrite(f'/media/data/mu/test/cond_{i}.jpg', condition+image) - # cv2.imwrite(f'/media/data/mu/test/image.jpg', image) - - condition_heatmap_list = np.moveaxis(np.array(condition_heatmap_list), 0, -1) - return condition_heatmap_list - - -@KEYPOINT_ENCODERS.register_module -class ColoredKeypointEncoder(BaseKeypointEncoder): - """Encodes keypoints into a given number of color channels - - Modified from BUCTD/data/JointsDataset, get_condition_image_colored - """ - - def __init__(self, colors: list[float], **kwargs) -> None: - """ - Args: - colors: the color to use for each keypoint - """ - super().__init__(**kwargs) - self.colors = colors - - def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: - """ - Args: - keypoints: the keypoints to encode - size: the (height, width) of the heatmap in which the keypoints should - be encoded - - Returns: - the encoded keypoints - """ - if not len(keypoints) == len(self.colors): - raise ValueError( - f"Cannot encode the keypoints. Initialized with {len(self.colors)} " - f"colors, but there are {len(keypoints)} to encode" - ) - - kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) - zero_matrix = np.zeros(size) - - def _get_condition_matrix(zero_matrix, kpts): - for color, kpt in zip(self.colors, kpts): - if 0 < kpt[0] < size[1] and 0 < kpt[1] < size[0]: - zero_matrix[kpt[1] - 1][kpt[0] - 1] = color - return zero_matrix - - condition = _get_condition_matrix(zero_matrix, kpts) - condition_heatmap = self.blur_heatmap(condition) - return condition_heatmap - class GenerativeSampler: """Performs generative sampling of keypoints for CTD model training""" diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py new file mode 100644 index 0000000000..8039aa4133 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -0,0 +1,158 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +from abc import ABC, abstractmethod + +import cv2 +import numpy as np + +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg + + +KEYPOINT_ENCODERS = Registry("detectors", build_func=build_from_cfg) + + +class BaseKeypointEncoder(ABC): + """Encodes keypoints into heatmaps + + Modified from BUCTD/data/JointsDataset + """ + + def __init__(self, kernel_size: tuple[int, int] = (15, 15)) -> None: + """ + Args: + kernel_size: the Gaussian kernel size to use when blurring a heatmap + """ + self.kernel_size = kernel_size + + @abstractmethod + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + raise NotImplementedError + + def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: + """Applies a Gaussian blur to a heatmap + + Taken from BUCTD/data/JointsDataset, generate_heatmap + + Args: + heatmap: the heatmap to blur (with values in [0, 1] or [0, 255]) + + Returns: + The heatmap with a Gaussian blur, such that max(heatmap) = 255 + """ + heatmap = cv2.GaussianBlur(heatmap, self.kernel_size, sigmaX=0) + am = np.amax(heatmap) + if am == 0: + return heatmap + heatmap /= (am / 255) + return heatmap + + +@KEYPOINT_ENCODERS.register_module +class StackedKeypointEncoder(BaseKeypointEncoder): + """Encodes keypoints into heatmaps, where each + + Modified from BUCTD/data/JointsDataset, get_stacked_condition + """ + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) + zero_matrix = np.zeros(size) + + def _get_condition_matrix(zero_matrix_, kpt_): + if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: + zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 + return zero_matrix_ + + condition_heatmap_list = [] + for i, kpt in enumerate(kpts): + condition = _get_condition_matrix(zero_matrix, kpt) + condition_heatmap = self.blur_heatmap(condition) + condition_heatmap_list.append(condition_heatmap) + zero_matrix = np.zeros(size) + + # ### debug: visualization -> check conditions + # condition_heatmap = np.expand_dims(condition_heatmap, axis=0) + # condition = np.repeat(condition_heatmap, 3, axis=0) + # print("condition", condition.shape) + # condition = np.transpose(condition, (1, 2, 0)) + # cv2.imwrite(f'/media/data/mu/test/cond_{i}.jpg', condition+image) + # cv2.imwrite(f'/media/data/mu/test/image.jpg', image) + + condition_heatmap_list = np.moveaxis(np.array(condition_heatmap_list), 0, -1) + return condition_heatmap_list + + +@KEYPOINT_ENCODERS.register_module +class ColoredKeypointEncoder(BaseKeypointEncoder): + """Encodes keypoints into a given number of color channels + + Modified from BUCTD/data/JointsDataset, get_condition_image_colored + """ + + def __init__(self, colors: list[float], **kwargs) -> None: + """ + Args: + colors: the color to use for each keypoint + """ + super().__init__(**kwargs) + self.colors = colors + + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: + """ + Args: + keypoints: the keypoints to encode + size: the (height, width) of the heatmap in which the keypoints should + be encoded + + Returns: + the encoded keypoints + """ + if not len(keypoints) == len(self.colors): + raise ValueError( + f"Cannot encode the keypoints. Initialized with {len(self.colors)} " + f"colors, but there are {len(keypoints)} to encode" + ) + + kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) + zero_matrix = np.zeros(size) + + def _get_condition_matrix(zero_matrix, kpts): + for color, kpt in zip(self.colors, kpts): + if 0 < kpt[0] < size[1] and 0 < kpt[1] < size[0]: + zero_matrix[kpt[1] - 1][kpt[0] - 1] = color + return zero_matrix + + condition = _get_condition_matrix(zero_matrix, kpts) + condition_heatmap = self.blur_heatmap(condition) + return condition_heatmap From a53ba27ae365c7b251f6adf376d28d901a775a08 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 21 Mar 2024 17:00:48 +0100 Subject: [PATCH 09/95] add kpt encoder to hrnet_coam --- .../config/ctd/hrnet_coam_w32.yaml | 5 +++- .../config/ctd/hrnet_coam_w48.yaml | 5 +++- .../pose_estimation_pytorch/data/dataset.py | 2 +- .../models/backbones/hrnet_coam.py | 27 ++++++++++-------- .../models/modules/__init__.py | 6 ++++ .../models/modules/coam_module.py | 9 ++---- .../models/modules/kpt_encoders.py | 28 ++++++++++++++++--- .../pose_estimation_pytorch/runners/train.py | 7 ++++- 8 files changed, 62 insertions(+), 27 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml index 9b90d9524f..a7ba5d0b24 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml @@ -10,6 +10,9 @@ model: coam_modules: (2,) channel_att_only: false att_heads: 1 - cond_enc: colored + cond_enc: + type: ColoredKeypointEncoder + num_joints: 17 + kernel_size: [15, 15] num_joints: 17 backbone_output_channels: 480 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml index e597b1c1f5..875987473b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml @@ -10,6 +10,9 @@ model: coam_modules: (2,) channel_att_only: false att_heads: 1 - cond_enc: colored + cond_enc: + type: ColoredKeypointEncoder + num_joints: 17 + kernel_size: [15, 15] num_joints: 17 backbone_output_channels: 720 diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 98c81ad20b..0ff83c9640 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -242,7 +242,7 @@ def _prepare_final_data_dict( keypoints, keypoints_unique, bboxes, annotations_merged ), "context": { - "cond_keypoints": keypoints[1,:,:2].astype(np.single) if self.task == Task.CTD else None, + "cond_keypoints": keypoints[1,:,:2].astype(np.single) if self.task == Task.CTD else False, } } diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index c8c30edc5f..7a18ab9cb7 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -19,6 +19,10 @@ from deeplabcut.pose_estimation_pytorch.models.modules import ( CoAMBlock, SelfAttentionModule_CoAM, + BaseKeypointEncoder, + #ColoredKeypointEncoder, + #StackedKeypointEncoder, + KEYPOINT_ENCODERS ) @@ -35,13 +39,13 @@ class HRNetCoAM(HRNet): def __init__( self, + kpt_encoder: dict | BaseKeypointEncoder, base_model_name: str = "hrnet_w32", pretrained: bool = True, coam_modules: tuple[int,...] = (2,), selfatt_coam_modules: tuple[int,...] | None = None, channel_att_only: bool = False, att_heads: int = 1, - cond_enc: str = 'colored', img_size: tuple[int,int] = (256, 256), num_joints: int = 17, **kwargs, @@ -69,7 +73,9 @@ def __init__( self.coam_modules = coam_modules self.selfatt_coam_modules = selfatt_coam_modules self.channel_att_only = channel_att_only - self.cond_enc = cond_enc + if not isinstance(kpt_encoder, BaseKeypointEncoder): + kpt_encoder = KEYPOINT_ENCODERS.build(kpt_encoder) + self.cond_enc = kpt_encoder self.coam_stages = [None, None, None, None] self.selfatt_coam_stages = [None, None, None, None] @@ -95,8 +101,8 @@ def __init__( channels = all_output_channels[coam_pos-1] self.coam_stages[coam_pos-1] = CoAMBlock(spat_dims=spat_dims_, channel_list=channels, - cond_stacked=self.cond_enc, num_joints = num_joints, - n_heads=att_heads, channel_only=self.channel_att_only) + cond_enc=self.cond_enc, n_heads=att_heads, + channel_only=self.channel_att_only) if self.selfatt_coam_modules: for selfatt_coam_pos in self.selfatt_coam_modules: @@ -147,7 +153,7 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: return yl - def forward(self, x): + def forward(self, x, cond_hm): """Forward pass through the HRNetCoAM backbone. Args: @@ -163,15 +169,12 @@ def forward(self, x): >>> x = torch.randn(1, 6, 256, 256) >>> y = backbone(x) """ - - if x[:,3:].shape[1] == 0: - raise Exception("condition is empty, please check your dataloader") - x_ = x[:,:3] - cond_hm = x[:,3:] - # TODO: cond_hm = self.cond_encoder(cond_hm) + cond_hm = self.cond_enc(cond_hm, x.size()[2:]).to(x.device) + cond_hm = cond_hm.permute(2, 0, 1) + # Stem - x = self.model.conv1(x_) + x = self.model.conv1(x) x = self.model.bn1(x) x = self.model.act1(x) x = self.model.conv2(x) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py index 1ce83d79e3..6dc9ac2fcf 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py @@ -20,3 +20,9 @@ CoAMBlock, SelfAttentionModule_CoAM ) +from deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders import ( + BaseKeypointEncoder, + ColoredKeypointEncoder, + StackedKeypointEncoder, + KEYPOINT_ENCODERS +) \ No newline at end of file diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index 26fc6cb0c8..f8c7c86ef9 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -17,17 +17,12 @@ class CoAMBlock(nn.Module): - def __init__(self, spat_dims, channel_list, cond_enc, num_joints, n_heads=1, channel_only=False): + def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): super(CoAMBlock, self).__init__() self.att_layers = [] self.spat_dims = spat_dims self.cond_enc = cond_enc - if cond_enc == 'stacked': - d_cond = num_joints - elif cond_enc == 'colored': - d_cond = 3 - else: - d_cond = 1 + d_cond = cond_enc.num_channels() for i in range(len(spat_dims)): att_layer = DAModule(d_model = channel_list[i], d_cond = d_cond, kernel_size = 3, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 8039aa4133..e0d6500873 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -14,11 +14,12 @@ import cv2 import numpy as np +import matplotlib.pyplot as plt from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg -KEYPOINT_ENCODERS = Registry("detectors", build_func=build_from_cfg) +KEYPOINT_ENCODERS = Registry("kpt_encoders", build_func=build_from_cfg) class BaseKeypointEncoder(ABC): @@ -27,12 +28,17 @@ class BaseKeypointEncoder(ABC): Modified from BUCTD/data/JointsDataset """ - def __init__(self, kernel_size: tuple[int, int] = (15, 15)) -> None: + def __init__(self, num_joints, kernel_size: tuple[int, int] = (15, 15)) -> None: """ Args: kernel_size: the Gaussian kernel size to use when blurring a heatmap """ self.kernel_size = kernel_size + self.num_joints = num_joints + + @property + def num_channels(self): + pass @abstractmethod def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: @@ -76,6 +82,10 @@ class StackedKeypointEncoder(BaseKeypointEncoder): def __init__(self, **kwargs) -> None: super().__init__(**kwargs) + @property + def num_channels(self): + return self.num_joints + def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: """ Args: @@ -120,13 +130,17 @@ class ColoredKeypointEncoder(BaseKeypointEncoder): Modified from BUCTD/data/JointsDataset, get_condition_image_colored """ - def __init__(self, colors: list[float], **kwargs) -> None: + def __init__(self, **kwargs) -> None: """ Args: colors: the color to use for each keypoint """ super().__init__(**kwargs) - self.colors = colors + self.colors = self.get_colors_from_cmap('rainbow', self.num_joints) + + @property + def num_channels(self): + return 3 def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: """ @@ -156,3 +170,9 @@ def _get_condition_matrix(zero_matrix, kpts): condition = _get_condition_matrix(zero_matrix, kpts) condition_heatmap = self.blur_heatmap(condition) return condition_heatmap + + def get_colors_from_cmap(self, cmap_name, num_colors): + cmap = plt.get_cmap(cmap_name) + colors_float = [cmap(i) for i in range(0, 256, 256 // num_colors)] + colors = [(int(r*255), int(g*255), int(b*255)) for r, g, b, _ in colors_float] + return colors diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index 9e90791e57..cf3480d02f 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -275,7 +275,12 @@ def step( inputs = batch["image"] inputs = inputs.to(self.device) - outputs = self.model(inputs) + if batch['context']['cond_keypoints'][0]: + cond_kpts = batch['context']['cond_keypoints'] + #cond_kpts = cond_kpts.to(self.device) + outputs = self.model(inputs, cond_kpts) + else: + outputs = self.model(inputs) target = self.model.get_target(inputs, outputs, batch["annotations"]) losses_dict = self.model.get_loss(outputs, target) From 964dd5c595576e5bd8516662011b704b14aedbff Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 22 Mar 2024 11:01:27 +0100 Subject: [PATCH 10/95] small additions --- .../pose_estimation_pytorch/models/modules/coam_module.py | 3 +++ .../pose_estimation_pytorch/models/modules/kpt_encoders.py | 2 +- deeplabcut/pose_estimation_pytorch/runners/train.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index f8c7c86ef9..aa7cd7dec2 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -17,6 +17,9 @@ class CoAMBlock(nn.Module): + """ + Conditional Attention Module (CoAM) block. + """ def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): super(CoAMBlock, self).__init__() self.att_layers = [] diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index e0d6500873..4f256d9c73 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -28,7 +28,7 @@ class BaseKeypointEncoder(ABC): Modified from BUCTD/data/JointsDataset """ - def __init__(self, num_joints, kernel_size: tuple[int, int] = (15, 15)) -> None: + def __init__(self, num_joints: int, kernel_size: tuple[int, int] = (15, 15)) -> None: """ Args: kernel_size: the Gaussian kernel size to use when blurring a heatmap diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index cf3480d02f..42a6159c2e 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -277,7 +277,7 @@ def step( inputs = inputs.to(self.device) if batch['context']['cond_keypoints'][0]: cond_kpts = batch['context']['cond_keypoints'] - #cond_kpts = cond_kpts.to(self.device) + #cond_kpts = cond_kpts.to(self.device) # cond kpts are put on device after heatmap creation outputs = self.model(inputs, cond_kpts) else: outputs = self.model(inputs) From 3e85fd1879bf1c67d3af3eabb44c44fadc8d2ad7 Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 22 Mar 2024 15:37:31 +0100 Subject: [PATCH 11/95] init CTD inference --- .../pose_estimation_pytorch/apis/evaluate.py | 8 +++ .../pose_estimation_pytorch/apis/utils.py | 18 ++++-- .../data/preprocessor.py | 58 ++++++++++++++++++- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index 7c70851372..fd333fde4e 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -78,6 +78,14 @@ def predict( ground_truth_bboxes = loader.ground_truth_bboxes(mode=mode) context = [{"bboxes": ground_truth_bboxes[image]} for image in image_paths] + elif pose_task == Task.CTD: + # Get conditional keypoints for context + if pose_runner is not None: + pose_predictions = pose_runner.inference(images=tqdm(image_paths)) + context = [{"cond_kpts": pose_pred} for pose_pred in pose_predictions] + else: + raise NotImplementedError("Conditional top-down models require a pose runner") + images_with_context = image_paths if context is not None: if len(context) != len(image_paths): diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index f67ab422a6..6eb1b2d87b 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -28,6 +28,7 @@ from deeplabcut.pose_estimation_pytorch.data.preprocessor import ( build_bottom_up_preprocessor, build_top_down_preprocessor, + build_conditional_top_down_preprocessor, ) from deeplabcut.pose_estimation_pytorch.data.transforms import build_transforms from deeplabcut.pose_estimation_pytorch.models import DETECTORS, PoseModel @@ -285,11 +286,18 @@ def get_inference_runners( with_identity=with_identity, ) else: - pose_preprocessor = build_top_down_preprocessor( - color_mode=model_config["data"]["colormode"], - transform=transform, - cropped_image_size=(256, 256), - ) + if pose_task == Task.CTD: + pose_preprocessor = build_conditional_top_down_preprocessor( + color_mode=model_config["data"]["colormode"], + transform=transform, + cropped_image_size=(256, 256), + ) + else: + pose_preprocessor = build_top_down_preprocessor( + color_mode=model_config["data"]["colormode"], + transform=transform, + cropped_image_size=(256, 256), + ) pose_postprocessor = build_top_down_postprocessor( max_individuals=max_individuals, num_bodyparts=num_bodyparts, diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index a7cdfa5092..0aaefdd47a 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -23,6 +23,9 @@ load_image, _crop_and_pad_image_torch, ) +from deeplabcut.pose_estimation_pytorch.data.utils import ( + bbox_from_keypoints, +) Image = TypeVar("Image", torch.Tensor, np.ndarray, str, Path) Context = TypeVar("Context", dict[str, Any], None) @@ -109,6 +112,35 @@ def build_top_down_preprocessor( ] ) +def build_conditional_top_down_preprocessor( + color_mode: str, transform: A.BaseCompose, cropped_image_size: tuple[int, int] +) -> Preprocessor: + """Creates a preprocessor for conditional top-down pose estimation + + Creates a preprocessor that loads an image, computes bounding boxes from conditional + keypoints (given as a context (through a "cond_kpts" key), crops all bounding boxes, + runs some transforms on each cropped image (such as normalization), creates a tensor + from the numpy array (going from (num_ind, h, w, 3) to (num_ind, 3, h, w)). + + Args: + color_mode: whether to load the image as an RGB or BGR + transform: the transform to apply to the image + cropped_image_size: the size of images for each individual to give to the pose + estimator + + Returns: + A default top-down Preprocessor + """ + return ComposePreprocessor( + components=[ + LoadImage(color_mode), + ComputeBoundingBoxesFromCondKeypoints(), + TorchCropDetections(cropped_image_size=cropped_image_size[0]), + AugmentImage(transform), + ToTensor(), + ] + ) + class ComposePreprocessor(Preprocessor): """ @@ -318,7 +350,7 @@ def __call__( images, offsets, scales = [], [], [] for bbox in context["bboxes"]: cropped_image, offset, scale = _crop_and_pad_image_torch( - image, bbox, "xywh", self.cropped_image_size + image, bbox, self.bbox_format, self.cropped_image_size ) images.append(cropped_image) offsets.append(offset) @@ -327,6 +359,10 @@ def __call__( context["offsets"] = np.array(offsets) context["scales"] = np.array(scales) + if "cond_kpts" in context: + context["cond_kpts"][:, :, 0] = (context["cond_kpts"][:, :, 0] - offsets[0]) / scales[0] + context["cond_kpts"][:, :, 1] = (context["cond_kpts"][:, :, 1] - offsets[1]) / scales[1] + # can have no bounding boxes if detector made no detections if len(images) == 0: images = np.zeros((0, *image.shape)) @@ -334,3 +370,23 @@ def __call__( images = np.stack(images, axis=0) return images, context + + +class ComputeBoundingBoxesFromCondKeypoints(Preprocessor): + """TODO""" + + def __init__(self, cropped_image_size: int, bbox_format: str = "xywh") -> None: + self.cropped_image_size = cropped_image_size + self.bbox_format = bbox_format + + def __call__( + self, image: np.ndarray, context: Context + ) -> tuple[np.ndarray, Context]: + """TODO: numpy implementation""" + if "cond_kpts" not in context: + raise ValueError(f"Must include cond kpts to ComputeBBoxes, found {context}") + + context["bboxes"] = [bbox_from_keypoints(cond_kpts, image.shape[0], image.shape[1], 10) + for cond_kpts in context["cond_kpts"]] + + return image, context From 8c94c7b647ca58577cc6c24b72fee8d460cff14d Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 22 Mar 2024 20:26:51 +0100 Subject: [PATCH 12/95] test CTD training - I --- .../config/ctd/hrnet_coam_w32.yaml | 24 +++++++++++++++++++ .../config/ctd/hrnet_coam_w48.yaml | 24 +++++++++++++++++++ .../data/preprocessor.py | 7 +++--- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml index a7ba5d0b24..13544b6583 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml @@ -16,3 +16,27 @@ model: kernel_size: [15, 15] num_joints: 17 backbone_output_channels: 480 + heads: + bodypart: + type: HeatmapHead + predictor: + type: HeatmapPredictor + location_refinement: false + target_generator: + type: HeatmapGaussianGenerator + num_heatmaps: "num_bodyparts" + pos_dist_thresh: 17 + heatmap_mode: KEYPOINT + generate_locref: false + criterion: + heatmap: + type: WeightedBCECriterion + weight: 1.0 + heatmap_config: + channels: + - "backbone_output_channels" + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts" + kernel_size: 1 \ No newline at end of file diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml index 875987473b..fe81d2f264 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml @@ -16,3 +16,27 @@ model: kernel_size: [15, 15] num_joints: 17 backbone_output_channels: 720 + heads: + bodypart: + type: HeatmapHead + predictor: + type: HeatmapPredictor + location_refinement: false + target_generator: + type: HeatmapGaussianGenerator + num_heatmaps: "num_bodyparts" + pos_dist_thresh: 17 + heatmap_mode: KEYPOINT + generate_locref: false + criterion: + heatmap: + type: WeightedBCECriterion + weight: 1.0 + heatmap_config: + channels: + - "backbone_output_channels" + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts" + kernel_size: 1 \ No newline at end of file diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 0aaefdd47a..6c294b1804 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -375,9 +375,8 @@ def __call__( class ComputeBoundingBoxesFromCondKeypoints(Preprocessor): """TODO""" - def __init__(self, cropped_image_size: int, bbox_format: str = "xywh") -> None: - self.cropped_image_size = cropped_image_size - self.bbox_format = bbox_format + def __init__(self, cond_kpt_key: str = "cond_kpts") -> None: + self.cond_kpt_key = cond_kpt_key def __call__( self, image: np.ndarray, context: Context @@ -387,6 +386,6 @@ def __call__( raise ValueError(f"Must include cond kpts to ComputeBBoxes, found {context}") context["bboxes"] = [bbox_from_keypoints(cond_kpts, image.shape[0], image.shape[1], 10) - for cond_kpts in context["cond_kpts"]] + for cond_kpts in context[self.cond_kpt_key]] return image, context From 107ea708d6a57bded0d3a12cefeee998a65a43b9 Mon Sep 17 00:00:00 2001 From: LucZot Date: Tue, 26 Mar 2024 18:20:10 +0100 Subject: [PATCH 13/95] buctd training! --- benchmark/benchmark_madlc.py | 113 ++++++++++-------- benchmark/benchmark_run_experiments.py | 4 +- benchmark/projects.py | 7 +- ...{hrnet_coam_w32.yaml => ctd_coam_w32.yaml} | 23 ++-- ...{hrnet_coam_w48.yaml => ctd_coam_w48.yaml} | 23 ++-- .../pose_estimation_pytorch/data/dataset.py | 30 ++--- .../data/generative_sampling.py | 13 +- .../models/backbones/hrnet_coam.py | 32 ++--- .../pose_estimation_pytorch/models/model.py | 4 +- .../models/modules/coam_module.py | 10 +- .../models/modules/kpt_encoders.py | 38 +++--- .../pose_estimation_pytorch/runners/train.py | 4 +- 12 files changed, 174 insertions(+), 127 deletions(-) rename deeplabcut/pose_estimation_pytorch/config/ctd/{hrnet_coam_w32.yaml => ctd_coam_w32.yaml} (68%) rename deeplabcut/pose_estimation_pytorch/config/ctd/{hrnet_coam_w48.yaml => ctd_coam_w48.yaml} (68%) diff --git a/benchmark/benchmark_madlc.py b/benchmark/benchmark_madlc.py index 88ee3b0680..124777f312 100644 --- a/benchmark/benchmark_madlc.py +++ b/benchmark/benchmark_madlc.py @@ -21,7 +21,7 @@ if __name__ == "__main__": - PROJECT_NAME = "trimouse" # "trimouse", "fish", "marmosets", "parenting" + PROJECT_NAME = "fish" # "trimouse", "fish", "marmosets", "parenting" PROJECT_BENCHMARKED = MA_DLC_BENCHMARKS[PROJECT_NAME] SPLIT_FILE = MA_DLC_DATA_ROOT / "maDLC_benchmarking_splits.json" NUM_BPT = len(get_bodyparts(PROJECT_BENCHMARKED.cfg)) @@ -32,76 +32,95 @@ DETECTOR_BATCH_SIZE = 1 DEFAULT_OPTIMIZER = {"type": "AdamW", "params": {"lr": 5e-4}} - DEFAULT_SCHEDULER["params"] = {"lr_list": [[1e-4], [1e-5]], "milestones": [2, 4]} + DEFAULT_SCHEDULER["params"] = {"lr_list": [[1e-4], [1e-5]], "milestones": [170, 200]} - EPOCHS = 5 - SAVE_EPOCHS = 5 + EPOCHS = 210 + SAVE_EPOCHS = 30 DEKR_BATCH_SIZE = 8 TD_HRNET_BATCH_SIZE = 8 + CTD_HRNET_BATCH_SIZE = 32 # logging params - WANDB_PROJECT = "dlc3-benchmark-dev" - BASE_TAGS = (f"project={PROJECT_NAME}", "server=m0") + WANDB_PROJECT = "dlc3-buctd-test" + BASE_TAGS = (f"project={PROJECT_NAME}", "server=4") GROUP_UID = "base" model_configs = [ + # ModelConfig( + # net_type="dekr_w32", + # batch_size=DEKR_BATCH_SIZE, + # epochs=EPOCHS, + # save_epochs=SAVE_EPOCHS, + # train_aug=AUG_TRAIN, + # inference_aug=AUG_INFERENCE, + # optimizer_config=DEFAULT_OPTIMIZER, + # scheduler_config=DEFAULT_SCHEDULER, + # wandb_config=WandBConfig( + # project=WANDB_PROJECT, + # run_name=f"{PROJECT_NAME}-{GROUP_UID}-dekr32", + # group=f"{PROJECT_NAME}-{GROUP_UID}-dekr32", + # tags=(*BASE_TAGS, "arch=dekr32", "ndeconv=1"), + # ), + # ), + # ( + # DetectorConfig( + # batch_size=DETECTOR_BATCH_SIZE, + # epochs=DETECTOR_EPOCHS, + # save_epochs=DETECTOR_SAVE_EPOCHS, + # train_aug=None, + # inference_aug=None, + # optimizer_config=None, + # scheduler_config=None, + # ), + # ModelConfig( + # net_type="top_down_hrnet_w32", + # batch_size=TD_HRNET_BATCH_SIZE, + # epochs=EPOCHS, + # save_epochs=SAVE_EPOCHS, + # train_aug=AUG_TRAIN, + # inference_aug=AUG_INFERENCE, + # backbone_config=None, + # head_config=HeadConfig.build_plateau_head( + # c_in=32, + # c_out=NUM_BPT, + # deconv=[], + # final_conv=True, + # ), + # optimizer_config=DEFAULT_OPTIMIZER, + # scheduler_config=DEFAULT_SCHEDULER, + # wandb_config=WandBConfig( + # project=WANDB_PROJECT, + # run_name=f"{PROJECT_NAME}-{GROUP_UID}-td-hrnet32", + # group=f"{PROJECT_NAME}-{GROUP_UID}-td-hrnet32", + # tags=(*BASE_TAGS, "arch=td-hrnet32", "ndeconv=0"), + # ), + # ), + # ), ModelConfig( - net_type="dekr_w32", - batch_size=DEKR_BATCH_SIZE, + net_type="ctd_coam_w32", + batch_size=CTD_HRNET_BATCH_SIZE, epochs=EPOCHS, save_epochs=SAVE_EPOCHS, train_aug=AUG_TRAIN, inference_aug=AUG_INFERENCE, + backbone_config=None, + head_config=None, optimizer_config=DEFAULT_OPTIMIZER, scheduler_config=DEFAULT_SCHEDULER, wandb_config=WandBConfig( project=WANDB_PROJECT, - run_name=f"{PROJECT_NAME}-{GROUP_UID}-dekr32", - group=f"{PROJECT_NAME}-{GROUP_UID}-dekr32", - tags=(*BASE_TAGS, "arch=dekr32", "ndeconv=1"), + run_name=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet32", + group=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet32", + tags=(*BASE_TAGS, "arch=ctd-hrnet32"), ), ), - ( - DetectorConfig( - batch_size=DETECTOR_BATCH_SIZE, - epochs=DETECTOR_EPOCHS, - save_epochs=DETECTOR_SAVE_EPOCHS, - train_aug=None, - inference_aug=None, - optimizer_config=None, - scheduler_config=None, - ), - ModelConfig( - net_type="top_down_hrnet_w32", - batch_size=TD_HRNET_BATCH_SIZE, - epochs=EPOCHS, - save_epochs=SAVE_EPOCHS, - train_aug=AUG_TRAIN, - inference_aug=AUG_INFERENCE, - backbone_config=None, - head_config=HeadConfig.build_plateau_head( - c_in=32, - c_out=NUM_BPT, - deconv=[], - final_conv=True, - ), - optimizer_config=DEFAULT_OPTIMIZER, - scheduler_config=DEFAULT_SCHEDULER, - wandb_config=WandBConfig( - project=WANDB_PROJECT, - run_name=f"{PROJECT_NAME}-{GROUP_UID}-td-hrnet32", - group=f"{PROJECT_NAME}-{GROUP_UID}-td-hrnet32", - tags=(*BASE_TAGS, "arch=td-hrnet32", "ndeconv=0"), - ), - ), - ) ] main( project=PROJECT_BENCHMARKED, splits_file=SPLIT_FILE, trainset_index=0, - train_fraction=0.95, - models_to_train=[model_configs[1]], + train_fraction=0.94, + models_to_train=[model_configs[0]], splits_to_train=(0, ), ) diff --git a/benchmark/benchmark_run_experiments.py b/benchmark/benchmark_run_experiments.py index 484c3f5dfb..4ca975d6f4 100644 --- a/benchmark/benchmark_run_experiments.py +++ b/benchmark/benchmark_run_experiments.py @@ -352,8 +352,8 @@ def main( model_prefix="", ), train=True, - evaluate=True, - device="cuda:0", + evaluate=False, + device="cuda", train_params=model_config, detector_train_params=detector_config, eval_params=eval_params, diff --git a/benchmark/projects.py b/benchmark/projects.py index 143de3db2c..25566af9d7 100644 --- a/benchmark/projects.py +++ b/benchmark/projects.py @@ -6,8 +6,9 @@ from utils import Project -MA_DLC_DATA_ROOT = Path("/home/niels/datasets/ma_dlc") -SA_DLC_DATA_ROOT = Path("/home/niels/datasets/single_animal_dlc") +#MA_DLC_DATA_ROOT = Path("/home/niels/datasets/ma_dlc") +MA_DLC_DATA_ROOT = Path("/home/lucas/datasets") +SA_DLC_DATA_ROOT = Path("/home/lucas/datasets/single_animal_dlc") MA_DLC_BENCHMARKS = { "trimouse": Project( @@ -18,7 +19,7 @@ "fish": Project( root=MA_DLC_DATA_ROOT, name="fish-dlc-2021-05-07", - iteration=1, + iteration=29, ), "parenting": Project( root=MA_DLC_DATA_ROOT, diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml similarity index 68% rename from deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml rename to deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 13544b6583..b5f6b48289 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -1,21 +1,26 @@ data: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 + inference: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 + train: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +method: ctd model: backbone: type: HRNetCoAM base_model_name: hrnet_w32 pretrained: true - coam_modules: (2,) + coam_modules: [2,] channel_att_only: false att_heads: 1 - cond_enc: + kpt_encoder: type: ColoredKeypointEncoder - num_joints: 17 + num_joints: "num_bodyparts" kernel_size: [15, 15] - num_joints: 17 - backbone_output_channels: 480 + backbone_output_channels: 32 heads: bodypart: type: HeatmapHead @@ -34,7 +39,7 @@ model: weight: 1.0 heatmap_config: channels: - - "backbone_output_channels" + - 32 kernel_size: [] strides: [] final_conv: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml similarity index 68% rename from deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml rename to deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index fe81d2f264..4c9739586c 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/hrnet_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -1,21 +1,26 @@ data: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 + inference: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 + train: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +method: ctd model: backbone: type: HRNetCoAM base_model_name: hrnet_w48 pretrained: true - coam_modules: (2,) + coam_modules: [2,] channel_att_only: false att_heads: 1 - cond_enc: + kpt_encoder: type: ColoredKeypointEncoder - num_joints: 17 + num_joints: "num_bodyparts" kernel_size: [15, 15] - num_joints: 17 - backbone_output_channels: 720 + backbone_output_channels: 48 heads: bodypart: type: HeatmapHead @@ -34,7 +39,7 @@ model: weight: 1.0 heatmap_config: channels: - - "backbone_output_channels" + - 48 kernel_size: [] strides: [] final_conv: diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 0ff83c9640..c4545de268 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -82,7 +82,7 @@ def __post_init__(self): } if self.task == Task.CTD: - self.generative_sampler = GenerativeSampler(self.parameters.num_joints()) + self.generative_sampler = GenerativeSampler(self.parameters.num_joints) def __len__(self): # TODO: TD/CTD should only return the number of annotations that aren't unique_bodyparts @@ -166,7 +166,7 @@ def __getitem__(self, index: int) -> dict: raise ValueError( "You must specify a cropped image size for top-down models" ) - if len(bboxes) > 1: + if len(bboxes) > 1 and self.task == Task.TOP_DOWN: raise ValueError( "There can only be one bbox per item in TD datasets, found " f"{bboxes} for {index} (image {image_path})" @@ -174,12 +174,13 @@ def __getitem__(self, index: int) -> dict: bboxes = bboxes.astype(int) if self.task == Task.CTD: - keypoints = keypoints[0] near_keypoints = keypoints[1:] + keypoints = keypoints[:1] synthesized_keypoints = self.generative_sampler( keypoints=keypoints.reshape(-1, 3), - near_keypoints=near_keypoints.reshape(-1, 3), + near_keypoints=near_keypoints.reshape(len(near_keypoints),-1, 3), area=bboxes[0,2]*bboxes[0,3], + image_size=original_size, ) bboxes[0] = bbox_from_keypoints(synthesized_keypoints[:, :2], original_size[0], original_size[1], 10) @@ -192,7 +193,7 @@ def __getitem__(self, index: int) -> dict: if self.task == Task.CTD: synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] - keypoints = safe_stack([keypoints, synthesized_keypoints[None,...]], (0, self.parameters.num_joints(), 3)) + keypoints = safe_stack([keypoints, synthesized_keypoints[None,...]], (0, self.parameters.num_joints, 3)) bboxes = np.zeros((0, 4)) # No more bboxes as we cropped around them transformed = self.apply_transform_all_keypoints( @@ -231,6 +232,9 @@ def _prepare_final_data_dict( offsets: tuple[int, int], scales: tuple[float, float], ) -> dict[str, np.ndarray | dict[str, np.ndarray]]: + context = dict() + if self.task == Task.CTD: + context["cond_keypoints"] = keypoints[1,:,:,:2].astype(np.single) return { "image": image.transpose((2, 0, 1)), "image_id": image_id, @@ -241,9 +245,7 @@ def _prepare_final_data_dict( "annotations": self._prepare_final_annotation_dict( keypoints, keypoints_unique, bboxes, annotations_merged ), - "context": { - "cond_keypoints": keypoints[1,:,:2].astype(np.single) if self.task == Task.CTD else False, - } + "context": context } def _prepare_final_annotation_dict( @@ -254,22 +256,22 @@ def _prepare_final_annotation_dict( anns: dict, ) -> dict[str, np.ndarray]: num_animals = self.parameters.max_num_animals + individual_ids = anns["individual_id"] if self.task in (Task.TOP_DOWN, Task.CTD): num_animals = 1 if self.task == Task.CTD: keypoints = keypoints[0] + individual_ids = anns["individual_id"][:1] return { "keypoints": pad_to_length(keypoints[..., :2], num_animals, -1).astype(np.single), "keypoints_unique": keypoints_unique[..., :2].astype(np.single), "with_center_keypoints": self.parameters.with_center_keypoints, - "area": pad_to_length(anns["area"], num_animals, 0).astype(np.single), + "area": pad_to_length(bboxes[...,2]*bboxes[...,3], num_animals, 0).astype(np.single), "boxes": pad_to_length(bboxes, num_animals, 0).astype(np.single), - "is_crowd": pad_to_length(anns["iscrowd"], num_animals, 0).astype(int), - "labels": pad_to_length(anns["category_id"], num_animals, -1).astype(int), - "individual_ids": pad_to_length( - anns["individual_id"], num_animals, -1 - ).astype(int), + #"is_crowd": pad_to_length(anns["iscrowd"], num_animals, 0).astype(int), + #"labels": pad_to_length(anns["category_id"], num_animals, -1).astype(int), + "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype(int), } def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 1e47bb62d2..0a2a30cd8b 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -54,7 +54,7 @@ def __init__( self, num_keypoints: int, keypoint_sigmas: float | list[float] = 0.1, - keypoints_symmetry: list[tuple[int, int]] | None = None + keypoints_symmetry: list[tuple[int, int]] = [] ): """ Args: @@ -78,6 +78,7 @@ def __call__( near_keypoints: np.ndarray, area: float, # ?? #num_overlap: int, # ?? + image_size: tuple[int, int], ) -> np.ndarray: """Samples keypoints @@ -112,7 +113,7 @@ def __call__( # if joints[j, 2] == 0: # synth_joints[j] = estimated_joints[j] - num_valid_joint = np.sum(keypoints[:, 2] > 0) + #num_valid_joint = np.sum(keypoints[:, 2] > 0) N = 500 # TODO: do not know how this is set for j in range(self.num_keypoints): @@ -132,6 +133,9 @@ def __call__( # keypoint with any other keypoint by randomly selecting keypoints to swap kps_symmetry = self.keypoints_symmetry pair_exist = False + # for now, we randomly sample keypoint pairs to swap + kps_symmetry = np.random.choice(list(range(self.num_keypoints)), + size=(self.num_keypoints//2, 2), replace=False) for (q, w) in kps_symmetry: if j == q or j == w: if j == q: @@ -345,7 +349,10 @@ def __call__( synth_list = [synth_jitter, synth_miss, synth_inv, synth_swap, synth_good] sampled_idx = np.random.choice(5, 1, p=prob_list)[0] synth_joints[j] = synth_list[sampled_idx] - synth_joints[j, 2] = 0 + synth_joints[j, 2] = 2 + + np.clip(synth_joints[:, 0], 0, image_size[1], out=synth_joints[:, 0]) + np.clip(synth_joints[:, 1], 0, image_size[0], out=synth_joints[:, 1]) return synth_joints diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 7a18ab9cb7..203689a7b1 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -47,7 +47,6 @@ def __init__( channel_att_only: bool = False, att_heads: int = 1, img_size: tuple[int,int] = (256, 256), - num_joints: int = 17, **kwargs, ) -> None: """Constructs an ImageNet pretrained HRNet from timm and creates CoAM blocks. @@ -67,7 +66,6 @@ def __init__( super().__init__( model_name = base_model_name, pretrained = pretrained, - only_high_res = True, **kwargs) self.coam_modules = coam_modules @@ -77,8 +75,8 @@ def __init__( kpt_encoder = KEYPOINT_ENCODERS.build(kpt_encoder) self.cond_enc = kpt_encoder - self.coam_stages = [None, None, None, None] - self.selfatt_coam_stages = [None, None, None, None] + self.coam_stages = nn.ModuleList([None, None, None, None]) + self.selfatt_coam_stages = nn.ModuleList([None, None, None, None]) spat_dims = [(int(img_size[0]/4),int(img_size[1]/4)), (int(img_size[0]/8),int(img_size[1]/8)), @@ -122,7 +120,7 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: if self.coam_stages[0]: xl = self.coam_stages[0](xl, cond_hm) - elif self.selfatt_coam_modules[0]: + elif self.selfatt_coam_stages[0]: xl = self.selfatt_coam_stages[0](xl) yl = self.model.stage2(xl) @@ -131,7 +129,7 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: if self.coam_stages[1]: xl = self.coam_stages[1](xl, cond_hm) - elif self.selfatt_coam_modules[1]: + elif self.selfatt_coam_stages[1]: xl = self.selfatt_coam_stages[1](xl) yl = self.model.stage3(xl) @@ -140,38 +138,34 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: if self.coam_stages[2]: xl = self.coam_stages[2](xl, cond_hm) - elif self.selfatt_coam_modules[2]: + elif self.selfatt_coam_stages[2]: xl = self.selfatt_coam_stages[2](xl) yl = self.model.stage4(xl) if self.coam_stages[3]: yl = self.coam_stages[3](yl, cond_hm) - elif self.selfatt_coam_modules[3]: + elif self.selfatt_coam_stages[3]: yl = self.selfatt_coam_stages[3](yl) return yl - def forward(self, x, cond_hm): + def forward(self, x, cond_kpts): """Forward pass through the HRNetCoAM backbone. Args: - x: Input tensor of shape (batch_size, channels, height, width, condition_channels). + x: Input tensor of shape (batch_size, channels, height, width). + cond_kpts: Conditional keypoints of shape (batch_size, num_joints, 2). Returns: the feature map - - Example: - >>> import torch - >>> from deeplabcut.pose_estimation_pytorch.models.backbones import HRNetCoAM - >>> backbone = HRNetCoAM(model_name='hrnet_w32', pretrained=False) - >>> x = torch.randn(1, 6, 256, 256) - >>> y = backbone(x) """ - cond_hm = self.cond_enc(cond_hm, x.size()[2:]).to(x.device) - cond_hm = cond_hm.permute(2, 0, 1) + # create conditional heatmap + cond_hm = self.cond_enc(cond_kpts.squeeze(1), x.size()[2:]) + cond_hm = torch.from_numpy(cond_hm).float().to(x.device) + cond_hm = cond_hm.permute(0, 3, 1, 2) # (B, C, H, W) # Stem x = self.model.conv1(x) diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py index 189a4ebf08..731f905342 100644 --- a/deeplabcut/pose_estimation_pytorch/models/model.py +++ b/deeplabcut/pose_estimation_pytorch/models/model.py @@ -56,7 +56,7 @@ def __init__( self.heads = nn.ModuleDict(heads) self.neck = neck - def forward(self, x: torch.Tensor) -> dict[str, dict[str, torch.Tensor]]: + def forward(self, x: torch.Tensor, **backbone_kwargs) -> dict[str, dict[str, torch.Tensor]]: """ Forward pass of the PoseModel. @@ -68,7 +68,7 @@ def forward(self, x: torch.Tensor) -> dict[str, dict[str, torch.Tensor]]: """ if x.dim() == 3: x = x[None, :] - features = self.backbone(x) + features = self.backbone(x, **backbone_kwargs) if self.neck: features = self.neck(features) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index aa7cd7dec2..b03066c1fa 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -15,6 +15,10 @@ from torch.nn import init import torchvision.transforms.functional as TF +# from deeplabcut.pose_estimation_pytorch.models.modules import ( +# ColoredKeypointEncoder, +# StackedKeypointEncoder, +# ) class CoAMBlock(nn.Module): """ @@ -25,7 +29,7 @@ def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=Fa self.att_layers = [] self.spat_dims = spat_dims self.cond_enc = cond_enc - d_cond = cond_enc.num_channels() + d_cond = cond_enc.num_channels for i in range(len(spat_dims)): att_layer = DAModule(d_model = channel_list[i], d_cond = d_cond, kernel_size = 3, @@ -35,8 +39,8 @@ def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=Fa self.att_layers = nn.ModuleList(self.att_layers) def forward(self, y_list, cond_hm): - if not self.cond_enc == 'stacked' and not self.cond_enc == 'colored': - cond_hm = cond_hm[:,0].unsqueeze(1) # we only want one channel of the heatmap + # if not isinstance(self.cond_enc, (StackedKeypointEncoder, ColoredKeypointEncoder)): + # cond_hm = cond_hm[:,0].unsqueeze(1) # we only want one channel of the heatmap y_list_att = [] for i in range(len(y_list)): y_att = self.att_layers[i](y_list[i], TF.resize(cond_hm, (self.spat_dims[i][1],self.spat_dims[i][0]))) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 4f256d9c73..4489473a7b 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -96,6 +96,9 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: Returns: the encoded keypoints """ + + raise NotImplementedError("StackedKeypointEncoder not implemented yet with batch processing") + kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) zero_matrix = np.zeros(size) @@ -145,34 +148,41 @@ def num_channels(self): def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: """ Args: - keypoints: the keypoints to encode - size: the (height, width) of the heatmap in which the keypoints should - be encoded + keypoints: batch of keypoints to encode with shape (batch_size, num_joints, 2) + size: the (height, width) of the heatmap in which the keypoints should be encoded Returns: - the encoded keypoints + encoded keypoints with shape (batch_size, num_joints, height, width, 3) """ - if not len(keypoints) == len(self.colors): + + batch_size, num_kpts, _ = keypoints.shape + + if not num_kpts == len(self.colors): raise ValueError( f"Cannot encode the keypoints. Initialized with {len(self.colors)} " - f"colors, but there are {len(keypoints)} to encode" + f"colors, but there are {num_kpts} to encode" ) - kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) - zero_matrix = np.zeros(size) + kpts = np.array(keypoints).astype(int) + zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) def _get_condition_matrix(zero_matrix, kpts): - for color, kpt in zip(self.colors, kpts): - if 0 < kpt[0] < size[1] and 0 < kpt[1] < size[0]: - zero_matrix[kpt[1] - 1][kpt[0] - 1] = color + for i, pose in enumerate(kpts): + for color, kpt in zip(self.colors, pose): + x, y = kpt + if 0 < x < size[1] and 0 < y < size[0]: + zero_matrix[i, y-1, x-1] = color return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) - condition_heatmap = self.blur_heatmap(condition) - return condition_heatmap + for i in range(batch_size): + condition_heatmap = self.blur_heatmap(condition[i]) + condition[i] = condition_heatmap + return condition def get_colors_from_cmap(self, cmap_name, num_colors): cmap = plt.get_cmap(cmap_name) - colors_float = [cmap(i) for i in range(0, 256, 256 // num_colors)] + #colors_float = [cmap(i) for i in range(0, 256, 256 // num_colors)] + colors_float = [cmap(i) for i in np.linspace(0, 256, num_colors, dtype=int)] colors = [(int(r*255), int(g*255), int(b*255)) for r, g, b, _ in colors_float] return colors diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index 42a6159c2e..a61ee7cc9f 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -275,10 +275,10 @@ def step( inputs = batch["image"] inputs = inputs.to(self.device) - if batch['context']['cond_keypoints'][0]: + if 'cond_keypoints' in batch['context']: cond_kpts = batch['context']['cond_keypoints'] #cond_kpts = cond_kpts.to(self.device) # cond kpts are put on device after heatmap creation - outputs = self.model(inputs, cond_kpts) + outputs = self.model(inputs, cond_kpts=cond_kpts) else: outputs = self.model(inputs) From f8b7fa9d969f906e424b0eb1586c7d2913ce668d Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 27 Mar 2024 12:12:41 +0100 Subject: [PATCH 14/95] remove one for loop in _get_condition_matrix --- benchmark/projects.py | 2 +- .../models/modules/kpt_encoders.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/benchmark/projects.py b/benchmark/projects.py index 25566af9d7..7f2dd7cf1c 100644 --- a/benchmark/projects.py +++ b/benchmark/projects.py @@ -19,7 +19,7 @@ "fish": Project( root=MA_DLC_DATA_ROOT, name="fish-dlc-2021-05-07", - iteration=29, + iteration=30, ), "parenting": Project( root=MA_DLC_DATA_ROOT, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 4489473a7b..4ab37c29cf 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -139,7 +139,7 @@ def __init__(self, **kwargs) -> None: colors: the color to use for each keypoint """ super().__init__(**kwargs) - self.colors = self.get_colors_from_cmap('rainbow', self.num_joints) + self.colors = np.array(self.get_colors_from_cmap('rainbow', self.num_joints)) @property def num_channels(self): @@ -165,14 +165,15 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: kpts = np.array(keypoints).astype(int) zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) - + def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): - for color, kpt in zip(self.colors, pose): - x, y = kpt - if 0 < x < size[1] and 0 < y < size[0]: - zero_matrix[i, y-1, x-1] = color + x, y = pose.T + mask = (0 < x) & (x < size[1]) & (0 < y) & (y < size[0]) + x_masked, y_masked, colors_masked = x[mask], y[mask], self.colors[mask] + zero_matrix[i, y_masked-1, x_masked-1] = colors_masked return zero_matrix + condition = _get_condition_matrix(zero_matrix, kpts) for i in range(batch_size): From 9bb4ae639083274e1d386a09d8a43bcf60d7e14b Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 28 Mar 2024 14:22:13 +0100 Subject: [PATCH 15/95] optim trials for cond kpt encoding --- .../pose_estimation_pytorch/data/dataset.py | 3 +++ .../models/modules/kpt_encoders.py | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index c4545de268..dd1d1af780 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -182,6 +182,9 @@ def __getitem__(self, index: int) -> dict: area=bboxes[0,2]*bboxes[0,3], image_size=original_size, ) + # for testing the speed of the generative sampling + #synthesized_keypoints = keypoints.reshape(-1,3) + np.random.normal(0, 1, keypoints.reshape(-1,3).shape) + bboxes[0] = bbox_from_keypoints(synthesized_keypoints[:, :2], original_size[0], original_size[1], 10) # TODO: The following code should be replaced by a numpy version diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 4ab37c29cf..9ade2e3fc8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -14,6 +14,8 @@ import cv2 import numpy as np +import torch +import torchvision.transforms.functional as TF import matplotlib.pyplot as plt from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg @@ -70,6 +72,14 @@ def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: return heatmap heatmap /= (am / 255) return heatmap + + # def blur_heatmap_batch(self, heatmaps: torch.tensor) -> np.ndarray: + # heatmaps = TF.gaussian_blur(heatmaps.permute(0,3,1,2), self.kernel_size).permute(0,2,3,1).numpy() + # am = np.amax(heatmaps) + # if am == 0: + # return heatmaps + # heatmaps /= (am / 255) + # return heatmaps @KEYPOINT_ENCODERS.register_module @@ -174,11 +184,24 @@ def _get_condition_matrix(zero_matrix, kpts): zero_matrix[i, y_masked-1, x_masked-1] = colors_masked return zero_matrix + def _get_condition_matrix_optim(zero_matrix, kpts): + x, y = np.array(kpts).T + mask = (0 < x) & (x < zero_matrix.shape[2]) & (0 < y) & (y < zero_matrix.shape[1]) + colors_masked = np.repeat(self.colors[:, None, :], len(zero_matrix), 1) * np.repeat(mask[:, :, None], 3, 2) + kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) + batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) + kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) + zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) + return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) + #condition = _get_condition_matrix_optim(zero_matrix, kpts) + for i in range(batch_size): condition_heatmap = self.blur_heatmap(condition[i]) condition[i] = condition_heatmap + #condition = self.blur_heatmap_batch(torch.from_numpy(condition)) + return condition def get_colors_from_cmap(self, cmap_name, num_colors): From ac29688cd2a79cb9739a6e5114d1828ca30d2285 Mon Sep 17 00:00:00 2001 From: LucZot Date: Sun, 7 Apr 2024 18:21:46 +0200 Subject: [PATCH 16/95] add ctd evaluation based on loaded bu predictions --- benchmark/benchmark_run_experiments.py | 2 +- .../pose_estimation_pytorch/apis/evaluate.py | 10 ++--- .../config/ctd/ctd_coam_w32.yaml | 3 ++ .../config/ctd/ctd_coam_w48.yaml | 3 ++ .../pose_estimation_pytorch/data/dlcloader.py | 37 +++++++++++++++++++ .../data/generative_sampling.py | 14 ++++++- .../data/preprocessor.py | 26 +++++++++++-- .../models/modules/kpt_encoders.py | 6 +-- .../runners/inference.py | 14 +++++-- 9 files changed, 99 insertions(+), 16 deletions(-) diff --git a/benchmark/benchmark_run_experiments.py b/benchmark/benchmark_run_experiments.py index 4ca975d6f4..3d90f0ea3c 100644 --- a/benchmark/benchmark_run_experiments.py +++ b/benchmark/benchmark_run_experiments.py @@ -352,7 +352,7 @@ def main( model_prefix="", ), train=True, - evaluate=False, + evaluate=True, device="cuda", train_params=model_config, detector_train_params=detector_config, diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index fd333fde4e..37a4adc20d 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -80,11 +80,11 @@ def predict( elif pose_task == Task.CTD: # Get conditional keypoints for context - if pose_runner is not None: - pose_predictions = pose_runner.inference(images=tqdm(image_paths)) - context = [{"cond_kpts": pose_pred} for pose_pred in pose_predictions] - else: - raise NotImplementedError("Conditional top-down models require a pose runner") + bu_snapshot = loader.model_cfg["data"]["inference"]["bu_snapshot"] + bu_preds = loader.model_cfg["data"]["inference"]["bu_predictions"] + pose_predictions = loader.load_predictions(Path(bu_snapshot), Path(bu_preds), + loader.get_dataset_parameters()) + context = [{"cond_kpts": pose_predictions[image]} for image in image_paths] images_with_context = image_paths if context is not None: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index b5f6b48289..4bcf2ededd 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -3,6 +3,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth + bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index 4c9739586c..bd32f5b535 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -3,6 +3,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth + bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 1a892c412f..888f215c92 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -16,6 +16,7 @@ import numpy as np import pandas as pd +import re import deeplabcut.utils.auxiliaryfunctions as af from deeplabcut.core.engine import Engine @@ -194,6 +195,42 @@ def load_ground_truth(config: dict) -> pd.DataFrame: f"Found {df}" ) return df + + @staticmethod + def load_predictions(bu_snapshot: Path, bu_predictions: Path, parameters: PoseDatasetParameters) -> pd.DataFrame: + + if bu_predictions is None: + + pred_path = Path(str(bu_snapshot).replace('dlc-models', 'evaluation-results')).parent.parent + cfg = af.read_config(pred_path.parent.parent.parent / "config.yaml") + scorer = af.get_scorer_name( + cfg=cfg, + shuffle=int(re.search(r'shuffle(\d+)', str(bu_snapshot)).group(1)), + trainFraction=int(re.search(r'trainset(\d+)', str(bu_snapshot)).group(1)) / 100, + engine=Engine.PYTORCH, + trainingsiterations=re.search(r'snapshot-(.+)\.pth', str(bu_snapshot)).group(1), + modelprefix="", + ) + pred_file = pred_path / f"{scorer[0]}.h5" + + dlc_preds = pd.read_hdf(pred_file, key="df_with_missing") + + #FIXME: Implement the case where snapshot is loaded + raise NotImplementedError("Need to implement the case with loaded snapshot") + + else: + pred_path = bu_predictions.parent.parent + dlc_preds = pd.read_hdf(bu_predictions, key="df_with_missing") + + predictions = {} + for idx in dlc_preds.index.unique(): + img_path = pred_path.parent.parent / Path(*idx) + keypoints = dlc_preds.loc[idx].values.reshape(-1,len(parameters.bodyparts),3)[:,:,:2] + keypoints = keypoints[~np.isnan(keypoints).all(axis=-1).all(axis=-1)] + predictions[str(img_path)] = keypoints + + return predictions + @staticmethod def split_data( diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 0a2a30cd8b..aa294720b1 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -110,9 +110,16 @@ def __call__( # predictions? which model?) so we ignore it for now # for j in range(self.num_keypoints): # # in case of not annotated joints, use other models`s result and add noise - # if joints[j, 2] == 0: + # if keypoints[j, 2] == 0: # synth_joints[j] = estimated_joints[j] + # instead we fill empty annotations with the mean of the other annotations + # in order to prevent corrupted bboxes + # for j in range(self.num_keypoints): + # #if keypoints[j, 2] == 0: + # if sum(keypoints[j,:2]) == 0: + # synth_joints[j] = np.mean(keypoints[keypoints[:, 2] > 0], axis=0) + #num_valid_joint = np.sum(keypoints[:, 2] > 0) N = 500 # TODO: do not know how this is set @@ -334,6 +341,8 @@ def __call__( if synth_good[2] == 0: good_prob = 0 + #swap_prob = 0 + normalizer = jitter_prob + miss_prob + inv_prob + swap_prob + good_prob if normalizer == 0: synth_joints[j] = 0 @@ -354,6 +363,9 @@ def __call__( np.clip(synth_joints[:, 0], 0, image_size[1], out=synth_joints[:, 0]) np.clip(synth_joints[:, 1], 0, image_size[0], out=synth_joints[:, 1]) + # bring format of empty annotations back to DLC format (0 -> nan) + synth_joints[(synth_joints[...,0]==0) & (synth_joints[...,1]==0)] = np.nan + return synth_joints def get_distance_wrt_keypoint_sim(self, ks: float, area: float) -> np.ndarray: diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 6c294b1804..342b697a5f 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -137,6 +137,7 @@ def build_conditional_top_down_preprocessor( ComputeBoundingBoxesFromCondKeypoints(), TorchCropDetections(cropped_image_size=cropped_image_size[0]), AugmentImage(transform), + ConditionalKeypointsToModelInputs(), ToTensor(), ] ) @@ -359,9 +360,9 @@ def __call__( context["offsets"] = np.array(offsets) context["scales"] = np.array(scales) - if "cond_kpts" in context: - context["cond_kpts"][:, :, 0] = (context["cond_kpts"][:, :, 0] - offsets[0]) / scales[0] - context["cond_kpts"][:, :, 1] = (context["cond_kpts"][:, :, 1] - offsets[1]) / scales[1] + # if "cond_kpts" in context: + # context["cond_kpts"][:, :, 0] = (context["cond_kpts"][:, :, 0] - offsets[0]) / scales[0] + # context["cond_kpts"][:, :, 1] = (context["cond_kpts"][:, :, 1] - offsets[1]) / scales[1] # can have no bounding boxes if detector made no detections if len(images) == 0: @@ -389,3 +390,22 @@ def __call__( for cond_kpts in context[self.cond_kpt_key]] return image, context + + +class ConditionalKeypointsToModelInputs(Preprocessor): + + def __init__(self, cond_kpt_key: str = "cond_kpts") -> None: + self.cond_kpt_key = cond_kpt_key + + def __call__( + self, image: np.ndarray, context: Context + ) -> tuple[np.ndarray, Context]: + + context["model_kwargs"] = {"cond_kpts": context[self.cond_kpt_key]} + + cond_keypoints = context[self.cond_kpt_key] + rescaled = cond_keypoints.copy() + rescaled[..., :2] = (rescaled[..., :2] - np.array(context["offsets"])[:,None]) / np.array(context["scales"])[:,None] + context["model_kwargs"] = {"cond_kpts": np.expand_dims(rescaled, axis=1)} + + return image, context diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 9ade2e3fc8..9c8c0ead78 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -194,9 +194,9 @@ def _get_condition_matrix_optim(zero_matrix, kpts): zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) return zero_matrix - condition = _get_condition_matrix(zero_matrix, kpts) - #condition = _get_condition_matrix_optim(zero_matrix, kpts) - + #condition = _get_condition_matrix(zero_matrix, kpts) + condition = _get_condition_matrix_optim(zero_matrix, kpts) + for i in range(batch_size): condition_heatmap = self.blur_heatmap(condition[i]) condition[i] = condition_heatmap diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 4452aad3b6..fddd5bb68c 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -103,7 +103,7 @@ def inference( # TODO: input batch should also be able to be a dict[str, torch.Tensor] input_image, context = self.preprocessor(input_image, context) - image_predictions = self.predict(input_image) + image_predictions = self.predict(input_image, **context.get("model_kwargs", {})) if self.postprocessor is not None: # TODO: Should we return context? # TODO: typing update - the post-processor can remove a dict level @@ -120,7 +120,7 @@ class PoseInferenceRunner(InferenceRunner[PoseModel]): def __init__(self, model: PoseModel, **kwargs): super().__init__(model, **kwargs) - def predict(self, inputs: torch.Tensor) -> list[dict[str, dict[str, np.ndarray]]]: + def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: """Makes predictions from a model input and output Args: @@ -137,10 +137,18 @@ def predict(self, inputs: torch.Tensor) -> list[dict[str, dict[str, np.ndarray]] # TODO: iterates over batch one element at a time batch_size = 1 batch_predictions = [] + for i in range(0, len(inputs), batch_size): batch_inputs = inputs[i : i + batch_size] batch_inputs = batch_inputs.to(self.device) - batch_outputs = self.model(batch_inputs) + + if kwargs: + # Get the i-th element of "cond kpts" + kwargs_ = {} + kwargs_["cond_kpts"] = kwargs["cond_kpts"][i : i + batch_size] + batch_outputs = self.model(batch_inputs, **kwargs_) + else: + batch_outputs = self.model(batch_inputs, **kwargs) raw_predictions = self.model.get_predictions(batch_inputs, batch_outputs) for b in range(batch_size): From bb31cc17f7ebfa1828744de2f55010e4a1e68fad Mon Sep 17 00:00:00 2001 From: LucZot Date: Mon, 8 Apr 2024 15:43:27 +0200 Subject: [PATCH 17/95] use only overlapping individuals for swapping error --- benchmark/benchmark_madlc.py | 8 ++++-- benchmark/benchmark_run_experiments.py | 9 ++++-- .../pose_estimation_pytorch/data/dataset.py | 9 +++++- .../pose_estimation_pytorch/data/utils.py | 28 +++++++++++++++++++ .../models/modules/kpt_encoders.py | 3 +- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/benchmark/benchmark_madlc.py b/benchmark/benchmark_madlc.py index 124777f312..ec3e07bc48 100644 --- a/benchmark/benchmark_madlc.py +++ b/benchmark/benchmark_madlc.py @@ -21,7 +21,7 @@ if __name__ == "__main__": - PROJECT_NAME = "fish" # "trimouse", "fish", "marmosets", "parenting" + PROJECT_NAME = "trimouse" # "trimouse", "fish", "marmosets", "parenting" PROJECT_BENCHMARKED = MA_DLC_BENCHMARKS[PROJECT_NAME] SPLIT_FILE = MA_DLC_DATA_ROOT / "maDLC_benchmarking_splits.json" NUM_BPT = len(get_bodyparts(PROJECT_BENCHMARKED.cfg)) @@ -120,7 +120,11 @@ project=PROJECT_BENCHMARKED, splits_file=SPLIT_FILE, trainset_index=0, - train_fraction=0.94, + #train_fraction=0.94, # for fish + train_fraction=0.95, models_to_train=[model_configs[0]], splits_to_train=(0, ), + train=True, + evaluate=True, + manual_shuffle_index=None ) diff --git a/benchmark/benchmark_run_experiments.py b/benchmark/benchmark_run_experiments.py index 3d90f0ea3c..5d39130f28 100644 --- a/benchmark/benchmark_run_experiments.py +++ b/benchmark/benchmark_run_experiments.py @@ -286,6 +286,9 @@ def main( models_to_train: list[ModelConfig | tuple[DetectorConfig, ModelConfig]], splits_to_train: tuple[int, ...] = (0, 1, 2), eval_params: EvalParameters | None = None, + train: bool = True, + evaluate: bool = True, + manual_shuffle_index: int | None = None, ): if eval_params is None: eval_params = EvalParameters(snapshotindex="all", plotting=False) @@ -321,6 +324,8 @@ def main( shuffle_indices = create_shuffles( project, splits_file, trainset_index, model_config.net_type ) + if manual_shuffle_index: + shuffle_indices = [manual_shuffle_index] shuffles_to_train = [shuffle_indices[i] for i in splits_to_train] print(f"training shuffles {shuffles_to_train}") for split_idx, shuffle_idx in zip(splits_to_train, shuffles_to_train): @@ -351,8 +356,8 @@ def main( index=shuffle_idx, model_prefix="", ), - train=True, - evaluate=True, + train=train, + evaluate=evaluate, device="cuda", train_params=model_config, detector_train_params=detector_config, diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index dd1d1af780..86c50f61ec 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -22,6 +22,7 @@ _extract_keypoints_and_bboxes, apply_transform, bbox_from_keypoints, + calc_bbox_overlap, map_id_to_annotations, map_image_path_to_id, pad_to_length, @@ -121,7 +122,13 @@ def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: """ ann = self.annotations[index] img = self.images[self.img_id_to_index[ann["image_id"]]] - near_anns = [self.annotations[idx] for idx in self.annotation_idx_map[img["id"]] if idx != index] + near_anns = [] + for idx in self.annotation_idx_map[img["id"]]: + # we consider near annotations to be those whose bounding boxes overlap wih the current item + #if idx != index and calc_bbox_overlap(ann['bbox'], self.annotations[idx]['bbox']) > 0: + #HACK: add same annotation as near keypoints so that we don't have empty list + if calc_bbox_overlap(ann['bbox'], self.annotations[idx]['bbox']) > 0: + near_anns.append(self.annotations[idx]) return img["file_name"], [ann] + near_anns, img["id"] def __getitem__(self, index: int) -> dict: diff --git a/deeplabcut/pose_estimation_pytorch/data/utils.py b/deeplabcut/pose_estimation_pytorch/data/utils.py index 8d1339a8ed..e6bdec69c0 100644 --- a/deeplabcut/pose_estimation_pytorch/data/utils.py +++ b/deeplabcut/pose_estimation_pytorch/data/utils.py @@ -345,6 +345,34 @@ def calc_area_from_keypoints(keypoints: np.ndarray) -> np.ndarray: return area +def calc_bbox_overlap(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: + """ + Calculate the overlap between two bounding boxes + + Args: + bbox1: the first bounding box in the format (x, y, w, h) + bbox2: the second bounding box in the format (x, y, w, h) + + Returns: + The overlap between + """ + x1, y1, w1, h1 = bbox1 + x2, y2, w2, h2 = bbox2 + + x1_max = x1 + w1 + y1_max = y1 + h1 + x2_max = x2 + w2 + y2_max = y2 + h2 + + x_overlap = max(0, min(x1_max, x2_max) - max(x1, x2)) + y_overlap = max(0, min(y1_max, y2_max) - max(y1, y2)) + + intersection = x_overlap * y_overlap + union = w1 * h1 + w2 * h2 - intersection + + return intersection / union + + def _annotation_to_keypoints(annotation: dict) -> np.array: """ Convert the coco annotations into array of keypoints returns the array of the diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 9c8c0ead78..d989edcf20 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -191,7 +191,8 @@ def _get_condition_matrix_optim(zero_matrix, kpts): kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) - zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) + #zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) + zero_matrix[kpt_input[...,0], kpt_input[...,2]-1, kpt_input[...,1]-1] = colors_masked.transpose(1,0,2) return zero_matrix #condition = _get_condition_matrix(zero_matrix, kpts) From ee7918f32eb6e68635d31176aa2030c44210747e Mon Sep 17 00:00:00 2001 From: LucZot Date: Mon, 22 Apr 2024 16:08:40 +0200 Subject: [PATCH 18/95] add marmoset bu path --- .../pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml | 3 ++- .../pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 4bcf2ededd..7b6f90883b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -4,8 +4,9 @@ data: pad_width_divisor: 32 pad_height_divisor: 32 bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index bd32f5b535..d85ec284a8 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -6,6 +6,7 @@ data: bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 From e769277f8835f41e3d545790648edf06059e2a7a Mon Sep 17 00:00:00 2001 From: LucZot Date: Mon, 22 Apr 2024 17:08:33 +0200 Subject: [PATCH 19/95] scale cond_kpts in collate --- .../pose_estimation_pytorch/data/image.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index 3b12091f13..8510e33747 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -188,6 +188,17 @@ def get_resize_preserve_ratio( h = max_long_side return h, w + + def scale_kpts( + keypoints: np.ndarray, kpt_scale: np.ndarray, kpt_offset: np.ndarray, + tgt_h: int, tgt_w: int + ) -> np.ndarray: + scaled_kpts = keypoints.copy() + scaled_kpts[..., :2] = (scaled_kpts[..., :2] / kpt_scale) - kpt_offset + scaled_kpts[(scaled_kpts[..., 0] >= tgt_w)] = -1 + scaled_kpts[(scaled_kpts[..., 1] >= tgt_h)] = -1 + scaled_kpts[(scaled_kpts[..., :2] < 0).any(axis=-1)] = -1 + return scaled_kpts oh, ow = image.shape[1:] if isinstance(size, int): @@ -227,21 +238,20 @@ def get_resize_preserve_ratio( targets["offsets"] = ox + (offset_x * sx), oy + (offset_y * sy) targets["scales"] = sx * scale_x, sy * scale_y - # update annotations + # update annotations and context anns = targets.get("annotations", {}) + context = targets.get("context", {}) kpt_scale = np.array([scale_x, scale_y]) kpt_offset = np.array([offset_x, offset_y]) for kpt_key in ["keypoints", "keypoints_unique"]: keypoints = anns.get(kpt_key) if keypoints is not None and len(keypoints) > 0: - scaled_kpts = keypoints.copy() - scaled_kpts[..., :2] = (scaled_kpts[..., :2] / kpt_scale) - kpt_offset - scaled_kpts[(scaled_kpts[..., 0] >= tgt_w)] = -1 - scaled_kpts[(scaled_kpts[..., 1] >= tgt_h)] = -1 - scaled_kpts[(scaled_kpts[..., :2] < 0).any(axis=-1)] = -1 - anns[kpt_key] = scaled_kpts - + anns[kpt_key] = scale_kpts(keypoints, kpt_scale, kpt_offset, tgt_h, tgt_w) + cond_keypoints = context.get("cond_keypoints") + if cond_keypoints is not None and len(cond_keypoints) > 0: + context["cond_keypoints"] = scale_kpts(cond_keypoints, kpt_scale, kpt_offset, tgt_h, tgt_w) + bbox_scale = np.array([scale_x, scale_y, scale_x, scale_y]) bbox_offset = np.array([offset_x, offset_y, 0, 0]) for bbox_key in ["boxes"]: From 006b13a0361902cc96635ee2f9b9df76569e5e5f Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 24 Apr 2024 11:17:05 +0200 Subject: [PATCH 20/95] switch back to original function for creating conditional matrix --- benchmark/benchmark_madlc.py | 39 +++++++++++++------ .../models/modules/kpt_encoders.py | 5 ++- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/benchmark/benchmark_madlc.py b/benchmark/benchmark_madlc.py index bc0ee4696d..bd8c9c7574 100644 --- a/benchmark/benchmark_madlc.py +++ b/benchmark/benchmark_madlc.py @@ -25,7 +25,27 @@ SPLIT_FILE = MA_DLC_DATA_ROOT / "maDLC_benchmarking_splits.json" NUM_BPT = len(get_bodyparts(PROJECT_BENCHMARKED.cfg)) - AUG_TRAIN = ImageAugmentations( + # AUG_TRAIN = ImageAugmentations( + # normalize=True, + # covering=True, + # gaussian_noise=12.75, + # hist_eq=True, + # motion_blur=True, + # affine=AffineAugmentation( + # p=0.5, + # rotation=30, + # scale=(1, 1), + # translation=40, + # ), + # collate=BatchCollate( + # min_scale=0.4, + # max_scale=1.0, + # min_short_side=256, + # max_short_side=1152, + # multiple_of=32, + # ), + # ) + AUG_TRAIN_TD = ImageAugmentations( normalize=True, covering=True, gaussian_noise=12.75, @@ -34,16 +54,10 @@ affine=AffineAugmentation( p=0.5, rotation=30, - scale=(1, 1), - translation=40, - ), - collate=BatchCollate( - min_scale=0.4, - max_scale=1.0, - min_short_side=256, - max_short_side=1152, - multiple_of=32, + scale=(0.5, 1.25), + translation=1, ), + collate=None, ) DEFAULT_OPTIMIZER = {"type": "AdamW", "params": {"lr": 5e-4}} @@ -118,7 +132,8 @@ save_epochs=SAVE_EPOCHS, dataloader_workers=2, dataloader_pin_memory=True, - train_aug=AUG_TRAIN, + #train_aug=AUG_TRAIN, + train_aug=AUG_TRAIN_TD, inference_aug=None, backbone_config=None, head_config=None, @@ -142,5 +157,5 @@ splits_to_train=(0, ), train=True, evaluate=True, - manual_shuffle_index=None + manual_shuffle_index=None, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index d989edcf20..906a9fdab0 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -189,14 +189,15 @@ def _get_condition_matrix_optim(zero_matrix, kpts): mask = (0 < x) & (x < zero_matrix.shape[2]) & (0 < y) & (y < zero_matrix.shape[1]) colors_masked = np.repeat(self.colors[:, None, :], len(zero_matrix), 1) * np.repeat(mask[:, :, None], 3, 2) kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) + #kpt_indices = np.stack([x[mask[:,0]].T, y[mask[:,0]].T]).transpose(1, 2, 0) batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) #zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) zero_matrix[kpt_input[...,0], kpt_input[...,2]-1, kpt_input[...,1]-1] = colors_masked.transpose(1,0,2) return zero_matrix - #condition = _get_condition_matrix(zero_matrix, kpts) - condition = _get_condition_matrix_optim(zero_matrix, kpts) + condition = _get_condition_matrix(zero_matrix, kpts) + #condition = _get_condition_matrix_optim(zero_matrix, kpts) for i in range(batch_size): condition_heatmap = self.blur_heatmap(condition[i]) From 1ccf70593e123ff1298f58b56d05ce40bef17fcd Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 22 May 2024 11:26:02 +0200 Subject: [PATCH 21/95] update benchmark scripts --- benchmark/benchmark_madlc.py | 20 ++++++++++++++------ benchmark/benchmark_run_experiments.py | 1 + 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/benchmark/benchmark_madlc.py b/benchmark/benchmark_madlc.py index bd8c9c7574..a500a5d217 100644 --- a/benchmark/benchmark_madlc.py +++ b/benchmark/benchmark_madlc.py @@ -18,8 +18,8 @@ for PROJECT_NAME, TRAIN_FRACTION in [ #("parenting", 0.95), #("trimouse", 0.95), - #("fish", 0.94), - ("marmoset", 0.95), + ("fish", 0.94), + #("marmoset", 0.95), ]: PROJECT_BENCHMARKED = MA_DLC_BENCHMARKS[PROJECT_NAME] SPLIT_FILE = MA_DLC_DATA_ROOT / "maDLC_benchmarking_splits.json" @@ -51,11 +51,16 @@ gaussian_noise=12.75, hist_eq=True, motion_blur=True, + hflip=False, + #hflip=True, affine=AffineAugmentation( - p=0.5, + #p=0.5, + p=0.9, rotation=30, - scale=(0.5, 1.25), - translation=1, + #scale=(0.5, 1.25), + scale=(0.75, 1.25), + #translation=1, + translation=40, ), collate=None, ) @@ -155,7 +160,10 @@ train_fraction=TRAIN_FRACTION, models_to_train=[model_configs[0]], splits_to_train=(0, ), - train=True, + train=False, evaluate=True, + #manual_shuffle_index=41 # fish + #manual_shuffle_index=52 # buctd-fish + #manual_shuffle_index=114 # marmoset manual_shuffle_index=None, ) diff --git a/benchmark/benchmark_run_experiments.py b/benchmark/benchmark_run_experiments.py index 1451692a40..9ce1a96402 100644 --- a/benchmark/benchmark_run_experiments.py +++ b/benchmark/benchmark_run_experiments.py @@ -39,6 +39,7 @@ def main( ): if eval_params is None: eval_params = EvalParameters(snapshotindex="all", plotting=False) + #eval_params = EvalParameters(snapshotindex=-1, plotting=True) project.update_iteration_in_config() for config in models_to_train: From a4973c9508709169c25de56c612b1bf0933480fa Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 22 May 2024 11:27:01 +0200 Subject: [PATCH 22/95] add hflip to internal benchmark script --- benchmark/utils_augmentation.py | 2 ++ .../pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml | 4 ++-- .../pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/benchmark/utils_augmentation.py b/benchmark/utils_augmentation.py index a9e3213373..9060e5b5b3 100644 --- a/benchmark/utils_augmentation.py +++ b/benchmark/utils_augmentation.py @@ -101,6 +101,7 @@ class ImageAugmentations: gaussian_noise: float | bool = False hist_eq: bool = False motion_blur: bool = False + hflip: bool | float = False resize: dict | None = None crop_sampling: CropSampling | None = None collate: BatchCollate | None = None @@ -112,6 +113,7 @@ def data(self) -> dict: "gaussian_noise": self.gaussian_noise, "hist_eq": self.hist_eq, "motion_blur": self.motion_blur, + "hflip": self.hflip, "auto_padding": False, "affine": False, "resize": False, diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 7b6f90883b..12c94a40d4 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -5,8 +5,8 @@ data: pad_height_divisor: 32 bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 - #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 - bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 + bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index d85ec284a8..bbdceac6c4 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -4,8 +4,8 @@ data: pad_width_divisor: 32 pad_height_divisor: 32 bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 - #bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones From 304f9c2939e8ad1cce3dff82e880b052c47f2355 Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 22 May 2024 12:03:19 +0200 Subject: [PATCH 23/95] pad with black pixels instead of context for CTD + add margin for BU-derived bbox --- .../pose_estimation_pytorch/apis/evaluate.py | 10 +++++++++ .../pose_estimation_pytorch/data/dataset.py | 2 +- .../pose_estimation_pytorch/data/image.py | 21 ++++++++++++++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index 37a4adc20d..e5b6a91851 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -267,7 +267,13 @@ def evaluate_snapshot( save_evaluation_results(df_scores, scores_filepath, show_errors, pcutoff) if plotting: + # # use BU predictions for plotting + # bu_predictions = pd.read_hdf('/home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5', key='df_with_missing') + # bu_predictions = pd.read_hdf('/home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5', key='df_with_missing') + # predictions = {'train': bu_predictions, 'test': bu_predictions} + folder_name = f"LabeledImages_{scorer}" + #folder_name = f"LabeledImages_{scorer}_individual" folder_path = loader.evaluation_folder / folder_name folder_path.mkdir(parents=True, exist_ok=True) if isinstance(plotting, str): @@ -275,6 +281,8 @@ def evaluate_snapshot( else: plot_mode = "bodypart" + #plot_mode = 'individual' + df_ground_truth = ensure_multianimal_df_format(loader.df) for mode in ["train", "test"]: df_combined = predictions[mode].merge( @@ -286,6 +294,8 @@ def evaluate_snapshot( project_root=cfg["project_path"], scorer=cfg["scorer"], model_name=scorer, + #model_name='DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140', + #model_name='DLC_DekrW32_marmosetMay7shuffle1_140', output_folder=str(folder_path), in_train_set=mode == "train", plot_unique_bodyparts=len(unique_bodyparts) > 0, diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 5cea1d3084..00d4d2de10 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -196,7 +196,7 @@ def __getitem__(self, index: int) -> dict: # TODO: The following code should be replaced by a numpy version image, offsets, scales = _crop_and_pad_image_torch( - image, bboxes[0], "xywh", self.parameters.cropped_image_size[0] + image, bboxes[0], "xywh", self.parameters.cropped_image_size[0], self.task ) keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index 8510e33747..6c97231167 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -21,6 +21,7 @@ from torchvision.ops import box_convert from deeplabcut.pose_estimation_pytorch.data.utils import _compute_crop_bounds +from deeplabcut.pose_estimation_pytorch.task import Task def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: @@ -43,7 +44,7 @@ def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: def _crop_and_pad_image_torch( - image: np.array, bbox: np.array, bbox_format: str, output_size: int + image: np.array, bbox: np.array, bbox_format: str, output_size: int, task: Task ) -> tuple[np.array, tuple[int, int], tuple[int, int]]: """TODO: Reimplement this function with numpy and for non-square resize :) Only works for square cropped bounding boxes. Crops images around bounding boxes @@ -70,10 +71,20 @@ def _crop_and_pad_image_torch( c, h, w = image.shape crop_size = torch.max(bbox[2:]) - xmin = int(torch.clip(bbox[0] - (crop_size / 2), min=0, max=w - 1).cpu().item()) - xmax = int(torch.clip(bbox[0] + (crop_size / 2), min=1, max=w).cpu().item()) - ymin = int(torch.clip(bbox[1] - (crop_size / 2), min=0, max=h - 1).cpu().item()) - ymax = int(torch.clip(bbox[1] + (crop_size / 2), min=1, max=h).cpu().item()) + if task == Task.CTD: + # pad with empty pixels instead of context + cx, cy, boxw, boxh = bbox + xmin, xmax = int(cx - boxw/2), int(cx + boxw/2) + ymin, ymax = int(cy - boxh/2), int(cy + boxh/2) + # add 25 pixels of margin to the bbox + xmin, ymin = max(0, xmin - 25), max(0, ymin - 25) + xmax, ymax = min(w, xmax + 25), min(h, ymax + 25) + else: + xmin = int(torch.clip(bbox[0] - (crop_size / 2), min=0, max=w - 1).cpu().item()) + xmax = int(torch.clip(bbox[0] + (crop_size / 2), min=1, max=w).cpu().item()) + ymin = int(torch.clip(bbox[1] - (crop_size / 2), min=0, max=h - 1).cpu().item()) + ymax = int(torch.clip(bbox[1] + (crop_size / 2), min=1, max=h).cpu().item()) + cropped_image = image[:, ymin:ymax, xmin:xmax] crop_h, crop_w = cropped_image.shape[1:3] From 0b4be31d8e465ae0dda7171fd18cd5f8885a2af4 Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 22 May 2024 17:59:13 +0200 Subject: [PATCH 24/95] add script for testing ctd performance with coco api --- benchmark/benchmark_ctd_coco_api.py | 353 ++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 benchmark/benchmark_ctd_coco_api.py diff --git a/benchmark/benchmark_ctd_coco_api.py b/benchmark/benchmark_ctd_coco_api.py new file mode 100644 index 0000000000..bdb3319226 --- /dev/null +++ b/benchmark/benchmark_ctd_coco_api.py @@ -0,0 +1,353 @@ + +from __future__ import annotations + +import csv +import pickle +from functools import lru_cache +from pathlib import Path + +import numpy as np +import pandas as pd +from PIL import Image +from pycocotools.coco import COCO +from pycocotools.cocoeval import COCOeval +from ruamel.yaml import YAML + + +@lru_cache(maxsize=None) +def read_image_shape_fast(path: str | Path) -> tuple[int, int, int]: + """Blazing fast and does not load the image into memory""" + with Image.open(path) as img: + width, height = img.size + return len(img.getbands()), height, width + + +def bbox_from_keypoints( + keypoints: np.ndarray, + image_h: int, + image_w: int, + margin: int, +) -> np.ndarray: + squeeze = False + if len(keypoints.shape) == 2: + squeeze = True + keypoints = np.expand_dims(keypoints, axis=0) + + bboxes = np.full((keypoints.shape[0], 4), np.nan) + bboxes[:, :2] = np.nanmin(keypoints[..., :2], axis=1) - margin # X1, Y1 + bboxes[:, 2:4] = np.nanmax(keypoints[..., :2], axis=1) + margin # X2, Y2 + bboxes = np.clip( + bboxes, + a_min=[0, 0, 0, 0], + a_max=[image_w, image_h, image_w, image_h], + ) + bboxes[..., 2] = bboxes[..., 2] - bboxes[..., 0] # to width + bboxes[..., 3] = bboxes[..., 3] - bboxes[..., 1] # to height + if squeeze: + return bboxes[0] + + return bboxes + + +def to_coco( + df: pd.DataFrame, + project_dir: Path, + individuals: list[str], + bodyparts: list[str], + unique_bpts: list[str], + bbox_margin: int = 0, + image_size: tuple[int, int] | None = None +) -> dict: + with_individuals = "individuals" in df.columns.names + categories = [ + { + "id": 1, + "name": "animals", + "supercategory": "animal", + "keypoints": bodyparts, + "skeleton": [], + }, + ] + individuals = [idv for idv in individuals] + if len(unique_bpts) > 0: + individuals += ["single"] + categories.append( + { + "id": 2, + "name": "unique_bodypart", + "supercategory": "animal", + "keypoints": unique_bpts, + } + ) + + anns, images = [], [] + for idx, row in df.iterrows(): + image_id = len(images) + 1 + rel_path = Path(*idx) if isinstance(idx, tuple) else Path(str(idx)) + path = str(project_dir / rel_path) + if image_size is None: + _, height, width = read_image_shape_fast(path) + else: + width, height = image_size + + images.append( + { + "id": image_id, + "file_name": path, + "width": width, + "height": height, + } + ) + + for idv_idx, idv in enumerate(individuals): + category_id = 1 + if with_individuals: + if idv == "single": + category_id = 2 + data = row.xs(idv, level="individuals") + else: + data = row + + mask = np.array([(bpt in bodyparts) for bpt in data.index.get_level_values(level="bodyparts")]) + raw_keypoints = data.to_numpy() + raw_keypoints = raw_keypoints[mask] + raw_keypoints = raw_keypoints.reshape((-1, 2)) + + keypoints = np.zeros((len(raw_keypoints), 3)) + keypoints[:, :2] = raw_keypoints + is_visible = np.logical_and( + ~pd.isnull(raw_keypoints).all(axis=1), + np.logical_and( + np.logical_and( + 0 < keypoints[..., 0], + keypoints[..., 0] < width, + ), + np.logical_and( + 0 < keypoints[..., 1], + keypoints[..., 1] < height, + ), + ) + ) + keypoints[:, 2] = np.where(is_visible, 2, 0) + num_keypoints = is_visible.sum() + if num_keypoints > 1: # TODO: AT LEAST 2 KEYPOINTS TO COMPUTE mAP + bbox = bbox_from_keypoints( + keypoints=keypoints, + image_h=height, + image_w=width, + margin=bbox_margin, + ) + area = bbox[2].item() * bbox[3].item() + anns.append( + { + "id": len(anns) + 1, + "image_id": image_id, + "category_id": category_id, + "area": area, + "bbox": bbox, + "keypoints": keypoints.reshape(-1).tolist(), + "iscrowd": 0, + "num_keypoints": num_keypoints, + } + ) + + return {"annotations": anns, "categories": categories, "images": images} + + +def to_coco_predictions( + project_dir: Path, + images: list[dict], + individuals: list[str], + bodyparts: list[str], + df_pred: pd.DataFrame, +) -> list[dict]: + image_path_to_image = {image["file_name"]: image for image in images} + coco_predictions = [] + + for idx, row in df_pred.iterrows(): + rel_path = Path(*idx) if isinstance(idx, tuple) else Path(str(idx)) + full_path = str(project_dir / rel_path) + if full_path not in image_path_to_image: + continue # train image + + image = image_path_to_image[full_path] + image_id = image["id"] + image_h, image_w = image["height"], image["width"] + + image_keypoints = row.to_numpy().reshape((len(individuals), len(bodyparts), 3)) + for keypoints in image_keypoints: + if np.any(~np.isnan(keypoints)): + score = np.nanmean(keypoints[:, 2]).item() + keypoints = keypoints.copy() + keypoints[:, 2] = 2 + + bbox = bbox_from_keypoints( + keypoints=keypoints, + image_h=image_h, + image_w=image_w, + margin=0, + ) + area = bbox[2].item() * bbox[3].item() + + # NaN predictions to infinity + keypoints[np.isnan(keypoints)] = np.inf + + coco_pred = { + "image_id": int(image_id), + "category_id": 1, # TODO: get category ID from prediction? + "keypoints": keypoints.reshape(-1).tolist(), + "bbox": bbox, + "area": area, + "score": float(score), + } + coco_predictions.append(coco_pred) + # else: + # print(f"REMOVING {keypoints}") + + return coco_predictions + + +def load_split(file: Path) -> tuple[list[int], list[int]]: + with open(file, "rb") as f: + data = pickle.load(f) + + train_idx = sorted([int(idx) for idx in data[1]]) # ARE RESULTS INCONSISTENT AS WE DONT SORT INDICES? + test_idx = sorted([int(idx) for idx in data[2]]) + return list(train_idx), list(test_idx) + + +def load_prediction_filenames( + project_prefix: str, + output_folder: Path, +) -> dict[str, Path]: + shuffles = [p for p in output_folder.iterdir() if p.is_dir() and p.name.startswith(project_prefix)] + return { + p.stem.split("DLC_")[1]: p + for shuffle in shuffles + for p in shuffle.iterdir() + if p.suffix == ".h5" + } + + +def evaluate(results_path: Path, paths: dict[str, dict], bbox_margin: int = 0, plot: bool = False): + results = [] + + for project, data_paths in paths.items(): + print((3 * (100 * "-" + "\n"))[:-1]) + print(f"COCO EVALUATION RESULTS FOR {project}") + + _, test_idx = load_split(data_paths["split"]) + df_gt = pd.read_hdf(data_paths["gt"]) + df_test = df_gt.iloc[test_idx] + + reader = YAML() + with open(data_paths["project"] / "config.yaml", "r") as f: + cfg = reader.load(f) + + coco_test_dict = to_coco( + df_test, + data_paths["project"], + individuals=cfg["individuals"], + bodyparts=cfg["multianimalbodyparts"], + unique_bpts=cfg["uniquebodyparts"], + bbox_margin=bbox_margin, + image_size=(4096, 4096), # not needed + ) + + for scorer, pred_path in data_paths["predictions"].items(): + print(100 * "-") + print(pred_path.name) + print("Scorer", scorer) + print(100 * "-") + df_predictions = pd.read_hdf(pred_path) + predictions = to_coco_predictions( + data_paths["project"], + coco_test_dict["images"], + individuals=cfg["individuals"], + bodyparts=cfg["multianimalbodyparts"], + df_pred=df_predictions, + ) + + coco = COCO() + coco.dataset["annotations"] = coco_test_dict["annotations"] + coco.dataset["categories"] = coco_test_dict["categories"] + coco.dataset["images"] = coco_test_dict["images"] + coco.createIndex() + + coco_det = coco.loadRes(predictions) + coco_eval = COCOeval(coco, coco_det, iouType="keypoints") + coco_eval.params.kpt_oks_sigmas = np.array(len(cfg["multianimalbodyparts"]) * [0.1]) + # coco_eval.params.areaRng = [coco_eval.params.areaRng[0]] + # coco_eval.params.areaRngLbl = [coco_eval.params.areaRngLbl[0]] + + coco_eval.evaluate() + coco_eval.accumulate() + coco_eval.summarize() + + results.append((scorer, project.upper(), coco_eval.stats[0])) + + # for p in predictions: + # print(p) + + # if plot: + # image_id = 1 + # image_data = coco.loadImgs(ids=[image_id])[0] + # image = Image.open(image_data["file_name"]) + # ann_ids = coco.getAnnIds(imgIds=[image_id]) + # anns = coco.loadAnns(ids=ann_ids) + # plt.imshow(image) + # coco.showAnns(anns) + + print((3 * (100 * "-" + "\n"))[:-1]) + + print(f"Saving to {results_path}") + with open(results_path, "w") as f: + writer = csv.writer(f, delimiter='\t') + for row in results: + writer.writerow(row) + + +def main(): + dlc_root_dir = Path("/home/lucas/datasets/") + root_gt = dlc_root_dir / "ground_truth_test" + root_preds = Path("/home/lucas/datasets/test-images/fish-dlc-2021-05-07/evaluation-results/iteration-30/fishMay7-trainset94shuffle41/benchmark_BU_EfficientNet/") + + predictions = {"BU_model": dlc_root_dir / "benchmark_chkpts/fish/DLC_EfficientNet_B7_s4_30k.h5", + "CTD_90": root_preds / "fishMay7-trainset94shuffle41-snapshot-090.h5", + "CTD_120": root_preds / "fishMay7-trainset94shuffle41-snapshot-120.h5", + "CTD_150": root_preds / "fishMay7-trainset94shuffle41-snapshot-150.h5", + "CTD_180": root_preds / "fishMay7-trainset94shuffle41-snapshot-180.h5", + "CTD_210": root_preds / "fishMay7-trainset94shuffle41-snapshot-210.h5"} + + paths = { + # "fish": { + # "project": dlc_root_dir / "fish-dlc-2021-05-07", + # "gt": root_gt / "CollectedData_Valentina.h5", + # "split": root_gt / "Documentation_data-Schooling_70shuffle1.pickle", + # "predictions": load_prediction_filenames("fish", root_preds), + # }, + # "trimou/media1/data/lucas/DLC_projects/ma_dlc/test-images/fish-dlc-2021-05-07/evaluation-results/iteration-30/fishMay7-trainset94shuffle41/benchmark_BU_EfficientNetse": { + # "project": dlc_root_dir / "trimice-dlc-2021-06-22", + # "gt": root_gt / "CollectedData_Daniel.h5", + # "split": root_gt / "Documentation_data-MultiMouse_70shuffle1.pickle", + # "predictions": load_prediction_filenames("trimice", root_preds), + # }, + # "marmoset": { + # "project": dlc_root_dir / "marmoset-dlc-2021-05-07", + # "gt": root_gt / "CollectedData_Mackenzie.h5", + # "split": root_gt / "Documentation_data-Marmoset_70shuffle1.pickle", + # "predictions": load_prediction_filenames("marmoset", root_preds), + # }, + + "fish": { + "project": dlc_root_dir / "fish-dlc-2021-05-07", + "gt": root_gt / "CollectedData_Valentina.h5", + "split": root_gt / "Documentation_data-Schooling_70shuffle1.pickle", + "predictions": predictions, + }, + } + evaluate(root_preds / "pycocotools.csv", paths, bbox_margin=0, plot=False) + + +if __name__ == "__main__": + main() From c93c1bdc2f9ab5011851add1d80c30de08ad2af6 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 23 May 2024 14:34:44 +0200 Subject: [PATCH 25/95] make CTD test inference compatible with previous code --- benchmark/madlc_test_inference.py | 38 ++++++++++++++++--- .../pose_estimation_pytorch/apis/utils.py | 1 + .../pose_estimation_pytorch/data/image.py | 5 +-- .../data/postprocessor.py | 2 + .../data/preprocessor.py | 9 +++-- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/benchmark/madlc_test_inference.py b/benchmark/madlc_test_inference.py index 1ed0c744a9..023476d448 100644 --- a/benchmark/madlc_test_inference.py +++ b/benchmark/madlc_test_inference.py @@ -11,6 +11,7 @@ import pandas as pd from deeplabcut.pose_estimation_pytorch import PoseDatasetParameters from deeplabcut.pose_estimation_pytorch.apis.utils import get_inference_runners +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader from deeplabcut.utils.visualization import make_labeled_images_from_dataframe from ruamel.yaml import YAML from tqdm import tqdm @@ -25,6 +26,9 @@ def run_inference_on_all_images( save_as_csv: bool, plot: bool, detector_snapshot: Path | None = None, + bu_snapshot: Path | None = None, + bu_predictions: Path | None = None, + ) -> None: pytorch_config_path = snapshot.parent / "pytorch_config.yaml" with open(pytorch_config_path, "r") as file: @@ -57,7 +61,8 @@ def run_inference_on_all_images( num_unique_bodyparts=parameters.num_unique_bpts, with_identity=False, # TODO: implement transform=None, - detector_path=str(detector_snapshot), + #detector_path=str(detector_snapshot), + detector_path=detector_snapshot, detector_transform=None, ) @@ -67,6 +72,18 @@ def run_inference_on_all_images( bbox_predictions = detector_runner.inference(images=tqdm(pose_inputs)) pose_inputs = list(zip(pose_inputs, bbox_predictions)) + if bu_predictions is not None: + # #add cond kpts to context if you run inference with CTD + bu_pose_preds = DLCLoader.load_predictions(bu_snapshot, bu_predictions, parameters) + for k in list(bu_pose_preds.keys()): + # TODO: for dlcrnet & resnet: adapt image pathes in prediction file + #path = str(Path(*idx)) if isinstance(idx, tuple) else idx + bu_pose_preds[k.split('labeled-data')[1]] = bu_pose_preds.pop(k) + context = [{"cond_kpts": bu_pose_preds[image.split('labeled-data')[1]]} for image in pose_inputs] + if len(context) != len(pose_inputs): + raise ValueError(f"Missing context for some images: {len(context)} != {len(pose_inputs)}") + pose_inputs = list(zip(pose_inputs, context)) + print("Running pose prediction") predictions = runner.inference(tqdm(pose_inputs)) poses = np.array([p["bodyparts"] for p in predictions]) @@ -160,6 +177,7 @@ def main( shuffle: Shuffle, snapshot_indices: int | list[int] | None = None, detector_snapshot_indices: int | list[int] | None = None, + bu_predictions: Path | None = None, save_as_csv: bool = False, plot: bool = False, ) -> None: @@ -195,19 +213,29 @@ def main( for detector in detectors: for snapshot in snapshots: run_inference_on_all_images( - shuffle.project, snapshot, save_as_csv, plot, detector + shuffle.project, snapshot, save_as_csv, plot, detector, + bu_snapshot=None, bu_predictions=bu_predictions ) if __name__ == "__main__": + + #bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_DLCRNet_ms4_30k.h5' + bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_EfficientNet_B7_s4_30k.h5' + #bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_ResNet50_s4_30k.h5' + main( shuffle=Shuffle( - project=MA_DLC_BENCHMARKS["trimouse"], - index=0, - train_fraction=0.95, + #project=MA_DLC_BENCHMARKS["trimouse"], + project=MA_DLC_BENCHMARKS["fish"], + #index=0, + index=41, + #train_fraction=0.95, + train_fraction=0.94, ), snapshot_indices=None, detector_snapshot_indices=-1, + bu_predictions=Path(bu_preds), save_as_csv=False, plot=False, ) diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index e90cc74c15..e33f8c25f0 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -301,6 +301,7 @@ def get_inference_runners( ) if detector_path is not None: + detector_path = str(detector_path) if detector_transform is None: detector_transform = build_transforms( model_config["detector"]["data"]["inference"] diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index 6c97231167..c9566fe156 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -21,7 +21,6 @@ from torchvision.ops import box_convert from deeplabcut.pose_estimation_pytorch.data.utils import _compute_crop_bounds -from deeplabcut.pose_estimation_pytorch.task import Task def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: @@ -44,7 +43,7 @@ def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: def _crop_and_pad_image_torch( - image: np.array, bbox: np.array, bbox_format: str, output_size: int, task: Task + image: np.array, bbox: np.array, bbox_format: str, output_size: int, cond_td: bool = False ) -> tuple[np.array, tuple[int, int], tuple[int, int]]: """TODO: Reimplement this function with numpy and for non-square resize :) Only works for square cropped bounding boxes. Crops images around bounding boxes @@ -71,7 +70,7 @@ def _crop_and_pad_image_torch( c, h, w = image.shape crop_size = torch.max(bbox[2:]) - if task == Task.CTD: + if cond_td: # pad with empty pixels instead of context cx, cy, boxw, boxh = bbox xmin, xmax = int(cx - boxw/2), int(cx + boxw/2) diff --git a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py index 609ff00ef9..2053bd39a8 100644 --- a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py @@ -257,6 +257,8 @@ def __call__( ) -> tuple[dict[str, np.ndarray], Context]: for name in predictions: output = predictions[name] + if type(output) is list: + output = np.array(output) if len(output) < self.max_individuals[name]: pad_size = self.max_individuals[name] - len(output) tail_shape = output.shape[1:] diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 342b697a5f..4b53664661 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -106,7 +106,7 @@ def build_top_down_preprocessor( return ComposePreprocessor( components=[ LoadImage(color_mode), - TorchCropDetections(cropped_image_size=cropped_image_size[0]), + TorchCropDetections(cropped_image_size=cropped_image_size[0], ctd=False), AugmentImage(transform), ToTensor(), ] @@ -135,7 +135,7 @@ def build_conditional_top_down_preprocessor( components=[ LoadImage(color_mode), ComputeBoundingBoxesFromCondKeypoints(), - TorchCropDetections(cropped_image_size=cropped_image_size[0]), + TorchCropDetections(cropped_image_size=cropped_image_size[0], ctd=True), AugmentImage(transform), ConditionalKeypointsToModelInputs(), ToTensor(), @@ -337,9 +337,10 @@ def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context] class TorchCropDetections(Preprocessor): """TODO""" - def __init__(self, cropped_image_size: int, bbox_format: str = "xywh") -> None: + def __init__(self, cropped_image_size: int, bbox_format: str = "xywh", ctd: bool = False) -> None: self.cropped_image_size = cropped_image_size self.bbox_format = bbox_format + self.ctd = ctd def __call__( self, image: np.ndarray, context: Context @@ -351,7 +352,7 @@ def __call__( images, offsets, scales = [], [], [] for bbox in context["bboxes"]: cropped_image, offset, scale = _crop_and_pad_image_torch( - image, bbox, self.bbox_format, self.cropped_image_size + image, bbox, self.bbox_format, self.cropped_image_size, self.ctd ) images.append(cropped_image) offsets.append(offset) From 9c9ef50ab25cde506a1305eb7d50d5c119adc27d Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 23 May 2024 14:42:09 +0200 Subject: [PATCH 26/95] add bu name to output path names for CTD test inference --- benchmark/madlc_test_inference.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/benchmark/madlc_test_inference.py b/benchmark/madlc_test_inference.py index 023476d448..76a92407ac 100644 --- a/benchmark/madlc_test_inference.py +++ b/benchmark/madlc_test_inference.py @@ -73,10 +73,10 @@ def run_inference_on_all_images( pose_inputs = list(zip(pose_inputs, bbox_predictions)) if bu_predictions is not None: - # #add cond kpts to context if you run inference with CTD + # add cond kpts to context if you run inference with CTD bu_pose_preds = DLCLoader.load_predictions(bu_snapshot, bu_predictions, parameters) for k in list(bu_pose_preds.keys()): - # TODO: for dlcrnet & resnet: adapt image pathes in prediction file + # TODO: for dlcrnet & resnet: adapt image paths in prediction file #path = str(Path(*idx)) if isinstance(idx, tuple) else idx bu_pose_preds[k.split('labeled-data')[1]] = bu_pose_preds.pop(k) context = [{"cond_kpts": bu_pose_preds[image.split('labeled-data')[1]]} for image in pose_inputs] @@ -102,6 +102,7 @@ def run_inference_on_all_images( / f"iteration-{project.iteration}" / shuffle_name / "benchmark" + / "" if bu_predictions is None else f"_{bu_predictions.stem}" / f"{scorer}.h5" ) output_path.parent.mkdir(exist_ok=True, parents=True) From 4de92f9dcca6e6af7d6784c701991db6ba3c399b Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 23 May 2024 15:13:04 +0200 Subject: [PATCH 27/95] add bu name to output path names for CTD test inference - fix --- benchmark/benchmark_ctd_coco_api.py | 20 ++++++++++++-------- benchmark/madlc_test_inference.py | 5 +++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/benchmark/benchmark_ctd_coco_api.py b/benchmark/benchmark_ctd_coco_api.py index bdb3319226..9acde69b4e 100644 --- a/benchmark/benchmark_ctd_coco_api.py +++ b/benchmark/benchmark_ctd_coco_api.py @@ -310,14 +310,18 @@ def evaluate(results_path: Path, paths: dict[str, dict], bbox_margin: int = 0, p def main(): dlc_root_dir = Path("/home/lucas/datasets/") root_gt = dlc_root_dir / "ground_truth_test" - root_preds = Path("/home/lucas/datasets/test-images/fish-dlc-2021-05-07/evaluation-results/iteration-30/fishMay7-trainset94shuffle41/benchmark_BU_EfficientNet/") - - predictions = {"BU_model": dlc_root_dir / "benchmark_chkpts/fish/DLC_EfficientNet_B7_s4_30k.h5", - "CTD_90": root_preds / "fishMay7-trainset94shuffle41-snapshot-090.h5", - "CTD_120": root_preds / "fishMay7-trainset94shuffle41-snapshot-120.h5", - "CTD_150": root_preds / "fishMay7-trainset94shuffle41-snapshot-150.h5", - "CTD_180": root_preds / "fishMay7-trainset94shuffle41-snapshot-180.h5", - "CTD_210": root_preds / "fishMay7-trainset94shuffle41-snapshot-210.h5"} + + bu_model = Path('DLC_EfficientNet_B7_s4_30k.h5') + shuffle_ix = 41 + root_preds = Path(f"/home/lucas/datasets/test-images/fish-dlc-2021-05-07/evaluation-results/iteration-30/fishMay7-trainset94shuffle{shuffle_ix}/benchmark/{bu_model.stem}") + + predictions = {"BU_model": dlc_root_dir / "benchmark_chkpts/fish" / bu_model, + "CTD_90": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-090.h5", + "CTD_120": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-120.h5", + "CTD_150": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-150.h5", + "CTD_180": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-180.h5", + "CTD_210": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-210.h5",} + #"CTD_best": root_preds / f"fishMay7-trainset94shuffle{shuffle_ix}-snapshot-best.h5"} paths = { # "fish": { diff --git a/benchmark/madlc_test_inference.py b/benchmark/madlc_test_inference.py index 76a92407ac..05cecef01f 100644 --- a/benchmark/madlc_test_inference.py +++ b/benchmark/madlc_test_inference.py @@ -102,9 +102,10 @@ def run_inference_on_all_images( / f"iteration-{project.iteration}" / shuffle_name / "benchmark" - / "" if bu_predictions is None else f"_{bu_predictions.stem}" - / f"{scorer}.h5" ) + if bu_predictions is not None: + output_path /= bu_predictions.stem + output_path /= f"{scorer}.h5" output_path.parent.mkdir(exist_ok=True, parents=True) index = pd.MultiIndex.from_tuples( From fbbaf13fd743dcd2c4fb1f5b3b0a1d30beeacb7e Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 30 May 2024 17:17:20 +0200 Subject: [PATCH 28/95] update BUCTD training with new TD developments --- benchmark/benchmark_ctd_coco_api.py | 6 ++- benchmark/benchmark_madlc.py | 38 ++++++++++--------- benchmark/madlc_test_inference.py | 6 +-- .../config/ctd/ctd_coam_w32.yaml | 6 ++- .../config/ctd/ctd_coam_w48.yaml | 6 ++- .../pose_estimation_pytorch/data/dlcloader.py | 5 ++- .../models/backbones/hrnet_coam.py | 1 + .../pose_estimation_pytorch/models/model.py | 11 +++++- 8 files changed, 53 insertions(+), 26 deletions(-) diff --git a/benchmark/benchmark_ctd_coco_api.py b/benchmark/benchmark_ctd_coco_api.py index 9acde69b4e..865cd8de19 100644 --- a/benchmark/benchmark_ctd_coco_api.py +++ b/benchmark/benchmark_ctd_coco_api.py @@ -311,8 +311,10 @@ def main(): dlc_root_dir = Path("/home/lucas/datasets/") root_gt = dlc_root_dir / "ground_truth_test" - bu_model = Path('DLC_EfficientNet_B7_s4_30k.h5') - shuffle_ix = 41 + bu_model = Path('DLC_DLCRNet_ms4_30k.h5') + #bu_model = Path('DLC_EfficientNet_B7_s4_30k.h5') + #bu_model = Path('DLC_ResNet50_s4_30k.h5') + shuffle_ix = 72 root_preds = Path(f"/home/lucas/datasets/test-images/fish-dlc-2021-05-07/evaluation-results/iteration-30/fishMay7-trainset94shuffle{shuffle_ix}/benchmark/{bu_model.stem}") predictions = {"BU_model": dlc_root_dir / "benchmark_chkpts/fish" / bu_model, diff --git a/benchmark/benchmark_madlc.py b/benchmark/benchmark_madlc.py index a500a5d217..97092bbbd1 100644 --- a/benchmark/benchmark_madlc.py +++ b/benchmark/benchmark_madlc.py @@ -47,31 +47,38 @@ # ) AUG_TRAIN_TD = ImageAugmentations( normalize=True, - covering=True, + #covering=True, + covering=False, gaussian_noise=12.75, - hist_eq=True, - motion_blur=True, + #hist_eq=True, + hist_eq=False, + #motion_blur=True, + motion_blur=False, hflip=False, #hflip=True, affine=AffineAugmentation( - #p=0.5, - p=0.9, + p=0.5, + #p=0.9, rotation=30, #scale=(0.5, 1.25), - scale=(0.75, 1.25), + scale=(1.0, 1.0), #translation=1, - translation=40, + translation=0, ), collate=None, ) + HRNET_VERSION = "w32" + #HRNET_VERSION = "w48" + DEFAULT_OPTIMIZER = {"type": "AdamW", "params": {"lr": 5e-4}} + #DEFAULT_OPTIMIZER = {"type": "Adam", "params": {"lr": 1e-3}} DEFAULT_SCHEDULER["params"] = {"lr_list": [[1e-4], [1e-5]], "milestones": [170, 200]} EPOCHS = 210 SAVE_EPOCHS = 30 - DEKR_BATCH_SIZE = 8 - TD_HRNET_BATCH_SIZE = 8 + #DEKR_BATCH_SIZE = 8 + #TD_HRNET_BATCH_SIZE = 8 CTD_HRNET_BATCH_SIZE = 32 # logging params @@ -131,7 +138,7 @@ # ), # ), ModelConfig( - net_type="ctd_coam_w32", + net_type=f"ctd_coam_{HRNET_VERSION}", batch_size=CTD_HRNET_BATCH_SIZE, epochs=EPOCHS, save_epochs=SAVE_EPOCHS, @@ -146,9 +153,9 @@ scheduler_config=DEFAULT_SCHEDULER, wandb_config=WandBConfig( project=WANDB_PROJECT, - run_name=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet32", - group=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet32", - tags=(*BASE_TAGS, "arch=ctd-hrnet32"), + run_name=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet{HRNET_VERSION}", + group=f"{PROJECT_NAME}-{GROUP_UID}-ctd-hrnet{HRNET_VERSION}", + tags=(*BASE_TAGS, f"arch=ctd-hrnet{HRNET_VERSION}"), ), ), ] @@ -160,10 +167,7 @@ train_fraction=TRAIN_FRACTION, models_to_train=[model_configs[0]], splits_to_train=(0, ), - train=False, + train=True, evaluate=True, - #manual_shuffle_index=41 # fish - #manual_shuffle_index=52 # buctd-fish - #manual_shuffle_index=114 # marmoset manual_shuffle_index=None, ) diff --git a/benchmark/madlc_test_inference.py b/benchmark/madlc_test_inference.py index 05cecef01f..b8b2cae0d2 100644 --- a/benchmark/madlc_test_inference.py +++ b/benchmark/madlc_test_inference.py @@ -222,8 +222,8 @@ def main( if __name__ == "__main__": - #bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_DLCRNet_ms4_30k.h5' - bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_EfficientNet_B7_s4_30k.h5' + bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_DLCRNet_ms4_30k.h5' + #bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_EfficientNet_B7_s4_30k.h5' #bu_preds = '/home/lucas/datasets/benchmark_chkpts/fish/DLC_ResNet50_s4_30k.h5' main( @@ -231,7 +231,7 @@ def main( #project=MA_DLC_BENCHMARKS["trimouse"], project=MA_DLC_BENCHMARKS["fish"], #index=0, - index=41, + index=72, #train_fraction=0.95, train_fraction=0.94, ), diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 12c94a40d4..6edf597e6c 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -17,6 +17,8 @@ model: type: HRNetCoAM base_model_name: hrnet_w32 pretrained: true + freeze_bn_stats: false + freeze_bn_weights: false coam_modules: [2,] channel_att_only: false att_heads: 1 @@ -30,6 +32,8 @@ model: type: HeatmapHead predictor: type: HeatmapPredictor + apply_sigmoid: false + #clip_scores: true location_refinement: false target_generator: type: HeatmapGaussianGenerator @@ -39,7 +43,7 @@ model: generate_locref: false criterion: heatmap: - type: WeightedBCECriterion + type: WeightedMSECriterion weight: 1.0 heatmap_config: channels: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index bbdceac6c4..8d3d2cbefd 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -17,6 +17,8 @@ model: type: HRNetCoAM base_model_name: hrnet_w48 pretrained: true + freeze_bn_stats: false + freeze_bn_weights: false coam_modules: [2,] channel_att_only: false att_heads: 1 @@ -30,6 +32,8 @@ model: type: HeatmapHead predictor: type: HeatmapPredictor + apply_sigmoid: false + #clip_scores: true location_refinement: false target_generator: type: HeatmapGaussianGenerator @@ -39,7 +43,7 @@ model: generate_locref: false criterion: heatmap: - type: WeightedBCECriterion + type: WeightedMSECriterion weight: 1.0 heatmap_config: channels: diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 6637c2e51d..9b363211b9 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -260,7 +260,10 @@ def load_predictions(bu_snapshot: Path, bu_predictions: Path, parameters: PoseDa predictions = {} for idx in dlc_preds.index.unique(): - img_path = pred_path.parent.parent / Path(*idx) + if type(idx) == tuple: + img_path = pred_path.parent.parent / Path(*idx) + else: + img_path = pred_path.parent.parent / Path(idx) keypoints = dlc_preds.loc[idx].values.reshape(-1,len(parameters.bodyparts),3)[:,:,:2] keypoints = keypoints[~np.isnan(keypoints).all(axis=-1).all(axis=-1)] predictions[str(img_path)] = keypoints diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 203689a7b1..2a0f6d3f39 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -179,6 +179,7 @@ def forward(self, x, cond_kpts): y = self.stages(x, cond_hm) if self.model.incre_modules is not None: + raise NotImplementedError("Incremental HRNet modules not supported for HRNetCoAM") x = [incre(f) for f, incre in zip(x, self.model.incre_modules)] return self.prepare_output(y) diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py index 731f905342..8f86530cb0 100644 --- a/deeplabcut/pose_estimation_pytorch/models/model.py +++ b/deeplabcut/pose_estimation_pytorch/models/model.py @@ -178,4 +178,13 @@ def build(cfg: dict, no_pretrained_backbone: bool = False) -> "PoseModel": head_cfg["predictor"] = PREDICTORS.build(head_cfg["predictor"]) heads[name] = HEADS.build(head_cfg) - return PoseModel(cfg=cfg, backbone=backbone, neck=neck, heads=heads) + model = PoseModel(cfg=cfg, backbone=backbone, neck=neck, heads=heads) + + # let's try init the head with normal init as done in hrnet + for name, module in model.heads.named_parameters(): + if 'bias' in name: + nn.init.constant_(module, 0) + else: + nn.init.normal_(module, std=0.001) + + return model From 8f3b82d7624644a23fb456e040b40d56a4a9f34a Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Jun 2024 18:43:22 +0200 Subject: [PATCH 29/95] finish merge w main -> works as before --- .../pose_estimation_pytorch/data/dataset.py | 4 ++-- .../pose_estimation_pytorch/data/dlcloader.py | 8 ++++++-- .../data/generative_sampling.py | 3 --- .../models/backbones/hrnet_coam.py | 2 ++ .../models/modules/kpt_encoders.py | 18 +++++++++++++++--- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 80ad412dc7..dffad75097 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -202,7 +202,7 @@ def __getitem__(self, index: int) -> dict: ) bboxes[0] = bbox_from_keypoints( - synthesized_keypoints[:, :2], + synthesized_keypoints, original_size[0], original_size[1], 10, # FIXME: bbox_margin should be a parameter set in cfg @@ -257,7 +257,7 @@ def _prepare_final_data_dict( ) -> dict[str, np.ndarray | dict[str, np.ndarray]]: context = dict() if self.task == Task.CTD: - context["cond_keypoints"] = keypoints[1, :, :, :2].astype(np.single) + context["cond_keypoints"] = keypoints[1, :, :, :].astype(np.single) return { "image": image.transpose((2, 0, 1)), diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index fdc5f93120..38355ffd3d 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -297,9 +297,13 @@ def load_predictions(bu_snapshot: Path, bu_predictions: Path, parameters: PoseDa img_path = pred_path.parent.parent / Path(*idx) else: img_path = pred_path.parent.parent / Path(idx) - keypoints = dlc_preds.loc[idx].values.reshape(-1,len(parameters.bodyparts),3)[:,:,:2] + + keypoints = dlc_preds.loc[idx].values.reshape(-1,len(parameters.bodyparts),3)[...,:2] keypoints = keypoints[~np.isnan(keypoints).all(axis=-1).all(axis=-1)] - predictions[str(img_path)] = keypoints + cond_keypoints = np.zeros((*keypoints.shape[:-1], 3)) + cond_keypoints[..., :2] = keypoints + cond_keypoints[..., 2] = 2 + predictions[str(img_path)] = cond_keypoints return predictions diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index aa294720b1..9040ac6d00 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -363,9 +363,6 @@ def __call__( np.clip(synth_joints[:, 0], 0, image_size[1], out=synth_joints[:, 0]) np.clip(synth_joints[:, 1], 0, image_size[0], out=synth_joints[:, 1]) - # bring format of empty annotations back to DLC format (0 -> nan) - synth_joints[(synth_joints[...,0]==0) & (synth_joints[...,1]==0)] = np.nan - return synth_joints def get_distance_wrt_keypoint_sim(self, ks: float, area: float) -> np.ndarray: diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 6441da8075..51e64db860 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -174,6 +174,8 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): """ # create conditional heatmap + if isinstance(cond_kpts, torch.Tensor): + cond_kpts = cond_kpts.detach().numpy() cond_hm = self.cond_enc(cond_kpts.squeeze(1), x.size()[2:]) cond_hm = torch.from_numpy(cond_hm).float().to(x.device) cond_hm = cond_hm.permute(0, 3, 1, 2) # (B, C, H, W) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 906a9fdab0..5230f1e780 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -19,6 +19,7 @@ import matplotlib.pyplot as plt from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg +from deeplabcut.pose_estimation_pytorch.data.utils import _out_of_bounds_keypoints KEYPOINT_ENCODERS = Registry("kpt_encoders", build_func=build_from_cfg) @@ -173,13 +174,24 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: f"colors, but there are {num_kpts} to encode" ) - kpts = np.array(keypoints).astype(int) + #kpts = keypoints.detach().numpy() + kpts = keypoints.copy() + kpts[keypoints[..., 2] <= 0] = 0 + kpts = np.nan_to_num(kpts) + oob_mask = _out_of_bounds_keypoints(kpts, (256,256)) + if np.sum(oob_mask) > 0: + kpts[oob_mask] = 0 + kpts = kpts.astype(int) + + #kpts = np.array(keypoints).astype(int) zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): - x, y = pose.T - mask = (0 < x) & (x < size[1]) & (0 < y) & (y < size[0]) + #x, y = pose.T + x, y, vis = pose.T + #mask = (0 < x) & (x < size[1]) & (0 < y) & (y < size[0]) + mask = vis > 0 x_masked, y_masked, colors_masked = x[mask], y[mask], self.colors[mask] zero_matrix[i, y_masked-1, x_masked-1] = colors_masked return zero_matrix From e930274e039f6b773831d876183d464be5662f94 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Mon, 9 Dec 2024 10:46:33 +0100 Subject: [PATCH 30/95] get_pose_inference_runner for CTD --- .../pose_estimation_pytorch/apis/utils.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index 20f57d7485..636f36785d 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -591,11 +591,19 @@ def get_pose_inference_runner( with_identity=with_identity, ) else: - pose_preprocessor = build_top_down_preprocessor( - color_mode=model_config["data"]["colormode"], - transform=transform, - cropped_image_size=(256, 256), - ) + if pose_task == Task.CTD: + pose_preprocessor = build_conditional_top_down_preprocessor( + color_mode=model_config["data"]["colormode"], + transform=transform, + cropped_image_size=(256, 256), + ) + else: + pose_preprocessor = build_top_down_preprocessor( + color_mode=model_config["data"]["colormode"], + transform=transform, + cropped_image_size=(256, 256), + ) + pose_postprocessor = build_top_down_postprocessor( max_individuals=max_individuals, num_bodyparts=num_bodyparts, From 0773924e616509e8956ba4a1982e55f3fbe566a5 Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 15 Jan 2025 13:22:42 +0100 Subject: [PATCH 31/95] lastest buctd devs --- .../config/base/base.yaml | 3 +- .../pose_estimation_pytorch/data/dataset.py | 32 ++- .../data/generative_sampling.py | 3 + .../pose_estimation_pytorch/data/image.py | 267 ++++++++++++------ .../data/preprocessor.py | 14 +- .../models/backbones/hrnet_coam.py | 8 + .../pose_estimation_pytorch/runners/base.py | 18 ++ 7 files changed, 248 insertions(+), 97 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/base/base.yaml b/deeplabcut/pose_estimation_pytorch/config/base/base.yaml index 0e8a125bd0..45e764a0a9 100644 --- a/deeplabcut/pose_estimation_pytorch/config/base/base.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/base/base.yaml @@ -5,7 +5,8 @@ runner: gpus: null key_metric: "test.mAP" key_metric_asc: true - eval_interval: 10 + #eval_interval: 10 + eval_interval: 3 optimizer: type: AdamW params: diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index dffad75097..e151c8f060 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -16,7 +16,7 @@ import numpy as np from torch.utils.data import Dataset -from deeplabcut.pose_estimation_pytorch.data.image import load_image, _crop_and_pad_image_torch +from deeplabcut.pose_estimation_pytorch.data.image import load_image, top_down_crop from deeplabcut.pose_estimation_pytorch.data.utils import ( _crop_image_keypoints, _extract_keypoints_and_bboxes, @@ -201,24 +201,34 @@ def __getitem__(self, index: int) -> dict: image_size=original_size, ) - bboxes[0] = bbox_from_keypoints( - synthesized_keypoints, - original_size[0], - original_size[1], - 10, # FIXME: bbox_margin should be a parameter set in cfg - ) - - # TODO: The following code should be replaced by a numpy version - image, offsets, scales = _crop_and_pad_image_torch( - image, bboxes[0], "xywh", self.parameters.cropped_image_size[0], self.task + # if conditional keypoints are empty, we take original bbox + if np.any(synthesized_keypoints[..., -1] > 0): + bboxes[0] = bbox_from_keypoints( + synthesized_keypoints, + original_size[0], + original_size[1], + 25, # FIXME: bbox_margin should be a parameter set in cfg (25 for animals, 5 for humans?) + ) + + image, offsets, scales = top_down_crop( + image, bboxes[0], self.parameters.cropped_image_size, margin=0, + cond_td_padding=(self.task==Task.CTD) ) + keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] + # print('keypoints GT', keypoints) if self.task == Task.CTD: synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] + # synthesized_keypoints[synthesized_keypoints < 0] = 0 + # print('keypoints COND', synthesized_keypoints) + # print('') keypoints = safe_stack([keypoints, synthesized_keypoints[None, ...]], (0, self.parameters.num_joints, 3)) + # from deeplabcut.pose_estimation_pytorch.data.image import plot_keypoints + # plot_keypoints(image, keypoints[0,0], keypoints[1,0], "/home/lucas/logs/debug_images", index) + bboxes = bboxes[:1] bboxes[..., 0] = (bboxes[..., 0] - offsets[0]) / scales[0] bboxes[..., 1] = (bboxes[..., 1] - offsets[1]) / scales[1] diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 9040ac6d00..dc0b0dd36e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -360,8 +360,11 @@ def __call__( synth_joints[j] = synth_list[sampled_idx] synth_joints[j, 2] = 2 + nan_mask = np.isnan(synth_joints).any(axis=1) + synth_joints[nan_mask, 2] = 0 np.clip(synth_joints[:, 0], 0, image_size[1], out=synth_joints[:, 0]) np.clip(synth_joints[:, 1], 0, image_size[0], out=synth_joints[:, 1]) + # print('synth_joints', synth_joints) return synth_joints diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index c9566fe156..334147d8b4 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -23,6 +23,96 @@ from deeplabcut.pose_estimation_pytorch.data.utils import _compute_crop_bounds +import matplotlib.pyplot as plt +def plot_keypoints(image, keypoints1, keypoints2, output_path="", img_ix=0): + """ + Plots an image with two sets of keypoints and saves the result to disk. + + Args: + image (numpy array): The image to be plotted (shape: [256, 256, 3]). + keypoints1 (numpy array): First set of keypoints (shape: [5, 3]). + keypoints2 (numpy array): Second set of keypoints (shape: [5, 3]). + output_path (str): Path to save the output image. + """ + + keypoints2[keypoints2 < 0] = 0 + + # Check if the keypoints have a confidence score + if keypoints1.shape[1] == 3: + keypoints1 = keypoints1[:, :2] # Ignore confidence for now + if keypoints2.shape[1] == 3: + keypoints2 = keypoints2[:, :2] + # Create a figure and axis + fig, ax = plt.subplots(figsize=(6, 6)) + # Plot the image + ax.imshow(image) + # Plot the first set of keypoints + for ix, keypoint in enumerate(keypoints1): + if ix == len(keypoints1) - 1: + ax.scatter(keypoint[0], keypoint[1], c='green', s=50, label="GT keypoints") + else: + ax.scatter(keypoint[0], keypoint[1], c='green', s=50) + # Plot the second set of keypoints + for ix, keypoint in enumerate(keypoints2): + if ix == len(keypoints2) - 1: + ax.scatter(keypoint[0], keypoint[1], c='red', s=50, label="Cond keypoints") + else: + ax.scatter(keypoint[0], keypoint[1], c='red', s=50) + # Add a legend to differentiate keypoints + ax.legend() + # Remove axis ticks + ax.axis("off") + # Save the image to disk + plt.savefig(f'{output_path}/loaded_cropped_{img_ix}.png', bbox_inches='tight') + plt.close() + +def plot_image_grid(images, conditions, save_path="", batch_ix=0, single=False): + """ + Plots a grid of 16 images from vector1 in the left column and 16 images from vector2 in the right column. + Args: + images (numpy array): First vector of shape [32, 3, 256, 256]. + conditions (numpy array): Second vector of shape [32, 3, 256, 256]. + save_path (str): Path to save the output image. + """ + # Ensure the inputs have the correct shape + num_images = 1 if single else 32 + + assert images.shape == (num_images, 3, 256, 256), "images must have shape [32, 3, 256, 256]" + assert conditions.shape == (num_images, 3, 256, 256), "conditions must have shape [32, 3, 256, 256]" + + if single: + images1, images2 = images, conditions + else: + num_images = num_images // 2 + # Select the first 16 images from each vector + images1 = images[:num_images] + images2 = conditions[:num_images] + + # Create a figure with 16 rows and 2 columns + fig, axes = plt.subplots(num_images, 2, figsize=(8, 32)) + + # Loop through the rows and plot images + for i in range(num_images): + if single: + curr_ax = axes + else: + curr_ax = axes[i] + # Left column: images from vector1 + curr_ax[0].imshow(np.transpose(images1[i], (1, 2, 0))) # Convert [3, 256, 256] to [256, 256, 3] + curr_ax[0].axis("off") # Turn off the axis + curr_ax[0].set_title(f"Input Images {i+1}", fontsize=8) + # Right column: images from vector2 + curr_ax[1].imshow(np.transpose(images2[i], (1, 2, 0))) # Convert [3, 256, 256] to [256, 256, 3] + curr_ax[1].axis("off") # Turn off the axis + curr_ax[1].set_title(f"Cond. Heatmaps {i+1}", fontsize=8) + + # Adjust spacing + plt.tight_layout() + # Save the figure + plt.savefig(f'{save_path}/hrnet_coam_input_{batch_ix}.png', bbox_inches='tight') + plt.close(fig) # Close the figure to free up memory + + def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: """Loads an image from a file using cv2 @@ -42,85 +132,6 @@ def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: return image -def _crop_and_pad_image_torch( - image: np.array, bbox: np.array, bbox_format: str, output_size: int, cond_td: bool = False -) -> tuple[np.array, tuple[int, int], tuple[int, int]]: - """TODO: Reimplement this function with numpy and for non-square resize :) - Only works for square cropped bounding boxes. Crops images around bounding boxes - for top-down pose estimation in a MMpose style. Computes offsets so that - coordinates in the original image can be mapped to the cropped one; - - x_cropped = (x - offset_x) / scale_x - x_cropped = (y - offset_y) / scale_y - - Args: - image: (h, w, c) the image to crop - bbox: (4,) the bounding box to crop around - bbox_format: {"xyxy", "xywh", "cxcywh"} the format of the bounding box - output_size: the size to resize the image to - - Returns: - cropped_image, (offset_x, offset_y), (scale_x, scale_y) - """ - image = torch.tensor(image).permute(2, 0, 1) - bbox = torch.tensor(bbox) - if bbox_format != "cxcywh": - bbox = box_convert(bbox.unsqueeze(0), bbox_format, "cxcywh").squeeze() - - c, h, w = image.shape - crop_size = torch.max(bbox[2:]) - - if cond_td: - # pad with empty pixels instead of context - cx, cy, boxw, boxh = bbox - xmin, xmax = int(cx - boxw/2), int(cx + boxw/2) - ymin, ymax = int(cy - boxh/2), int(cy + boxh/2) - # add 25 pixels of margin to the bbox - xmin, ymin = max(0, xmin - 25), max(0, ymin - 25) - xmax, ymax = min(w, xmax + 25), min(h, ymax + 25) - else: - xmin = int(torch.clip(bbox[0] - (crop_size / 2), min=0, max=w - 1).cpu().item()) - xmax = int(torch.clip(bbox[0] + (crop_size / 2), min=1, max=w).cpu().item()) - ymin = int(torch.clip(bbox[1] - (crop_size / 2), min=0, max=h - 1).cpu().item()) - ymax = int(torch.clip(bbox[1] + (crop_size / 2), min=1, max=h).cpu().item()) - - cropped_image = image[:, ymin:ymax, xmin:xmax] - - crop_h, crop_w = cropped_image.shape[1:3] - pad_size = max(crop_h, crop_w) - offset = (xmin, ymin) - - # Pad image if not square - if not crop_h == crop_w: - padded_cropped_image = torch.zeros((c, pad_size, pad_size), dtype=image.dtype) - # Try to center bbox in padding - w_start = 0 - if bbox[0] - (crop_size / 2) < 0: - # padding on the left - w_start = pad_size - crop_w - elif bbox[0] + (crop_size / 2) >= w: - # padding on the right - w_start = 0 - - h_start = 0 - if bbox[1] - (crop_size / 2) < 0: - # padding at the top - h_start = pad_size - crop_h - elif bbox[1] + (crop_size / 2) >= h: - # padding at the bottom - h_start = 0 - - h_end = h_start + crop_h - w_end = w_start + crop_w - offset = (offset[0] - w_start, offset[1] - h_start) - padded_cropped_image[:, h_start:h_end, w_start:w_end] = cropped_image - cropped_image = padded_cropped_image - - scale = pad_size / output_size - output = F.resize(cropped_image, [output_size, output_size], antialias=True) - return output.permute(1, 2, 0).numpy(), offset, (scale, scale) - - def resize_and_random_crop( image: np.ndarray, targets: dict, @@ -284,3 +295,101 @@ def scale_kpts( anns["area"] = scaled_area return scaled_cropped_image, targets + + +def top_down_crop( + image: np.ndarray, + bbox: np.ndarray, + output_size: tuple[int, int], + margin: int = 0, + center_padding: bool = False, + cond_td_padding: bool = False, +) -> tuple[np.array, tuple[int, int], tuple[float, float]]: + """ + Crops images around bounding boxes for top-down pose estimation. Computes offsets so + that coordinates in the original image can be mapped to the cropped one; + + x_cropped = (x - offset_x) / scale_x + x_cropped = (y - offset_y) / scale_y + + Bounding boxes are expected to be in COCO-format (xywh). + + Args: + image: (h, w, c) the image to crop + bbox: (4,) the bounding box to crop around + output_size: the (width, height) of the output cropped image + margin: a margin to add around the bounding box before cropping + center_padding: whether to center the image in the padding if any is needed + cond_td_padding: whether to pad the image with empty pixels instead of context + + Returns: + cropped_image, (offset_x, offset_y), (scale_x, scale_y) + """ + image_h, image_w, c = image.shape + out_w, out_h = output_size + x, y, w, h = bbox + + cx = x + w / 2 + cy = y + h / 2 + w += 2 * margin + h += 2 * margin + + if not cond_td_padding: + input_ratio = w / h + output_ratio = out_w / out_h + if input_ratio > output_ratio: # h/w < h0/w0 => h' = w * h0/w0 + h = w / output_ratio + elif input_ratio < output_ratio: # w/h < w0/h0 => w' = h * w0/h0 + w = h * output_ratio + + # cx,cy,w,h will now give the right ratio -> check if padding is needed + x1, y1 = int(round(cx - (w / 2))), int(round(cy - (h / 2))) + x2, y2 = int(round(cx + (w / 2))), int(round(cy + (h / 2))) + + # pad symmetrically - compute total padding across axis + pad_left, pad_right, pad_top, pad_bottom = 0, 0, 0, 0 + if x1 < 0: + pad_left = -x1 + x1 = 0 + if x2 > image_w: + pad_right = x2 - image_w + x2 = image_w + if y1 < 0: + pad_top = -y1 + y1 = 0 + if y2 > image_h: + pad_bottom = y2 - image_h + y2 = image_h + + w, h = x2 - x1, y2 - y1 + + if cond_td_padding: + input_ratio = w / h + output_ratio = out_w / out_h + if input_ratio > output_ratio: # h/w < h0/w0 => h' = w * h0/w0 + w_pad = int(w - h * output_ratio) // 2 + pad_top += w_pad + pad_bottom += w_pad + + elif input_ratio < output_ratio: # w/h < w0/h0 => w' = h * w0/h0 + h_pad = int(h - (w / output_ratio)) // 2 + pad_left += h_pad + pad_right += h_pad + + pad_x = pad_left + pad_right + pad_y = pad_top + pad_bottom + if center_padding: + pad_left = pad_x // 2 + pad_top = pad_y // 2 + + # crop the pixels we care about + image_crop = np.zeros((h + pad_y, w + pad_x, c), dtype=image.dtype) + image_crop[pad_top:pad_top + h, pad_left:pad_left + w] = image[y1:y2, x1:x2] + + # resize the cropped image + image = cv2.resize(image_crop, (out_w, out_h), interpolation=cv2.INTER_LINEAR) + + # compute scale and offset + offset = x1 - pad_left, y1 - pad_top + scale = (w + pad_x) / out_w, (h + pad_y) / out_h + return image, offset, scale \ No newline at end of file diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index b77ea87683..80c29e3681 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -20,8 +20,8 @@ import torch from deeplabcut.pose_estimation_pytorch.data.image import ( - _crop_and_pad_image_torch, load_image, + top_down_crop ) from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints @@ -128,7 +128,7 @@ def build_conditional_top_down_preprocessor( estimator Returns: - A default top-down Preprocessor + A default conditional top-down Preprocessor """ return ComposePreprocessor( components=[ @@ -352,8 +352,9 @@ def __call__( images, offsets, scales = [], [], [] for bbox in context["bboxes"]: - cropped_image, offset, scale = _crop_and_pad_image_torch( - image, bbox, self.bbox_format, self.cropped_image_size, self.ctd + cropped_image, offset, scale = top_down_crop( + image, bbox, (self.cropped_image_size, self.cropped_image_size), + margin=0, cond_td_padding=self.ctd ) images.append(cropped_image) offsets.append(offset) @@ -391,10 +392,11 @@ def __call__( ) context["bboxes"] = [ - # FIXME: bbox_margin should be a parameter set in the configuration - bbox_from_keypoints(cond_kpts, image.shape[0], image.shape[1], 10) + # FIXME: bbox_margin should be a parameter set in the configuration (25 for animals, 5 for humans?) + bbox_from_keypoints(cond_kpts, image.shape[0], image.shape[1], 25) for cond_kpts in context[self.cond_kpt_key] ] + # context["bboxes"] = np.clip(context["bboxes"], 0, image.shape[0] - 1) return image, context diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 51e64db860..fcedfaedb5 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -180,6 +180,14 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): cond_hm = torch.from_numpy(cond_hm).float().to(x.device) cond_hm = cond_hm.permute(0, 3, 1, 2) # (B, C, H, W) + from deeplabcut.pose_estimation_pytorch.data.image import plot_image_grid + # during training + # plot_image_grid(x.cpu().numpy(), cond_hm.cpu().numpy(), '/home/lucas/logs/debug_images', x[19,1,55,77]) + # during inference + # plot_image_grid(x.cpu().numpy(), cond_hm.cpu().numpy(), '/home/lucas/logs/debug_images', np.random.rand(3,2), single=True) + # print('x', x.shape) + # print('cond_hm', cond_hm.shape) + # Stem x = self.model.conv1(x) x = self.model.bn1(x) diff --git a/deeplabcut/pose_estimation_pytorch/runners/base.py b/deeplabcut/pose_estimation_pytorch/runners/base.py index f25def2856..f65a0041ce 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/base.py +++ b/deeplabcut/pose_estimation_pytorch/runners/base.py @@ -78,5 +78,23 @@ def load_snapshot( The content of the snapshot file. """ snapshot = torch.load(snapshot_path, map_location=device) + ############################################################################################# + # snapshot = torch.load("/home/lucas/checkpoints/fish/w48/model_best.pth", map_location="cpu") + + # import re + # snapshot = {"backbone.model." + k: v for k, v in snapshot.items()} + # snapshot = {re.sub(r"model.stage([0-4])_att", r"coam_stages.\1", k): v for k, v in snapshot.items()} + # snapshot = {k.replace("backbone.model.final_layer.weigth", + # "heads.bodypart.heatmap_head.final_conv.weight"): v for k, v in snapshot.items()} + # snapshot = {k.replace("backbone.model.final_layer.bias", + # "heads.bodypart.heatmap_head.final_conv.bias"): v for k, v in snapshot.items()} + + # # print(model) + # # for key in model.state_dict().keys(): + # # print(key) + + # model.load_state_dict(snapshot) + + ############################################################################################ model.load_state_dict(snapshot["model"]) return snapshot From e432b9fb0c77daf10d1a316afb1f16afcf177f3f Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 15 Jan 2025 13:48:41 +0100 Subject: [PATCH 32/95] import fix --- .../pose_estimation_pytorch/models/modules/kpt_encoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 5230f1e780..c6929f0477 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -19,7 +19,7 @@ import matplotlib.pyplot as plt from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg -from deeplabcut.pose_estimation_pytorch.data.utils import _out_of_bounds_keypoints +from deeplabcut.pose_estimation_pytorch.data.utils import out_of_bounds_keypoints KEYPOINT_ENCODERS = Registry("kpt_encoders", build_func=build_from_cfg) From 699a25f0eb56eb80fb9dc57446a29d0812a005c9 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 15 Jan 2025 13:48:59 +0100 Subject: [PATCH 33/95] func name fix --- .../pose_estimation_pytorch/models/modules/kpt_encoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index c6929f0477..a312887fd8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -178,7 +178,7 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: kpts = keypoints.copy() kpts[keypoints[..., 2] <= 0] = 0 kpts = np.nan_to_num(kpts) - oob_mask = _out_of_bounds_keypoints(kpts, (256,256)) + oob_mask = out_of_bounds_keypoints(kpts, (256,256)) if np.sum(oob_mask) > 0: kpts[oob_mask] = 0 kpts = kpts.astype(int) From c792c9cf00697675e659c56d3dc8747bf221a67a Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 15 Jan 2025 14:34:07 +0100 Subject: [PATCH 34/95] bug fix: pose postprocessor not built --- deeplabcut/pose_estimation_pytorch/apis/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index 84762b6d05..08034a0a9d 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -525,6 +525,12 @@ def get_inference_runners( top_down_crop_with_context=crop_cfg.get("with_context", True), ) + pose_postprocessor = build_top_down_postprocessor( + max_individuals=max_individuals, + num_bodyparts=num_bodyparts, + num_unique_bodyparts=num_unique_bodyparts, + ) + # FIXME: Cannot run detectors on MPS detector_device = device if device == "mps": From 1b723c2b8cd5d4489cd701017e8f5abf0697e905 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 17 Jan 2025 17:35:33 +0100 Subject: [PATCH 35/95] bug fix: evaluate and inference --- .../pose_estimation_pytorch/apis/evaluate.py | 26 ++++++++++++++----- .../pose_estimation_pytorch/data/dlcloader.py | 12 +++++---- .../models/backbones/hrnet.py | 1 - .../runners/inference.py | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index f0340215d9..2b34fe693e 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -84,13 +84,27 @@ def predict( for image in image_paths ] - elif pose_task == Task.CTD: + elif loader.pose_task == Task.CTD: # Get conditional keypoints for context - bu_snapshot = loader.model_cfg["data"]["inference"]["bu_snapshot"] - bu_preds = loader.model_cfg["data"]["inference"]["bu_predictions"] - pose_predictions = loader.load_predictions(Path(bu_snapshot), Path(bu_preds), - loader.get_dataset_parameters()) - context = [{"cond_kpts": pose_predictions[image]} for image in image_paths] + import json + with open(loader.model_cfg["data"]["inference"]["conditions"], "r") as f: + conditions = json.load(f) + + def _get_condition(image): + if not isinstance(image, Path): + image = Path(image) + video_name, image_name = image.parent.name, image.name + image_key = f"labeled-data/{video_name}/{image_name}" + image_conditions = np.stack( + [ + np.array(c["keypoints"]).reshape((-1, 3)) + for c in conditions + if c["image_path"] == image_key + ] + ) + return image_conditions + + context = [{"cond_kpts": _get_condition[image]} for image in image_paths] images_with_context = image_paths if context is not None: diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 75f380c973..a766bda296 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -269,12 +269,14 @@ def load_split( train_ids = [int(i) for i in meta[1]] test_ids = [int(i) for i in meta[2]] return {"train": train_ids, "test": test_ids} - - @staticmethod - def load_predictions(bu_snapshot: Path, bu_predictions: Path, parameters: PoseDatasetParameters) -> pd.DataFrame: + @staticmethod + def load_predictions( + bu_snapshot: Path, + bu_predictions: Path, + parameters: PoseDatasetParameters, + ) -> pd.DataFrame: if bu_predictions is None: - pred_path = Path(str(bu_snapshot).replace('dlc-models', 'evaluation-results')).parent.parent cfg = af.read_config(pred_path.parent.parent.parent / "config.yaml") scorer = af.get_scorer_name( @@ -285,8 +287,8 @@ def load_predictions(bu_snapshot: Path, bu_predictions: Path, parameters: PoseDa trainingsiterations=re.search(r'snapshot-(.+)\.pth', str(bu_snapshot)).group(1), modelprefix="", ) - pred_file = pred_path / f"{scorer[0]}.h5" + pred_file = pred_path / f"{scorer[0]}.h5" dlc_preds = pd.read_hdf(pred_file, key="df_with_missing") #FIXME: Implement the case where snapshot is loaded diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py index 398fe84759..6af4905723 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet.py @@ -97,7 +97,6 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return self.prepare_output(y_list) - def _load_hrnet( model_name: str, pretrained: bool, diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 47e29c3351..e622371d3a 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -167,7 +167,7 @@ def _prepare_inputs( if curr_v is None: curr_v = v elif isinstance(curr_v, np.ndarray): - curr_v = np.concatenate([curr_v, v], dim=0) + curr_v = np.concatenate([curr_v, v], axis=0) elif isinstance(curr_v, torch.Tensor): curr_v = torch.cat([curr_v, v], dim=0) else: From 7cd12030a5a29dac13d5a3be1214cabf6602e926 Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 17 Jan 2025 17:56:08 +0100 Subject: [PATCH 36/95] fix cropped_size variable and hacky test loading --- .../pose_estimation_pytorch/data/dataset.py | 6 +-- .../pose_estimation_pytorch/runners/base.py | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index cb8270bd56..c1c93af89b 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -195,7 +195,7 @@ def __getitem__(self, index: int) -> dict: scales = (1.0, 1.0) if self.task in (Task.TOP_DOWN, Task.CTD): - if self.parameters.cropped_image_size is None: + if self.parameters.top_down_crop_size is None: raise ValueError( "You must specify a cropped image size for top-down models" ) @@ -236,7 +236,7 @@ def __getitem__(self, index: int) -> dict: image, offsets, scales = top_down_crop( image, bboxes[0], - self.parameters.cropped_image_size, + self.parameters.top_down_crop_size, margin=0, crop_with_context=(self.task != Task.CTD), ) @@ -260,7 +260,7 @@ def __getitem__(self, index: int) -> dict: bboxes[..., 1] = (bboxes[..., 1] - offsets[1]) / scales[1] bboxes[..., 2] = bboxes[..., 2] / scales[0] bboxes[..., 3] = bboxes[..., 3] / scales[1] - bboxes = np.clip(bboxes, 0, self.parameters.cropped_image_size[0] - 1) #TODO: clip based on [x,y,x,y]? + bboxes = np.clip(bboxes, 0, self.parameters.top_down_crop_size[0] - 1) #TODO: clip based on [x,y,x,y]? # as a RandomBBoxTransform can be added, keypoints may be outside of the # image after the crop diff --git a/deeplabcut/pose_estimation_pytorch/runners/base.py b/deeplabcut/pose_estimation_pytorch/runners/base.py index 9ef671c49c..ac373d909d 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/base.py +++ b/deeplabcut/pose_estimation_pytorch/runners/base.py @@ -115,22 +115,31 @@ def load_snapshot( """ snapshot = attempt_snapshot_load(snapshot_path, device, weights_only) ############################################################################################# - # snapshot = torch.load("/home/lucas/checkpoints/fish/w48/model_best.pth", map_location="cpu") + #snapshot = torch.load("/home/lucas/checkpoints/fish/w48/model_best.pth", map_location="cpu") + # snapshot = torch.load("/home/lucas/checkpoints/fish/w48/model_best_new.pth", map_location="cpu") # import re - # snapshot = {"backbone.model." + k: v for k, v in snapshot.items()} - # snapshot = {re.sub(r"model.stage([0-4])_att", r"coam_stages.\1", k): v for k, v in snapshot.items()} - # snapshot = {k.replace("backbone.model.final_layer.weigth", - # "heads.bodypart.heatmap_head.final_conv.weight"): v for k, v in snapshot.items()} - # snapshot = {k.replace("backbone.model.final_layer.bias", - # "heads.bodypart.heatmap_head.final_conv.bias"): v for k, v in snapshot.items()} - - # # print(model) - # # for key in model.state_dict().keys(): - # # print(key) - - # model.load_state_dict(snapshot) - + + # snapshot = { + # "backbone.model." + k: v for k, v in snapshot.items() + # } + # snapshot = { + # re.sub(r"model.stage2_att", r"coam_stages.1", k): v for k, v in snapshot.items() + # } + # snapshot = { + # k.replace("backbone.model.final_layer.weight", "heads.bodypart.heatmap_head.final_conv.weight"): v + # for k, v in snapshot.items() + # } + # snapshot = { + # k.replace("backbone.model.final_layer.bias", "heads.bodypart.heatmap_head.final_conv.bias"): v + # for k, v in snapshot.items() + # } + + # fc = "heads.bodypart.heatmap_head.final_conv" + # snapshot[f"{fc}.weight"] = snapshot[f"{fc}.weight"][[0, 1, 2, 3, 6]] + # snapshot[f"{fc}.bias"] = snapshot[f"{fc}.bias"][[0, 1, 2, 3, 6]] + + # model.load_state_dict(snapshot, strict=False) ############################################################################################ model.load_state_dict(snapshot["model"]) return snapshot From c012941fb78ad0f0850e1156fec9c55dc1608b41 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Thu, 6 Feb 2025 16:52:50 +0100 Subject: [PATCH 37/95] Fixes to evaluation/inference on empty conditions --- .../pose_estimation_pytorch/apis/evaluate.py | 27 +++++++++++++------ .../runners/inference.py | 4 ++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index 2b34fe693e..ab1ae430a5 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -95,16 +95,27 @@ def _get_condition(image): image = Path(image) video_name, image_name = image.parent.name, image.name image_key = f"labeled-data/{video_name}/{image_name}" - image_conditions = np.stack( - [ - np.array(c["keypoints"]).reshape((-1, 3)) - for c in conditions - if c["image_path"] == image_key - ] - ) + # image_conditions = np.stack( + # [ + # np.array(c["keypoints"]).reshape((-1, 3)) + # for c in conditions + # if c["image_path"] == image_key + # ] + # ) + if image_key not in conditions: + return np.zeros((0, 0, 3)) + + image_conditions = np.array(conditions[image_key]) + if len(image_conditions) == 0: + return np.zeros((0, 0, 3)) + + image_conditions[:, :, 2] = 1.0 + if image_conditions.shape[1] == 7: + image_conditions = image_conditions[:, [0, 1, 2, 3, 6], :] + return image_conditions - context = [{"cond_kpts": _get_condition[image]} for image in image_paths] + context = [{"cond_kpts": _get_condition(image)} for image in image_paths] images_with_context = image_paths if context is not None: diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index e622371d3a..cfb6910b1e 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -164,8 +164,10 @@ def _prepare_inputs( model_kwargs = context.pop("model_kwargs", {}) for k, v in model_kwargs.items(): curr_v = self._model_kwargs.get(k) - if curr_v is None: + if curr_v is None or len(curr_v) == 0: curr_v = v + elif len(v) == 0: + continue elif isinstance(curr_v, np.ndarray): curr_v = np.concatenate([curr_v, v], axis=0) elif isinstance(curr_v, torch.Tensor): From 0be7e2e9a2cadaf6cdd6ffcdc2b7b965cb141df1 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Thu, 6 Feb 2025 16:59:17 +0100 Subject: [PATCH 38/95] added expected format --- deeplabcut/pose_estimation_pytorch/apis/evaluate.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py index ab1ae430a5..f719dbe09d 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluate.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluate.py @@ -89,6 +89,14 @@ def predict( import json with open(loader.model_cfg["data"]["inference"]["conditions"], "r") as f: conditions = json.load(f) + # { + # "img0000.png": [ (num_conditions, num_bodyparts, 3) + # [ # condition 1 + # [[x, y, score], [x, y, score], ... ] + # ] + # ], + # "img0001.png": [...] + # } def _get_condition(image): if not isinstance(image, Path): From 46c213cf946a633c8ed6693c5692ee61c096874b Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 7 Feb 2025 14:25:01 +0100 Subject: [PATCH 39/95] black for CoAM module --- .../models/modules/coam_module.py | 290 ++++++++++++------ 1 file changed, 189 insertions(+), 101 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index b03066c1fa..1b4ef98179 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -20,21 +20,30 @@ # StackedKeypointEncoder, # ) + class CoAMBlock(nn.Module): """ Conditional Attention Module (CoAM) block. """ - def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): + + def __init__( + self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False + ): super(CoAMBlock, self).__init__() self.att_layers = [] self.spat_dims = spat_dims self.cond_enc = cond_enc d_cond = cond_enc.num_channels for i in range(len(spat_dims)): - att_layer = DAModule(d_model = channel_list[i], - d_cond = d_cond, kernel_size = 3, - H = spat_dims[i][1], W = spat_dims[i][0], - n_heads = n_heads, channel_only = channel_only) + att_layer = DAModule( + d_model=channel_list[i], + d_cond=d_cond, + kernel_size=3, + H=spat_dims[i][1], + W=spat_dims[i][0], + n_heads=n_heads, + channel_only=channel_only, + ) self.att_layers.append(att_layer) self.att_layers = nn.ModuleList(self.att_layers) @@ -43,120 +52,173 @@ def forward(self, y_list, cond_hm): # cond_hm = cond_hm[:,0].unsqueeze(1) # we only want one channel of the heatmap y_list_att = [] for i in range(len(y_list)): - y_att = self.att_layers[i](y_list[i], TF.resize(cond_hm, (self.spat_dims[i][1],self.spat_dims[i][0]))) + y_att = self.att_layers[i]( + y_list[i], + TF.resize(cond_hm, (self.spat_dims[i][1], self.spat_dims[i][0])), + ) y_list_att.append(y_att) return y_list_att # modified from https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/DANet.py class PositionAttentionModule(nn.Module): - def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): + def __init__( + self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False + ): super().__init__() - self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, - padding=(kernel_size-1)//2) - self.pa = ScaledDotProductAttention(in_dim_q = d_model, in_dim_k = d_model, - d_k = d_model, d_v = d_model, h = n_heads) + self.cnn = nn.Conv2d( + d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 + ) + self.pa = ScaledDotProductAttention( + in_dim_q=d_model, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads + ) self.self_att = self_att if not self_att: - self.cnn_cond = nn.Conv2d(d_cond, d_cond, kernel_size=kernel_size, padding=(kernel_size-1)//2) - self.pa = ScaledDotProductAttention(in_dim_q = d_cond, in_dim_k = d_model, - d_k = d_model, d_v = d_model, h = n_heads) - - def forward(self,x,cond=None): - bs,c,h,w = x.shape + self.cnn_cond = nn.Conv2d( + d_cond, d_cond, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 + ) + self.pa = ScaledDotProductAttention( + in_dim_q=d_cond, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads + ) + + def forward(self, x, cond=None): + bs, c, h, w = x.shape y = self.cnn(x) - y = y.view(bs,c,-1).permute(0,2,1) #bs,h*w,c + y = y.view(bs, c, -1).permute(0, 2, 1) # bs,h*w,c if not self.self_att: - _,c_cond,_,_ = cond.shape + _, c_cond, _, _ = cond.shape y_cond = self.cnn_cond(cond) - y_cond = y_cond.view(bs,c_cond,-1).permute(0,2,1) - y = self.pa(y_cond, y, y) # bs,h*w,c - + y_cond = y_cond.view(bs, c_cond, -1).permute(0, 2, 1) + y = self.pa(y_cond, y, y) # bs,h*w,c + else: - y = self.pa(y,y,y) - + y = self.pa(y, y, y) + return y -class ChannelAttentionModule(nn.Module): - def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): + +class ChannelAttentionModule(nn.Module): + def __init__( + self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False + ): super().__init__() - self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, padding=(kernel_size-1)//2) + self.cnn = nn.Conv2d( + d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 + ) self.self_att = self_att if not self_att: - self.cnn_cond = nn.Conv2d(d_cond, d_model, kernel_size=kernel_size, padding=(kernel_size-1)//2) - self.pa = SimplifiedScaledDotProductAttention(H*W, h = n_heads) - - def forward(self,x,cond=None): - bs,c,h,w = x.shape + self.cnn_cond = nn.Conv2d( + d_cond, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 + ) + self.pa = SimplifiedScaledDotProductAttention(H * W, h=n_heads) + + def forward(self, x, cond=None): + bs, c, h, w = x.shape y = self.cnn(x) - y = y.view(bs,c,-1) # bs,c,h*w + y = y.view(bs, c, -1) # bs,c,h*w if not self.self_att: y_cond = self.cnn_cond(cond) - y_cond = y_cond.view(bs,c,-1) - y = self.pa(y_cond, y, y) # bs,c_cond,h*w + y_cond = y_cond.view(bs, c, -1) + y = self.pa(y_cond, y, y) # bs,c_cond,h*w else: - y = self.pa(y,y,y) # bs,c,h*w - + y = self.pa(y, y, y) # bs,c,h*w + return y + class DAModule(nn.Module): - def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, channel_only=False): + def __init__( + self, + d_model=512, + d_cond=3, + kernel_size=3, + H=7, + W=7, + n_heads=1, + channel_only=False, + ): super().__init__() self.channel_only = channel_only if not channel_only: - self.position_attention_module=PositionAttentionModule(d_model=d_model, d_cond=d_cond, - kernel_size=kernel_size, H=H, W=W, - n_heads=n_heads) - self.channel_attention_module=ChannelAttentionModule(d_model=d_model, d_cond=d_cond, - kernel_size=kernel_size, H=H, W=W, - n_heads=n_heads) - - def forward(self,input,cond): - - bs,c,h,w = input.shape + self.position_attention_module = PositionAttentionModule( + d_model=d_model, + d_cond=d_cond, + kernel_size=kernel_size, + H=H, + W=W, + n_heads=n_heads, + ) + self.channel_attention_module = ChannelAttentionModule( + d_model=d_model, + d_cond=d_cond, + kernel_size=kernel_size, + H=H, + W=W, + n_heads=n_heads, + ) + + def forward(self, input, cond): + + bs, c, h, w = input.shape c_out = self.channel_attention_module(input, cond) - c_out = c_out.view(bs,c,h,w) + c_out = c_out.view(bs, c, h, w) if self.channel_only: return input * c_out p_out = self.position_attention_module(input, cond) - p_out = p_out.permute(0,2,1).view(bs,c,h,w) - + p_out = p_out.permute(0, 2, 1).view(bs, c, h, w) + return input + (p_out + c_out) + class SelfDAModule(nn.Module): def __init__(self, d_model=512, kernel_size=3, H=7, W=7): super().__init__() - self.position_attention_module=PositionAttentionModule(d_model=d_model, d_cond=None, - kernel_size=kernel_size, H=H, W=W, - self_att=True) - self.channel_attention_module=ChannelAttentionModule(d_model=d_model, d_cond=None, - kernel_size=kernel_size, H=H, W=W, - self_att=True) - - def forward(self,input): - - bs,c,h,w = input.shape + self.position_attention_module = PositionAttentionModule( + d_model=d_model, + d_cond=None, + kernel_size=kernel_size, + H=H, + W=W, + self_att=True, + ) + self.channel_attention_module = ChannelAttentionModule( + d_model=d_model, + d_cond=None, + kernel_size=kernel_size, + H=H, + W=W, + self_att=True, + ) + + def forward(self, input): + + bs, c, h, w = input.shape p_out = self.position_attention_module(input) c_out = self.channel_attention_module(input) - - p_out = p_out.permute(0,2,1).view(bs,c,h,w) - c_out = c_out.view(bs,c,h,w) - + + p_out = p_out.permute(0, 2, 1).view(bs, c, h, w) + c_out = c_out.view(bs, c, h, w) + return p_out + c_out + class SelfAttentionModule_CoAM(nn.Module): def __init__(self, spat_dims, channel_list): super(SelfAttentionModule_CoAM, self).__init__() self.att_layers = [] for i in range(len(spat_dims)): - att_layer = SelfDAModule(d_model = channel_list[i], kernel_size = 3, - H = spat_dims[i][0], W = spat_dims[i][1]) + att_layer = SelfDAModule( + d_model=channel_list[i], + kernel_size=3, + H=spat_dims[i][0], + W=spat_dims[i][1], + ) self.att_layers.append(att_layer) self.att_layers = nn.ModuleList(self.att_layers) @@ -168,19 +230,19 @@ def forward(self, y_list, *args): return y_list_att - # taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SelfAttention.py class ScaledDotProductAttention(nn.Module): - ''' + """ Scaled dot-product attention - ''' - def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=.1, rev=False): - ''' + """ + + def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=0.1, rev=False): + """ :param d_model: Output dimensionality of the model :param d_k: Dimensionality of queries and keys :param d_v: Dimensionality of values :param h: Number of heads - ''' + """ super(ScaledDotProductAttention, self).__init__() # 'rev': condition is key/value and orig. feature map is query @@ -204,7 +266,7 @@ def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=.1, rev=False): def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): - init.kaiming_normal_(m.weight, mode='fan_out') + init.kaiming_normal_(m.weight, mode="fan_out") if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): @@ -215,8 +277,10 @@ def init_weights(self): if m.bias is not None: init.constant_(m.bias, 0) - def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): - ''' + def forward( + self, queries, keys, values, attention_mask=None, attention_weights=None + ): + """ Computes :param queries: Queries (b_s, nq, d_model) :param keys: Keys (b_s, nk, d_model) @@ -224,13 +288,19 @@ def forward(self, queries, keys, values, attention_mask=None, attention_weights= :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). :return: - ''' + """ b_s, nq = queries.shape[:2] nk = keys.shape[1] - q = self.fc_q(queries).view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) - k = self.fc_k(keys).view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) - v = self.fc_v(values).view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) + q = ( + self.fc_q(queries).view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) + ) # (b_s, h, nq, d_k) + k = ( + self.fc_k(keys).view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) + ) # (b_s, h, d_k, nk) + v = ( + self.fc_v(values).view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) + ) # (b_s, h, nk, d_v) att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) if attention_weights is not None: @@ -238,42 +308,47 @@ def forward(self, queries, keys, values, attention_mask=None, attention_weights= if attention_mask is not None: att = att.masked_fill(attention_mask, -np.inf) att = torch.softmax(att, -1) - att=self.dropout(att) - - out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) + att = self.dropout(att) + + out = ( + torch.matmul(att, v) + .permute(0, 2, 1, 3) + .contiguous() + .view(b_s, nq, self.h * self.d_v) + ) # (b_s, nq, h*d_v) out = self.fc_o(out) # (b_s, nq, d_model) return out # taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SimplifiedSelfAttention.py class SimplifiedScaledDotProductAttention(nn.Module): - ''' + """ Scaled dot-product attention - ''' + """ - def __init__(self, d_model, h, dropout=.1): - ''' + def __init__(self, d_model, h, dropout=0.1): + """ :param d_model: Output dimensionality of the model :param d_k: Dimensionality of queries and keys :param d_v: Dimensionality of values :param h: Number of heads - ''' + """ super(SimplifiedScaledDotProductAttention, self).__init__() self.d_model = d_model - self.d_k = d_model//h - self.d_v = d_model//h + self.d_k = d_model // h + self.d_v = d_model // h self.h = h self.fc_o = nn.Linear(h * self.d_v, d_model) - self.dropout=nn.Dropout(dropout) + self.dropout = nn.Dropout(dropout) self.init_weights() def init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): - init.kaiming_normal_(m.weight, mode='fan_out') + init.kaiming_normal_(m.weight, mode="fan_out") if m.bias is not None: init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm2d): @@ -284,8 +359,10 @@ def init_weights(self): if m.bias is not None: init.constant_(m.bias, 0) - def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): - ''' + def forward( + self, queries, keys, values, attention_mask=None, attention_weights=None + ): + """ Computes :param queries: Queries (b_s, nq, d_model) :param keys: Keys (b_s, nk, d_model) @@ -293,13 +370,19 @@ def forward(self, queries, keys, values, attention_mask=None, attention_weights= :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). :return: - ''' + """ b_s, nq = queries.shape[:2] nk = keys.shape[1] - q = queries.view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) - k = keys.view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) - v = values.view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) + q = queries.view(b_s, nq, self.h, self.d_k).permute( + 0, 2, 1, 3 + ) # (b_s, h, nq, d_k) + k = keys.view(b_s, nk, self.h, self.d_k).permute( + 0, 2, 3, 1 + ) # (b_s, h, d_k, nk) + v = values.view(b_s, nk, self.h, self.d_v).permute( + 0, 2, 1, 3 + ) # (b_s, h, nk, d_v) att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) if attention_weights is not None: @@ -307,8 +390,13 @@ def forward(self, queries, keys, values, attention_mask=None, attention_weights= if attention_mask is not None: att = att.masked_fill(attention_mask, -np.inf) att = torch.softmax(att, -1) - att=self.dropout(att) - - out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) + att = self.dropout(att) + + out = ( + torch.matmul(att, v) + .permute(0, 2, 1, 3) + .contiguous() + .view(b_s, nq, self.h * self.d_v) + ) # (b_s, nq, h*d_v) out = self.fc_o(out) # (b_s, nq, d_model) return out From a521ff3c57eeb3b83f69de76dad51827816f6dbb Mon Sep 17 00:00:00 2001 From: LucZot Date: Tue, 18 Mar 2025 18:44:25 +0100 Subject: [PATCH 40/95] add conditional prenet draft --- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 51 +++++++++ .../models/backbones/__init__.py | 1 + .../models/backbones/cond_prenet.py | 102 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml create mode 100644 deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml new file mode 100644 index 0000000000..3155730d85 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -0,0 +1,51 @@ +data: + inference: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 + train: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +model: + backbone: + type: CondPreNet + backbone: + type: HRNet + model_name: hrnet_w32 + freeze_bn_stats: true + freeze_bn_weights: false + interpolate_branches: false + increased_channel_count: false # changes backbone_output_channels to 128 when true + kpt_encoder: + type: ColoredKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + backbone_output_channels: 32 + heads: + bodypart: + type: HeatmapHead + weight_init: normal + predictor: + type: HeatmapPredictor + apply_sigmoid: false + #clip_scores: true + location_refinement: false + target_generator: + type: HeatmapGaussianGenerator + num_heatmaps: "num_bodyparts" + pos_dist_thresh: 17 + heatmap_mode: KEYPOINT + generate_locref: false + criterion: + heatmap: + type: WeightedMSECriterion + weight: 1.0 + heatmap_config: + channels: + - 32 + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts" + kernel_size: 1 diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py index d4c0b3d319..8879d8b244 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py @@ -16,3 +16,4 @@ from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet import HRNet from deeplabcut.pose_estimation_pytorch.models.backbones.resnet import ResNet, DLCRNet from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet_coam import HRNetCoAM +from deeplabcut.pose_estimation_pytorch.models.backbones.cond_prenet import CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py new file mode 100644 index 0000000000..46ac43c476 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -0,0 +1,102 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import torch +import torch.nn as nn +from typing import Dict, Any, List, Union + +from deeplabcut.pose_estimation_pytorch.models.backbones.base import ( + BACKBONES, + BaseBackbone, +) +from deeplabcut.pose_estimation_pytorch.models.modules import ( # ColoredKeypointEncoder,; StackedKeypointEncoder, + BaseKeypointEncoder, + KEYPOINT_ENCODERS, +) + + +@BACKBONES.register_module +class CondPreNet(BaseBackbone): + """ + Wrapper module that adds a conditional preNet before any backbone. + This allows to process image and condition features and prepare them for the main backbone. + """ + + def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | BaseBackbone, **kwargs): + """ + Initialize the PreNetWrapper. + + Args: + backbone: The backbone model to wrap + """ + if not isinstance(backbone, BaseBackbone): + backbone = BACKBONES.build(backbone) + + super().__init__(stride=backbone.stride, **kwargs) + + if not isinstance(kpt_encoder, BaseKeypointEncoder): + kpt_encoder = KEYPOINT_ENCODERS.build(kpt_encoder) + self.cond_enc = kpt_encoder + + self.backbone = backbone + self.rgb_preNet = self._make_preNet(3, input_image=True) + self.cond_preNet = self._make_preNet(3, input_image=False) + + self.init_weights() + + def _make_preNet(self, num_outputs, input_image = False): + if not input_image: # cond + preNet = nn.Sequential( + nn.Conv2d(3, num_outputs, kernel_size=7, stride = 1, padding='same'), + nn.BatchNorm2d(num_outputs)) + else: + preNet = nn.Sequential( + nn.Conv2d(3, 64, kernel_size=3, stride = 1, padding='same'), + nn.BatchNorm2d(64), + nn.Conv2d(64, num_outputs, kernel_size = 7, stride = 1, padding='same'), + nn.BatchNorm2d(num_outputs)) + return preNet + + def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): + """Forward pass through the conditional preNet + backbone. + + Args: + x: Input tensor of shape (batch_size, channels, height, width). + cond_kpts: Conditional keypoints of shape (batch_size, num_joints, 2). + + Returns: + the feature map + """ + + # create conditional heatmap + if isinstance(cond_kpts, torch.Tensor): + cond_kpts = cond_kpts.detach().numpy() + cond_hm = self.cond_enc(cond_kpts.squeeze(1), x.size()[2:]) + cond_hm = torch.from_numpy(cond_hm).float().to(x.device) + cond_hm = cond_hm.permute(0, 3, 1, 2) # (B, C, H, W) + + x0 = self.rgb_preNet(x) + x1 = self.cond_preNet(cond_hm) + x = x0 + x1 + + return self.backbone(x) + + def init_weights(self, pretrained=None): + # Initialize prenet with kaiming initialization + for prenet in [self.rgb_preNet, self.cond_preNet]: + for m in prenet.modules(): + if isinstance(m, nn.Conv2d): + #nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') + nn.init.normal_(m.weight, std=0.001) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.BatchNorm2d): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) From b0dcefd81abf0ac9a26a352c98baf52fe4bf26bb Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 19 Mar 2025 11:18:05 +0100 Subject: [PATCH 41/95] add config files for hrnet+prenet --- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 2 +- .../config/ctd/ctd_prenet_hrnet_w48.yaml | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index 3155730d85..7d4ab8224f 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -17,7 +17,7 @@ model: freeze_bn_weights: false interpolate_branches: false increased_channel_count: false # changes backbone_output_channels to 128 when true - kpt_encoder: + kpt_encoder: type: ColoredKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml new file mode 100644 index 0000000000..fd029e0b84 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -0,0 +1,51 @@ +data: + inference: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 + train: + auto_padding: # Required for HRNet backbones + pad_width_divisor: 32 + pad_height_divisor: 32 +model: + backbone: + type: CondPreNet + backbone: + type: HRNet + model_name: hrnet_w48 + freeze_bn_stats: true + freeze_bn_weights: false + interpolate_branches: false + increased_channel_count: false # changes backbone_output_channels to 128 when true + kpt_encoder: + type: ColoredKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + backbone_output_channels: 48 + heads: + bodypart: + type: HeatmapHead + weight_init: normal + predictor: + type: HeatmapPredictor + apply_sigmoid: false + #clip_scores: true + location_refinement: false + target_generator: + type: HeatmapGaussianGenerator + num_heatmaps: "num_bodyparts" + pos_dist_thresh: 17 + heatmap_mode: KEYPOINT + generate_locref: false + criterion: + heatmap: + type: WeightedMSECriterion + weight: 1.0 + heatmap_config: + channels: + - 48 + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts" + kernel_size: 1 \ No newline at end of file From 86724c46746e0ddaa00c0f3dc99499e9082091c0 Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 19 Mar 2025 14:33:13 +0100 Subject: [PATCH 42/95] add draft stacked keypoint enc --- .../models/backbones/cond_prenet.py | 3 +- .../models/modules/kpt_encoders.py | 59 ++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 46ac43c476..826009433e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -10,7 +10,6 @@ # import torch import torch.nn as nn -from typing import Dict, Any, List, Union from deeplabcut.pose_estimation_pytorch.models.backbones.base import ( BACKBONES, @@ -51,7 +50,7 @@ def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | Bas self.init_weights() - def _make_preNet(self, num_outputs, input_image = False): + def _make_preNet(self, num_outputs, input_image = False): if not input_image: # cond preNet = nn.Sequential( nn.Conv2d(3, num_outputs, kernel_size=7, stride = 1, padding='same'), diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index a312887fd8..88da26e5ff 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -108,33 +108,38 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: the encoded keypoints """ - raise NotImplementedError("StackedKeypointEncoder not implemented yet with batch processing") - - kpts = np.array(keypoints).astype(int) # .reshape(-1, 2).astype(int) - zero_matrix = np.zeros(size) - - def _get_condition_matrix(zero_matrix_, kpt_): - if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: - zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 - return zero_matrix_ - - condition_heatmap_list = [] - for i, kpt in enumerate(kpts): - condition = _get_condition_matrix(zero_matrix, kpt) - condition_heatmap = self.blur_heatmap(condition) - condition_heatmap_list.append(condition_heatmap) - zero_matrix = np.zeros(size) - - # ### debug: visualization -> check conditions - # condition_heatmap = np.expand_dims(condition_heatmap, axis=0) - # condition = np.repeat(condition_heatmap, 3, axis=0) - # print("condition", condition.shape) - # condition = np.transpose(condition, (1, 2, 0)) - # cv2.imwrite(f'/media/data/mu/test/cond_{i}.jpg', condition+image) - # cv2.imwrite(f'/media/data/mu/test/image.jpg', image) - - condition_heatmap_list = np.moveaxis(np.array(condition_heatmap_list), 0, -1) - return condition_heatmap_list + batch_size, _, _ = keypoints.shape + + kpts = keypoints.copy() + kpts[keypoints[..., 2] <= 0] = 0 + kpts = np.nan_to_num(kpts) + oob_mask = out_of_bounds_keypoints(kpts, (256,256)) + if np.sum(oob_mask) > 0: + kpts[oob_mask] = 0 + kpts = kpts.astype(int) + + zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) + + # def _get_condition_matrix(zero_matrix, kpts): + # if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: + # zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 + # return zero_matrix_ + + def _get_condition_matrix(zero_matrix, kpts): + for i, pose in enumerate(kpts): + x, y, vis = pose.T + mask = vis > 0 + x_masked, y_masked = x[mask], y[mask] + zero_matrix[i, y_masked-1, x_masked-1, np.arange(self.num_joints)] = 255 + return zero_matrix + + condition = _get_condition_matrix(zero_matrix, kpts) + + for i in range(batch_size): + condition_heatmap = self.blur_heatmap(condition[i]) + condition[i] = condition_heatmap + + return condition @KEYPOINT_ENCODERS.register_module From c58ddbe0472f5ed93b3ab0151ec2e2b1d09124d1 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 19 Mar 2025 15:56:02 +0100 Subject: [PATCH 43/95] updated code for CrowdPose/COCO OKS sigma --- deeplabcut/core/inferenceutils.py | 3 +++ deeplabcut/core/metrics/api.py | 2 +- deeplabcut/core/metrics/distance_metrics.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py index 64f94f5b00..9f4ec679ec 100644 --- a/deeplabcut/core/inferenceutils.py +++ b/deeplabcut/core/inferenceutils.py @@ -961,6 +961,9 @@ def calc_object_keypoint_similarity( k_squared = (2 * sigma) ** 2 denom = 2 * scale_squared * k_squared + if isinstance(sigma, np.ndarray): + denom = denom[visible_gt] + if symmetric_kpts is None: pred = xy_pred[visible_gt] pred[np.isnan(pred)] = np.inf diff --git a/deeplabcut/core/metrics/api.py b/deeplabcut/core/metrics/api.py index 1805b87180..78c9e38622 100644 --- a/deeplabcut/core/metrics/api.py +++ b/deeplabcut/core/metrics/api.py @@ -24,7 +24,7 @@ def compute_metrics( unique_bodypart_poses: dict[str, np.ndarray] | None = None, pcutoff: float = -1, oks_bbox_margin: int = 0, - oks_sigma: float = 0.1, + oks_sigma: float | np.ndarray = 0.1, per_keypoint_rmse: bool = False, ) -> dict: """Computes pose estimation performance metrics diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py index be6782e8eb..d42bb8cbf5 100644 --- a/deeplabcut/core/metrics/distance_metrics.py +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -21,7 +21,7 @@ def compute_oks_matrix( ground_truth: np.ndarray, predictions: np.ndarray, - oks_sigma: float, + oks_sigma: float | np.ndarray, oks_bbox_margin: float = 0.0, ) -> np.ndarray: """Computes the OKS score for each (prediction, gt) pair in an image @@ -53,7 +53,7 @@ def compute_oks_matrix( def compute_oks( data: list[tuple[np.ndarray, np.ndarray]], oks_bbox_margin: float = 0.0, - oks_sigma: float = 0.1, + oks_sigma: float | np.ndarray = 0.1, oks_thresholds: np.ndarray | None = None, oks_recall_thresholds: np.ndarray | None = None, ) -> dict[str, float]: From 14b3eae2d61cbc2057babbf892d71308951b15f7 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 20 Mar 2025 17:53:16 +0100 Subject: [PATCH 44/95] fix prenet details --- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 5 +++++ .../config/ctd/ctd_prenet_hrnet_w48.yaml | 5 +++++ .../pose_estimation_pytorch/models/backbones/cond_prenet.py | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index 7d4ab8224f..43bbc03b3d 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -3,10 +3,15 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth + #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 +method: ctd model: backbone: type: CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index fd029e0b84..958194c727 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -3,10 +3,15 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth + #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 + bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 +method: ctd model: backbone: type: CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 826009433e..7ed7d6b0b1 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -8,6 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # +import numpy as np import torch import torch.nn as nn @@ -36,8 +37,11 @@ def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | Bas backbone: The backbone model to wrap """ if not isinstance(backbone, BaseBackbone): + backbone['pretrained'] = kwargs.get('pretrained', False) backbone = BACKBONES.build(backbone) + if 'pretrained' in kwargs: + pretrained = kwargs.pop('pretrained') super().__init__(stride=backbone.stride, **kwargs) if not isinstance(kpt_encoder, BaseKeypointEncoder): From a1aff5cc7ae7a57d7e5ab1a32db364bd337d620f Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 20 Mar 2025 17:54:49 +0100 Subject: [PATCH 45/95] unfreeze bn stats of prenet+hrnet --- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 2 +- .../config/ctd/ctd_prenet_hrnet_w48.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index 43bbc03b3d..c7fd6dc28f 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -18,7 +18,7 @@ model: backbone: type: HRNet model_name: hrnet_w32 - freeze_bn_stats: true + freeze_bn_stats: false freeze_bn_weights: false interpolate_branches: false increased_channel_count: false # changes backbone_output_channels to 128 when true diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index 958194c727..2d9eaf1828 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -18,7 +18,7 @@ model: backbone: type: HRNet model_name: hrnet_w48 - freeze_bn_stats: true + freeze_bn_stats: false freeze_bn_weights: false interpolate_branches: false increased_channel_count: false # changes backbone_output_channels to 128 when true From 77ee4cf2a08db8b448266ea5de31abbca466940f Mon Sep 17 00:00:00 2001 From: LucZot Date: Mon, 24 Mar 2025 16:45:01 +0100 Subject: [PATCH 46/95] fix stacked keypoint encoding --- .../models/backbones/cond_prenet.py | 10 +++++----- .../models/modules/kpt_encoders.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 7ed7d6b0b1..b90393939a 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -49,19 +49,19 @@ def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | Bas self.cond_enc = kpt_encoder self.backbone = backbone - self.rgb_preNet = self._make_preNet(3, input_image=True) - self.cond_preNet = self._make_preNet(3, input_image=False) + self.rgb_preNet = self._make_preNet(num_inputs=3, num_outputs=3, input_image=True) + self.cond_preNet = self._make_preNet(num_inputs=self.cond_enc.num_channels, num_outputs=3, input_image=False) self.init_weights() - def _make_preNet(self, num_outputs, input_image = False): + def _make_preNet(self, num_inputs, num_outputs, input_image = False): if not input_image: # cond preNet = nn.Sequential( - nn.Conv2d(3, num_outputs, kernel_size=7, stride = 1, padding='same'), + nn.Conv2d(num_inputs, num_outputs, kernel_size=7, stride = 1, padding='same'), nn.BatchNorm2d(num_outputs)) else: preNet = nn.Sequential( - nn.Conv2d(3, 64, kernel_size=3, stride = 1, padding='same'), + nn.Conv2d(num_inputs, 64, kernel_size=3, stride = 1, padding='same'), nn.BatchNorm2d(64), nn.Conv2d(64, num_outputs, kernel_size = 7, stride = 1, padding='same'), nn.BatchNorm2d(num_outputs)) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 88da26e5ff..1146294b6e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -129,8 +129,8 @@ def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): x, y, vis = pose.T mask = vis > 0 - x_masked, y_masked = x[mask], y[mask] - zero_matrix[i, y_masked-1, x_masked-1, np.arange(self.num_joints)] = 255 + x_masked, y_masked, joint_inds_masked = x[mask], y[mask], np.arange(self.num_joints)[mask] + zero_matrix[i, y_masked-1, x_masked-1, joint_inds_masked] = 255 return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) From d4ed35c163f83d65ef2e8b3fcc7c37eb565daeeb Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 26 Mar 2025 11:16:56 +0100 Subject: [PATCH 47/95] improved loading conditions from files --- .../pose_estimation_pytorch/apis/ctd.py | 205 ++++++++++++++++++ .../apis/evaluation.py | 46 +--- .../pose_estimation_pytorch/data/base.py | 9 +- .../data/cocoloader.py | 9 +- .../pose_estimation_pytorch/data/dlcloader.py | 33 ++- .../apis/test_apis_ctd.py | 140 ++++++++++++ 6 files changed, 385 insertions(+), 57 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/apis/ctd.py create mode 100644 tests/pose_estimation_pytorch/apis/test_apis_ctd.py diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py new file mode 100644 index 0000000000..5e9b68290b --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -0,0 +1,205 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Methods to help with conditional top-down models""" +import json +from pathlib import Path + +import numpy as np +import pandas as pd + + +def load_conditions_h5( + images: list[str], + filepath: str | Path, + path_prefix: str | Path | None = None, +) -> dict[str, np.ndarray]: + """Loads conditions for a model from a pandas DataFrame stored in an HDF file + + The DataFrame must be in the same format as DeepLabCut Predictions: + + ``` + scorer model-name ... + individuals idv0 ... idvM + bodyparts bpt0 ... bptN + coords x y likelihood ... x y likelihood + --------------------------------------------------------------------------------- + (labeled-data, v0, img0.png) 87.0 62.0 0.73 ... 83.2 99.1 0.8326 + ``` + + Args: + images: A list of image paths to load conditions for + filepath: Path to the JSON file containing conditions. + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + A dictionary mapping image paths to condition arrays. Each array has shape + (num_conditions, num_bodyparts, 3). + """ + if path_prefix is not None: + path_prefix = Path(path_prefix) + + df = pd.read_hdf(filepath) + if not isinstance(df, pd.DataFrame): + raise ValueError(f"{filepath} is not a dataframe.") + + num_bodyparts = len(df.columns.get_level_values("bodyparts").unique()) + num_conditions = 1 + if "individuals" in df.columns.names: + num_conditions = len(df.columns.get_level_values("individuals").unique()) + + image_set = set(images) + conditions = {} + for filename, row in df.iterrows(): + if isinstance(filename, tuple): + filename = str(Path(*filename)) + + if path_prefix is not None and filename not in image_set: + filename = str(path_prefix / filename) + + if filename in image_set: + pose = row.to_numpy().reshape((num_conditions, num_bodyparts, 3)) + + # Remove NaNs and set likelihood to 0 for missing keypoints + missing_keypoints = np.any(np.isnan(pose) | (pose < 0), axis=2) + pose[missing_keypoints] = 0 + + # Only keep conditions with at least one visible keypoint + visible_conditions = np.any(~missing_keypoints, axis=1) + if np.sum(visible_conditions) > 0: + pose = pose[visible_conditions] + else: + pose = np.zeros((0, num_bodyparts, 3)) + + conditions[filename] = pose + + missing = image_set.difference(set(conditions.keys())) + if len(missing) > 0: + print( + f"Warning: did not find conditions for {len(missing)} of the {len(images)} " + f"images. Missing conditions:" + ) + for img_path in missing: + print(f" - {img_path}") + + return conditions + + +def load_conditions_json( + images: list[str], + filepath: str | Path, + path_prefix: str | Path | None = None, +) -> dict[str, np.ndarray]: + """Loads conditions for a model from a JSON file. + + The JSON file must contain data in the format: + ``` + { + "img000.png": [ # conditions for image 0 + [ # condition 0 pose + [x, y, score], # keypoint 0 + [x, y, score], # keypoint 1 + ... + [x, y, score], # keypoint N + ], + [ ... ], # condition 1 + ... + [ ... ] # condition M + ], + "img001.png": [...] # conditions for image 1 + } + ``` + + Args: + images: A list of image paths to load conditions for + filepath: Path to the JSON file containing conditions. + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + A dictionary mapping image paths to condition arrays. Each array has shape + (num_conditions, num_bodyparts, 3). + """ + with open(filepath, "r") as f: + conditions = json.load(f) + + if not isinstance(conditions, dict): + raise ValueError( + f"Conditions are expected to be of type dict, got {type(conditions)}. They " + "should be in the format 'labeled-data/video-0/img0000.png' -> " + "list[list[list[float]]], where the list represents an array of shape " + "(num_conditions, num_bodyparts, 3)." + ) + + path_with_prefix_to_key = {} + if path_prefix is not None: + path_with_prefix_to_key = { + str(Path(path_prefix) / k): k for k in conditions.keys() + } + + parsed = {} + missing = [] + for img_path in images: + if img_path in conditions: + pose = np.asarray(conditions[img_path]) + elif img_path in path_with_prefix_to_key: + pose = np.asarray(conditions[path_with_prefix_to_key[img_path]]) + else: + pose = np.zeros((0, 0, 3)) + missing.append(img_path) + + if len(pose) == 0: + pose = np.zeros((0, 0, 3)) + + parsed[img_path] = pose + + if len(missing) > 0: + print( + f"Warning: did not find conditions for {len(missing)} of the {len(images)} " + f"images. Missing conditions:" + ) + for img_path in missing: + print(f" - {img_path}") + + return parsed + + +def load_conditions( + images: list[str], + filepath: str | Path, + path_prefix: str | Path | None = None, +) -> dict[str, np.ndarray]: + """Loads conditions for a model from a file + + Args: + images: A list of image paths to load conditions for + filepath: Path to the file containing conditions. Must be either a JSON (with a + ".json" suffix) or HDF5 file (with a ".h5" suffix). + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + A dictionary mapping image paths to condition arrays. Each array has shape + (num_conditions, num_bodyparts, 3). + """ + suffix = Path(filepath).suffix.lower() + if suffix == ".h5": + return load_conditions_h5(images, filepath, path_prefix) + elif suffix == ".json": + return load_conditions_json(images, filepath, path_prefix) + + raise ValueError( + f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " + f"files. Received {filepath}." + ) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index e50040a4b4..3480cbd0f1 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -21,6 +21,7 @@ from tqdm import tqdm import deeplabcut.core.metrics as metrics +import deeplabcut.pose_estimation_pytorch.apis.ctd as ctd import deeplabcut.pose_estimation_pytorch.apis.prune_paf_graph as prune_paf_graph from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.pose_estimation_pytorch import utils @@ -85,45 +86,12 @@ def predict( ] elif loader.pose_task == Task.CTD: - # Get conditional keypoints for context - import json - with open(loader.model_cfg["data"]["inference"]["conditions"], "r") as f: - conditions = json.load(f) - # { - # "img0000.png": [ (num_conditions, num_bodyparts, 3) - # [ # condition 1 - # [[x, y, score], [x, y, score], ... ] - # ] - # ], - # "img0001.png": [...] - # } - - def _get_condition(image): - if not isinstance(image, Path): - image = Path(image) - video_name, image_name = image.parent.name, image.name - image_key = f"labeled-data/{video_name}/{image_name}" - # image_conditions = np.stack( - # [ - # np.array(c["keypoints"]).reshape((-1, 3)) - # for c in conditions - # if c["image_path"] == image_key - # ] - # ) - if image_key not in conditions: - return np.zeros((0, 0, 3)) - - image_conditions = np.array(conditions[image_key]) - if len(image_conditions) == 0: - return np.zeros((0, 0, 3)) - - image_conditions[:, :, 2] = 1.0 - if image_conditions.shape[1] == 7: - image_conditions = image_conditions[:, [0, 1, 2, 3, 6], :] - - return image_conditions - - context = [{"cond_kpts": _get_condition(image)} for image in image_paths] + # Load conditions for context + conditions_filepath = loader.model_cfg["data"]["inference"]["conditions"] + conditions = ctd.load_conditions( + image_paths, conditions_filepath, path_prefix=loader.image_root, + ) + context = [{"cond_kpts": conditions[image]} for image in image_paths] images_with_context = image_paths if context is not None: diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index cb54b1d97c..196cee58d1 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -46,7 +46,14 @@ class Loader(ABC): Returns a dictionary containing dataset parameters derived from the configuration. """ - def __init__(self, model_config_path: str | Path) -> None: + def __init__( + self, + project_root: str | Path, + image_root: str | Path, + model_config_path: str | Path, + ) -> None: + self.project_root = Path(project_root) + self.image_root = Path(image_root) self.model_config_path = Path(model_config_path) self.model_cfg = config_utils.read_config_as_dict(str(model_config_path)) self.pose_task = Task(self.model_cfg["method"]) diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 0fa4f872bf..80e34f3983 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -49,8 +49,8 @@ def __init__( train_json_filename: str = "train.json", test_json_filename: str = "test.json", ): - super().__init__(Path(model_config_path)) - self.project_root = Path(project_root) + image_root = Path(project_root) / "images" + super().__init__(project_root, image_root, Path(model_config_path)) self.train_json_filename = train_json_filename self.test_json_filename = test_json_filename self._dataset_parameters = None @@ -161,8 +161,7 @@ def validate_categories(coco_json: dict) -> dict: return coco_json - @staticmethod - def validate_images(project_root: str | Path, coco_json: dict) -> dict: + def validate_images(self, coco_json: dict) -> dict: """Goes over images and annotations to look for potential errors This code tries to ensure that training a model on this project does not crash @@ -188,7 +187,7 @@ def validate_images(project_root: str | Path, coco_json: dict) -> dict: if image_filename.is_absolute(): image_path = image_filename else: - image_path = Path(project_root) / "images" / image["file_name"] + image_path = self.image_root / image["file_name"] image["file_name"] = str(image_path) if not image_path.exists(): diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index a766bda296..3cd9230b07 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -68,13 +68,13 @@ def __init__( engine=Engine.PYTORCH, modelprefix=modelprefix, ) - - super().__init__( + model_config_path = ( self._project_root / self._model_folder / "train" / Engine.PYTORCH.pose_cfg_name ) + super().__init__(self._project_root, self._project_root, model_config_path) # lazy-load split and DataFrames self._split: dict[str, list[int]] | None = None @@ -277,27 +277,34 @@ def load_predictions( parameters: PoseDatasetParameters, ) -> pd.DataFrame: if bu_predictions is None: - pred_path = Path(str(bu_snapshot).replace('dlc-models', 'evaluation-results')).parent.parent + pred_path = Path( + str(bu_snapshot).replace("dlc-models", "evaluation-results") + ).parent.parent cfg = af.read_config(pred_path.parent.parent.parent / "config.yaml") scorer = af.get_scorer_name( - cfg=cfg, - shuffle=int(re.search(r'shuffle(\d+)', str(bu_snapshot)).group(1)), - trainFraction=int(re.search(r'trainset(\d+)', str(bu_snapshot)).group(1)) / 100, - engine=Engine.PYTORCH, - trainingsiterations=re.search(r'snapshot-(.+)\.pth', str(bu_snapshot)).group(1), - modelprefix="", + cfg=cfg, + shuffle=int(re.search(r"shuffle(\d+)", str(bu_snapshot)).group(1)), + trainFraction=int( + re.search(r"trainset(\d+)", str(bu_snapshot)).group(1) ) + / 100, + engine=Engine.PYTORCH, + trainingsiterations=re.search( + r"snapshot-(.+)\.pth", str(bu_snapshot) + ).group(1), + modelprefix="", + ) pred_file = pred_path / f"{scorer[0]}.h5" dlc_preds = pd.read_hdf(pred_file, key="df_with_missing") - #FIXME: Implement the case where snapshot is loaded + # FIXME: Implement the case where snapshot is loaded raise NotImplementedError("Need to implement the case with loaded snapshot") else: pred_path = bu_predictions.parent.parent dlc_preds = pd.read_hdf(bu_predictions, key="df_with_missing") - + predictions = {} for idx in dlc_preds.index.unique(): if type(idx) == tuple: @@ -305,7 +312,9 @@ def load_predictions( else: img_path = pred_path.parent.parent / Path(idx) - keypoints = dlc_preds.loc[idx].values.reshape(-1,len(parameters.bodyparts),3)[...,:2] + keypoints = dlc_preds.loc[idx].values.reshape( + -1, len(parameters.bodyparts), 3 + )[..., :2] keypoints = keypoints[~np.isnan(keypoints).all(axis=-1).all(axis=-1)] cond_keypoints = np.zeros((*keypoints.shape[:-1], 3)) cond_keypoints[..., :2] = keypoints diff --git a/tests/pose_estimation_pytorch/apis/test_apis_ctd.py b/tests/pose_estimation_pytorch/apis/test_apis_ctd.py new file mode 100644 index 0000000000..e42a39bba3 --- /dev/null +++ b/tests/pose_estimation_pytorch/apis/test_apis_ctd.py @@ -0,0 +1,140 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +import json +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +import deeplabcut.pose_estimation_pytorch.apis.ctd as ctd + + +CONDITIONS = [ + np.zeros((4, 3, 3)).tolist(), + np.ones((4, 3, 3)).tolist(), + 2 * np.ones((4, 3, 3)).tolist(), + 3 * np.ones((4, 3, 3)).tolist(), +] + + +@pytest.mark.parametrize("path_prefix", ["/a/b", Path("/a/b")]) +@pytest.mark.parametrize( + "data", [ + [("/a/b/c/d.png", "/a/b/c/d.png", CONDITIONS[1])], + [("/a/b/c/d.png", "c/d.png", CONDITIONS[1])], + [ + ("/a/b/c.png", "c.png", CONDITIONS[1]), + ("/a/b/c/d.png", "c/d.png", CONDITIONS[2]), + ("/a/b/c/e.png", "/a/b/c/e.png", CONDITIONS[3]), + ], + ] +) +def test_ctd_load_json_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + data: tuple[list[str], list[str], list], +) -> None: + print("Starting test") + images = [elem[0] for elem in data] + conditions = {key: cond for _, key, cond in data} + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + conditions_filepath = tmp_folder / "conditions.json" + with open(conditions_filepath, "w") as f: + json.dump(conditions, f) + + conditions = ctd.load_conditions_json( + images, conditions_filepath, path_prefix=path_prefix, + ) + for img_path, _, condition in data: + assert img_path in conditions + np.testing.assert_allclose(condition, conditions[img_path]) + + +@pytest.mark.parametrize("path_prefix", ["/p", Path("/p")]) +@pytest.mark.parametrize("num_conditions", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize("num_bodyparts", [1, 2, 3, 5, 10]) +@pytest.mark.parametrize( + "data", [ + [("/p/data/video0/img0.png", ("data", "video0", "img0.png"))], + [("/p/data/video0/img0.png", "data/video0/img0.png")], + [ + ("/p/b/c/d0.png", ("b", "c", "d0.png")), + ("/p/b/c/d1.png", ("b", "c", "d1.png")), + ("/p/b/c/d2.png", ("b", "c", "d2.png")), + ], + [ + ("/p/b/c/d0.png", "b/c/d0.png"), + ("/p/b/c/d1.png", "b/c/d1.png"), + ("/p/b/c/d2.png", "b/c/d2.png"), + ], + ] +) +def test_ctd_load_hdf_containing_rel_paths( + tmp_path_factory, + path_prefix: str | Path, + num_conditions: int, + num_bodyparts: int, + data: tuple[list[str], list[str]], +) -> None: + print("\nStarting test") + num_images = len(data) + images = [img for img, _ in data] + index = [idx for _, idx in data] + if isinstance(index[0], tuple): + index = pd.MultiIndex.from_tuples(index) + + # generate random pose data + size = (num_images, num_conditions, num_bodyparts, 3) + rng = np.random.default_rng(0) + pose = rng.integers(low=0, high=1024, size=size).astype(float) + pose[:, :, :, 2] = rng.random(size=(num_images, num_conditions, num_bodyparts)) + + # set some missing data + is_nans = rng.random(size=size) > 0.8 + pose[is_nans] = np.nan + + # create what the output data will look like + keypoint_mask = np.any(is_nans, axis=3) + output_pose = pose.copy() + output_pose[keypoint_mask] = 0.0 + idv_mask = ~np.all(keypoint_mask, axis=2) + + output_pose = [ + p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) + for p, p_mask in zip(output_pose, idv_mask) + ] + + # generate columns for the dataframe + columns = pd.MultiIndex.from_product( + [ + ["scorer"], + [f"idv{i}" for i in range(num_conditions)], + [f"bpt{i}" for i in range(num_bodyparts)], + ["x", "y", "likelihood"] + ], + names=["scorer", "individuals", "bodyparts", "coords"], + ) + df = pd.DataFrame(data=pose.reshape(num_images, -1), index=index, columns=columns) + + print(df.head()) + + tmp_folder = Path(tmp_path_factory.mktemp("tmp-project")) + conditions_filepath = tmp_folder / "conditions.h5" + df.to_hdf(conditions_filepath, key="df_with_missing") + + conditions = ctd.load_conditions_h5( + images, conditions_filepath, path_prefix=path_prefix, + ) + for idx, (img_path, img_index) in enumerate(data): + assert img_path in conditions + np.testing.assert_allclose(output_pose[idx], conditions[img_path]) From dc7f1f032a88e871a19e95be25a9a1e1cb96f9c0 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 26 Mar 2025 11:19:03 +0100 Subject: [PATCH 48/95] removed debug prints --- deeplabcut/pose_estimation_pytorch/apis/evaluation.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index 3480cbd0f1..4cfb8a3678 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -635,13 +635,7 @@ def evaluate_snapshot( save_rmse_per_bodypart(rmse_per_bodypart, rmse_per_bpt_path, show_errors) if plotting: - # # use BU predictions for plotting - # bu_predictions = pd.read_hdf('/home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5', key='df_with_missing') - # bu_predictions = pd.read_hdf('/home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5', key='df_with_missing') - # predictions = {'train': bu_predictions, 'test': bu_predictions} - folder_name = f"LabeledImages_{scorer}" - #folder_name = f"LabeledImages_{scorer}_individual" folder_path = loader.evaluation_folder / folder_name folder_path.mkdir(parents=True, exist_ok=True) if isinstance(plotting, str): @@ -649,8 +643,6 @@ def evaluate_snapshot( else: plot_mode = "bodypart" - #plot_mode = 'individual' - df_ground_truth = ensure_multianimal_df_format(loader.df) bboxes_cutoff = ( @@ -670,8 +662,6 @@ def evaluate_snapshot( project_root=cfg["project_path"], scorer=cfg["scorer"], model_name=scorer, - #model_name='DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140', - #model_name='DLC_DekrW32_marmosetMay7shuffle1_140', output_folder=str(folder_path), in_train_set=mode == "train", plot_unique_bodyparts=eval_parameters.num_unique_bpts > 0, From 2c9a1b95bcaaabfd56f3d1ff073d959d24a76d9c Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 26 Mar 2025 11:27:17 +0100 Subject: [PATCH 49/95] removed some debug comments --- .../config/base/base.yaml | 3 +-- .../pose_estimation_pytorch/data/dataset.py | 16 ++++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/base/base.yaml b/deeplabcut/pose_estimation_pytorch/config/base/base.yaml index ab1e6c3493..93d751bcf9 100644 --- a/deeplabcut/pose_estimation_pytorch/config/base/base.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/base/base.yaml @@ -5,8 +5,7 @@ runner: gpus: null key_metric: "test.mAP" key_metric_asc: true - #eval_interval: 10 - eval_interval: 3 + eval_interval: 10 optimizer: type: AdamW params: diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index c1c93af89b..c074570420 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -139,9 +139,9 @@ def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: img = self.images[self.img_id_to_index[ann["image_id"]]] near_anns = [] for idx in self.annotation_idx_map[img["id"]]: - # we consider near annotations to be those whose bounding boxes overlap wih the current item - #if idx != index and calc_bbox_overlap(ann['bbox'], self.annotations[idx]['bbox']) > 0: - #HACK: add same annotation as near keypoints so that we don't have empty list + # we consider near annotations to be those whose bounding boxes overlap with + # the current item + # HACK: add same annotation as near keypoints so that we don't have empty list if calc_bbox_overlap(ann['bbox'], self.annotations[idx]['bbox']) > 0: near_anns.append(self.annotations[idx]) return img["file_name"], [ann] + near_anns, img["id"] @@ -243,18 +243,11 @@ def __getitem__(self, index: int) -> dict: keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] - # print('keypoints GT', keypoints) if self.task == Task.CTD: synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] - # synthesized_keypoints[synthesized_keypoints < 0] = 0 - # print('keypoints COND', synthesized_keypoints) - # print('') keypoints = safe_stack([keypoints, synthesized_keypoints[None, ...]], (0, self.parameters.num_joints, 3)) - # from deeplabcut.pose_estimation_pytorch.data.image import plot_keypoints - # plot_keypoints(image, keypoints[0,0], keypoints[1,0], "/home/lucas/logs/debug_images", index) - bboxes = bboxes[:1] bboxes[..., 0] = (bboxes[..., 0] - offsets[0]) / scales[0] bboxes[..., 1] = (bboxes[..., 1] - offsets[1]) / scales[1] @@ -262,8 +255,7 @@ def __getitem__(self, index: int) -> dict: bboxes[..., 3] = bboxes[..., 3] / scales[1] bboxes = np.clip(bboxes, 0, self.parameters.top_down_crop_size[0] - 1) #TODO: clip based on [x,y,x,y]? - # as a RandomBBoxTransform can be added, keypoints may be outside of the - # image after the crop + # RandomBBoxTransform may move keypoints outside the cropped image oob_mask = out_of_bounds_keypoints(keypoints, self.td_crop_size) if np.sum(oob_mask) > 0: keypoints[oob_mask, 2] = 0.0 From e09595049e8c48848d2e0e631e3ec215ba4dbf22 Mon Sep 17 00:00:00 2001 From: LucZot Date: Wed, 26 Mar 2025 11:55:31 +0100 Subject: [PATCH 50/95] small clean up ctd configs --- .../pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml | 5 +---- .../pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml | 5 +---- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 5 +---- .../config/ctd/ctd_prenet_hrnet_w48.yaml | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index f445f92cf6..9d147cc96a 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -3,10 +3,7 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 - bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 - #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 @@ -23,7 +20,7 @@ model: channel_att_only: false att_heads: 1 kpt_encoder: - type: ColoredKeypointEncoder + type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] backbone_output_channels: 32 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index 36b22ebb6a..88b04f3886 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -3,10 +3,7 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 - bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 - #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 @@ -23,7 +20,7 @@ model: channel_att_only: false att_heads: 1 kpt_encoder: - type: ColoredKeypointEncoder + type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] backbone_output_channels: 48 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index c7fd6dc28f..bf2b57aa15 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -3,10 +3,7 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 - bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 - #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 @@ -23,7 +20,7 @@ model: interpolate_branches: false increased_channel_count: false # changes backbone_output_channels to 128 when true kpt_encoder: - type: ColoredKeypointEncoder + type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] backbone_output_channels: 32 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index 2d9eaf1828..3fa8579021 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -3,10 +3,7 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 - bu_snapshot: /home/lucas/datasets/trimice-dlc-2021-06-22/dlc-models-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/train/snapshot-best.pth - #bu_predictions: /home/lucas/datasets/trimice-dlc-2021-06-22/evaluation-results-pytorch/iteration-17/trimiceJun22-trainset95shuffle1/DLC_DekrW32_trimiceJun22shuffle1_best.h5 bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 - #bu_predictions: /home/lucas/datasets/marmoset-dlc-2021-05-07/evaluation-results-pytorch/iteration-19/marmosetMay7-trainset95shuffle1/DLC_DekrW32_marmosetMay7shuffle1_140.h5 train: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 @@ -23,7 +20,7 @@ model: interpolate_branches: false increased_channel_count: false # changes backbone_output_channels to 128 when true kpt_encoder: - type: ColoredKeypointEncoder + type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] backbone_output_channels: 48 From b6d99fe1270632c3ff4ffc91afd42b2be9a51f1b Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Wed, 26 Mar 2025 15:11:30 +0100 Subject: [PATCH 51/95] improved loading conditions --- .../pose_estimation_pytorch/apis/ctd.py | 185 +++++++++++++++--- .../apis/evaluation.py | 12 +- 2 files changed, 162 insertions(+), 35 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index 5e9b68290b..99d3752067 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -15,6 +15,128 @@ import numpy as np import pandas as pd +import deeplabcut.pose_estimation_pytorch.apis.utils as utils +import deeplabcut.pose_estimation_pytorch.data as data +from deeplabcut.pose_estimation_pytorch.task import Task + + +def load_conditions(loader: data.Loader, images: list[str]) -> dict[str, np.ndarray]: + if loader.pose_task != Task.CTD: + raise ValueError(f"Conditions can only be loaded for CTD models") + + condition_cfg = loader.model_cfg["data"].get("conditions") + error_message = ( + f"Misconfigured conditions in the pytorch_config: {condition_cfg}. Valid " + f"examples:\n" + _CONDITION_EXAMPLES + ) + if condition_cfg is None: + raise ValueError(error_message) + + elif isinstance(condition_cfg, str): + return load_conditions_from_file( + images=images, filepath=condition_cfg, path_prefix=loader.image_root + ) + + elif ( + isinstance(loader, data.DLCLoader) + and isinstance(condition_cfg, dict) + and "shuffle" in condition_cfg + ): + # Create a loader for the BU model to use for conditions + shuffle = condition_cfg["shuffle"] + trainset_index = condition_cfg.get("trainset_index", 0) + modelprefix = condition_cfg.get("modelprefix", "") + bu_loader = data.DLCLoader( + loader.project_root / "config.yaml", + trainset_index=trainset_index, + shuffle=shuffle, + modelprefix=modelprefix, + ) + if bu_loader.pose_task != Task.BOTTOM_UP: + raise ValueError( + "Only BU models can be used as conditions for CTD models. Found " + f"shuffle {shuffle} to be {bu_loader.pose_task}. Please select" + "another shuffle as condition." + ) + + # Get the snapshot to use for conditions + snapshots = utils.get_model_snapshots( + "all", bu_loader.model_folder, bu_loader.pose_task + ) + if "snapshot" in condition_cfg: + snapshot_name = condition_cfg["snapshot"] + snapshot_matches = [ + s + for s in snapshots + if (s.path.name == snapshot_name) or (s.path.stem == snapshot_name) + ] + if len(snapshot_matches) == 0: + raise ValueError( + f"Could not find {snapshot_name} for shuffle {shuffle}. Found " + f" {len(snapshots)} snapshots: {[s.path.name for s in snapshots]}" + ) + snapshot = snapshot_matches[0] + elif "snapshot_index" in condition_cfg: + snapshot_index = condition_cfg["snapshot_index"] + snapshot = snapshots[snapshot_index] + else: + snapshot = snapshots[-1] + + bu_scorer = utils.get_scorer_name( + cfg=bu_loader.project_cfg, + shuffle=shuffle, + train_fraction=loader.train_fraction, + snapshot_uid=utils.get_scorer_uid(snapshot, None), + modelprefix=modelprefix, + ) + conditions_filepath = loader.evaluation_folder / f"{bu_scorer}.h5" + if not conditions_filepath.exists(): + raise ValueError( + f"Conditions file {conditions_filepath} does not exist. Please make " + f"sure snapshot {snapshot.path.name} for shuffle {shuffle} was " + "evaluated (which) is when the predictions file is created." + ) + + return load_conditions_from_file( + images=images, filepath=conditions_filepath, path_prefix=loader.image_root + ) + + if isinstance(loader, data.DLCLoader): + error_message += _CONDITION_DLCLOADER_EXAMPLES + + raise ValueError(error_message) + + +def load_conditions_from_file( + images: list[str], + filepath: str | Path, + path_prefix: str | Path | None = None, +) -> dict[str, np.ndarray]: + """Loads conditions for a model from a file + + Args: + images: A list of image paths to load conditions for + filepath: Path to the file containing conditions. Must be either a JSON (with a + ".json" suffix) or HDF5 file (with a ".h5" suffix). + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + A dictionary mapping image paths to condition arrays. Each array has shape + (num_conditions, num_bodyparts, 3). + """ + suffix = Path(filepath).suffix.lower() + if suffix == ".h5": + return load_conditions_h5(images, filepath, path_prefix) + elif suffix == ".json": + return load_conditions_json(images, filepath, path_prefix) + + raise ValueError( + f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " + f"files. Received {filepath}." + ) + def load_conditions_h5( images: list[str], @@ -174,32 +296,37 @@ def load_conditions_json( return parsed -def load_conditions( - images: list[str], - filepath: str | Path, - path_prefix: str | Path | None = None, -) -> dict[str, np.ndarray]: - """Loads conditions for a model from a file - - Args: - images: A list of image paths to load conditions for - filepath: Path to the file containing conditions. Must be either a JSON (with a - ".json" suffix) or HDF5 file (with a ".h5" suffix). - path_prefix: Optional prefix to prepend to image paths when looking up - conditions. This is useful when the paths in the conditions file are - relative but the provided image paths are absolute, or vice versa. - - Returns: - A dictionary mapping image paths to condition arrays. Each array has shape - (num_conditions, num_bodyparts, 3). - """ - suffix = Path(filepath).suffix.lower() - if suffix == ".h5": - return load_conditions_h5(images, filepath, path_prefix) - elif suffix == ".json": - return load_conditions_json(images, filepath, path_prefix) - - raise ValueError( - f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " - f"files. Received {filepath}." - ) +_CONDITION_EXAMPLES = """ +Example: Loading the predictions contained in an h5 file. + ``` + data: + conditions: /path/to/bu_predictions.h5 + ``` +Example: Loading the predictions contained in an json file. + ``` + data: + conditions: /path/to/bu_predictions.json + ``` +""" + +_CONDITION_DLCLOADER_EXAMPLES = """ +Example: Loading the predictions for the default snapshot of shuffle 1. + data: + conditions: + shuffle: 1 + ``` +Example: Loading the predictions for snapshot-250.pt of shuffle 1. + ``` + data: + conditions: + shuffle: 1 + snapshot: snapshot-250.pt + ``` +Example: Loading the predictions for the snapshot with index 2 of shuffle 1. + ``` + data: + conditions: + shuffle: 1 + snapshot_index: 2 + ``` +""" diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index 4cfb8a3678..a66b4803be 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -87,10 +87,7 @@ def predict( elif loader.pose_task == Task.CTD: # Load conditions for context - conditions_filepath = loader.model_cfg["data"]["inference"]["conditions"] - conditions = ctd.load_conditions( - image_paths, conditions_filepath, path_prefix=loader.image_root, - ) + conditions = ctd.load_conditions(loader, image_paths) context = [{"cond_kpts": conditions[image]} for image in image_paths] images_with_context = image_paths @@ -510,7 +507,9 @@ def evaluate_snapshot( head_type = loader.model_cfg["model"]["heads"]["bodypart"]["type"] if head_type == "DLCRNetHead": prune_paf_graph.benchmark_paf_graphs( - loader=loader, snapshot_path=snapshot.path, verbose=False, + loader=loader, + snapshot_path=snapshot.path, + verbose=False, ) parameters = loader.get_dataset_parameters() @@ -572,7 +571,8 @@ def evaluate_snapshot( ), "pcutoff": ( ", ".join([str(v) for v in pcutoff]) - if isinstance(pcutoff, list) else pcutoff + if isinstance(pcutoff, list) + else pcutoff ), } for split in ["train", "test"]: From 4460b63097f30019b649ae825dbf68452a0dd16c Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 15:03:43 +0100 Subject: [PATCH 52/95] add config for rtmpose+prenet --- .../config/ctd/ctd_prenet_rtmpose_m.yaml | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml new file mode 100644 index 0000000000..998a5e6b7b --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml @@ -0,0 +1,105 @@ +data: + inference: + top_down_crop: + width: 256 + height: 256 + bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + train: + random_bbox_transform: + shift_factor: 0.16 + shift_prob: 0.3 + scale_factor: [0.75, 1.25] + scale_prob: 1.0 + p: 1.0 + top_down_crop: + width: 256 + height: 256 +method: ctd +model: + backbone: + type: CondPreNet + backbone: + type: CSPNeXt + model_name: cspnext_m + freeze_bn_stats: false + freeze_bn_weights: false + deepen_factor: 0.67 + widen_factor: 0.75 + kpt_encoder: + type: ColoredKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + backbone_output_channels: 768 + heads: + bodypart: + type: RTMCCHead + weight_init: RTMPose + target_generator: + type: SimCCGenerator + input_size: [256, 256] + smoothing_type: gaussian + sigma: [5.66, 5.66] + simcc_split_ratio: 2.0 + label_smooth_weight: 0.0 + normalize: false + criterion: + x: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + y: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + predictor: + type: SimCCPredictor + simcc_split_ratio: 2.0 + input_size: [256, 256] + in_channels: 768 + out_channels: "num_bodyparts" + in_featuremap_size: [8, 8] # input_size / backbone stride + simcc_split_ratio: 2.0 + final_layer_kernel_size: 7 + gau_cfg: + hidden_dims: 256 + s: 128 + expansion_factor: 2 + dropout_rate: 0 + drop_path: 0.0 + act_fn: "SiLU" + use_rel_bias: false + pos_enc: false +runner: + optimizer: + type: AdamW + params: + lr: 1e-3 + scheduler: + type: SequentialLR + params: + schedulers: + - type: LinearLR + params: + start_factor: 0.001 + end_factor: 1.0 + total_iters: 5 + - type: CosineAnnealingLR + params: + T_max: 200 # max_epochs // 2 + eta_min: 5e-5 # ~base_lr / 20 + - type: LRListScheduler + params: + milestones: + - 0 + lr_list: + - - 5e-5 + milestones: + - 200 # max_epochs // 2 + - 400 +train_settings: + batch_size: 32 + dataloader_workers: 4 + dataloader_pin_memory: false + epochs: 400 From a26d015002ae78ba93808396382eb7900a14f544 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 15:37:22 +0100 Subject: [PATCH 53/95] add img_size to hrnet_coam config --- deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml | 1 + deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 9d147cc96a..9162f69eff 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -23,6 +23,7 @@ model: type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [256, 256] backbone_output_channels: 32 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index 88b04f3886..adb455e5a2 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -23,6 +23,7 @@ model: type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [256, 256] backbone_output_channels: 48 heads: bodypart: From 369048d72d22961f0a12360d4d5c9f78594f1c3a Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Thu, 27 Mar 2025 17:05:26 +0100 Subject: [PATCH 54/95] First implementation CTDInferenceRunner --- .../models/backbones/cond_prenet.py | 5 +- .../runners/inference.py | 130 +++++++++++++++++- 2 files changed, 127 insertions(+), 8 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index b90393939a..dda069a693 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -36,12 +36,11 @@ def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | Bas Args: backbone: The backbone model to wrap """ + pretrained = kwargs.pop("pretrained", False) if not isinstance(backbone, BaseBackbone): - backbone['pretrained'] = kwargs.get('pretrained', False) + backbone["pretrained"] = pretrained backbone = BACKBONES.build(backbone) - if 'pretrained' in kwargs: - pretrained = kwargs.pop('pretrained') super().__init__(stride=backbone.stride, **kwargs) if not isinstance(kpt_encoder, BaseKeypointEncoder): diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 67714088d8..adb114b5f2 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -20,7 +20,7 @@ import deeplabcut.pose_estimation_pytorch.runners.shelving as shelving from deeplabcut.pose_estimation_pytorch.data.postprocessor import Postprocessor -from deeplabcut.pose_estimation_pytorch.data.preprocessor import Preprocessor +from deeplabcut.pose_estimation_pytorch.data.preprocessor import LoadImage, Preprocessor from deeplabcut.pose_estimation_pytorch.models.detectors import BaseDetector from deeplabcut.pose_estimation_pytorch.models.model import PoseModel from deeplabcut.pose_estimation_pytorch.runners.base import ModelType, Runner @@ -76,6 +76,9 @@ def __init__( weights_only=load_weights_only, ) + self.model.to(self.device) + self.model.eval() + self._batch: torch.Tensor | None = None self._model_kwargs: dict[str, np.ndarray | torch.Tensor] = {} @@ -127,9 +130,6 @@ def inference( } ] """ - self.model.to(self.device) - self.model.eval() - results = [] for data in images: self._prepare_inputs(data) @@ -316,6 +316,126 @@ def predict( return predictions +class CTDInferenceRunner(InferenceRunner[PoseModel]): + """Runner for pose estimation inference""" + + def __init__( + self, + model: PoseModel, + bu_runner: PoseInferenceRunner, + **kwargs, + ): + super().__init__(model, **kwargs) + self.bu_runner = bu_runner + self._image_loader = LoadImage() + + if False and self.batch_size != 1: + raise ValueError( + "Dynamic cropping can only be used with batch size 1. Please set " + "your batch size to 1." + ) + + @torch.no_grad() + def inference( + self, + images: ( + Iterable[str | Path | np.ndarray] + | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] + ), + shelf_writer: shelving.ShelfWriter | None = None, + ) -> list[dict[str, np.ndarray]]: + """Run CTD model inference on the given dataset + + Args: + images: the images to run inference on, optionally with context + shelf_writer: by default, data are saved in a list and returned at the end + of inference. Passing a shelf manager writes data to disk on-the-fly + using a "shelf" (a pickle-based, persistent, database-like object by + default, resulting in constant memory footprint). The returned list is + then empty. + + Returns: + a dict containing head predictions for each image + [ + { + "bodypart": {"poses": np.array}, + "unique_bodypart": {"poses": np.array}, + } + ] + """ + results = [] + for data in images: + data = self.add_conditions(data) + self._prepare_inputs(data) + self._process_full_batches() + results += self._extract_results(shelf_writer) + + # Process the last batch even if not full + if self._inputs_waiting_for_processing(): + self._process_batch() + results += self._extract_results(shelf_writer) + + return results + + def predict( + self, inputs: torch.Tensor, **kwargs + ) -> list[dict[str, dict[str, np.ndarray]]]: + """Makes predictions from a model input and output + + Args: + the inputs to the model, of shape (batch_size, ...) + + Returns: + predictions for each of the 'batch_size' inputs, made by each head, e.g. + [ + { + "bodypart": {"poses": np.ndarray}, + "unique_bodypart": {"poses": np.ndarray}, + } + ] + """ + self.bu_runner.model.eval() + outputs = self.model(inputs.to(self.device), **kwargs) + raw_predictions = self.model.get_predictions(outputs) + predictions = [ + { + head: { + pred_name: pred[b].cpu().numpy() + for pred_name, pred in head_outputs.items() + } + for head, head_outputs in raw_predictions.items() + } + for b in range(len(inputs)) + ] + return predictions + + def add_conditions( + self, + data: str | Path | np.ndarray | tuple[str | Path | np.ndarray, dict], + ) -> tuple[torch.Tensor, dict]: + if isinstance(data, (str, Path, np.ndarray)): + inputs, context = data, {} + else: + inputs, context = data + + if self.bu_runner.preprocessor is not None: + inputs, context = self.bu_runner.preprocessor(inputs, context) + else: + inputs = torch.as_tensor(inputs) + + predictions = self.bu_runner.predict(inputs, context=context) + if self.bu_runner.postprocessor is not None: + predictions, _ = self.postprocessor(predictions, context) + + input_image = inputs[0] + context["cond_kpts"] = [ + p for p in predictions["bodypart"]["poses"] + if np.any((p > 0) & ~np.isnan(p), axis=(1, 2)) + ] + + return input_image, context + + class DetectorInferenceRunner(InferenceRunner[BaseDetector]): """Runner for object detection inference""" @@ -360,7 +480,7 @@ def build_inference_runner( task: Task, model: nn.Module, device: str, - snapshot_path: str | Path, + snapshot_path: str | Path | None = None, batch_size: int = 1, preprocessor: Preprocessor | None = None, postprocessor: Postprocessor | None = None, From a9ea34140c64e4c79b08cf016dec7ee0f7e4d361 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 17:33:59 +0100 Subject: [PATCH 55/95] clean cond enc; extend ctd configs; add ctd related funcs to data loading --- .../config/ctd/ctd_coam_w32.yaml | 3 + .../config/ctd/ctd_coam_w48.yaml | 3 + .../config/ctd/ctd_prenet_hrnet_w32.yaml | 3 + .../config/ctd/ctd_prenet_hrnet_w48.yaml | 3 + .../config/ctd/ctd_prenet_rtmpose_m.yaml | 9 +- .../config/ctd/ctd_prenet_rtmpose_x.yaml | 101 ++++++++++++++++++ .../pose_estimation_pytorch/data/base.py | 8 ++ .../pose_estimation_pytorch/data/dataset.py | 21 +++- .../models/modules/kpt_encoders.py | 11 -- 9 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 9162f69eff..30adc54e13 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -8,6 +8,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index adb455e5a2..356c902184 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -8,6 +8,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index bf2b57aa15..9324c49396 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -8,6 +8,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index 3fa8579021..d01d69f699 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -8,6 +8,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml index 998a5e6b7b..2ddbd322b4 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml @@ -5,15 +5,12 @@ data: height: 256 bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 train: - random_bbox_transform: - shift_factor: 0.16 - shift_prob: 0.3 - scale_factor: [0.75, 1.25] - scale_prob: 1.0 - p: 1.0 top_down_crop: width: 256 height: 256 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml new file mode 100644 index 0000000000..bcaa8f306b --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml @@ -0,0 +1,101 @@ +data: + inference: + top_down_crop: + width: 384 + height: 384 + train: + top_down_crop: + width: 384 + height: 384 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] +method: td # Need to add a detector +model: + backbone: + type: CondPreNet + backbone: + type: CSPNeXt + model_name: cspnext_x + freeze_bn_stats: false + freeze_bn_weights: false + deepen_factor: 1.33 + widen_factor: 1.25 + kpt_encoder: + type: ColoredKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + backbone_output_channels: 1280 + heads: + bodypart: + type: RTMCCHead + weight_init: RTMPose + target_generator: + type: SimCCGenerator + input_size: [384, 384] + smoothing_type: gaussian + sigma: [6.93, 6.93] + simcc_split_ratio: 2.0 + label_smooth_weight: 0.0 + normalize: false + criterion: + x: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + y: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + predictor: + type: SimCCPredictor + simcc_split_ratio: 2.0 + input_size: [384, 384] + in_channels: 1280 + out_channels: "num_bodyparts" + in_featuremap_size: [12, 12] # input_size / backbone stride + simcc_split_ratio: 2.0 + final_layer_kernel_size: 7 + gau_cfg: + hidden_dims: 256 + s: 128 + expansion_factor: 2 + dropout_rate: 0 + drop_path: 0.0 + act_fn: "SiLU" + use_rel_bias: false + pos_enc: false +runner: + optimizer: + type: AdamW + params: + lr: 1e-3 + scheduler: + type: SequentialLR + params: + schedulers: + - type: LinearLR + params: + start_factor: 0.001 + end_factor: 1.0 + total_iters: 5 + - type: CosineAnnealingLR + params: + T_max: 200 # max_epochs // 2 + eta_min: 5e-5 # ~base_lr / 20 + - type: LRListScheduler + params: + milestones: + - 0 + lr_list: + - - 5e-5 + milestones: + - 200 # max_epochs // 2 + - 400 +train_settings: + batch_size: 32 + dataloader_workers: 4 + dataloader_pin_memory: false + epochs: 400 diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index 196cee58d1..f7bfcf14f9 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -19,6 +19,7 @@ import deeplabcut.core.config as config_utils import deeplabcut.pose_estimation_pytorch.config as config from deeplabcut.pose_estimation_pytorch.data.dataset import ( + CTDConfig, PoseDataset, PoseDatasetParameters, ) @@ -223,6 +224,12 @@ def create_dataset( parameters = self.get_dataset_parameters() data = self.load_data(mode) data["annotations"] = self.filter_annotations(data["annotations"], task) + if self.pose_task == Task.CTD: + ctd_config = CTDConfig( + self.model_cfg["data"].get("bbox_margin", 25), + self.model_cfg["data"].get("gen_sampling_sigmas", 0.1), + self.model_cfg["data"].get("gen_sampling_symmetries", []) + ) dataset = PoseDataset( images=data["images"], annotations=data["annotations"], @@ -230,6 +237,7 @@ def create_dataset( mode=mode, task=task, parameters=parameters, + ctd_config=ctd_config if self.pose_task == Task.CTD else None, ) return dataset diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index c074570420..dd722e4988 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -33,6 +33,13 @@ from deeplabcut.pose_estimation_pytorch.data.generative_sampling import GenerativeSampler +@dataclass(frozen=True) +class CTDConfig: + bbox_margin: int + gen_sampling_sigmas: float | list[float] = 0.1 + gen_sampling_symmetries: bool = True + + @dataclass(frozen=True) class PoseDatasetParameters: """Parameters for a pose dataset @@ -43,6 +50,7 @@ class PoseDatasetParameters: individuals: the names of individuals with_center_keypoints: whether to compute center keypoints for individuals color_mode: {"RGB", "BGR"} the mode to load images in + ctd_config: for CTD models, the configuration for bbox calculation and error sampling top_down_crop_size: for top-down models, the (width, height) to crop bboxes to top_down_crop_margin: for top-down models, the margin to add around bboxes """ @@ -52,6 +60,7 @@ class PoseDatasetParameters: individuals: list[str] with_center_keypoints: bool = False color_mode: str = "RGB" + ctd_config: CTDConfig | None = None top_down_crop_size: tuple[int, int] | None = None top_down_crop_margin: int | None = None @@ -78,6 +87,7 @@ class PoseDataset(Dataset): transform: A.BaseCompose | None = None mode: str = "train" task: Task = Task.BOTTOM_UP + ctd_config: CTDConfig | None = None def __post_init__(self): self.image_path_id_map = map_image_path_to_id(self.images) @@ -98,7 +108,11 @@ def __post_init__(self): self.td_crop_margin = self.parameters.top_down_crop_margin if self.task == Task.CTD: - self.generative_sampler = GenerativeSampler(self.parameters.num_joints) + self.generative_sampler = GenerativeSampler( + self.parameters.num_joints, + keypoint_sigmas=self.ctd_config.gen_sampling_sigmas, + keypoint_symmetries=self.ctd_config.gen_sampling_symmetries, + ) def __len__(self): # TODO: TD/CTD should only return the number of annotations that aren't unique_bodyparts @@ -222,7 +236,7 @@ def __getitem__(self, index: int) -> dict: synthesized_keypoints, original_size[0], original_size[1], - 25, # FIXME: bbox_margin should be a parameter set in cfg (25 for animals, 5 for humans?) + self.ctd_config.bbox_margin, ) if bboxes[0, 2] == 0 or bboxes[0, 3] == 0: @@ -237,7 +251,8 @@ def __getitem__(self, index: int) -> dict: image, bboxes[0], self.parameters.top_down_crop_size, - margin=0, + #margin=0, + self.parameters.top_down_crop_margin, #TODO: check crop_with_context=(self.task != Task.CTD), ) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 1146294b6e..4488aa9cad 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -120,11 +120,6 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) - # def _get_condition_matrix(zero_matrix, kpts): - # if 0 < kpt_[0] < size[1] and 0 < kpt_[1] < size[0]: - # zero_matrix_[kpt_[1] - 1][kpt_[0] - 1] = 255 - # return zero_matrix_ - def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): x, y, vis = pose.T @@ -188,14 +183,11 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: kpts[oob_mask] = 0 kpts = kpts.astype(int) - #kpts = np.array(keypoints).astype(int) zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): - #x, y = pose.T x, y, vis = pose.T - #mask = (0 < x) & (x < size[1]) & (0 < y) & (y < size[0]) mask = vis > 0 x_masked, y_masked, colors_masked = x[mask], y[mask], self.colors[mask] zero_matrix[i, y_masked-1, x_masked-1] = colors_masked @@ -206,10 +198,8 @@ def _get_condition_matrix_optim(zero_matrix, kpts): mask = (0 < x) & (x < zero_matrix.shape[2]) & (0 < y) & (y < zero_matrix.shape[1]) colors_masked = np.repeat(self.colors[:, None, :], len(zero_matrix), 1) * np.repeat(mask[:, :, None], 3, 2) kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) - #kpt_indices = np.stack([x[mask[:,0]].T, y[mask[:,0]].T]).transpose(1, 2, 0) batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) - #zero_matrix[kpt_input[...,0], kpt_input[...,2], kpt_input[...,1]] = colors_masked.transpose(1,0,2) zero_matrix[kpt_input[...,0], kpt_input[...,2]-1, kpt_input[...,1]-1] = colors_masked.transpose(1,0,2) return zero_matrix @@ -225,7 +215,6 @@ def _get_condition_matrix_optim(zero_matrix, kpts): def get_colors_from_cmap(self, cmap_name, num_colors): cmap = plt.get_cmap(cmap_name) - #colors_float = [cmap(i) for i in range(0, 256, 256 // num_colors)] colors_float = [cmap(i) for i in np.linspace(0, 256, num_colors, dtype=int)] colors = [(int(r*255), int(g*255), int(b*255)) for r, g, b, _ in colors_float] return colors From cf424bad506c1163de4d3e29043c3aa5857b4799 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 17:52:35 +0100 Subject: [PATCH 56/95] add config for humans and fix rtmpose ctd config --- .../config/ctd/ctd_prenet_rtmpose_x.yaml | 2 +- .../ctd/ctd_prenet_rtmpose_x_human.yaml | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml index bcaa8f306b..fb3ee84620 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml @@ -10,7 +10,7 @@ data: bbox_margin: 25 gen_sampling_sigmas: 0.1 gen_sampling_symmetries: [] -method: td # Need to add a detector +method: ctd # Need to add a detector model: backbone: type: CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml new file mode 100644 index 0000000000..46c1ec7b34 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -0,0 +1,101 @@ +data: + inference: + top_down_crop: + width: 288 + height: 384 + train: + top_down_crop: + width: 288 + height: 384 + bbox_margin: 25 + gen_sampling_sigmas: 0.1 + gen_sampling_symmetries: [] +method: ctd # Need to add a detector +model: + backbone: + type: CondPreNet + backbone: + type: CSPNeXt + model_name: cspnext_x + freeze_bn_stats: false + freeze_bn_weights: false + deepen_factor: 1.33 + widen_factor: 1.25 + kpt_encoder: + type: ColoredKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + backbone_output_channels: 1280 + heads: + bodypart: + type: RTMCCHead + weight_init: RTMPose + target_generator: + type: SimCCGenerator + input_size: [288, 384] + smoothing_type: gaussian + sigma: [6., 6.93] + simcc_split_ratio: 2.0 + label_smooth_weight: 0.0 + normalize: false + criterion: + x: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + y: + type: KLDiscreteLoss + use_target_weight: true + beta: 10.0 + label_softmax: true + predictor: + type: SimCCPredictor + simcc_split_ratio: 2.0 + input_size: [288, 384] + in_channels: 1280 + out_channels: "num_bodyparts" + in_featuremap_size: [9, 12] # input_size / backbone stride + simcc_split_ratio: 2.0 + final_layer_kernel_size: 7 + gau_cfg: + hidden_dims: 256 + s: 128 + expansion_factor: 2 + dropout_rate: 0 + drop_path: 0.0 + act_fn: "SiLU" + use_rel_bias: false + pos_enc: false +runner: + optimizer: + type: AdamW + params: + lr: 1e-3 + scheduler: + type: SequentialLR + params: + schedulers: + - type: LinearLR + params: + start_factor: 0.001 + end_factor: 1.0 + total_iters: 5 + - type: CosineAnnealingLR + params: + T_max: 200 # max_epochs // 2 + eta_min: 5e-5 # ~base_lr / 20 + - type: LRListScheduler + params: + milestones: + - 0 + lr_list: + - - 5e-5 + milestones: + - 200 # max_epochs // 2 + - 400 +train_settings: + batch_size: 32 + dataloader_workers: 4 + dataloader_pin_memory: false + epochs: 400 From 3fa99cb691cc71c709247b6f17fde7101cdf02a7 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 17:58:46 +0100 Subject: [PATCH 57/95] fix config human --- .../config/ctd/ctd_prenet_rtmpose_x_human.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index 46c1ec7b34..fb891de0b6 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -7,7 +7,7 @@ data: top_down_crop: width: 288 height: 384 - bbox_margin: 25 + bbox_margin: 5 gen_sampling_sigmas: 0.1 gen_sampling_symmetries: [] method: ctd # Need to add a detector From e568ca55fb6cbdf4bf607f5a01b10f0c39bb4126 Mon Sep 17 00:00:00 2001 From: LucZot Date: Thu, 27 Mar 2025 18:42:50 +0100 Subject: [PATCH 58/95] small fixes --- .../config/ctd/ctd_prenet_rtmpose_x.yaml | 2 +- .../config/ctd/ctd_prenet_rtmpose_x_human.yaml | 2 +- deeplabcut/pose_estimation_pytorch/data/cocoloader.py | 2 +- deeplabcut/pose_estimation_pytorch/data/dataset.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml index fb3ee84620..eca30468ee 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml @@ -10,7 +10,7 @@ data: bbox_margin: 25 gen_sampling_sigmas: 0.1 gen_sampling_symmetries: [] -method: ctd # Need to add a detector +method: ctd model: backbone: type: CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index fb891de0b6..576936594c 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -10,7 +10,7 @@ data: bbox_margin: 5 gen_sampling_sigmas: 0.1 gen_sampling_symmetries: [] -method: ctd # Need to add a detector +method: ctd model: backbone: type: CondPreNet diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 80e34f3983..207791d242 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -247,7 +247,7 @@ def load_data(self, mode: str = "train") -> dict: raise AttributeError(f"Unknown mode: {mode}") data = COCOLoader.validate_categories(data) - data = COCOLoader.validate_images(self.project_root, data) + data = self.validate_images(data) annotations_per_image = {} for annotation in data["annotations"]: diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index dd722e4988..22ea67a3b0 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -111,7 +111,7 @@ def __post_init__(self): self.generative_sampler = GenerativeSampler( self.parameters.num_joints, keypoint_sigmas=self.ctd_config.gen_sampling_sigmas, - keypoint_symmetries=self.ctd_config.gen_sampling_symmetries, + keypoints_symmetry=self.ctd_config.gen_sampling_symmetries, ) def __len__(self): From 7b100ce95269f07a92d6a3317efb51fe603f9277 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 10:06:32 +0100 Subject: [PATCH 59/95] improved CTD model config code --- .../config/ctd/ctd_coam_w32.yaml | 3 + .../config/ctd/ctd_prenet_hrnet_w32.yaml | 1 + .../config/ctd/ctd_prenet_hrnet_w48.yaml | 1 + .../config/ctd/ctd_prenet_rtmpose_m.yaml | 1 + .../config/ctd/ctd_prenet_rtmpose_x.yaml | 1 + .../ctd/ctd_prenet_rtmpose_x_human.yaml | 1 + .../models/backbones/cond_prenet.py | 13 +++- .../models/backbones/hrnet_coam.py | 5 +- .../models/modules/kpt_encoders.py | 71 +++++++++++++------ 9 files changed, 73 insertions(+), 24 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 30adc54e13..23ef009c5b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -8,6 +8,9 @@ data: auto_padding: # Required for HRNet backbones pad_width_divisor: 32 pad_height_divisor: 32 + top_down_crop: + width: 256 + height: 256 bbox_margin: 25 gen_sampling_sigmas: 0.1 gen_sampling_symmetries: [] diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index 9324c49396..31a39cf687 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -26,6 +26,7 @@ model: type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [256, 256] backbone_output_channels: 32 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index d01d69f699..3433188ec8 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -26,6 +26,7 @@ model: type: StackedKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [256, 256] backbone_output_channels: 48 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml index 2ddbd322b4..7fe4606c9f 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml @@ -26,6 +26,7 @@ model: type: ColoredKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [256, 256] backbone_output_channels: 768 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml index eca30468ee..1c81472d05 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml @@ -25,6 +25,7 @@ model: type: ColoredKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + img_size: [384, 384] backbone_output_channels: 1280 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index 576936594c..644fda998d 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -25,6 +25,7 @@ model: type: ColoredKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] + input_size: [384, 288] backbone_output_channels: 1280 heads: bodypart: diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index dda069a693..894cb49e9f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -29,12 +29,19 @@ class CondPreNet(BaseBackbone): This allows to process image and condition features and prepare them for the main backbone. """ - def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | BaseBackbone, **kwargs): + def __init__( + self, + kpt_encoder: dict | BaseKeypointEncoder, + backbone: dict | BaseBackbone, + img_size: tuple[int, int] = (256, 256), + **kwargs, + ): """ Initialize the PreNetWrapper. Args: - backbone: The backbone model to wrap + backbone: The backbone model to wrap. + img_size: The (height, width) of the input images. """ pretrained = kwargs.pop("pretrained", False) if not isinstance(backbone, BaseBackbone): @@ -44,6 +51,8 @@ def __init__(self, kpt_encoder: dict | BaseKeypointEncoder, backbone: dict | Bas super().__init__(stride=backbone.stride, **kwargs) if not isinstance(kpt_encoder, BaseKeypointEncoder): + if "img_size" not in kpt_encoder: + kpt_encoder["img_size"] = img_size kpt_encoder = KEYPOINT_ENCODERS.build(kpt_encoder) self.cond_enc = kpt_encoder diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index fcedfaedb5..15b6780959 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -54,7 +54,7 @@ def __init__( channel_att_only: Whether to use only channel attention block in CoAM. att_heads: Number of attention heads. cond_enc: Type of conditional encoding ('stacked', 'colored', or greyscale). - img_size: Size of the input image. + img_size: The (height, width) size of the input images. num_joints: Number of joints in the dataset. """ @@ -64,7 +64,10 @@ def __init__( self.selfatt_coam_modules = selfatt_coam_modules self.channel_att_only = channel_att_only if not isinstance(kpt_encoder, BaseKeypointEncoder): + if "img_size" not in kpt_encoder: + kpt_encoder["img_size"] = img_size kpt_encoder = KEYPOINT_ENCODERS.build(kpt_encoder) + self.cond_enc = kpt_encoder self.coam_stages = nn.ModuleList([None, None, None, None]) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 4488aa9cad..6e82f6445e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -31,13 +31,21 @@ class BaseKeypointEncoder(ABC): Modified from BUCTD/data/JointsDataset """ - def __init__(self, num_joints: int, kernel_size: tuple[int, int] = (15, 15)) -> None: + def __init__( + self, + num_joints: int, + kernel_size: tuple[int, int] = (15, 15), + img_size: tuple[int, int] = (256, 256), + ) -> None: """ Args: - kernel_size: the Gaussian kernel size to use when blurring a heatmap + num_joints: The number of joints to encode + kernel_size: The Gaussian kernel size to use when blurring a heatmap + img_size: The (height, width) of the input images """ self.kernel_size = kernel_size self.num_joints = num_joints + self.img_size = img_size @property def num_channels(self): @@ -71,9 +79,9 @@ def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: am = np.amax(heatmap) if am == 0: return heatmap - heatmap /= (am / 255) + heatmap /= am / 255 return heatmap - + # def blur_heatmap_batch(self, heatmaps: torch.tensor) -> np.ndarray: # heatmaps = TF.gaussian_blur(heatmaps.permute(0,3,1,2), self.kernel_size).permute(0,2,3,1).numpy() # am = np.amax(heatmaps) @@ -113,7 +121,7 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: kpts = keypoints.copy() kpts[keypoints[..., 2] <= 0] = 0 kpts = np.nan_to_num(kpts) - oob_mask = out_of_bounds_keypoints(kpts, (256,256)) + oob_mask = out_of_bounds_keypoints(kpts, self.img_size) if np.sum(oob_mask) > 0: kpts[oob_mask] = 0 kpts = kpts.astype(int) @@ -124,8 +132,12 @@ def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): x, y, vis = pose.T mask = vis > 0 - x_masked, y_masked, joint_inds_masked = x[mask], y[mask], np.arange(self.num_joints)[mask] - zero_matrix[i, y_masked-1, x_masked-1, joint_inds_masked] = 255 + x_masked, y_masked, joint_inds_masked = ( + x[mask], + y[mask], + np.arange(self.num_joints)[mask], + ) + zero_matrix[i, y_masked - 1, x_masked - 1, joint_inds_masked] = 255 return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) @@ -144,13 +156,17 @@ class ColoredKeypointEncoder(BaseKeypointEncoder): Modified from BUCTD/data/JointsDataset, get_condition_image_colored """ - def __init__(self, **kwargs) -> None: + def __init__( + self, colors: list[tuple[int, int, int]] | None = None, **kwargs + ) -> None: """ Args: colors: the color to use for each keypoint """ super().__init__(**kwargs) - self.colors = np.array(self.get_colors_from_cmap('rainbow', self.num_joints)) + if colors is None: + colors = self.get_colors_from_cmap("rainbow", self.num_joints) + self.colors = np.array(colors) @property def num_channels(self): @@ -174,47 +190,60 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: f"colors, but there are {num_kpts} to encode" ) - #kpts = keypoints.detach().numpy() + # kpts = keypoints.detach().numpy() kpts = keypoints.copy() kpts[keypoints[..., 2] <= 0] = 0 kpts = np.nan_to_num(kpts) - oob_mask = out_of_bounds_keypoints(kpts, (256,256)) + oob_mask = out_of_bounds_keypoints(kpts, (256, 256)) if np.sum(oob_mask) > 0: kpts[oob_mask] = 0 kpts = kpts.astype(int) zero_matrix = np.zeros((batch_size, size[0], size[1], self.num_channels)) - + def _get_condition_matrix(zero_matrix, kpts): for i, pose in enumerate(kpts): x, y, vis = pose.T mask = vis > 0 x_masked, y_masked, colors_masked = x[mask], y[mask], self.colors[mask] - zero_matrix[i, y_masked-1, x_masked-1] = colors_masked + zero_matrix[i, y_masked - 1, x_masked - 1] = colors_masked return zero_matrix - + def _get_condition_matrix_optim(zero_matrix, kpts): x, y = np.array(kpts).T - mask = (0 < x) & (x < zero_matrix.shape[2]) & (0 < y) & (y < zero_matrix.shape[1]) - colors_masked = np.repeat(self.colors[:, None, :], len(zero_matrix), 1) * np.repeat(mask[:, :, None], 3, 2) + mask = ( + (0 < x) + & (x < zero_matrix.shape[2]) + & (0 < y) + & (y < zero_matrix.shape[1]) + ) + colors_masked = np.repeat( + self.colors[:, None, :], len(zero_matrix), 1 + ) * np.repeat(mask[:, :, None], 3, 2) kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) - batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) + batch_indices = np.repeat( + np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1 + ) kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) - zero_matrix[kpt_input[...,0], kpt_input[...,2]-1, kpt_input[...,1]-1] = colors_masked.transpose(1,0,2) + zero_matrix[ + kpt_input[..., 0], kpt_input[..., 2] - 1, kpt_input[..., 1] - 1 + ] = colors_masked.transpose(1, 0, 2) return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) - #condition = _get_condition_matrix_optim(zero_matrix, kpts) + # condition = _get_condition_matrix_optim(zero_matrix, kpts) for i in range(batch_size): condition_heatmap = self.blur_heatmap(condition[i]) condition[i] = condition_heatmap - #condition = self.blur_heatmap_batch(torch.from_numpy(condition)) + # condition = self.blur_heatmap_batch(torch.from_numpy(condition)) return condition def get_colors_from_cmap(self, cmap_name, num_colors): cmap = plt.get_cmap(cmap_name) colors_float = [cmap(i) for i in np.linspace(0, 256, num_colors, dtype=int)] - colors = [(int(r*255), int(g*255), int(b*255)) for r, g, b, _ in colors_float] + colors = [ + (int(r * 255), int(g * 255), int(b * 255)) for r, g, b, _ in colors_float + ] return colors From 4ef5b479cae2ab7dddd6d1a73af944f82b32b1d3 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 10:17:24 +0100 Subject: [PATCH 60/95] implement re-scoring for CTD --- .../data/postprocessor.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py index ee78d55820..f941a86b14 100644 --- a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py @@ -114,7 +114,8 @@ def build_bottom_up_postprocessor( if with_identity: components.append( AssignIndividualIdentities( - identity_key="identity_scores", pose_key="bodyparts", + identity_key="identity_scores", + pose_key="bodyparts", ) ) @@ -310,7 +311,7 @@ def __call__( for name in predictions: output = predictions[name] if len(output) > self.max_individuals[name]: - predictions[name] = output[:self.max_individuals[name]] + predictions[name] = output[: self.max_individuals[name]] return predictions, context @@ -384,6 +385,25 @@ def __call__( rescaled_individuals.append(output_rescaled) rescaled = np.stack(rescaled_individuals) + # rescoring: https://github.com/amathislab/BUCTD/blob/main/lib/dataset/crowdpose.py#L182-L206 + if "cond_kpts" in context: + kpt_scores = rescaled[:, :, 2].copy() + valid_kpt_scores = kpt_scores >= 0.2 + + num_valid_kpts = np.sum(valid_kpt_scores, axis=1) + num_valid_kpts[num_valid_kpts == 0] = 1 + kpt_scores[~valid_kpt_scores] = 0 + kpt_score_sums = np.sum(kpt_scores, axis=1) + idv_scores = kpt_score_sums / num_valid_kpts + + cond_kpt_scores = np.mean( + context["cond_kpts"][:, :, 2], axis=1 + ) + + rescaled[:, :, 2] = (cond_kpt_scores * idv_scores).reshape( + -1, 1 + ) + updated_predictions[name] = rescaled else: updated_predictions[name] = outputs.copy() From d34aece3b58d1210d3a02d7d7581d4ad92159b80 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 10:41:24 +0100 Subject: [PATCH 61/95] add TD aug for CTD models --- .../config/make_pose_config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index 396a431c05..b1df75962c 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -23,6 +23,7 @@ replace_default_values, update_config, ) +from deeplabcut.pose_estimation_pytorch.task import Task from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal @@ -117,8 +118,8 @@ def make_pytorch_pose_config( **default_value_kwargs, ) - is_top_down = model_cfg.get("method", "BU").upper() == "TD" - if is_top_down: + task = Task(model_cfg["method"]) + if task == Task.TOP_DOWN: model_cfg = add_detector( configs_dir, model_cfg, @@ -127,7 +128,7 @@ def make_pytorch_pose_config( ) # add the default augmentations to the config - aug_filename = "aug_top_down.yaml" if is_top_down else "aug_default.yaml" + aug_filename = "aug_default.yaml" if task == Task.BOTTOM_UP else "aug_top_down.yaml" aug_cfg = {"data": read_config_as_dict(configs_dir / "base" / aug_filename)} pose_config = update_config(pose_config, aug_cfg) @@ -140,7 +141,7 @@ def make_pytorch_pose_config( # add a unique bodypart head if needed if len(unique_bpts) > 0: - if is_top_down: + if task != Task.BOTTOM_UP: raise ValueError( f"You selected a top-down model architecture ({net_type}), but you have" f" unique bodyparts, which is not yet implemented for top-down models." @@ -157,7 +158,7 @@ def make_pytorch_pose_config( # add an identity head if needed if with_identity: - if is_top_down: + if task != Task.BOTTOM_UP: raise ValueError( f"You selected a top-down model architecture ({net_type}), but you have" f" set `identity: true`, which is not yet implemented for top-down" From e80bb5b368de7fad4cc62c208e184e1b589361ed Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 12:03:52 +0100 Subject: [PATCH 62/95] fix evaluation and debug print --- deeplabcut/pose_estimation_pytorch/apis/ctd.py | 8 ++++---- .../models/modules/kpt_encoders.py | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index 99d3752067..8758943f92 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -29,6 +29,9 @@ def load_conditions(loader: data.Loader, images: list[str]) -> dict[str, np.ndar f"Misconfigured conditions in the pytorch_config: {condition_cfg}. Valid " f"examples:\n" + _CONDITION_EXAMPLES ) + if isinstance(loader, data.DLCLoader): + error_message += _CONDITION_DLCLOADER_EXAMPLES + if condition_cfg is None: raise ValueError(error_message) @@ -94,16 +97,13 @@ def load_conditions(loader: data.Loader, images: list[str]) -> dict[str, np.ndar raise ValueError( f"Conditions file {conditions_filepath} does not exist. Please make " f"sure snapshot {snapshot.path.name} for shuffle {shuffle} was " - "evaluated (which) is when the predictions file is created." + "evaluated (which is when the predictions file is created)." ) return load_conditions_from_file( images=images, filepath=conditions_filepath, path_prefix=loader.image_root ) - if isinstance(loader, data.DLCLoader): - error_message += _CONDITION_DLCLOADER_EXAMPLES - raise ValueError(error_message) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 6e82f6445e..6fe530e488 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -120,7 +120,11 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: kpts = keypoints.copy() kpts[keypoints[..., 2] <= 0] = 0 + + # Mark keypoints as visible, remove NaNs + kpts[kpts[..., 2] > 0, 2] = 2 kpts = np.nan_to_num(kpts) + oob_mask = out_of_bounds_keypoints(kpts, self.img_size) if np.sum(oob_mask) > 0: kpts[oob_mask] = 0 @@ -193,8 +197,12 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: # kpts = keypoints.detach().numpy() kpts = keypoints.copy() kpts[keypoints[..., 2] <= 0] = 0 + + # Mark keypoints as visible, remove NaNs + kpts[kpts[..., 2] > 0, 2] = 2 kpts = np.nan_to_num(kpts) - oob_mask = out_of_bounds_keypoints(kpts, (256, 256)) + + oob_mask = out_of_bounds_keypoints(kpts, self.img_size) if np.sum(oob_mask) > 0: kpts[oob_mask] = 0 kpts = kpts.astype(int) From 19a3258db277e7fdd72bb77970ebd2cca505bc0d Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 13:34:30 +0100 Subject: [PATCH 63/95] fixes to video inference --- .../data/preprocessor.py | 8 ++--- .../runners/inference.py | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index a053108375..0f228cfa64 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -442,14 +442,14 @@ def __init__(self, cond_kpt_key: str = "cond_kpts") -> None: def __call__( self, image: np.ndarray, context: Context ) -> tuple[np.ndarray, Context]: - - context["model_kwargs"] = {"cond_kpts": context[self.cond_kpt_key]} - cond_keypoints = context[self.cond_kpt_key] + context["model_kwargs"] = {"cond_kpts": cond_keypoints} + if len(cond_keypoints) == 0: + return image, context + rescaled = cond_keypoints.copy() rescaled[..., :2] = ( rescaled[..., :2] - np.array(context["offsets"])[:, None] ) / np.array(context["scales"])[:, None] context["model_kwargs"] = {"cond_kpts": np.expand_dims(rescaled, axis=1)} - return image, context diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index adb114b5f2..ea5338a693 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -327,6 +327,8 @@ def __init__( ): super().__init__(model, **kwargs) self.bu_runner = bu_runner + self.bu_runner.model.eval() + self._image_loader = LoadImage() if False and self.batch_size != 1: @@ -394,7 +396,6 @@ def predict( } ] """ - self.bu_runner.model.eval() outputs = self.model(inputs.to(self.device), **kwargs) raw_predictions = self.model.get_predictions(outputs) predictions = [ @@ -412,28 +413,33 @@ def predict( def add_conditions( self, data: str | Path | np.ndarray | tuple[str | Path | np.ndarray, dict], - ) -> tuple[torch.Tensor, dict]: + ) -> tuple[np.ndarray, dict]: if isinstance(data, (str, Path, np.ndarray)): inputs, context = data, {} else: inputs, context = data + # Load the image once - then given as a numpy array to CTD + image, _ = self._image_loader(inputs, context) + + # Run the pre-processor if self.bu_runner.preprocessor is not None: - inputs, context = self.bu_runner.preprocessor(inputs, context) + inputs, context = self.bu_runner.preprocessor(image, context) else: - inputs = torch.as_tensor(inputs) + inputs = torch.as_tensor(image) - predictions = self.bu_runner.predict(inputs, context=context) + predictions = self.bu_runner.predict(inputs) if self.bu_runner.postprocessor is not None: - predictions, _ = self.postprocessor(predictions, context) + predictions, context = self.bu_runner.postprocessor(predictions, context) - input_image = inputs[0] - context["cond_kpts"] = [ - p for p in predictions["bodypart"]["poses"] - if np.any((p > 0) & ~np.isnan(p), axis=(1, 2)) - ] + conds = predictions["bodyparts"][..., :3] + pred_mask = ~np.all(np.any(conds <= 0 | np.isnan(conds), axis=2), axis=1) + if np.sum(pred_mask) > 0: + conds = conds[pred_mask] + else: + conds = np.zeros((0, conds.shape[1], 3)) - return input_image, context + return image, {"cond_kpts": conds} class DetectorInferenceRunner(InferenceRunner[BaseDetector]): From 5ebcbdce1c98ada2b92e3d12e0863340c4ea3f3f Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 28 Mar 2025 13:41:05 +0100 Subject: [PATCH 64/95] add gen sampling params for crowdpose --- .../config/ctd/ctd_prenet_rtmpose_x_human.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index 576936594c..305fe09ec2 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -8,8 +8,8 @@ data: width: 288 height: 384 bbox_margin: 5 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] + gen_sampling_symmetries: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] # CrowdPose symmetries method: ctd model: backbone: From 86b67043d9b9cdb6a5b5b5d626cfa666d14f6255 Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 28 Mar 2025 13:43:15 +0100 Subject: [PATCH 65/95] small fix config human ctd --- .../config/ctd/ctd_prenet_rtmpose_x_human.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index d60cbe97d0..05a928473e 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -8,7 +8,7 @@ data: width: 288 height: 384 bbox_margin: 5 - gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] + gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] # CrowdPose sigmas gen_sampling_symmetries: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] # CrowdPose symmetries method: ctd model: @@ -25,7 +25,7 @@ model: type: ColoredKeypointEncoder num_joints: "num_bodyparts" kernel_size: [15, 15] - input_size: [384, 288] + img_size: [384, 288] backbone_output_channels: 1280 heads: bodypart: From 6f55b9f8a19bae77acfb628d0865cb24aebadf43 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 13:48:31 +0100 Subject: [PATCH 66/95] bug fix: dataset idv IDs --- deeplabcut/pose_estimation_pytorch/data/dataset.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 22ea67a3b0..e494e4dd24 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -329,12 +329,8 @@ def _prepare_final_annotation_dict( anns: dict, ) -> dict[str, np.ndarray]: num_animals = self.parameters.max_num_animals - individual_ids = anns["individual_id"] if self.task in (Task.TOP_DOWN, Task.CTD): num_animals = 1 - if self.task == Task.CTD: - keypoints = keypoints[0] - individual_ids = anns["individual_id"][:1] bbox_widths = np.maximum(1, bboxes[..., 2]) bbox_heights = np.maximum(1, bboxes[..., 3]) @@ -342,6 +338,11 @@ def _prepare_final_annotation_dict( if "individual_id" not in anns: anns["individual_id"] = -np.ones(len(anns["category_id"]), dtype=int) + individual_ids = anns["individual_id"] + if self.task == Task.CTD: + keypoints = keypoints[0] + individual_ids = individual_ids[:1] + # we use ..., :3 to pass the visibility flag along return { "keypoints": pad_to_length(keypoints[..., :3], num_animals, 0).astype( @@ -351,8 +352,8 @@ def _prepare_final_annotation_dict( "with_center_keypoints": self.parameters.with_center_keypoints, "area": pad_to_length(area, num_animals, 0).astype(np.single), "boxes": pad_to_length(bboxes, num_animals, 0).astype(np.single), - #"is_crowd": pad_to_length(anns["iscrowd"], num_animals, 0).astype(int), - #"labels": pad_to_length(anns["category_id"], num_animals, -1).astype(int), + "is_crowd": pad_to_length(anns["iscrowd"], num_animals, 0).astype(int), + "labels": pad_to_length(anns["category_id"], num_animals, -1).astype(int), "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype(int), } From 5a3f22772b9f82aa994fd5baaa71f48a7257dbb6 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 13:49:43 +0100 Subject: [PATCH 67/95] bug fix: dataset idv IDs --- deeplabcut/pose_estimation_pytorch/data/dataset.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index e494e4dd24..67acee9da8 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -339,9 +339,13 @@ def _prepare_final_annotation_dict( anns["individual_id"] = -np.ones(len(anns["category_id"]), dtype=int) individual_ids = anns["individual_id"] + is_crowd = anns["iscrowd"] + labels = anns["category_id"] if self.task == Task.CTD: keypoints = keypoints[0] individual_ids = individual_ids[:1] + is_crowd = is_crowd[:1] + labels = labels[:1] # we use ..., :3 to pass the visibility flag along return { @@ -352,8 +356,8 @@ def _prepare_final_annotation_dict( "with_center_keypoints": self.parameters.with_center_keypoints, "area": pad_to_length(area, num_animals, 0).astype(np.single), "boxes": pad_to_length(bboxes, num_animals, 0).astype(np.single), - "is_crowd": pad_to_length(anns["iscrowd"], num_animals, 0).astype(int), - "labels": pad_to_length(anns["category_id"], num_animals, -1).astype(int), + "is_crowd": pad_to_length(is_crowd, num_animals, 0).astype(int), + "labels": pad_to_length(labels, num_animals, -1).astype(int), "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype(int), } From edbc25a028d0d94a73f8f85994edea9d0cbf0fbe Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 14:07:10 +0100 Subject: [PATCH 68/95] fix bug when no keypoints are visible --- deeplabcut/pose_estimation_pytorch/data/dataset.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 67acee9da8..7616375b6b 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -242,6 +242,12 @@ def __getitem__(self, index: int) -> dict: if bboxes[0, 2] == 0 or bboxes[0, 3] == 0: # bbox was augmented out of the image; blank image, no keypoints keypoints[..., 2] = 0.0 + if self.task == Task.CTD: + keypoints = safe_stack( + [keypoints, keypoints], + (2, 1, self.parameters.num_joints, 3), + ) + image = np.zeros( (self.td_crop_size[1], self.td_crop_size[0], image.shape[-1]), dtype=image.dtype, @@ -261,7 +267,10 @@ def __getitem__(self, index: int) -> dict: if self.task == Task.CTD: synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] - keypoints = safe_stack([keypoints, synthesized_keypoints[None, ...]], (0, self.parameters.num_joints, 3)) + keypoints = safe_stack( + [keypoints, synthesized_keypoints[None, ...]], + (2, 1, self.parameters.num_joints, 3) + ) bboxes = bboxes[:1] bboxes[..., 0] = (bboxes[..., 0] - offsets[0]) / scales[0] From ab75ce9864bb4b6c7337215f7c15e51f9eda2e29 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 14:09:27 +0100 Subject: [PATCH 69/95] fix dataloader: trim area and bboxes --- deeplabcut/pose_estimation_pytorch/data/dataset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 7616375b6b..836b04065b 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -352,6 +352,8 @@ def _prepare_final_annotation_dict( labels = anns["category_id"] if self.task == Task.CTD: keypoints = keypoints[0] + area = area[:1] + bboxes = bboxes[:1] individual_ids = individual_ids[:1] is_crowd = is_crowd[:1] labels = labels[:1] From 29d966300c8370f648cfb1934fe86aea626092fa Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Fri, 28 Mar 2025 16:20:22 +0100 Subject: [PATCH 70/95] implemented CTD inference --- .../data/preprocessor.py | 1 - .../runners/inference.py | 84 ++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 0f228cfa64..0fb151ff9c 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -443,7 +443,6 @@ def __call__( self, image: np.ndarray, context: Context ) -> tuple[np.ndarray, Context]: cond_keypoints = context[self.cond_kpt_key] - context["model_kwargs"] = {"cond_kpts": cond_keypoints} if len(cond_keypoints) == 0: return image, context diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index ea5338a693..8fd437fba4 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -323,19 +323,22 @@ def __init__( self, model: PoseModel, bu_runner: PoseInferenceRunner, + tracking: bool = True, **kwargs, ): super().__init__(model, **kwargs) self.bu_runner = bu_runner self.bu_runner.model.eval() + self.tracking = tracking self._image_loader = LoadImage() - if False and self.batch_size != 1: - raise ValueError( - "Dynamic cropping can only be used with batch size 1. Please set " - "your batch size to 1." + if self.tracking and self.batch_size != 1: + print( + "Dynamic cropping can only be used with batch size 1. Setting the batch" + " size to 1." ) + self.batch_size = 1 @torch.no_grad() def inference( @@ -365,6 +368,9 @@ def inference( } ] """ + if self.tracking: + return self._inference(images, shelf_writer) + results = [] for data in images: data = self.add_conditions(data) @@ -408,6 +414,7 @@ def predict( } for b in range(len(inputs)) ] + return predictions def add_conditions( @@ -428,10 +435,12 @@ def add_conditions( else: inputs = torch.as_tensor(image) + # Get and post-process the predictions predictions = self.bu_runner.predict(inputs) if self.bu_runner.postprocessor is not None: predictions, context = self.bu_runner.postprocessor(predictions, context) + # Extract the conditions conds = predictions["bodyparts"][..., :3] pred_mask = ~np.all(np.any(conds <= 0 | np.isnan(conds), axis=2), axis=1) if np.sum(pred_mask) > 0: @@ -441,6 +450,73 @@ def add_conditions( return image, {"cond_kpts": conds} + def _inference( + self, + images: ( + Iterable[str | Path | np.ndarray] + | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] + ), + shelf_writer: shelving.ShelfWriter | None = None, + ) -> list[dict[str, np.ndarray]]: + prev_pose = None + + results = [] + for data in images: + inputs, context = self._prepare_ctd_inputs(data, prev_pose) + model_kwargs = context.pop("model_kwargs", {}) + predictions = self.predict(inputs, **model_kwargs) + if self.postprocessor is not None: + # Pop the "cond_kpts" from the context so there's no re-scoring + if prev_pose is not None: + context.pop("cond_kpts") + + predictions, _ = self.postprocessor(predictions, context) + + # Set the predictions as context for the next frame + prev_pose = predictions["bodyparts"][..., :3] + + if shelf_writer is not None: + shelf_writer.add_prediction( + bodyparts=predictions["bodyparts"], + unique_bodyparts=predictions.get("unique_bodyparts"), + identity_scores=predictions.get("identity_scores"), + features=predictions.get("features"), + ) + else: + results.append(predictions) + + return results + + def _prepare_ctd_inputs( + self, + data, + prev_pose: np.ndarray | None, + ) -> tuple[torch.Tensor, dict[str, Any]]: + # Get valid conditions + conds = None + if prev_pose is not None: + bad_data = np.any(prev_pose <= 0 | np.isnan(prev_pose), axis=2) + pred_mask = ~np.all(bad_data | (prev_pose[..., 2] <= 0.25), axis=1) + if np.sum(pred_mask) > 0: + conds = prev_pose[pred_mask] + + # If there's any valid pose, use it as a condition for the next frame + if conds is None: + inputs, context = self.add_conditions(data) + else: + if isinstance(data, (str, Path, np.ndarray)): + inputs, context = data, {} + else: + inputs, context = data + + context["cond_kpts"] = conds + + if self.preprocessor is None: + return torch.as_tensor(inputs), context + + inputs, context = self.preprocessor(inputs, context) + return inputs, context + class DetectorInferenceRunner(InferenceRunner[BaseDetector]): """Runner for object detection inference""" From 9e157bf5b593fbd1ff3698fadeb71c75e9dee98b Mon Sep 17 00:00:00 2001 From: LucZot Date: Fri, 28 Mar 2025 17:05:03 +0100 Subject: [PATCH 71/95] add coam config for crowdpose --- .../config/ctd/ctd_coam_w48_human.yaml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml new file mode 100644 index 0000000000..9cbab9509b --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml @@ -0,0 +1,56 @@ +data: + inference: + top_down_crop: + width: 288 + height: 384 + train: + top_down_crop: + width: 288 + height: 384 + bbox_margin: 5 + gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] # CrowdPose sigmas + gen_sampling_symmetries: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] # CrowdPose symmetries +method: ctd +model: + backbone: + type: HRNetCoAM + base_model_name: hrnet_w48 + pretrained: true + freeze_bn_stats: false + freeze_bn_weights: false + coam_modules: [2,] + channel_att_only: false + att_heads: 1 + kpt_encoder: + type: StackedKeypointEncoder + num_joints: "num_bodyparts" + kernel_size: [15, 15] + img_size: [384, 288] + backbone_output_channels: 48 + heads: + bodypart: + type: HeatmapHead + weight_init: normal + predictor: + type: HeatmapPredictor + apply_sigmoid: false + #clip_scores: true + location_refinement: false + target_generator: + type: HeatmapGaussianGenerator + num_heatmaps: "num_bodyparts" + pos_dist_thresh: 17 + heatmap_mode: KEYPOINT + generate_locref: false + criterion: + heatmap: + type: WeightedMSECriterion + weight: 1.0 + heatmap_config: + channels: + - 48 + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts" + kernel_size: 1 \ No newline at end of file From 43bda999112d8702fa21919f6c78508f13c01176 Mon Sep 17 00:00:00 2001 From: Niels Poulsen Date: Tue, 1 Apr 2025 10:45:56 +0200 Subject: [PATCH 72/95] First implementation of CTD tracking --- .../pose_estimation_pytorch/apis/utils.py | 32 +++++++ .../pose_estimation_pytorch/apis/videos.py | 95 +++++++++++-------- .../data/preprocessor.py | 21 ++-- .../runners/inference.py | 10 ++ 4 files changed, 113 insertions(+), 45 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index 106af642d7..aec4c23d10 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -525,6 +525,7 @@ def get_inference_runners( pose_preprocessor = build_conditional_top_down_preprocessor( color_mode=model_config["data"]["colormode"], transform=transform, + bbox_margin=model_config["data"].get("bbox_margin", 20), top_down_crop_size=(width, height), top_down_crop_margin=margin, top_down_crop_with_context=crop_cfg.get("with_context", False), @@ -657,6 +658,7 @@ def get_pose_inference_runner( max_individuals: int | None = None, transform: A.BaseCompose | None = None, dynamic: DynamicCropper | None = None, + ctd_tracking: bool | dict = False, ) -> PoseInferenceRunner: """Builds an inference runner for pose estimation. @@ -672,6 +674,20 @@ def get_pose_inference_runner( cropping should not be used. Only for bottom-up pose estimation models. Should only be used when creating inference runners for video pose estimation with batch size 1. + ctd_tracking: Only for CTD models. Conditional top-down models can be used + to directly track individuals. Poses from frame T are given as conditions + for frame T+1. This also means a BU model is only needed to "initialize" the + pose in the first frame, and for the remaining frames only the CTD model is + needed. If `True`, the `pytorch_config.yaml` for the model contain the + configuration for conditional pose tracking: + ``` + data: + conditions: + shuffle: 1 + snapshot: snapshot-250.pt + ``` + To use another model or configure conditional pose tracking differently, you + can pass a CTDTrackingConfig instance. Returns: an inference runner for pose estimation @@ -690,6 +706,7 @@ def get_pose_inference_runner( if transform is None: transform = build_transforms(model_config["data"]["inference"]) + kwargs = {} if pose_task == Task.BOTTOM_UP: pose_preprocessor = build_bottom_up_preprocessor( color_mode=model_config["data"]["colormode"], @@ -705,10 +722,24 @@ def get_pose_inference_runner( crop_cfg = model_config["data"]["inference"].get("top_down_crop", {}) width, height = crop_cfg.get("width", 256), crop_cfg.get("height", 256) margin = crop_cfg.get("margin", 0) + if pose_task == Task.CTD: + # FIXME(niels) - allow to load conditions for a video from a file + kwargs["bu_runner"] = get_pose_inference_runner( + model_config=read_config_as_dict(model_config["data"]["config_path"]), + snapshot_path=model_config["data"]["snapshot_path"], + batch_size=1, + device=device, + max_individuals=max_individuals, + ) + + # FIXME(niels) - add configuration for CTD tracking + kwargs["ctd_tracking"] = bool(ctd_tracking) + pose_preprocessor = build_conditional_top_down_preprocessor( color_mode=model_config["data"]["colormode"], transform=transform, + bbox_margin=model_config["data"].get("bbox_margin", 20), top_down_crop_size=(width, height), top_down_crop_margin=margin, top_down_crop_with_context=crop_cfg.get("with_context", False), @@ -738,6 +769,7 @@ def get_pose_inference_runner( postprocessor=pose_postprocessor, dynamic=dynamic, load_weights_only=model_config["runner"].get("load_weights_only", None), + **kwargs, ) if not isinstance(runner, PoseInferenceRunner): raise RuntimeError(f"Failed to build PoseInferenceRunner for {model_config}") diff --git a/deeplabcut/pose_estimation_pytorch/apis/videos.py b/deeplabcut/pose_estimation_pytorch/apis/videos.py index 7d0ce69516..7ee0d1b3bd 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/videos.py +++ b/deeplabcut/pose_estimation_pytorch/apis/videos.py @@ -24,11 +24,14 @@ import deeplabcut.pose_estimation_pytorch.apis.utils as utils import deeplabcut.pose_estimation_pytorch.runners.shelving as shelving -from deeplabcut.core.engine import Engine from deeplabcut.pose_estimation_pytorch.apis.tracklets import ( convert_detections2tracklets, ) -from deeplabcut.pose_estimation_pytorch.runners import InferenceRunner, DynamicCropper +from deeplabcut.pose_estimation_pytorch.data import DLCLoader +from deeplabcut.pose_estimation_pytorch.runners import ( + DynamicCropper, + InferenceRunner, +) from deeplabcut.pose_estimation_pytorch.task import Task from deeplabcut.refine_training_dataset.stitch import stitch_tracklets from deeplabcut.utils import auxiliaryfunctions, VideoReader @@ -223,6 +226,7 @@ def analyze_videos( batch_size: int | None = None, detector_batch_size: int | None = None, dynamic: tuple[bool, float, int] = (False, 0.5, 10), + ctd_tracking: bool | dict = False, modelprefix: str = "", use_shelve: bool = False, robust_nframes: bool = False, @@ -280,6 +284,20 @@ def analyze_videos( is utilized for updating the crop window for the next frame (this is why the margin is important and should be set large enough given the movement of the animal). + ctd_tracking: Only for CTD models. Conditional top-down models can be used + to directly track individuals. Poses from frame T are given as conditions + for frame T+1. This also means a BU model is only needed to "initialize" the + pose in the first frame, and for the remaining frames only the CTD model is + needed. If `True`, the `pytorch_config.yaml` for the model contain the + configuration for conditional pose tracking: + ``` + data: + conditions: + shuffle: 1 + snapshot: snapshot-250.pt + ``` + To use another model or configure conditional pose tracking differently, you + can pass a CTDTrackingConfig instance. modelprefix: directory containing the deeplabcut models to use when evaluating the network. By default, they are assumed to exist in the project folder. batch_size: the batch size to use for inference. Takes the value from the @@ -330,45 +348,41 @@ def analyze_videos( _validate_destfolder(destfolder) # Load the project configuration - cfg = auxiliaryfunctions.read_config(config) - project_path = Path(cfg["project_path"]) - train_fraction = cfg["TrainingFraction"][trainingsetindex] - model_folder = project_path / auxiliaryfunctions.get_model_folder( - train_fraction, - shuffle, - cfg, + loader = DLCLoader( + config, + trainset_index=trainingsetindex, + shuffle=shuffle, modelprefix=modelprefix, - engine=Engine.PYTORCH, ) - train_folder = model_folder / "train" - # Read the inference configuration, load the model - model_cfg_path = train_folder / Engine.PYTORCH.pose_cfg_name - model_cfg = auxiliaryfunctions.read_plainconfig(model_cfg_path) - pose_task = Task(model_cfg["method"]) - - pose_cfg_path = model_folder / "test" / "pose_cfg.yaml" + train_fraction = loader.project_cfg["TrainingFraction"][trainingsetindex] + pose_cfg_path = loader.model_folder.parent / "test" / "pose_cfg.yaml" pose_cfg = auxiliaryfunctions.read_plainconfig(pose_cfg_path) snapshot_index, detector_snapshot_index = utils.parse_snapshot_index_for_analysis( - cfg, model_cfg, snapshot_index, detector_snapshot_index, + loader.project_cfg, loader.model_cfg, snapshot_index, detector_snapshot_index, ) - if cropping is None and cfg.get("cropping", False): - cropping = cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"] + if cropping is None and loader.project_cfg.get("cropping", False): + cropping = ( + loader.project_cfg["x1"], + loader.project_cfg["x2"], + loader.project_cfg["y1"], + loader.project_cfg["y2"], + ) # Get general project parameters - multi_animal = cfg["multianimalproject"] - bodyparts = model_cfg["metadata"]["bodyparts"] - unique_bodyparts = model_cfg["metadata"]["unique_bodyparts"] - individuals = model_cfg["metadata"]["individuals"] + multi_animal = loader.project_cfg["multianimalproject"] + bodyparts = loader.model_cfg["metadata"]["bodyparts"] + unique_bodyparts = loader.model_cfg["metadata"]["unique_bodyparts"] + individuals = loader.model_cfg["metadata"]["individuals"] max_num_animals = len(individuals) if device is not None: - model_cfg["device"] = device + loader.model_cfg["device"] = device if batch_size is None: - batch_size = cfg.get("batch_size", 1) + batch_size = loader.project_cfg.get("batch_size", 1) if not multi_animal: save_as_df = True @@ -380,27 +394,32 @@ def analyze_videos( use_shelve = False dynamic = DynamicCropper.build(*dynamic) - if pose_task != Task.BOTTOM_UP and dynamic is not None: + if loader.pose_task != Task.BOTTOM_UP and dynamic is not None: print( "Turning off dynamic cropping. It should only be used for bottom-up " f"pose estimation models, but you are using a top-down model." ) dynamic = None - snapshot = utils.get_model_snapshots(snapshot_index, train_folder, pose_task)[0] + snapshot = utils.get_model_snapshots( + snapshot_index, loader.model_cfg, loader.pose_task + )[0] print(f"Analyzing videos with {snapshot.path}") pose_runner = utils.get_pose_inference_runner( - model_config=model_cfg, + model_config=loader.model_cfg, snapshot_path=snapshot.path, max_individuals=max_num_animals, batch_size=batch_size, transform=transform, dynamic=dynamic, + ctd_tracking=ctd_tracking, ) - detector_runner = None + # FIXME(niels) - when ctd_tracking is true, save a "_ctd" track file! + + detector_runner = None detector_path, detector_snapshot = None, None - if pose_task == Task.TOP_DOWN: + if loader.pose_task == Task.TOP_DOWN: if detector_snapshot_index is None: raise ValueError( "Cannot run videos analysis for top-down models without a detector " @@ -409,21 +428,21 @@ def analyze_videos( ) if detector_batch_size is None: - detector_batch_size = cfg.get("detector_batch_size", 1) + detector_batch_size = loader.project_cfg.get("detector_batch_size", 1) detector_snapshot = utils.get_model_snapshots( - detector_snapshot_index, train_folder, Task.DETECT + detector_snapshot_index, loader.model_folder, Task.DETECT )[0] print(f" -> Using detector {detector_snapshot.path}") detector_runner = utils.get_detector_inference_runner( - model_config=model_cfg, + model_config=loader.model_cfg, snapshot_path=detector_snapshot.path, max_individuals=max_num_animals, batch_size=detector_batch_size, ) dlc_scorer = utils.get_scorer_name( - cfg, + loader.project_cfg, shuffle, train_fraction, snapshot_uid=utils.get_scorer_uid(snapshot, detector_snapshot), @@ -464,8 +483,8 @@ def analyze_videos( ) runtime.append(time.time()) metadata = _generate_metadata( - cfg=cfg, - pytorch_config=model_cfg, + cfg=loader.project_cfg, + pytorch_config=loader.model_cfg, dlc_scorer=dlc_scorer, train_fraction=train_fraction, batch_size=batch_size, @@ -490,7 +509,7 @@ def analyze_videos( create_df_from_prediction( predictions=predictions, multi_animal=multi_animal, - model_cfg=model_cfg, + model_cfg=loader.model_cfg, dlc_scorer=dlc_scorer, output_path=output_path, output_prefix=output_prefix, diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 0fb151ff9c..8322cda2d6 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -124,6 +124,7 @@ def build_top_down_preprocessor( def build_conditional_top_down_preprocessor( color_mode: str, transform: A.BaseCompose, + bbox_margin: int, top_down_crop_size: tuple[int, int], top_down_crop_margin: int = 0, top_down_crop_with_context: bool = False, @@ -138,6 +139,8 @@ def build_conditional_top_down_preprocessor( Args: color_mode: whether to load the image as an RGB or BGR transform: the transform to apply to the image + bbox_margin: The margin to add around keypoints when generating bounding boxes + from conditional keypoints. top_down_crop_size: the (width, height) to resize cropped bboxes to top_down_crop_margin: the margin to add around detected bboxes for the crop top_down_crop_with_context: whether to keep context when applying the top-down crop @@ -148,7 +151,7 @@ def build_conditional_top_down_preprocessor( return ComposePreprocessor( components=[ LoadImage(color_mode), - ComputeBoundingBoxesFromCondKeypoints(), + ComputeBoundingBoxesFromCondKeypoints(bbox_margin=bbox_margin), TopDownCrop( output_size=top_down_crop_size, margin=top_down_crop_margin, @@ -410,10 +413,16 @@ def __call__( class ComputeBoundingBoxesFromCondKeypoints(Preprocessor): - """TODO""" + """Generates bounding boxes from predicted keypoints - def __init__(self, cond_kpt_key: str = "cond_kpts") -> None: + Args: + cond_kpt_key: The key under which cond. keypoints are stored in the context. + bbox_margin: The margin to add around keypoints when generating bounding boxes. + """ + + def __init__(self, cond_kpt_key: str = "cond_kpts", bbox_margin: int = 0) -> None: self.cond_kpt_key = cond_kpt_key + self.bbox_margin = bbox_margin def __call__( self, image: np.ndarray, context: Context @@ -424,13 +433,11 @@ def __call__( f"Must include cond kpts to ComputeBBoxes, found {context}" ) + h, w = image.shape[:2] context["bboxes"] = [ - # FIXME: bbox_margin should be a parameter set in the configuration (25 for animals, 5 for humans?) - bbox_from_keypoints(cond_kpts, image.shape[0], image.shape[1], 25) + bbox_from_keypoints(cond_kpts, h, w, self.bbox_margin) for cond_kpts in context[self.cond_kpt_key] ] - # context["bboxes"] = np.clip(context["bboxes"], 0, image.shape[0] - 1) - return image, context diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 8fd437fba4..6651f29d4d 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -568,6 +568,7 @@ def build_inference_runner( postprocessor: Postprocessor | None = None, dynamic: DynamicCropper | None = None, load_weights_only: bool | None = None, + **kwargs, ) -> InferenceRunner: """ Build a runner object according to a pytorch configuration file @@ -591,6 +592,7 @@ def build_inference_runner( https://pytorch.org/docs/stable/generated/torch.load.html If None, the default value is used: `deeplabcut.pose_estimation_pytorch.get_load_weights_only()` + **kwargs: Other arguments for the InferenceRunner. Returns: The inference runner. @@ -603,6 +605,7 @@ def build_inference_runner( preprocessor=preprocessor, postprocessor=postprocessor, load_weights_only=load_weights_only, + **kwargs, ) if task == Task.DETECT: if dynamic is not None: @@ -620,4 +623,11 @@ def build_inference_runner( ) dynamic = None + if task == Task.CTD: + # FIXME(niels) - allow running CTD with conditions from a file + if "bu_runner" not in kwargs: + raise ValueError(f"A `bu_runner` must be given for CTD inference.") + + return CTDInferenceRunner(**kwargs) + return PoseInferenceRunner(dynamic=dynamic, **kwargs) From 24b06ba6668a3e3f0d6c8fe2b82d3fba942ae8e5 Mon Sep 17 00:00:00 2001 From: maximpavliv <37336830+maximpavliv@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:48:19 +0200 Subject: [PATCH 73/95] Add `ctd_conditions` argument to `deeplabcut.create_training_dataset()` (#267) --- ...ple_individuals_trainingsetmanipulation.py | 41 +++++++++-- .../trainingsetmanipulation.py | 46 ++++++++++--- .../pose_estimation_pytorch/apis/ctd.py | 2 +- .../config/make_pose_config.py | 68 ++++++++++++++++++- 4 files changed, 141 insertions(+), 16 deletions(-) diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 1095dfef6f..0886a4f60e 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -119,6 +119,7 @@ def create_multianimaltraining_dataset( userfeedback: bool = True, weight_init: WeightInitialization | None = None, engine: Engine | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): """ Creates a training dataset for multi-animal datasets. Labels from all the extracted @@ -158,17 +159,38 @@ def create_multianimaltraining_dataset( * ``efficientnet-b6`` PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` for a complete list) - * ``resnet_50`` - * ``resnet_101`` + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` * ``dekr_w18`` * ``dekr_w32`` * ``dekr_w48`` - * ``top_down_resnet_50`` - * ``top_down_resnet_101`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` * ``top_down_hrnet_w18`` * ``top_down_hrnet_w32`` * ``top_down_hrnet_w48`` - * ``animaltokenpose_base`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` detector_type: string, optional, default=None Only for the PyTorch engine. @@ -233,6 +255,14 @@ def create_multianimaltraining_dataset( the value specified in the project configuration file. If no engine is specified for the project, defaults to ``deeplabcut.compat.DEFAULT_ENGINE``. + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, + If using a conditional-top-down (CTD) net_type, this argument needs to be specified. + It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + Example -------- >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml',num_shuffles=1) @@ -596,6 +626,7 @@ def create_multianimaltraining_dataset( detector_type=detector_type, weight_init=weight_init, save=True, + ctd_conditions=ctd_conditions, ) make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index ea08d46af8..aa2f76254d 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -791,6 +791,7 @@ def create_training_dataset( superanimal_name="", weight_init: WeightInitialization | None = None, engine: Engine | None = None, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): """Creates a training dataset. @@ -841,20 +842,38 @@ def create_training_dataset( * ``efficientnet-b6`` PyTorch (call ``deeplabcut.pose_estimation_pytorch.available_models()`` for a complete list) - * ``resnet_50`` - * ``resnet_101`` - * ``hrnet_w18`` - * ``hrnet_w32`` - * ``hrnet_w48`` + * ``animaltokenpose_base`` + * ``cspnext_m`` + * ``cspnext_s`` + * ``cspnext_x`` + * ``ctd_coam_w32`` + * ``ctd_coam_w48`` + * ``ctd_prenet_hrnet_w32`` + * ``ctd_prenet_hrnet_w48`` + * ``ctd_prenet_rtmpose_m`` + * ``ctd_prenet_rtmpose_x`` + * ``ctd_prenet_rtmpose_x_human`` * ``dekr_w18`` * ``dekr_w32`` * ``dekr_w48`` - * ``top_down_resnet_50`` - * ``top_down_resnet_101`` + * ``dlcrnet_stride16_ms5`` + * ``dlcrnet_stride32_ms5`` + * ``hrnet_w18`` + * ``hrnet_w32`` + * ``hrnet_w48`` + * ``resnet_101`` + * ``resnet_50`` + * ``rtmpose_m`` + * ``rtmpose_s`` + * ``rtmpose_x`` + * ``top_down_cspnext_m`` + * ``top_down_cspnext_s`` + * ``top_down_cspnext_x`` * ``top_down_hrnet_w18`` * ``top_down_hrnet_w32`` * ``top_down_hrnet_w48`` - * ``animaltokenpose_base`` + * ``top_down_resnet_101`` + * ``top_down_resnet_50`` detector_type: string, optional, default=None Only for the PyTorch engine. @@ -900,6 +919,14 @@ def create_training_dataset( the value specified in the project configuration file. If no engine is specified for the project, defaults to ``deeplabcut.compat.DEFAULT_ENGINE``. + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, + If using a conditional-top-down (CTD) net_type, this argument needs to be specified. + It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + Returns ------- list(tuple) or None @@ -985,6 +1012,7 @@ def create_training_dataset( userfeedback=userfeedback, engine=engine, weight_init=weight_init, + ctd_conditions=ctd_conditions, ) else: scorer = cfg["scorer"] @@ -1088,7 +1116,6 @@ def create_training_dataset( Shuffles = validate_shuffles(cfg, Shuffles, num_shuffles, userfeedback) - # print(trainIndices,testIndices, Shuffles, augmenter_type,net_type) if trainIndices is None and testIndices is None: splits = [ ( @@ -1305,6 +1332,7 @@ def create_training_dataset( detector_type=detector_type, weight_init=weight_init, save=True, + ctd_conditions=ctd_conditions, ) make_pytorch_test_config(pytorch_cfg, path_test_config, save=True) diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index 8758943f92..77390b6477 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -92,7 +92,7 @@ def load_conditions(loader: data.Loader, images: list[str]) -> dict[str, np.ndar snapshot_uid=utils.get_scorer_uid(snapshot, None), modelprefix=modelprefix, ) - conditions_filepath = loader.evaluation_folder / f"{bu_scorer}.h5" + conditions_filepath = bu_loader.evaluation_folder / f"{bu_scorer}.h5" if not conditions_filepath.exists(): raise ValueError( f"Conditions file {conditions_filepath} does not exist. Please make " diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index b1df75962c..9e2972b99d 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -35,6 +35,7 @@ def make_pytorch_pose_config( detector_type: str | None = None, weight_init: WeightInitialization | None = None, save: bool = False, + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ) -> dict: """Creates a PyTorch pose configuration file for a DeepLabCut project @@ -67,6 +68,14 @@ def make_pytorch_pose_config( weight_init: Specify how model weights should be initialized. If None, ImageNet pretrained weights from Timm will be loaded when training. save: Whether to save the model configuration file to the ``pose_config_path``. + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, + If using a conditional-top-down (CTD) net_type, this argument needs to be specified. + It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + Returns: the PyTorch pose configuration file @@ -86,6 +95,7 @@ def make_pytorch_pose_config( pose_config["net_type"] = net_type backbones = load_backbones(configs_dir) + add_conditions_to_aug_cfg = False if net_type in backbones: if not top_down and multianimal_project: model_cfg = create_backbone_with_paf_model( @@ -108,6 +118,12 @@ def make_pytorch_pose_config( default_value_kwargs = {} if architecture == "dlcrnet": default_value_kwargs.update(_get_paf_parameters(project_config, bodyparts)) + elif architecture == "ctd": + if ctd_conditions is None: + raise ValueError( + "When using a conditional top down (ctd) architecture, conditions need to be specified." + ) + add_conditions_to_aug_cfg = True cfg_path = configs_dir / architecture / f"{net_type}.yaml" model_cfg = read_config_as_dict(cfg_path) @@ -130,6 +146,8 @@ def make_pytorch_pose_config( # add the default augmentations to the config aug_filename = "aug_default.yaml" if task == Task.BOTTOM_UP else "aug_top_down.yaml" aug_cfg = {"data": read_config_as_dict(configs_dir / "base" / aug_filename)} + if add_conditions_to_aug_cfg: + _add_ctd_conditions(aug_cfg, ctd_conditions) pose_config = update_config(pose_config, aug_cfg) # add the model to the config @@ -182,6 +200,53 @@ def make_pytorch_pose_config( return pose_config +def _add_ctd_conditions( + aug_cfg: dict, ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] +): + """ + Args: + aug_cfg: dict, data augmentation configuration + ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, + If using a conditional-top-down (CTD) net_type, this argument needs to be specified. + It defines the conditions that will be used with the CTD model. + It can be either: + * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + """ + if isinstance(ctd_conditions, int): + conditions = {"shuffle": ctd_conditions} + + elif isinstance(ctd_conditions, str) or isinstance(ctd_conditions, Path): + ctd_conditions = Path(ctd_conditions) + if not ctd_conditions.exist(): + raise FileNotFoundError(f"Invalid path: {ctd_conditions}") + if ctd_conditions.suffix not in (".h5", ".json"): + raise ValueError(f"Invalid conditions file extension.") + conditions = str(ctd_conditions.resolve()) + + elif isinstance(ctd_conditions, tuple): + if len(ctd_conditions) != 2: + raise ValueError(f"Invalid conditions tuple length.") + if not isinstance(ctd_conditions[0], int): + raise TypeError("Conditions shuffle number must be of type int.") + if isinstance(ctd_conditions[1], int): + conditions = { + "shuffle": ctd_conditions[0], + "snapshot_index": ctd_conditions[1], + } + elif isinstance(ctd_conditions[1], str): + conditions = {"shuffle": ctd_conditions[0], "snapshot": ctd_conditions[1]} + else: + raise TypeError( + "Conditions snapshot must be of type int (index) or string (snapshot name)." + ) + else: + raise TypeError("Conditions ctd_conditions is of invalid type.") + + aug_cfg["data"]["conditions"] = conditions + + def make_pytorch_test_config( model_config: dict, test_config_path: str | Path, @@ -440,7 +505,8 @@ def add_detector( read_config_as_dict(configs_dir / "detectors" / f"{detector_type}.yaml"), ) detector_config = replace_default_values( - detector_config, num_individuals=num_individuals, + detector_config, + num_individuals=num_individuals, ) config["detector"] = dict(sorted(detector_config.items())) return config From 2453441f59b2c13770011db60fe7620739cd0f82 Mon Sep 17 00:00:00 2001 From: Niels <45132115+n-poulsen@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:08:02 +0200 Subject: [PATCH 74/95] implement user-friendly CTD inference (#268) --- deeplabcut/core/trackingutils.py | 1 + .../pose_estimation_pytorch/__init__.py | 2 + .../pose_estimation_pytorch/apis/ctd.py | 355 ++++--------- .../apis/evaluation.py | 2 +- .../pose_estimation_pytorch/apis/tracklets.py | 6 + .../pose_estimation_pytorch/apis/utils.py | 38 +- .../pose_estimation_pytorch/apis/videos.py | 120 ++++- .../config/ctd/ctd_coam_w32.yaml | 43 +- .../config/ctd/ctd_coam_w48.yaml | 21 +- .../config/ctd/ctd_coam_w48_human.yaml | 9 +- .../config/ctd/ctd_prenet_hrnet_w32.yaml | 21 +- .../config/ctd/ctd_prenet_hrnet_w48.yaml | 21 +- .../config/ctd/ctd_prenet_rtmpose_m.yaml | 11 +- .../config/ctd/ctd_prenet_rtmpose_x.yaml | 8 +- .../ctd/ctd_prenet_rtmpose_x_human.yaml | 9 +- .../config/make_pose_config.py | 32 +- .../pose_estimation_pytorch/data/__init__.py | 1 + .../pose_estimation_pytorch/data/base.py | 40 +- .../data/cocoloader.py | 2 + .../pose_estimation_pytorch/data/ctd.py | 481 ++++++++++++++++++ .../pose_estimation_pytorch/data/dataset.py | 63 ++- .../pose_estimation_pytorch/data/dlcloader.py | 25 + .../data/generative_sampling.py | 187 ++++--- .../pose_estimation_pytorch/data/snapshots.py | 74 +++ .../models/backbones/cond_prenet.py | 45 +- .../post_processing/nms.py | 92 ++++ .../runners/__init__.py | 1 + .../pose_estimation_pytorch/runners/ctd.py | 85 ++++ .../runners/inference.py | 200 ++++++-- .../runners/snapshots.py | 39 +- .../predict_videos.py | 5 + deeplabcut/refine_training_dataset/stitch.py | 5 + deeplabcut/utils/auxfun_multianimal.py | 3 +- deeplabcut/utils/make_labeled_video.py | 18 +- docs/pytorch/architectures.md | 58 ++- docs/pytorch/assets/bottom-up-approach.png | Bin 0 -> 519568 bytes docs/pytorch/assets/buctd_figure_1.png | Bin 0 -> 279056 bytes docs/pytorch/assets/top-down-approach.png | Bin 0 -> 611425 bytes .../test_data_ctd.py} | 10 +- .../test_postprocessing_nms.py | 107 ++++ 40 files changed, 1645 insertions(+), 595 deletions(-) create mode 100644 deeplabcut/pose_estimation_pytorch/data/ctd.py create mode 100644 deeplabcut/pose_estimation_pytorch/data/snapshots.py create mode 100644 deeplabcut/pose_estimation_pytorch/post_processing/nms.py create mode 100644 deeplabcut/pose_estimation_pytorch/runners/ctd.py create mode 100644 docs/pytorch/assets/bottom-up-approach.png create mode 100644 docs/pytorch/assets/buctd_figure_1.png create mode 100644 docs/pytorch/assets/top-down-approach.png rename tests/pose_estimation_pytorch/{apis/test_apis_ctd.py => data/test_data_ctd.py} (93%) create mode 100644 tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py index 83d8723c10..084bdb4502 100644 --- a/deeplabcut/core/trackingutils.py +++ b/deeplabcut/core/trackingutils.py @@ -28,6 +28,7 @@ TRACK_METHODS = { "box": "_bx", + "ctd": "_ctd", "skeleton": "_sk", "ellipse": "_el", "transformer": "_tr", diff --git a/deeplabcut/pose_estimation_pytorch/__init__.py b/deeplabcut/pose_estimation_pytorch/__init__.py index 999ff14ab6..66c73ef401 100644 --- a/deeplabcut/pose_estimation_pytorch/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/__init__.py @@ -40,9 +40,11 @@ COCOLoader, COLLATE_FUNCTIONS, DLCLoader, + list_snapshots, Loader, PoseDataset, PoseDatasetParameters, + Snapshot, ) from deeplabcut.pose_estimation_pytorch.runners import ( build_inference_runner, diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index 77390b6477..653699fba6 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -9,311 +9,132 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods to help with conditional top-down models""" -import json from pathlib import Path import numpy as np -import pandas as pd -import deeplabcut.pose_estimation_pytorch.apis.utils as utils import deeplabcut.pose_estimation_pytorch.data as data +from deeplabcut.pose_estimation_pytorch.data.ctd import ( + CondFromFile, + CondFromModel, +) from deeplabcut.pose_estimation_pytorch.task import Task -def load_conditions(loader: data.Loader, images: list[str]) -> dict[str, np.ndarray]: - if loader.pose_task != Task.CTD: - raise ValueError(f"Conditions can only be loaded for CTD models") - - condition_cfg = loader.model_cfg["data"].get("conditions") - error_message = ( - f"Misconfigured conditions in the pytorch_config: {condition_cfg}. Valid " - f"examples:\n" + _CONDITION_EXAMPLES - ) - if isinstance(loader, data.DLCLoader): - error_message += _CONDITION_DLCLOADER_EXAMPLES - - if condition_cfg is None: - raise ValueError(error_message) - - elif isinstance(condition_cfg, str): - return load_conditions_from_file( - images=images, filepath=condition_cfg, path_prefix=loader.image_root - ) - - elif ( - isinstance(loader, data.DLCLoader) - and isinstance(condition_cfg, dict) - and "shuffle" in condition_cfg - ): - # Create a loader for the BU model to use for conditions - shuffle = condition_cfg["shuffle"] - trainset_index = condition_cfg.get("trainset_index", 0) - modelprefix = condition_cfg.get("modelprefix", "") - bu_loader = data.DLCLoader( - loader.project_root / "config.yaml", - trainset_index=trainset_index, - shuffle=shuffle, - modelprefix=modelprefix, - ) - if bu_loader.pose_task != Task.BOTTOM_UP: - raise ValueError( - "Only BU models can be used as conditions for CTD models. Found " - f"shuffle {shuffle} to be {bu_loader.pose_task}. Please select" - "another shuffle as condition." - ) - - # Get the snapshot to use for conditions - snapshots = utils.get_model_snapshots( - "all", bu_loader.model_folder, bu_loader.pose_task - ) - if "snapshot" in condition_cfg: - snapshot_name = condition_cfg["snapshot"] - snapshot_matches = [ - s - for s in snapshots - if (s.path.name == snapshot_name) or (s.path.stem == snapshot_name) - ] - if len(snapshot_matches) == 0: - raise ValueError( - f"Could not find {snapshot_name} for shuffle {shuffle}. Found " - f" {len(snapshots)} snapshots: {[s.path.name for s in snapshots]}" - ) - snapshot = snapshot_matches[0] - elif "snapshot_index" in condition_cfg: - snapshot_index = condition_cfg["snapshot_index"] - snapshot = snapshots[snapshot_index] - else: - snapshot = snapshots[-1] - - bu_scorer = utils.get_scorer_name( - cfg=bu_loader.project_cfg, - shuffle=shuffle, - train_fraction=loader.train_fraction, - snapshot_uid=utils.get_scorer_uid(snapshot, None), - modelprefix=modelprefix, - ) - conditions_filepath = bu_loader.evaluation_folder / f"{bu_scorer}.h5" - if not conditions_filepath.exists(): - raise ValueError( - f"Conditions file {conditions_filepath} does not exist. Please make " - f"sure snapshot {snapshot.path.name} for shuffle {shuffle} was " - "evaluated (which is when the predictions file is created)." - ) - - return load_conditions_from_file( - images=images, filepath=conditions_filepath, path_prefix=loader.image_root - ) - - raise ValueError(error_message) - - -def load_conditions_from_file( - images: list[str], - filepath: str | Path, - path_prefix: str | Path | None = None, -) -> dict[str, np.ndarray]: - """Loads conditions for a model from a file +def get_condition_provider( + condition_cfg: dict, + config: str | Path | None = None, +) -> CondFromModel: + """Creates a CondFromModel conditions provider for a CTD model. Args: - images: A list of image paths to load conditions for - filepath: Path to the file containing conditions. Must be either a JSON (with a - ".json" suffix) or HDF5 file (with a ".h5" suffix). - path_prefix: Optional prefix to prepend to image paths when looking up - conditions. This is useful when the paths in the conditions file are - relative but the provided image paths are absolute, or vice versa. + condition_cfg: The configuration for the condition provider. This is the + content of "data": "conditions" in the pytorch_config + config: The path to the project config file, if the condition provider is + given as a snapshot from a DeepLabCut shuffle. Returns: - A dictionary mapping image paths to condition arrays. Each array has shape - (num_conditions, num_bodyparts, 3). + The CondFromModel provider that can be used to generate conditions from a BU + model for a CTD model. """ - suffix = Path(filepath).suffix.lower() - if suffix == ".h5": - return load_conditions_h5(images, filepath, path_prefix) - elif suffix == ".json": - return load_conditions_json(images, filepath, path_prefix) - - raise ValueError( - f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " - f"files. Received {filepath}." + error_message = ( + f"Misconfigured conditions in the pytorch_config: {condition_cfg}. Valid " + f"examples:\n" + _CONDITION_EXAMPLES_INFERENCE ) + if isinstance(condition_cfg, (str, Path)): + error_message = ( + "To run inference with CTD models, you must specify the BU model you " + "want to use to generate conditions.\n" + ) + error_message + raise ValueError(error_message) + elif not isinstance(condition_cfg, dict): + raise ValueError(error_message) -def load_conditions_h5( - images: list[str], - filepath: str | Path, - path_prefix: str | Path | None = None, -) -> dict[str, np.ndarray]: - """Loads conditions for a model from a pandas DataFrame stored in an HDF file + if config is not None: + condition_cfg["config"] = Path(config) + + return CondFromModel(**condition_cfg) - The DataFrame must be in the same format as DeepLabCut Predictions: - ``` - scorer model-name ... - individuals idv0 ... idvM - bodyparts bpt0 ... bptN - coords x y likelihood ... x y likelihood - --------------------------------------------------------------------------------- - (labeled-data, v0, img0.png) 87.0 62.0 0.73 ... 83.2 99.1 0.8326 - ``` +def get_conditions_provider_for_video( + cond_provider: CondFromModel, + video: str | Path, +) -> CondFromFile | None: + """Tries to create a conditions loader Args: - images: A list of image paths to load conditions for - filepath: Path to the JSON file containing conditions. - path_prefix: Optional prefix to prepend to image paths when looking up - conditions. This is useful when the paths in the conditions file are - relative but the provided image paths are absolute, or vice versa. + cond_provider: The CondFromModel condition provider that will be used. The + scorer must be set, or potential conditions files for the video cannot be + found. + video: The path to the video file for which to look for the conditions. Returns: - A dictionary mapping image paths to condition arrays. Each array has shape - (num_conditions, num_bodyparts, 3). + None if no condition files for this BU model and video can be found. + The CondFromFile provider to load the conditions for the video from a file. """ - if path_prefix is not None: - path_prefix = Path(path_prefix) + if cond_provider.scorer is None: + return None - df = pd.read_hdf(filepath) - if not isinstance(df, pd.DataFrame): - raise ValueError(f"{filepath} is not a dataframe.") + video = Path(video) - num_bodyparts = len(df.columns.get_level_values("bodyparts").unique()) - num_conditions = 1 - if "individuals" in df.columns.names: - num_conditions = len(df.columns.get_level_values("individuals").unique()) + # Load pickle for multi-animal projects + cond_file = video.parent / f"{video.stem}{cond_provider.scorer}_full.pickle" + if not cond_file.exists(): - image_set = set(images) - conditions = {} - for filename, row in df.iterrows(): - if isinstance(filename, tuple): - filename = str(Path(*filename)) + # Load h5 for single-animal projects + cond_file = video.parent / f"{video.stem}{cond_provider.scorer}.h5" + if not cond_file.exists(): + return None - if path_prefix is not None and filename not in image_set: - filename = str(path_prefix / filename) + return CondFromFile(filepath=cond_file) - if filename in image_set: - pose = row.to_numpy().reshape((num_conditions, num_bodyparts, 3)) - # Remove NaNs and set likelihood to 0 for missing keypoints - missing_keypoints = np.any(np.isnan(pose) | (pose < 0), axis=2) - pose[missing_keypoints] = 0 - - # Only keep conditions with at least one visible keypoint - visible_conditions = np.any(~missing_keypoints, axis=1) - if np.sum(visible_conditions) > 0: - pose = pose[visible_conditions] - else: - pose = np.zeros((0, num_bodyparts, 3)) - - conditions[filename] = pose - - missing = image_set.difference(set(conditions.keys())) - if len(missing) > 0: - print( - f"Warning: did not find conditions for {len(missing)} of the {len(images)} " - f"images. Missing conditions:" - ) - for img_path in missing: - print(f" - {img_path}") - - return conditions - - -def load_conditions_json( - images: list[str], - filepath: str | Path, - path_prefix: str | Path | None = None, +def load_conditions_for_evaluation( + loader: data.Loader, images: list[str] ) -> dict[str, np.ndarray]: - """Loads conditions for a model from a JSON file. - - The JSON file must contain data in the format: - ``` - { - "img000.png": [ # conditions for image 0 - [ # condition 0 pose - [x, y, score], # keypoint 0 - [x, y, score], # keypoint 1 - ... - [x, y, score], # keypoint N - ], - [ ... ], # condition 1 - ... - [ ... ] # condition M - ], - "img001.png": [...] # conditions for image 1 - } - ``` + """Loads the conditions needed to evaluate a CTD model Args: - images: A list of image paths to load conditions for - filepath: Path to the JSON file containing conditions. - path_prefix: Optional prefix to prepend to image paths when looking up - conditions. This is useful when the paths in the conditions file are - relative but the provided image paths are absolute, or vice versa. + loader: The Loader for the CTD model to evaluate. + images: A list of image paths to load conditions for. Returns: - A dictionary mapping image paths to condition arrays. Each array has shape - (num_conditions, num_bodyparts, 3). + The conditions for the images. """ - with open(filepath, "r") as f: - conditions = json.load(f) - - if not isinstance(conditions, dict): - raise ValueError( - f"Conditions are expected to be of type dict, got {type(conditions)}. They " - "should be in the format 'labeled-data/video-0/img0000.png' -> " - "list[list[list[float]]], where the list represents an array of shape " - "(num_conditions, num_bodyparts, 3)." - ) - - path_with_prefix_to_key = {} - if path_prefix is not None: - path_with_prefix_to_key = { - str(Path(path_prefix) / k): k for k in conditions.keys() - } + if loader.pose_task != Task.CTD: + raise ValueError(f"Conditions can only be loaded for CTD models") - parsed = {} - missing = [] - for img_path in images: - if img_path in conditions: - pose = np.asarray(conditions[img_path]) - elif img_path in path_with_prefix_to_key: - pose = np.asarray(conditions[path_with_prefix_to_key[img_path]]) - else: - pose = np.zeros((0, 0, 3)) - missing.append(img_path) + # load the conditions config + condition_cfg = loader.model_cfg["data"].get("conditions") - if len(pose) == 0: - pose = np.zeros((0, 0, 3)) + # prepare error message + error_message = ( + f"Misconfigured conditions in the pytorch_config: {condition_cfg}. Valid " + f"examples:\n" + _CONDITION_EXAMPLES_INFERENCE + _CONDITION_EXAMPLES_FROM_FILE + ) - parsed[img_path] = pose + if isinstance(condition_cfg, (str, Path)): + condition_filepath = Path(condition_cfg) + cond_provider = CondFromFile(filepath=condition_filepath) + elif isinstance(condition_cfg, dict): + if isinstance(loader, data.DLCLoader) and "config" not in condition_cfg: + condition_cfg["config"] = loader.project_root / "config.yaml" - if len(missing) > 0: - print( - f"Warning: did not find conditions for {len(missing)} of the {len(images)} " - f"images. Missing conditions:" - ) - for img_path in missing: - print(f" - {img_path}") + cond_provider = CondFromFile(**condition_cfg) + else: + raise ValueError(error_message) - return parsed + return cond_provider.load_conditions(images, path_prefix=loader.image_root) -_CONDITION_EXAMPLES = """ -Example: Loading the predictions contained in an h5 file. +_CONDITION_EXAMPLES_INFERENCE = """ +Example: Using a bottom-up model for conditions ``` - data: - conditions: /path/to/bu_predictions.h5 - ``` -Example: Loading the predictions contained in an json file. - ``` - data: - conditions: /path/to/bu_predictions.json - ``` -""" - -_CONDITION_DLCLOADER_EXAMPLES = """ -Example: Loading the predictions for the default snapshot of shuffle 1. data: conditions: - shuffle: 1 + config_path: /path/to/model-dir/pytorch_config.yaml + snapshot_path: /path/to/model-dir/snapshot-best-150.pth ``` Example: Loading the predictions for snapshot-250.pt of shuffle 1. ``` @@ -330,3 +151,17 @@ def load_conditions_json( snapshot_index: 2 ``` """ + + +_CONDITION_EXAMPLES_FROM_FILE = """ +Example: Loading the predictions contained in an h5 file. + ``` + data: + conditions: /path/to/bu_predictions.h5 + ``` +Example: Loading the predictions contained in an json file. + ``` + data: + conditions: /path/to/bu_predictions.json + ``` +""" diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index a66b4803be..3537393772 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -87,7 +87,7 @@ def predict( elif loader.pose_task == Task.CTD: # Load conditions for context - conditions = ctd.load_conditions(loader, image_paths) + conditions = ctd.load_conditions_for_evaluation(loader, image_paths) context = [{"cond_kpts": conditions[image]} for image in image_paths] images_with_context = image_paths diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py index e739d6b432..c2c4c55aa4 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py @@ -83,6 +83,12 @@ def convert_detections2tracklets( if "multi-animal" not in dlc_cfg["dataset_type"]: raise ValueError("This function is only required for multianimal projects!") + if track_method == "ctd": + raise ValueError( + "CTD tracking occurs directly during video analysis. No need to call " + "`convert_detections2tracklets` with `track_method=='ctd'`." + ) + if inference_cfg is None: inference_cfg = auxfun_multianimal.read_inferencecfg( model_dir / "test" / "inference_cfg.yaml", cfg diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index aec4c23d10..bbbe738e07 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -21,6 +21,7 @@ from deeplabcut.core.config import read_config_as_dict from deeplabcut.core.engine import Engine +from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromModel from deeplabcut.pose_estimation_pytorch.data.dataset import PoseDatasetParameters from deeplabcut.pose_estimation_pytorch.data.dlcloader import ( build_dlc_dataframe_columns, @@ -39,6 +40,7 @@ from deeplabcut.pose_estimation_pytorch.models import DETECTORS, PoseModel from deeplabcut.pose_estimation_pytorch.runners import ( build_inference_runner, + CTDTrackingConfig, DetectorInferenceRunner, DynamicCropper, InferenceRunner, @@ -658,7 +660,8 @@ def get_pose_inference_runner( max_individuals: int | None = None, transform: A.BaseCompose | None = None, dynamic: DynamicCropper | None = None, - ctd_tracking: bool | dict = False, + cond_provider: CondFromModel | None = None, + ctd_tracking: bool | CTDTrackingConfig = False, ) -> PoseInferenceRunner: """Builds an inference runner for pose estimation. @@ -674,20 +677,14 @@ def get_pose_inference_runner( cropping should not be used. Only for bottom-up pose estimation models. Should only be used when creating inference runners for video pose estimation with batch size 1. + cond_provider: Only for CTD models. If None, the CondProvider is created from + the pytorch_cfg. ctd_tracking: Only for CTD models. Conditional top-down models can be used to directly track individuals. Poses from frame T are given as conditions for frame T+1. This also means a BU model is only needed to "initialize" the pose in the first frame, and for the remaining frames only the CTD model is - needed. If `True`, the `pytorch_config.yaml` for the model contain the - configuration for conditional pose tracking: - ``` - data: - conditions: - shuffle: 1 - snapshot: snapshot-250.pt - ``` - To use another model or configure conditional pose tracking differently, you - can pass a CTDTrackingConfig instance. + needed. To configure conditional pose tracking differently, you can pass a + CTDTrackingConfig instance. Returns: an inference runner for pose estimation @@ -724,17 +721,16 @@ def get_pose_inference_runner( margin = crop_cfg.get("margin", 0) if pose_task == Task.CTD: - # FIXME(niels) - allow to load conditions for a video from a file - kwargs["bu_runner"] = get_pose_inference_runner( - model_config=read_config_as_dict(model_config["data"]["config_path"]), - snapshot_path=model_config["data"]["snapshot_path"], - batch_size=1, - device=device, - max_individuals=max_individuals, - ) + if cond_provider is not None: + kwargs["bu_runner"] = get_pose_inference_runner( + model_config=read_config_as_dict(cond_provider.config_path), + snapshot_path=cond_provider.snapshot_path, + batch_size=1, + device=device, + max_individuals=max_individuals, + ) - # FIXME(niels) - add configuration for CTD tracking - kwargs["ctd_tracking"] = bool(ctd_tracking) + kwargs["ctd_tracking"] = ctd_tracking pose_preprocessor = build_conditional_top_down_preprocessor( color_mode=model_config["data"]["colormode"], diff --git a/deeplabcut/pose_estimation_pytorch/apis/videos.py b/deeplabcut/pose_estimation_pytorch/apis/videos.py index 7ee0d1b3bd..a7f7efb83b 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/videos.py +++ b/deeplabcut/pose_estimation_pytorch/apis/videos.py @@ -24,11 +24,17 @@ import deeplabcut.pose_estimation_pytorch.apis.utils as utils import deeplabcut.pose_estimation_pytorch.runners.shelving as shelving +from deeplabcut.pose_estimation_pytorch.apis.ctd import ( + get_condition_provider, + get_conditions_provider_for_video, +) from deeplabcut.pose_estimation_pytorch.apis.tracklets import ( convert_detections2tracklets, ) from deeplabcut.pose_estimation_pytorch.data import DLCLoader +from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromModel from deeplabcut.pose_estimation_pytorch.runners import ( + CTDTrackingConfig, DynamicCropper, InferenceRunner, ) @@ -134,7 +140,7 @@ def video_inference( Examples: Bottom-up video analysis: >>> import deeplabcut.pose_estimation_pytorch as pep - >>> from deeplabcut.core.config_utils import read_config_as_dict + >>> from deeplabcut.core.config import read_config_as_dict >>> model_cfg = read_config_as_dict("pytorch_config.yaml") >>> runner = pep.get_pose_inference_runner(model_cfg, "snapshot.pt") >>> video_predictions = pep.video_inference("video.mp4", runner) @@ -142,7 +148,7 @@ def video_inference( Top-down video analysis: >>> import deeplabcut.pose_estimation_pytorch as pep - >>> from deeplabcut.core.config_utils import read_config_as_dict + >>> from deeplabcut.core.config import read_config_as_dict >>> model_cfg = read_config_as_dict("pytorch_config.yaml") >>> runner = pep.get_pose_inference_runner(model_cfg, "snapshot.pt") >>> d_runner = pep.get_pose_inference_runner(model_cfg, "snapshot-detector.pt") @@ -152,7 +158,7 @@ def video_inference( Top-Down pose estimation with pre-computed bounding boxes: >>> import numpy as np >>> import deeplabcut.pose_estimation_pytorch as pep - >>> from deeplabcut.core.config_utils import read_config_as_dict + >>> from deeplabcut.core.config import read_config_as_dict >>> >>> video_iterator = pep.VideoIterator("video.mp4") >>> video_iterator.set_context([ @@ -226,7 +232,8 @@ def analyze_videos( batch_size: int | None = None, detector_batch_size: int | None = None, dynamic: tuple[bool, float, int] = (False, 0.5, 10), - ctd_tracking: bool | dict = False, + ctd_conditions: dict | CondFromModel | None = None, + ctd_tracking: bool | dict | CTDTrackingConfig = False, modelprefix: str = "", use_shelve: bool = False, robust_nframes: bool = False, @@ -284,20 +291,20 @@ def analyze_videos( is utilized for updating the crop window for the next frame (this is why the margin is important and should be set large enough given the movement of the animal). + ctd_conditions: Only for CTD models. If None, the configuration for the + condition provider will be loaded from the pytorch_config file (under the + "data": "conditions"). If the ctd_conditions is given as a dict, creates a + CondFromModel from the dict. Otherwise, a CondFromModel can be given + directly. Example configuration: + ``` + ctd_conditions = {"shuffle": 17, "snapshot": "snapshot-best-190.pt"} + ``` ctd_tracking: Only for CTD models. Conditional top-down models can be used to directly track individuals. Poses from frame T are given as conditions for frame T+1. This also means a BU model is only needed to "initialize" the pose in the first frame, and for the remaining frames only the CTD model is - needed. If `True`, the `pytorch_config.yaml` for the model contain the - configuration for conditional pose tracking: - ``` - data: - conditions: - shuffle: 1 - snapshot: snapshot-250.pt - ``` - To use another model or configure conditional pose tracking differently, you - can pass a CTDTrackingConfig instance. + needed. To configure conditional pose tracking differently, you can pass a + CTDTrackingConfig instance. modelprefix: directory containing the deeplabcut models to use when evaluating the network. By default, they are assumed to exist in the project folder. batch_size: the batch size to use for inference. Takes the value from the @@ -402,8 +409,28 @@ def analyze_videos( dynamic = None snapshot = utils.get_model_snapshots( - snapshot_index, loader.model_cfg, loader.pose_task + snapshot_index, loader.model_folder, loader.pose_task )[0] + + # Load the BU model for the conditions provider + cond_provider = None + if loader.pose_task == Task.CTD: + if ctd_conditions is None: + cond_provider = get_condition_provider( + condition_cfg=loader.model_cfg["data"]["conditions"], + config=config, + ) + elif isinstance(ctd_conditions, dict): + cond_provider = get_condition_provider( + condition_cfg=ctd_conditions, config=config, + ) + else: + cond_provider = ctd_conditions + + if isinstance(ctd_tracking, dict): + # FIXME(niels) - add video FPS setting + ctd_tracking = CTDTrackingConfig.build(ctd_tracking) + print(f"Analyzing videos with {snapshot.path}") pose_runner = utils.get_pose_inference_runner( model_config=loader.model_cfg, @@ -412,11 +439,10 @@ def analyze_videos( batch_size=batch_size, transform=transform, dynamic=dynamic, + cond_provider=cond_provider, ctd_tracking=ctd_tracking, ) - # FIXME(niels) - when ctd_tracking is true, save a "_ctd" track file! - detector_runner = None detector_path, detector_snapshot = None, None if loader.pose_task == Task.TOP_DOWN: @@ -441,13 +467,7 @@ def analyze_videos( batch_size=detector_batch_size, ) - dlc_scorer = utils.get_scorer_name( - loader.project_cfg, - shuffle, - train_fraction, - snapshot_uid=utils.get_scorer_uid(snapshot, detector_snapshot), - modelprefix=modelprefix, - ) + dlc_scorer = loader.scorer(snapshot, detector_snapshot) # Reading video and init variables videos = utils.list_videos_in_folder(videos, videotype, shuffle=in_random_order) @@ -462,6 +482,13 @@ def analyze_videos( video_iterator = VideoIterator(video, cropping=cropping) + # Check if BU model pose predictions exist so the model does not need to be run + if loader.pose_task == Task.CTD: + vid_cond_provider = get_conditions_provider_for_video(cond_provider, video) + if vid_cond_provider is not None: + video_cond = vid_cond_provider.load_conditions() + video_iterator.set_context([dict(cond_kpts=c) for c in video_cond]) + shelf_writer = None if use_shelve: shelf_writer = shelving.ShelfWriter( @@ -517,14 +544,54 @@ def analyze_videos( ) if multi_animal: + assemblies_path = output_path / f"{output_prefix}_assemblies.pickle" _generate_assemblies_file( full_data_path=output_pkl, - output_path=output_path / f"{output_prefix}_assemblies.pickle", + output_path=assemblies_path, num_bodyparts=len(bodyparts), num_unique_bodyparts=len(unique_bodyparts), ) - if auto_track: + # when running CTD tracking, don't auto-track as CTD did the tracking + # for us! + if ctd_tracking: + full_data = auxiliaryfunctions.read_pickle(output_pkl) + full_data_meta = full_data.pop("metadata") + + num_frames = full_data_meta["nframes"] + str_width = full_data_meta["key_str_width"] + + ctd_predictions = [] + for i in range(num_frames): + frame_data = full_data.get("frame" + str(i).zfill(str_width)) + if frame_data is None: + pose = np.full((len(individuals), len(bodyparts), 3), np.nan) + ctd_predictions.append(dict(bodyparts=pose)) + continue + + # there can't be unique bodyparts for CTD models + # -> so coords has shape (num_bodyparts, num_idv, _) + coords = np.stack(frame_data["coordinates"][0], axis=0) + scores = np.stack(frame_data["confidence"], axis=0) + pose = np.concatenate([coords, scores], axis=-1) + + # transpose to (num_idv, num_bodyparts, _) + pose = pose.transpose((1, 0, 2)) + + # add poses to the predictions + ctd_predictions.append(dict(bodyparts=pose)) + + create_df_from_prediction( + predictions=predictions, + multi_animal=multi_animal, + model_cfg=loader.model_cfg, + dlc_scorer=dlc_scorer, + output_path=output_path, + output_prefix=output_prefix + "_ctd", + save_as_csv=save_as_csv, + ) + + elif auto_track: convert_detections2tracklets( config=config, videos=str(video), @@ -783,6 +850,7 @@ def _generate_output_data( if "bboxes" in frame_predictions: output[key]["bboxes"] = frame_predictions["bboxes"] + if "bbox_scores" in frame_predictions: output[key]["bbox_scores"] = frame_predictions["bbox_scores"] if "identity_scores" in frame_predictions: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index 23ef009c5b..cc77370773 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -1,19 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + top_down_crop: + width: 256 + height: 256 + crop_with_context: false train: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - top_down_crop: - width: 256 - height: 256 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + top_down_crop: + width: 256 + height: 256 + crop_with_context: false method: ctd model: backbone: @@ -38,18 +36,23 @@ model: predictor: type: HeatmapPredictor apply_sigmoid: false - #clip_scores: true - location_refinement: false + clip_scores: true + location_refinement: true + locref_std: 7.2801 target_generator: type: HeatmapGaussianGenerator num_heatmaps: "num_bodyparts" pos_dist_thresh: 17 heatmap_mode: KEYPOINT - generate_locref: false + generate_locref: true + locref_std: 7.2801 criterion: heatmap: type: WeightedMSECriterion weight: 1.0 + locref: + type: WeightedHuberCriterion + weight: 0.05 heatmap_config: channels: - 32 @@ -57,4 +60,12 @@ model: strides: [] final_conv: out_channels: "num_bodyparts" + kernel_size: 1 + locref_config: + channels: + - 32 + kernel_size: [] + strides: [] + final_conv: + out_channels: "num_bodyparts x 2" kernel_size: 1 \ No newline at end of file diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index 356c902184..f9c39c0322 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -1,16 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + top_down_crop: + width: 256 + height: 256 + crop_with_context: false train: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + top_down_crop: + width: 256 + height: 256 + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml index 9cbab9509b..ccb278448e 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml @@ -1,15 +1,18 @@ data: + bbox_margin: 5 + gen_sampling: + keypoint_sigmas: [ .079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025 ] # CrowdPose sigmas + keypoints_symmetry: [ [ 0, 1 ], [ 2, 3 ], [ 4, 5 ], [ 6, 7 ], [ 8, 9 ], [ 10, 11 ] ] # CrowdPose symmetries inference: top_down_crop: width: 288 height: 384 + crop_with_context: false train: top_down_crop: width: 288 height: 384 - bbox_margin: 5 - gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] # CrowdPose sigmas - gen_sampling_symmetries: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] # CrowdPose symmetries + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml index 31a39cf687..cb8a8c7122 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w32.yaml @@ -1,16 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + top_down_crop: + width: 256 + height: 256 + crop_with_context: false train: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + top_down_crop: + width: 256 + height: 256 + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index 3433188ec8..ae8d4bc606 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -1,16 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + top_down_crop: + width: 256 + height: 256 + crop_with_context: false train: - auto_padding: # Required for HRNet backbones - pad_width_divisor: 32 - pad_height_divisor: 32 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + top_down_crop: + width: 256 + height: 256 + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml index 7fe4606c9f..eb808cb79b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_m.yaml @@ -1,16 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: top_down_crop: width: 256 height: 256 - bu_predictions: /home/lucas/datasets/fish-dlc-2021-05-07/evaluation-results-pytorch/iteration-17/fishMay7-trainset94shuffle5/DLC_HrnetW32_fishMay7shuffle5_detector_250_snapshot_140.h5 + crop_with_context: false train: top_down_crop: width: 256 height: 256 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + crop_with_context: false method: ctd model: backbone: @@ -73,7 +74,7 @@ runner: optimizer: type: AdamW params: - lr: 1e-3 + lr: 5e-2 scheduler: type: SequentialLR params: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml index 1c81472d05..94aaac8d19 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x.yaml @@ -1,15 +1,17 @@ data: + bbox_margin: 25 + gen_sampling: + keypoint_sigmas: 0.1 inference: top_down_crop: width: 384 height: 384 + crop_with_context: false train: top_down_crop: width: 384 height: 384 - bbox_margin: 25 - gen_sampling_sigmas: 0.1 - gen_sampling_symmetries: [] + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml index 05a928473e..b890e91366 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_rtmpose_x_human.yaml @@ -1,15 +1,18 @@ data: + bbox_margin: 5 + gen_sampling: + keypoint_sigmas: [ .079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025 ] # CrowdPose sigmas + keypoints_symmetry: [ [ 0, 1 ], [ 2, 3 ], [ 4, 5 ], [ 6, 7 ], [ 8, 9 ], [ 10, 11 ] ] # CrowdPose symmetries inference: top_down_crop: width: 288 height: 384 + crop_with_context: false train: top_down_crop: width: 288 height: 384 - bbox_margin: 5 - gen_sampling_sigmas: [.079, .079, .072, .072, .062, .062, .107, .107, .087, .087, .089, .089, .025, .025] # CrowdPose sigmas - gen_sampling_symmetries: [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11]] # CrowdPose symmetries + crop_with_context: false method: ctd model: backbone: diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index 9e2972b99d..6d92e3e315 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -95,7 +95,6 @@ def make_pytorch_pose_config( pose_config["net_type"] = net_type backbones = load_backbones(configs_dir) - add_conditions_to_aug_cfg = False if net_type in backbones: if not top_down and multianimal_project: model_cfg = create_backbone_with_paf_model( @@ -118,12 +117,6 @@ def make_pytorch_pose_config( default_value_kwargs = {} if architecture == "dlcrnet": default_value_kwargs.update(_get_paf_parameters(project_config, bodyparts)) - elif architecture == "ctd": - if ctd_conditions is None: - raise ValueError( - "When using a conditional top down (ctd) architecture, conditions need to be specified." - ) - add_conditions_to_aug_cfg = True cfg_path = configs_dir / architecture / f"{net_type}.yaml" model_cfg = read_config_as_dict(cfg_path) @@ -134,7 +127,7 @@ def make_pytorch_pose_config( **default_value_kwargs, ) - task = Task(model_cfg["method"]) + task = Task(model_cfg.get("method", "BU").upper()) if task == Task.TOP_DOWN: model_cfg = add_detector( configs_dir, @@ -146,8 +139,11 @@ def make_pytorch_pose_config( # add the default augmentations to the config aug_filename = "aug_default.yaml" if task == Task.BOTTOM_UP else "aug_top_down.yaml" aug_cfg = {"data": read_config_as_dict(configs_dir / "base" / aug_filename)} - if add_conditions_to_aug_cfg: + + # Add conditions for CTD models if specified + if task == Task.CTD and ctd_conditions is not None: _add_ctd_conditions(aug_cfg, ctd_conditions) + pose_config = update_config(pose_config, aug_cfg) # add the model to the config @@ -206,20 +202,22 @@ def _add_ctd_conditions( """ Args: aug_cfg: dict, data augmentation configuration - ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] , optional, default = None, - If using a conditional-top-down (CTD) net_type, this argument needs to be specified. - It defines the conditions that will be used with the CTD model. - It can be either: - * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. - * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. - * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + ctd_conditions: Only for using conditional-top-down (CTD) models. It defines + the conditions that will be used with the CTD model. It can be: + * A shuffle number (ctd_conditions: int), which must correspond to a + bottom-up (BU) network type. + * A predictions file path (ctd_conditions: string | Path), which must + correspond to a .json or .h5 predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: + tuple[int, str] | tuple[int, int]), which respectively correspond to a + bottom-up (BU) network type and a particular snapshot name or index. """ if isinstance(ctd_conditions, int): conditions = {"shuffle": ctd_conditions} elif isinstance(ctd_conditions, str) or isinstance(ctd_conditions, Path): ctd_conditions = Path(ctd_conditions) - if not ctd_conditions.exist(): + if not ctd_conditions.exists(): raise FileNotFoundError(f"Invalid path: {ctd_conditions}") if ctd_conditions.suffix not in (".h5", ".json"): raise ValueError(f"Invalid conditions file extension.") diff --git a/deeplabcut/pose_estimation_pytorch/data/__init__.py b/deeplabcut/pose_estimation_pytorch/data/__init__.py index e4ae41d5b1..2592eb2240 100644 --- a/deeplabcut/pose_estimation_pytorch/data/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/data/__init__.py @@ -28,4 +28,5 @@ build_top_down_preprocessor, Preprocessor, ) +from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot from deeplabcut.pose_estimation_pytorch.data.transforms import build_transforms diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index 3b93bcf890..67dad8d694 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -19,10 +19,13 @@ import deeplabcut.core.config as config_utils import deeplabcut.pose_estimation_pytorch.config as config from deeplabcut.pose_estimation_pytorch.data.dataset import ( - CTDConfig, PoseDataset, PoseDatasetParameters, ) +from deeplabcut.pose_estimation_pytorch.data.generative_sampling import ( + GenSamplingConfig, +) +from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot from deeplabcut.pose_estimation_pytorch.data.utils import ( _compute_crop_bounds, bbox_from_keypoints, @@ -65,6 +68,30 @@ def model_folder(self) -> Path: """Returns: The path of the folder containing the model data""" return self.model_config_path.parent + def snapshots( + self, + detector: bool = False, + best_in_last: bool = True, + ) -> list[Snapshot]: + """Lists snapshots saved for the model. + + Args: + detector: If the Loader is for a Top-Down model, passing detector=True + will return the snapshots for the detector. Otherwise, the snapshots + for the pose model are returned. + best_in_last: Whether to place the snapshot with the best performance in the + last position in the list, even if it wasn't the last epoch. + + Returns: + The snapshots stored in a folder, sorted by the number of epochs they were + trained for. If best_in_last=True and a best snapshot exists, it will be the + last one in the list. + """ + prefix = self.pose_task.snapshot_prefix + if detector: + prefix = Task.DETECT.snapshot_prefix + return list_snapshots(self.model_folder, prefix, best_in_last=best_in_last) + def update_model_cfg(self, updates: dict) -> None: """Updates the model configuration @@ -224,12 +251,13 @@ def create_dataset( parameters = self.get_dataset_parameters() data = self.load_data(mode) data["annotations"] = self.filter_annotations(data["annotations"], task) + ctd_config = None if self.pose_task == Task.CTD: - ctd_config = CTDConfig( - self.model_cfg["data"].get("bbox_margin", 25), - self.model_cfg["data"].get("gen_sampling_sigmas", 0.1), - self.model_cfg["data"].get("gen_sampling_symmetries", []) + ctd_config = GenSamplingConfig( + bbox_margin=self.model_cfg["data"].get("bbox_margin", 20), + **self.model_cfg["data"].get("gen_sampling", {}), ) + dataset = PoseDataset( images=data["images"], annotations=data["annotations"], @@ -237,7 +265,7 @@ def create_dataset( mode=mode, task=task, parameters=parameters, - ctd_config=ctd_config if self.pose_task == Task.CTD else None, + ctd_config=ctd_config, ) return dataset diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 207791d242..c2b4636e6a 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -73,6 +73,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: crop_cfg = self.model_cfg["data"]["train"].get("top_down_crop", {}) crop_w, crop_h = crop_cfg.get("width", 256), crop_cfg.get("height", 256) crop_margin = crop_cfg.get("margin", 0) + crop_with_context = crop_cfg.get("crop_with_context", True) self._dataset_parameters = PoseDatasetParameters( bodyparts=bodyparts, @@ -82,6 +83,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: color_mode=self.model_cfg.get("color_mode", "RGB"), top_down_crop_size=(crop_w, crop_h), top_down_crop_margin=crop_margin, + top_down_crop_with_context=crop_with_context, ) return self._dataset_parameters diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py new file mode 100644 index 0000000000..1996f6a199 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -0,0 +1,481 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +from __future__ import annotations + +import json +import pickle +from abc import ABC +from pathlib import Path + +import numpy as np +import pandas as pd + +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader +from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot +from deeplabcut.pose_estimation_pytorch.task import Task + + +class CondProvider(ABC): + """A class providing conditions for a CTD model.""" + + @classmethod + def get_loader_and_snapshot( + cls, + config: str | Path, + shuffle: int, + trainset_index: int = 0, + modelprefix: str = "", + snapshot: str | None = None, + snapshot_index: int | None = None, + ) -> tuple[DLCLoader, Snapshot]: + """Creates a DLCLoader for the BU shuffle and the path to conditions snapshot. + + One of `snapshot` or `snapshot_index` must be provided. + + Args: + config: Path to the DeepLabCut project config, or the project config itself + trainset_index: The index of the TrainingsetFraction for which to load data + shuffle: The index of the shuffle for which to load data. + modelprefix: The modelprefix for the shuffle. + snapshot: The name of the snapshot to use. + snapshot_index: The index of the snapshot to use. If `snapshot` is + provided, the `snapshot_index` is not used. + + Returns: + loader: The DLCLoader for the BU shuffle. + snapshot: The BU Snapshot to use for conditions. + + Raises: + ValueError: If the given shuffle is not for a BU model. + """ + loader = DLCLoader( + config, + trainset_index=trainset_index, + shuffle=shuffle, + modelprefix=modelprefix, + ) + if loader.pose_task != Task.BOTTOM_UP: + raise ValueError( + "Conditions can only be loaded from shuffles for bottom-up models, but " + f"shuffle {shuffle} has task {loader.pose_task} (config={config}, " + f"trainset_index={trainset_index}, modelprefix={modelprefix})." + ) + + if snapshot is not None: + snapshot_path = loader.model_folder / snapshot + if not snapshot_path.exists(): + raise ValueError(f"Snapshot file {snapshot_path} does not exist.") + bu_snapshot = Snapshot.from_path(snapshot_path) + + else: + if snapshot_index is None: + snapshot_index = -1 + + snapshots = loader.snapshots() + if len(snapshots) == 0: + raise ValueError( + f"No snapshots found for shuffle={shuffle} in {loader.model_folder}" + ) + + if snapshot_index > len(snapshots): + snapshot_str = "\n".join( + [f" {i}: {s.path.name}" for i, s in enumerate(snapshots)] + ) + raise ValueError( + f"Snapshot index {snapshot_index} is out of range. Existing " + f"snapshots: {snapshot_str}" + ) + + bu_snapshot = snapshots[snapshot_index] + + return loader, bu_snapshot + + +class CondFromFile(CondProvider): + """A class providing conditions for a CTD model from a file + + Args: + filepath: The path to the file containing the conditions for the CTD model. + These conditions must be pose predictions made by a BU model on the data + images: Only load the conditions for the given image keys. + kwargs: A `CondFromFile` instance can also be created from a DeepLabCut + shuffle by passing kwargs and setting `filepath=None`. See examples for more + information. + """ + + def __init__( + self, + filepath: str | Path | None = None, + **kwargs, + ) -> None: + if filepath is None: + # Load the conditions filepath from the Shuffle + bu_loader, bu_snapshot = self.get_loader_and_snapshot(**kwargs) + bu_scorer = bu_loader.scorer(bu_snapshot) + filepath = bu_loader.evaluation_folder / f"{bu_scorer}.h5" + if not filepath.exists(): + raise ValueError( + f"Conditions file {filepath} does not exist. Please make sure " + f"snapshot {bu_snapshot.path.name} for {kwargs['shuffle']} " + f"was evaluated (which is when the predictions file is created)." + ) + + if not filepath.exists(): + raise ValueError( + "Conditions file {conditions_filepath} does not exist. Please check " + f"the given path." + ) + + self.filepath = filepath + + def load_conditions( + self, + images: list[str] | None = None, + path_prefix: str | None = None, + ) -> dict[str, np.ndarray] | list[np.ndarray]: + """Loads conditions for a model from a file. + + When loading conditions for individual images, the `images` must be provided + (indicating which images to load conditions for). A dict is returned containing + the conditions for each requested image. + + When loading conditions for a video, the `images` parameter must be set to None. + A list is returned containing the conditions for each frame. + + Args: + images: A list of image paths to load conditions for. + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + If "images" is given: a dictionary mapping image paths to condition arrays. + Each array has shape (num_conditions, num_bodyparts, 3). + If "images" is None: a list containing the conditions for each frame. + """ + suffix = Path(self.filepath).suffix.lower() + if suffix == ".h5": + return self.load_conditions_h5(self.filepath, images, path_prefix) + elif suffix == ".json": + return self.load_conditions_json(self.filepath, images, path_prefix) + elif suffix == ".pickle": + return self.load_conditions_pickle(self.filepath) + + raise ValueError( + f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " + f"files. Received {self.filepath}." + ) + + @staticmethod + def load_conditions_h5( + filepath: str | Path, + images: list[str] | None = None, + path_prefix: str | Path | None = None, + ) -> dict[str, np.ndarray] | list[np.ndarray]: + """Loads conditions for a model from a pandas DataFrame stored in an HDF file + + When loading conditions for individual images, the `images` must be provided + (indicating which images to load conditions for). A dict is returned containing + the conditions for each requested image. + + When loading conditions for a video, the `images` parameter must be set to None. + A list is returned containing the conditions for each frame. + + The DataFrame must be in the same format as DeepLabCut Predictions. For + predictions on images (e.g. on a training/test set), the DataFrame should be in + the format: + + ``` + scorer model-name ... + individuals idv0 ... idvM + bodyparts bpt0 ... bptN + coords x y likelihood ... x y likelihood + ---------------------------------------------------------------------------- + (labeled-data, v0, 0.png) 87.0 62.0 0.73 ... 83.2 99.1 0.8326 + ``` + + While for conditions for videos, the DataFrame should be in the format: + + ``` + scorer model-name ... + individuals idv0 ... idvM + bodyparts bpt0 ... bptN + coords x y likelihood ... x y likelihood + ---------------------------------------------------------------------------- + frame0000.png 87.0 62.0 0.73 ... 83.2 99.1 0.8326 + ``` + + Args: + images: A list of image paths to load conditions for + filepath: Path to the JSON file containing conditions. + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + If "images" is given: a dictionary mapping image paths to condition arrays. + Each array has shape (num_conditions, num_bodyparts, 3). + If "images" is None: a list containing the conditions for each frame. + """ + def _parse_row(df_row) -> np.ndarray: + # Row to numpy and reshape + pose = df_row.to_numpy().reshape((num_conditions, num_bodyparts, 3)) + + # Remove missing data + missing_keypoints = np.any(np.isnan(pose) | (pose < 0), axis=2) + pose[missing_keypoints] = 0 + + # Only keep conditions with at least one visible keypoint + visible_conditions = np.any(~missing_keypoints, axis=1) + if np.sum(visible_conditions) > 0: + pose = pose[visible_conditions] + else: + pose = np.zeros((0, num_bodyparts, 3)) + + return pose + + if path_prefix is not None: + path_prefix = Path(path_prefix) + + df = pd.read_hdf(filepath) + if not isinstance(df, pd.DataFrame): + raise ValueError(f"{filepath} is not a dataframe.") + + num_bodyparts = len(df.columns.get_level_values("bodyparts").unique()) + num_conditions = 1 + if "individuals" in df.columns.names: + num_conditions = len(df.columns.get_level_values("individuals").unique()) + + # Parse as list and return + if images is None: + parsed = [] + for _, cond in df.iterrows(): + parsed.append(_parse_row(cond)) + + return parsed + + image_set = set(images) + conditions = {} + for filename, row in df.iterrows(): + if isinstance(filename, tuple): + filename = str(Path(*filename)) + + if path_prefix is not None and filename not in image_set: + filename = str(path_prefix / filename) + + if filename in image_set: + conditions[filename] = _parse_row(row) + + missing = image_set.difference(set(conditions.keys())) + if len(missing) > 0: + print( + f"Warning: did not find conditions for {len(missing)} of the {len(images)} " + f"images. Missing conditions:" + ) + for img_path in missing: + print(f" - {img_path}") + + return conditions + + @staticmethod + def load_conditions_json( + filepath: str | Path, + images: list[str] | None = None, + path_prefix: str | Path | None = None, + ) -> dict[str, np.ndarray] | list[np.ndarray]: + """Loads conditions for a model from a JSON file. + + When loading conditions for individual images, the `images` must be provided + (indicating which images to load conditions for). A dict is returned containing + the conditions for each requested image. The JSON data structure should be: + + ``` + { + "img000.png": [ # conditions for image 0 + [ # condition 0 pose + [x, y, score], # keypoint 0 + [x, y, score], # keypoint 1 + ... + [x, y, score], # keypoint N + ], + [ ... ], # condition 1 + ... + [ ... ] # condition M + ], + "img001.png": [...] # conditions for image 1 + } + ``` + + When loading conditions for a video, the `images` parameter must be set to None. + A list is returned containing the conditions for each frame. The JSON data + structure should be: + + ``` + [ + [ # conditions for frame 0 + [ # condition 0 pose + [x, y, score], # keypoint 0 + [x, y, score], # keypoint 1 + ... + [x, y, score], # keypoint N + ], + [ ... ], # condition 1 + ... + [ ... ] # condition M + ], + [ ... ], # conditions for frame 1 + ... + [ ... ] # conditions for frame N + ] + ``` + + Args: + images: A list of image paths to load conditions for. + filepath: Path to the JSON file containing conditions. + path_prefix: Optional prefix to prepend to image paths when looking up + conditions. This is useful when the paths in the conditions file are + relative but the provided image paths are absolute, or vice versa. + + Returns: + A dictionary mapping image paths to condition arrays. Each array has shape + (num_conditions, num_bodyparts, 3). + """ + with open(filepath, "r") as f: + conditions = json.load(f) + + # Parse list and return + if images is None: + if not isinstance(conditions, list): + raise ValueError( + f"Conditions are expected to be of type list when `images=None`, " + f"got {type(conditions)}." + ) + + parsed = [] + for cond in conditions: + if len(cond) == 0: + parsed.append(np.zeros((0, 0, 3))) + else: + parsed.append(np.asarray(cond)) + return parsed + + if not isinstance(conditions, dict): + raise ValueError( + f"Conditions are expected to be of type dict, got {type(conditions)}. " + "They should be in the format 'labeled-data/video-0/img0000.png' -> " + "list[list[list[float]]], where the list represents an array of shape " + "(num_conditions, num_bodyparts, 3)." + ) + + path_with_prefix_to_key = {} + if path_prefix is not None: + path_with_prefix_to_key = { + str(Path(path_prefix) / k): k for k in conditions.keys() + } + + parsed = {} + missing = [] + for img_path in images: + if img_path in conditions: + pose = np.asarray(conditions[img_path]) + elif img_path in path_with_prefix_to_key: + pose = np.asarray(conditions[path_with_prefix_to_key[img_path]]) + else: + pose = np.zeros((0, 0, 3)) + missing.append(img_path) + + if len(pose) == 0: + pose = np.zeros((0, 0, 3)) + + parsed[img_path] = pose + + if len(missing) > 0: + print( + f"Warning: did not find conditions for {len(missing)} of the " + f"{len(images)} images. Missing conditions:" + ) + for img_path in missing: + print(f" - {img_path}") + + return parsed + + @staticmethod + def load_conditions_pickle(filepath: str | Path) -> list[np.ndarray]: + """Loads conditions from a `*_assemblies.pickle` file containing predictions + + Args: + filepath: Path to the Pickle file containing conditions. + """ + with open(filepath, "rb") as f: + data = pickle.load(f) + + frames = [f for f in data.keys() if isinstance(f, int)] + n_frames = max(*frames) + + parsed = [] + for i in range(n_frames): + assemblies = data.get(i) + if assemblies is None or len(assemblies) == 0: + pose = np.zeros((0, 0, 3)) + else: + pose = np.stack(assemblies, axis=0)[:, :, :3] + + mask = np.any(np.all(pose > 0, axis=-1), axis=-1) + if np.sum(mask) == 0: + pose = np.zeros((0, 0, 3)) + else: + pose = pose[mask] + + parsed.append(pose) + return parsed + + +class CondFromModel(CondProvider): + """A class providing conditions for a CTD model from a BU model. + + Attributes: + config_path: (Path) + The path to the `pytorch_config.yaml` for the BU model to use as conditions. + snapshot_path: (Path) + The path to the BU snapshot to use to generate conditions for the CTD model. + scorer: str + The scorer name for the BU model. This can be used to look for files + containing conditions instead of recomputing them. + + Args: + config_path: (Path) + The path to the `pytorch_config.yaml` for the BU model to use as conditions. + snapshot_path: (Path) + The path to the BU snapshot to use to generate conditions for the CTD model. + **kwargs: A `CondFromModel` instance can also be created from a DeepLabCut + shuffle. See examples for more information. + """ + + def __init__( + self, + config_path: str | Path | None = None, + snapshot_path: str | Path | None = None, + scorer: str | None = None, + **kwargs, + ) -> None: + if config_path is not None and snapshot_path is not None: + config_path = Path(config_path) + snapshot_path = Path(config_path) + elif "config" in kwargs and "shuffle" in kwargs: + bu_loader, snapshot = self.get_loader_and_snapshot(**kwargs) + config_path = bu_loader.model_config_path + snapshot_path = snapshot.path + if scorer is None: + scorer = bu_loader.scorer(snapshot) + + self.config_path = config_path + self.snapshot_path = snapshot_path + self.scorer = scorer diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index 836b04065b..049ecae6da 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -16,6 +16,10 @@ import numpy as np from torch.utils.data import Dataset +from deeplabcut.pose_estimation_pytorch.data.generative_sampling import ( + GenerativeSampler, + GenSamplingConfig, +) from deeplabcut.pose_estimation_pytorch.data.image import load_image, top_down_crop from deeplabcut.pose_estimation_pytorch.data.utils import ( _crop_image_keypoints, @@ -30,14 +34,6 @@ safe_stack, ) from deeplabcut.pose_estimation_pytorch.task import Task -from deeplabcut.pose_estimation_pytorch.data.generative_sampling import GenerativeSampler - - -@dataclass(frozen=True) -class CTDConfig: - bbox_margin: int - gen_sampling_sigmas: float | list[float] = 0.1 - gen_sampling_symmetries: bool = True @dataclass(frozen=True) @@ -60,9 +56,10 @@ class PoseDatasetParameters: individuals: list[str] with_center_keypoints: bool = False color_mode: str = "RGB" - ctd_config: CTDConfig | None = None + ctd_config: GenSamplingConfig | None = None top_down_crop_size: tuple[int, int] | None = None top_down_crop_margin: int | None = None + top_down_crop_with_context: bool = True @property def num_joints(self) -> int: @@ -87,7 +84,7 @@ class PoseDataset(Dataset): transform: A.BaseCompose | None = None mode: str = "train" task: Task = Task.BOTTOM_UP - ctd_config: CTDConfig | None = None + ctd_config: GenSamplingConfig | None = None def __post_init__(self): self.image_path_id_map = map_image_path_to_id(self.images) @@ -108,10 +105,15 @@ def __post_init__(self): self.td_crop_margin = self.parameters.top_down_crop_margin if self.task == Task.CTD: + if self.ctd_config is None: + raise ValueError( + "Must specify a ``ctd_config`` in your PoseDatasetParameters for " + "CTD models." + ) + self.generative_sampler = GenerativeSampler( self.parameters.num_joints, - keypoint_sigmas=self.ctd_config.gen_sampling_sigmas, - keypoints_symmetry=self.ctd_config.gen_sampling_symmetries, + **self.ctd_config.to_dict(), ) def __len__(self): @@ -144,7 +146,7 @@ def _get_raw_item_crop(self, index: int) -> tuple[str, list[dict], int]: ann = self.annotations[index] img = self.images[self.img_id_to_index[ann["image_id"]]] return img["file_name"], [ann], img["id"] - + def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: """ Includes keypoints from other individuals in the image ("context"). @@ -156,7 +158,7 @@ def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: # we consider near annotations to be those whose bounding boxes overlap with # the current item # HACK: add same annotation as near keypoints so that we don't have empty list - if calc_bbox_overlap(ann['bbox'], self.annotations[idx]['bbox']) > 0: + if calc_bbox_overlap(ann["bbox"], self.annotations[idx]["bbox"]) > 0: near_anns.append(self.annotations[idx]) return img["file_name"], [ann] + near_anns, img["id"] @@ -224,10 +226,10 @@ def __getitem__(self, index: int) -> dict: near_keypoints = keypoints[1:] keypoints = keypoints[:1] synthesized_keypoints = self.generative_sampler( - keypoints=keypoints.reshape(-1, 3), - near_keypoints=near_keypoints.reshape(len(near_keypoints),-1, 3), - area=bboxes[0, 2]*bboxes[0, 3], - image_size=original_size, + keypoints=keypoints.reshape(-1, 3), + near_keypoints=near_keypoints.reshape(len(near_keypoints), -1, 3), + area=bboxes[0, 2] * bboxes[0, 3], + image_size=original_size, ) # if conditional keypoints are empty, we take original bbox @@ -257,19 +259,22 @@ def __getitem__(self, index: int) -> dict: image, bboxes[0], self.parameters.top_down_crop_size, - #margin=0, - self.parameters.top_down_crop_margin, #TODO: check - crop_with_context=(self.task != Task.CTD), + self.parameters.top_down_crop_margin, + crop_with_context=self.parameters.top_down_crop_with_context, ) keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] if self.task == Task.CTD: - synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] - synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] + synthesized_keypoints[:, 0] = ( + synthesized_keypoints[:, 0] - offsets[0] + ) / scales[0] + synthesized_keypoints[:, 1] = ( + synthesized_keypoints[:, 1] - offsets[1] + ) / scales[1] keypoints = safe_stack( [keypoints, synthesized_keypoints[None, ...]], - (2, 1, self.parameters.num_joints, 3) + (2, 1, self.parameters.num_joints, 3), ) bboxes = bboxes[:1] @@ -277,7 +282,9 @@ def __getitem__(self, index: int) -> dict: bboxes[..., 1] = (bboxes[..., 1] - offsets[1]) / scales[1] bboxes[..., 2] = bboxes[..., 2] / scales[0] bboxes[..., 3] = bboxes[..., 3] / scales[1] - bboxes = np.clip(bboxes, 0, self.parameters.top_down_crop_size[0] - 1) #TODO: clip based on [x,y,x,y]? + bboxes = np.clip( + bboxes, 0, self.parameters.top_down_crop_size[0] - 1 + ) # TODO: clip based on [x,y,x,y]? # RandomBBoxTransform may move keypoints outside the cropped image oob_mask = out_of_bounds_keypoints(keypoints, self.td_crop_size) @@ -327,7 +334,7 @@ def _prepare_final_data_dict( "annotations": self._prepare_final_annotation_dict( keypoints, keypoints_unique, bboxes, annotations_merged ), - "context": context + "context": context, } def _prepare_final_annotation_dict( @@ -369,7 +376,9 @@ def _prepare_final_annotation_dict( "boxes": pad_to_length(bboxes, num_animals, 0).astype(np.single), "is_crowd": pad_to_length(is_crowd, num_animals, 0).astype(int), "labels": pad_to_length(labels, num_animals, -1).astype(int), - "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype(int), + "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype( + int + ), } def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 1374c27733..c8f43d007e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -24,6 +24,7 @@ from deeplabcut.core.engine import Engine from deeplabcut.pose_estimation_pytorch.data.base import Loader from deeplabcut.pose_estimation_pytorch.data.dataset import PoseDatasetParameters +from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot from deeplabcut.pose_estimation_pytorch.data.utils import read_image_shape_fast @@ -134,6 +135,28 @@ def split(self) -> dict[str, list[int]]: return self._split + def scorer( + self, + snapshot: Snapshot | str | Path, + detector_snapshot: Snapshot | str | Path | None = None, + ) -> str: + """Returns the scorer for this DLCLoader and the given snapshot.""" + task, date = self.project_cfg["Task"], self.project_cfg["date"] + name = "".join([p.capitalize() for p in self.model_cfg["net_type"].split("_")]) + + if not isinstance(snapshot, Snapshot): + snapshot = Snapshot.from_path(Path(snapshot)) + + snapshot_id = f"snapshot_{snapshot.uid()}" + if detector_snapshot is not None: + if not isinstance(detector_snapshot, Snapshot): + detector_snapshot = Snapshot.from_path(Path(detector_snapshot)) + + detect_id = detector_snapshot.uid() + snapshot_id = f"detector_{detect_id}_{snapshot_id}" + + return f"DLC_{name}_{task}{date}shuffle{self.shuffle}_{snapshot_id}" + def get_dataset_parameters(self) -> PoseDatasetParameters: """Retrieves dataset parameters based on the instance's configuration. @@ -143,6 +166,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: crop_cfg = self.model_cfg["data"]["train"].get("top_down_crop", {}) crop_w, crop_h = crop_cfg.get("width", 256), crop_cfg.get("height", 256) crop_margin = crop_cfg.get("margin", 0) + crop_with_context = crop_cfg.get("crop_with_context", True) return PoseDatasetParameters( bodyparts=self.model_cfg["metadata"]["bodyparts"], @@ -152,6 +176,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: color_mode=self.model_cfg.get("color_mode", "RGB"), top_down_crop_size=(crop_w, crop_h), top_down_crop_margin=crop_margin, + top_down_crop_with_context=crop_with_context, ) def load_data(self, mode: str = "train") -> dict: diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index dc0b0dd36e..84fe6ae01a 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -44,9 +44,49 @@ import math import random +from dataclasses import dataclass, asdict + import numpy as np +@dataclass(frozen=True) +class GenSamplingConfig: + """Configuration for CTD models. + + Args: + bbox_margin: The margin added around conditional keypoints + keypoint_sigmas: The sigma for each keypoint. + keypoints_symmetry: Indices of symmetric keypoints (e.g. left/right eye) + jitter_prob: The probability of applying jitter. Jitter error is defined as + a small displacement from the GT keypoint. + swap_prob: The probability of applying a swap error. Swap error represents + a confusion between the same or similar parts which belong to different + persons. + inv_prob: The probability of applying an inversion error. Inversion error + occurs when a pose estimation model is confused between semantically + similar parts that belong to the same instance. + miss_prob: The probability of applying a miss error. Miss error represents a + large displacement from the GT keypoint position. + """ + bbox_margin: int + keypoint_sigmas: float | list[float] = 0.1 + keypoints_symmetry: list[tuple[int, int]] | None = None + jitter_prob: float = 0.16 + swap_prob: float = 0.08 + inv_prob: float = 0.03 + miss_prob: float = 0.10 + + def to_dict(self) -> dict: + return { + "keypoint_sigmas": self.keypoint_sigmas, + "keypoints_symmetry": self.keypoints_symmetry, + "jitter_prob": self.jitter_prob, + "swap_prob": self.swap_prob, + "inv_prob": self.inv_prob, + "miss_prob": self.miss_prob, + } + + class GenerativeSampler: """Performs generative sampling of keypoints for CTD model training""" @@ -54,7 +94,11 @@ def __init__( self, num_keypoints: int, keypoint_sigmas: float | list[float] = 0.1, - keypoints_symmetry: list[tuple[int, int]] = [] + keypoints_symmetry: list[tuple[int, int]] | None = None, + jitter_prob: float = 0.16, + swap_prob: float = 0.08, + inv_prob: float = 0.03, + miss_prob: float = 0.10, ): """ Args: @@ -62,22 +106,33 @@ def __init__( keypoint_sigmas: the sigma for each keypoint keypoints_symmetry: indices of keypoints that are symmetric (e.g., left and right eye) + jitter_prob: The probability of applying jitter. Jitter error is defined as + a small displacement from the GT keypoint. + swap_prob: The probability of applying a swap error. Swap error represents + a confusion between the same or similar parts which belong to different + persons. + inv_prob: The probability of applying an inversion error. Inversion error + occurs when a pose estimation model is confused between semantically + similar parts that belong to the same instance. + miss_prob: The probability of applying a miss error. Miss error represents a + large displacement from the GT keypoint position. """ if isinstance(keypoint_sigmas, float): keypoint_sigmas = num_keypoints * [keypoint_sigmas] - if keypoints_symmetry is None: - keypoints_symmetry = keypoints_symmetry self.keypoint_sigmas = np.array(keypoint_sigmas) self.keypoints_symmetry = keypoints_symmetry self.num_keypoints = num_keypoints + self.jitter_prob = jitter_prob + self.swap_prob = swap_prob + self.inv_prob = inv_prob + self.miss_prob = miss_prob def __call__( self, keypoints: np.ndarray, near_keypoints: np.ndarray, - area: float, # ?? - #num_overlap: int, # ?? + area: float, image_size: tuple[int, int], ) -> np.ndarray: """Samples keypoints @@ -91,7 +146,6 @@ def __call__( near_keypoints: (num_other_individuals, num_keypoints, x-y-visibility) joints from other individuals near this one, for which keypoints might be swapped area: the total area of the bounding box surrounding the keypoints - num_overlap: Returns: the generative sampled keypoints, of shape (num_keypoints, x-y-visibility) @@ -113,14 +167,7 @@ def __call__( # if keypoints[j, 2] == 0: # synth_joints[j] = estimated_joints[j] - # instead we fill empty annotations with the mean of the other annotations - # in order to prevent corrupted bboxes - # for j in range(self.num_keypoints): - # #if keypoints[j, 2] == 0: - # if sum(keypoints[j,:2]) == 0: - # synth_joints[j] = np.mean(keypoints[keypoints[:, 2] > 0], axis=0) - - #num_valid_joint = np.sum(keypoints[:, 2] > 0) + # num_valid_joint = np.sum(keypoints[:, 2] > 0) N = 500 # TODO: do not know how this is set for j in range(self.num_keypoints): @@ -135,29 +182,34 @@ def __call__( coord_list.append(swap_coord) # on top of inv gt, swap inv gt - # FIXME: In the original codebase, they only swap symmetric keypoints. As - # we don't always have symmetries for keypoints in DeepLabCut, we swap any - # keypoint with any other keypoint by randomly selecting keypoints to swap - kps_symmetry = self.keypoints_symmetry - pair_exist = False - # for now, we randomly sample keypoint pairs to swap - kps_symmetry = np.random.choice(list(range(self.num_keypoints)), - size=(self.num_keypoints//2, 2), replace=False) - for (q, w) in kps_symmetry: + if self.keypoints_symmetry is None or len(self.keypoints_symmetry) == 0: + # randomly sample keypoint pairs to swap + kps_symmetry = np.random.choice( + list(range(self.num_keypoints)), + size=(self.num_keypoints // 2, 2), + replace=False, + ) + else: + kps_symmetry = self.keypoints_symmetry + + pair_idx = None + for q, w in kps_symmetry: if j == q or j == w: if j == q: pair_idx = w else: pair_idx = q - pair_exist = True - if pair_exist and (keypoints[pair_idx, 2] > 0): + + if pair_idx is not None and (keypoints[pair_idx, 2] > 0): inv_coord = np.expand_dims(synth_joints[pair_idx, :2], 0) coord_list.append(inv_coord) else: coord_list.append(np.empty([0, 2])) - if pair_exist: - swap_inv_coord = near_keypoints[near_keypoints[:, pair_idx, 2] > 0, pair_idx, :2] + if pair_idx is not None: + swap_inv_coord = near_keypoints[ + near_keypoints[:, pair_idx, 2] > 0, pair_idx, :2 + ] coord_list.append(swap_inv_coord) else: coord_list.append(np.empty([0, 2])) @@ -169,12 +221,7 @@ def __call__( # jitter error synth_jitter = np.zeros(3) - - # if num_valid_joint <= 4: - # jitter_prob = 0.20 - # else: - # jitter_prob = 0.15 - jitter_prob = 0.16 + jitter_prob = self.jitter_prob angle = np.random.uniform(0, 2 * math.pi, [N]) r = np.random.uniform(ks_85_dist[j], ks_50_dist[j], [N]) @@ -186,8 +233,14 @@ def __call__( if i == jitter_idx: continue dist_mask = np.logical_and( - dist_mask, np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r + dist_mask, + np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + + (tot_coord_list[i][1] - y) ** 2 + ) + > r, ) + x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) if len(x) > 0: @@ -198,14 +251,7 @@ def __call__( # miss error synth_miss = np.zeros(3) - - # if num_valid_joint <= 2: - # miss_prob = 0.20 - # elif num_valid_joint <= 4: - # miss_prob = 0.13 - # else: - # miss_prob = 0.05 - miss_prob = 0.10 + miss_prob = self.miss_prob miss_pt_list = [] for miss_idx in range(len(tot_coord_list)): @@ -219,8 +265,11 @@ def __call__( continue dist_mask = np.logical_and( dist_mask, - np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > ks_50_dist[ - j] + np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + + (tot_coord_list[i][1] - y) ** 2 + ) + > ks_50_dist[j], ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) @@ -243,11 +292,11 @@ def __call__( # inversion prob synth_inv = np.zeros(3) - inv_prob = 0.03 - if pair_exist and keypoints[pair_idx, 2] > 0: + inv_prob = self.inv_prob + if pair_idx is not None and keypoints[pair_idx, 2] > 0: angle = np.random.uniform(0, 2 * math.pi, [N]) r = np.random.uniform(0, ks_50_dist[j], [N]) - inv_idx = (len(coord_list[0]) + len(coord_list[1])) + inv_idx = len(coord_list[0]) + len(coord_list[1]) x = tot_coord_list[inv_idx][0] + r * np.cos(angle) y = tot_coord_list[inv_idx][1] + r * np.sin(angle) dist_mask = True @@ -255,9 +304,12 @@ def __call__( if i == inv_idx: continue dist_mask = np.logical_and( - dist_mask, np.sqrt( - (tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2 - ) > r + dist_mask, + np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + + (tot_coord_list[i][1] - y) ** 2 + ) + > r, ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) @@ -270,17 +322,14 @@ def __call__( # swap prob synth_swap = np.zeros(3) swap_exist = (len(coord_list[1]) > 0) or (len(coord_list[3]) > 0) - - # if (num_valid_joint <= 4 and num_overlap > 0) or (num_valid_joint <= 5 and num_overlap >= 1): - # swap_prob = 0.10 - # else: - # swap_prob = 0.04 - swap_prob = 0.08 - + swap_prob = self.swap_prob + if swap_exist: swap_pt_list = [] for swap_idx in range(len(tot_coord_list)): - if swap_idx == 0 or swap_idx == len(coord_list[0]) + len(coord_list[1]): + if swap_idx == 0 or swap_idx == len(coord_list[0]) + len( + coord_list[1] + ): continue angle = np.random.uniform(0, 2 * math.pi, [N]) r = np.random.uniform(0, ks_50_dist[j], [N]) @@ -290,15 +339,19 @@ def __call__( for i in range(len(tot_coord_list)): if i == 0 or i == len(coord_list[0]) + len(coord_list[1]): dist_mask = np.logical_and( - dist_mask, np.sqrt( - (tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2 - ) > r + dist_mask, + np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + + (tot_coord_list[i][1] - y) ** 2 + ) + > r, ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) if len(x) > 0: coord = np.transpose(np.vstack([x, y]), [1, 0]) swap_pt_list.append(coord) + if len(swap_pt_list) > 0: swap_pt_list = np.concatenate(swap_pt_list, axis=0).reshape(-1, 2) rand_idx = random.randrange(0, len(swap_pt_list)) @@ -320,8 +373,14 @@ def __call__( if i == good_idx: continue dist_mask = np.logical_and( - dist_mask, np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r + dist_mask, + np.sqrt( + (tot_coord_list[i][0] - x) ** 2 + + (tot_coord_list[i][1] - y) ** 2 + ) + > r, ) + x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) if len(x) > 0: @@ -341,8 +400,6 @@ def __call__( if synth_good[2] == 0: good_prob = 0 - #swap_prob = 0 - normalizer = jitter_prob + miss_prob + inv_prob + swap_prob + good_prob if normalizer == 0: synth_joints[j] = 0 @@ -364,8 +421,6 @@ def __call__( synth_joints[nan_mask, 2] = 0 np.clip(synth_joints[:, 0], 0, image_size[1], out=synth_joints[:, 0]) np.clip(synth_joints[:, 1], 0, image_size[0], out=synth_joints[:, 1]) - # print('synth_joints', synth_joints) - return synth_joints def get_distance_wrt_keypoint_sim(self, ks: float, area: float) -> np.ndarray: diff --git a/deeplabcut/pose_estimation_pytorch/data/snapshots.py b/deeplabcut/pose_estimation_pytorch/data/snapshots.py new file mode 100644 index 0000000000..dce0a64d39 --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/data/snapshots.py @@ -0,0 +1,74 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Code to handle storing models""" +from __future__ import annotations + +import re +import warnings +from dataclasses import dataclass, field +from pathlib import Path + +import numpy as np +import torch + + +@dataclass(frozen=True) +class Snapshot: + """A snapshot for a model""" + + best: bool + epochs: int | None + path: Path + + def uid(self) -> str: + return self.path.stem.split("-")[-1] + + @staticmethod + def from_path(path: Path) -> "Snapshot": + best = "-best" in path.stem + epochs = int(path.stem.split("-")[-1]) + return Snapshot(best=best, epochs=epochs, path=path) + + +def list_snapshots( + model_folder: Path, + snapshot_prefix: str, + best_in_last: bool = True, +) -> list[Snapshot]: + """Lists snapshots in a model folder. + + Args: + model_folder: The model in which the snapshots are found. + snapshot_prefix: The prefix for the snapshot names. + best_in_last: Whether to place the snapshot with the best performance in the + last position in the list, even if it wasn't the last epoch. + + Returns: + The snapshots stored in a folder, sorted by the number of epochs they were + trained for. If ``best_in_last=True`` and a best snapshot exists, it will be + the last one in the list. + """ + def _sort_key(snapshot: Snapshot) -> int: + return snapshot.epochs + + def _sort_key_best_as_last(snapshot: Snapshot) -> tuple[int, int]: + return 1 if snapshot.best else 0, snapshot.epochs + + pattern = r"^(" + snapshot_prefix + r"(-best)?-\d+\.pt)$" + snapshots = [ + Snapshot.from_path(f) for f in model_folder.iterdir() if re.match(pattern, f.name) + ] + + sort_key = _sort_key + if best_in_last: + sort_key = _sort_key_best_as_last + snapshots.sort(key=sort_key) + return snapshots diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 894cb49e9f..f3e23731a4 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -49,7 +49,7 @@ def __init__( backbone = BACKBONES.build(backbone) super().__init__(stride=backbone.stride, **kwargs) - + if not isinstance(kpt_encoder, BaseKeypointEncoder): if "img_size" not in kpt_encoder: kpt_encoder["img_size"] = img_size @@ -57,25 +57,33 @@ def __init__( self.cond_enc = kpt_encoder self.backbone = backbone - self.rgb_preNet = self._make_preNet(num_inputs=3, num_outputs=3, input_image=True) - self.cond_preNet = self._make_preNet(num_inputs=self.cond_enc.num_channels, num_outputs=3, input_image=False) - + self.rgb_preNet = self._make_preNet( + num_inputs=3, num_outputs=3, input_image=True + ) + self.cond_preNet = self._make_preNet( + num_inputs=self.cond_enc.num_channels, num_outputs=3, input_image=False + ) + self.init_weights() - - def _make_preNet(self, num_inputs, num_outputs, input_image = False): - if not input_image: # cond + + def _make_preNet(self, num_inputs, num_outputs, input_image=False): + if not input_image: # cond preNet = nn.Sequential( - nn.Conv2d(num_inputs, num_outputs, kernel_size=7, stride = 1, padding='same'), - nn.BatchNorm2d(num_outputs)) + nn.Conv2d( + num_inputs, num_outputs, kernel_size=7, stride=1, padding="same" + ), + nn.BatchNorm2d(num_outputs), + ) else: preNet = nn.Sequential( - nn.Conv2d(num_inputs, 64, kernel_size=3, stride = 1, padding='same'), - nn.BatchNorm2d(64), - nn.Conv2d(64, num_outputs, kernel_size = 7, stride = 1, padding='same'), - nn.BatchNorm2d(num_outputs)) + nn.Conv2d(num_inputs, 64, kernel_size=3, stride=1, padding="same"), + nn.BatchNorm2d(64), + nn.Conv2d(64, num_outputs, kernel_size=7, stride=1, padding="same"), + nn.BatchNorm2d(num_outputs), + ) return preNet - def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): + def forward(self, x: torch.Tensor, cond_kpts: np.ndarray | torch.Tensor) -> torch.Tensor: """Forward pass through the conditional preNet + backbone. Args: @@ -85,10 +93,10 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): Returns: the feature map """ - # create conditional heatmap if isinstance(cond_kpts, torch.Tensor): cond_kpts = cond_kpts.detach().numpy() + cond_hm = self.cond_enc(cond_kpts.squeeze(1), x.size()[2:]) cond_hm = torch.from_numpy(cond_hm).float().to(x.device) cond_hm = cond_hm.permute(0, 3, 1, 2) # (B, C, H, W) @@ -98,13 +106,12 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): x = x0 + x1 return self.backbone(x) - - def init_weights(self, pretrained=None): - # Initialize prenet with kaiming initialization + + def init_weights(self): + """Initialize PreNet weights from a Normal distribution.""" for prenet in [self.rgb_preNet, self.cond_preNet]: for m in prenet.modules(): if isinstance(m, nn.Conv2d): - #nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') nn.init.normal_(m.weight, std=0.001) if m.bias is not None: nn.init.constant_(m.bias, 0) diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/nms.py b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py new file mode 100644 index 0000000000..05e1dce2cb --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py @@ -0,0 +1,92 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Methods for non-maximum suppression of detected poses.""" +import numpy as np + +from deeplabcut.core.inferenceutils import calc_object_keypoint_similarity + + +def nms_oks( + predictions: np.ndarray, + oks_threshold: float, + oks_sigmas: float | np.ndarray = 0.1, + oks_margin: float = 1.0, + score_threshold: float | None = None, + order: np.ndarray | None = None, +) -> np.ndarray: + """Implementation of NMS using OKS. + + Args: + predictions: The predicted poses, of shape (num_predictions, num_keypoints, 3). + oks_threshold: The threshold for NMS. Keeps predictions for which the OKS score + is below this threshold. + oks_sigmas: The sigmas to use to compute OKS scores. + oks_margin: The margin to add around keypoints when computing area. + score_threshold: If not None, computes NMS using only keypoints for which the + score is above this threshold. + order: If predictions should be sorted by another means than score, the order + to use in NMS. + + Returns: + An array of length num_predictions indicating which keypoints should be kept. + """ + if len(predictions) == 0: + return np.zeros(0, dtype=bool) + elif len(predictions) == 1: + return np.ones(1, dtype=bool) + + predictions = predictions.copy() + + # mask keypoints with score below the threshold + if score_threshold is None: + score_threshold = 0.0 + predictions[predictions[:, :, 2] < score_threshold] = np.nan + + # get visibility masks for the keypoints and individuals + kpt_vis = np.all(~np.isnan(predictions), axis=-1) + idv_vis = np.sum(kpt_vis, axis=-1) > 1 # need at least 2 keypoints to compute OKS + + # if no keypoints match the visibility criteria, mask all + if np.sum(idv_vis) == 0: + return np.zeros(len(predictions), dtype=bool) + + # mask keypoints that aren't visible + predictions[~kpt_vis] = np.nan + + if order is None: + # compute scores for each individual + scores = np.zeros(len(predictions)) + scores[idv_vis] = np.nanmean(predictions[idv_vis, :, 2], axis=-1) + + # only compute OKS for non-zero score poses + order = scores.argsort()[::-1] + order = order[scores[order] > 0] + + # NMS suppression + keep = np.zeros(len(predictions), dtype=bool) + while len(order) > 0: + i = order[0] + order = order[1:] + keep[i] = True + + oks_scores = [ + calc_object_keypoint_similarity( + predictions[i], + predictions[j], + sigma=oks_sigmas, + margin=oks_margin, + ) + for j in order + ] + to_keep = [s < oks_threshold and not np.isnan(s) for s in oks_scores] + order = [idx for idx, kept in zip(order, to_keep) if kept] + + return keep diff --git a/deeplabcut/pose_estimation_pytorch/runners/__init__.py b/deeplabcut/pose_estimation_pytorch/runners/__init__.py index e5a5922df6..3783e2a76d 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/runners/__init__.py @@ -16,6 +16,7 @@ Runner, set_load_weights_only, ) +from deeplabcut.pose_estimation_pytorch.runners.ctd import CTDTrackingConfig from deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping import DynamicCropper from deeplabcut.pose_estimation_pytorch.runners.inference import ( build_inference_runner, diff --git a/deeplabcut/pose_estimation_pytorch/runners/ctd.py b/deeplabcut/pose_estimation_pytorch/runners/ctd.py new file mode 100644 index 0000000000..29a4bdb6ec --- /dev/null +++ b/deeplabcut/pose_estimation_pytorch/runners/ctd.py @@ -0,0 +1,85 @@ +# +# DeepLabCut Toolbox (deeplabcut.org) +# © A. & M.W. Mathis Labs +# https://github.com/DeepLabCut/DeepLabCut +# +# Please see AUTHORS for contributors. +# https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS +# +# Licensed under GNU Lesser General Public License v3.0 +# +"""Configuration for CTD tracking""" +from dataclasses import dataclass + + +@dataclass +class CTDTrackingConfig: + """Configuration for CTD tracking. + + Args: + bu_on_lost_idv: When True, the BU model is run when there are fewer conditions + found than the expected number of individuals in the video. + bu_min_frequency: The minimum frequency at which the BU model is run to generate + conditions. If None, the BU model is only run to initialize the pose in the + first frame, and then is not run again. If a positive number N, the BU model + is run every N frames. The BU predictions are then combined with the CTD + predictions to continue the tracklets. + bu_max_frequency: The maximum frequency at which the BU model can be run. Must + be greater than `bu_min_frequency`. When there are fewer conditions than + individuals expected in the video and `bu_on_lost_idv` is True, the BU model + may be run on every frame. This can happen if individuals can disappear from + the video, and each frame may have a variable number of individuals. If + `bu_max_frequency` is set to N, then the BU model will be run at most every + N-th frame, which improves the inference speed of the model. + threshold_bu_add: The OKS threshold below which a BU pose must be (wrt. any + existing CTD pose) to be added to the poses. + threshold_ctd: The score threshold below which detected keypoints are NOT given + to the CTD model to predict pose for the next frame. + threshold_nms: The OKS threshold for non-maximum suppression to remove + duplicates poses when two CTD model predictions converge to a single animal. + """ + bu_on_lost_idv: bool = True + bu_min_frequency: int | None = None + bu_max_frequency: int | None = 100 + threshold_bu_add: float = 0.3 + threshold_ctd: float = 0.05 + threshold_nms: float = 0.9 + + @staticmethod + def build(config: dict, video_fps: float | None = None) -> "CTDTrackingConfig": + """Builds a CTD tracking configuration from a configuration dictionary. + + Examples: + Building a CTDTrackingConfig from a basic dict: + >>> ctd_tracking = CTDTrackingConfig.build( + >>> dict(bu_on_lost_idv=True, threshold_nms=0.75) + >>> ) + + Building a CTDTrackingConfig from a basic dict: + >>> ctd_tracking = CTDTrackingConfig.build( + >>> dict( + >>> bu_on_lost_idv=True, + >>> bu_max_frequency=5, # When no FPS is given, this is in frames! + >>> threshold_nms=0.5, + >>> ) + >>> ) + + Building a CTDTrackingConfig from a dict for a video with a given FPS: + >>> ctd_tracking = CTDTrackingConfig.build( + >>> dict( + >>> bu_on_lost_idv=True, + >>> bu_min_frequency=1, # When an FPS is given, this is in seconds! + >>> bu_max_frequency=5, # When an FPS is given, this is in seconds! + >>> threshold_ctd=0.1, + >>> threshold_nms=0.9 + >>> ), + >>> video_fps=30.0, + >>> ) + """ + kwargs = {**config} + if video_fps is not None: + if "bu_min_frequency" in config: + kwargs["bu_min_frequency"] = int(config["bu_min_frequency"] * video_fps) + if "bu_max_frequency" in config: + kwargs["bu_max_frequency"] = int(config["bu_max_frequency"] * video_fps) + return CTDTrackingConfig(**kwargs) diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 6651f29d4d..3752aac9fd 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -18,7 +18,10 @@ import torch import torch.nn as nn +import deeplabcut.pose_estimation_pytorch.post_processing.nms as nms +import deeplabcut.pose_estimation_pytorch.runners.ctd as ctd import deeplabcut.pose_estimation_pytorch.runners.shelving as shelving +from deeplabcut.core.inferenceutils import calc_object_keypoint_similarity from deeplabcut.pose_estimation_pytorch.data.postprocessor import Postprocessor from deeplabcut.pose_estimation_pytorch.data.preprocessor import LoadImage, Preprocessor from deeplabcut.pose_estimation_pytorch.models.detectors import BaseDetector @@ -316,30 +319,48 @@ def predict( return predictions -class CTDInferenceRunner(InferenceRunner[PoseModel]): - """Runner for pose estimation inference""" +class CTDInferenceRunner(PoseInferenceRunner): + """Runner for pose estimation inference + + Args: + model: The CTD model to run inference with. + bu_runner: A runner for the BU model to run inference with. If no BU runner is + given, conditions must be given in the context for the data. Otherwise an + error will be raised during inference. + tracking: Whether to track using the CTD model. If + """ def __init__( self, model: PoseModel, - bu_runner: PoseInferenceRunner, - tracking: bool = True, + bu_runner: PoseInferenceRunner | None = None, + ctd_tracking: bool | ctd.CTDTrackingConfig = False, **kwargs, ): super().__init__(model, **kwargs) self.bu_runner = bu_runner - self.bu_runner.model.eval() - self.tracking = tracking + if bu_runner is not None: + self.bu_runner.model.eval() - self._image_loader = LoadImage() + self.tracking = None + if isinstance(ctd_tracking, ctd.CTDTrackingConfig): + self.tracking = ctd_tracking + elif ctd_tracking: # generate default config + self.tracking = ctd.CTDTrackingConfig() if self.tracking and self.batch_size != 1: - print( - "Dynamic cropping can only be used with batch size 1. Setting the batch" - " size to 1." - ) + print("CTD tracking can only be used with batch size 1. Updating it.") self.batch_size = 1 + self._image_loader = LoadImage() + + # Stored poses and IDX -> ID map for CTD tracking + self._bu_age = -1 + self._missing_idvs = False + self._prev_pose = None + self._idx_to_id = None + self._ctd_track_ages = None # the age of each CTD tracklet + @torch.no_grad() def inference( self, @@ -369,7 +390,7 @@ def inference( ] """ if self.tracking: - return self._inference(images, shelf_writer) + return self._ctd_tracking_inference(images, shelf_writer) results = [] for data in images: @@ -429,6 +450,10 @@ def add_conditions( # Load the image once - then given as a numpy array to CTD image, _ = self._image_loader(inputs, context) + # If the conditional keypoints are in the context, return the context + if "cond_kpts" in context: + return image, context + # Run the pre-processor if self.bu_runner.preprocessor is not None: inputs, context = self.bu_runner.preprocessor(image, context) @@ -450,7 +475,7 @@ def add_conditions( return image, {"cond_kpts": conds} - def _inference( + def _ctd_tracking_inference( self, images: ( Iterable[str | Path | np.ndarray] @@ -458,22 +483,21 @@ def _inference( ), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: - prev_pose = None - results = [] for data in images: - inputs, context = self._prepare_ctd_inputs(data, prev_pose) + inputs, context = self._prepare_ctd_inputs(data) model_kwargs = context.pop("model_kwargs", {}) predictions = self.predict(inputs, **model_kwargs) if self.postprocessor is not None: # Pop the "cond_kpts" from the context so there's no re-scoring - if prev_pose is not None: + # This is required when tracking with CTD, otherwise scores go to 0 + if self._prev_pose is not None: context.pop("cond_kpts") predictions, _ = self.postprocessor(predictions, context) # Set the predictions as context for the next frame - prev_pose = predictions["bodyparts"][..., :3] + self._ctd_tracking_postprocess(predictions) if shelf_writer is not None: shelf_writer.add_prediction( @@ -487,29 +511,34 @@ def _inference( return results - def _prepare_ctd_inputs( - self, - data, - prev_pose: np.ndarray | None, - ) -> tuple[torch.Tensor, dict[str, Any]]: - # Get valid conditions - conds = None - if prev_pose is not None: - bad_data = np.any(prev_pose <= 0 | np.isnan(prev_pose), axis=2) - pred_mask = ~np.all(bad_data | (prev_pose[..., 2] <= 0.25), axis=1) - if np.sum(pred_mask) > 0: - conds = prev_pose[pred_mask] - - # If there's any valid pose, use it as a condition for the next frame - if conds is None: + def _prepare_ctd_inputs(self, data) -> tuple[torch.Tensor, dict[str, Any]]: + # If there's no valid poses, use the BU model to get conditions + self._bu_age += 1 + if ( + self._prev_pose is None + or ( + self._missing_idvs + and self.tracking.bu_on_lost_idv + and self._bu_age >= self.tracking.bu_max_frequency + ) + or ( + self.tracking.bu_min_frequency is not None + and self._bu_age >= self.tracking.bu_min_frequency + ) + ): + self._bu_age = 0 inputs, context = self.add_conditions(data) + + if self._prev_pose is not None: + context["cond_kpts"] = self._merge_conditions(context["cond_kpts"]) + else: if isinstance(data, (str, Path, np.ndarray)): inputs, context = data, {} else: inputs, context = data - context["cond_kpts"] = conds + context["cond_kpts"] = self._prev_pose if self.preprocessor is None: return torch.as_tensor(inputs), context @@ -517,6 +546,105 @@ def _prepare_ctd_inputs( inputs, context = self.preprocessor(inputs, context) return inputs, context + def _ctd_tracking_postprocess(self, predictions: dict[str, np.ndarray]) -> None: + """Post-processes predictions. In-place changes to the predictions dict.""" + # reorder the previous poses so the indices match the track IDs + if self._idx_to_id is not None: + predictions["bodyparts"] = predictions["bodyparts"][self._idx_to_id] + + # mask all keypoints below the CTD tracking threshold + prev_pose = predictions["bodyparts"][..., :3].copy() + prev_pose[prev_pose[..., 2] <= self.tracking.threshold_ctd] = np.nan + + # apply NMS on the conditions, keeping older tracks + order = None + if self._ctd_track_ages is not None: + ordering = self._ctd_track_ages.copy() + + # sort by track age, then score + vis = np.sum(np.all(~np.isnan(prev_pose), axis=-1), axis=-1) > 1 + scores = np.nanmean(prev_pose[vis, :, 2], axis=-1) + ordering[vis] += scores + + # only keep non-zero scores + order = ordering.argsort()[::-1] + order = order[ordering[order] > 0] + + nms_mask = nms.nms_oks( + prev_pose, + oks_threshold=self.tracking.threshold_nms, + oks_sigmas=0.1, + oks_margin=1.0, + score_threshold=self.tracking.threshold_ctd, + order=order, + ) + + # Set the previous pose and ID ordering + if np.any(nms_mask): + self._prev_pose = prev_pose[nms_mask] + + # get the IDs of the kept poses + found_idx_to_id = np.where(nms_mask)[0] + missing_ids = np.where(~nms_mask)[0] + self._idx_to_id = np.concatenate([found_idx_to_id, missing_ids]) + + # add 1 to the age of kept tracks + if self._ctd_track_ages is None: + self._ctd_track_ages = np.zeros(len(self._idx_to_id)) + self._ctd_track_ages[nms_mask] += 1 + self._ctd_track_ages[~nms_mask] = 0 + + # check if there are any missing individuals + self._missing_idvs = len(self._prev_pose) != len(self._idx_to_id) + else: + self._prev_pose = None + self._idx_to_id = None + self._idx_ages = None + + def _merge_conditions(self, bu_cond: np.ndarray) -> np.ndarray: + """ + Merges conditions made by a BU model with existing conditions from CTD tracking. + """ + # prepare the BU conditions for matching + bu_cond = bu_cond.copy()[:, :, :3] + # mask low-quality keypoints + bu_cond[bu_cond[..., 2] < self.tracking.threshold_ctd] = np.nan + + # remove non-visible individuals + kpt_vis = np.all(~np.isnan(bu_cond), axis=-1) + idv_vis = np.sum(kpt_vis, axis=-1) > 1 # need at least 2 kpts for OKS + + # if no valid BU predictions are left, return the CTD conditions + if np.sum(idv_vis) == 0: + return self._prev_pose + + # match BU conditions to CTD poses from the highest score to the lowest + bu_cond = bu_cond[idv_vis] + new_conditions = [] + for bu_pose in bu_cond: + best_oks = 0 + for ctd_pose in self._prev_pose: + best_oks = max( + best_oks, + calc_object_keypoint_similarity(bu_pose, ctd_pose, sigma=0.1) + ) + + if best_oks < self.tracking.threshold_bu_add: + new_conditions.append((best_oks, bu_pose)) + + # add the conditions with the lowest OKS score + new_conditions = [ + c[1] for c in sorted(new_conditions, key=lambda x: x[0]) + ] + + # if there are no new conditions, + if len(new_conditions) == 0: + return self._prev_pose + + new_conditions = np.stack(new_conditions, axis=0) + cond_pose = np.concatenate([self._prev_pose, new_conditions], axis=0) + return cond_pose[:len(self._idx_to_id)] + class DetectorInferenceRunner(InferenceRunner[BaseDetector]): """Runner for object detection inference""" @@ -624,10 +752,6 @@ def build_inference_runner( dynamic = None if task == Task.CTD: - # FIXME(niels) - allow running CTD with conditions from a file - if "bu_runner" not in kwargs: - raise ValueError(f"A `bu_runner` must be given for CTD inference.") - return CTDInferenceRunner(**kwargs) return PoseInferenceRunner(dynamic=dynamic, **kwargs) diff --git a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py index 74203f5be1..a5e9f23cf6 100755 --- a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py @@ -11,7 +11,6 @@ """Code to handle storing models""" from __future__ import annotations -import re import warnings from dataclasses import dataclass, field from pathlib import Path @@ -19,21 +18,7 @@ import numpy as np import torch - -@dataclass(frozen=True) -class Snapshot: - best: bool - epochs: int | None - path: Path - - def uid(self) -> str: - return self.path.stem.split("-")[-1] - - @staticmethod - def from_path(path: Path) -> "Snapshot": - best = "-best" in path.stem - epochs = int(path.stem.split("-")[-1]) - return Snapshot(best=best, epochs=epochs, path=path) +from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot @dataclass @@ -182,25 +167,9 @@ def snapshots(self, best_in_last: bool = True) -> list[Snapshot]: trained for. If ``best_in_last=True`` and a best snapshot exists, it will be the last one in the list. """ - - def _sort_key(snapshot: Snapshot) -> int: - return snapshot.epochs - - def _sort_key_best_as_last(snapshot: Snapshot) -> tuple[int, int]: - return 1 if snapshot.best else 0, snapshot.epochs - - pattern = r"^(" + self.snapshot_prefix + r"(-best)?-\d+\.pt)$" - snapshots = [ - Snapshot.from_path(f) - for f in self.model_folder.iterdir() - if re.match(pattern, f.name) - ] - - sort_key = _sort_key - if best_in_last: - sort_key = _sort_key_best_as_last - snapshots.sort(key=sort_key) - return snapshots + return list_snapshots( + self.model_folder, self.snapshot_prefix, best_in_last=best_in_last + ) def snapshot_path(self, epoch: int, best: bool = False) -> Path: """ diff --git a/deeplabcut/pose_estimation_tensorflow/predict_videos.py b/deeplabcut/pose_estimation_tensorflow/predict_videos.py index cb9eb5e25f..dd4c66c0e8 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_videos.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_videos.py @@ -1436,6 +1436,11 @@ def _convert_detections_to_tracklets( f"Invalid tracking method. Only {', '.join(trackingutils.TRACK_METHODS)} are currently supported." ) + if track_method == "ctd": + raise ValueError( + "CTD tracking is only available for BUCTD models with the PyTorch engine." + ) + joints = data["metadata"]["all_joints_names"] partaffinityfield_graph = data["metadata"]["PAFgraph"] paf_inds = data["metadata"]["PAFinds"] diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index 274802370e..8e87974633 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -1147,6 +1147,11 @@ def stitch_tracklets( cfg = auxiliaryfunctions.read_config(config_path) track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) + if track_method == "ctd": + raise ValueError( + "CTD tracking occurs directly during video analysis. No need to call " + "`stitch_tracklets` with `track_method=='ctd'`." + ) if animal_names is None: animal_names = cfg["individuals"] diff --git a/deeplabcut/utils/auxfun_multianimal.py b/deeplabcut/utils/auxfun_multianimal.py index 5f0314f887..f9dd5ab346 100644 --- a/deeplabcut/utils/auxfun_multianimal.py +++ b/deeplabcut/utils/auxfun_multianimal.py @@ -74,7 +74,8 @@ def get_track_method(cfg, track_method=""): # check if it exists: if track_method not in TRACK_METHODS: raise ValueError( - f"Invalid tracking method. Only {', '.join(TRACK_METHODS)} are currently supported." + f"Invalid tracking method. Only {', '.join(TRACK_METHODS)} are " + "currently supported." ) return track_method else: # default diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py index 57622710ca..a9cdb46825 100644 --- a/deeplabcut/utils/make_labeled_video.py +++ b/deeplabcut/utils/make_labeled_video.py @@ -169,16 +169,15 @@ def CreateVideo( # Draw bounding boxes if required and present if plot_bboxes and bboxes_list: bboxes = bboxes_list[index]["bboxes"] - bbox_scores = bboxes_list[index]["bbox_scores"] - n_bboxes = bboxes.shape[0] + bbox_scores = bboxes_list[index].get("bbox_scores") + n_bboxes = len(bboxes) for i in range(n_bboxes): - bbox = bboxes[i, :] + bbox = bboxes[i] x, y = bbox[0], bbox[1] x += x1 y += y1 w, h = bbox[2], bbox[3] - confidence = bbox_scores[i] - if confidence < bboxes_pcutoff: + if bbox_scores is not None and bbox_scores[i] < bboxes_pcutoff: continue rect_coords = rectangle_perimeter(start=(y, x), extent=(h, w)) @@ -351,14 +350,13 @@ def CreateVideoSlow( # Draw bounding boxes of required and present if plot_bboxes and bboxes_list: bboxes = bboxes_list[index]["bboxes"] - bbox_scores = bboxes_list[index]["bbox_scores"] - n_bboxes = bboxes.shape[0] + bbox_scores = bboxes_list[index].get("bbox_scores") + n_bboxes = len(bboxes) for i in range(n_bboxes): - bbox = bboxes[i, :] + bbox = bboxes[i] bbox_origin = (bbox[0], bbox[1]) (bbox_width, bbox_height) = (bbox[2], bbox[3]) - bbox_confidence = bbox_scores[i] - if bbox_confidence < bboxes_pcutoff: + if bbox_scores is not None and bbox_scores[i] < bboxes_pcutoff: continue rectangle = patches.Rectangle( bbox_origin, diff --git a/docs/pytorch/architectures.md b/docs/pytorch/architectures.md index 53dfaca621..f44896a259 100644 --- a/docs/pytorch/architectures.md +++ b/docs/pytorch/architectures.md @@ -73,9 +73,61 @@ but it might improve performance), you can simply edit your `pytorch_config.yaml Of course, any multi-animal model can also be used for single-animal projects! -## Information on Multi-Animal Models - -### Backbones with Part-Affinity Fields +## Approaches to multi-animal pose estimation + +Single-animal pose estimation is quite straightforward: the model takes an image as +input, and it outputs the predicted coordinate of each bodypart. + +Multi-animal pose estimation is more complex. Not only do you need to localize bodyparts +in the image, but you also need to group bodyparts per individual. There are two main +approaches to multi-animal pose estimation. + +The first approach, **bottom-up** pose estimation, starts by detecting bodyparts in the +image before figuring out how they belong together (i.e., which keypoints belong to the +same animal). + +![Schema representing the bottom-up approach to pose estimation]( +assets/bottom-up-approach.png) + +The second approach, **top-down** pose estimation, uses a two-step approach. A first +model (an object detector) is used to localize every animal present in the image through +its bounding box. Then, the pose for each animal is determined by predicting bodyparts +in each bounding box. The pose estimation + +![Schema representing the top-down approach to pose estimation]( +assets/top-down-approach.png) + +The top-down approach tends to be more accurate in less crowded scenes, as the pose +model only needs to process the pixels related to a single animal. However, in more +crowded scenes, the pose estimation task becomes ambiguous. Multiple overlapping +individuals will have very similar bounding boxes, and the pose model has no way of +knowing which animal it is supposed to predict keypoints for. + +The bottom-up approach does not have this ambiguïty, and also has the advantage of +only needing to run a pose estimation model, instead of needing to run an object +detector first. However, grouping keypoints is a difficult problem. + +A new approach to pose estimation, named bottom-up conditioned top-down (or BUCTD), was +introduced in [Zhou, Stoffl, Mathis, Mathis. "Rethinking Pose Estimation in Crowds: +Overcoming the Detection Information Bottleneck and Ambiguity." Proceedings of the +IEEE/CVF International Conference on Computer Vision (ICCV). 2023]( +https://openaccess.thecvf.com/content/ICCV2023/papers/Zhou_Rethinking_Pose_Estimation_in_Crowds_Overcoming_the_Detection_Information_Bottleneck_ICCV_2023_paper.pdf) +. It's a hybrid two-stage approach leveraging the strengths of the bottom-up and +top-down approaches to overcome the ambiguïty introduced through bounding boxes. Instead +of using an object detection model to localize individuals, it uses a bottom-up pose +estimation model. The predictions made by the bottom-up model are given as proposals (or +_conditions_) to the pose estimation model. This is illustrated in the figure below. + +
+ +
Zhou, Mu, et al. "Rethinking pose estimation in crowds: overcoming the +detection information bottleneck and ambiguity." Proceedings of the IEEE/CVF +International Conference on Computer Vision. 2023.
+
+ +### Bottom-up Models + +#### Backbones with Part-Affinity Fields As in DeepLabCut 2.X, the base multi-animal model is composed of a backbone (encoder) and a head predicting keypoints and part-affinity fields (PAFs). These PAFs are used to diff --git a/docs/pytorch/assets/bottom-up-approach.png b/docs/pytorch/assets/bottom-up-approach.png new file mode 100644 index 0000000000000000000000000000000000000000..025c292c8d7409fba6a36cf09df7f3995c38d851 GIT binary patch literal 519568 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�HK6#Wm`zv=lu5;ov3x#?{Zqj1Gd&t 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

FM(bdJD3xl^Q#>}hf1 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�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

d%VFhjKED|Vu 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/*r*fw}t!ag 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

Vg@|zCHum<-B9DlZ`&&)Zt&O94g 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

p5&t@}ZSzLE1k%~SDo$(f3Biz% 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 z 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π`D?g0vJg@yn5sI}(;(C5x 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

80p|e{ zQaa{cg*#e7SsKxw*Rf7?GD@zXaixu1go&q{bYnvcy~o!Ma;ku1efHvG0K+RRSp-n_ zQ{g`LVas&=1aS@?eaIeUeN!6#?-qLEy;f-cIhd)MZH5UdEwwX@CmqY@M;9CP%yI3c zwW|jS9)U*Ek6r*|3b}*8VK7j!!Jz?HSeD_LR4|~iD{CuhAXW6DjwY*1DaL7rF-sp` zy2WJjV&VbF6hmly>NV7;@p*MUsAL-Tr|c!)jaT*?-MFl$pQT||0g!;Q0DBnl+}+n^ zJp>n1)6;-8G=4C<9W()uYebEQInTmS!hf^Mn3|rLG!i0i9?a#^)`o9mM@I+GCz6k9 z%sS99cJsSN7-^434?ovgITfA@jaB(S29XNr^W8CMQgw4HKl2QBwV~st>iYcHd6zy9 z!Z3!J4+ZiyzvbCSh}3ukovk8tt(l`~f!d}P0lGebZwa6|J551NC!m+WK?QjgETi;n zw6xQ~g~FGPj#iGVLVLsoo1=&=Ra|k>n#Ji=p4~cg|H{6h8%Os_xB|ikEO~Zd8`+Cq zGB`}hubfS(W9DDhVL$7(%lhz~R0zIR5T~c|rEVk}?cfB~af@07UQs9gj8nRFppnZG zfEld7awtkywVx|GV8j$)=)$BNna5ybNc*5s%;#5F7+F}EuqMwfI>vEsiaxxU;dXBC zrBh0uO^uVPBfXqsCasXN_RA{|=2Gnn!;HKblc6fBm4dEsFhgk6ORzp#jB})vvn=wm zsHjv@=0(H9TLFp;62^iwJYWG*%{eT0&>pfV@4ey*TTV|i@mFkx1(Z*qI3`7*3sE~j zI6~^jN0nrbK^2R{U?xT2sTI^>()~PN=j-@*p72FhttG!ouY(@SZHP4vkMq z4qRqLk{S&^BnkqLntR$|DkUF~wlXBS0{4YSn&+7Lg94p4`kx2UN-om+R2p;j!rp)N z9+$~UF?MR$hp7vcg60zQtBlMSvkE+5B9YIe@xcwp35clJm!4NynP3e`g{r$b7&#Jb zY#UeL`L2KzU^s)>+paac71xk?<}%?iJu{bnQ?`acOV1DP5+2Fiu*xfC!>q~qXRL=W z^jI}Tl2mX&`G4P!SMCEZ^qrUHb(ah@)+as`i+~EH{YJKp*b;2t($WI9ASy6`)k6!T zRC1LH)Cy=`I>j{>74=JBMVVbP>ZRKyL!Lqmi%k@t6Z0>oNL zvtd&*GdX5$4Omq$WoDRc3owm2_A4t4v71`_R1BbwG7Uuat?QI!x+Y}inb282&?FV{-^;@w2%xH+oNQK-NzA=v!GXp}U2Vh^M6m}N z*|dp1*5kIaifA6)PzZLjtEbax7(=uYQzf`sR;?n0PpJi1o~mm+0xLxE5ufOBSWke7 zyyBj1%W;l7Zg{J+tXZ!*RvLfi?04OyAEa%dK4X2809WbX+0);38paS6;f{4sK zW)yoH$MUuH^ZW17@ru3#>K4Wh+y2-862}uk5`d@UgP#HrQM@#~+wzm&TK|{}Uj-_@ z___aUV+&>5d;8A-NWW|6VK|$plss8;(V~ zQfd3d!bnj1jg{m_9m!6!G8F`!!a4sTtl^u!Uk9V(2lX2#n|GZPqjK z5nF3}Gszm*N3rp~xaBsz-NN_$tBth(ci1i3QDP)7@4oY+QP|5~ixn5q&0&>7P^SMy z{Ag@h|VE_XJeSk?kAxWFGMp|8ZzJcDp zMItUS@g82IE+wiA#`M4ie5#~&to<<4WMVE`or$T@xq+5V8oZE@2eT--=n->+S{{?C+*9 ziE*Ggh5MMx5u_r;(b$okB9+RLb-*){Dt7WE4mj%4%eF@znsFHLUAy<#w;y?w&O_)A zDPk%y&qMQrNS6lxl=rcFO(@&4DFFVboG#9>=G^$|i2S(hhWQpV|Mq#l@V817Fb_ ztHPz*RLr6jRuApwZolnTyYIgH0ga0wcP2hoA19=zmNZc|-89{$< zi0T?FP8_juICEk4ez-X(f;QD1T_7YUY@bh$G>blFy4;h zDW|3D{1abe(Mw_Y{qwfAa9*hD`wsEGuYb*soLxjS`3}G@`=Q{bf7i{{z4^d}u9#IsFu1{NXJ=b=I5sf`wSjw^&A1p-S_r^t39~tFV7nDulD#(=4nqG>hv!t$Vuy z^S!cMBG{lv0MIDOM8F5LNO^S$#!-u?M1$&{>F#x%;f9&?7VH=w$&b* zQaTX119FcVpIZT_sybF~1F~+4Hvl*VVw@~N*Ap!ouPZFBVDrG&7RE8`l!B=uwml)z znT?GNFlQL!^PFYy`BR{iCGDD|*kPR^Bbm?Gpa4f;m1cC~r@0I>l za3Di->9KWxtBBZxfcjLjN>`O6tSy-mq%wo}E6SBYOrC<14vf^*;9!*o1gn6+LWZ%$ z^%H;;(2LNrF%S+r_Oi(M5crpEPKv-}ZhqO)$Vt8 zW0~XW9oS^M_EDivM=8k=X3mb$eihK_fNqSjj1(cO;uyZ?cNylcV^5y6B?9tjw9%Rg zAQebdB)=j1K*=IqX&SMU>bK2XM_drBzUp2Al8|&+>b$^lMLE9)jO*RCdDY(SjRXI$ zz5?rW$FnRO|J8?b;q$Z}l=JwUE}te#G-zS6k;Uu+0pTob#rH^*3vYN4->jeSi+IZ= zdFiDCjQ}Gp%1Ya)pR-!@DP0m|5mRqeO{`kDU8^>jy=b73nUCsItV|kJwG871J;Q_; zfYoUNI8pV-;##9GZveev2n|{_gy}VDwb2u2n7D#=$7|ncJ4dzx!ir3~nL3aJjMdnO zDwP7HlT-x=5{1EIV&=W)&QGGxNuc9usk4qul_3^jA6+?miR_beS(`X}%<`~-QBsDI zJPvLecD6b*h?EL2M_Ih~28RdnR%>%cE?ng4E40>4<2%2>(>8il-0j~=IfRIO)gE?Vc5Me z3&c3O!7&-GX=CA0x0U{#H(BrY>z)A^^}c*+#d$T#-?6oC8xKzv;Q3AZVvo8)88MsB z1{lA2-#6@`Lv_m>8BK%D9C#05E7D5A&N|2y@Kx3J3j1@OaP_15q5M~@ix%?$mA^eb zeL8iUo|@uXOWY5DqPjE$7*dNs{j7OH0f0_X1rb?_uWBroOIXts^Jn;@PMyL6jb}io zx(0zN00g89+nWH4UZe`+mTl~E6@#VO$1RXP1R%w>2Y)_FPaPo0mKtm-dWQ|o{+ksC zev&|nd(&YshcG;!G<8k_B<1q>AsQarTuPkFX19-{7zDrY= z=b4Pb&`R#WDLNKaXM4GBuINOgfO8is2p7$>3~NUK7ot6Ai5`%ZB;KA`())lr^@a|| zQh+Zi(_yd^ixk}b|LnbIkfryP-gj<}eQ)2+dAg^2GA027EC3=B00Rh;%OxePD6PDK zwpOw&SJ~w%e{#8Gf3U0M(g({QRAo!HYD?PHE~%xNKoTSvK;$$4CUwtr&hg&9Is5ng z?+iF30WO!jm>v%11$w&s_6=`1@A*IHoaa0Tii31mOVC#W1QsRTa{^stZX=vWHNyxz zY68^muK17SCGiz`I-Bb}PmO#Ty4`(LNpLIp2PipQMn9Aqa*(`^$^>?>w3M;+owD6= z;!aywCn!mKEJJYXA>hoxH5Q;1CpC$mD`g3g0DE=U@xnBi6`EY4WGCr{)=es&?q+t| zx`z)V>C+9rw>5=pb!`p0(6()^?7BeMhyMxzU>od<%JrI8eS5oo2L8HdKtA|p*{B1M zd|J1{>hsVqlw=|il)hxu+zfvIZ`S2~6CLI+J>7c-8YN2+i3%vRPL&{xO2W#vsO%#! zsVKQ24i5pVNTvdninOaZx4Oa)UMeBo5I|GFLPT_xW1apW-JL4AfW~l|MLTooKKuBC zk6TZw+q{6gu6WS#a8&9}qa~)dYn7)3W_e?PfTVIZrmmo8M)qfz3_vr7^P}tr_p6SR zO`xY2SD6POt5TCTEvV~j@W?3jEG;dwSd=IkrjQL91Kc4(j==N2{vkTR+sI1Mb;z_1 z>hCs3iCqiHU1#p3!7lO0Ht zQGv|K6DL`a)3!__gQ5XV3KIBJDwzB?HlyFkTC;cyxGO{604Ow(C{ad6d{s$F#mO)S z$kcfN?{x%^UDFqfe_5G7YA`+oT-(dYNtI@;Yx!RzTf!8VwiUqQ z!FDx=SeZ6a8%m()7$osJK-Gyr>&3U|rfj#D0y7!DqL-;pOSZkH&PzDhoZARoZcTa8w3&{MB-z1U{#1ppJ#G6C zxvye}83Idjqf00+NRJazfD4-IwG06?6^<0I;eHW!cAe~HjcSc1`G^+dBkdR_+g*Z2 zLH?Y-F>_pHXtL&NmMT+;$wLECuEyZhb+11HG}(xun}HI zMqG2QHE^Iv_q23BbqsMTApu0<2}|64b&Hz9?Zf(zoB>yvBXLkItdPT60|%ghkz1tO zS-kdJ*VkQtdG`!7DoY~}6+(JKrMj*44VPt8HpGn$WP#RvU15|0`gTq)4@WE^>>MS* z&aO_^$=S*b3RLiZkn&Z!DT!A|Ik8E0s6hwl!F~Jfb07PZ_0v75T1|*WT+TB>_E7}! z#4gP%fID#prTOUQ?Ba}Bs&UBTf#LAsuLau@7Q_KdhN6hxo+d10Xdn>S6&b!rC#vSO z&ace!`82t}CDT~Lw)U9q&@J6yarTfwP!_euBHS~8?iyT6&7eJSVB9XAKZlOrwuN(m zC(4u>Z@9j)#eM-A;dB<5ETP3Yji|XgF;mEIfCyX(QOEr^-vGpMrqK)^{V7NT7;4`1 z51#EH-d6Pd&l8MZSm<@)hPlAH4Us5dm(^v=$ir`EB~j6*qat7iw$0 zFBvlNMfA0IHy_~&3!uwpVr$BNr{SV1`>h573cf7H%GzQhAOV-nCOcGQk@rOjG+gPB z=(9o24z8I%K%Qo(JZ*7WfFn+TT-;fYZ4p>xcYJp2;1N5$f5f7@-!M;U)!OK&ZNLS~ zb^SBciELx<<^Rjh?l1uc+pMxUABar?65+rBC{abR^$=aw)dHNL z2!RhBvI=fk^!tYTY!3dc1iqDFjHd`-_KmpvNgdECuV+ybDB%EA&X!=cz@#{Hy4EcV zqdd<&+2r^ufW@MSN8Bq?FHj&;uP~!p_pY$i&tLRg@OMs&3o!0DcGP91&%N^wfhs_b z#{&R<*X%@~7y)Fd8nUyVx4~WkZcYY|`QqQXbr6TKE}sc-tdVL|fSX}5*1{_O3EK#g;R`~42m$6R)wJc{ zC8yU`ZGCY8Yo^Z4l~kjig8VXxfZN;cGjMC10m7hw205Sqt+D`DQTK@kpYp+M4gD&uX3x%-YiP$;tj%GRCHaDm(?N$FpN@Gk!*zs z*!UtA?ULizgyg*70*OB+`I8_ZrUMb~r#}9K?MJ3(7XZ-H*~$9|d@3_Uw;_vigpNT0 zg=Ugc3j3qQFv9%kB9bC?COE`tNJ)^nRL-Xc7_0zVB{aUcv|^VoGLsc>sFI*0eX=<9 zJo9HY`9Yb&Vh+7LCeJK0e^rZcn6Ah|ewXYcoscx_NOmU<7#$etp+nc~>}$*#W^oa~ zSVqoAfUyklg{A4{S~uuG_H%qHU2Ecv3%{T4jC;u5E0!MV;xs5~>{8Yg5 z&wbW)ANzs+=oWeex-|<}yWq|R3#m%eB%V1QWt(vp*bKj6r!1PBFJWTI+URdhvcIIq z9cYRm^(1j@VJKy1{EUlJ=X;a#w*Xo{-K_WD`JfG8cfm_9g1so~22K^jCIB26rUFe1=^_&as=c;WIbc2h z1*?)Vt<>hog0H#7Tbawg!G1dEk+rJl0hR4&fp!vOUj3FNe zq@+QxW<3NqZ!XQi!$R_lfL%?89bHV^f?~41iYfrS?B0m!hyo+&QOX{4oIWz}5xAC>l7cY&u>z{=Pu}gHZ=ZqR@H5~; zE15_cpHHJ9v@s7nUOBx6)HPSCzC|72o`FW$ON^!!Io{4rL}PJYJ;+ML;yxw~Yy%)Q zX@w>MIVp{|!g;BIq^`0tSrTdGN&ZBuvp}MdhcY;I0TsMc2lL+RAo9wtRbbMvYkiHk|;d~kX0vcILgR1y1i9$4ANbk zr~9zUBpX~kC32c&IF>k^qAQo?7|GVbv8dSrDIi{7Vzii-a-k$OG(xNmQR|vYp+Y1a z@QrlH0!iP2A-u0_@n&Yyv78FZO5u@yq66u4t9) zmS_h*1*nwrcponLo@_dYvL982!z`#JGTb{m+W;YK3PdSru8ek*C#XBN!qkG6xN_?n zh-%RUWL2@#0NF-?P-W@#9358yTWV4gRvckmgn|JFK z^B`l=K5~*#f{OPz6*nc+VYr1c{E6!$RJ zE;V7tq6h7bSsKa2xsxgY-O{_1M|Y5MQnpESkUGaBwE&GFj36lPx_*`A|69p^?};h= zN*~JQ=01kggbgb=$RjmV5eHP)oxlKe$F+z5iA<5xaKzgx+vqsO+_4JAe9&7w05AQg zN{Qw|%P&;`pc2Ko|M)%j#C;#7k%S;Kfp~U`u6)LlZa_=F9fZo98S`AS+dd0DS2bfptaqA;eg9 zPN(4BN(QKok4m+d_s^puq|mq|F?nkDN!1C8P6|6FW-KBcgT^095LZ@OrDHWj1}FxpT|BQGnMo}y(tDC-nnwJo zqhfGikf{%YHd9}7-IH;eC5q*|%eIO;sIhY)7WIY%cL6TqGWvPF-d`Dx8@1{DinAC^ z_E0v@lM$Ijz`NCny`S0D@A4t9+aYBS#WDTzpZ|rm?Yqw^fh!J3UH6o||54yhfdrZE z7?L@DoK1x?FP8~>WYGjLIyNI2UTN@2tFEEu5*A@4*R(KJ$uN_9bXjGA{y36M+jP7; zelG9f=fcNmD*3I`ciFvz>()K>KUrz;?^^A|e~%Oj(*$$#^g(nY>DUEm$nmD!8{!Aj zOrep0je5YAs!v&+ZrqurqAg}R?alfD`^m^3+K|7`Hn$75rVi)|GC)YLI3)lwLAAV^ zRYc+7L%@hdv>I`p+&xSl06=6j9_%vYMq+xf+j=Jf3;ejzi4nW77IfN_kv?S^^9Yku zzD9mP_lfw#AvF2~rk&K48gw{Dfgm~PTX~busW%xOpo|>{x`}US5PVXt#}>FB0Z9Vr zMQlQJ6jd|o$la(qpv{e}6D{Wk>S*JeG5rk&Ymc6tO8%S55B{2gjECxhnqUMP{m~wacHeEW{`>K}8OtsGkmVfhS{iqMc}F$(g!2VBWakpN z_uU@esI&9yea?+T8t<`xB65o$t%$coguX@gLgklaB1V*}NfQN}sk93LOJwQB_K%W{ z?69}bo^736EI=|SNw{Yjro||__an?b1b6GRpZvHzcJF<3E*5NN@`A0?lu{#0x3xpG ztnx_+DH=drknfsqpf4?RAu^uoY()#1_YN~fq0K58X6xeAOUS2eGm9T^9tqP584un6!J@Y5892+cLS zOq9{+E_L5}Y5vhd(9?$o6b@etQH*m^VV-XmuG2tYKVL((JWu(EWUDeAdtq$DhwryR zzO-o$^wQKz?&tSf9JFAbKlirv?muIPj*Z#c%-gN!?$N&ju{pA4TKL;(2GOR)TtSQ| z5n%!p@lnGp*aL$-2&|_8@Hhyf%K+E5sEiU0NlvIvn#X}ges|nY~4*k4MN5zp=r=+NA zW%i#y*2hqSI6limrqj_)Tt(zow}{)vce9-VXu!6=%VQtE!|d_HaL@6{y)Z&HvP~#F z^4-7Q9&3mJM0P3#F~H)378`hwHJ>Ki(mMBjPmAxU1>$`+M=Qf`_#$1%V=*PFo%<^| z&$Bb{^NO42;XMP5awIh}fL%y7q#$ljO-)#Q%7o)1y3W;re|2t;W-WukJ&wUowVWJK{8+~w#fT#t}Q_Rp@R@%=GlcM z>l{Y5Cnf0`X_CP?QnzPSyd!0ESVVF3E?a=3(;jO;jz{9gG@&%;fV8^0wJ?>!qWhg8@;uTul2bCOC0Pk z?gERTI@`5CiuX||?%F%g{)y$%(qnw!7oUHF9ua|mX}f5D`of(351%>Ab!HJ}b38TK zfSMRMySr!^mg%C`TwM>`RM85`WDIq@VYs!iXuIu_)hn?8D+8<9yP*oQC;TiRl}D$t zC7w>>6+-cM9zI}S7`tHo*%xgx@`pB8*>Bw&Yt}h1fa2(mg#l)T)SY}X*&;wkk_j6+ zcTx!mki`az63tSQ&hN92Z+yj4!5vE6TNd>U16G*ML1wZ&#(j`j;r@Ul$9?1Z%xCB zc#e5Ehe4_rMSvczhoCRv{=SQ*tWKT+fS|~q67a`ISeK}kH<2*6Ali1Godw24<*h;z<0Kk?0d+wpS?wPkvlX;wbG)YPf&lD77nS$+0CJ$Rj1*srsf##h4 zvA+8)*m)OPrzDLMUuCU4$Jf=H=Y3ziw}03(uPz|rCyNsW=t&59Yh%-u0E>|8n?4pG zvK;}ym_&lrofpKprpd-h->|p8-*&^hTp0Av$f~H*m|U?PJ8{O2A3tNGWE$Ui;{~gy zH!KCwUt_GY0IccRB};8JYz3^X*a<-&L)XX4`SA&d@oVhZZM~_d~U3Z^k9~*Y% z$`yAUDMXU5Oilv|S2&&&WoH=*97QHL96Nc)@hx@hyj36kZaaX2Dv9XW*4sL z`YDIRP zS zwxO*Jdw#abTdaG{pR@QfF=qAQ@C3gcjfy zqVD1n3j{PNv+B5{qnd}~s75R=H;e?%B}T8t3(35c$%xnB0SoM2E5&}`mvaw3Kzw42 zhzt{q<_Io!ps~!&EkdP{AU6|*Xc$rVc?==-ZuA%>;%AEoYbJwDpZGv|_#1H8^aKP26d7_*#hCR*6BkA#0u`9TQ{`lh4Bd0gM3G z1EUCvLt~&)vyS#+fUHUf!K#|@wzjsRHz3JGm?N9-ph;>AdkX<%C0H-NES_wC?+}tw z@I9G4>A1i6$=loQGjJo%0Iu8?P_=tw!HnKhJJwH3#gL8+iWjax_iOBo{CTM3s6{%C zSz+@nOHcj>C%+TxyVv5wPY@(RBdWp`S8)A4o8BW3?YC&}y%y@ZAF9_JmCu*K3yUPJ zK(s<5T1;-jZO=fXqRtfhg}WOxy-;#d=|)W+n$N1{Qx6w56hXp+#Ybdww;)rO@lk>i z;0nN{T3J}ZVQR8Q6pl{^9e;;xbYu+SZRWQUdQVNyP+m?r&A82U&URP?JrQ)phlXK3$_m)k{=<@YG#{`QZyF_Kw3hBF-uu!c6t$BSejD%0T!1|Ko#XC;AI6a zr4M5P{@FHvSLrVrOgavu_(m#=M&#T!&BM0AW+bb+7u;9Sq zi(hezWZ(YN@E*5F;WI9en@O@Ad$BnU&Z{3RF=c5l{`t$xcCbHT_v}wlT2;n6NLjFp zj#eggxDHsd-7y+G2xO|*rvOrfYpO{@YM?5TEk*%bChFu@0*KCcV%y@M&Z{9Sk+cVo zwcF>De~CS}*$eg$ZEal~S(=-8uT{#*UC1a6HLh5b(grQfWDe=9<^9!6B6%pM14_`L z;MGSEv{Q`P)AM)PqZ9v+6>5|X$$kd0zZmxCqw~HEPH2h^CubW5HbyrzeTQ ztMu63DZ%b8?!z`WDW;5#uF|nvVj2=wW$gqie#FQ{uh1NHFM5)Rb|hk$a-vMDDg~mK zC>RpYE&>m;QNu{~8t zdiGHv=>!~Ndoe(LgbIe}3JQ1q1gGMvaw+J@CX0z}muOg0X5S_X*L@!%%PLCF!YWTa3}sg!Rw*205*N7X85?jdEk9tyVrl%Gtekvw-%`| zW3O^~oGqCfbrCjkf(|eVY~c=Fhc%BY%gE|QBZ1de=2ifYDuMYZM~{-J8zy77X6N31 z$ChT$p(|wE*t-p)|CtpsIXL7H-G@FF=nUn;*%i8z=?ZMp3=oFUN+JbKG@@>11gnEo zbAh`6bWOTgOEeG^rGKVrGl06b&R&am^w1&e++3v|DSI8OkYMKVLOsT`%m|MR=k*8g`XQ$ts9B@Os_I~nHh zlA&$@+HwE@FP$o~(Ew#2XO@mzGLnHiOG%h(wXw8n`RX>(P~$cdeADWQqfV#M%L4AQ@%Ro+sZpn&x|3@J zCk?u`FU~XX*Lw+YP_mxt8#e1dWKl$&1F;TDkZ9s@d;{>$Ub;fZdNGd8DeJ3{g!hAf z(bLyu$BrK+3l>3xZ=G)3^;Q{?>$XE?wbDUj1p63;_bYeV0aaq8?--MbBpU><7H^lp zD?)j62fn8ly}%;f=4$9rgNL}Bu0*Mma*J*Pf1)EKxF3C7qf7?>!F9?&Wl^QX2jxLy zQeA@(=VW4b@7-?<_xLMz7{BF8(4iDTcn41=JZ36gYX9fRG9Ck`K+`}m;#xLQ z4KK~gwq@tONxK8XJBA*mgL={`yVJT^f9Asiv3~0~_x1#62w^J;^Q z*6y2i+cVIp6;_&M?Wq*ytpW?R8V&{+)7F|U;lb(fKfsI(9U`^b3Ic9gK;d__wNd`V z@%iA-WFVePT$m9HOk{Y2*W_3j8MOs4t*9Q@OjfkI!zNkhQfC>!@oBH*XRtO<8sD?^8Cx4*3j~%GKPJ zqq!l+^)Inc9`27C4Kr_FsoFp8{+?Yd-Dh3?dFyW@gd)o*Ze1Hd!VAU28Z=MbP_sla%YFH><3x{Ct(bB+m@zAiU=w zBJHkx4KFl@#)q1tG|p7_hkQeXjB>nkWhgk(&`l%l zGe5sT07M0Z{gNN2%LaQ^*&6?eF0Oo?lrA)CQ^$XMyL|@s;tZ(hA^V*nzQ1qWY_R=j zf!xpUlh$Hiw3kfa-ON`%Y5v$CDiNcs-z&Ct?q4IZbKDXmpTNFVF_T@l-meO`F}LuH zbsYYEOOAiRcCUN|A5JGVAX6o4N%M5w@sG(ScG~XTw~){}Y=KVr&P8ZI>bB;qK|)9r zqMLQwGtj8hEXNrjv!h5?3!@0+aZ0{M6d>N1xKwg_HFd4x(c+ZZl+DO6=UHpL1f?b9 zSOYj6IYfpA$2N229TqG~b%4o;%5ugu6`otgV7zju7{l_)Ec%KUYm$yRGj+!hcCK3SGkfi~P zJ4jYU=>;hL12{1#9Os5*d-rwW1M|v*V(eNar0hjIB?5FLyETY}T zDM};gS#E4#>oifoW%RQ6(wc9}3zPu6cWkr9%xNeRBm=P%0wyw+5|4HOg)+A!RL)Ov z{d1R)Xxg@y%AaR3W};B&RlDMO#L*zCK1~jy5rSG}cLFe1>!6W=h7GcVnsHPmDpuKB z4J&zqip&lTFMtVA92AUY;R17_+&vUVhDr`}V-2mxD2+8VT1Y!oAfrZ^rvmU*BSf3B zZ9I=hT4hq~K=cyXpaAu)w#kk(5v2mC_mOSXv=fa*E%LjjSNQ?{LC!~%^De#50+kGv z^7HvHnMMLfu31n25J6@S0p+T#EUs~Fu%#lrZKP~gk+G5RcA}kf6@D&5_2lfua~4Fa zQO}Dyb%~no@Z2)NWEDmnxgASW%9a{M3EK?EQx+Fx2F~Ixv={l*DjZY=J^|jp z1C3l&c{I&MQ-B1970ho_md+!uBrbUyyy`mFI#7q6!~4kZ2@n_1WQ4(LsWvD?fJPlt z3Bat5@-C7ANkHl*z7C@$NI`N;8fLe*+h<@8&%i1z0hqq^k#STE!!r7wn`m#rxSG%I zMrq&yC#zH1K5yIS|1D}Nmn}Bv&P$d1{&qGG^;es zqA3yVO`MdM1=`E>0*yJ7)?Wx`tV|FXsU?_q>NN#TpTU(Ww-|#ALj*>25{zbs^WjC38uz^A*b7cJjT|}Dp>?TBYslT zVL~duOPB@G=deFXhJZ;LJ7h>{*;g4#6C-vhm#)mOxDvA(4%C&pMM=nuQ4S6bu*i!X zTXS5{asf_Lqzb5|3|?1RTGszQcYsdP4ccVr#q5^#jGVB2#~*fC7gICcXt$~01Fx{Eq-)dj7l1q7(bICi>*i)H?{k>3WCP^!kx)$pN7 zBn7yh3UCmQa=}U&gxgK_J4w)%Wdc$Y?LKJ=_H$3&s+mSz3ok*gCc#LHbCdFHC)%*7 zjxGXKpfZ_tiFFG^l~jpfp(+u4R8-bd7M9m*hW9pE#ex1|N56UP)t?Y_KtDjftHJpd zXe<*H2k1!8kx_i^`R51{+pU+faM+*avl7sA;CFjcWDn7aM2NnGUaI_1i9p;FZpS{z zR_=2B3QGhw*c3qC&u7Sx6?m#@(Ls||7O?3j80a4y0t`0+m^m`qXh%{RS)5zrdPix% z>gJdhZDD%Zbxj9+oqTqLtmF;>PSM#%0bZwQJ57@c2AtS!_Jd$Hf|h4H0QL5E`wZ;m z8Q6r8M3UV)NQSet8%4hN+}ujaKhA%-QZW^k_y~N)&sjA)XW_(t7p%KUFeH|m-0Sad z+~1Ym8A{KnzYIPCEo9shl%c($q~(`>Xgibt-r@rfS`abt^yHt|?!u3(FbG{`KRkEo zxeBY?1Z(-b$MbVP>=|ej@Kf_kiG^AO>I&tpAhqEd84vPlZa39ch-W4mpa_Ttm>iNy z9U7gb#acOxAdcIQ?8Vk5ye!_Uql2_t2O# zsv&}dDKS2)0?3e4jN^GPf~US z{80+e(y${ArACkGoCwHByqkoOwfWk3e~pPF+gM{CTxN&N0NgT_@fqXsbC-9oeua+} zYlk;F&UsZi-ba}Za8AkWrr$V)6?E&A1<%qDrSh=4`2Bn-YgR$KP-nS{JqipzS$j0`j-6;<qUP(M#svZ43vU+Js7Ha^LEF9apm4JP&>t&nvJ^=tBLsTPtZl>*M?cZ>J ze9{&?Pa&SpNI`%>N3=qAvmG5j01dQ>5Se_9ic@weLFQ7_4oxr8zN{~!1mMZhxWjYe zG~2-OT_r23W(yx9?}|(y(G-~)_O47m8s&}-p|N)AsOC54NW(Nc=`+;)rHtII3LM4D z)-g)Rd|6+6q;a7Q{8MW}6#x)-Cf}1FOPDWF!zM#1u3s8TK$9LS$x!*Zm-Tmz zPz_^inLy5gN|jX9wO&S!sQ@^=c;UP)%+at#@EanC?CpoD(&}MhUn&F}1PXC7$kmYT za7}a4VC3^6F#=3V&lrWfAH~6TlnCshA6if(4TD@NeOy8-P&~R-!Z9-fo|P zy*vXX$7v2kdI!)*VeuD0qf|(|_-aUtXgsf2VdHg&Gc+==ff|B8+-b>)&r@B1QUt(@ zbRD2Um~Nip@(24L||Epz!V0Fn<{+vulkcjjxhJO3Tl z4S*s(!W9Yi(foHaZ+iwBRh}Ce9b{q7*an2@;r+CqQwGuEEMARDBY8$)RarbW?^aDB znyR2NXv&mqLcZ7BXb+j15K<&#`*|;rm3WQ;vUfZ1X_WbiP+}G#y-p@Ck!Z6AA9>V1 z{>i7fCM@1Kd(8mege;!kVY0~@ozghtLx;z#J=q2?@i@&ZUAF&@JFT7Up@*{g244mE zW$HMrla*9Iib{tKoUzj0;_#?*Hi6UCB#`y|CQ>HMohA}#qjNDtcG5}m(3PtB(q)|P zHl4E~j%z;gZiCVhHj~+AQU~jc%0XeA?k;?>0Dw`mYn>I&zth)aLDcJ1o>b#elVC?@S=sYE<+$y9n|{GA1+0$T&4Zl~`=U@Wi^hd+Z|EvbXlO4a9fs zt)en{bxU~Rk$Gq;^A=oobBpqHBiPC3)vYZ^){oM8w$zUOQAWo`qVRc@nN}GUh8p&W z4ZBG$#^*8thxTmRjvn*yQ|*LxRPu2$g7u`}AU4EI!-ML&KAGjX<5fnuOyfIr-#Xz2XrxW+XM zar)9lGRcevrHMoVmV!h{k%hRqd7ATg3G^Qij1H5{>>#VTL$lJdycaTHEBM4JLjwA& zzkkr)KL3urLn*tn2U#$5WSjWToB|w)(i7kW@gY)ms3PKBW1sL`*jNBN$KFt9(A?;y z1r|6oO!FB{nnL0SAem(XmgJNw`|!tPA0;&I(S5?})y=)deM0zR(GCI>nhuq9#+l29U`)|%adk7l+WblqtQdzZq`v-)0kPKl##Zf8~qmJNK zBu|u0q%AVOqTVvVQC)i5fW|OUvK#fL2wSsRakR=FYBZpDk99hmdng(CD4~?{b=yBS zZjV0t7>>6euv!Ex?zm3rv9S@W?IY8OV=PdnnS&Sk(hD>8qvu|yY(0i3GjpKXuLx!9 zF3L}9RVKs$^2Cc1xR9W-I6vYw`Ea`GNF5#;WD$rVpR+{n z@Db}7U=d;AP`7D8yiPo;$nz8oc|P5lG&NLMtVt$X-#`xl8GaKPpaK&=ieyvCVc?J{ zDYn7Aq%?mWW@PUmw@XfK54INBA8^KXRmMd!M|-|;a-wv#dN@xQ7w3wyAG8AJF<8(; zWU)_-r>wTN?1B!BeAGz68jeB?M-SIeCC4%p0f9yVV*OquUBqSKx!3?eZ3ouGrEI^I z$TlkT7zl|A2mk;i)}c~(x!zt%!s1Ns))KZ->a~F=@;=Fvl%vl8(E8!DEwP|u1)v5^ z42jVg6>T4|g$osXD*6A~PVj_v51&C^1u(ZxQ&s_~CQ8+rzyw)FE`6*qO0VXPmYoduJ|Uk8Q?e>Tr~sAr!bX;ES_{*HN#ufff$vE z(`W9or=I#8=eh&`#=^I?26*3c&2nV`-{`&(8yy=0aF%S1YRJU#Q&1~%B5BA#pmd;gL(sy-7u-fiy{o&*w6uNvmoJX-z)g z9SiqPI-7Pr2@x|uA{lWy$mg127D)z&v6%S%BgtDw3#IAPbl}@DFH48?w^I zYnGY*D*hRL>EFO@?^bdaSWS`89D+nA}@C*XvY7G^9T8O1taz%A-5_F8lUG_l0F*ryTy?&yS-OZQKf{6_#nrNea<(+GaEsRoFvy{+d(

?@h2Hr8f4`>NeqzWPyx>KS0}jP1z>!I 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