Deep Neural Networks – Convert PyTorch Classification Model and Launch OpenCV v4.8.0 with OpenCV Python

Previous tutorial: How to run a custom OCR model

Next tutorial: Convert PyTorch classification model and launch with OpenCV C++

Original author Anastasia Murzova
Compatibility OpenCV >= 4.5

Goals

In this tutorial you will learn how to

  • Convert PyTorch classification model to ONNX format
  • Run the converted PyTorch model using the OpenCV Python API
  • Evaluate PyTorch and OpenCV DNN models.

We will discuss the above points using the ResNet-50 architecture as an example.

Introduction

Let’s briefly review the key concepts involved in converting PyTorch models to the OpenCV API. The first step in converting a PyTorch model to cv.dnn.Net is to convert the model to ONNX format. ONNX aims to enable the interchange of neural networks between different frameworks. There is a built-in function in PyTorch for ONNX conversion: torch.onnx.export. Additionally, the obtained .onnx model is passed into cv.dnn.readNetFromONNX.

Requirements

To experiment with the code below, you need to install a set of libraries. For this we will use a virtual environment with python3.7+:

virtualenv -p /usr/bin/python3.7 <env_dir_path>
source <env_dir_path>/bin/activate

To build OpenCV-Python from source, follow the instructions in Introduction to OpenCV.

Before you start installing the library, you can customize requirements.txt to exclude or include (for example, opencv-python) certain dependencies. The following line installs the startup requirements into the previously activated virtual environment:

pip install -r requirements.txt

Practice

In this part we will cover the following points:

  1. Create a classification model transformation pipeline and provide inferences
  2. Evaluate and test classification models

If you only want to run an evaluation or test model pipeline, you can skip the “Model Transformation Pipeline” section.

Model conversion pipeline

The code for this subchapter is located in the dnn_model_runner module and can be executed through the following command line:

python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_resnet50

The following code contains instructions for the following steps:

  1. Instantiate a PyTorch model
  2. Convert PyTorch model to .onnx
  3. Reading transmitted network using OpenCV API
  4. Prepare to enter data
  5. provide reasoning
