adityaramesh12/my-course-project - Jovian
Learn data science and machine learning by building real-world projects on Jovian
# !pip install jovian --upgrade --quiet

Facial Keypoints Detection

Lets try to apply the basic convolutional deep learning concepts to the Facial Keypoints Detection Kaggle Competition and see how our model performs.

The link to the competition is -> https://www.kaggle.com/c/facial-keypoints-detection/overview/description

The objective of the task is to predict key points on face images. This can be used for a variety of application like -

  • tracking faces in image and video
  • analysing facial expressions
  • detecting dysmorphic facial signs for medical diagnosis
  • biometrics / face recognition

Detecing facial keypoints is a very challenging problem. Facial features vary greatly from one individual to another, and even for a single individual, there is a large amount of variation due to 3D pose, size, position, viewing angle, and illumination conditions.

import os
from google.colab import files
import pandas as pd
import torch
import torchvision
import numpy as np
import matplotlib.pyplot as plt
import torch.nn as nn
from datetime import datetime
import torch.nn.functional as F
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor
from torchvision.utils import make_grid
from torchvision.datasets.utils import download_url
from torch.utils.data import DataLoader, TensorDataset, random_split
#import kaggle
%matplotlib inline
# Project name used for jovian.commit
project_name = 'my-course-project'

Getting the Kaggle dataset

Let's first install the kaggle api.

# !pip install kaggle

We need to first download my personal Kaggle API JSON token which contains my Kaggle credentials. We can get it by clicking on the Create New API Token under the My Accounts tab. This will download the json to our local system.

We need this file in our colab to access kaggle datasets and competitions. We can do this by using the following command and then specifying the file path from our system.

# THIS AND THE NEXT CELL NEEDS TO ONLY BE RUN WHILE FIRST CREATING THE PROJECT TO INCLUDE KAGGLE.JSON
files.upload()
Saving kaggle.json to kaggle.json
{'kaggle.json': b'{"username":"adityaramesh12","key":"7ee5a2f72b8090d78aa104532455d7cf"}'}

Move the kaggle.json file into ~/.kaggle, which is where the API client expects your token to be located. You'll get an error stating the file cant be found in this particular path otherwise. We can also change the environment path of 'KAGGLE_CONFIG_DIR' in the OS instead.

!mkdir -p ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

To get a list of dataset's available, use the following command.

# !kaggle datasets list

We are going to working with the dataset from the facial keypoints detections competition. We can get the required command under the data tab for the corresponding kaggle competition.

!kaggle competitions download -c facial-keypoints-detection
Warning: Looks like you're using an outdated API Version, please consider updating (server 1.5.10 / client 1.5.4) Downloading training.zip to /content 68% 41.0M/60.1M [00:00<00:00, 62.5MB/s] 100% 60.1M/60.1M [00:00<00:00, 136MB/s] Downloading test.zip to /content 0% 0.00/16.0M [00:00<?, ?B/s] 100% 16.0M/16.0M [00:00<00:00, 147MB/s] Downloading SampleSubmission.csv to /content 0% 0.00/201k [00:00<?, ?B/s] 100% 201k/201k [00:00<00:00, 182MB/s] Downloading IdLookupTable.csv to /content 0% 0.00/843k [00:00<?, ?B/s] 100% 843k/843k [00:00<00:00, 120MB/s]
#jovian.commit(project=project_name, environment=None)
 

Unzipping the Data

Look at the files and folder in the directory to find the names of the train and test zip files. Unzip these files using !unzip.

!ls
IdLookupTable.csv sample_data test.zip kaggle.json SampleSubmission.csv training.zip
!unzip training.zip -d 'training'
!unzip test.zip -d 'test'
Archive: training.zip inflating: training/training.csv Archive: test.zip inflating: test/test.csv

Creating Dataframes

Create dataframes by reading the csv's from obtained from the command above.

train_csv = 'training/training.csv'
test_csv = 'test/test.csv'
lookupid_csv = 'IdLookupTable.csv'
train_df = pd.read_csv(train_csv)
test_df = pd.read_csv(test_csv)
lookupid_df = pd.read_csv(lookupid_csv)

Lets explore the dataset and how the data and columns are formatted.

train_shape = train_df.shape
test_shape = test_df.shape

