This notebook teaches you how to train a pose classification model using MoveNet and TensorFlow Lite. The result is a new TensorFlow Lite model that accepts the output from the MoveNet model as its input, and outputs a pose classification, such as the name of a yoga pose.
The procedure in this notebook consists of 3 parts:
- Part 1: Preprocess the pose classification training data into a CSV file that specifies the landmarks (body keypoints) detected by the MoveNet model, along with the ground truth pose labels.
- Part 2: Build and train a pose classification model that takes the landmark coordinates from the CSV file as input, and outputs the predicted labels.
- Part 3: Convert the pose classification model to TFLite.
By default, this notebook uses an image dataset with labeled yoga poses, but we've also included a section in Part 1 where you can upload your own image dataset of poses.
View on TensorFlow.org | Run in Google Colab | View source on GitHub | Download notebook | See TF Hub model |
Preparation
In this section, you'll import the necessary libraries and define several functions to preprocess the training images into a CSV file that contains the landmark coordinates and ground truth labels.
Nothing observable happens here, but you can expand the hidden code cells to see the implementation for some of the functions we'll be calling later on.
If you only want to create the CSV file without knowing all the details, just run this section and proceed to Part 1.
pip install -q opencv-python
import csv
import cv2
import itertools
import numpy as np
import pandas as pd
import os
import sys
import tempfile
import tqdm
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow import keras
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
Code to run pose estimation using MoveNet
Functions to run pose estimation with MoveNet
# Download model from TF Hub and check out inference code from GitHub
!wget -q -O movenet_thunder.tflite https://tfhub.dev/google/lite-model/movenet/singlepose/thunder/tflite/float16/4?lite-format=tflite
!git clone https://github.com/tensorflow/examples.git
pose_sample_rpi_path = os.path.join(os.getcwd(), 'examples/lite/examples/pose_estimation/raspberry_pi')
sys.path.append(pose_sample_rpi_path)
# Load MoveNet Thunder model
import utils
from data import BodyPart
from ml import Movenet
movenet = Movenet('movenet_thunder')
# Define function to run pose estimation using MoveNet Thunder.
# You'll apply MoveNet's cropping algorithm and run inference multiple times on
# the input image to improve pose estimation accuracy.
def detect(input_tensor, inference_count=3):
"""Runs detection on an input image.
Args:
input_tensor: A [height, width, 3] Tensor of type tf.float32.
Note that height and width can be anything since the image will be
immediately resized according to the needs of the model within this
function.
inference_count: Number of times the model should run repeatly on the
same input image to improve detection accuracy.
Returns:
A Person entity detected by the MoveNet.SinglePose.
"""
image_height, image_width, channel = input_tensor.shape
# Detect pose using the full input image
movenet.detect(input_tensor.numpy(), reset_crop_region=True)
# Repeatedly using previous detection result to identify the region of
# interest and only croping that region to improve detection accuracy
for _ in range(inference_count - 1):
person = movenet.detect(input_tensor.numpy(),
reset_crop_region=False)
return person
Functions to visualize the pose estimation results.
def draw_prediction_on_image(
image, person, crop_region=None, close_figure=True,
keep_input_size=False):
"""Draws the keypoint predictions on image.
Args:
image: An numpy array with shape [height, width, channel] representing the
pixel values of the input image.
person: A person entity returned from the MoveNet.SinglePose model.
close_figure: Whether to close the plt figure after the function returns.
keep_input_size: Whether to keep the size of the input image.
Returns:
An numpy array with shape [out_height, out_width, channel] representing the
image overlaid with keypoint predictions.
"""
# Draw the detection result on top of the image.
image_np = utils.visualize(image, [person])
# Plot the image with detection results.
height, width, channel = image.shape
aspect_ratio = float(width) / height
fig, ax = plt.subplots(figsize=(12 * aspect_ratio, 12))
im = ax.imshow(image_np)
if close_figure:
plt.close(fig)
if not keep_input_size:
image_np = utils.keep_aspect_ratio_resizer(image_np, (512, 512))
return image_np
Code to load the images, detect pose landmarks and save them into a CSV file
class MoveNetPreprocessor(object):
"""Helper class to preprocess pose sample images for classification."""
def __init__(self,
images_in_folder,
images_out_folder,
csvs_out_path):
"""Creates a preprocessor to detection pose from images and save as CSV.
Args:
images_in_folder: Path to the folder with the input images. It should
follow this structure:
yoga_poses
|__ downdog
|______ 00000128.jpg
|______ 00000181.bmp
|______ ...
|__ goddess
|______ 00000243.jpg
|______ 00000306.jpg
|______ ...
...
images_out_folder: Path to write the images overlay with detected
landmarks. These images are useful when you need to debug accuracy
issues.
csvs_out_path: Path to write the CSV containing the detected landmark
coordinates and label of each image that can be used to train a pose
classification model.
"""
self._images_in_folder = images_in_folder
self._images_out_folder = images_out_folder
self._csvs_out_path = csvs_out_path
self._messages = []
# Create a temp dir to store the pose CSVs per class
self._csvs_out_folder_per_class = tempfile.mkdtemp()
# Get list of pose classes and print image statistics
self._pose_class_names = sorted(
[n for n in os.listdir(self._images_in_folder) if not n.startswith('.')]
)
def process(self, per_pose_class_limit=None, detection_threshold=0.1):
"""Preprocesses images in the given folder.
Args:
per_pose_class_limit: Number of images to load. As preprocessing usually
takes time, this parameter can be specified to make the reduce of the
dataset for testing.
detection_threshold: Only keep images with all landmark confidence score
above this threshold.
"""
# Loop through the classes and preprocess its images
for pose_class_name in self._pose_class_names:
print('Preprocessing', pose_class_name, file=sys.stderr)
# Paths for the pose class.
images_in_folder = os.path.join(self._images_in_folder, pose_class_name)
images_out_folder = os.path.join(self._images_out_folder, pose_class_name)
csv_out_path = os.path.join(self._csvs_out_folder_per_class,
pose_class_name + '.csv')
if not os.path.exists(images_out_folder):
os.makedirs(images_out_folder)
# Detect landmarks in each image and write it to a CSV file
with open(csv_out_path, 'w') as csv_out_file:
csv_out_writer = csv.writer(csv_out_file,
delimiter=',',
quoting=csv.QUOTE_MINIMAL)
# Get list of images
image_names = sorted(
[n for n in os.listdir(images_in_folder) if not n.startswith('.')])
if per_pose_class_limit is not None:
image_names = image_names[:per_pose_class_limit]
valid_image_count = 0
# Detect pose landmarks from each image
for image_name in tqdm.tqdm(image_names):
image_path = os.path.join(images_in_folder, image_name)
try:
image = tf.io.read_file(image_path)
image = tf.io.decode_jpeg(image)
except:
self._messages.append('Skipped ' + image_path + '. Invalid image.')
continue
else:
image = tf.io.read_file(image_path)
image = tf.io.decode_jpeg(image)
image_height, image_width, channel = image.shape
# Skip images that isn't RGB because Movenet requires RGB images
if channel != 3:
self._messages.append('Skipped ' + image_path +
'. Image isn\'t in RGB format.')
continue
person = detect(image)
# Save landmarks if all landmarks were detected
min_landmark_score = min(
[keypoint.score for keypoint in person.keypoints])
should_keep_image = min_landmark_score >= detection_threshold
if not should_keep_image:
self._messages.append('Skipped ' + image_path +
'. No pose was confidentlly detected.')
continue
valid_image_count += 1
# Draw the prediction result on top of the image for debugging later
output_overlay = draw_prediction_on_image(
image.numpy().astype(np.uint8), person,
close_figure=True, keep_input_size=True)
# Write detection result into an image file
output_frame = cv2.cvtColor(output_overlay, cv2.COLOR_RGB2BGR)
cv2.imwrite(os.path.join(images_out_folder, image_name), output_frame)
# Get landmarks and scale it to the same size as the input image
pose_landmarks = np.array(
[[keypoint.coordinate.x, keypoint.coordinate.y, keypoint.score]
for keypoint in person.keypoints],
dtype=np.float32)
# Write the landmark coordinates to its per-class CSV file
coordinates = pose_landmarks.flatten().astype(np.str).tolist()
csv_out_writer.writerow([image_name] + coordinates)
if not valid_image_count:
raise RuntimeError(
'No valid images found for the "{}" class.'
.format(pose_class_name))
# Print the error message collected during preprocessing.
print('\n'.join(self._messages))
# Combine all per-class CSVs into a single output file
all_landmarks_df = self._all_landmarks_as_dataframe()
all_landmarks_df.to_csv(self._csvs_out_path, index=False)
def class_names(self):
"""List of classes found in the training dataset."""
return self._pose_class_names
def _all_landmarks_as_dataframe(self):
"""Merge all per-class CSVs into a single dataframe."""
total_df = None
for class_index, class_name in enumerate(self._pose_class_names):
csv_out_path = os.path.join(self._csvs_out_folder_per_class,
class_name + '.csv')
per_class_df = pd.read_csv(csv_out_path, header=None)
# Add the labels
per_class_df['class_no'] = [class_index]*len(per_class_df)
per_class_df['class_name'] = [class_name]*len(per_class_df)
# Append the folder name to the filename column (first column)
per_class_df[per_class_df.columns[0]] = (os.path.join(class_name, '')
+ per_class_df[per_class_df.columns[0]].astype(str))
if total_df is None:
# For the first class, assign its data to the total dataframe
total_df = per_class_df
else:
# Concatenate each class's data into the total dataframe
total_df = pd.concat([total_df, per_class_df], axis=0)
list_name = [[bodypart.name + '_x', bodypart.name + '_y',
bodypart.name + '_score'] for bodypart in BodyPart]
header_name = []
for columns_name in list_name:
header_name += columns_name
header_name = ['file_name'] + header_name
header_map = {total_df.columns[i]: header_name[i]
for i in range(len(header_name))}
total_df.rename(header_map, axis=1, inplace=True)
return total_df
(Optional) Code snippet to try out the Movenet pose estimation logic
test_image_url = "https://cdn.pixabay.com/photo/2017/03/03/17/30/yoga-2114512_960_720.jpg"
!wget -O /tmp/image.jpeg {test_image_url}
if len(test_image_url):
image = tf.io.read_file('/tmp/image.jpeg')
image = tf.io.decode_jpeg(image)
person = detect(image)
_ = draw_prediction_on_image(image.numpy(), person, crop_region=None,
close_figure=False, keep_input_size=True)
Part 1: Preprocess the input images
Because the input for our pose classifier is the output landmarks from the MoveNet model, we need to generate our training dataset by running labeled images through MoveNet and then capturing all the landmark data and ground truth labels into a CSV file.
The dataset we've provided for this tutorial is a CG-generated yoga pose dataset. It contains images of multiple CG-generated models doing 5 different yoga poses. The directory is already split into a train
dataset and a test
dataset.
So in this section, we'll download the yoga dataset and run it through MoveNet so we can capture all the landmarks into a CSV file... However, it takes about 15 minutes to feed our yoga dataset to MoveNet and generate this CSV file. So as an alternative, you can download a pre-existing CSV file for the yoga dataset by setting is_skip_step_1
parameter below to True. That way, you'll skip this step and instead download the same CSV file that will be created in this preprocessing step.
On the other hand, if you want to train the pose classifier with your own image dataset, you need to upload your images and run this preprocessing step (leave is_skip_step_1
False)—follow the instructions below to upload your own pose dataset.
is_skip_step_1 = False
(Optional) Upload your own pose dataset
use_custom_dataset = False
dataset_is_split = False
If you want to train the pose classifier with your own labeled poses (they can be any poses, not just yoga poses), follow these steps:
Set the above
use_custom_dataset
option to True.Prepare an archive file (ZIP, TAR, or other) that includes a folder with your images dataset. The folder must include sorted images of your poses as follows.
If you've already split your dataset into train and test sets, then set
dataset_is_split
to True. That is, your images folder must include "train" and "test" directories like this:yoga_poses/ |__ train/ |__ downdog/ |______ 00000128.jpg |______ ... |__ test/ |__ downdog/ |______ 00000181.jpg |______ ...
Or, if your dataset is NOT split yet, then set
dataset_is_split
to False and we'll split it up based on a specified split fraction. That is, your uploaded images folder should look like this:yoga_poses/ |__ downdog/ |______ 00000128.jpg |______ 00000181.jpg |______ ... |__ goddess/ |______ 00000243.jpg |______ 00000306.jpg |______ ...
Click the Files tab on the left (folder icon) and then click Upload to session storage (file icon).
Select your archive file and wait until it finishes uploading before you proceed.
Edit the following code block to specify the name of your archive file and images directory. (By default, we expect a ZIP file, so you'll need to also modify that part if your archive is another format.)
Now run the rest of the notebook.
import os
import random
import shutil
def split_into_train_test(images_origin, images_dest, test_split):
"""Splits a directory of sorted images into training and test sets.
Args:
images_origin: Path to the directory with your images. This directory
must include subdirectories for each of your labeled classes. For example:
yoga_poses/
|__ downdog/
|______ 00000128.jpg
|______ 00000181.jpg
|______ ...
|__ goddess/
|______ 00000243.jpg
|______ 00000306.jpg
|______ ...
...
images_dest: Path to a directory where you want the split dataset to be
saved. The results looks like this:
split_yoga_poses/
|__ train/
|__ downdog/
|______ 00000128.jpg
|______ ...
|__ test/
|__ downdog/
|______ 00000181.jpg
|______ ...
test_split: Fraction of data to reserve for test (float between 0 and 1).
"""
_, dirs, _ = next(os.walk(images_origin))
TRAIN_DIR = os.path.join(images_dest, 'train')
TEST_DIR = os.path.join(images_dest, 'test')
os.makedirs(TRAIN_DIR, exist_ok=True)
os.makedirs(TEST_DIR, exist_ok=True)
for dir in dirs:
# Get all filenames for this dir, filtered by filetype
filenames = os.listdir(os.path.join(images_origin, dir))
filenames = [os.path.join(images_origin, dir, f) for f in filenames if (
f.endswith('.png') or f.endswith('.jpg') or f.endswith('.jpeg') or f.endswith('.bmp'))]
# Shuffle the files, deterministically
filenames.sort()
random.seed(42)
random.shuffle(filenames)
# Divide them into train/test dirs
os.makedirs(os.path.join(TEST_DIR, dir), exist_ok=True)
os.makedirs(os.path.join(TRAIN_DIR, dir), exist_ok=True)
test_count = int(len(filenames) * test_split)
for i, file in enumerate(filenames):
if i < test_count:
destination = os.path.join(TEST_DIR, dir, os.path.split(file)[1])
else:
destination = os.path.join(TRAIN_DIR, dir, os.path.split(file)[1])
shutil.copyfile(file, destination)
print(f'Moved {test_count} of {len(filenames)} from class "{dir}" into test.')
print(f'Your split dataset is in "{images_dest}"')
if use_custom_dataset:
# ATTENTION:
# You must edit these two lines to match your archive and images folder name:
# !tar -xf YOUR_DATASET_ARCHIVE_NAME.tar
!unzip -q YOUR_DATASET_ARCHIVE_NAME.zip
dataset_in = 'YOUR_DATASET_DIR_NAME'
# You can leave the rest alone:
if not os.path.isdir(dataset_in):
raise Exception("dataset_in is not a valid directory")
if dataset_is_split:
IMAGES_ROOT = dataset_in
else:
dataset_out = 'split_' + dataset_in
split_into_train_test(dataset_in, dataset_out, test_split=0.2)
IMAGES_ROOT = dataset_out
Download the yoga dataset
if not is_skip_step_1 and not use_custom_dataset:
!wget -O yoga_poses.zip http://download.tensorflow.org/data/pose_classification/yoga_poses.zip
!unzip -q yoga_poses.zip -d yoga_cg
IMAGES_ROOT = "yoga_cg"
Preprocess the TRAIN
dataset
if not is_skip_step_1:
images_in_train_folder = os.path.join(IMAGES_ROOT, 'train')
images_out_train_folder = 'poses_images_out_train'
csvs_out_train_path = 'train_data.csv'
preprocessor = MoveNetPreprocessor(
images_in_folder=images_in_train_folder,
images_out_folder=images_out_train_folder,
csvs_out_path=csvs_out_train_path,
)
preprocessor.process(per_pose_class_limit=None)
Preprocess the TEST
dataset
if not is_skip_step_1:
images_in_test_folder = os.path.join(IMAGES_ROOT, 'test')
images_out_test_folder = 'poses_images_out_test'
csvs_out_test_path = 'test_data.csv'
preprocessor = MoveNetPreprocessor(
images_in_folder=images_in_test_folder,
images_out_folder=images_out_test_folder,
csvs_out_path=csvs_out_test_path,
)
preprocessor.process(per_pose_class_limit=None)
Part 2: Train a pose classification model that takes the landmark coordinates as input, and output the predicted labels.
You'll build a TensorFlow model that takes the landmark coordinates and predicts the pose class that the person in the input image performs. The model consists of two submodels:
- Submodel 1 calculates a pose embedding (a.k.a feature vector) from the detected landmark coordinates.
- Submodel 2 feeds pose embedding through several
Dense
layer to predict the pose class.
You'll then train the model based on the dataset that were preprocessed in part 1.
(Optional) Download the preprocessed dataset if you didn't run part 1
# Download the preprocessed CSV files which are the same as the output of step 1
if is_skip_step_1:
!wget -O train_data.csv http://download.tensorflow.org/data/pose_classification/yoga_train_data.csv
!wget -O test_data.csv http://download.tensorflow.org/data/pose_classification/yoga_test_data.csv
csvs_out_train_path = 'train_data.csv'
csvs_out_test_path = 'test_data.csv'
is_skipped_step_1 = True
Load the preprocessed CSVs into TRAIN
and TEST
datasets.
def load_pose_landmarks(csv_path):
"""Loads a CSV created by MoveNetPreprocessor.
Returns:
X: Detected landmark coordinates and scores of shape (N, 17 * 3)
y: Ground truth labels of shape (N, label_count)
classes: The list of all class names found in the dataset
dataframe: The CSV loaded as a Pandas dataframe features (X) and ground
truth labels (y) to use later to train a pose classification model.
"""
# Load the CSV file
dataframe = pd.read_csv(csv_path)
df_to_process = dataframe.copy()
# Drop the file_name columns as you don't need it during training.
df_to_process.drop(columns=['file_name'], inplace=True)
# Extract the list of class names
classes = df_to_process.pop('class_name').unique()
# Extract the labels
y = df_to_process.pop('class_no')
# Convert the input features and labels into the correct format for training.
X = df_to_process.astype('float64')
y = keras.utils.to_categorical(y)
return X, y, classes, dataframe
Load and split the original TRAIN
dataset into TRAIN
(85% of the data) and VALIDATE
(the remaining 15%).
# Load the train data
X, y, class_names, _ = load_pose_landmarks(csvs_out_train_path)
# Split training data (X, y) into (X_train, y_train) and (X_val, y_val)
X_train, X_val, y_train, y_val = train_test_split(X, y,
test_size=0.15)
# Load the test data
X_test, y_test, _, df_test = load_pose_landmarks(csvs_out_test_path)
Define functions to convert the pose landmarks to a pose embedding (a.k.a. feature vector) for pose classification
Next, convert the landmark coordinates to a feature vector by:
- Moving the pose center to the origin.
- Scaling the pose so that the pose size becomes 1
- Flattening these coordinates into a feature vector
Then use this feature vector to train a neural-network based pose classifier.
def get_center_point(landmarks, left_bodypart, right_bodypart):
"""Calculates the center point of the two given landmarks."""
left = tf.gather(landmarks, left_bodypart.value, axis=1)
right = tf.gather(landmarks, right_bodypart.value, axis=1)
center = left * 0.5 + right * 0.5
return center
def get_pose_size(landmarks, torso_size_multiplier=2.5):
"""Calculates pose size.
It is the maximum of two values:
* Torso size multiplied by `torso_size_multiplier`
* Maximum distance from pose center to any pose landmark
"""
# Hips center
hips_center = get_center_point(landmarks, BodyPart.LEFT_HIP,
BodyPart.RIGHT_HIP)
# Shoulders center
shoulders_center = get_center_point(landmarks, BodyPart.LEFT_SHOULDER,
BodyPart.RIGHT_SHOULDER)
# Torso size as the minimum body size
torso_size = tf.linalg.norm(shoulders_center - hips_center)
# Pose center
pose_center_new = get_center_point(landmarks, BodyPart.LEFT_HIP,
BodyPart.RIGHT_HIP)
pose_center_new = tf.expand_dims(pose_center_new, axis=1)
# Broadcast the pose center to the same size as the landmark vector to
# perform substraction
pose_center_new = tf.broadcast_to(pose_center_new,
[tf.size(landmarks) // (17*2), 17, 2])
# Dist to pose center
d = tf.gather(landmarks - pose_center_new, 0, axis=0,
name="dist_to_pose_center")
# Max dist to pose center
max_dist = tf.reduce_max(tf.linalg.norm(d, axis=0))
# Normalize scale
pose_size = tf.maximum(torso_size * torso_size_multiplier, max_dist)
return pose_size
def normalize_pose_landmarks(landmarks):
"""Normalizes the landmarks translation by moving the pose center to (0,0) and
scaling it to a constant pose size.
"""
# Move landmarks so that the pose center becomes (0,0)
pose_center = get_center_point(landmarks, BodyPart.LEFT_HIP,
BodyPart.RIGHT_HIP)
pose_center = tf.expand_dims(pose_center, axis=1)
# Broadcast the pose center to the same size as the landmark vector to perform
# substraction
pose_center = tf.broadcast_to(pose_center,
[tf.size(landmarks) // (17*2), 17, 2])
landmarks = landmarks - pose_center
# Scale the landmarks to a constant pose size
pose_size = get_pose_size(landmarks)
landmarks /= pose_size
return landmarks
def landmarks_to_embedding(landmarks_and_scores):
"""Converts the input landmarks into a pose embedding."""
# Reshape the flat input into a matrix with shape=(17, 3)
reshaped_inputs = keras.layers.Reshape((17, 3))(landmarks_and_scores)
# Normalize landmarks 2D
landmarks = normalize_pose_landmarks(reshaped_inputs[:, :, :2])
# Flatten the normalized landmark coordinates into a vector
embedding = keras.layers.Flatten()(landmarks)
return embedding
Define a Keras model for pose classification
Our Keras model takes the detected pose landmarks, then calculates the pose embedding and predicts the pose class.
# Define the model
inputs = tf.keras.Input(shape=(51))
embedding = landmarks_to_embedding(inputs)
layer = keras.layers.Dense(128, activation=tf.nn.relu6)(embedding)
layer = keras.layers.Dropout(0.5)(layer)
layer = keras.layers.Dense(64, activation=tf.nn.relu6)(layer)
layer = keras.layers.Dropout(0.5)(layer)
outputs = keras.layers.Dense(len(class_names), activation="softmax")(layer)
model = keras.Model(inputs, outputs)
model.summary()
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)
# Add a checkpoint callback to store the checkpoint that has the highest
# validation accuracy.
checkpoint_path = "weights.best.hdf5"
checkpoint = keras.callbacks.ModelCheckpoint(checkpoint_path,
monitor='val_accuracy',
verbose=1,
save_best_only=True,
mode='max')
earlystopping = keras.callbacks.EarlyStopping(monitor='val_accuracy',
patience=20)
# Start training
history = model.fit(X_train, y_train,
epochs=200,
batch_size=16,
validation_data=(X_val, y_val),
callbacks=[checkpoint, earlystopping])
# Visualize the training history to see whether you're overfitting.
plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['TRAIN', 'VAL'], loc='lower right')
plt.show()
# Evaluate the model using the TEST dataset
loss, accuracy = model.evaluate(X_test, y_test)
Draw the confusion matrix to better understand the model performance
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
"""Plots the confusion matrix."""
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
else:
print('Confusion matrix, without normalization')
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=55)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.tight_layout()
# Classify pose in the TEST dataset using the trained model
y_pred = model.predict(X_test)
# Convert the prediction result to class name
y_pred_label = [class_names[i] for i in np.argmax(y_pred, axis=1)]
y_true_label = [class_names[i] for i in np.argmax(y_test, axis=1)]
# Plot the confusion matrix
cm = confusion_matrix(np.argmax(y_test, axis=1), np.argmax(y_pred, axis=1))
plot_confusion_matrix(cm,
class_names,
title ='Confusion Matrix of Pose Classification Model')
# Print the classification report
print('\nClassification Report:\n', classification_report(y_true_label,
y_pred_label))
(Optional) Investigate incorrect predictions
You can look at the poses from the TEST
dataset that were incorrectly predicted to see whether the model accuracy can be improved.
if is_skip_step_1:
raise RuntimeError('You must have run step 1 to run this cell.')
# If step 1 was skipped, skip this step.
IMAGE_PER_ROW = 3
MAX_NO_OF_IMAGE_TO_PLOT = 30
# Extract the list of incorrectly predicted poses
false_predict = [id_in_df for id_in_df in range(len(y_test)) \
if y_pred_label[id_in_df] != y_true_label[id_in_df]]
if len(false_predict) > MAX_NO_OF_IMAGE_TO_PLOT:
false_predict = false_predict[:MAX_NO_OF_IMAGE_TO_PLOT]
# Plot the incorrectly predicted images
row_count = len(false_predict) // IMAGE_PER_ROW + 1
fig = plt.figure(figsize=(10 * IMAGE_PER_ROW, 10 * row_count))
for i, id_in_df in enumerate(false_predict):
ax = fig.add_subplot(row_count, IMAGE_PER_ROW, i + 1)
image_path = os.path.join(images_out_test_folder,
df_test.iloc[id_in_df]['file_name'])
image = cv2.imread(image_path)
plt.title("Predict: %s; Actual: %s"
% (y_pred_label[id_in_df], y_true_label[id_in_df]))
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.show()
Part 3: Convert the pose classification model to TensorFlow Lite
You'll convert the Keras pose classification model to the TensorFlow Lite format so that you can deploy it to mobile apps, web browsers and edge devices. When converting the model, you'll apply dynamic range quantization to reduce the pose classification TensorFlow Lite model size by about 4 times with insignificant accuracy loss.
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
print('Model size: %dKB' % (len(tflite_model) / 1024))
with open('pose_classifier.tflite', 'wb') as f:
f.write(tflite_model)
Then you'll write the label file which contains mapping from the class indexes to the human readable class names.
with open('pose_labels.txt', 'w') as f:
f.write('\n'.join(class_names))
As you've applied quantization to reduce the model size, let's evaluate the quantized TFLite model to check whether the accuracy drop is acceptable.
def evaluate_model(interpreter, X, y_true):
"""Evaluates the given TFLite model and return its accuracy."""
input_index = interpreter.get_input_details()[0]["index"]
output_index = interpreter.get_output_details()[0]["index"]
# Run predictions on all given poses.
y_pred = []
for i in range(len(y_true)):
# Pre-processing: add batch dimension and convert to float32 to match with
# the model's input data format.
test_image = X[i: i + 1].astype('float32')
interpreter.set_tensor(input_index, test_image)
# Run inference.
interpreter.invoke()
# Post-processing: remove batch dimension and find the class with highest
# probability.
output = interpreter.tensor(output_index)
predicted_label = np.argmax(output()[0])
y_pred.append(predicted_label)
# Compare prediction results with ground truth labels to calculate accuracy.
y_pred = keras.utils.to_categorical(y_pred)
return accuracy_score(y_true, y_pred)
# Evaluate the accuracy of the converted TFLite model
classifier_interpreter = tf.lite.Interpreter(model_content=tflite_model)
classifier_interpreter.allocate_tensors()
print('Accuracy of TFLite model: %s' %
evaluate_model(classifier_interpreter, X_test, y_test))
Now you can download the TFLite model (pose_classifier.tflite
) and the label file (pose_labels.txt
) to classify custom poses. See the Android and Python/Raspberry Pi sample app for an end-to-end example of how to use the TFLite pose classification model.
zip pose_classifier.zip pose_labels.txt pose_classifier.tflite
# Download the zip archive if running on Colab.
try:
from google.colab import files
files.download('pose_classifier.zip')
except:
pass