Annuaire d'articles
aperçu
Ce document décrit principalement python
la plateforme, utilisant opencv-python
le module réseau de neurones profonds dnn
, et la méthode de raisonnement YOLOv5
du modèle.
Le document comprend principalement le contenu suivant :
opencv-python
mise en place des modulesYOLOv5
Description du format du modèleONNX
Chargement du modèle de format- Prétraitement des données d'image
- modèle de raisonnement
- Post-traitement des résultats d'inférence, y compris la conversion
NMS
descxcywh
coordonnées enxyxy
coordonnées, etc. - Appels de méthodes clés et descriptions de paramètres
- exemple de code complet
1. Déploiement de l'environnement
YOLOv5
ONNX
Acquisition du modèle d'algorithme
Le modèle officiel de pré-formation de YOLOv5 peut être téléchargé via le lien officiel. Le format du modèle est .Lien de téléchargementpt
. Le projet officiel fournit un script pour convertir le modèle de format en modèle de format. Lien du projet
YOLOv5
pt
ONNX
Commande d'exportation de modèle :
python export --weights yolov5s.pt --include onnx
Remarque : Pour l'installation et la configuration de l'environnement nécessaire à l'exportation des fichiers pour exécuter les instructions, veuillez vous référer à la
README
documentation officielle du projet, et ne les répéterez pas ici.
opencv-python
mise en place des modules
-
Créer un environnement virtuel et l'activer
conda create -n opencv python=3.8 -y conda activate opencv
-
pip
opencv-python
module d'installationpip install opencv-python
Remarque : lors de
pip
l'installationopencv-python
de modules, l'installation par défaut ne prend en charge queCPU
l'inférence. Si vous avez besoin de prendre en chargeGPU
l'inférence, vous devez compiler et installer à partir du code source. La méthode d'installation spécifique est compliquée et ne sera pas décrite ici.
2. Code clé
2.1 Chargement du modèle
opencv-python
Le module fournit readNetFromONNX
des méthodes pour charger ONNX
des modèles de format.
import cv2
cv2.dnn.readNetFromONNX(model_path)
2.2 Prétraitement des données d'image
Les étapes de prétraitement des données comprennent le redimensionnement, la normalisation, la conversion des canaux de couleur, la conversion des dimensions NCWH, etc.
resize
Auparavant, il existait une astuce très courante pour traiter les images non carrées, c'est-à-dire calculer le côté le plus long du graphique, en fonction de ce côté le plus long, créer un carré, placer le graphique d'origine dans le coin supérieur gauche et remplir le reste avec du noir. L'avantage de faire cela est que le rapport hauteur/largeur des graphiques d'origine ne sera pas modifié et que le contenu des graphiques d'origine ne sera pas modifié en même temps.
# image preprocessing, the trick is to make the frame to be a square but not twist the image
row, col, _ = frame.shape # get the row and column of the origin frame array
_max = max(row, col) # get the max value of row and column
input_image = np.zeros((_max, _max, 3), dtype=np.uint8) # create a new array with the max value
input_image[:row, :col, :] = frame # paste the original frame to make the input_image to be a square
Après avoir terminé le remplissage de l'image, continuez à effectuer des opérations telles que le redimensionnement, la normalisation et la conversion des canaux de couleur.
blob = cv2.dnn.blobFromImage(image, scalefactor=1 / 255.0, size=(640,640), swapRB=True, crop=False)
image
: Entrez les données d'image,numpy.ndarray
le formatshape
est(H,W,C)
, et l'ordre des canaux estBGR
.scalefactor
: Coefficient de normalisation des données d'image, généralement1/255.0
.size
: La taille de redimensionnement de l'image est soumise aux exigences d'entrée du modèle, voici(640,640)
.swapRB
: s'il faut échanger des canaux de couleur, c'est-à-dire convertirBGR
pourRGB
True
indiquer un échangeFalse
et indiquer de ne pas échanger. Étant donné queopencv
l'ordre des canaux de couleur pour la lecture des données d'image estBGR
, etYOLOv5
que l'exigence d'entrée du modèle estRGB
, les canaux de couleur doivent être échangés ici.crop
: Que ce soit pour recadrer l'image,False
signifie pas de recadrage.
blobFromImage
La fonction renvoie un objet Mat à quatre dimensions (ordre des dimensions NCHW) et la forme des données est(1,3,640,640)
2.3 Modèle de raisonnement
-
Définir le backend et la cible d'inférence
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) model.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
Une fois le modèle chargé, vous devez définir le périphérique pour l'inférence. Généralement, le périphérique d'inférence est
CPU
, et la méthode de configuration est la suivante :model.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV) model.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
opencv-python
Bien sûr, si les modules de l'environnement prennent en chargeGPU
le raisonnement à ce moment-là, il peut également être défini surGPU
raisonnement. La méthode de configuration est la suivante :model.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) model.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
Remarque : La méthode pour juger
opencv-python
si le module prend en chargeGPU
le raisonnement est la suivante :cv2.cuda.getCudaEnabledDeviceCount()
, la valeur de retour est supérieure à 0, indiquant queGPU
le raisonnement est pris en charge, sinon, cela signifie qu'il ne prend pas en charge. -
Définir les données d'entrée du modèle
model.setInput(blob)
blob
Prétraitez les données obtenues pour les données de l'étape précédente. -
Appelez la
forward
méthode de propagation vers l'avant du modèleoutputs = model.forward()
outputs
est la sortie de l'inférence du modèle, le format de sortie est (1,25200,5+nc), est le nombre de25200
grilles générées par le modèle, et est la valeur5+nc
prédite pour chaque grille5+nc
,5
et est le nombre de catégories.x,y,w,h,conf
nc
2.4 Post-traitement des résultats d'inférence
Puisqu'il existe un grand nombre de résultats d'inférence qui se chevauchent bbox
, ils doivent être NMS
traités, puis filtrés en fonction de chaque bbox
niveau de confiance et du seuil de confiance défini par l'utilisateur, et enfin la bbox
catégorie et le niveau de confiance correspondants finaux sont obtenus.
2.4.1 SMN
opencv-python
Les modules fournissent NMSBoxes
des méthodes de NMS
traitement.
cv2.dnn.NMSBoxes(bboxes, scores, score_threshold, nms_threshold, eta=None, top_k=None)
bboxes
:bbox
liste, pourshape
,(N,4)
pourN
quantitébbox
,4
pourbbox
.x,y,w,h
scores
:bbox
La liste de confiance correspondante,shape
est(N,1)
,N
estbbox
la quantité.score_threshold
: Le seuil de confiance, qui est inférieur au seuil,bbox
sera filtré.nms_threshold
:NMS
seuil
NMSBoxes
La valeur de retour de la fonction est bbox
une liste d'indices, qui shape
est le nombre.(M,)
M
bbox
2.4.2 filtrage score_threshold
Selon la liste d'index NMS
traités bbox
, filtrez la confiance inférieure score_threshold
à bbox
.
2.4.3 Conversion et restauration des coordonnées bbox
YOLOv5
bbox
Les coordonnées générées par le modèle sont dans cxcywh
un format qui doit être converti en xyxy
un format. De plus, puisque l'image a été manipulée avant resize
, les coordonnées doivent être bbox
restaurées à la taille de l'image d'origine.
La méthode de conversion est la suivante :
# 获取原始图片的尺寸(填充后)
image_width, image_height, _ = input_image.shape
# 计算缩放比
x_factor = image_width / INPUT_WIDTH # 640
y_factor = image_height / INPUT_HEIGHT # 640
# 将cxcywh坐标转换为xyxy坐标
x1 = int((x - w / 2) * x_factor)
y1 = int((y - h / 2) * y_factor)
w = int(w * x_factor)
h = int(h * y_factor)
x2 = x1 + w
y2 = y1 + h
x1
, y1
, x2
, y2
sont les coordonnées bbox
de xyxy
.
3. Exemple de code (exécutable)
Il existe deux codes sources, dont l'un est des fonctions d'épissage et d'appel, ce qui est plus pratique pour le débogage, et l'autre est empaqueté dans des classes, ce qui est pratique pour l'intégration dans d'autres projets.
3.1 Non emballé
"""
running the onnx model inference with opencv dnn module
"""
from typing import List
import cv2
import numpy as np
import time
from pathlib import Path
def build_model(model_path: str) -> cv2.dnn_Net:
"""
build the model with opencv dnn module
Args:
model_path: the path of the model, the model should be in onnx format
Returns:
the model object
"""
# check if the model file exists
if not Path(model_path).exists():
raise FileNotFoundError(f"model file {
model_path} not found")
model = cv2.dnn.readNetFromONNX(model_path)
# check if the opencv-python in your environment supports cuda
cuda_available = cv2.cuda.getCudaEnabledDeviceCount() > 0
if cuda_available: # if cuda is available, use cuda
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
model.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
else: # if cuda is not available, use cpu
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
model.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
return model
def inference(image: np.ndarray, model: cv2.dnn_Net) -> np.ndarray:
"""
inference the model with the input image
Args:
image: the input image in numpy array format, the shape should be (height, width, channel),
the color channels should be in GBR order, like the original opencv image format
model: the model object
Returns:
the output data of the model, the shape should be (1, 25200, nc+5), nc is the number of classes
"""
# image preprocessing, include resize, normalization, channel swap like BGR to RGB, and convert to blob format
# get a 4-dimensional Mat with NCHW dimensions order.
blob = cv2.dnn.blobFromImage(image, 1 / 255.0, (INPUT_WIDTH, INPUT_HEIGHT), swapRB=True, crop=False)
# the alternative way to get the blob
# rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# input_image = cv2.resize(src=rgb, dsize=(INPUT_WIDTH, INPUT_HEIGHT))
# blob_img = np.float32(input_image) / 255.0
# input_x = blob_img.transpose((2, 0, 1))
# blob = np.expand_dims(input_x, 0)
if cv2.cuda.getCudaEnabledDeviceCount() > 0:
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
model.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
else:
model.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
model.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
# set the input data
model.setInput(blob)
start = time.perf_counter()
# inference
outs = model.forward()
end = time.perf_counter()
print("inference time: ", end - start)
# the shape of the output data is (1, 25200, nc+5), nc is the number of classes
return outs
def xywh_to_xyxy(bbox_xywh, image_width, image_height):
"""
Convert bounding box coordinates from (center_x, center_y, width, height) to (x_min, y_min, x_max, y_max) format.
Parameters:
bbox_xywh (list or tuple): Bounding box coordinates in (center_x, center_y, width, height) format.
image_width (int): Width of the image.
image_height (int): Height of the image.
Returns:
tuple: Bounding box coordinates in (x_min, y_min, x_max, y_max) format.
"""
center_x, center_y, width, height = bbox_xywh
x_min = max(0, int(center_x - width / 2))
y_min = max(0, int(center_y - height / 2))
x_max = min(image_width - 1, int(center_x + width / 2))
y_max = min(image_height - 1, int(center_y + height / 2))
return x_min, y_min, x_max, y_max
def wrap_detection(
input_image: np.ndarray,
output_data: np.ndarray,
labels: List[str],
confidence_threshold: float = 0.6
) -> (List[int], List[float], List[List[int]]):
# the shape of the output_data is (25200,5+nc),
# the first 5 elements are [x, y, w, h, confidence], the rest are prediction scores of each class
image_width, image_height, _ = input_image.shape
x_factor = image_width / INPUT_WIDTH
y_factor = image_height / INPUT_HEIGHT
# transform the output_data[:, 0:4] from (x, y, w, h) to (x_min, y_min, x_max, y_max)
indices = cv2.dnn.NMSBoxes(output_data[:, 0:4].tolist(), output_data[:, 4].tolist(), 0.6, 0.4)
raw_boxes = output_data[:, 0:4][indices]
raw_confidences = output_data[:, 4][indices]
raw_class_prediction_probabilities = output_data[:, 5:][indices]
criteria = raw_confidences > confidence_threshold
raw_class_prediction_probabilities = raw_class_prediction_probabilities[criteria]
raw_boxes = raw_boxes[criteria]
raw_confidences = raw_confidences[criteria]
bounding_boxes, confidences, class_ids = [], [], []
for class_prediction_probability, box, confidence in zip(raw_class_prediction_probabilities, raw_boxes,
raw_confidences):
#
# find the least and most probable classes' indices and their probabilities
# min_val, max_val, min_loc, mac_loc = cv2.minMaxLoc(class_prediction_probability)
most_probable_class_index = np.argmax(class_prediction_probability)
label = labels[most_probable_class_index]
confidence = float(confidence)
# bounding_boxes.append(box)
# confidences.append(confidence)
# class_ids.append(most_probable_class_index)
x, y, w, h = box
left = int((x - 0.5 * w) * x_factor)
top = int((y - 0.5 * h) * y_factor)
width = int(w * x_factor)
height = int(h * y_factor)
bounding_box = [left, top, width, height]
bounding_boxes.append(bounding_box)
confidences.append(confidence)
class_ids.append(most_probable_class_index)
return class_ids, confidences, bounding_boxes
coco_class_names = ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
"couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
"remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
"refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
"toothbrush"]
# generate different colors for coco classes
colors = np.random.uniform(0, 255, size=(len(coco_class_names), 3))
INPUT_WIDTH = 640
INPUT_HEIGHT = 640
CONFIDENCE_THRESHOLD = 0.7
NMS_THRESHOLD = 0.45
def video_detector(video_src):
cap = cv2.VideoCapture(video_src)
# 3. inference and show the result in a loop
while cap.isOpened():
success, frame = cap.read()
start = time.perf_counter()
if not success:
break
# image preprocessing, the trick is to make the frame to be a square but not twist the image
row, col, _ = frame.shape # get the row and column of the origin frame array
_max = max(row, col) # get the max value of row and column
input_image = np.zeros((_max, _max, 3), dtype=np.uint8) # create a new array with the max value
input_image[:row, :col, :] = frame # paste the original frame to make the input_image to be a square
# inference
output_data = inference(input_image, net) # the shape of output_data is (1, 25200, 85)
# 4. wrap the detection result
class_ids, confidences, boxes = wrap_detection(input_image, output_data[0], coco_class_names)
# 5. draw the detection result on the frame
for (class_id, confidence, box) in zip(class_ids, confidences, boxes):
color = colors[int(class_id) % len(colors)]
label = coco_class_names[int(class_id)]
xmin, ymin, width, height = box
cv2.rectangle(frame, (xmin, ymin), (xmin + width, ymin + height), color, 2)
# cv2.rectangle(frame, box, color, 2)
# cv2.rectangle(frame, [box[0], box[1], box[2], box[3]], color, thickness=2)
# cv2.rectangle(frame, (box[0], box[1] - 20), (box[0] + 100, box[1]), color, -1)
cv2.putText(frame, str(label), (box[0], box[1] - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
finish = time.perf_counter()
FPS = round(1.0 / (finish - start), 2)
cv2.putText(frame, str(FPS), (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
# 6. show the frame
cv2.imshow("frame", frame)
# 7. press 'q' to exit
if cv2.waitKey(1) == ord('q'):
break
# 8. release the capture and destroy all windows
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
# there are 4 steps to use opencv dnn module to inference onnx model exported by yolov5 and show the result
# 1. load the model
model_path = Path("weights/yolov5s.onnx")
net = build_model(str(model_path))
# 2. load the video capture
# video_source = 0
video_source = 'rtsp://admin:[email protected]:554/h264/ch1/main/av_stream'
video_detector(video_source)
exit(0)
3.2 Encapsulé dans un appel de classe
from typing import List
import onnx
from torchvision import transforms
from torchvision.ops import nms,box_convert
import cv2
import time
import numpy as np
import onnxruntime as ort
import torch
INPUT_WIDTH = 640
INPUT_HEIGHT = 640
def wrap_detection(
input_image: np.ndarray,
output_data: np.ndarray,
labels: List[str],
confidence_threshold: float = 0.6
) -> (List[int], List[float], List[List[int]]):
# the shape of the output_data is (25200,5+nc),
# the first 5 elements are [x, y, w, h, confidence], the rest are prediction scores of each class
image_width, image_height, _ = input_image.shape
x_factor = image_width / INPUT_WIDTH
y_factor = image_height / INPUT_HEIGHT
# transform the output_data[:, 0:4] from (x, y, w, h) to (x_min, y_min, x_max, y_max)
# output_data[:, 0:4] = np.apply_along_axis(xywh_to_xyxy, 1, output_data[:, 0:4], image_width, image_height)
nms_start = time.perf_counter()
indices = cv2.dnn.NMSBoxes(output_data[:, 0:4].tolist(), output_data[:, 4].tolist(), 0.6, 0.4)
nms_finish = time.perf_counter()
print(f"nms time: {
nms_finish - nms_start}")
# print(indices)
raw_boxes = output_data[:, 0:4][indices]
raw_confidences = output_data[:, 4][indices]
raw_class_prediction_probabilities = output_data[:, 5:][indices]
criteria = raw_confidences > confidence_threshold
raw_class_prediction_probabilities = raw_class_prediction_probabilities[criteria]
raw_boxes = raw_boxes[criteria]
raw_confidences = raw_confidences[criteria]
bounding_boxes, confidences, class_ids = [], [], []
for class_prediction_probability, box, confidence in zip(raw_class_prediction_probabilities, raw_boxes,
raw_confidences):
#
# find the least and most probable classes' indices and their probabilities
# min_val, max_val, min_loc, mac_loc = cv2.minMaxLoc(class_prediction_probability)
most_probable_class_index = np.argmax(class_prediction_probability)
label = labels[most_probable_class_index]
confidence = float(confidence)
# bounding_boxes.append(box)
# confidences.append(confidence)
# class_ids.append(most_probable_class_index)
x, y, w, h = box
left = int((x - 0.5 * w) * x_factor)
top = int((y - 0.5 * h) * y_factor)
width = int(w * x_factor)
height = int(h * y_factor)
bounding_box = [left, top, width, height]
bounding_boxes.append(bounding_box)
confidences.append(confidence)
class_ids.append(most_probable_class_index)
return class_ids, confidences, bounding_boxes
coco_class_names = ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
"couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
"remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
"refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
"toothbrush"]
colors = np.random.uniform(0, 255, size=(len(coco_class_names), 3))
if __name__ == '__main__':
# Load the model
model_path = "weights/yolov5s.onnx"
onnx_model = onnx.load(model_path)
onnx.checker.check_model(onnx_model)
session = ort.InferenceSession(model_path, providers=['CUDAExecutionProvider',"CPUExecutionProvider"])
capture = cv2.VideoCapture(0)
trans = transforms.Compose([
transforms.Resize((640, 640)),
transforms.ToTensor()
])
from PIL import Image
while capture.isOpened():
success, frame = capture.read()
start = time.perf_counter()
if not success:
break
rows, cols, channels = frame.shape
# Preprocessing
max_size = max(rows, cols)
input_image = np.zeros((max_size, max_size, 3), dtype=np.uint8)
input_image[:rows, :cols, :] = frame
input_image = cv2.cvtColor(input_image, cv2.COLOR_BGR2RGB)
inputs = trans(Image.fromarray(input_image))
inputs = inputs.unsqueeze(0)
print(inputs.shape)
# inputs.to('cuda')
ort_inputs = {
session.get_inputs()[0].name: inputs.numpy()}
ort_outs = session.run(None, ort_inputs)
out_prob = ort_outs[0][0]
print(out_prob.shape)
scores = out_prob[:, 4] # Confidence scores are in the 5th column (0-indexed)
class_ids = out_prob[:, 5:].argmax(axis=1) # Class labels are from the 6th column onwards
bounding_boxes_xywh = out_prob[:, :4] # Bounding boxes in cxcywh format
# Filter out boxes based on confidence threshold
confidence_threshold = 0.7
mask = scores >= confidence_threshold
class_ids = class_ids[mask]
bounding_boxes_xywh = bounding_boxes_xywh[mask]
scores = scores[mask]
bounding_boxes_xywh = torch.tensor(bounding_boxes_xywh, dtype=torch.float32)
# Convert bounding boxes from xywh to xyxy format
bounding_boxes_xyxy = box_convert(bounding_boxes_xywh, in_fmt='cxcywh', out_fmt='xyxy')
# Perform Non-Maximum Suppression to filter candidate boxes
scores = torch.tensor(scores, dtype=torch.float32)
bounding_boxes_xyxy.to('cuda')
scores.to('cuda')
nms_start = time.perf_counter()
keep_indices = nms(bounding_boxes_xyxy, scores, 0.4)
nms_end = time.perf_counter()
print(f"NMS took {
nms_end - nms_start} seconds")
class_ids = class_ids[keep_indices]
confidences = scores[keep_indices]
bounding_boxes = bounding_boxes_xyxy[keep_indices]
# class_ids, confidences, bounding_boxes = wrap_detection(input_image, out_prob[0], coco_class_names, 0.6)
# break
for i in range(len(keep_indices)):
try:
class_id = class_ids[i]
except IndexError as e:
print(e)
print(class_ids,i, len(keep_indices))
break
confidence = confidences[i]
box = bounding_boxes[i]
color = colors[int(class_id) % len(colors)]
label = coco_class_names[int(class_id)]
# cv2.rectangle(frame, box, color, 2)
print(type(box), box[0], box[1], box[2], box[3], box)
xmin, ymin, xmax, ymax = int(box[0]), int(box[1]), int(box[2]), int(box[3])
cv2.rectangle(frame, (xmin, ymin), (xmax, ymax), color, 2)
# cv2.rectangle(frame, box, color, 2)
# cv2.rectangle(frame, [box[0], box[1], box[2], box[3]], color, thickness=2)
cv2.rectangle(frame, (xmin, ymin - 20), (xmin + 100, ymin), color, -1)
cv2.putText(frame, str(label), (xmin, ymin - 5), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
finish = time.perf_counter()
FPS = round(1.0 / (finish - start), 2)
cv2.putText(frame, f"FPS: {
str(FPS)}", (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
# 6. show the frame
cv2.imshow("frame", frame)
# 7. press 'q' to exit
if cv2.waitKey(1) == ord('q'):
break
# 8. release the capture and destroy all windows
capture.release()
cv2.destroyAllWindows()
exit(0)