print('raw shape of training data ->', train_shape)
print('raw shape of test data ->', test_shape)
raw shape of training data -> (7049, 31) raw shape of test data -> (1783, 2)
train_df.head()

Since there are a lot of columns, it is difficult to interpret. Lets view this transposed.

train_df.head().T

Let's check for the missing values

train_df.isnull().any().value_counts()
True     28
False     3
dtype: int64
train_df.isnull().sum()
left_eye_center_x              10
left_eye_center_y              10
right_eye_center_x             13
right_eye_center_y             13
left_eye_inner_corner_x      4778
left_eye_inner_corner_y      4778
left_eye_outer_corner_x      4782
left_eye_outer_corner_y      4782
right_eye_inner_corner_x     4781
right_eye_inner_corner_y     4781
right_eye_outer_corner_x     4781
right_eye_outer_corner_y     4781
left_eyebrow_inner_end_x     4779
left_eyebrow_inner_end_y     4779
left_eyebrow_outer_end_x     4824
left_eyebrow_outer_end_y     4824
right_eyebrow_inner_end_x    4779
right_eyebrow_inner_end_y    4779
right_eyebrow_outer_end_x    4813
right_eyebrow_outer_end_y    4813
nose_tip_x                      0
nose_tip_y                      0
mouth_left_corner_x          4780
mouth_left_corner_y          4780
mouth_right_corner_x         4779
mouth_right_corner_y         4779
mouth_center_top_lip_x       4774
mouth_center_top_lip_y       4774
mouth_center_bottom_lip_x      33
mouth_center_bottom_lip_y      33
Image                           0
dtype: int64

This shows that there are 28 columns that have null values and 3 that have no null values. We can handle this either by removing records with na or filling these na values with something(say previous record in the row).

When we tried removing na records, 7049 records reduced to 2140. Which is very limited. Let's try the other approach instead.

#train_df = train_df.dropna(axis=0,how='any') # axis=0 means drop rows that contain missing values and 'any' means if any na is present, drop that row
train_df.fillna(method = 'ffill',inplace = True) #to fill those na records with previous value
train_df
train_shape = train_df.shape
test_shape = test_df.shape

print('updated shape of training data ->', train_shape)
print('updated shape of test data ->', test_shape)
updated shape of training data -> (7049, 31) updated shape of test data -> (1783, 2)

Lets look at the updated missing value now.

train_df.isnull().any().value_counts()
False    31
dtype: int64

Separting the image pixels from features

We observe that the image pixes are the last column in the dataframe. Lets extract that to separate the lables(x,y co ordinates of features) and image pixels. Also notice that the pixels are separated by a space. Lets change that to a list.

images_temp_list = []
train_len = train_shape[0]
for i in range(0, train_len):
  img = train_df['Image'][i].split(' ')
  img = ['0' if x == '' else x for x in img]
  images_temp_list.append(img)

Let use the list above and convert the values to float to get our final training images list.

len(images_temp_list)
7049
images_list = np.array(images_temp_list, dtype='float')

The competition brief says the following about the images-