# Initialize PyTorch ResNet-50 model
original_model = models.resnet50(pretrained=True)
# Get the path converted to ONNX PyTorch model
full_model_path = get_pytorch_onnx_model(original_model)
# Use OpenCV API to read the converted .onnx model
opencv_net = cv2.dnn.readNetFromONNX(full_model_path)
print("OpenCV model read successfully. Layer ID: \\
", opencv_net.getLayerNames())
# Get preprocessed image
input_img = get_preprocessed_img("../data/squirrel_cls.jpg")
# Get image network tags
imagenet_labels = get_imagenet_labels("../data/dnn/classification_classes_ILSVRC2012.txt")
# Get OpenCV DNN prediction results
get_opencv_dnn_prediction(opencv_net, input_img, imagenet_labels)
# Get the original PyTorch ResNet50 prediction results
get_pytorch_dnn_prediction(original_model, input_img, imagenet_labels)

To provide model inference, we will use the following photo of a squirrel corresponding to ImageNet class ID 335 (licensed under CC0 license):

Fox squirrel, Eastern fox squirrel, Sciurus niger


Classification model input image

In order to label decode the predictions, we also need the imagenet_classes.txt file, which contains the complete list of ImageNet classes.

Let’s take a pre-trained PyTorch ResNet-50 as an example and dive into each step:

  • Instantiate the PyTorch ResNet-50 model:
# Initialize PyTorch ResNet-50 model
original_model = models.resnet50(pretrained=True)
  • Convert PyTorch model to ONNX:
# Define the saving directory for further converted models
onnx_model_path = "models" # Define names for further converted models
# Define names for further converted models
onnx_model_name = "resnet50.onnx" # Create a directory for further converted models
# Create directories for further converted models
os.makedirs(onnx_model_path, exist_ok=True)
# Get the full path of the converted model
full_model_path = os.path.join(onnx_model_path, onnx_model_name)
# Generate model input
generated_input = Variable(
    torch.randn(1, 3, 224, 224)
)
# Export the model to ONNX format
torch.onnx.export(
    original_model,
    generated_input,
    full_model_path,
    verbose=True,
    input_names=["input"]、
    output_names=["output"]、
    opset_version=11
)

After successfully executing the above code, we will get models/resnet50.onnx.

  • Use cv.dnn.readNetFromONNX to read the transferred network and pass the ONNX model obtained in the previous step into it:
# Read the converted .onnx model using OpenCV API
opencv_net = cv2.dnn.readNetFromONNX(full_model_path)
  • Prepare to enter data:
# Read image
input_img = cv2.imread(img_path, cv2.IMREAD_COLOR)
input_img = input_img.astype(np.float32)
input_img = cv2.resize(input_img, (256, 256))
# Define preprocessing parameters


mean = np.array([0.485, 0.456, 0.406]) * 255.0
scale=1/255.0
std = [0.229, 0.224, 0.225] # Prepare input blob to fit the model input: 0.229, 0.224, 0.225
# Prepare the input blob to fit the model input:
# Subtract the average
# 2. Scale pixel value from 0 to 1
input_blob = cv2.dnn.blobFromImage(
    image=input_img、
    scalefactor=scale、
    size=(224, 224), # Image target size
    mean=mean、
    swapRB=True, # BGR -> RGB
    crop=True # Center crop
)
# 3. Divide by standard
input_blob[0] /= np.asarray(std, dtype=np.float32).reshape(3, 1, 1)

In this step, we read the image and prepare the model input using the cv.dnn.blobFromImage function, which returns a 4-dimensional blob. Note that cv.dnn.blobFromImage first subtracts the average value and only then multiplies the pixel value by the scale. Therefore, the average is multiplied by 255.0 to reproduce the preprocessing sequence of the original image:

img /= 255.0
img -= [0.485, 0.456, 0.406] (0.485, 0.456, 0.406)
img /= [0.229, 0.224, 0.225]
  • OpenCV cv.dnn.Net inference:
# Set up OpenCV DNN input
opencv_net.setInput(preproc_img)
# Set up OpenCV DNN input
out = opencv_net.forward()
print("OpenCV DNN prediction: \\
")
print("* shape: ", out.shape)
# Get the predicted class ID
imagenet_class_id = np.argmax(out)
# Get confidence
confidence = out[0][imagenet_class_id].
print("* class ID: {}, label: {}".format(imagenet_class_id, imagenet_labels[imagenet_class_id]))
print("* confidence: {:.4f}".format(confidence))

After executing the above code, we will get the following output:

OpenCV DNN prediction:
* Shape: (1, 1000)
* Class ID: 335, label: fox squirrel, eastern fox squirrel, Sciurus niger
* Confidence 14.8308
  • PyTorch ResNet-50 model inference:
original_net.eval()
preproc_img = torch.FloatTensor(preproc_img)
# Reasoning
out = original_net(preproc_img)
print("\\
PyTorch model prediction:\\
")
print("* shape: ", out.shape)
# Get the predicted class ID
imagenet_class_id = torch.argmax(out, axis=1).item()
print("* class ID: {}, label: {}".format(imagenet_class_id, imagenet_labels[imagenet_class_id]))
# Get confidence
confidence = out[0][imagenet_class_id].
print("* confidence: {:.4f}".format(confidence.item())

After executing the above code, we will get the following output:

PyTorch model prediction:
* shape: torch.Size([1, 1000])
* Class ID: 335, label: fox squirrel, eastern fox squirrel, Sciurus niger
* Confidence 14.8308

The original ResNet-50 model and cv.dnn.Net have the same inference results. For extended evaluation of models, we can use py_to_py_cls from the dnn_model_runner module. This module part will be introduced in the next chapter.

Model Evaluation

The dnn_model_runner module proposed in samples/dnn allows running the complete evaluation pipeline on the ImageNet dataset and test execution of the following PyTorch classification models:

  • alexnet
  • vgg11
  • vgg13
  • vgg16
  • vgg19
  • resnet18
  • resnet34
  • resnet50
  • resnet101
  • resnet152
  • squeezenet1_0
  • squeezenet1_1
  • resnext50_32x4d
  • resnext101_32x8d
  • wide_resnet50_2
  • wide_resnet101_2

This list can also be extended with further appropriate evaluation pipeline configurations.

Evaluation Mode

The following line indicates running the module in evaluation mode:

python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls--model_name<pytorch_cls_model_name>

The classification model selected from the list will be read into an OpenCV cv.dnn.Net object. The evaluation results (accuracy, inference time, L1) of PyTorch and OpenCV models are written to log files. Inference time values are also displayed graphically to summarize the model information obtained.

The necessary evaluation configuration is defined in test_config.py, which can be modified based on the actual path to the data location:

@dataclass
class TestClsConfig:
    batch_size: int = 50
    frame_size: int = 224
    img_root_dir: str = "./ILSVRC2012_img_val" # Position of image class matching
    # Position of image class matching
    img_cls_file: str = "./val.txt" # Position of image class matching
    bgr_too_rgb: bool = True

To start the evaluation of PyTorch ResNet-50, run the following line:

python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls --model_name resnet50

After the script is started, a log file containing the evaluation data will be generated in dnn_model_runner/dnn_conversion/logs:

The model PyTorch resnet50 has been successfully obtained and converted to OpenCV DNN resnet50
===== The model is being evaluated using the following parameters:
    * Val data location: ./ILSVRC2012_img_val
    * Log file location: dnn_model_runner/dnn_conversion/logs/PyTorch_resnet50_log.txt

Test Mode

The following line represents running the module in test mode, i.e. providing the steps for model inference:

python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls --model_name <pytorch_cls_model_name> --test True --default_img_preprocess <True/False> --evaluate False

The default_img_preprocess keyword here defines whether you want to parameterize the model testing process with some specific values, or use default values such as scale, mean or std.

The test configuration is represented in the test_config.py TestClsModuleConfig class:

@dataclass
class TestClsModuleConfig:
    cls_test_data_dir: str = "../data"
    test_module_name: str = "classification"
    test_module_path: str = "classification.py"
    input_img: str = os.path.join(cls_test_data_dir, "squirrel_cls.jpg")
    model: str = ""
    frame_height: str = str(TestClsConfig.frame_size)
    frame_width: str = str(TestClsConfig.frame_size)
    scale: str = "1.0"
    mean: List[str] = field(default_factory=lambda: ["0.0", "0.0", "0.0"])
    std: List[str] = field(default_factory=list)
    crop: str = "False"
    rgb: str = "True"
    rsz_height: str = ""
    rsz_width: str = ""
    classes: str = os.path.join(cls_test_data_dir, "dnn", "classification_classes_ILSVRC2012.txt")

Default image preprocessing options are defined in default_preprocess_config.py. For example

BASE_IMG_SCALE_FACTOR = 1 / 255.0
PYTORCH_RSZ_HEIGHT = 256
PYTORCH_RSZ_WIDTH = 256
pytorch_resize_input_blob = {<!-- -->
    "mean": ["123.675", "116.28", "103.53"],
    "scale": str(BASE_IMG_SCALE_FACTOR),
    "std": ["0.229", "0.224", "0.225"],
    "crop": "True",
    "rgb": "True",
    "rsz_height": str(PYTORCH_RSZ_HEIGHT),
    "rsz_width": str(PYTORCH_RSZ_WIDTH)
}

The basis for model testing is provided in samples/dnn/classification.py. Classification.py can be executed automatically using the conversion model provided in --input and the fill parameters of cv.dnn.blobFromImage.

To reproduce the OpenCV steps described in “Model Conversion Pipeline” using dnn_model_runner from scratch, execute the following line:

python -m dnn_model_runner.dnn_conversion.pytorch.classification.py_to_py_cls --model_name resnet50 --test True --default_img_preprocess True --evaluate False

The network prediction results are displayed in the upper left corner of the output window:


ResNet50 OpenCV inference output