`"The input image is given in the last field of the data files, and consists of a list of pixels (ordered by row), as integers in (0,255). The images are 96x96 pixels."

Let's reshape the 1D Vector array to the image format.

X_train = images_list.reshape(-1,96,96,1)
X_train.shape
(7049, 96, 96, 1)
X_train[0]
array([[[238.],
        [236.],
        [237.],
        ...,
        [250.],
        [250.],
        [250.]],

       [[235.],
        [238.],
        [236.],
        ...,
        [249.],
        [250.],
        [251.]],

       [[237.],
        [236.],
        [237.],
        ...,
        [251.],
        [251.],
        [250.]],

       ...,

       [[186.],
        [183.],
        [181.],
        ...,
        [ 52.],
        [ 57.],
        [ 60.]],

       [[189.],
        [188.],
        [207.],
        ...,
        [ 61.],
        [ 69.],
        [ 78.]],

       [[191.],
        [184.],
        [184.],
        ...,
        [ 70.],
        [ 75.],
        [ 90.]]])

Let's plot an image and see how it looks.

plt.imshow(X_train[0].reshape(96,96),cmap='gray')
plt.show()
Notebook Image
#jovian.commit(project=project_name, environment=None)

Now that we know how to plot an image, lets try to make methods to return images and keypoints.

def load_images(image_data):
  images_temp_list = []
  for i, sample_image in image_data.iterrows():
    img = np.array(sample_image['Image'].split(' '), dtype=int)
    img = np.reshape(img, (1,96,96))
    images_temp_list.append(img)

  images_list = np.array(images_temp_list, dtype='float')
  return images_list

def load_keypoints(record):
  keypoints_data = record.drop('Image',axis=1)
  keypoints_temp_list = []

  for k, sample_keypoint in keypoints_data.iterrows():
    keypoints_temp_list.append(sample_keypoint)

  keypoints = np.array(keypoints_temp_list, dtype='float')
  return keypoints

def plot_sample(image,axis,title, true_keypoints=None,pred_keypoints=None):
  image = image.reshape(96,96)
  plt.imshow(image,cmap='gray')
  if pred_keypoints is not None:
    plt.scatter(pred_keypoints[0::2], pred_keypoints[1::2], color='red',marker='x', s=20)
  if true_keypoints is not None:
    plt.scatter(true_keypoints[0::2], true_keypoints[1::2], color='yellow',marker='x', s=20)
  plt.show()
clean_train_images = load_images(train_df)
print("Shape of clean_train_images: {}".format(np.shape(clean_train_images)))
clean_train_keypoints = load_keypoints(train_df)
print("Shape of clean_train_keypoints: {}".format(np.shape(clean_train_keypoints)))
test_images = load_images(test_df)
print("Shape of test_images: {}".format(np.shape(test_images)))

train_images = clean_train_images
train_keypoints = clean_train_keypoints
Shape of clean_train_images: (7049, 1, 96, 96) Shape of clean_train_keypoints: (7049, 30) Shape of test_images: (1783, 1, 96, 96)
 

Plotting a Sample

Lets plot some random samples from the training images to see how the training data is.

sample_image_index = 15
fig, axis = plt.subplots()
plot_sample(image=clean_train_images[sample_image_index],axis=axis,title="Sample image & keypoints", true_keypoints=clean_train_keypoints[sample_image_index])
Notebook Image
sample_image_index = 30
fig, axis = plt.subplots()
plot_sample(image=clean_train_images[sample_image_index],axis=axis,title="Sample image & keypoints", true_keypoints=clean_train_keypoints[sample_image_index])
Notebook Image
sample_image_index = 500
fig, axis = plt.subplots()
plot_sample(image=clean_train_images[sample_image_index],axis=axis,title="Sample image & keypoints", true_keypoints=clean_train_keypoints[sample_image_index])
Notebook Image

We see that the keypoints represent facial features(both left and right) for the eye brows, eyes and pupils, nose tip, and corners of the lips.

#jovian.commit(project=project_name, environment=None)

Creating a tensor Dataset

The images(input) and keypoints(output) array that we have are numpy arrays. We need to convert these to PyTorch tensors.

inputs = torch.from_numpy(clean_train_images.astype(np.float32))
targets = torch.from_numpy(clean_train_keypoints.astype(np.float32))
inputs
tensor([[[[238., 236., 237.,  ..., 250., 250., 250.],
          [235., 238., 236.,  ..., 249., 250., 251.],
          [237., 236., 237.,  ..., 251., 251., 250.],
          ...,
          [186., 183., 181.,  ...,  52.,  57.,  60.],
          [189., 188., 207.,  ...,  61.,  69.,  78.],
          [191., 184., 184.,  ...,  70.,  75.,  90.]]],


        [[[219., 215., 204.,  ...,  92.,  88.,  84.],
          [222., 219., 220.,  ...,  92.,  88.,  86.],
          [231., 224., 212.,  ...,  77.,  80.,  84.],
          ...,
          [  1.,   1.,   1.,  ...,   1.,   1.,   1.],
          [  1.,   1.,   1.,  ...,   1.,   1.,   1.],
          [  1.,   1.,   1.,  ...,   1.,   1.,   1.]]],


        [[[144., 142., 159.,  ..., 208., 207., 207.],
          [143., 142., 161.,  ..., 208., 208., 207.],
          [143., 140., 160.,  ..., 209., 209., 207.],
          ...,
          [ 66.,  70.,  69.,  ...,  81., 134., 194.],
          [ 65.,  69.,  71.,  ...,  75.,  83., 109.],
          [ 65.,  68.,  70.,  ...,  78.,  78.,  77.]]],


        ...,


        [[[ 74.,  74.,  74.,  ...,  68.,  60.,  64.],
          [ 74.,  74.,  74.,  ...,  67.,  63.,  62.],
          [ 74.,  74.,  74.,  ...,  69.,  66.,  60.],
          ...,
          [ 24.,  24.,  24.,  ...,  20.,  20.,  20.],
          [ 25.,  25.,  25.,  ...,  20.,  20.,  20.],
          [ 25.,  25.,  25.,  ...,  20.,  20.,  20.]]],


        [[[254., 254., 254.,  ..., 250., 253., 254.],
          [254., 254., 254.,  ..., 245., 253., 254.],
          [254., 254., 254.,  ..., 248., 253., 254.],
          ...,
          [254., 254., 254.,  ..., 254., 254., 254.],
          [254., 254., 254.,  ..., 254., 254., 254.],
          [254., 254., 254.,  ..., 254., 254., 254.]]],


        [[[ 53.,  62.,  67.,  ..., 120., 117.,  97.],
          [ 53.,  64.,  70.,  ..., 114., 113.,  96.],
          [ 60.,  70.,  77.,  ..., 106., 108.,  94.],
          ...,
          [ 41.,  56.,  77.,  ..., 159., 155., 153.],
          [ 41.,  48.,  67.,  ..., 159., 157., 157.],
          [ 46.,  44.,  56.,  ..., 158., 158., 159.]]]])
dataset = TensorDataset(inputs, targets)
img, keypoints = dataset[0]
print(img.shape, keypoints)
img
torch.Size([1, 96, 96]) tensor([66.0336, 39.0023, 30.2270, 36.4217, 59.5821, 39.6474, 73.1303, 39.9700, 36.3566, 37.3894, 23.4529, 37.3894, 56.9533, 29.0336, 80.2271, 32.2281, 40.2276, 29.0023, 16.3564, 29.6475, 44.4206, 57.0668, 61.1953, 79.9702, 28.6145, 77.3890, 43.3126, 72.9355, 43.1307, 84.4858])
tensor([[[238., 236., 237.,  ..., 250., 250., 250.],
         [235., 238., 236.,  ..., 249., 250., 251.],
         [237., 236., 237.,  ..., 251., 251., 250.],
         ...,
         [186., 183., 181.,  ...,  52.,  57.,  60.],
         [189., 188., 207.,  ...,  61.,  69.,  78.],
         [191., 184., 184.,  ...,  70.,  75.,  90.]]])
#jovian.commit(project=project_name, environment=None)

Training and Validation Datasets

Lets use the random_split method available to split the dataset into training and validation datasets.

random_seed = 42
torch.manual_seed(random_seed)
<torch._C.Generator at 0x7fe959afd1e0>
val_percent = 0.1
val_size = int(val_percent * len(dataset))
train_size = len(dataset) - val_size

train_ds, val_ds = random_split(dataset=dataset, lengths=[train_size, val_size])
len(train_ds),len(val_ds)
(6345, 704)
#jovian.log_dataset(train_size=train_size, val_size=val_size, random_seed=random_seed)
batch_size = 128
train_loader = DataLoader(dataset=train_ds,batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(dataset=val_ds, batch_size=batch_size, num_workers=4, pin_memory=True)

Lets use the make_grid function provided by TorchVision to see a grid of the batch.

def show_batch(dl):
  for images, keypoints in dl:
    fig, ax = plt.subplots(figsize=(12,6))
    ax.set_xticks([]); ax.set_yticks([])
    print(images.shape)
    ax.imshow(make_grid(images,nrow=16).permute(1, 2, 0))
    break
 
show_batch(train_loader)
Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
torch.Size([128, 1, 96, 96])
Notebook Image

Defining the CNN Model

Let's first define the base ImageClassificationBase Model with the helper methods for training and validation.

Since we are ultimately dealing with a regression problem for each keypoint, recommended loss functions are mse, mae etc. But since the kaggle calculates scores based on mse, lets pick the same.

We also need to find a good way to calculate accuracy.

class ImageClassificationBase(nn.Module):
  def training_step(self, batch):
    images, keypoints = batch
    out = self(images)
    loss = F.mse_loss(out, keypoints)
    return loss

  def validation_step(self,batch):
    images, keypoints = batch
    out = self(images)
    loss = F.mse_loss(out,keypoints)
    #acc = accuracy(out, keypoints)
    #return {'val_loss':loss.detach(), 'val_acc':acc}
    return {'val_loss': loss.detach()}


  def validation_epoch_end(self, outputs):
    batch_losses = [x['val_loss'] for x in outputs]
    epoch_loss = torch.stack(batch_losses).mean() #combine all losses and find mean
    # batch_accs = [x['val_acc'] for x in outputs]
    # epoch_acc = torch.stack(batch_accs).mean() 
    # return {'val_loss':epoch_loss.item(), 'val_acc':epoch_acc.item()}
    return {'val_loss': epoch_loss.item()}

  def epoch_end(self, epoch, result):
    print("Epoch [{}], train_loss: {:.4f}, val_loss: {:.4f}".format(
    epoch, result['train_loss'], result['val_loss']))
    # if (epoch+1) % 1000 == 0 or epoch == num_epochs-1:
    #   print("Epoch [{}], val_loss: {:.4f}".format(epoch+1, result['val_loss']))

def accuracy(outputs, keypoints):
  _, preds = torch.max(outputs,dim=1)
  return torch.tensor(torch.sum(preds == labels).item() / len(preds))
class FacialKeypointsCNNModel(ImageClassificationBase):
  def __init__(self):
    super().__init__()
    self.network = nn.Sequential(
        #output: 1 * 96 * 96
        nn.Conv2d(in_channels=1,out_channels=32,kernel_size=3,padding=1),
        nn.ReLU(),
        #output: 32 * 96 * 96
        nn.Conv2d(in_channels=32,out_channels=64,kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2,2),
        #output: 64 * 48 * 48
        nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2,2),
        #output: 128 * 24 * 24
        nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2,2),
        #output: 256 * 12 * 12
        
        nn.Flatten(),
        nn.Linear(256*12*12, 1024),
        nn.ReLU(),
        nn.Linear(1024,512),
        nn.ReLU(),
        nn.Linear(512,30)
    )

  def forward(self,xb):
    return self.network(xb)
# model = FacialKeypointsCNNModel()
# model
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-52-484166afbbd2> in <module>() ----> 1 model = FacialKeypointsCNNModel() 2 model <ipython-input-51-f7ea115c7eb5> in __init__(self) 1 class FacialKeypointsCNNModel(ImageClassificationBase): 2 def __init__(self): ----> 3 super().__init() 4 self.network = nn.Sequential( 5 #output: 1 * 96 * 96 AttributeError: 'super' object has no attribute '_FacialKeypointsCNNModel__init'
for images, keypoints in train_loader:
    print('images.shape:', images.shape)
    out = model(images)
    print('out.shape:', out.shape)
    print('out[0]:', out[0])
    break
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-54-2d316d62bc28> in <module>() ----> 1 for images, keypoints in train_dl: 2 print('images.shape:', images.shape) 3 out = model(images) 4 print('out.shape:', out.shape) 5 print('out[0]:', out[0]) NameError: name 'train_dl' is not defined

Loading the model to the GPU

Lets define a few helper functions to move the data and the model to the GPU

def get_default_device():
  # pick gpu if available, else cpu
  if torch.cuda.is_available():
    return torch.device('cuda')
  else:
    return torch.device('cpu')

def to_device(data, device):
  # move tensor to the chosen device
  if isinstance(data, (list,tuple)):
    return [to_device(x,device) for x in data]
  return data.to(device,non_blocking=True)

class DeviceDataLoader():
  # Wrapping the dataloader to move data to device
  def __init__(self,dl,device):
    self.dl = dl
    self.device = device

  def __iter__(self):
    # yield a batch of data after moving it to device
    for b in self.dl:
      yield to_device(b,self.device)
    
  def __len__(self):
    # number of batches
    return len(self.dl)
device = get_default_device()
device
device(type='cuda')

Let's wrap our training and validation data loaders using DeviceDataLoader for automatically transferring batches of data to the GPU if available and use to_device to move model to GPU too.

train_dl = DeviceDataLoader(train_loader,device)
val_dl = DeviceDataLoader(val_loader,device)
# to_device(model,device)

Training the Model

Lets define the evaluate and fit function to train the model and evaluate the performance.

@torch.no_grad()
def evaluate(model,val_loader):
  model.eval() #to set the model mode as evaluation
  outputs = [model.validation_step(batch) for batch in val_loader]
  return model.validation_epoch_end(outputs)

def fit(epochs, lr, model, train_loader, val_loader, opt_func=torch.optim.SGD):
  history = []
  optimizer = opt_func(model.parameters(), lr)
  for epoch in range(epochs):
    # Training Phase
    model.train()
    train_losses = []
    for batch in train_loader:
      loss = model.training_step(batch)
      train_losses.append(loss)
      loss.backward()
      optimizer.step()
      optimizer.zero_grad()

    # Validation Phase
    result = evaluate(model, val_loader)
    result['train_loss'] = torch.stack(train_losses).mean().item()
    model.epoch_end(epoch,result)
    history.append(result)
  
  return history

Let's first move the model to the GPU

model = to_device(FacialKeypointsCNNModel(),device)
evaluate(model, val_dl)
{'outputs': [{'val_loss': tensor(4.3486, device='cuda:0')},
  {'val_loss': tensor(8.6849, device='cuda:0')},
  {'val_loss': tensor(6.5877, device='cuda:0')},
  {'val_loss': tensor(8.1221, device='cuda:0')},
  {'val_loss': tensor(6.8381, device='cuda:0')},
  {'val_loss': tensor(4.7090, device='cuda:0')}],
 'val_loss': 6.5484113693237305}
num_epochs = 10
opt_func = torch.optim.Adam
lr = 0.001
history = fit(num_epochs, lr, model, train_dl, val_dl, opt_func)
Epoch [0], train_loss: 850.5801, val_loss: 115.3946 Epoch [1], train_loss: 106.4659, val_loss: 96.0000 Epoch [2], train_loss: 89.2559, val_loss: 98.7692 Epoch [3], train_loss: 68.1213, val_loss: 52.8382 Epoch [4], train_loss: 34.4038, val_loss: 17.4110 Epoch [5], train_loss: 11.7930, val_loss: 9.5330 Epoch [6], train_loss: 9.0067, val_loss: 8.7381 Epoch [7], train_loss: 8.0757, val_loss: 7.9395 Epoch [8], train_loss: 7.2681, val_loss: 7.0621 Epoch [9], train_loss: 6.4229, val_loss: 6.5484
def plot_losses(history):
  train_losses = [x.get('train_loss') for x in history]
  val_losses = [x['val_loss'] for x in history]
  plt.plot(train_losses, '-bx')
  plt.plot(val_losses,'-rx')
  plt.xlabel('epoch')
  plt.ylabel('loss')
  plt.legend(['Training','Validation'])
  plt.title('Loss vs No. of Epochs')
plot_losses(history)
Notebook Image

Predict

def predict_image(img, model):
    # Convert to a batch of 1
    xb = to_device(img.unsqueeze(0), device)
    # Get predictions from model
    yb = model(xb)
    yb_numpy = yb.detach().cpu().numpy()
    print(yb_numpy[0])
    return yb_numpy[0]
    # Pick index with highest probability
    # _, preds  = torch.max(yb, dim=1)
    # # Retrieve the class label
    # return dataset.classes[preds[0].item()]
image_check_index = 45
image, true_keypoints = train_ds[image_check_index]
predicted_keypoints = predict_image(image, model)
fig, axis = plt.subplots()
plot_sample(image=image,axis=axis,title="Sample image & keypoints", true_keypoints=true_keypoints, pred_keypoints=predicted_keypoints)
[67.84906 39.11566 30.483137 37.730564 60.277576 37.6839 75.18134 36.64938 37.35944 39.144245 22.472336 39.85859 54.973 30.38744 81.998764 29.695744 39.343452 32.598522 15.311227 34.17721 45.75437 63.461807 69.498436 74.58491 34.174675 78.00407 50.600235 75.60271 46.186733 80.17069 ] 30 30
Notebook Image
jovian.commit(project=project_name, environment=None)